Observability¶
The gateway exposes three observable surfaces:
- Structured JSON logs to stdout (visible via
docker logs). - Prometheus metrics at
/metrics. - The OpenRouter completion ID returned to the client, which can be cross-referenced against upstream logs.
No tracing is emitted in v1.0.0. No request bodies or message content are ever logged.
Structured JSON logs¶
src/smart_gateway/observability.py::setup_logging configures a
JSON formatter. Each line is a single JSON object:
{
"level": "INFO",
"message": "Routing decision made",
"logger": "smart_gateway",
"timestamp": "2026-08-23T10:11:12.345Z",
"task_id": "task-17",
"decision": {
"action": "ROUTE",
"model": "deepseek_v4_flash",
"cause": "score_flash"
}
}
Standard fields:
| Field | Notes |
|---|---|
level |
INFO, WARNING, ERROR. |
message |
Free-text. |
logger |
Always smart_gateway. |
timestamp |
ISO-8601 UTC. |
task_id |
The routing task ID (or *_fallback for fallback events). |
decision |
Action, model, cause. |
Routing-event-specific messages:
| Message | When |
|---|---|
Routing decision made |
Always emitted on a chat completions call. |
OpenRouter API failed after retries with status <code> |
Retryable upstream failure exhausted. |
OpenRouter API error: <code> |
Non-retryable upstream failure. |
OpenRouter connection failed: <reason> |
Transport failure. |
Tail the logs¶
Filter for routing decisions:
Filter for failures:
Save a 24-hour audit trail¶
docker logs --since 24h smart-supervisor-production-gateway-1 \
> /srv/apps/smart-supervisor-production/backups/log-$(date -u +%Y%m%dT%H%M%SZ).jsonl
Prometheus metrics¶
See API reference → Metrics for the full metric list. Three metrics are emitted:
sgw_requests_total{model, status}sgw_routing_decisions_total{action, model}sgw_request_latency_seconds(histogram)
What is intentionally absent¶
- No request body in logs. Even redacted message content is
not logged. Audit trails rely on
task_id,decision.cause, and the OpenRouter completion ID. - No PII in metrics. Metric labels are model IDs and HTTP statuses only.
- No tracing. v1.0.0 does not emit OpenTelemetry spans. The JSON-formatter logs are the audit trail.
- No log shipping. The gateway writes to stdout; you are expected to collect stdout via your container runtime.
How to debug a routing decision¶
- Find the request's task ID. Clients should set
X-Task-Idormetadata.task_id. If neither is set, the gateway derives one from the timestamp. - Grep the logs for the task ID.
- Look at the
decision.causefield. Map it to a rule (see Architecture → Routing pipeline). - If
decision.causeisscore_flashorscore_pro, the log line shows the score indecision.score(added by the policy module). The dry-run endpoint/v1/route/decisionis a more convenient way to inspect scores. - If the cause is
all_models_unhealthy, check the OpenRouter connectivity and the model health registry state.
See also¶
- Reference → Metrics — metric reference.
- The operator runbook at
docs-internal/operations/daily-operations.mddescribes how the operator uses these surfaces on the production VPS (operator-only).