Skip to content

Observability

tinyguard can export OpenTelemetry spans and log records to any OTLP collector over http/protobuf. The feature is off by default: with [observability] disabled no provider, exporter, or span exists, and the stdout logs are byte-identical to a deployment without it.

[observability]
enabled = true
# Base collector endpoint; /v1/traces and /v1/logs are appended.
# https:// everywhere except loopback; a plaintext non-loopback endpoint
# must also set allow_insecure_endpoint = true.
endpoint = "https://otel-collector.observability:4318"
service_name = "tinyguard"

Every key has an environment override (GUARD_OBSERVABILITY_ENDPOINT, GUARD_OBSERVABILITY_SERVICE_NAME, …), and the standard OTEL_* variables are read too (see the environment reference). The keys are in the configuration reference. The exporter runs on its own threads with a bounded queue: a slow or unreachable collector drops telemetry, it never delays or blocks a request. On SIGTERM the shutdown path flushes both batch processors before the process exits.

Sampling: sample_ratio (0–1) with parent_based = true (the default) honors an incoming traceparent header — a request sampled by the caller is sampled here; with parent_based = false the ratio applies on its own. An incoming traceparent also becomes the request span’s parent, so cross-service traces stay connected.

  • Spans — only spans the server itself creates for telemetry, never arbitrary internal spans:
    • Request span (one per admitted request): HTTP method, path without the query string, response status, duration in milliseconds, and the bound tenant slug.
    • Token issuance: grant type, tenant, outcome class (issued / denied / a status class for the device grant).
    • Login verification: outcome class only (authenticated, invalid_credentials, second_factor_pending, …).
    • Federation: one span per provider-addressed federation request with the provider slug and a response-status outcome class.
  • Log records — the same events the stdout logs carry, exported through the masked bridge with the process resource (service.name, service.version, any configured resource attributes) attached.

With logging.format = "json" (GUARD_LOGGING_FORMAT=json) every line is one JSON object with these top-level fields, the same for every TinyBlox component:

Field Meaning
timestamp RFC 3339, UTC, millisecond precision.
level error, warn, info, debug or trace.
message The event’s message.
tenant The request’s tenant (default for the default tenant). Absent on lines written outside a request.
trace_id The W3C trace id, when telemetry is enabled and the request has a trace. Absent otherwise.

Everything else an event carries is an extra key at the same level (method, path, status, …), not nested. The target is target. An extra field cannot overwrite a standard one: it is written as field_<name>.

The masking below applies to stdout too, not only to the OTLP exporter: a field named for a credential is replaced with [REDACTED], and a bearer token or a password= fragment in a message or value is scrubbed. The check the platform runs is kubectl logs <pod> | jq -e 'has("timestamp") and has("level") and has("message")'.

The metrics listener ([server.metrics], port 9090, never routed by the ingress) serves Prometheus text. Every name starts with tinyguard_: tinyguard_requests_total, tinyguard_logged_requests_total, tinyguard_responses_total{class="2xx|4xx|5xx"}, tinyguard_security_blocks_total and tinyguard_uptime_seconds.

Masking is unconditional whenever observability is enabled — there is no configuration that weakens it, and it applies to span attributes, log field values, and log message bodies alike:

  • By key: a field whose key names a credential is replaced wholesale with [REDACTED]. This covers passwords, access/refresh/ID tokens, API keys, client secrets, authorization headers, cookies, session ids (including CSRF tokens and DPoP nonces), one-time codes (authorization, device, user, verification, await codes), recovery codes, sealed values, JWT ids, assertions, webhook URLs, and email addresses / subjects. Pure classification keys (error_code, status_code, …) survive.
  • By value pattern: any remaining string — the log message included — is scrubbed of code=, token=, client_secret=, password=, and assertion= query/form fragments and of Bearer / Basic credentials, with the secret replaced by [REDACTED].

Neither spans nor logs ever carry: passwords, raw request bodies, query strings, subject or email values (emails are [REDACTED] if a field ever names one; the login and federation spans never record them), or token values of any kind.

What does leave the process: HTTP method, path, status codes and durations, tenant and provider slugs, grant types, outcome classes, opaque user and client identifiers, error classifications, and the same structured log fields the stdout logs print (post-masking).

  • OTLP metrics: the server’s own metrics surface stays on its dedicated listener ([server.metrics]); the OTLP exporter carries traces and logs only.
  • Request or response bodies, query strings, cookies, or headers (beyond the traceparent propagation header).
  • Spans from arbitrary internal code: the span bridge accepts only the server’s own telemetry spans, so future internal spans cannot leak unmasked by accident.