Feature: 021-request-id-logging Date: 2026-01-07 Status: Complete
This document captures design decisions for implementing request ID logging across all clients (CLI, tray, Web UI). The approach uses standard HTTP headers with server-side generation fallback.
Decision: Use X-Request-Id header
Rationale:
- Industry standard header name (used by Heroku, AWS, nginx, etc.)
- Well-documented behavior and expectations
- Existing tooling support (log parsers, proxy passthrough)
X-prefix indicates non-standard but widely adopted
Alternatives Considered:
X-Correlation-Id: Already used for OAuth flows; would cause confusionRequest-Id(no X- prefix): Less widely recognizedTrace-Id: Implies distributed tracing which is out of scope- Custom header: No benefit over standard name
Decision: Use UUID v4 for server-generated IDs; accept alphanumeric with dashes/underscores from clients
Rationale:
- UUID v4 provides sufficient uniqueness without coordination
- Alphanumeric validation prevents injection attacks
- Dashes and underscores allow readable client-provided IDs
- 256 character limit prevents abuse
ID Validation Rules:
Pattern: ^[a-zA-Z0-9_-]{1,256}$
Alternatives Considered:
- UUID only: Too restrictive for clients wanting readable IDs
- Any string: Security risk (injection, memory exhaustion)
- Shorter IDs (ULID, nanoid): UUID is universally understood
Decision: Generate in HTTP middleware (earliest point in request lifecycle)
Rationale:
- Ensures ALL requests have an ID, including errors in routing
- Single point of generation/validation
- ID available for entire request lifecycle
- Consistent behavior across all endpoints
Implementation:
func RequestIDMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
requestID := r.Header.Get("X-Request-Id")
if requestID == "" || !isValidRequestID(requestID) {
requestID = uuid.New().String()
}
ctx := context.WithValue(r.Context(), RequestIDKey, requestID)
w.Header().Set("X-Request-Id", requestID)
next.ServeHTTP(w, r.WithContext(ctx))
})
}Alternatives Considered:
- Generate in handler: Misses early errors
- Generate in client: Requires all clients to implement
- Generate in router: Still misses some early errors
Decision: Always return X-Request-Id in response header (success and error)
Rationale:
- Clients can always correlate request with response
- Works even when response body parsing fails
- Standard behavior matching industry practice
- No conditional logic needed
Header Timing:
- Set header in middleware BEFORE calling next handler
- Ensures header present even if handler panics
Alternatives Considered:
- Only on errors: Inconsistent; clients can't predict when ID available
- Only when client provides: Breaks server-generated ID flow
Decision: Include request_id field in ALL error JSON responses
Rationale:
- Redundant with header but more visible in error messages
- Easier for users to copy from JSON than inspect headers
- Aligns with existing error response patterns
- Enables direct display in UI without header parsing
Error Response Structure:
{
"error": "server_not_found",
"message": "Server 'foo' not found",
"request_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
}Alternatives Considered:
- Header only: Harder for users to find
- Success responses too: Unnecessary noise; header sufficient
Decision: Add request_id field to Zap logger context at middleware level
Rationale:
- Single place to add context field
- All downstream logs automatically include ID
- No changes needed to individual log calls
- Zap's structured logging makes filtering easy
Implementation:
// In middleware
logger := zap.L().With(zap.String("request_id", requestID))
ctx := context.WithValue(ctx, LoggerKey, logger)Log Output:
{"level":"info","msg":"handling request","request_id":"abc123","path":"/api/v1/servers"}Alternatives Considered:
- Manual logging: Error-prone, inconsistent
- Separate log stream: Over-engineered for this use case
- Log correlation service: Adds external dependency
Decision: Print Request ID to stderr on errors with log lookup suggestion
Rationale:
- stderr is appropriate for error information
- Suggestion provides actionable next step
- Does not clutter stdout (data output)
- Only shown on errors to reduce noise
CLI Output Format:
Error: Server 'foo' not found
Request ID: a1b2c3d4-e5f6-7890-abcd-ef1234567890
Run 'mcpproxy logs --request-id a1b2c3d4-e5f6-7890-abcd-ef1234567890' to see detailed logs
Alternatives Considered:
- Always show ID: Too noisy for successful operations
- Show ID without suggestion: Less actionable
- Hide ID entirely: Loses debugging value
Decision: Add --request-id flag to existing logs command; add request_id query param to API
Rationale:
- Extends existing infrastructure (no new commands/endpoints)
- Consistent with existing filtering patterns
- Works for both CLI and API clients
- Activity log infrastructure (spec 016) already supports filtering
CLI Usage:
mcpproxy logs --request-id abc123
mcpproxy logs --request-id abc123 --tail 50API Usage:
GET /api/v1/logs?request_id=abc123Alternatives Considered:
- New endpoint: Unnecessary; filtering is the same pattern
- Grep server-side: Activity log already has structured data
- Client-side filtering: Inefficient for large logs
Decision: OAuth flows include both request_id and correlation_id in logs
Rationale:
request_idtracks the initiating HTTP requestcorrelation_idtracks the entire OAuth flow (multiple callbacks)- Both IDs useful for different debugging scenarios
- No conflict; they serve different purposes
Log Entry Example:
{
"msg": "OAuth callback received",
"request_id": "abc123",
"correlation_id": "def456",
"server": "google-drive"
}Lookup Behavior:
--request-id abc123: Finds the login request logs--correlation-id def456: Finds all OAuth flow logs
Alternatives Considered:
- Single ID: Loses ability to trace specific request vs flow
- Merge IDs: Confusing; they have different scopes
Decision: Error modals/notifications include Request ID with copy affordance
Rationale:
- Users need to copy ID for support/debugging
- Copy button is more user-friendly than selecting text
- Link to logs provides immediate access
- Consistent experience across clients
Web UI Example:
<div class="error-modal">
<h3>Error</h3>
<p>Server 'foo' not found</p>
<div class="request-id">
<span>Request ID: abc123</span>
<button onclick="copyToClipboard('abc123')">Copy</button>
</div>
<a href="/logs?request_id=abc123">View Logs</a>
</div>Tray Notification:
- Shows abbreviated ID in notification body
- "Copy ID" action button
- Click notification opens logs in browser
Alternatives Considered:
- Hide ID from users: Loses debugging value
- Show full ID always: Too long for notifications
| Dependency | Purpose | Version |
|---|---|---|
| google/uuid | UUID generation | Existing |
| uber-go/zap | Structured logging | Existing |
| Chi router | HTTP middleware | Existing |
No new external dependencies required.
router.Use(RequestIDMiddleware) // First - generates ID
router.Use(LoggingMiddleware) // Second - uses ID for logging
router.Use(AuthMiddleware) // Third - may log auth errors with IDtype contextKey string
const (
RequestIDKey contextKey = "request_id"
LoggerKey contextKey = "logger"
)To ensure header is set even on panic:
func (m *requestIDMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request) {
requestID := getOrGenerateRequestID(r)
w.Header().Set("X-Request-Id", requestID) // Set before calling next
// ... rest of middleware
}| Risk | Mitigation |
|---|---|
| Performance impact of UUID generation | UUID v4 is fast (~100ns); negligible |
| Log storage increase | request_id is 36 chars; minimal impact |
| Client forgetting to display ID | Server always includes in header/body |
| ID collision | UUID v4 collision probability is negligible |
- Create data-model.md with RequestContext, ErrorResponse entities
- Update plan.md to reflect implementation approach
- Generate tasks.md via /speckit.tasks