Guides

Log Custom Request and Response Data

Applies to
Gatewayruntime for API, AI, and MCP gateways
Hosting
Managed EdgeManaged DedicatedSelf-Hosted

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.

TypeScriptmodules/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:

TypeScriptmodules/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.

TypeScriptmodules/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:

TypeScriptmodules/log-response-headers.ts
import type { ZuploContext, ZuploRequest } from "@zuplo/runtime"; export default async function outboundPolicy( response: Response, 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.

TypeScriptmodules/log-response-body.ts
import type { ZuploContext, ZuploRequest } from "@zuplo/runtime"; export default async function outboundPolicy( response: Response, 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:

TypeScriptmodules/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:

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

For outbound policies:

JSONconfig/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:

JSONconfig/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:

TypeScriptmodules/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:

JSONconfig/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:

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

See Also

Last modified on