Last updated: July 21, 2026
Compile-Time OpenTelemetry Auto-Instrumentation in Go
Java, Python, .NET, and Node.js developers have had zero-code auto-instrumentation for years. An agent attaches when the application starts and generates telemetry without requiring changes throughout the codebase.
Go has been a harder target because it compiles to a native binary without a virtual machine, bytecode, or classloader that an instrumentation agent can intercept. Until recently, your main options were an eBPF agent with Linux-specific privileges or manual OpenTelemetry instrumentation inside each service.
That changed in July 2026, when the OpenTelemetry
Go Compile-Time Instrumentation SIG
released v1 of otelc. The command-line tool injects traces, metrics, and log
correlation while your application is being compiled, without requiring source
changes or a runtime agent.
In this guide, you'll clone a small Go service, build it with otelc, and
inspect the resulting telemetry data. You'll also learn how the build-time
instrumentation works, which libraries it supports, and how to customize it.
Prerequisites
You'll need the following installed on your machine:
- Go 1.22 or later, as the demo uses
net/httprouting patterns introduced in 1.22. - Docker and Docker Compose to run the OpenTelemetry Collector that receives telemetry from the demo app.
curl(or any HTTP client) to send test requests.
Cloning the demo application (optional)
To demonstrate compile-time instrumentation, you'll use a small Go service that
exercises three libraries supported by otelc: net/http, database/sql, and
log/slog.
The application records visits in an in-memory SQLite database. Its / endpoint
inserts a visit and returns the current count, while /fetch makes an HTTP
request back to /. The second endpoint produces nested HTTP client, HTTP
server, and database operations within one trace.
You can follow the rest of the article without running the demo, but cloning the starter project lets you build the application, generate telemetry, and inspect the resulting traces, metrics, and logs in OTel Desktop Viewer.
Clone the starter project and enter its directory:
12git clone https://github.com/dash0hq/dash0-examples.git &&cd dash0-examples/otelc-demo
The project contains the following files:
12345678.├── main.go├── go.mod├── go.sum├── docker-compose.yml├── otelcol.yaml├── Makefile└── README.md
-
main.gocontains the HTTP server, SQLite operations, and structured logging. It does not import the OpenTelemetry SDK or an instrumentation package becauseotelcadds the required instrumentation while compiling the application. -
otelcol.yamldefines the local telemetry pipeline. -
docker-compose.ymlruns the Collector and OTel Desktop Viewer.
Running the application without instrumentation
Before using otelc, build and run the application normally to verify that the
starter project and its dependencies work before OpenTelemetry instrumentation
is introduced.
Download the pinned dependencies and compile the application:
12go mod downloadgo build -o otelc-demo .
Start the resulting binary:
1./otelc-demo
The service listens on port 8080. From another terminal, send two requests to
confirm that the visit count increases:
12curl http://localhost:8080/curl http://localhost:8080/
The second response should report a higher visits value:
1234{"message": "hello from otelc-demo","visits": 2}
The /fetch endpoint makes an outbound HTTP request back to /, which will
later let you inspect HTTP client, HTTP server, and database operations within
the same trace:
1curl http://localhost:8080/fetch
The response includes the result returned by the internal request:
1234567{"proxied": true,"upstream": {"message": "hello from otelc-demo","visits": 3}}
At this point, the service only writes normal
JSON logs. It doesn't emit traces,
metrics, or
OpenTelemetry log records.
Stop the service with Ctrl+C before proceeding to the next section.
Installing otelc
The next step is installing the build tool that will add OpenTelemetry instrumentation during compilation.
Install the otelc command-line interface with go install:
12go install \go.opentelemetry.io/otelc/tool/cmd/otelc@latest
go install downloads the otelc source, compiles it, and places the binary in
your configured Go binary directory, which is usually $GOPATH/bin or
$HOME/go/bin.
Verify that the command is available in your PATH:
1otelc version
You should see:
1otelc version v1.0.1
If your shell cannot find otelc, add the Go binary directory to your PATH
before continuing.
Starting the local telemetry stack
The instrumented application exports telemetry over OTLP, so it needs a Collector or observability backend to receive it.
The Collector receives OTLP from the application and sends each signal to two destinations: its debug exporter and OTel Desktop Viewer.
The repository includes the following Collector configuration:
12345678910111213141516171819202122232425262728293031# otelcol.yamlreceivers:otlp:protocols:grpc:endpoint: 0.0.0.0:4317http:endpoint: 0.0.0.0:4318exporters:debug:verbosity: detailedotlp_grpc/otel-desktop-viewer:endpoint: otel-desktop-viewer:4317tls:insecure: trueservice:pipelines:traces:receivers: [otlp]exporters: [debug, otlp_grpc/otel-desktop-viewer]metrics:receivers: [otlp]exporters: [debug, otlp_grpc/otel-desktop-viewer]logs:receivers: [otlp]exporters: [debug, otlp_grpc/otel-desktop-viewer]
Each pipeline receives one OpenTelemetry signal and sends it to both exporters.
The debug exporter writes detailed telemetry to the Collector logs, while the
OTLP gRPC exporter sends a
copy to OTel Desktop Viewer for inspection in a browser.
The accompanying Docker Compose configuration runs both services and exposes the necessary ports:
123456789101112131415# docker-compose.ymlservices:collector:image: otel/opentelemetry-collector-contrib:0.156.0command: [--config=/etc/otelcol/config.yaml]volumes:- ./otelcol.yaml:/etc/otelcol/config.yaml:roports:- 4317:4317- 4318:4318otel-desktop-viewer:image: ghcr.io/ctrlspice/otel-desktop-viewer:latestports:- 8000:8000
The application sends OTLP to the Collector through ports 4317 or 4318, and
the viewer is available at http://localhost:8000. Its OTLP receiver remains
internal to the Docker network because only the Collector needs to reach it.
Start both services in the background:
1docker compose up -d
Then check that both containers are running:
1docker compose ps
Both the collector and otel-desktop-viewer services should have a status of
Up.
To watch the Collector receive and export telemetry, follow its logs:
1docker compose logs -f collector
You can also open http://localhost:8000 to inspect the same traces, metrics, and logs in OTel Desktop Viewer. The interface will remain empty until you start the instrumented application and send it some requests.
Building the auto-instrumented Go binary
With the local telemetry stack running, rebuild the application through otelc.
The command wraps the normal Go build process and injects the matching
OpenTelemetry instrumentation before the compiler produces the binary:
1otelc go build -o otelc-demo .
The first instrumented build is usually more verbose than a normal go build
because otelc must resolve its instrumentation modules and their dependencies:
12345678go: downloading gotest.tools/v3 v3.5.2...go: found go.opentelemetry.io/otelc/instrumentation/database/sql ingo.opentelemetry.io/otelc/instrumentation/database/sqlv0.0.0-00010101000000-000000000000...go: downloading go.opentelemetry.io/contrib/exporters/autoexport v0.69.0...
The go: found entries show which instrumentation modules matched packages in
the application's dependency graph. For this demo, otelc detects
instrumentation for:
database/sql- OpenTelemetry SDK initialization
- the standard
logpackage log/slog- the
net/httpclient and server - Go runtime metrics
These messages provide a useful first check when troubleshooting. If an expected module is missing, the application may be using an unsupported library or version, or the relevant package may not be present in the compiled dependency graph.
The injected hooks are compiled and linked with the rest of the application. The result is therefore a normal Go executable rather than a binary that depends on a separate runtime agent.
When the command completes, the otelc-demo binary is ready to run with
OpenTelemetry enabled.
You can also integrate otelc through Go's -toolexec mechanism when changing
every build command is inconvenient. First, prepare the required instrumentation
dependencies:
1otelc setup
Then configure go build to invoke otelc through GOFLAGS:
12export GOFLAGS="${GOFLAGS} '-toolexec=otelc toolexec'"go build -o otelc-demo .
Both approaches produce an instrumented binary. Prefixing go build with
otelc is clearer for local use, while the GOFLAGS approach can be easier to
apply across an existing build or continuous integration workflow.
Running the instrumented application
The binary now contains the injected OpenTelemetry instrumentation. Start it with the standard OpenTelemetry environment variables that identify the service and point its exporters at the local Collector:
1234OTEL_SERVICE_NAME=otelc-demo \OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318 \OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf \./otelc-demo
OTEL_SERVICE_NAME assigns the service name attached to its telemetry,
OTEL_EXPORTER_OTLP_ENDPOINT directs all signals to the Collector's OTLP/HTTP
receiver, while OTEL_EXPORTER_OTLP_PROTOCOL explicitly selects the
http/protobuf transport used on port 4318.
When the application starts, otelc logs the OpenTelemetry components it
initialized before the server begins accepting requests:
123456{"time":"2026-07-18T11:13:29.601424789+01:00","level":"INFO","msg":"trace provider initialized with auto-export"}{"time":"2026-07-18T11:13:29.601562593+01:00","level":"INFO","msg":"meter provider initialized with auto-export"}{"time":"2026-07-18T11:13:29.602471007+01:00","level":"INFO","msg":"logger provider initialized with auto-export"}{"time":"2026-07-18T11:13:29.602501929+01:00","level":"INFO","msg":"OpenTelemetry initialized","instrumentation_name":"go.opentelemetry.io/otelc","instrumentation_version":"dev"}{"time":"2026-07-18T11:13:29.602884833+01:00","level":"INFO","msg":"runtime metrics enabled"}{"time":"2026-07-18T11:13:29.603102925+01:00","level":"INFO","msg":"DB client instrumentation initialized"}
These messages confirm that otelc created providers for traces, metrics, and
logs, configured their exporters, enabled Go runtime metrics, and activated the
database/sql instrumentation detected during the build.
From another terminal, send requests to both endpoints so the application produces HTTP, database, and runtime telemetry:
12curl http://localhost:8080/curl http://localhost:8080/fetch
To watch the Collector receive and forward the telemetry, follow its logs:
1docker compose logs -f collector
You should see trace spans, metric data points, and log records printed by the debug exporter. That same telemetry is forwarded to OTel Desktop Viewer, so you can open http://localhost:8000 in your browser to explore them through its web interface.
What you should see
The /fetch trace begins with the incoming request, followed by the outbound
call back to the service and the nested GET / request. The INSERT and
SELECT spans appear beneath that request because the handler passes its
context to ExecContext and QueryRowContext.
Select an HTTP span to inspect attributes defined by the OpenTelemetry HTTP semantic conventions, such as the request method, response status code, server address, and route.
The database spans include attributes describing the database system, operation, and executed statement. The exact attribute names and values can vary with the semantic convention version used by the injected instrumentation.
You should also see metrics produced by the HTTP and runtime instrumentations. The HTTP metrics describe request duration and active requests, while the Go runtime metrics expose details such as goroutine counts, memory allocation, and garbage collection activity.
Runtime metrics are emitted periodically, so they may take longer to appear than traces. Send several requests and wait for the next metric export interval before concluding that metrics are missing.
How otelc instruments a Go build
otelc works by joining the standard Go build process rather than attaching an
agent after the application starts.
The Go toolchain provides a
-toolexec
flag that allows an external command to intercept tool invocations during a
build. When you run otelc go build, otelc uses this mechanism to inspect and
modify packages as they pass through the compiler.
The process has two main phases:
-
During preprocessing,
otelcexamines the dependency graph and selects the instrumentation rules that match the compiled packages. -
During instrumentation,
otelcrewrites the matched code and inserts the hooks required to initialize OpenTelemetry, create spans, record metrics, and propagate context.
The injected hooks are compiled and linked with the rest of the application. The
result is therefore a normal Go executable rather than a binary that depends on
a separate runtime agent. The same mechanism also allows otelc to instrument
supported dependencies and parts of the standard library, not just the code in
your own modules.
Note that compile-time instrumentation avoids a separate runtime agent, but it does not eliminate runtime overhead. The injected code still creates spans, records metrics, propagates context, and exports telemetry, all of which consume CPU, memory, and network capacity.
Pinning instrumentation dependencies
The default otelc go build workflow discovers the required instrumentation
modules for that build without permanently adding them to your application's
go.mod or go.sum.
The dependencies are still compiled into the resulting executable, but this means a scanner that only reads the module files may not see every package shipped in the instrumented binary. For release builds, inspect the binary directly:
1go version -m ./otelc-demo
You can also generate a software bill of materials from the built artifact rather than relying only on the source module files.
To make the selected instrumentation modules explicit, run:
1otelc pin
This creates or updates otel.instrumentation.go, which uses blank imports to
record the instrumentation packages selected for the module. Go can then track
their versions and checksums in go.mod and go.sum.
Because the file has a tools build constraint, it is not included in a normal
application build. It acts as a persistent instrumentation manifest that you can
review and commit when reproducible dependency tracking is required.
Note that the OpenTelemetry project currently describes committed otelc pin
output as a workflow that is still under development.
Security considerations
Because otelc modifies the build process and injects code into the executable,
treat it as part of your trusted toolchain.
Pin the version used in production instead of installing latest:
12go install \go.opentelemetry.io/otelc/tool/cmd/otelc@v1.0.1
For sensitive builds, enable debug output and retain the working directory:
12otelc --debug --work-dir .otelc-build \go build -o otelc-demo .
These files help you review the matched rules, generated sources, and build
diagnostics, although they are not a substitute for reviewing otelc and its
instrumentation packages.
Reproducible builds also require more than pinned application dependencies. Pin
the Go toolchain, otelc, instrumentation packages, and build flags, then build
in a clean environment and compare the hashes of artifacts produced by
equivalent builds.
Supported libraries for compile-time auto-instrumentation
otelc includes instrumentation rules for a focused set of standard-library
packages and widely used Go dependencies. The current built-in instrumentations
include:
| Library or framework | Import path | Instrumented operations |
|---|---|---|
| HTTP | net/http | Client and server requests |
| gRPC | google.golang.org/grpc | Client and server calls |
| SQL databases | database/sql | Database calls |
| Gin | github.com/gin-gonic/gin | Server requests |
| Redis | github.com/redis/go-redis/v9 | Client commands |
| MongoDB | go.mongodb.org/mongo-driver/mongo | Client commands |
| Kafka | github.com/segmentio/kafka-go | Produced and consumed messages |
| OpenAI | github.com/openai/openai-go | Client requests |
| Kubernetes | k8s.io/client-go/tools/cache | Informer cache operations |
| Structured logging | log/slog | Log records |
| Logrus | github.com/sirupsen/logrus | Log records |
| Runtime metrics | Go runtime | Runtime and process metrics |
Support is version-specific, so finding a package in this table does not
necessarily mean that every release of that package can be instrumented. otelc
only applies a rule when the dependency version falls within the range declared
by that instrumentation.
The supported set will continue to change as the project adds rules and extends
existing version ranges. Consult the
official supported-libraries page
before adopting otelc for a particular dependency. It remains the source of
truth for the current libraries, operations, and supported versions.
Configuring the compile-time instrumentation
You can configure compile-time instrumentation at two different stages. Build
options control how otelc modifies the application, while runtime environment
variables control the telemetry produced by the resulting binary.
Configuring the build
otelc accepts its own flags before the go subcommand. The most useful
options are:
-
--debugor-denables diagnostic logging for the instrumentation process. You can also enable it by settingOTELC_DEBUG=1. -
--rules <file>loads additional instrumentation rules from a YAML file. This is useful when you need to instrument a package that is not covered by the built-in rules. -
--work-dir <path>or-w <path>changes whereotelcstores its temporary build files. You can set the same value throughOTELC_WORK_DIR.
The following command enables debug output and loads a custom rules file before building the application:
12otelc --debug --rules custom-rules.yaml \go build -o myapp .
When debugging a failed or incomplete build, start with --debug and inspect
the files in the working directory. They show which packages otelc examined
and which instrumentation rules it applied.
The other primary commands are:
| Command | Purpose |
|---|---|
otelc go ... | Run a Go command with instrumentation applied |
otelc setup | Prepare the environment for the GOFLAGS workflow |
otelc cleanup | Remove artifacts created during setup and builds |
otelc version | Print the installed otelc version |
Configuring OpenTelemetry instrumentation at runtime
Runtime behavior is controlled through the standard OpenTelemetry SDK environment variables, so that service identity, exporter, and resource settings can change without rebuilding the application.
The other thing to configure are what instrumentation modules are active at runtime.
Use OTEL_GO_ENABLED_INSTRUMENTATIONS as an allowlist when you only want
specific modules to run:
1export OTEL_GO_ENABLED_INSTRUMENTATIONS=nethttp,database_sql
And use OTEL_GO_DISABLED_INSTRUMENTATIONS to turn off selected modules while
leaving the rest active:
1export OTEL_GO_DISABLED_INSTRUMENTATIONS=runtimemetrics
All supported instrumentation modules are active by default so these two variables are your only fine-tuning lever.
Troubleshooting compile-time instrumentation
If no telemetry appears, confirm that you built the application with otelc rather than a normal
go build command:
1otelc --debug go build -o otelc-demo .
Then verify that the Collector is running and reachable at the endpoint
configured in OTEL_EXPORTER_OTLP_ENDPOINT:
12docker compose psdocker compose logs collector
You can also enable OpenTelemetry SDK diagnostics when starting the application as this can reveal connection, protocol, and export errors.
1OTEL_LOG_LEVEL=debug ./otelc-demo
If an expected instrumentation is missing, confirm that the application imports a supported library at a compatible version. The supported libraries page lists the current version ranges.
Also check whether the instrumentation was disabled through
OTEL_GO_ENABLED_INSTRUMENTATIONS or OTEL_GO_DISABLED_INSTRUMENTATIONS.
If you updated otelc, changed dependencies, or switched branches, remove old
build artifacts before compiling again:
12otelc cleanupotelc go build -o otelc-demo .
Report bugs through the project's
GitHub issues,
ask usage questions in
GitHub Discussions,
or join the #otel-go-compile-instrumentation channel in
CNCF Slack.
Final thoughts
For a long time, Go was the language where you paid for observability in
boilerplate. otelc removes that tax so that you can change one line in your
build, and get traces, metrics, and log correlation out of the box.
If you're evaluating this for your team, start with a single service that uses
one of the supported instrumentations. Build it with otelc, point it at your
Collector, and see what shows up.
Once you're happy with the coverage, roll it into your CI pipeline so every
release build is instrumented automatically. For spans that otelc can't
generate (business logic, domain events), layer manual SDK spans on top.
Since otelc emits standard OTLP, the telemetry works with any OTel-native
backend. Dash0 is built natively on OpenTelemetry, so
the resource attributes, semantic conventions, and relationships produced by
otelc remain useful throughout the investigation workflow rather than being
reduced to a vendor-specific ingestion format.
For more details, see the official documentation, the v1 announcement, and the project's GitHub repository.


