Dash0 Raises $110M Series B at $1B Valuation

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/http routing 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:

bash
12
git clone https://github.com/dash0hq/dash0-examples.git &&
cd dash0-examples/otelc-demo

The project contains the following files:

text
12345678
.
├── main.go
├── go.mod
├── go.sum
├── docker-compose.yml
├── otelcol.yaml
├── Makefile
└── README.md
  • main.go contains the HTTP server, SQLite operations, and structured logging. It does not import the OpenTelemetry SDK or an instrumentation package because otelc adds the required instrumentation while compiling the application.

  • otelcol.yaml defines the local telemetry pipeline.

  • docker-compose.yml runs 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:

bash
12
go mod download
go build -o otelc-demo .

Start the resulting binary:

bash
1
./otelc-demo

The service listens on port 8080. From another terminal, send two requests to confirm that the visit count increases:

bash
12
curl http://localhost:8080/
curl http://localhost:8080/

The second response should report a higher visits value:

json
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:

bash
1
curl http://localhost:8080/fetch

The response includes the result returned by the internal request:

json
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:

bash
12
go 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:

bash
1
otelc version

You should see:

text
1
otelc 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:

yaml
12345678910111213141516171819202122232425262728293031
# otelcol.yaml
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
http:
endpoint: 0.0.0.0:4318
exporters:
debug:
verbosity: detailed
otlp_grpc/otel-desktop-viewer:
endpoint: otel-desktop-viewer:4317
tls:
insecure: true
service:
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:

yaml
123456789101112131415
# docker-compose.yml
services:
collector:
image: otel/opentelemetry-collector-contrib:0.156.0
command: [--config=/etc/otelcol/config.yaml]
volumes:
- ./otelcol.yaml:/etc/otelcol/config.yaml:ro
ports:
- 4317:4317
- 4318:4318
otel-desktop-viewer:
image: ghcr.io/ctrlspice/otel-desktop-viewer:latest
ports:
- 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:

bash
1
docker compose up -d

Then check that both containers are running:

bash
1
docker 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:

bash
1
docker 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:

bash
1
otelc 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:

text
12345678
go: downloading gotest.tools/v3 v3.5.2
...
go: found go.opentelemetry.io/otelc/instrumentation/database/sql in
go.opentelemetry.io/otelc/instrumentation/database/sql
v0.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 log package
  • log/slog
  • the net/http client 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:

bash
1
otelc setup

Then configure go build to invoke otelc through GOFLAGS:

bash
12
export 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:

bash
1234
OTEL_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:

json
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:

bash
12
curl http://localhost:8080/
curl http://localhost:8080/fetch

To watch the Collector receive and forward the telemetry, follow its logs:

bash
1
docker 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

Traces, metrics, and logs from otelc-demo in OTel Desktop Viewer

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.

auto-instrumented Go metrics in OTel Desktop Viewer

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:

  1. During preprocessing,otelc examines the dependency graph and selects the instrumentation rules that match the compiled packages.

  2. During instrumentation, otelc rewrites 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:

bash
1
go 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:

bash
1
otelc 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:

bash
12
go install \
go.opentelemetry.io/otelc/tool/cmd/otelc@v1.0.1

For sensitive builds, enable debug output and retain the working directory:

bash
12
otelc --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 frameworkImport pathInstrumented operations
HTTPnet/httpClient and server requests
gRPCgoogle.golang.org/grpcClient and server calls
SQL databasesdatabase/sqlDatabase calls
Gingithub.com/gin-gonic/ginServer requests
Redisgithub.com/redis/go-redis/v9Client commands
MongoDBgo.mongodb.org/mongo-driver/mongoClient commands
Kafkagithub.com/segmentio/kafka-goProduced and consumed messages
OpenAIgithub.com/openai/openai-goClient requests
Kubernetesk8s.io/client-go/tools/cacheInformer cache operations
Structured logginglog/slogLog records
Logrusgithub.com/sirupsen/logrusLog records
Runtime metricsGo runtimeRuntime 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:

  • --debug or -d enables diagnostic logging for the instrumentation process. You can also enable it by setting OTELC_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 where otelc stores its temporary build files. You can set the same value through OTELC_WORK_DIR.

The following command enables debug output and loads a custom rules file before building the application:

bash
12
otelc --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:

CommandPurpose
otelc go ...Run a Go command with instrumentation applied
otelc setupPrepare the environment for the GOFLAGS workflow
otelc cleanupRemove artifacts created during setup and builds
otelc versionPrint 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:

bash
1
export OTEL_GO_ENABLED_INSTRUMENTATIONS=nethttp,database_sql

And use OTEL_GO_DISABLED_INSTRUMENTATIONS to turn off selected modules while leaving the rest active:

bash
1
export 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:

bash
1
otelc --debug go build -o otelc-demo .

Then verify that the Collector is running and reachable at the endpoint configured in OTEL_EXPORTER_OTLP_ENDPOINT:

bash
12
docker compose ps
docker compose logs collector

You can also enable OpenTelemetry SDK diagnostics when starting the application as this can reveal connection, protocol, and export errors.

bash
1
OTEL_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:

bash
12
otelc cleanup
otelc 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.

Authors
Ayooluwa Isaiah
Ayooluwa Isaiah