# Instrument Go without changing code: otelc, OBI, eBPF profiler > [!summary] TL;DR > Go builds a self-contained native binary: no bytecode, no classloader, no general runtime hook, so there is no `-javaagent` slot. Zero-touch instrumentation moves to three places: the build (`otelc`, a `-toolexec` wrapper that injects OpenTelemetry spans at compile time), the kernel (OBI, eBPF uprobes and kprobes on a running binary), and whole-node profiling (the `.gopclntab` section read from outside the process). The series covers what each route sees, what it costs, the open gaps, and a Docker Compose demo with no Kubernetes. ## Start here 1. [[resources/documentation/Go/Instrumentation/Go Instrumentation - Why Go Cannot Be Monkey Patched|Why Go cannot be monkey patched]]: Why is there no `-javaagent` for Go, and where can you intervene instead? 2. [[resources/documentation/Go/Instrumentation/Go Instrumentation - otelc Compile-Time Instrumentation|otelc compile-time instrumentation]]: How does one word in front of `go build` inject OpenTelemetry spans? 3. [[resources/documentation/Go/Instrumentation/Go Instrumentation - OBI eBPF Auto-Instrumentation|OBI eBPF auto-instrumentation]]: How does OBI trace a running Go binary with no source change, rebuild or restart? 4. [[resources/documentation/Go/Instrumentation/Go Instrumentation - uprobe vs USDT Cost|uprobe vs USDT cost]]: What does an eBPF probe cost per hit, and why does OBI skip uretprobes on Go? 5. [[resources/documentation/Go/Instrumentation/Go Instrumentation - Flight Recording and USDT|Flight recording and USDT]]: What shipped in Go 1.25's `runtime/trace`, and which httptrace gap breaks HTTP/2 client spans? 6. [[resources/documentation/Go/Instrumentation/Go Instrumentation - Continuous Profiling Without Code Changes|Continuous profiling without code changes]]: How do you profile a stripped production Go binary from outside the process? 7. [[resources/documentation/Go/Instrumentation/Go Instrumentation - Context Propagation Across Goroutines|Context propagation across goroutines]]: What crosses a `go` statement, and what does a goroutine slot repair? 8. [[resources/documentation/Go/Instrumentation/Go Instrumentation - Agent-Actionable Observability|Agent-actionable observability]]: OBI or otelc: which one, when? 9. [[resources/documentation/Go/Instrumentation/Go Instrumentation - OBI and eBPF Profiler Demo Without Kubernetes|OBI and eBPF profiler demo without Kubernetes]]: What does one untouched Go server look like as a trace and as CPU samples? ## Key points - Go compiles to a self-contained static binary: no bytecode to rewrite at load time, no classloader moment, no general runtime hook API. `LD_PRELOAD` needs the dynamic linker, and pure-Go programs link statically by default. - Three places to intervene: build time (a `-toolexec` wrapper in front of every `go tool compile` call), process start (an injector library, with limits), and the kernel (eBPF). - otelc: `otelc go build -o myapp .` is the whole usage change. It needs Go 1.25 or newer and a rebuild; v1.0.1 is the first usable stable release (v1.0.0 was retracted). Default rules at v1.0.1 cover `net/http`, gRPC, `database/sql`, gin, go-redis v9, mongo-driver and Kubernetes client-go, among others. - OBI, the direct successor to Grafana Beyla, attaches eBPF uprobes and kprobes to a running binary: no source changes, no recompile, no restart, no in-process agent. RED metrics and library-level spans for 13 Go libraries at v0.10.0. It needs Linux 5.8+ (RHEL 8: 4.18+), BTF, and a set of Linux capabilities. - Kernel uprobe benchmark (x86_64): a uprobe on a NOP costs about 313 ns per hit, a USDT site 309 ns, a probe on a bare RET 913 ns. OBI skips uretprobes because Go stacks move; it decodes each function and attaches a uprobe at every RET instruction instead. - Go 1.25 shipped a flight recorder in `runtime/trace`: a circular buffer that keeps the last N seconds of execution trace in memory and snapshots on demand (`trace.NewFlightRecorder` with `MinAge` and `MaxBytes`). - An httptrace gap (no `GotResponseEnd` hook) means OTel Go's HTTP/2 client receive spans are never ended; golang/go#75654 proposes the hook. - Production builds strip the symbol table and DWARF debug info, but `.gopclntab` survives because the runtime needs it. The opentelemetry-ebpf-profiler reads it from outside the process: no SDK import, no rebuild, no restart. The price is one privileged agent per node, and the OTel profiles signal is Alpha. - Context propagation across goroutines is unsolved at the language level: Go has no goroutine-local storage, and a goroutine handed `context.Background()` loses the trace ID, the deadline and the cancellation. otelc's goroutine slot repairs the span tree but not the request context; OBI can propagate trace headers from outside (off by default, needs extra privileges) via uprobes on `runtime.newproc1` and BPF maps keyed by goroutine. - Decision rule: the service is already running and cannot be rebuilt, OBI is the default; you control the build and need function-level spans on Go 1.25+, otelc is the default. - Disclosure: the author works at Datadog, which maintains Orchestrion and dd-trace-go, and is one of otelc's maintainers; the author does not contribute to OBI; read the comparisons with that in mind. - Demo without Kubernetes: one Docker Compose stack, five services, one untouched Go server. OBI produced one server span (`GET /orders/{id}`, 11.63 ms) in Jaeger with no rebuild, and the profiler read `main.lookupOrder` off a stripped binary (100 of 106 samples landed there). ## FAQ ### How do I instrument Go without changing code? Two routes: `otelc go build` injects spans at compile time (Go 1.25+, needs a rebuild), or OBI's eBPF probes attach to the already-running binary. The routing question in [[resources/documentation/Go/Instrumentation/Go Instrumentation - Agent-Actionable Observability|Agent-Actionable Observability]] ### Why can Go not be monkey patched like Java? Go compiles to static native code: no bytecode, no classloader, no runtime hook API to intercept. Intervene at build time, process start or in the kernel instead, per [[resources/documentation/Go/Instrumentation/Go Instrumentation - Why Go Cannot Be Monkey Patched|Why Go Cannot Be Monkey Patched]] ### What is otelc? The OpenTelemetry Go compile-time instrumentation tool. Prefix `go build` with `otelc` and get a binary with OTel spans for the libraries it knows, `net/http` and gRPC among them. Go 1.25+ and a rebuild required: [[resources/documentation/Go/Instrumentation/Go Instrumentation - otelc Compile-Time Instrumentation|otelc Compile-Time Instrumentation]] ### What is OBI? OpenTelemetry eBPF Instrumentation, the direct successor to Grafana Beyla. eBPF uprobes and kprobes attach to a running Go binary with no source change, recompile or restart; 13 Go library baselines at v0.10.0: [[resources/documentation/Go/Instrumentation/Go Instrumentation - OBI eBPF Auto-Instrumentation|OBI eBPF Auto-Instrumentation]] ### How much does an eBPF uprobe cost? About 313 ns per hit on a NOP (kernel benchmark, x86_64); a USDT site costs 309 ns and a probe on a bare RET 913 ns. OBI skips uretprobes on Go because Go stacks move: [[resources/documentation/Go/Instrumentation/Go Instrumentation - uprobe vs USDT Cost|uprobe vs USDT Cost]] ### How do I profile a stripped Go binary in production? `.gopclntab` survives stripping because the runtime needs it to walk stacks. The opentelemetry-ebpf-profiler reads it from outside the process; one privileged agent per node, and the OTel profiles signal is still Alpha: [[resources/documentation/Go/Instrumentation/Go Instrumentation - Continuous Profiling Without Code Changes|Continuous Profiling Without Code Changes]] ### How do I propagate trace context across goroutines? Not automatically. Go has no goroutine-local storage, and a goroutine handed `context.Background()` loses the trace ID, the deadline and the cancellation. otelc's slot repairs the span tree; OBI can propagate headers via uprobes on `runtime.newproc1` (off by default, needs extra privileges): [[resources/documentation/Go/Instrumentation/Go Instrumentation - Context Propagation Across Goroutines|Context Propagation Across Goroutines]] ### OBI or otelc: which should I use? Service already running and cannot be rebuilt: OBI. You control the build and want function-level spans on Go 1.25+: otelc. The decision rule in [[resources/documentation/Go/Instrumentation/Go Instrumentation - Agent-Actionable Observability|Agent-Actionable Observability]] ### Can I see OBI run without Kubernetes? Yes. One Docker Compose stack, one untouched Go server: OBI produced a trace in Jaeger and the profiler read function names from a stripped binary. The walkthrough in [[resources/documentation/Go/Instrumentation/Go Instrumentation - OBI and eBPF Profiler Demo Without Kubernetes|OBI and eBPF Profiler Demo Without Kubernetes]] ### Did Go 1.25 add a flight recorder? Yes, in `runtime/trace`: a circular buffer of the last N seconds of execution trace, written on demand. The API and its gaps in [[resources/documentation/Go/Instrumentation/Go Instrumentation - Flight Recording and USDT|Flight Recording and USDT]] ## Related - [[resources/documentation/Go/Go - uretprobes]] - [[resources/documentation/Go/Go - Internals]] ## Sources - [How to Instrument Go Without Changing a Single Line of Code (GopherCon UK 2026, GoLab 2026)](https://kakkoyun.me/talks/instrument-go-without-changing-a-single-line/)