Просмотр исходного кода

docs: design relay capture json storage

master
fengsilin 2 недель назад
Родитель
Сommit
19d55dc255
1 измененных файлов: 133 добавлений и 0 удалений
  1. +133
    -0
      docs/superpowers/specs/2026-06-23-relay-capture-json-design.md

+ 133
- 0
docs/superpowers/specs/2026-06-23-relay-capture-json-design.md Просмотреть файл

@@ -0,0 +1,133 @@
# 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:

```json
{
"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:

```powershell
go test ./middleware
```


Загрузка…
Отмена
Сохранить