You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

5.8 KiB

Relay Capture JSON Storage Design

Context

RelayCaptureMiddleware currently writes capture files under data/relay-capture/YYYY-MM-DD/<request_id>.log using custom text sections:

  • === REQUEST ... ===
  • request method, path, user ID, headers, and body
  • === RESPONSE ===
  • raw response chunks
  • === END duration_ms=... ===

This format is easy to append to, but hard to parse reliably. It also stores sensitive request headers in plaintext. The new design changes relay capture output to one structured JSON file per captured request while preserving relay behavior.

Goals

  • Store request headers, request body, response headers, and response body as JSON fields.
  • Generate data/relay-capture/YYYY-MM-DD/<request_id>.json.
  • Keep request and response bodies as strings, without JSON parsing.
  • Store response body twice:
    • response.body as the complete concatenated response string.
    • response.body_chunks as the ordered chunks captured from ResponseWriter.Write and WriteString.
  • Store headers as map[string][]string, matching http.Header semantics.
  • Redact sensitive request headers in capture output.
  • Ensure capture failures never change the client-visible relay response.
  • Use common.Marshal for JSON encoding.

Non-Goals

  • Do not keep writing the legacy .log format.
  • Do not parse request or response bodies into JSON objects.
  • Do not redact request or response bodies in this change.
  • Do not capture upstream provider request or response payloads after adapter conversion. This middleware captures the client-facing relay request and response at the Gin layer.
  • Do not introduce database storage for capture records.

JSON Shape

Each capture file contains one complete JSON object:

{
  "request_id": "test-non-stream-120000",
  "captured_at": "2026-06-23T12:00:00.000000000Z",
  "duration_ms": 123,
  "user_id": 42,
  "request": {
    "method": "POST",
    "path": "/v1/chat/completions",
    "query": "stream=true",
    "headers": {
      "Authorization": ["[REDACTED]"],
      "Content-Type": ["application/json"]
    },
    "body": "{\"model\":\"gpt-4\"}"
  },
  "response": {
    "status_code": 200,
    "headers": {
      "Content-Type": ["application/json; charset=utf-8"]
    },
    "body": "{\"id\":\"chatcmpl-test\"}",
    "body_chunks": ["{\"id\":\"chatcmpl-test\"}"]
  },
  "capture_errors": []
}

capture_errors is present to preserve non-fatal capture problems without failing the relay request. It should be an empty array when capture succeeds.

Middleware Flow

  1. Check capture_relay cache with the authenticated user ID. Disabled users continue through the middleware unchanged.
  2. Record start, captured_at, request_id, user ID, method, path, query, and a redacted copy of request headers.
  3. Read the request body through common.GetBodyStorage(c), not directly from c.Request.Body, so downstream handlers can continue using the reusable body storage.
  4. Replace c.Writer with a capture response writer that delegates writes to the original writer and copies only the successfully written bytes into an in-memory collector.
  5. Run c.Next().
  6. Build response metadata from the final status code, final response headers, collected body, and ordered body_chunks.
  7. Marshal the full record with common.Marshal.
  8. Write the JSON file under data/relay-capture/YYYY-MM-DD/<request_id>.json.

If request_id is empty, generate a fallback value like capture-<unix_nano> to avoid invalid or colliding file names in tests and edge paths.

Header Redaction

Request header redaction is case-insensitive and only affects the capture record. It must not mutate c.Request.Header.

Initial sensitive request headers:

  • Authorization
  • Proxy-Authorization
  • X-Api-Key
  • Api-Key
  • Cookie

Each value for a sensitive header is replaced with [REDACTED]. Response headers are copied as-is in this change.

Error Handling

Capture is diagnostic. It must not block or alter relay behavior.

  • If request body capture fails, continue the request and append a message to capture_errors.
  • If response writes fail, return the underlying ResponseWriter result exactly as today. The collector only records bytes that were successfully written.
  • If JSON marshaling fails, log with common.SysError and do not write a partial capture file.
  • If directory creation or file writing fails, log with common.SysError and leave the relay response untouched.
  • The existing writer goroutine and === END text trailer are removed because JSON is written once after c.Next().

Testing

Update the existing relay capture tests around middleware/relay_capture.go.

Unit coverage:

  • Header copying preserves map[string][]string and redacts sensitive request headers, including case variants.
  • Capture file creation uses .json and the existing date directory layout.
  • The response capture writer preserves client-visible output while collecting body and body_chunks.
  • Request body capture stores POST JSON as a string and GET/no-body requests as an empty string.

Integration coverage:

  • Enabled non-streaming users produce a valid JSON file with request metadata, redacted request headers, response headers, status code, body, duration_ms, user_id, and request_id.
  • Enabled streaming users produce response.body containing the full SSE response and response.body_chunks preserving chunk order, including [DONE].
  • Disabled users do not produce capture files.
  • Capturing does not change the client-visible response body.
  • Concurrent requests produce one valid JSON file per request.
  • Sensitive header values such as Bearer sk-test are absent from capture files and replaced with [REDACTED].
  • Empty request IDs use the fallback capture-*.json file name.

Primary verification command:

go test ./middleware