Ollygarden's recommended pattern for setting up the OpenTelemetry SDK in Go services using otelconf. Covers project structure, the Providers struct, no-op fallback, runtime attribute injection, and the zap log bridge. Use when adding OTel to a Go project, structuring telemetry code, or reviewing an existing setup — including when DB spans show up as trace roots or GORM/database spans are disconnected from HTTP spans. Triggers on "go otel setup", "go telemetry pattern", "Providers struct go otel", "gorm WithContext", "root client span go".
التثبيت
التثبيت باستخدام Codex أو Claude انسخ هذا Prompt والصقه في Codex أو Claude أو مساعد آخر ليراجع صفحة Skill ويثبّتها لك.
Ollygarden's recommended pattern for setting up the OpenTelemetry SDK in Go services using otelconf. Covers project structure, the Providers struct, no-op fallback, runtime attribute injection, and the zap log bridge. Use when adding OTel to a Go project, structuring telemetry code, or reviewing an existing setup — including when DB spans show up as trace roots or GORM/database spans are disconnected from HTTP spans. Triggers on "go otel setup", "go telemetry pattern", "Providers struct go otel", "gorm WithContext", "root client span go".
Go SDK Setup Conventions
Setup Checklist — verify every item before you finish
Setup is not done when the SDK boots. Each unchecked item below produces a specific
telemetry-quality finding in production; work through all of them.
Thread the request context into the data layer. Every database, HTTP-client, and
queue call on a request path must receive the request's ctx — never context.Background()
and never a bare global handle. With GORM this means db.WithContext(ctx) at every query
site, with the context passed handler → service → model:
// handler: take ctx from the framework request
articles, err := svc.ListArticles(c.Request.Context(), filter)
// service: every method takes ctx as its first parameterfunc(s *Service) ListArticles(ctx context.Context, f Filter) ([]Article, error) {
return s.repo.List(ctx, f)
}
// repo/model: the only layer touching *gorm.DBfunc(r *Repo) List(ctx context.Context, f Filter) ([]Article, error) {
var out []Article
return out, r.db.WithContext(ctx).Where("tag = ?", f.Tag).Find(&out).Error
}
Verify by auditing every request-path DB call: each must receive the request context —
via db.WithContext(ctx), a per-request gorm.Session{Context: ctx}, or database/sql's
QueryContext/ExecContext. A quick spot-check is grep -rn "WithContext" --include='*.go' .
(zero hits on a GORM codebase is a strong signal the context is not threaded), but the grep
alone is not proof — wrappers and reused *gorm.DB handles hide call sites, so walk the
request paths. Without the request context, DB spans become detached CLIENT-kind trace roots
instead of children of the HTTP span (Root Client Span finding). Refactoring existing
ctx-less signatures across layers is part of setup, not optional follow-up.
Never record SQL parameter values — on any signal. Bound values must not appear in
db.query.text, SQL logs, database/sql instrumentation attributes, or custom spans; only
? placeholders are acceptable. With the GORM OTel plugin specifically, pass
tracing.WithoutQueryVariables(). Raw values in any of these leak PII (Critical
PII Leakage finding).
Configure the SDK declaratively, not in code. Exporters, processors, sampling, and
signal wiring live in an external YAML file (configs/otel.yaml) parsed with otelconf —
use the Setup Pattern below, not hand-constructed exporter/provider code. Operators must be
able to change the telemetry setup without recompiling, and the app must fall back to no-op
providers when the file is absent.
Inject service.instance.id (a per-process UUID) alongside service.version, as the
setup pattern below does programmatically (Missing service.instance.id finding).
Keep the resource lean.service.name, service.version, service.instance.id, and
deployment.environment.name — that is the full set. Do not add resource.WithOS(),
resource.WithProcess(), resource.WithHost(), or equivalent detectors: os.* and
process.* resource attributes are discouraged (Discouraged Resource Attribute finding).
Honor the standard OTEL_* environment variables end-to-end.OTEL_EXPORTER_OTLP_*,
OTEL_SERVICE_NAME, and OTEL_RESOURCE_ATTRIBUTES must all take effect at runtime. Do not
invent custom environment variables (DEPLOYMENT_ENVIRONMENT, SERVICE_VERSION, ...) for
values the standard variables already express, and never overwrite an attribute supplied via
OTEL_RESOURCE_ATTRIBUTES with a code-level default — a hardcoded fallback like
deployment.environment.name = "development" silently clobbers the deployment's real
environment and misfiles every signal the service emits.
Required: Verification Report
Setup is not complete until you produce this report. It is a table with one row per
checklist item above. Fill each row with artifacts from THIS run — the marker value you
sent, an excerpt of the exported span dump, a trace id, the config value you changed.
Never a restatement of the requirement, never a bare "done".
The table below is an illustrative example, not a report you can submit: every value in
it is a placeholder showing the expected shape of evidence. Replace every cell with your
own run's artifacts. If you did not run a check, write GAP — not run in that row and
leave it visible — a missing or hand-waved row is itself a finding.
Example (illustrative values — replace every cell with your own run's evidence):
Item
Check performed
Observed evidence
Context threaded to data layer
traced a request end-to-end and inspected the exported spans
DB span is a CHILD of the HTTP server span (same trace id 4bf9…), no parentless CLIENT-kind roots
No SQL parameter values on any signal
ran a query with marker value MARKER_7f3a, inspected the exported DB span, SQL logs, and DB metrics
db.query.text shows only ? placeholders; MARKER_7f3a appears in no span, log, or metric
SDK configured declaratively
changed a value in configs/otel.yaml (e.g. the sampler ratio) and restarted without recompiling; renamed the file to confirm fallback
new sampling behavior took effect from the file alone; with the file absent the app logged the no-op warning and still ran
service.instance.id injected
dumped the exported resource across two process starts
service.instance.id present as a UUID, and it differs between the two boots
Resource kept lean
dumped the exported resource attributes
exactly service.name, service.version, service.instance.id, deployment.environment.name; no os.* or process.* keys
Standard OTEL_* honored
booted with OTEL_SERVICE_NAME and OTEL_RESOURCE_ATTRIBUTES set to non-defaults, and OTEL_EXPORTER_OTLP_ENDPOINT pointed at a marker collector
service.name/deployment.environment.name carry the supplied values on exported telemetry, with no code-level default overwriting them; the marker collector's log / receipt confirms telemetry arrived at the overridden endpoint (the endpoint is a destination, evidenced there, not on the spans)
A row you cannot fill with observed evidence is a visible gap — that item is not done.
Do not delete the row, copy these example values, or write "N/A" to hide it; go run the
check and record what you actually saw.
Recommended import path
For new code, use the root otelconf package (go.opentelemetry.io/contrib/otelconf) — it
tracks the current schema and includes the propagator-from-YAML fix. The schema-pinned
otelconf/v0.3.0 subpackage is for keeping existing configs unchanged.
The core setup reads a YAML config file, injects runtime attributes, and creates an SDK
instance that provides all three providers (tracer, meter, logger) plus a propagator.
No-op fallback: If the config file doesn't exist, the setup returns no-op providers instead of failing. The application runs without telemetry.
Runtime attributes: service.version and service.instance.id are injected programmatically because they vary per deployment, not per environment.
Zap bridge: The otelzap bridge sends structured logs to the OTel LoggerProvider, enabling log correlation with traces. Stdout JSON output is preserved via a tee.
10-second shutdown timeout: Bounds shutdown so a hung exporter cannot block process exit.
Cross-References
Reference: otel-go skill — references/declarative-setup.md for otelconf fetch table, import path facts, schema version mapping; references/breaking-changes.md for SDK/contrib upgrade audits; references/instrumentation-libraries.md for wiring DB/HTTP/gRPC libraries, threading context.Context into the data layer (avoid detached CLIENT-root DB spans), and keeping PII out of db.query.text.
General conventions: ollygarden-otel-declarative-config — anti-patterns and common YAML patterns.