Skip to content

Telemetry

What the audit processes publish and how to install the alerts and dashboard. OpenTelemetry's environment configures telemetry, never a configuration file (0063). Without a collector a process publishes nothing.

Variable Effect
OTEL_EXPORTER_OTLP_ENDPOINT The collector for metrics and traces, such as http://<gateway>:4318
OTEL_EXPORTER_OTLP_METRICS_ENDPOINT, OTEL_EXPORTER_OTLP_TRACES_ENDPOINT One signal only
OTEL_SERVICE_NAME Defaults to audit-writer or audit-query
OTEL_TRACES_SAMPLER, OTEL_TRACES_SAMPLER_ARG Unset: parent-based always_on, so a caller's decision wins and every new trace is kept. For a tenth, set parentbased_traceidratio and 0.1

The chart sets the environment

telemetry.otlp renders the variables above:

telemetry:
  otlp:
    endpoint: http://gateway.observability.svc:4318   # empty: render nothing
    protocol: http/protobuf                           # or http/json; the exporters are OTLP/HTTP
    extraEnv:                                         # OTEL_* only
      OTEL_TRACES_SAMPLER: parentbased_traceidratio
      OTEL_TRACES_SAMPLER_ARG: "0.1"

With an endpoint, each pod gets the endpoint, the protocol, its own OTEL_SERVICE_NAME, then each extraEnv entry, sorted.

Pod OTEL_SERVICE_NAME
Front door, receiver, consumers audit-writer
Query service audit-query
Indexer audit-observe
Notary audit-notary
audit toolchain jobs audit (they export nothing)

The chart refuses an extraEnv key outside OTEL_* or named OTEL_EXPORTER_OTLP_ENDPOINT, and a non-http(s) endpoint. On Lambda, see AWS telemetry.

Metrics

The gateway stores dots as underscores and adds _total and unit suffixes. It keeps only cluster, namespace and tier as resource labels (emitting).

Series Type Labels Answers
audit_sink_records_acknowledged_total counter transport, durability Ingest rate and durability mix
audit_sink_records_rejected_total counter transport Records refused for their own sake
audit_sink_write_duration_seconds histogram transport, outcome Write time per hop
audit_sink_consume_failures_total counter transport Batches a consumer's target failed, to be redelivered
audit_observe_index_lag_seconds histogram profile Seconds from an object's put to its rows in the index; settle is the floor. Only for objects that arrived
audit_observe_index_deferred_total counter profile, reason Objects not indexed: retry is tried again, unreadable was skipped
audit_observe_passes_total counter outcome Indexing passes, succeeded or failed
audit_observe_pass_since_success_seconds gauge Seconds since a pass last succeeded (since start, before the first)
audit_observe_objects_indexed_total, audit_observe_records_indexed_total counter profile Indexer output
audit_queue_message_age_seconds histogram transport Seconds from send to the writer receiving it (AWS: SentTimestamp). Redelivery wait is included. Depth and the dead-letter queue are CloudWatch's (AWS)
audit_writer_dead_lettered_total counter Records the writer could not process
audit_writer_catalogue_unknown_total counter source, catalogue_version Records naming an unregistered catalogue version, also dead-lettered. After 20 distinct pairs the rest count as other; the log line event=unknown_catalogue has all. On AWS the library alarms on it
audit_seal_age_seconds gauge profile Seconds since the end of the newest sealed hour, of the tenant furthest behind, as of the notary's last run
audit_seal_written_total counter profile Seals written
audit_seal_failures_total counter profile Tenants a notary run could not seal further
audit_writer_objects_written_total, audit_writer_records_written_total counter profile Writer output
audit_emit_records_dropped_total counter action Records an emitter's queue gave up
audit_emit_queue_pending gauge Records an emitter holds, unacknowledged
audit_emit_batches_failed_total, audit_emit_records_written_total, audit_emit_records_refused_total counter delivery or action The emitter's other counts
Label Values
transport connect-server, connect-client, nats, sqs
durability archived, queued, logged, unspecified
Topic Behaviour
Cardinality No series is labelled by tenant: the store drops a series with too many labels while the write answers 200. The tenant is on the span
Seal age The hourly notary pushes it at exit, then the series stops. Alert and dashboard add the time since the last sample: last_over_time(m[1d]) + (time() - tlast_over_time(m[1d])). A notary that never reported leaves no series; the CronJob's state is the evidence
Index lag Never reads below settle (default 2 minutes, 0062); a healthy p99 sits a little above it. The writer counts no index metrics

Traces

One span per write at each hop, continued across processes.

Span Kind Where
HTTP server, then the RPC server Writer, receiver, query service (otelhttp, otelconnect)
audit.sink.write <transport> client, producer, internal sink.Client, NATS and SQS publishers, the front door
audit.sink.consume <transport> consumer NATS and SQS consumers

traceparent and tracestate cross a queue in NATS headers and SQS message attributes. A consumer's span is a child of the first message's trace and links up to sixteen others. The liveness probe is not traced.

No personal data Rule
Enforced where spans leave the process The exporter drops every attribute not on telemetry.SpanAttributeAllowlist, and every span event, status text and link attribute
Allowed action, outcome, tenant id, durability, delivery, record and rejection counts, transport, the shape of the RPC or HTTP request (method, route, status, path)
Never The actor, a subject, a client address, record data. A test enforces the list

Alerts

Nine rules in the group audit.write-path. Each rule's comment in charts/audit/templates/alerts.yaml gives its threshold and reason.

alert fires when threshold and why severity
AuditRecordsDeadLettered any increase in 15m zero is the only healthy count: the writer accepts every well-formed record critical
AuditEmitterDroppingRecords any increase in 15m, any namespace a dropped record never exists critical
AuditSealStale the newest sealed hour of a profile older than 3h, for 10m the hourly notary seals an hour about ten minutes after it ends, so a healthy newest seal is at most about 1h20m old and one missed run leaves it near 2h20m; three hours fires on the second, when a gap in the chain has opened that no later seal can close critical
AuditIndexLagHigh p99 index lag above 600s, for 10m the settle window (default 2m) is the floor of the lag, so ten minutes is an indexer that has stopped or is stuck; raise it with settle warning
AuditIndexRowsDeferred any increase in 15m an object the indexer could not take: search is late or missing it warning
AuditIndexStalled no successful indexing pass for 15m (maxAgeSeconds), for 5m the pod of an indexer that cannot index stays Ready; the gauge is the seconds since a pass last succeeded, so a stall behind any cause is caught. 15m is three polls at the default 5m interval: raise it with interval critical
AuditIndexPassesFailing over half the passes in 30m failed and at least 3, for 10m one failed pass is a blink and is retried at the next poll; this is passes that keep failing while some may still succeed warning
AuditWriterRejectingRecords over 5% of records refused and at least 10, for 10m a share, so one buggy producer on a busy stream is seen and one bad record on a quiet one is not warning
AuditQueueConsumerFailing a NATS or SQS consumer failing for 15m one failure is a restart or an election; fifteen minutes is batches going round critical

Installing them

renders: alerts renders only the rules. Install them from the platform that runs the ruler, beside the write-path release.

renders: alerts
presets:                        # the write path's; one must be standard or attested
  standard: {bucket: example-audit-main}
alerts:
  namespace: audit              # where the write path runs: the writer rules' series
  objectNamespace: monitoring   # where the ruler reads rule objects from
  emitterNamespace: ".+"        # the applications' namespaces: their series carry theirs
  ruleLabels:                   # on every rule, for routing
    k8s_cluster_name: prod
  runbookBaseUrl: https://github.com/truvity/sluis/blob/master/docs/guides/audit/operate/respond-to-alerts.md
Value Effect
alerts.format: prometheusrule Renders a PrometheusRule instead of a VMRule
alerts.rules.<name>.enabled\|severity\|for\|labels Tunes each rule; a release with every rule off is refused
presets Required: the write path's release's. Refused unless one is standard or attested, the presets with alarms
alerts.namespace Empty is this release's namespace; set it when the alerts release runs elsewhere

Responses: respond to an audit alert.

The dashboard

hack/dashboards/audit-overview.py generates charts/audit/dashboards/audit-overview.json, "Audit overview - $cluster". It follows truvity/observability's dashboard contract: datasource, cluster and namespace variables chained in that order, and $cluster in the title and every query.

Panels Content
Tiles Dead letters, rejections, index rows deferred, newest seal age, emitter drops, consumer failures
Series Ingest rate by transport, durability mix, rejection and dead-letter rates, write latency, index lag, newest seal age by profile, each application's emitter queue and drops

Install it where Grafana runs:

renders: dashboards
dashboards:
  namespace: monitoring   # Grafana's
  folder: Audit

The chart renders one ConfigMap per dashboard, labelled grafana_dashboard: "1". folder applies only when the sidecar reads k8s-sidecar-target-directory (sidecar.dashboards.folderAnnotation).

How it is proven

just telemetry runs in CI with pinned tools:

Check Detail
Dashboard Regenerated; must equal the committed file
Lint dashboardlint from truvity/observability at a pinned release, over the dashboard and over a copy with a literal datasource, which it must refuse
Rules Each is rendered and unit-tested with vmalert-tool unittest, from the VictoriaMetrics release the observability stack runs, checksum verified. Each rule has a test that fires it and one that must not
Chart Refusals in tests/invalid/audit/alerts-*.yaml; golden renders in tests/golden/audit/alerts.yaml and dashboards.yaml