# Continuous profiling Go without code changes: stripped binaries and eBPF > [!summary] TL;DR > Production Go builds strip the symbol table and DWARF debug info, but .gopclntab survives because the runtime needs it to walk stacks. The opentelemetry-ebpf-profiler reads that table from outside the process and samples on-CPU stacks per OS thread with correct Go function names. No SDK import, no rebuild, no restart; the price is one privileged agent per node, and the OTel profiles signal is Alpha. ## Key points - Production builds strip the ELF symbol table and DWARF debug info. Profilers that resolve names from them get addresses, not names. - .gopclntab maps raw instruction addresses to function names, file names and line numbers. strip leaves it alone because the runtime always needs it: for panics, the garbage collector, and goroutine dumps. - The profiler's docs confirm the data stays present even in static, stripped executables, which is what lets it symbolize and unwind production executables. - Build check with Go 1.27.1 on macOS, cross-compiling for linux/arm64: after go build -ldflags='-s -w', go tool nm reports "no symbol section" and "no symbols", .gopclntab is still listed, and strings finds main.checkout and main.main. - The reader is the opentelemetry-ebpf-profiler, started as the Elastic Universal Profiling Agent. Elastic proposed the donation in January 2024; OpenTelemetry approved it that June. - Run the official Collector distribution otelcol-ebpf-profiler, started with --feature-gates=+service.profilesSupport, from opentelemetry-collector-releases. The same-named test binary in the repo's own cmd/ directory is for internal use only. - The latest tag as of the talk (13 August 2026) is v0.0.202633, which reads as ISO week 33 of 2026. The project publishes tags, not GitHub Releases. - An eBPF program fires on CPU sample events in the kernel and reads process-internal data structures from outside the process. No LD_PRELOAD, no ptrace attach, no injector. README: "100% non-intrusive: there's no need to load agents or libraries into the processes that are being profiled." - Typically run as a DaemonSet with hostPID: true. - The node pays: run the Collector as root, or grant CAP_SYS_ADMIN, CAP_PERFMON, and/or CAP_BPF, plus access to /proc. Current code may require Linux 5.10 or greater. - net/http/pprof needs a blank import and a redeploy to switch on. It still has the edge on profile types and detail, and it needs no kernel privileges. - Statuses as of October 2026 (spec v1.61.0, OTLP 1.11.0): OTel profiles specification Alpha; OTLP wire format Development; Collector receiver Development; service.profilesSupport feature gate Alpha, off by default; traces, metrics and logs Stable. - README: "Implements the Alpha OTel Profiles signal", and its own implementation is "functional but work-in-progress / evolving". Alpha means evolving, not backward-compatibility-guaranteed. - What works today, reliably: on-CPU stacks sampled per OS thread, with correct Go function names via .gopclntab, plus Go pprof labels read by following the thread's current g to its goroutine. Off-CPU profiling is in development; allocation profiling is a design proposal. - Backends that take OTLP profiles: mature production-ready ones have yet to emerge. Pyroscope and devfiler are the open-source options; devfiler is Elastic's desktop viewer for experiments, not a production backend. ## Stripped binaries still carry function names The whole program, with //go:noinline so the compiler can't fold checkout into main: ```go package main import "fmt" //go:noinline func checkout(n int) int { return n * 2 } func main() { fmt.Println(checkout(21)) } ``` Build it for Linux, strip it at link time, and ask what's left (plain readelf -S works too if llvm-readelf isn't on your PATH): ```console $ GOOS=linux GOARCH=arm64 CGO_ENABLED=0 go build -ldflags='-s -w' -o stripped . $ go tool nm stripped reading stripped: no symbol section reading stripped: no symbols $ llvm-readelf -S stripped | grep -E 'symtab|debug|gopclntab' [ 5] .gopclntab PROGBITS 00000000000bbf28 0abf28 097cf1 00 A 0 0 8 $ strings stripped | grep -E '^main\.' main.checkout main.main ``` The symbol table and every .debug_* section are gone, yet .gopclntab is still there and strings can read the function names out of the file. The runtime can't live without it: it needs to walk the call stack at any time, for panics, for the garbage collector, for goroutine dumps, and it does that with this program-counter-to-line-number table. A stripped Go binary can be read from the outside. ## What the opentelemetry-ebpf-profiler is The reader is the opentelemetry-ebpf-profiler. It started as the Elastic Universal Profiling Agent; Elastic proposed the donation in January 2024 and OpenTelemetry approved it that June. Run the official Collector distribution, otelcol-ebpf-profiler, started with --feature-gates=+service.profilesSupport, from opentelemetry-collector-releases; the same-named test binary in the repo's own cmd/ directory is for internal use only. The latest tag as of the talk (13 August 2026) is v0.0.202633, ISO week 33 of 2026, and the project publishes tags, not GitHub Releases. An eBPF program fires on CPU sample events in the kernel and reads process-internal data structures from outside the target process. No LD_PRELOAD, no ptrace attach, no injector ([[resources/documentation/Go/Instrumentation/Go Instrumentation - Why Go Cannot Be Monkey Patched|Why Go Cannot Be Monkey Patched]] covers why that last one is hard for Go). The README states it plainly: "100% non-intrusive: there's no need to load agents or libraries into the processes that are being profiled." You typically run it as a DaemonSet with hostPID: true. ## What zero code changes costs the node The profiled applications need nothing: no rebuild, no restart, no SDK import. The node pays instead. The example config says to run the Collector as root, or to grant specific capabilities (CAP_SYS_ADMIN, CAP_PERFMON, and/or CAP_BPF, plus access to /proc). The README also says current code may require Linux 5.10 or greater. The bill arrives per node, not per service, which is what makes it useful at scale. You still need a backend that takes OTLP profiles, and mature production-ready ones have yet to emerge: Pyroscope and devfiler are the open-source options, and devfiler is Elastic's desktop viewer for experiments, not a production backend. Why not net/http/pprof? It needs a blank import and a redeploy to switch on, which is the change this series avoids. In the author's view it still has the edge on profile types and detail, and it needs no kernel privileges. ## How mature the profiles signal is These are the statuses as of October 2026, from the profiles concepts page and the specification (spec v1.61.0, OTLP 1.11.0): | Layer | Status | | ------- | -------- | | OTel specification (`/docs/specs/otel/profiles/`) | **Alpha** | | OTLP wire format (OTLP 1.11.0) | **Development** | | Collector receiver (`ebpf-profiler`) | **Development** | | Collector feature gate `service.profilesSupport` | **Alpha** (off by default, enable it explicitly) | | Traces / Metrics / Logs (for comparison) | Stable / Stable / Stable | The README is explicit: "Implements the Alpha OTel Profiles signal." It calls its own implementation "functional but work-in-progress / evolving", and Alpha means evolving, not backward-compatibility-guaranteed. What works today, reliably, is on-CPU stacks sampled per OS thread, with correct Go function names via .gopclntab. It also reads Go pprof labels by following the thread's current g to its goroutine, so samples carry whatever labels the application set. The README lists Java, Python and other runtimes it can unwind, though only the Go path was checked. Off-CPU profiling is in development, and allocation profiling is a design proposal. ## Where the fourth signal fits OBI gives traces and RED metrics for HTTP and gRPC from the kernel, otelc gives function-level spans for the price of a rebuild, and the profiler adds always-on CPU profiles for the whole node. Together they cover three of the four signals (traces, metrics, profiles) without a single source change in the application. Logs are only partly covered: OBI can add trace context to application logs (Linux 6.0 or newer), but it enriches them and does not export them. The useful move is to jump from a slow trace to the flame graph for the same time window: traces tell you a request was slow, profiles tell you where the CPU went. Lining them up by request is where zero-touch has a gap. The data model lets samples carry trace and span IDs, but for Go the profiler can only read request context from pprof labels that the application sets, the path OTEP 4947 describes. A compile-time tool could set span-ID labels for the profiler to read, but that is proposed work, and it hasn't been verified that otelc or dd-trace-go do it. Zero-touch profiling alone correlates by resource and time window, not by request; treat trace-to-profile pivoting as where this is heading. Elastic ran the same agent in customer production before donating it, so the capability is real: continuous CPU profiling with correct symbolization of stripped Go binaries. The commands at the top of this note check that any Go binary you ship still has its names; running the profiler itself takes a Linux host and the Collector command in its README, and [[resources/documentation/Go/Instrumentation/Go Instrumentation - OBI and eBPF Profiler Demo Without Kubernetes|OBI and eBPF Profiler Demo Without Kubernetes]] does that in a VM. Disclosure: the author works at Datadog and is one of otelc's maintainers, so read the comparisons with that in mind. Versions, links and commands checked on 2 October 2026. ## FAQ ### Can you profile a stripped Go binary? Yes. .gopclntab survives stripping because the runtime needs it, and it maps instruction addresses to function names, file names and line numbers. After a -ldflags='-s -w' build, strings still finds main.checkout. ### Why does strip leave .gopclntab alone? The runtime needs the program-counter-to-line-number table to walk the call stack for panics, the garbage collector, and goroutine dumps. Since the runtime always needs it, strip leaves it alone. ### How do I check that my binary still has its function names? Build with GOOS=linux GOARCH=arm64 CGO_ENABLED=0 go build -ldflags='-s -w', then go tool nm (reports "no symbols"), llvm-readelf -S (still lists .gopclntab), and strings | grep -E '^main\.' (finds main.* names). Commands are at the top of this note. ### What privileges does the eBPF profiler need? Run the Collector as root, or grant CAP_SYS_ADMIN, CAP_PERFMON, and/or CAP_BPF, plus access to /proc. Current code may require Linux 5.10 or greater. Typically run as a DaemonSet with hostPID: true. ### Is the OTel profiles signal stable? No. The specification is Alpha, the OTLP wire format and Collector receiver are Development, and the service.profilesSupport gate is off by default. Traces, metrics and logs are Stable. ### How does the profiler correlate samples with traces? For Go it can only read request context from pprof labels that the application sets, the path OTEP 4947 describes. Alone, zero-touch profiling correlates by resource and time window, not by request. ### What backend can receive OTLP profiles? Mature production-ready ones have yet to emerge. Pyroscope and devfiler are the open-source options, and devfiler is Elastic's desktop viewer for experiments, not a production backend. ### Why not just use net/http/pprof? It needs a blank import and a redeploy to switch on, the change zero-touch profiling avoids. It still has the edge on profile types and detail, and it needs no kernel privileges. ## Related - [[resources/documentation/Go/Instrumentation/Go Instrumentation|Go Instrumentation]] (series hub) - [[resources/documentation/Go/Instrumentation/Go Instrumentation - OBI eBPF Auto-Instrumentation|OBI eBPF Auto-Instrumentation]] - [[resources/documentation/Go/Instrumentation/Go Instrumentation - otelc Compile-Time Instrumentation|otelc Compile-Time Instrumentation]] - [[resources/documentation/Go/Instrumentation/Go Instrumentation - Flight Recording and USDT|Flight Recording and USDT]] - [[resources/documentation/Go/Instrumentation/Go Instrumentation - OBI and eBPF Profiler Demo Without Kubernetes|OBI and eBPF Profiler Demo Without Kubernetes]] - [[resources/documentation/Go/Go - uretprobes|uretprobes]] - [[resources/documentation/Go/Go - Internals|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/) - [.gopclntab stays present in static, stripped executables (profiler docs, v0.0.202633)](https://github.com/open-telemetry/opentelemetry-ebpf-profiler/blob/v0.0.202633/doc/gopclntab.md) - [opentelemetry-ebpf-profiler repository (v0.0.202633)](https://github.com/open-telemetry/opentelemetry-ebpf-profiler/tree/v0.0.202633) - [Elastic proposes the Universal Profiling Agent donation (community#1918)](https://github.com/open-telemetry/community/issues/1918) - [OpenTelemetry approves the donation (community#1918 comment)](https://github.com/open-telemetry/community/issues/1918#issuecomment-2153125431) - [opentelemetry-ebpf-profiler README (v0.0.202633)](https://github.com/open-telemetry/opentelemetry-ebpf-profiler/blob/v0.0.202633/README.md) - [Example Collector config (local.example.yaml, v0.0.202633)](https://github.com/open-telemetry/opentelemetry-ebpf-profiler/blob/v0.0.202633/cmd/otelcol-ebpf-profiler/local.example.yaml) - [OTel profiles concepts page](https://opentelemetry.io/docs/concepts/signals/profiles/) - [OTel profiles specification](https://opentelemetry.io/docs/specs/otel/profiles/) - [Reading Go pprof labels (go_labels.ebpf.c, v0.0.202633)](https://github.com/open-telemetry/opentelemetry-ebpf-profiler/blob/v0.0.202633/support/ebpf/go_labels.ebpf.c) - [Off-CPU profiling design doc (v0.0.202633)](https://github.com/open-telemetry/opentelemetry-ebpf-profiler/blob/v0.0.202633/design-docs/00001-off-cpu-profiling/README.md) - [Allocation profiling design proposal (v0.0.202633)](https://github.com/open-telemetry/opentelemetry-ebpf-profiler/blob/v0.0.202633/design-docs/00003-memory-profiling/README.md) - [OBI trace-log correlation](https://opentelemetry.io/docs/zero-code/obi/trace-log-correlation/) - [OTEP 4947: thread context, Go support path](https://github.com/open-telemetry/opentelemetry-specification/blob/v1.61.0/oteps/profiles/4947-thread-ctx.md#alternative-for-go-support)