Skip to content
🎉 GoReleaser v2.17 is out! with Windows MSIX packages, post-release verification, and more!
Telemetry

Telemetry

This feature is exclusively available with the GoReleaser Pro Enterprise plan.
This will be available in the next release (v2.18). Stay tuned!

GoReleaser can export OpenTelemetry traces of your releases, so you can see where the time goes: which pipes are slow, which builds dominate, which upload is flaky, and how it all trends over time.

Traces are always sent to your collector, configured through the standard OTEL_* environment variables. GoReleaser never sends anything to us.

Usage

Telemetry is opt-in, and needs two things:

  1. Enable it in your configuration:

    .goreleaser.yaml
    telemetry:
      # Whether to export OpenTelemetry traces.
      enabled: true
  2. Point GoReleaser at your collector:

    export OTEL_EXPORTER_OTLP_ENDPOINT="https://otel.mycompany.com"
    export OTEL_EXPORTER_OTLP_HEADERS="authorization=Bearer $TOKEN"
    goreleaser release

The configuration option exists on purpose: without it, GoReleaser would start talking to the network whenever a CI runner happens to have OTEL_* variables set for other tooling. You have to ask for it.

If telemetry is enabled but no endpoint is set, GoReleaser warns you, and the SDK falls back to its own default of https://localhost:4318.

Which commands are traced

The commands that validate a license: release, publish, continue, announce, and verify.

goreleaser build and goreleaser release --snapshot skip the license check entirely, so they are not traced. Verify your setup with a real release.

Environment variables

GoReleaser uses the standard OpenTelemetry environment variables. The most useful ones:

VariableDescription
OTEL_EXPORTER_OTLP_ENDPOINTCollector endpoint, e.g. https://otel.mycompany.com.
OTEL_EXPORTER_OTLP_HEADERSExtra headers, e.g. authorization=******.
OTEL_EXPORTER_OTLP_PROTOCOLhttp/protobuf (default) or grpc.
OTEL_SERVICE_NAMEService name. Defaults to goreleaser.
OTEL_RESOURCE_ATTRIBUTESExtra resource attributes, e.g. team=platform,env=prod.
TRACEPARENTIf set, the release is nested inside your CI pipeline’s trace.

The _TRACES_ variants (e.g. OTEL_EXPORTER_OTLP_TRACES_ENDPOINT) take precedence over the generic ones, as per the specification.

These are read from the actual process environment, not from the env section of your configuration file: the OpenTelemetry SDK reads them directly.

What you get

One trace per run, with:

  • a root span named after the command,
  • a child span per pipe (building binaries, archives, publishing…),
  • a span per publisher inside publishing (docker images, blobs, homebrew cask…),
  • a span per build target and per docker image, which is usually where the time actually goes.

Releasing a small Go project looks roughly like this:

release                                 1843ms
├─ getting and validating git state      177ms
├─ setting defaults                       29ms
├─ building binaries                     451ms
│  ├─ build                              416ms  goreleaser.build.target=linux_amd64_v1
│  ├─ build                              253ms  goreleaser.build.target=darwin_arm64_v8.0
│  └─ build                              428ms  goreleaser.build.target=windows_amd64_v1
├─ generating changelog                   11ms
├─ archives                              166ms
├─ calculating checksums                   1ms
└─ publishing                           1008ms
   └─ docker images                     1007ms

Builds run in parallel, so the build spans overlap and can add up to more than their parent.

Span names are kept low-cardinality on purpose: what varies from one build to the next lives in the attributes, so your backend can aggregate build across targets, projects, and runs.

Spans are annotated with the CI/CD semantic conventions, so they should light up in your existing dashboards without any mapping:

AttributeWhereExample
cicd.pipeline.namerootyour project name
cicd.pipeline.run.idrootthe trace ID
cicd.pipeline.resultrootsuccess, failure, timeout, cancellation
cicd.pipeline.run.url.fullrootthe release URL
vcs.repository.url.fullrootyour repository URL
vcs.ref.head.namerootthe branch
vcs.ref.head.revisionrootthe commit
goreleaser.project_versionroot1.2.3
goreleaser.artifactsroothow many artifacts were produced
goreleaser.snapshotrootwhether it is a snapshot
goreleaser.nightlyrootwhether it is a nightly
cicd.pipeline.task.namepipepublishing
cicd.pipeline.task.run.resultpipesuccess, failure, skip
goreleaser.build.idbuild spanyour build ID
goreleaser.build.builderbuild spango, rust, zig
goreleaser.build.targetbuild spanlinux_amd64_v1
goreleaser.docker.iddocker spanyour docker ID
goreleaser.docker.platformdocker spanlinux/amd64
goreleaser.docker.platformsdocker spanlinux/amd64, linux/arm64 (Docker v2)

Skipped pipes are recorded as skip rather than as errors, so --skip=publish runs don’t pollute your error rates.

Every span also carries the usual resource attributes: service.name (goreleaser unless you override it), service.version (the GoReleaser version that produced the release), the host, and the SDK details.

When a release fails, the failing span and the root span are both marked as errors, and the trace is still exported — the broken release is exactly the one you want to inspect afterwards.

Failure handling

Telemetry never fails a release. If the collector is unreachable or rejects the data, GoReleaser warns once and carries on — the release is not held hostage to your observability stack. The final flush is bounded, so a dead collector can’t hang the end of your pipeline.

A malformed OTEL_RESOURCE_ATTRIBUTES doesn’t disable tracing either: the attributes that did parse are kept, and GoReleaser warns about the rest.

Last updated on