sherlock.go and main.go from Setup, in either variant. Each section adds one thing to them. Every snippet below compiles against the pinned versions on that page.
Environment variables
OTEL_METRICS_EXEMPLAR_FILTER has no effect with the settings-in-the-file variant: it sets the filter in code, and code wins over the environment. The environment variant on the Setup page reads it.
Skip health checks
There is no default ignore list. A filtered request produces neither a span nor a data point, so a health checker that hits/healthz every few seconds does not show up anywhere.
Custom histograms
Record durations in seconds with units, and pass Sherlock’s boundaries so the chart matches the HTTP histograms. durationBoundariesS is the list in sherlock.go.
trace_flags 00.
Exemplar-only attributes
To carry a high-cardinality value on the exemplar but not on the data point, record it as a normal attribute and drop it from the stream with an attribute filter. The SDK moves dropped attributes onto the exemplar. Add the case tosherlockView in sherlock.go; do not add a second view, because two views that match one instrument export it twice.
Outbound HTTP
Wrap the transport. Each call gets a client span, W3C trace context headers for the callee, andhttp.client.request.duration with the same 20 boundaries.
context.Background() starts a new trace.
Manual spans
ctx to everything the span covers, including Record calls and log lines, so they attach to it.
http.route on spans
otelhttp names the span after the mux pattern and puts http.route on the metric, but it does not set http.route on the span. If you filter traces by route, add this middleware inside otelhttp.NewHandler and outside the mux. It runs after routing, while the span is still open.
semconv is go.opentelemetry.io/otel/semconv/v1.37.0.
Sampling
The default samples every trace. To sample a fraction, set the sampler in the environment:ParentBased follows the caller’s decision when a request arrives with trace context, so a trace is sampled whole across services. Metrics count every request regardless. Unsampled requests still produce exemplars, marked trace_flags 00; those exemplars have no trace to open.
Kill switches
Go has no built-in way to turn a signal off at runtime. Two small wrappers give you one. Both keep a flag you set from wherever you like, an admin endpoint or a config watcher. Traces. A sampler wrapper. Wrapped inParentBased, it stops new traces only; requests already being traced finish whole.
metricdata is go.opentelemetry.io/otel/sdk/metric/metricdata.
Logs with trace ids
Neither setup sends logs on its own. Two ways get log lines next to their trace in Sherlock. Both need context-aware calls:slog.InfoContext(r.Context(), "work done") gets the ids, slog.Info("work done") does not.
Send logs over OTLP
Four more modules, all on the pre-stable logs line of the SDK:Setup in sherlock.go, after the meter provider. It uses the same endpoint, headers, and res. The otelslog handler turns every slog record into an OTLP log record, and the SDK stamps trace_id, span_id, and trace_flags from the context.
sdklog is go.opentelemetry.io/otel/sdk/log. The attributes of a slog call become the log’s attributes as they are, so an error logged as slog.ErrorContext(ctx, "boom", "err", err) shows up on the Errors page when the source’s error rule reads LogAttributes['err']. Lines written through the standard log package after slog.SetDefault are exported too, without trace ids.
To keep JSON on stdout as well, fan the record out to two handlers. Handle may modify the record, so give each handler its own r.Clone(), and forward WithAttrs and WithGroup so logger.With(...) keeps the fan-out.
Stamp the ids and ship with a collector
If a collector already reads your stdout or files, keep logging JSON and add the ids yourself. Thisslog.Handler wrapper needs only the trace package. The WithAttrs and WithGroup methods matter: without them the wrapper is lost on the first logger.With(...).
trace_id field is the same lowercase hex Sherlock stores for the span, so the Logs page links each line to its trace.
Histogram boundaries
The settings-in-the-file variant ofsherlock.go gives the two HTTP duration histograms Sherlock’s 20 boundaries in seconds. The environment variant keeps otelhttp’s 14, which skip the steps between 100 and 500 ms, so p95 is coarser there. To add the 20 to the environment variant, replace its traceFlagsOnExemplars with this version, which is the other variant’s sherlockView under another name:
Runtime metrics
The settings-in-the-file variant callsruntime.Start, which gives seven go.* metrics. With the environment variant, add go.opentelemetry.io/contrib/instrumentation/runtime@v0.71.0 to the modules and start it after the meter provider is set:
go.schedule.duration, needs a producer on the reader in either variant:
Baggage
The environment variant propagates trace context only. To forward baggage as well to the services you call:Shutdown
The order in themain.go from Setup matters: stop the HTTP server first, so in-flight requests finish and record their spans and durations, then call the function Setup returned. It flushes both providers and shuts them down under the deadline you pass. Both exports land, including the last one.
Endpoint on a private network
To send through a proxy that needs a differentHost header, WithHeaders does not work: Go’s HTTP client takes the authority from the request, not the header map. Give the exporter a client whose transport sets req.Host.
WithHTTPClient replaces the exporter’s own client, so WithTimeout, WithProxy, and WithTLSClientConfig no longer apply. Set them on your client.
Routers and nested muxes
The tested path isnet/http with the Go 1.22 ServeMux, where otelhttp.NewHandler reads the route from the mux pattern.
Muxes mounted with http.StripPrefix report the mount pattern, not the inner route. StripPrefix hands the inner mux a copy of the request, so the inner pattern never reaches the request otelhttp holds. A service with mux.Handle("/api/v1/auth/", http.StripPrefix("/api/v1/auth", authMux)) gets one span name and one http.route, POST /api/v1/auth/, for every route inside authMux. To keep the inner routes apart, register full patterns on one mux, or mount the inner mux without StripPrefix and register its patterns with the prefix. Other routers need their own contrib middleware to get a route: otelgin for Gin, otelecho for Echo, otelmux for gorilla/mux, and otelchi for chi. sherlockView applies to any histogram named http.server.request.duration, whichever middleware produces it. Check what your middleware emits on the Metrics page after the first export; span-only middlewares leave you without the request duration histogram.
Related topics
Differences from Node.js
Exemplar counts, ignore lists, kill switches, and other traps.
Metrics
Chart a histogram, group by labels, click an exemplar.
Logs
Search log lines and open the trace behind one.
OpenTelemetry SDKs and collectors
Ship logs from files or stdout with a collector.

