OpenTelemetry Tracing¶
Helmfile can export OpenTelemetry traces of a run, which is especially useful in CI/CD: see where time is spent, which helm invocations dominate, how label selectors fan out, and what to target for improvement.
Tracing is experimental and off by default. It currently emits the command-level root span, state-loading spans (discover/load/render/parse), one span per external process (helm invocations, hooks, plugin execs), per-hook spans, and per-release spans for the sync/diff/delete/status/test/prepare paths.
Enabling¶
helmfile --otel-tracing -e production -l tier=backend apply
or via environment variable (handy for CI):
export HELMFILE_OTEL_TRACING=true
helmfile sync
--otel-tracing overrides HELMFILE_OTEL_TRACING when specified.
Configuration¶
Everything beyond the on/off switch uses the standard OpenTelemetry environment variables; helmfile defines no telemetry-specific variables of its own.
| Variable | Default | Notes |
|---|---|---|
OTEL_EXPORTER_OTLP_ENDPOINT |
http://localhost:4318 |
OTLP endpoint; use http://host:4317 for gRPC |
OTEL_EXPORTER_OTLP_PROTOCOL |
http/protobuf |
or grpc |
OTEL_EXPORTER_OTLP_HEADERS |
— | e.g. authorization=Bearer <token> |
OTEL_TRACES_EXPORTER |
otlp |
otlp | console | none |
OTEL_METRICS_EXPORTER |
otlp |
otlp | console | prometheus | none — see Metrics |
OTEL_TRACES_SAMPLER (+ OTEL_TRACES_SAMPLER_ARG) |
parentbased_always_on |
|
OTEL_SERVICE_NAME |
helmfile |
|
OTEL_RESOURCE_ATTRIBUTES |
— | e.g. cicd.pipeline=deploy,cicd.run_id=4821 |
OTEL_PROPAGATORS |
tracecontext,baggage |
|
OTEL_SDK_DISABLED |
false |
standard kill switch |
HTTP proxies (HTTPS_PROXY/HTTP_PROXY) are honored automatically by the Go runtime.
Quick start without a collector¶
To inspect what would be exported, print spans as JSON on stdout:
OTEL_TRACES_EXPORTER=console helmfile --otel-tracing -l name=myrelease template
Quick start with Jaeger¶
docker run --rm -p 16686:16686 -p 4318:4318 jaegertracing/all-in-one:latest
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318 \
helmfile --otel-tracing -e production apply
# Open http://localhost:16686 and pick service "helmfile"
What gets traced¶
The root span is named after the command (helmfile apply, helmfile sync, …) and carries helmfile.command, helmfile.file, helmfile.environment, and helmfile.selectors attributes, plus helmfile.exit_code and the error (if any) at the end.
Every external process helmfile starts — each helm invocation, hook command, and plugin exec — gets its own span nested under the command span: helm.exec (with helm.subcommand) for helm binaries, os.exec otherwise, both carrying exec.command, redacted exec.args, and exec.exit_code on failure.
State loading is traced too: helmfile.discover_states, one helmfile.load per state file (including nested helmfiles), with helmfile.render and helmfile.parse children — rendering is frequently the hidden time sink. Each hook execution produces a helmfile.hook span (with hook.event and hook.name) that its subprocess span nests under — and release-scoped hooks (presync/postsync & co. within sync/diff/delete) nest under their release’s span, keeping full release attribution.
Per-release spans (helmfile.release.sync / .diff / .delete / .status / .test / .prepare) carry the release name, namespace, chart, and labels, and nest under the state-file load span — with the release’s helm subprocesses (upgrade, diff, delete, status, test) nested under the release span. The remaining loops (flag preparation, template/lint/unittest) are being added incrementally; see the design proposal for the full taxonomy.
Secret-bearing command arguments (--set, --set-file, --username, --password, …) are never recorded in span attributes — they are masked before export.
Metrics¶
Tracing and metrics share the same switch and resource; metrics are exported through the standard metrics environment variables:
| Variable | Default | Notes |
|---|---|---|
OTEL_METRICS_EXPORTER |
otlp |
otlp | console | prometheus | none |
OTEL_METRIC_EXPORT_INTERVAL |
60000 (ms) |
periodic export interval; the final flush happens at exit |
Three instruments are emitted:
| Metric | Type | Attributes |
|---|---|---|
helmfile.helm.exec.duration |
histogram (seconds, buckets tuned for 5ms–600s helm invocations) | subcommand, success |
helmfile.release.duration |
histogram (seconds, same buckets) | verb, result — plus helmfile.release and helmfile.namespace when per-release metrics are enabled (below) |
helmfile.release.count |
counter (unit {release}) |
verb (sync/diff/delete/status/test/prepare), result (success/error) |
Per-release metrics (opt-in, high cardinality)¶
By default metric dimensions are bounded enumerations — release names never appear, so time-series count stays constant regardless of fleet size. If you need per-release durations on dashboards, opt in:
HELMFILE_OTEL_METRICS_PER_RELEASE=true helmfile --otel-tracing sync
This adds helmfile.release and helmfile.namespace to helmfile.release.duration. Time-series count then scales with your release fleet: well-suited to bounded CI runs, but long-lived centralized collection needs a backend capacity/TTL story. (Per-release timing is always available in traces, without this flag.)
Both dimensions are bounded enumerations — never release names — so metric cardinality stays constant regardless of fleet size. The instrumentation scope (helmfile) carries the helmfile version.
Inspect them without a collector with OTEL_METRICS_EXPORTER=console.
CI trace correlation¶
If your CI system injects a W3C TRACEPARENT environment variable into jobs, helmfile joins that trace automatically, so helmfile spans appear inside your pipeline’s trace.
Troubleshooting¶
- No spans arrive: check
OTEL_EXPORTER_OTLP_ENDPOINT/protocol match your collector (4318 forhttp/protobuf, 4317 forgrpc); verify withOTEL_TRACES_EXPORTER=console. - Tracing breaks nothing: exporter/sampler misconfiguration and export failures never fail a helmfile run; at worst you get no spans.
- Still stuck? Set
OTEL_SDK_DISABLED=falseexplicitly and rerun with--log-level debug.