Send OpenTelemetry traces to a collector¶
This guide shows you how to point the operator's OTLP gRPC tracer at a collector and what to filter on when the spans land. When the endpoint is unset, tracing is a no-op: zero overhead, no connection attempt, no startup latency.
How to enable¶
Helm¶
helm install hermes-operator oci://ghcr.io/undermountaincc/charts/hermes-operator \
--set tracing.otlp.endpoint="otel-collector.observability.svc.cluster.local:4317" \
--set tracing.otlp.serviceName="hermes-operator"
tracing.otlp.serviceName is optional. When unset, the operator reports service.name=hermes-operator on every span (the binary's built-in default).
Raw kustomize / make deploy¶
Uncomment the env: block in config/manager/manager.yaml and re-deploy.
Env vars the operator honors¶
Standard OTel:
| Env var | Effect |
|---|---|
OTEL_EXPORTER_OTLP_ENDPOINT |
OTLP gRPC endpoint (required to enable tracing). |
OTEL_SERVICE_NAME |
resource.service.name on every span (defaults to hermes-operator). |
OTEL_RESOURCE_ATTRIBUTES |
Additional resource attributes (comma-separated k=v pairs, standard OTel). |
OTEL_EXPORTER_OTLP_HEADERS |
Outbound headers, used by Honeycomb / Grafana Cloud for auth. |
Collector endpoints¶
The operator speaks OTLP gRPC. Common backends:
| Backend | Endpoint format |
|---|---|
| OTel Collector (in-cluster) | otel-collector.observability.svc.cluster.local:4317 |
| Grafana Tempo | tempo.observability.svc:4317 |
| Jaeger (via OTel Collector) | route OTLP → Jaeger via the collector's jaeger exporter |
| Honeycomb (direct OTLP) | api.honeycomb.io:443 + OTEL_EXPORTER_OTLP_HEADERS=x-honeycomb-team=… |
| Grafana Cloud | host from your stack settings + OTEL_EXPORTER_OTLP_HEADERS=Authorization=Basic … |
If your tracing backend doesn't speak OTLP natively, run the OTel Collector in front of it as a translation layer.
Span shape¶
Every reconcile produces a tree:
hermesagent.Reconcile (root)
├─ Reconcile.EnsureFinalizer
├─ Reconcile.PVC
├─ Reconcile.ServiceAccount
├─ Reconcile.SecretsValidation
├─ Reconcile.RBAC
├─ Reconcile.Deployment
├─ Reconcile.Service
├─ Reconcile.DashboardService
├─ Reconcile.DashboardIngress
├─ Reconcile.NetworkPolicy
├─ Reconcile.Status
│ └─ Reconcile.DashboardProbe # if spec.dashboard.enabled
└─ Reconcile.HandleDeletion # when CR is being deleted
Every span carries:
hermesagent.namehermesagent.namespace
Span Status: Ok on clean return, Error with the error message on any non-nil error.
Span events (timeline annotations)¶
Reconcile.PhaseTransition: onReconcile.Statuswhen Phase changes. Attributes:from,to.Reconcile.RBACRejected: onReconcile.RBACwhen aClusterRoleBindingreferences a role not in the--allowed-cluster-rolesallowlist. Attribute:roleName.Reconcile.DashboardProbeFailed: on bothReconcile.DashboardProbe(child) andReconcile.Status(parent) when the dashboard/api/statusprobe errors. Attribute:error.
These pinpoint exactly when a transition or failure happened, without requiring log correlation.
Viewing traces¶
- Jaeger UI: filter on
service.name = hermes-operator. Reconciles for a specific CR: filterhermesagent.name = <name>ANDhermesagent.namespace = <ns>. - Grafana Tempo: TraceQL:
{service.name="hermes-operator" && resource.hermesagent.name="<name>"}. - Honeycomb / Grafana Cloud: same
service.namefilter;hermesagent.*are facet filters automatically.
Failure modes¶
- Malformed endpoint URL: the operator pod fails fast at startup with
unable to initialize OpenTelemetry tracing, rather than silently dropping spans for the pod's lifetime. - Unreachable endpoint: the OTel SDK retries silently in the background; reconciles proceed normally. There is no operator-side metric for export failures today (the OTel SDK logs to stderr at WARN level); watch the operator pod's logs for
otelentries. - Tracing disabled, sidecar auto-instrumentation enabled: safe. When
OTEL_EXPORTER_OTLP_ENDPOINTis empty, the global TracerProvider stays no-op, so a sidecar's auto-instrumentation can run independently.
Shutdown semantics¶
10s dial timeout on connection setup. SIGTERM-bounded shutdown flushes the last batch of spans best-effort within 5s. The pod is going away regardless, but the operator tries not to lose the last reconcile.