Instrument a Go service for traces
The AtherOps agent receives traces over OTLP -- it cannot inject tracing into a compiled Go binary. Go services require manual SDK integration: you import the OpenTelemetry Go SDK, initialize a tracer provider, and instrument the code you want to trace.
If you haven't read Metrics/logs vs APM traces yet, that page explains why detecting a Go process does not automatically produce traces. It also covers an important limitation: the agent detects the Go toolchain (the go process from go run or go build), not compiled Go binaries, which use application-specific names.
Prerequisites
- The AtherOps agent is installed and running on the host.
collection.traces.enabled: trueis set in/etc/atherops/config.yaml(this is the default).- Go 1.21 or later (the OTel Go SDK supports 1.21+).
Step 1: Add the SDK dependencies
go get go.opentelemetry.io/otel \
go.opentelemetry.io/otel/sdk/trace \
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp
Use otlptracehttp for HTTP/protobuf (port 4318) or otlptracegrpc for gRPC (port 4317). HTTP requires no additional system libraries and is usually simpler.
Step 2: Initialize the tracer provider
Add a setup function to your application -- typically called from main before serving traffic:
package main
import (
"context"
"log"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp"
"go.opentelemetry.io/otel/sdk/resource"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
semconv "go.opentelemetry.io/otel/semconv/v1.26.0"
)
func initTracer(ctx context.Context) (func(), error) {
exporter, err := otlptracehttp.New(ctx,
otlptracehttp.WithEndpoint("localhost:4318"),
otlptracehttp.WithInsecure(),
)
if err != nil {
return nil, err
}
tp := sdktrace.NewTracerProvider(
sdktrace.WithBatcher(exporter),
sdktrace.WithResource(resource.NewWithAttributes(
semconv.SchemaURL,
semconv.ServiceName("my-go-service"),
)),
)
otel.SetTracerProvider(tp)
return func() {
if err := tp.Shutdown(ctx); err != nil {
log.Printf("tracer shutdown: %v", err)
}
}, nil
}
func main() {
ctx := context.Background()
shutdown, err := initTracer(ctx)
if err != nil {
log.Fatalf("init tracer: %v", err)
}
defer shutdown()
// your application code here
}
Step 3: Instrument your handlers
With the tracer provider set, create spans around the work you care about:
import "go.opentelemetry.io/otel"
tracer := otel.Tracer("my-go-service")
func handleRequest(ctx context.Context) {
ctx, span := tracer.Start(ctx, "handleRequest")
defer span.End()
// pass ctx through to downstream calls so child spans attach correctly
callDatabase(ctx)
}
For HTTP servers, use the OTel HTTP middleware instead of manual spans:
go get go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp
import "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
mux := http.NewServeMux()
mux.HandleFunc("/", yourHandler)
http.ListenAndServe(":8080", otelhttp.NewHandler(mux, "http.server"))
Step 4: Verify spans are reaching AtherOps
Build and run your service, generate some traffic, then query:
curl -s \
-H "Authorization: Bearer $ATHEROPS_JWT" \
"https://api.atherops.com/query/traces?service=my-go-service&limit=10"
A non-empty JSON array means spans are flowing. See Enable traces: verify spans reached AtherOps for details on the query parameters and how to obtain the bearer token.
Choosing the OTLP endpoint
To use gRPC (port 4317) instead of HTTP, replace the exporter:
go get go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc
import "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
exporter, err := otlptracegrpc.New(ctx,
otlptracegrpc.WithEndpoint("localhost:4317"),
otlptracegrpc.WithInsecure(),
)
Troubleshooting
No spans appear after startup. Check the agent log:
journalctl -u atherops-otel-agent --since "5 min ago" | grep -i error
A forward traces failed line means the agent receives spans but cannot reach AtherOps. No error lines combined with missing spans means the SDK is not initializing or the exporter endpoint is wrong -- add a log.Printf after initTracer to confirm it returns without error, and verify the host and port match what the agent is listening on.
connection refused when the exporter starts.
The OTLP receiver in the agent only starts when collection.traces.enabled: true in /etc/atherops/config.yaml and the collector is running. Verify with:
systemctl status atherops-otel-agent
ss -tlnp | grep 4318
Spans from go test or build tools appear unexpectedly.
The agent detects the go process name, which matches the Go toolchain. Spans here come from your service, not the toolchain -- the toolchain itself is not instrumented. If you see unexpected service names, check that semconv.ServiceName is set correctly in your resource.NewWithAttributes call.
Further reading
- OpenTelemetry Go getting started -- upstream reference including context propagation, baggage, and the full API.
- Metrics/logs vs APM traces -- why the agent requires app-side instrumentation for traces, and the Go detection limitation.
- Enable traces -- full walkthrough including receiver configuration.