Skip to main content
Headgate exposes an exporter-neutral telemetry interface. The optional headgate-otel Rust crate and headgateotel Go module translate worker events into OpenTelemetry traces and metrics. The integration has two distinct parts:
  1. Attach the adapter to the Rust worker or Go runner so completed attempts and worker signals are exported.
  2. Attach trace-context middleware to the producer client so the executing job span is a child of the request or operation that enqueued it.
The OpenTelemetry adapter belongs on the worker or runner, not on the producer client. The client uses enqueue middleware only for propagation. headgate-otel does not install an SDK, choose a sampler, configure an exporter, or replace a global provider.

Install

Keep all OpenTelemetry packages on compatible versions. The versions above match the current Headgate adapters.

Configure providers and an exporter

The application owns the providers because it usually needs one resource and one export pipeline for HTTP, database, and job telemetry. The examples below use OTLP and standard environment variables, so the same binary can send to an OpenTelemetry Collector or a vendor endpoint.

Rust providers

OTLP reads OTEL_EXPORTER_OTLP_ENDPOINT, signal-specific endpoints, headers, timeout, compression, and protocol from the environment. Configure those before constructing the exporters.

Go providers

autoexport selects exporters from OTEL_TRACES_EXPORTER and OTEL_METRICS_EXPORTER; both default to OTLP. Set either value to none to disable that signal without changing application code.

Attach telemetry to execution

This is the step that makes Headgate emit signals. Creating providers alone is not enough.
Passing nil to the Go adapter for either provider uses OpenTelemetry’s current global provider. This is useful when application bootstrap already installs globals, but a no-op global provider silently produces no telemetry. Passing providers explicitly makes the dependency visible and is easier to test.

Connect producer and worker traces

Headgate reserves the traceparent and tracestate envelope headers. The worker parses them at dispatch and uses a valid context as the remote parent of headgate.process. Missing or invalid context starts a root span and never makes the job undecodable. Install a W3C propagator once during application startup, then inject the current context into every envelope in the enqueue batch.

Rust producer middleware

This example assumes the application uses tracing-opentelemetry, so tracing::Span::current().context() returns the current OpenTelemetry context.

Go producer middleware

Use that configured client for direct, bulk, and transactional enqueue. If a handler enqueues follow-on work through the client in its job context, Headgate automatically inherits the current job’s envelope trace context unless the child envelope explicitly sets its own traceparent.
The adapter currently emits execution spans only. It does not create a producer headgate.enqueue span or inject context automatically when Client.Enqueue is called. Use the middleware above to connect traces across the queue boundary.

Execution span

Every job_span runtime event becomes one span with these semantics: Span attributes: The job payload and result are never attached. They can contain credentials, personal data, or large documents and should be inspected through access-controlled application tools instead.

Metrics

Exporter and backend naming rules may translate dots to underscores or append suffixes such as _total. Inspect the names in your backend before copying a query verbatim.

Useful dashboards and alerts

Start with operational questions rather than raw queue depth:
  • Why are jobs not starting? Graph the rejection rate by headgate.policy and queue.
  • Are workers saturated? Compare utilization, inflight, and capacity by worker.
  • Is polling wasteful? Alert on a sustained high empty-poll ratio, not a single sample.
  • Are attempts slowing down? Graph duration percentiles by kind, queue, and outcome.
  • Is the memory guard cycling workers? Correlate memory, memory limit, and restart count.
  • Is the fleet falling behind? Combine runtime metrics with the control API’s arrival rate, drain rate, oldest-job age, and time-to-drain. Those durable aggregates are not emitted by this process-local adapter.
Avoid alerting on depth alone. A deep queue that is draining faster than it is growing is different from a shallow queue whose oldest job has stopped moving.

Cardinality and privacy

Job IDs appear on spans because spans are sampled event records. They never appear on metric attributes. Fingerprints, partition keys, tenant IDs, and payload fields are also excluded from metric attributes.
Do not add tenant, job, fingerprint, or arbitrary error text as metric attributes in a custom adapter. Each new value creates more time series and can turn ordinary queue volume into an observability outage or an unexpected vendor bill.

Shutdown and flushing

Stop admission and let the worker or runner finish its bounded graceful shutdown first. Then shut down the tracer provider and meter provider. Providers commonly buffer spans and metrics; exiting without shutdown can lose the final attempts from a deployment. In Go, use a fresh bounded context for provider shutdown if the runner’s context has already been cancelled. In Rust, keep both provider values alive until worker.run() returns.

Troubleshooting

No spans or metrics arrive

  1. Confirm Telemetry is set on WorkerConfig or headgate.Config.
  2. Confirm the providers have a real exporter and are not no-op globals.
  3. Check OTEL_EXPORTER_OTLP_ENDPOINT, protocol, TLS, and authentication headers.
  4. Shut providers down during a local test to force buffered data to flush.

Job spans appear as separate traces

Inspect the stored envelope headers. The producer must inject a valid W3C traceparent. Also confirm the enqueue call receives the context containing the active request span. Creating the client once is fine; passing context.Background() to enqueue is not.

Metrics arrive but worker gauges do not

Worker gauges are emitted from runtime saturation and memory sampling events. Confirm the runner has started and that your metric reader’s collection interval has elapsed.

Duplicate-looking duration measurements

headgate.job.duration is recorded from both the compact completion event and the richer attempt-span event, with different attribute sets. Select the attribute form you need in the dashboard instead of summing both shapes indiscriminately.

Payload data is missing from traces

This is intentional. Payloads and results are not telemetry attributes. Add a bounded, non-sensitive business identifier in application instrumentation when correlation needs more than the Headgate job ID.

Connection budgets

Keep lease renewal and heartbeat traffic moving while handlers hold transactions.