ZuploZuplo
LoginSign Up
  • Documentation
  • API Reference
Introduction
Getting Started
    Develop using the Portal
      1 - Setup Your Gateway2 - Rate Limiting3 - API Key Auth4 - Deploy5 - Dynamic Rate LimitingMCP - Quick start
    Develop Locally
      1 - Setup Your Gateway2 - Rate Limiting3 - API Key Auth
Concepts
Development
Policies
Handlers
API Keys
MCP Server
MCP Gateway
AI Gateway
Developer Portal
Monetization
Deploying & Source Control
Observability
    Logging
    Data & Security
    Metrics PluginsOpenTelemetryProactive monitoring
    Guides
      Custom Logging PolicyLog Request/Response DataArchiving Requests
Networking & Infrastructure
Account Management
Programming API
Build with AI
Zuplo CLI
Migration Guides
Platform LimitsSecuritySupportTrust & ComplianceChangelog
powered by Zudoku
Guides

Log Custom Request and Response Data

Logging request and response data is useful for debugging API issues, monitoring traffic patterns, and auditing API usage. This guide shows how to create custom policies that log various parts of requests and responses while redacting sensitive information.

Logging Request Headers

Create an inbound policy to log headers from incoming requests. This policy redacts sensitive headers like Authorization and Cookie to prevent exposing credentials in logs.

modules/log-request-headers.ts
import type { ZuploContext, ZuploRequest } from "@zuplo/runtime"; export default async function policy( request: ZuploRequest, context: ZuploContext, ) { const headers: Record<string, string> = {}; for (const [key, value] of request.headers.entries()) { const k = key.toLowerCase(); headers[key] = k === "authorization" || k === "cookie" || k === "set-cookie" || k === "x-api-key" ? "[REDACTED]" : value; } context.log.info({ headers }, "Incoming request headers"); return request; }

Logging Query Parameters

Log query parameters from the request URL:

modules/log-query-params.ts
import type { ZuploContext, ZuploRequest } from "@zuplo/runtime"; export default async function policy( request: ZuploRequest, context: ZuploContext, ) { const url = new URL(request.url); const queryParams = Object.fromEntries(url.searchParams); context.log.info( { path: url.pathname, query: queryParams, }, "Request query parameters", ); return request; }

Logging Request Body

Log the request body for POST, PUT, or PATCH requests. Clone the request first to avoid consuming the body stream.

modules/log-request-body.ts
import type { ZuploContext, ZuploRequest } from "@zuplo/runtime"; export default async function policy( request: ZuploRequest, context: ZuploContext, ) { if (["POST", "PUT", "PATCH"].includes(request.method)) { const clone = request.clone(); const body = await clone.text(); // Parse JSON if applicable let parsedBody: unknown; try { parsedBody = JSON.parse(body); } catch { parsedBody = body; } context.log.info({ body: parsedBody }, "Request body"); } return request; }

Logging request bodies can expose sensitive data like passwords, tokens, or personal information. Always sanitize or redact sensitive fields before logging.

Logging Response Headers and Status

Create an outbound policy to log response information:

modules/log-response-headers.ts
import type { ZuploContext, ZuploRequest, ZuploResponse } from "@zuplo/runtime"; export default async function outboundPolicy( response: ZuploResponse, request: ZuploRequest, context: ZuploContext, ) { const headers: Record<string, string> = {}; for (const [key, value] of response.headers.entries()) { const k = key.toLowerCase(); headers[key] = k === "set-cookie" ? "[REDACTED]" : value; } context.log.info( { status: response.status, headers }, "Outgoing response headers", ); return response; }

Logging Response Body

Log the response body from your backend. Clone the response first to avoid consuming the body stream.

modules/log-response-body.ts
import type { ZuploContext, ZuploRequest, ZuploResponse } from "@zuplo/runtime"; export default async function outboundPolicy( response: ZuploResponse, request: ZuploRequest, context: ZuploContext, ) { const clone = response.clone(); const body = await clone.text(); let parsedBody: unknown; try { parsedBody = JSON.parse(body); } catch { parsedBody = body; } context.log.info( { status: response.status, body: parsedBody, }, "Response body", ); return response; }

Comprehensive Request Logging

Combine multiple data points into a single log entry:

modules/log-request-details.ts
import type { ZuploContext, ZuploRequest } from "@zuplo/runtime"; export default async function policy( request: ZuploRequest, context: ZuploContext, ) { const url = new URL(request.url); context.log.info( { method: request.method, path: url.pathname, query: Object.fromEntries(url.searchParams), headers: sanitizeHeaders(request.headers), userId: request.user?.sub, params: request.params, }, "Incoming request", ); return request; } function sanitizeHeaders(headers: Headers): Record<string, string> { const sensitiveHeaders = [ "authorization", "cookie", "set-cookie", "x-api-key", ]; const result: Record<string, string> = {}; for (const [key, value] of headers.entries()) { result[key] = sensitiveHeaders.includes(key.toLowerCase()) ? "[REDACTED]" : value; } return result; }

Policy Configuration

Configure the policy in your policies.json:

config/policies.json
{ "name": "log-request-data", "policyType": "custom-code-inbound", "handler": { "export": "default", "module": "$import(./modules/log-request-details)" } }

For outbound policies:

config/policies.json
{ "name": "log-response-data", "policyType": "custom-code-outbound", "handler": { "export": "default", "module": "$import(./modules/log-response-headers)" } }

Wiring Up the Policies

Add the policies to your routes in routes.oas.json:

config/routes.oas.json
{ "paths": { "/my-route": { "get": { "x-zuplo-route": { "handler": { "export": "urlForwardHandler", "module": "$import(@zuplo/runtime)", "options": { "baseUrl": "https://api.example.com" } }, "policies": { "inbound": ["log-request-data"], "outbound": ["log-response-data"] } } } } } }

Configurable Options

Make the logging behavior configurable using policy options:

modules/log-request-configurable.ts
import type { ZuploContext, ZuploRequest } from "@zuplo/runtime"; type PolicyOptions = { logHeaders?: boolean; logQuery?: boolean; logBody?: boolean; redactedHeaders?: string[]; }; const DEFAULT_REDACTED = ["authorization", "cookie", "set-cookie", "x-api-key"]; export default async function policy( request: ZuploRequest, context: ZuploContext, options: PolicyOptions, policyName: string, ) { const url = new URL(request.url); const logData: Record<string, unknown> = { method: request.method, path: url.pathname, }; if (options.logQuery !== false) { logData.query = Object.fromEntries(url.searchParams); } if (options.logHeaders !== false) { const redacted = (options.redactedHeaders ?? DEFAULT_REDACTED).map((h) => h.toLowerCase(), ); logData.headers = sanitizeHeaders(request.headers, redacted); } if (options.logBody && ["POST", "PUT", "PATCH"].includes(request.method)) { const clone = request.clone(); const body = await clone.text(); try { logData.body = JSON.parse(body); } catch { logData.body = body; } } context.log.info(logData, "Incoming request"); return request; } function sanitizeHeaders( headers: Headers, redacted: string[], ): Record<string, string> { const result: Record<string, string> = {}; for (const [key, value] of headers.entries()) { result[key] = redacted.includes(key.toLowerCase()) ? "[REDACTED]" : value; } return result; }

Configure with options:

config/policies.json
{ "name": "log-request-data", "policyType": "custom-code-inbound", "handler": { "export": "default", "module": "$import(./modules/log-request-configurable)", "options": { "logHeaders": true, "logQuery": true, "logBody": false, "redactedHeaders": ["authorization", "cookie", "x-api-key", "x-secret"] } } }

Best Practices

Always redact sensitive data before logging. Credentials, tokens, passwords, and personal information should never appear in logs.

  1. Redact sensitive data - Always redact Authorization, Cookie, Set-Cookie, API keys, passwords, and any fields containing secrets or personal data.

  2. Use structured logging - Pass objects to context.log instead of string concatenation. This enables better log searching and filtering.

  3. Clone before reading - Always clone requests and responses before reading their body to avoid consuming the stream.

  4. Consider log volume - Logging bodies can generate significant log volume and storage costs. Consider enabling body logging only for specific routes or in development environments.

  5. Use appropriate log levels - Use debug for verbose development logging and info for production audit trails.

  6. Limit body size - Consider truncating large bodies to avoid excessive log storage:

    Code
    const body = await clone.text(); const truncated = body.length > 1000 ? body.slice(0, 1000) + "..." : body;

See Also

  • Logger - Logger interface documentation
  • Custom Code Inbound Policy - Writing custom inbound policies
  • Custom Code Outbound Policy - Writing custom outbound policies
  • Custom Logging Policy - Full request/response logging to external services
Edit this page
Last modified on December 3, 2025
Custom Logging PolicyArchiving Requests
On this page
  • Logging Request Headers
  • Logging Query Parameters
  • Logging Request Body
  • Logging Response Headers and Status
  • Logging Response Body
  • Comprehensive Request Logging
  • Policy Configuration
  • Wiring Up the Policies
  • Configurable Options
  • Best Practices
  • See Also
TypeScript
TypeScript
TypeScript
TypeScript
TypeScript
TypeScript
JSON
JSON
JSON
TypeScript
JSON
TypeScript