Troubleshooting
July 26, 2026 ยท View on GitHub
Last modified: 2026-07-22
When something breaks, this is the first place to look. Each section is one failure: the symptom, the likely cause, and the fix. For why these things happen, see architecture.md; for what the proxy does on its own while a dependency is down, see degradation.md; for the dashboard-to-action triage flow, see operator-runbook.md.
Jump by symptom:
A config setting seems to be ignored
You set a config key and nothing changes.
Where the key sits decides what happens:
- A misspelled or misplaced key nested inside
proxy:, an origin, or a security block is a hard failure: boot andsbproxy validateboth reject the config and print the full key path, for exampleunknown or misspelled config key(s): origins.api.example.com.forward_rules.0.rules.0.user_agent. If the proxy is running at all, no nested key in the loaded file is being silently ignored. - A misspelled top-level key (a sibling of
proxy:andorigins:) is dropped with a boot-time warning, and everything under it takes defaults. This is the usual way a whole feature block "does nothing":key_management:at the top level instead of nested underproxy:leaves the feature off.
Check:
- Run
sbproxy validate sb.yml. Nested unknown keys fail the run; dropped top-level keys print the sameignored unknown/misspelled top-level key(s)warning the server logs. - Grep the boot log for
ignored unknown/misspelled top-level key(s). - Compare the key against
schemas/sb-config.schema.json, the generated source of truth for every valid key and its nesting. - Out-of-tree blocks belong under
proxy.extensions:(or an origin'sextensions:); that is the one place unrecognized keys are expected and passed through.
404, origin not found
The Host header on the request does not match any configured origin.
Check:
- Run
sbproxy validate --config sb.ymlto confirm the config parses. - Confirm the request's
Hostheader matches the origin name exactly, including any port suffix. - SBproxy uses a bloom filter for fast hostname lookup. If you just added an origin via hot reload, wait a second and retry.
- These 404s land in
sbproxy_requests_totalunder the client-supplied hostname (the cardinality limiter collapses excess values into__other__) and in the access log witherror_class: "not_found", so a flood of them is visible: it is usually a DNS record pointing at the proxy for a hostname you never configured, or scanning traffic.
Clients get 502 Bad Gateway
The upstream behind that origin is unreachable: connect refused, DNS failure, timeout, or the retry chain exhausted itself against 5xx responses.
Check:
- Look at the access-log line for the failed request. A missing
upstream_ttfb_msfield means the proxy never got a first byte from the upstream, so the failure was at connect time, not a slow backend. A presentupstream_statusshows what the upstream actually said before a retry or fallback rewrote it. - Curl the upstream directly from the proxy host. If that fails too, the backend or the network path is down and the proxy is reporting it accurately.
- Confirm
sbproxy_requests_total{hostname="...",status="502"}is rising for one origin only. If every origin is failing at once, suspect DNS or an egress network change instead of one dead backend. - If the origin is a
load_balancer, check which targets are ejected viaGET /api/health/targetson the admin server. Active health checks, outlier detection, and the circuit breaker each eject targets independently; with every target ejected the LB falls back to the unfiltered list rather than failing the client. - Give the origin
retry(connect errors and 502/503 are retryable), and consider afallback_originblock so callers get a degraded response instead of the 502 while the upstream heals. See degradation.md andexamples/fallback-origin/.
A circuit breaker keeps opening
Errors arrive in bursts separated by quiet periods: the breaker trips on consecutive failures, holds requests off the target for open_duration_secs, lets a few probes through in HalfOpen, then trips again because the target is still bad.
Check:
sbproxy_circuit_breaker_transitions_total{origin,from_state,to_state}tells you how often and in which direction the breaker is moving. A steadyopen -> half_open -> opencycle means the upstream never actually recovered.- Fix the upstream, or if a recent origin config change caused the failures, roll it back and watch the transitions stop.
- Do not just raise
failure_thresholdto quiet the breaker; that trades fast isolation for more client-visible failures. Tuneopen_duration_secsandsuccess_thresholdif the defaults recover too slowly for your workload.
Hot reload did not pick up changes
Usually one of: file watcher debounce, ConfigMap symlink swap, or a validation failure.
Check:
- A config with a validation error gets logged and rejected. The old config keeps running. Run
sbproxy validate --config sb.ymlto see the error. - The file watcher reacts to in-place writes. Saves that replace the file by atomic rename (many editors,
sed -i, and Kubernetes ConfigMap symlink swaps) may not be detected. After a ConfigMap update, sendSIGHUPor restart the pod to force the reload. - The
agent_classes,agent_detect, andtls_fingerprintinstallers are applied at startup and re-applied on every hot reload; each swaps its live state atomically, so changes to those blocks take effect without a restart. - Watch
sbproxy_config_reload_total{result}: a risingfailurecount or a stalledsuccesscadence is the reload path telling you it is stuck.
Rolling back a bad config change
Traffic degraded right after a config rollout and you need the old behavior back now.
Check:
- Revert
sb.ymlto the last-good revision, then sendSIGHUPorPOST /admin/reload. Validation runs first and a config that fails validation is rejected while the old pipeline keeps serving, so a rushed rollback cannot take the proxy down. - Before re-applying,
sbproxy plan -f sb.yml --against last-good.ymlprints the added, changed, and removed origins with a max-blast-radius line. Exit code 0 means no-op, 2 means changes present, 3 means semantic errors. Wire it into CI so oversized diffs stop before they ship. - One thing hot reload deliberately does not reset: the AI budget accumulators. Budget windows are wall-clock-relative, so a reload (or rollback) does not zero already-spent budget. There is no admin endpoint for resetting a budget; to zero one intentionally, restart the process.
- On Kubernetes,
helm history sbproxyandhelm rollbackwalk the same ladder at the deployment layer; operator-runbook.md has the exact commands.
AI requests fail with provider error
Check in order:
- Confirm the provider API key is set correctly. Check the
api_keyfield or the environment variable it references. - Run
sbproxy validate --config sb.ymlto confirm the provider block parses correctly. - Check the structured log for
providerandstatus_codefields on the failed request. - If using a fallback chain, check that at least one provider in the chain has available capacity. The log will show which provider was attempted last.
- If the error is "context window exceeded," the requested model does not support the token count in the prompt. Add a model with a larger context window to the provider list.
sbproxy_ai_provider_errors_total{provider,error_kind}splits the failures into transport, timeout, 4xx, 5xx, and parse classes, andsbproxy_ai_failovers_totalshows whether the routing chain is absorbing them.
Rate limiter rejecting requests unexpectedly
Check:
- The
requests_per_secondlimit is per-origin, not global. If you have multiple origins sharing an upstream, each origin has its own counter. - The default token bucket allows short bursts up to
burstsize. A sustained rate aboverequests_per_secondwill be rejected once the bucket drains. - If you are testing with many rapid requests, increase
burstto permit the test pattern. - Check the structured log for
policyandlimitfields to see which rule triggered.
Requests are slow
SBproxy adds well under 1 ms of overhead under normal load. If you see more, the cause is almost always upstream or DNS.
- Check
upstream_ttfb_msin the structured log. If it's high, the upstream is slow, not SBproxy. - If
upstream_ttfb_msis low but total latency is high, suspect DNS. Resolved addresses are cached and refreshed in the background by a refreshing resolver, so a request that lands right after a hostname goes stale pays the resolver round trip. - Turn on OpenTelemetry tracing (
telemetryblock) to get a per-span breakdown across the phase pipeline. - If you have Lua configured, cap runaway scripts with
proxy.scripting.lua.sandbox.max_execution_ms. JavaScript uses its built-in 100 ms budget; the parsedproxy.scripting.javascripttuning block is not installed yet. - The
sbproxy_phase_duration_seconds{phase}histogram (and the matchingauth_ms/upstream_ttfb_ms/response_filter_msaccess-log fields) splits end-to-end latency into auth, upstream wait, and response transforms, so you can see which phase grew without tracing.
No access-log lines appear
The access log is off by default. No access_log block, no lines; metrics and traces are unaffected.
Check:
- The config has
access_log.enabled: trueat the top level. See access-log.md. sample_rate: 0.0disables emission entirely, and low sample rates drop most lines by design. Setalways_log_errors: trueandslow_request_threshold_msso error and slow-request lines bypass the sampler.- The lines are emitted at info level through the
access_logtracing target. A log filter likeRUST_LOG=warnsilences them; useRUST_LOG=warn,access_log=info(or the--request-log-levelflag) to keep operator logs quiet while keeping access logs. status_codesandmethodsfilters narrow what gets logged; an empty list matches everything, but a list that omits your test request's method logs nothing for it.- If you configured
output.type: file, the lines go to that path, not stdout. Check the file and its rotation suffixes.
The metrics endpoint returns an empty body
Two harmless-looking behaviors cause most confusion here.
Check:
/metricsis served on the data-plane port (http_bind_port, default 8080), not a separate telemetry listener. The admin server exposes a second copy on its own port when enabled.- Scrapes are rate-limited to one per second; back-to-back requests get an empty body. A curl right after your Prometheus scrape hits this. Wait a second and retry. Scrape intervals of 15s never notice.
- Metrics are per-instance. In a cluster, each process reports only its own counters; aggregate across instances in Prometheus (the bundled dashboards already sum with PromQL).
Traces never arrive at the collector
Tracing is off by default; the exporter only starts when the telemetry block enables it.
Check:
proxy.observability.telemetry.enabled: trueis set and the process was restarted or reloaded after adding it.- Transport and port agree:
transport: grpcpairs with collector port 4317,transport: httpwith 4318. A gRPC exporter pointed at an HTTP receiver fails quietly. - Sampling: with
sample_rate: 0.1, ninety percent of healthy-traffic traces are dropped on purpose. Setalways_sample_errors: trueso error traces always export, and send a 5xx through the proxy as a test. - The telemetry block does not expose per-exporter auth headers. For API-key backends (Datadog, Honeycomb, Langfuse Cloud), put an OpenTelemetry Collector in the middle and let it attach credentials. observability.md has the verified backend matrix and Collector snippets.
- The reference Compose stack in
examples/observability-stack/receives OTLP on host port 4327 (not 4317, which Tempo owns there).
The admin server is unreachable or rejects you
The admin server is off by default, binds loopback only, and authenticates everything except the health probes.
Check:
proxy.admin.enabled: trueis set. Without it there is no admin listener at all.- From another machine you need
bindset to a reachable address and your caller's IP inallow_ips. An emptyallow_ipskeeps the loopback-only default even withbind: 0.0.0.0. - A 401 means missing or wrong credentials: HTTP Basic with the configured
username/password, or a browser session fromPOST /admin/login. A 403 on a mutation means your operator has theread_onlyrole. - With
tlsconfigured, plaintext requests to the port fail. Usehttps://(and-kfor a self-signed cert). - The probe routes (
/healthz,/health,/readyz,/livez) are intentionally unauthenticated; if those work but/api/requestsgets 401, connectivity is fine and the failure is credentials. - The web UI at
/admin/uionly exists in binaries built with theembed-admin-uifeature. The API works either way. See admin.md.
Grafana dashboards show no data
The bundled dashboards under dashboards/grafana/ render nothing when the datasource reference or the scrape is broken.
Check:
- Prometheus's targets page first. If the
sbproxyjob is down, fix the scrape: the target should be the data-plane port (default 8080) or the admin port, path/metrics. - The dashboard JSON references the datasource as
${DS_PROMETHEUS}. Importing through the Grafana UI resolves it with a prompt; file-based provisioning does not, so replace the placeholder with your datasource UID (the compose file inexamples/use-case-production-ops/shows a one-lineseddoing exactly this). Seedashboards/README.md. - Send some traffic. Counters that have never incremented emit no series, and a fresh proxy with zero requests renders empty panels that look broken but are not.
- Alert panels need the recording rules:
dashboards/prometheus/alerts.ymlreferences series computed bydashboards/prometheus/recording-rules.yml, so load both files.
Redis shared state is degraded
With proxy.l2_cache_settings on Redis, a runtime connection failure does not
stop general traffic. A response-cache lookup failure bypasses the cache and
does not write that request's upstream response to an outage cache. A shared
rate-limit operation failure admits the request fail-open instead of switching
to a local bucket. summary_buffer also fails open for the primary AI request,
but it does not create worker-local summary state. Other L2 consumers retain
their feature-specific failure posture.
Check:
- Run
sbproxy validate --config sb.ymlwith the required secret environment already set. Do not print the expanded DSN. Validation catches malformed or unsupported schemes, query strings, fragments, bad database syntax, missing certificate/key partners, unreadable PEM files, and a mismatched client key. - Remember that validation does not contact Redis. The first L2 operation
performs TLS,
AUTH, andSELECT, so trust, credential, server-side database, and reachability failures appear only when traffic uses shared state. - Check that the SBproxy process can read
ca_file,cert_file, andkey_file.openssl x509 -in <file> -noout -subject -issuersafely checks certificate parsing. Letsbproxy validatecheck that the client certificate and key match, rather than dumping either file. - Test Redis without putting the password or DSN on the command line. Set
REDISCLI_AUTHin the command environment and pass the host, port, CA, client certificate, client key, username, and database as separateredis-clioptions. RunPINGorDBSIZE; do not runKEYS,SCAN, orGETduring a privacy-sensitive incident. - Expected during an outage: failed response-cache lookups fetch from the upstream and do not arm cache write-back for that request. Failed shared rate-limit increments admit the request without consulting a local bucket. See degradation.md.
- Reconnection is automatic. Broken connections leave the pool, and a later operation opens a new connection. Fix Redis or the trust/authentication configuration, then send a new cache miss or shared-state operation. SBproxy does not need a restart when the configured connection material is unchanged.
The runtime error reason points at the next check without exposing the Redis response:
| Reason | Check |
|---|---|
pool_timeout | Pool saturation or an operation holding a slot too long |
connect_timeout | Reachability and time spent in TCP, TLS, AUTH, or SELECT setup |
command_timeout | Redis command latency and server load |
tls | CA trust, server name, client certificate, and client key |
auth | ACL username and password; percent-encoding of reserved URL characters |
transport | Listener, network, reset, or dropped connection |
server | Redis application errors, including a server-side SELECT rejection |
protocol | Invalid or unexpected Redis protocol data |
The platform health events named redis store health failed,
redis store health remains failed, and redis store health recovered are
transition-based. The first failure from an unknown or healthy state is
WARN, repeated platform health failures are DEBUG, and the first successful
operation after failure is INFO. Safe platform events look like this:
WARN operation="get" reason="tls" redis store health failed
INFO operation="get" redis store health recovered
That WARN to DEBUG to INFO sequence applies only to the platform health
events. Response-cache lookup, write, and invalidation call sites and the shared
rate-limit increment call site can emit a separate WARN for every failed
operation. Repeated consumer warnings do not mean that the platform transition
suppression failed.
The three Redis L2 metric families use only closed labels:
curl -fsS http://127.0.0.1:8080/metrics |
grep -E '^(# (HELP|TYPE) sbproxy_redis_kv_|sbproxy_redis_kv_)'
sbproxy_redis_kv_connections_total{result}usessuccessorerror.sbproxy_redis_kv_operation_duration_seconds{operation}usesget,set,set_ttl,delete,increment,lock,unlock, orscan.sbproxy_redis_kv_operation_errors_total{operation,reason}uses the operation values above andpool_timeout,connect_timeout,command_timeout,tls,auth,transport,server, orprotocol.
No Redis metric or platform transition log includes the endpoint, tenant, application key, username, database, credential, certificate path, or free-form server error. Consumer warnings have their own fixed message text and are not governed by the platform transition cadence. Correlate them with the closed-label metrics; do not paste a DSN, expanded configuration, credential, cache key, or cache value into a diagnostic command or incident ticket. Alert on connection errors or operation errors when running more than one replica because the application can continue while shared-state guarantees are degraded.
TLS handshake fails
Check:
- For ACME auto-cert, confirm
acme.emailis set and the DNS A/AAAA record points at this server. Let's Encrypt needs a successful HTTP-01 or TLS-ALPN-01 challenge. - For BYO certificates, check that the cert and key paths are readable by the SBproxy process and the cert chain matches the leaf.
- Run
openssl s_client -servername <host> -connect <host>:443to see the server's offered chain. - The TLS layer uses
rustlswith theringcrypto provider. TLS 1.3 by default with TLS 1.2 fallback.
ACME renewal is failing
Renewal failures are quiet at first because the existing certificate keeps serving until expiry.
Check:
- The log: every failed issuance or renewal logs an error naming the hostname (
ACME issuance failed, directory fetch and account-key errors likewise).sbproxy_acme_renewals_total{result}counts attempts by outcome. - The renewal loop re-checks every hostname on a 12 hour cadence (with an immediate first pass at startup), so a transient CA outage heals on a later pass. A renewal that has been failing for days is usually a changed DNS record, a firewall now blocking the challenge port, or an account/rate-limit problem at the CA.
sbproxy_cert_expiry_seconds{host}gauges seconds until each served certificate expires; graph it or alert on it dropping below your comfort window. No bundled alert rule covers cert expiry, so add one to your own rules if you rely on ACME.- If a listener has no usable cert at all (fresh boot, ACME never succeeded), the proxy serves a self-signed bootstrap cert and logs loudly rather than refusing to start. Clients will see trust errors until the first real issuance lands; that is the visible symptom to chase.
HTTP/3 requests fall back to HTTP/2
Cause: HTTP/3 is not served by this build. The proxy does not start a QUIC listener and does not advertise Alt-Svc, so HTTP/2 is the highest version served. Clients that try HTTP/3 fall back to HTTP/2, which is expected.
Check:
- Config compilation rejects
proxy.http3.enabled: trueand references WOR-1969. Remove the block or setenabled: false. - If you need a UDP/QUIC path today, terminate HTTP/3 at an upstream edge or CDN and forward HTTP/2 to SBproxy.
A local model will not serve
A managed deployment commits only after its catalog, artifact, engine, and capacity checks pass. A failed reload preserves the last good runtime.
Check:
- Run
sbproxy doctor --format json. It reports visible devices, engines, container runtime, cache, and availability asavailable,acquirable,incompatible, orblocked. - Query
sbproxy models ps --format jsonwith admin credentials. Check the deployment's state,reason_code, boundedlast_error, engine availability, artifact digest, selected device, and memory breakdown. insufficient_capacitymeans the selected device cannot hold weights, KV, runtime overhead, and safety margin together. Use a smaller variant, reduce context or concurrency, chooserollout: recreate, or free device memory.queue_fullandqueue_timeoutare load signals. Adjust queue depth or timeout, reduce callers, or configure a fallback provider.engine_unhealthyandcrash_loopretain the engine failure. Correct the version, runtime, artifact, or driver mismatch, then callPOST /admin/model-host/resetbefore another load.drainingmeans stop or replacement is still waiting on active stream permits. Check active and queued counts before forcing a process restart.- Pull policy is per deployment.
manualneedssbproxy models pull -f sb.yml;on_demandmay make the first request wait for a large verified download;on_bootmoves that work into candidate preparation. sbproxy_model_host_deployment_state, active and queued gauges, admission rejection counters, and weight-download timing separate lifecycle, load, and artifact failures.
Provider-level serve: blocks use the same runtime through compatibility
lowering. New configurations should use proxy.model_host. Live NVIDIA
remediation is verified in the final GCP integration PR; see
model-host-certification.md before treating a
simulated device test as hardware evidence.
The model cluster does not converge
Start with the authenticated cluster view, not a single worker log:
sbproxy cluster status --format json \
| jq '{summary,nodes,unhealthy_nodes,deployments,deployment_authority}'
Check in this order:
- If the process fails at startup, verify unique local gossip and transport
ports, reachable advertised addresses, and matching
cluster_id, seeds, CA, server name, gossip key, and enrolled files understate_dir. The signed identity must match the configured node ID, roles, labels, server name, and certificate fingerprint. Canonical cluster bind and security failures are fatal. A legacy key-cache mesh may fall back locally, butproxy.clusternever does. - If key-cache mesh fields are still present, they must exactly match
proxy.clusternode ID, seeds, listeners, advertised addresses, shared key, and mTLS fields. Mismatch is rejected so one process cannot split into two clusters. - Inspect
unhealthy_nodes[].reasonsand the matching fullnodesrow.membership_suspect,membership_dead,snapshot_missing,snapshot_expired,snapshot_unreachable, incompatible schema, reported health, missing model endpoint, engine incompatibility, and zero capacity all make a worker ineligible without removing it from the roster. - Compare
directory_age_ms,snapshot_age_ms, andlast_ack_age_mswithpublish_interval_secsandsnapshot_ttl_secs. The snapshot lifetime must cover at least two publish intervals. Persistent age growth usually means transport reachability or the worker maintenance thread is failing. - A dead node remains in the admin roster after
dead_peer_gc_secsremoves it from routing membership. That retained tombstone is expected; restart the same enrolled identity or remove it through a future explicit roster action. - When
deployment_digest_mismatchis true in file-managed mode, compare the normalizedproxy.model_hostdeployment revision on every node. The cluster reports drift; it never overwrites a local file. - For unplaced replicas, inspect each deployment's
rejections. Fix missing required labels, a missing advertisedmodel_endpoint, an unhealthy node, incompatible variants or engines, insufficient capacity, or manual-pull cache misses. Placement requires a worker role and advertised endpoint; it does not probemodel_bindor require model-plane health to be ready. A replicated homogeneous deployment must pin a variant unlessheterogeneous_variants: trueis explicit. - During rolling replacement,
retainedis expected until all targets report exact generation, variant, artifact digest, and ready state.timed_out: truemeanshandoff_timeout_mselapsed; inspect the target workers before retrying. - If a generation changes unexpectedly after restart, inspect
state_dir/model-deployment-generations.jsonand directory permissions. Do not delete or copy this file between nodes; it is the local controller's monotonic high-water record. - In cluster-authority mode, verify
deployment_authority.configured, key ID, active revision, signer node, and cursor state.invalid_bundle,stale_revision,revision_conflict, anddeployment_authority_read_onlyare stable admin error codes. Never copy the private signing key to a worker to bypass a read-only error.
A partition may temporarily overprovision because each side places only on its reachable directory. That is an availability tradeoff. It must not produce a cross-partition eligible route. Peer dispatch uses only the current eligible directory and exact deployment generation.
Managed model dispatch fails
First distinguish logical discovery, placement, the private model plane, and the worker engine:
curl --include "${SB_PUBLIC_URL}/v1/models" -H "host: ${SB_PUBLIC_HOST}"
curl --include "${SB_PUBLIC_URL}/v1/chat/completions" \
-H "host: ${SB_PUBLIC_HOST}" \
-H 'content-type: application/json' \
-d '{"model":"qwen","messages":[{"role":"user","content":"health check"}]}'
sbproxy cluster status --format json \
| jq '{summary,unhealthy_nodes,deployments,nodes:[.nodes[] | {node_id,health,model_eligible,model_plane:.reported_health.model_plane,exclusion_reason}]}'
sbproxy models ps --format json
Check these signals:
- If
/v1/modelsomits the logical model, verify the origin'smanaged_modelprovider, deployment ID, provider allowlist, and model allowlist. An entry withavailability.state: unavailablemeans discovery succeeded but no current ready or cold replica is eligible. - A successful response should carry
x-sbproxy-logical-modelandx-sbproxy-route-class.peerproves private dispatch without exposing a worker ID or endpoint. Missing headers usually means a non-managed route was selected or the request failed before selection. 503pluscode: no_ready_replicaandRetry-After: 1is the explicitcold_start: rejectcontract. Usewait, warm the deployment, or add a fallback provider if rejection is not desired. Withcold_start: fallback, inspect provider-attempt metrics because the managed lane intentionally starts no engine.- After placement, a worker with model-plane health unavailable cannot receive
peer work. This is a routability failure, not an unplaced-replica reason.
Check that
model_bindis an unused IP socket,model_endpointis reachable from gateways, and the endpoint scheme matches the security mode. Production ishttps://with mTLS; explicit development ishttp://h2c. - For production TLS failures, verify the same cluster CA and server-name SAN on both nodes, a current enrolled identity, the target node-ID SAN, HTTP/2 ALPN, and system clocks within five seconds. Restart after changing identity, listener, endpoint, or security fields.
replay_detected,replay_fence_full,peer_authentication_failed,request_digest_mismatch, andhop_limit_exceededare security failures. Do not turn them into capacity retries. Inspect gateway and worker clocks, identity state, and duplicate request generation.queue_full,queue_timeout,draining,engine_unhealthy, andcrash_loopcome from worker-local admission or lifecycle. Use the worker's model-host status and the remediation in A local model will not serve.- Pre-output transport, readiness, or capacity failures may move to another
current replica. Once an SSE frame reaches the client, failure is a partial
stream without
[DONE]and is never replayed. Client cancellation should decrement active work and increase the stream-cancellation counter.
Query the metrics endpoint for
sbproxy_managed_replica_attempts_total,
sbproxy_managed_replica_failovers_total,
sbproxy_model_plane_peer_dispatch_seconds,
sbproxy_model_plane_stream_cancellations_total, and
sbproxy_model_plane_rejections_total. Their labels contain bounded route and
reason classes, never worker topology. Enable debug logs only long enough to
trace a request ID; do not log public bearer values or request bodies.
An example docker compose stack will not start
The use-case-* stacks (and other recent examples) pull the published soapbucket/sbproxy image from Docker Hub; some older examples instead build the image from source in the container (build: ../.., Dockerfile.cloudbuild). Both kinds pull supporting images such as wiremock/wiremock from Docker Hub.
Check:
- Look for
pull access deniedorauth.docker.io ... unexpected EOFin the compose output. That is a Docker Hub connectivity problem, not an example defect. - Confirm the daemon is up with
docker info, and that the host can reach Docker Hub. - Pre-pull the images (or, for the build-from-source examples, build the
sbproxyimage once) so a laterdocker compose upworks from cache. - The build-from-source examples compile the whole workspace inside the container on first run; a long silent period during
docker compose upis the Rust build, not a hang.
Build and run quick reference
# Debug build
make build # -> target/debug/sbproxy
# Release build (required by the e2e harness)
cargo build --release -p sbproxy # -> target/release/sbproxy
# Validate a config offline before serving
sbproxy validate --config ./sb.yml
# Diff a proposed config against a baseline (exit 0 no-op / 2 changes / 3 errors)
sbproxy plan -f ./sb.yml --against ./last-good.yml
# Run
./target/release/sbproxy serve -f ./sb.yml
Structured log fields reference
The fields below are the ones most useful when triage-grepping the JSON access log. The canonical, exhaustive schema (with optional fields and stability rules) is access-log.md; names here mirror that file exactly.
| Field | Meaning |
|---|---|
timestamp | RFC 3339 UTC time of the log line. |
origin | Origin name matched. |
method, path, status | Request summary. |
latency_ms | End-to-end request duration, milliseconds. |
auth_ms, upstream_ttfb_ms, response_filter_ms | Phase splits of latency_ms; a missing upstream_ttfb_ms means the request never reached an upstream. |
upstream_status | Upstream's status when a retry, fallback, or modifier rewrote what the client saw. |
client_ip | Resolved client IP after trusted-proxy unwrapping. |
request_id, trace_id | Correlation ids; trace_id is set when an OTLP exporter is wired. |
cache_result | hit, miss, stale, or bypass. |
auth_provider | Auth method that ran (api_key, jwt, etc.). |
policy_action | When a policy intervened, the action it took. |
provider, model | AI-gateway selection for the request (only on AI requests). |
tokens_in, tokens_out | Token counts (only on AI requests). |