Overview
SBproxy emits structured JSON logs, one object per line, UTF-8. Every request-scoped line carries request_id, trace_id, span_id, tenant_id, and route, so logs correlate with metrics and traces (see Monitoring & Observability). Access lines split latency into pipeline phases (auth_ms, upstream_ttfb_ms, response_filter_ms) alongside the standard HTTP fields.
Multi-sink dispatch
Each emitted record fans out to one or more declared sinks. A sink chooses its channel, destination, wire format, and redaction profile. With no sinks declared, logs go to stdout as before.
proxy:
observability:
log:
sinks:
- name: stdout
target: access_log # access_log | error_log | audit_log | trace_exporter | external_log
format: json
output: { type: stdout }
profile: internal # internal keeps fingerprints and raw query; external strips them
- name: file-archive
target: audit_log
format: json
output:
type: file
path: /var/log/sbproxy/audit.json
max_size_mb: 100
max_backups: 7
compress: true
- name: otel-collector
target: access_log
format: json
output:
type: otlp
endpoint: http://otel-collector:4318/v1/logs
transport: http
Sinks can also be declared per tenant (tenants[].observability.log.sinks) and per origin (origins[].observability.log.sinks). A tenant-scoped sink only ever sees records whose resolved tenant matches, so one tenant's logs can ship to its own Loki instance and never cross into another's.
Redaction
Sensitive values are matched by field key, not by guessing at content. A built-in denylist runs first and cannot be disabled: authorization, cookie, set-cookie, *_secret, *_token, *_key, prompt, messages, ja3, ja4, and more. Each match becomes a [REDACTED:<NAME>] marker. Operators layer their own field keys and regex masks on top:
proxy:
observability:
log:
redact:
fields:
- x-internal-token
- internal_account_id
patterns:
- name: customer_uuid
pattern: 'cust_[a-z0-9]{20}'
replacement: '[REDACTED:CUSTOMER_UUID]'
Built-in PII detector
An optional fourth pass runs the rule-driven PII detector against the rendered JSON. It ships rules for email, US SSN, credit card (Luhn-validated), US phone, IPv4, IBAN, and common API-key shapes (OpenAI, Anthropic, AWS, GitHub, Slack). Enable all defaults or select a subset, and override per tenant or origin for stricter regulated workloads:
proxy:
observability:
log:
redact:
pii:
enabled: true
rules: [email, us_ssn, credit_card]
disable: [ipv4]
Reversible masking for AI origins
A customer copilot needs the model to answer with the same name or order number the user typed, but you do not want that value written to any log. Reversible masking replaces the value with a placeholder before the prompt is forwarded, lets the model echo the placeholder, and restores the original on the response before it reaches the client. The original lives in memory for the request lifetime only and is never written to the access log, audit log, or trace span. It works on both buffered and SSE-streamed responses.
origins:
- name: customer-copilot
action: ai_proxy
pii:
enabled: true
rules:
- name: email
pattern: '\b[a-z0-9._%+-]{1,64}@[a-z0-9.-]{1,255}\.[a-z]{2,63}\b'
reversible: true
mask_template: "<placeholder:email:%d>"
Reversible masking and the semantic cache cannot share an origin: the cache keys on prompt shape, so two requests with different captured values could collide. When both are configured on one AI origin, the semantic cache is dropped from the compiled config and a warning is logged.