Skip to content

Observability

Logs, request IDs, metrics, and error bodies share one goal: an operator can correlate any client complaint to exactly one gateway request.

Logs

The binary emits structured JSON on stdout (one JSON object per log line, easy to parse by log aggregators), filtered by DWARA_LOG (RUST_LOG syntax — a filter syntax like dwara=info,debug, default dwara=info). One access-log line per completed request carries: timestamp, request_id, method, path, status, duration_ms, route, consumer, upstream, endpoint, attempts, and rate_limited/broken/shed flags. route is unrouted for 404s and reserved paths; consumer is anonymous without authentication.

DWARA_ACCESS_LOG_SAMPLE (0.0–1.0, default 1.0) sets the fraction of non-error lines emitted — responses with status ≥ 500 are always logged regardless of sampling, and an invalid value falls back to 1.0 so a broken knob can never silence error visibility.

Redaction is exhaustive: logged paths never include the query string, and no credential material (Authorization, Proxy-Authorization, Cookie, Set-Cookie, X-API-Key values, keys, JWKS bodies (a JSON document of signing keys)) is ever logged.

Request IDs

An inbound X-Request-Id is respected when it's printable ASCII of at most 128 bytes; otherwise Dwara generates one (req-<hex nanoseconds>-<counter>). The resolved ID is echoed on every response as X-Request-Id and appears in every log line, span, and error body — use it as the single correlation key across client reports, gateway logs, and (if enabled) traces.

Metrics

/metrics serves Prometheus text format (a plain-text metrics format Prometheus scrapes — Prometheus is a monitoring system that scrapes metrics endpoints), reserved on every terminate and cleartext listener just like /healthz (see Operations).

MetricTypeLabels
requests_totalcounter (a value that only goes up)route, listener, status_class
request_duration_secondshistogram (counts observations into buckets for percentiles)route
upstream_attempts_totalcounterupstream, endpoint, status_class
retries_totalcounterupstream
rate_limited_totalcounterroute
shed_totalcounterpriority
dwara_policy_dry_run_totalcounterphase, route
breaker_stategauge (0/1/2 = closed/open/half-open; a gauge is a value that can go up or down, like a queue depth)upstream
endpoint_healthgauge (1/0 = available/ejected)upstream, endpoint
upstream_fail_open_picksgaugeupstream
active_requestsgauge
config_generationgauge
jwks_refresh_totalcounterprovider
dwara_rate_limiter_evictions_totalgauge
dwara_rate_limiter_live_keysgauge
dwara_webhook_events_totalcounterkind, outcome
dwara_events_dropped_totalgauge
dwara_events_emitted_totalgauge
dwara_slo_burn_rategaugeroute, objective, window
dwara_slo_targetgaugeroute, objective
dwara_adaptive_factorgaugepolicy
dwara_adaptive_min_factorgaugepolicy
dwara_adaptive_max_factorgaugepolicy
dwara_adaptive_origin_signal_totalcounterpolicy, signal
dwara_adaptive_tightened_totalcounterpolicy
dwara_adaptive_relaxed_totalcounterpolicy
dwara_ai_requests_totalcounterprovider, route, outcome, version
dwara_ai_tokens_totalcounterprovider, kind, version
dwara_ai_request_duration_secondshistogramprovider, route
dwara_ai_tokens_per_requesthistogramprovider, model, kind
dwara_ai_first_token_secondshistogramprovider
dwara_ai_stream_chunks_totalcounterprovider
dwara_ai_stream_duration_secondshistogramprovider
dwara_ai_budget_denied_totalcounterkind
dwara_ai_semantic_cache_hits_totalcountermodel
dwara_ai_semantic_cache_misses_totalcountermodel

Label cardinality is deliberately config-bounded — there is no consumer-name label anywhere, and the rate-limiter series are aggregate/unlabeled even though the engine tracks many per-key cells internally. The SLO series (dwara_slo_*) exist only for routes carrying an slo block: dwara_slo_burn_rate is the bad-request fraction over a 5m or 1h process-local sliding window divided by the allowed fraction — 1.0 consumes the error budget at exactly the allowed rate, and the dashboard's SLO panel draws the 6x (slow burn) and 14.4x (fast burn) alerting lines. dwara_policy_dry_run_total counts requests a dry-run policy would have rejected, by phase (route_limits, authz, rate_limit, load_shed) and route — its log counterpart is the dwara::policy warn event. A starter dashboard ships at grafana/dwara-overview.json; import it in Grafana via Dashboards → New → Import and point it at a Prometheus instance scraping the gateway's /metrics.

Error envelope

Every gateway-generated non-success response body (including the reserved /healthz//readyz paths) uses one shape:

json
{"error":{"code":"no_route","message":"no route","request_id":"req-..."}}

code is a stable machine token, message is a human string that never leaks upstream internals, and request_id ties the response to logs and traces.

Distributed tracing (OTLP)

Trace export over OTLP (OpenTelemetry Protocol — the standard for exporting traces) requires a build with the otlp cargo feature (off by default to keep the release binary small):

sh
cargo build -p dwara-bin

Point it at a collector with DWARA_OTLP_ENDPOINT (e.g. http://collector:4318/v1/traces is appended, or a full .../v1/traces URL is accepted as-is). In a default build, this environment variable is reserved but inert — setting it has no effect unless the binary was built with the feature.

AI traces: GenAI semantic conventions

AI route requests carry OpenTelemetry GenAI semantic convention attributes on their span, so AI traffic is observable in standard OTel tooling (Jaeger, Tempo, Grafana, Datadog) without custom configuration. The attributes include:

  • gen_ai.system — the provider system (openai, anthropic, gemini, azure_openai, bedrock).
  • gen_ai.request.model — the client-facing model alias.
  • gen_ai.request.max_tokens, gen_ai.request.temperature, gen_ai.request.top_p — request parameters, when present.
  • gen_ai.response.model — the provider model that served.
  • gen_ai.usage.prompt_tokens, gen_ai.usage.completion_tokens, gen_ai.usage.total_tokens — provider-reported token counts.
  • gen_ai.response.finish_reasons — the finish reasons.
  • gen_ai.response.id — the provider response ID.

These attributes appear on the AI request span (a child of the request root span) in any OTel-compatible trace viewer.

SLOs and error budgets

Routes can declare service-level objectives (SLOs — a Service Level Objective is a target like 99.9% availability); the gateway exports them as burn-rate metrics (how fast you are consuming the error budget your SLO allows) for multiwindow alerting:

yaml
routes:
  - name: checkout
    # ...
    slo:
      availability: 99.9        # percent of requests that must not be a 5xx
      latency_ms: 250           # optional latency objective threshold
      latency_target: 99        # percent of requests within latency_ms (default 99)

dwara_slo_burn_rate{route,objective,window} is the error-budget consumption rate — the bad-request fraction over a 5m or 1h sliding window divided by the allowed fraction. availability counts a request bad only when the GATEWAY answers 5xx (client errors are the caller's policy, not availability); latency counts a request bad when its end-to-end duration exceeds latency_ms. Alert on the standard pair: 14.4x over 1h pages (the 28-day budget would burn in ~2 days), 6x over 1h is the slow-burn signal. The windows are process-local and start empty at boot; the shipped dashboard's "SLO burn rate" panel draws both the 6x and 14.4x thresholds. Routes without an slo block export nothing.

tokio-console

For development and debugging, Dwara can expose tokio-console diagnostics for its async task runtime. tokio-console is an optional async task inspector -- connect with the tokio-console TUI to see live task polls, waker stats, and task lifetimes, which is invaluable for diagnosing stuck tasks, waker churn, or scheduler starvation.

This is controlled by the DWARA_TOKIO_CONSOLE_ADDR environment variable (default OFF) and is off at runtime unless explicitly enabled:

sh
cargo build -p dwara-bin

Enable it at runtime with the DWARA_TOKIO_CONSOLE_ADDR environment variable, which sets the address the console server binds to:

sh
DWARA_TOKIO_CONSOLE_ADDR=127.0.0.1:6666 ./dwara run

Then connect from another terminal:

sh
tokio-console http://127.0.0.1:6666

WARNING

tokio-console adds per-poll instrumentation overhead. It is intended for development and debugging only -- never enable it in a production build serving live traffic.

When DWARA_TOKIO_CONSOLE_ADDR is not set, DWARA_TOKIO_CONSOLE_ADDR is reserved but inert -- setting it has no effect. When the feature is on but the variable is unset, the console server does not bind.

Runnable demo

Run the signals against a live gateway: demos/06-observability/ in the repository. Test scripts assert the /metrics request counter (test-01-metrics.sh), X-Request-Id echoed on every response (test-02-request-id.sh), access logs landing in the gateway's container output (test-03-access-logs.sh), and dwara_slo_* gauges for a route carrying an SLO (test-11-slo.sh). The category README covers prerequisites and teardown.