Observability#
Both processes — the orchestrator and task-worker — are instrumented with OpenTelemetry for metrics and traces, and emit structured logs through a shared logging layer. This page describes what each process emits, how a single thumbnail becomes a single distributed trace, which log levels are used, and how to configure all of it through the Helm chart.
OpenTelemetry Overview#
Metrics and traces are exported over the OTLP protocol (gRPC) to a collector you supply, for example the OpenTelemetry Collector. Export is driven entirely by the standard OpenTelemetry environment variables, which the chart injects from independent observability.tracing.* and observability.metrics.* values — each signal can point at its own collector:
When
observability.tracing.enabledis true andobservability.tracing.endpoint.grpcis set, both pods receiveOTEL_EXPORTER_OTLP_TRACES_ENDPOINT,OTEL_TRACES_EXPORTER,OTEL_TRACES_SAMPLER, andOTEL_TRACES_SAMPLER_ARG. OtherwiseOTEL_TRACES_EXPORTER=noneis injected, so nothing attempts to reach a collector atlocalhost:4317.When
observability.metrics.enabledis true andobservability.metrics.endpoint.grpcis set, both pods receiveOTEL_EXPORTER_OTLP_METRICS_ENDPOINTandOTEL_METRICS_EXPORTER. OtherwiseOTEL_METRICS_EXPORTER=noneis injected.Logs are unaffected by either signal and always continue to stdout.
OTEL_SDK_DISABLED=trueis a full kill switch: it also suppresses stdout logging, not just traces and metrics. To disable OTLP export while keeping logs, use the per-signalenabledflags instead — both default totruebut take effect only once their endpoint is set.
All exported telemetry carries a common OpenTelemetry Resource: service.name (thumbnail-service or thumbnail-task-worker), service.namespace (omniverse), service.version (the compiled-in service version), and service.instance.id, which the chart always sets to the pod name through the downward API so replicas can be told apart. The chart deliberately does not inject OTEL_SERVICE_NAME: each binary passes its own name at startup, and a code-supplied name takes priority over the environment, so the variable would be ignored.
Metrics#
The two processes have different metric profiles. The worker exposes service-specific counters, gauges, and histograms describing conversion outcomes and the health of its consume stream. The service exposes generic transport metrics for its REST and gRPC surfaces; it has no service-specific instruments of its own, and its operational state is read from structured log lines (see Log levels).
Worker metrics#
Every worker metric is emitted twice: as an OpenTelemetry instrument on the global meter (exported over OTLP) and as Prometheus text on GET /metrics on the worker’s probe port (HEALTH_PORT, default 8080). The Prometheus endpoint always serves, whatever the OTLP configuration. Every label value is pre-registered at zero, so an absent class reads as 0 rather than as a missing series.
Name |
Type |
Unit |
Attributes |
Description |
|---|---|---|---|---|
|
Counter |
messages |
|
Generate messages by terminal outcome. |
|
Counter |
failures |
|
Failures by the pipeline stage that produced them, permanent and retryable alike. See Failure classes. |
|
Histogram |
|
— (buckets |
Elapsed time for messages that reached conversion — successes and |
|
Gauge |
deliveries |
— |
Deliveries received but not yet acknowledged or negatively acknowledged (in flight plus withheld). Reset to |
|
Counter |
deliveries |
|
Negative acknowledgements actually sent, by cause. |
|
Counter |
— |
— |
Deliberate consume-stream teardowns used to force redelivery — the fallback when a NACK cannot be delivered, and the circuit breaker after repeated early redeliveries. A healthy worker never recycles. |
|
Counter |
— |
|
Consume sessions ended, by cause. |
|
Counter |
— |
— |
Uploaded thumbnails left unreferenced because the association was rejected permanently after the upload. The blob is not deleted; reconcile from the destination named in the accompanying error log. |
|
Counter |
— |
— |
Stream reopens after a near-expiry token refresh produced a usable bearer. |
How to read them together:
thumbnail_worker_unacknowledged_deliveriesrising means retryable failures are accumulating faster than they retire and WorkerEventQueue is heading for its maximum length. Both an acknowledgement and a negative acknowledgement release a delivery, so the gauge reflects work genuinely in flight.Graph
thumbnail_worker_nacks_totalas a rate —sum by (reason) (rate(thumbnail_worker_nacks_total[5m])).retryable_failureis the retry rate; a steady climb means a dependency is persistently failing.early_redeliverymeans the broker is redelivering ahead of the configured delay and the consumer is rejecting the redelivery without reprocessing it; sustained, that trips the circuit breaker. Failures climbing while NACKs flatline means settlements are not reaching the broker.Any sustained rise in
thumbnail_worker_stream_recycles_totalmeans negative acknowledgements are not getting through or the broker is ignoring the redelivery delay.A 1 Hz climb in
thumbnail_worker_stream_token_reconnects_totalmeans refresh is not clearing expiry — a sick identity provider, or a sidecar token file still inside its skew window.
These counters are per pod and reset on restart. Use them for outcome rates, WorkerEventQueue depth for backlog, and the Notification Service’s dead-letter management for individual poison messages. There is no per-item task status to query.
Generic transport metrics#
The service applies shared REST and gRPC request-metrics middleware, which emits request metrics following the OpenTelemetry HTTP and RPC semantic conventions. They are exported over OTLP only; the service has no Prometheus scrape endpoint.
REST metrics#
Attributed with http.request.method, http.route (the matched route template, for example /v1alpha/thumbnails, never the raw path), http.response.status_code, and network.protocol.version:
Name |
Type |
Unit |
Description |
|---|---|---|---|
|
Counter |
|
Total HTTP requests received. |
|
Counter |
|
Requests that returned a 4xx or 5xx status. |
|
Histogram |
|
Duration of inbound requests from call entry to the start of the response. |
|
Histogram |
|
Request body size consumed by the handler. |
|
Histogram |
|
Response body size observed by the client. |
gRPC metrics#
Attributed with rpc.service, rpc.method, and the final rpc.grpc.status_code. Every ThumbnailService method is known to the metrics layer up front, so no method is missing from the label set on first use:
Name |
Type |
Unit |
Description |
|---|---|---|---|
|
Counter |
|
Total gRPC requests received. |
|
Histogram |
|
Duration of gRPC request handling. |
|
Counter |
|
Requests that completed with a non-OK status. |
The worker’s outbound calls (storage Read/Write, AssociateThumbnail) and the service’s outbound calls (storage metadata, Stat, publishes) go through the matching client layers, which emit rpc.client.* spans and a rpc.client.call.exception warning for every failed attempt.
Tracing#
Tracing uses the same logging layer described above, bridged to OpenTelemetry. The service creates one server span per inbound REST request (named after the method and matched route) and one per gRPC RPC (named after the full method path, with the final status code); the worker creates one thumbnail.generate span per consumed message with child spans thumbnail.download, thumbnail.convert, thumbnail.upload, and thumbnail.associate. The storage-event consumer creates one storage.event span per delivery covering eligibility, fan-out, and the acknowledgement decision — ineligible deliveries are spanned too, so a skipped file is visible in the trace backend.
Distributed tracing#
Distributed tracing is supported end to end. Both processes install the W3C Trace Context propagator, and trace context crosses every hop this service controls:
An inbound REST or gRPC request’s
traceparent/tracestatebecomes the parent of the server span. With no valid context, a new root trace starts.Every
omni.thumbnail.generateevent carries the publishing span’straceparentandtracestatein its message body (see omni.thumbnail.generate).The worker extracts that context and makes it the remote parent of its
thumbnail.generatespan, so the conversion appears in the same trace as the request or storage delivery that caused it.The worker’s outbound
AssociateThumbnailcall injects its context, so the service’s server span for that RPC is a child of the worker’s span.Association-event publishes run as their own
thumbnail.association_eventspan with the request as remote parent — not as a local descendant — so a slow publish never inflates the request span’s exported duration.
One thumbnail is therefore one trace, from the API call or storage delivery through the queue, the conversion, and the association. Three consequences are worth knowing before tuning:
Traces span queue dwell. A trace stays open from publish until the worker finishes, so its wall-clock duration includes queue wait. A backlogged worker produces long traces whose span durations sum to seconds — that is queue depth, not slow work.
Worker spans inherit the producer’s sampling decision. The sampler is always
parentbased_traceidratio; the sampled flag travels in the trace context, so loweringobservability.tracing.samplingRatiodrops conversion spans for every request that was not sampled at the root. Sample for the volume of conversions you want to see, not the volume of API calls.The first hop is storage’s to add. The storage service does not yet stamp
omni.storage.createdwith trace context, so an event-driven thumbnail’s trace starts at this service’sstorage.eventspan rather than at whatever wrote the source object. When storage starts sending one, bothtraceparentandtracestateare honored on that hop.
A message with absent or malformed trace context is processed normally and roots a new trace — telemetry is never a precondition for work. Oversized inbound traceparent values are refused with a single bounded warning and the delivery outcome is unchanged.
Logging#
Logs are written to stdout as structured JSON. Each record carries trace_id and span_id when a span is active, so a log pipeline that also ingests traces (for example Loki alongside Tempo) can pivot between the two. There is no pretty/text mode, and no logs-only toggle: the only way to suppress stdout logging is OTEL_SDK_DISABLED=true (see OpenTelemetry Overview), which also disables traces and metrics. The chart sets OTEL_LOGS_EXPORTER=console on both containers, which selects stdout rather than OTLP log export.
Log filtering uses RUST_LOG (Helm observability.logging.level): a level (info, debug), or comma-separated <component>=<level> pairs to set the level per component. When RUST_LOG is unset or unparseable the effective level is info; a typo never prevents startup.
Log levels#
Both binaries use the standard log levels with these conventions:
error— Failures that need investigation or leave state behind. Examples: a generate message permanently rejected (Permanently rejecting a generate message, naming destination and failure class — the only durable trace of a poison message before it is acknowledged); an uploaded thumbnail left orphaned by a permanent association rejection; a consume stream interrupted (Consume stream interrupted; reconnecting); a token refreshed and rejected again; the early-redelivery circuit breaker firing; a rollback of an association write that itself failed (names the address and key to reconcile by hand); a generated blob that could not be deleted after its metadata was removed.warn— Degraded but self-healing conditions. Examples: a retryable failure being scheduled for spaced redelivery; the NACK deferral bound reached; an unknown event type on a durable queue; a stored association entry skipped because it has no thumbnail address; a partial thumbnail list returned; the storage-event consumer at its concurrency bound ([BACKPRESSURE]); CORS enabled with no usable origins; an association-event publish skipped because no aggregation endpoint is configured.info— Lifecycle and operator-visible state. Examples: listener bind confirmations ([HTTP] Server listening on …,[GRPC] Server listening on …); durable queue provisioning ([PROVISION] Durable queue created/already existed); consume stream established;Thumbnail generated and associatedwith elapsed milliseconds; token refreshed and reconnecting; the storage-deleted sweep summary (N succeeded, M failed, K blobs left as caller-owned); the periodic StorageEventQueue[STATS]line every 300 s (totals, filtered, active/capacity, unacknowledged, deferred, stream age, time since last delivery);[SHUTDOWN]messages during drain.debug— Per-request diagnostics. Examples:Skipping CREATE eventwith the ineligibility reason; the resolved conversion specification; storage metadata retry attempts ([RETRY] <op> attempt i/n failed, demoted fromwarnbecause the client trace layer already reports each failed attempt); association recorded by the service; source object read and thumbnail object written.trace— Reserved. Neither binary emits trace-level records of its own; the level is wired up for dependencies.
info is the recommended steady-state level; raise it to debug for focused troubleshooting — in particular, CREATE skip reasons are only visible at debug.
Configuring Observability with the Helm Chart#
All observability settings live under the root observability key and apply to both Deployments. The chart translates them into RUST_LOG and the OTEL_* variables listed in the overview.
Helm values reference#
Value |
Default |
Description |
|---|---|---|
|
|
|
|
|
Takes effect only when |
|
|
OTLP/gRPC collector endpoint for trace spans, for example |
|
|
Sampling ratio for |
|
|
Takes effect only when |
|
|
OTLP/gRPC collector endpoint for metrics. Independent of |
|
|
Additional |
Example: export metrics and traces#
The following values block points both exporters at a collector inside the cluster, raises the log level for focused troubleshooting, and samples half of all root traces:
observability:
logging:
level: "debug"
tracing:
enabled: true
endpoint:
grpc: "http://otel-collector.observability.svc.cluster.local:4317"
samplingRatio: "0.5"
metrics:
enabled: true
endpoint:
grpc: "http://otel-collector.observability.svc.cluster.local:4317"
resourceAttributes: "deployment.environment=production"
Additional environment variables#
The OpenTelemetry SDK honors its standard environment variables beyond the ones the chart exposes as first-class values — for example OTEL_EXPORTER_OTLP_HEADERS or OTEL_METRIC_EXPORT_INTERVAL. Inject them through each component’s extraVars map when a deployment needs to authenticate to a managed collector or tune an interval the chart doesn’t surface:
thumbnailService:
extraVars:
OTEL_EXPORTER_OTLP_HEADERS: "authorization=Bearer <token>"
thumbnailWorker:
extraVars:
OTEL_EXPORTER_OTLP_HEADERS: "authorization=Bearer <token>"
References#
OpenTelemetry — the metrics and tracing standard both processes instrument
OpenTelemetry Collector — the collector both exporters send OTLP to
OpenTelemetry HTTP semantic conventions — REST request-metric attributes
OpenTelemetry RPC semantic conventions — gRPC request-metric attributes
OpenTelemetry SDK environment variables — the standard
OTEL_*variables both binaries honorW3C Trace Context — the propagation format both processes use for distributed tracing
See also#
Conformance and smoke tests — the next topic: the standalone test binaries that verify a deployment.
Thumbnail generation worker — the pipeline stages and failure classes the worker metrics are labeled with, and the
/healthz,/readyz,/metricsprobe surface.Storage event consumption — the
[STATS],[BACKPRESSURE], and[PROVISION]log lines that carry the service-side consumer’s state.Service notifications — how trace context rides the
omni.thumbnail.generatemessage.Troubleshooting — which metric or log line to look at for each symptom.
Thumbnail Service Configuration — the full Helm chart reference including
observabilityin its wider context.