Claude Skill

otel-go

OpenTelemetry in Go — SDK setup, API surface, breaking changes, contrib instrumentation libraries (otelhttp, otelgrpc, otelmongo), compile-time zero-code instrumentation (otelc), and performance tuning. Use when adding, reviewing, or configuring OpenTelemetry in a Go service. Tri

LLM Mart · 0 points · 0 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download ollygarden-opentelemetry-agent-skills-skills_otel-go-9671795.zip · 30 KB
Part of ollygarden/opentelemetry-agent-skills — 19 skills

Install

skills CLI npx skills add https://github.com/ollygarden/opentelemetry-agent-skills/tree/main/skills/otel-go
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install ollygarden-opentelemetry-agent-skills@llmmart
Git git clone https://github.com/ollygarden/opentelemetry-agent-skills.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole ollygarden/opentelemetry-agent-skills collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

OpenTelemetry in Go

Entry point for OpenTelemetry mechanics in Go services. Load a reference below based on the task; each reference is self-contained.

References

File Use when
references/declarative-setup.md Configuring the SDK via otelconf and YAML: providers, propagators, shutdown, env-var substitution.
references/api.md Looking up import paths, global API access, tracer/meter/logger usage, attributes, propagation, log bridges (zap, slog).
references/instrumentation-libraries.md Picking or wiring contrib libraries (otelhttp, otelgrpc, database, AWS, message queues, propagators, resource detectors), and writing manual instrumentation that follows semconv.
references/performance.md Tuning sampling, batch processor, metric reader, exporter compression/retry, attribute allocation, log Enabled() short-circuiting, graceful shutdown.
references/breaking-changes.md Auditing existing code for deprecated calls, renamed semantic conventions, and removed APIs across recent SDK / contrib releases.
references/compile-time-instrumentation.md Zero-code, compile-time instrumentation with otelc: usage modes (otelc go build, tool dependency, toolexec drop-in), subcommands, supported libraries, rule sources/precedence, and pinning via otel.instrumentation.go.

For upgrade reviews, always finish with a safe local verification path (go mod tidy -diff, go build ./..., and go test ./...). Test exporter URL or retry changes against a disposable local receiver, never a deployment endpoint.

Module versioning — read before adding dependencies

opentelemetry-go is split into independently versioned module groups. They do NOT share one version number. Assuming they do is the most common cause of broken builds and version churn:

Module group Example modules Version line
Stable (traces, metrics, logs API/SDK) go.opentelemetry.io/otel, otel/sdk, otel/trace, otel/metric, otel/log, otel/sdk/log, OTLP trace/metric exporters v1.x (e.g. v1.47.0; logs API/SDK joined at v1.47.0)
Log exporters and test helpers otel/exporters/otlp/otlplog/otlploghttp, .../otlploggrpc, otel/exporters/stdout/stdoutlog, otel/log/logtest, otel/sdk/log/logtest v0.x (separate, lower line, e.g. v0.23.0)
Contrib instrumentation contrib/instrumentation/net/http/otelhttp, .../otelgrpc v0.x (separate line, e.g. v0.72.0)
Contrib log bridges contrib/bridges/otelslog, otelzap, otellogrus, otellogr v0.x

The trap: pinning every module to the core version (e.g. go get go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp@v1.47.0) fails — log exporter, log test-helper, and bridge modules have no v1.x tag. Hand-picking and re-guessing each @vX is the churn to avoid.

Do this instead — add each module with @latest and let Go resolve a compatible set:

go get go.opentelemetry.io/otel@latest go.opentelemetry.io/otel/sdk@latest
# logs (API/SDK are v1.x; log exporters stay on a v0.x line — do NOT force the core version):
go get go.opentelemetry.io/otel/log@latest go.opentelemetry.io/otel/sdk/log@latest \
       go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp@latest
# contrib (instrumentation and bridges each resolve to their own v0.x line):
go get go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp@latest \
       go.opentelemetry.io/contrib/bridges/otelslog@latest
go mod tidy && go build ./...

If exact versions are required, fetch each module group's tag from its own source (see below) — never infer one group's version from another's.

Sources of Truth

For YAML schema details, fetch the upstream sources listed in the otel-declarative-config skill. For Go-specific facts:

Fact Fetch
Latest go.opentelemetry.io/otel core release gh api repos/open-telemetry/opentelemetry-go/releases/latest -q '.tag_name'
Latest go.opentelemetry.io/contrib release gh api repos/open-telemetry/opentelemetry-go-contrib/releases/latest -q '.tag_name'
Latest otelconf module tag the version-sorted tag command below the table
Latest Go semconv package Search the selected core release's CHANGELOG for Add the go.opentelemetry.io/otel/semconv/; do not infer package availability from the upstream semantic-conventions release. Offline: semconv/v1.43.0 in otel v1.47.0, verified 2026-10-15 (record: the otel-semantic-conventions skill's references/sdk-semconv-packages.md).
otel-go CHANGELOG WebFetch https://raw.githubusercontent.com/open-telemetry/opentelemetry-go/main/CHANGELOG.md
otel-go-contrib CHANGELOG WebFetch https://raw.githubusercontent.com/open-telemetry/opentelemetry-go-contrib/main/CHANGELOG.md
gh api repos/open-telemetry/opentelemetry-go-contrib/git/matching-refs/tags/otelconf -q '.[].ref' | sort -V | tail -1

Cross-References

  • Schema-level facts: otel-declarative-config skill (language-agnostic YAML schema sources).
  • SDK version selection across languages: otel-sdk-versions skill.
  • Semantic conventions lookup: otel-semantic-conventions skill.
Files (opentelemetry-agent-skills)
  • evals
    • files
      • go-v145-upgrade.md 856 B
        # OpenTelemetry Go upgrade review
        
        The service is upgrading from core OpenTelemetry Go v1.44.0 and log API/SDK v0.20.0 to
        core v1.45.0 and log API/SDK v0.21.0. It currently emits a log record like this:
        
        ```go
        var rec log.Record
        rec.SetBody(log.StringValue("checkout failed"))
        rec.AddAttributes(log.String("component", "checkout"))
        logger.Emit(ctx, rec)
        ```
        
        Its trace exporter is configured as follows, and the Collector expects traces at
        `http://otel-gateway:4318/v1/traces`:
        
        ```go
        exporter, err := otlptracehttp.New(ctx,
            otlptracehttp.WithEndpointURL("http://otel-gateway:4318"),
        )
        ```
        
        The Collector may return either `Retry-After: 5` or an HTTP-date value while throttling.
        The team needs to know whether the v1.45.0 exporter honors both forms and how that differs
        from v1.44.0. This is a static review only: do not contact the example endpoint.
        
    • evals.json 1.3 KB
      {
        "skill": "otel-go",
        "runtime": "Codex CLI 0.147.0",
        "model": "gpt-5.6-sol",
        "cases": [
          {
            "id": "go-v145-upgrade-review",
            "prompt": "Review skills/otel-go/evals/files/go-v145-upgrade.md and give the exact code/configuration changes needed for the OpenTelemetry Go v1.45.0 upgrade. Explain each behavior change and distinguish it from v1.44.0. Do not modify files or contact an endpoint.",
            "files": [
              "skills/otel-go/evals/files/go-v145-upgrade.md"
            ],
            "expectations": [
              "Replaces log.StringValue and log.String with attribute.StringValue and attribute.String, and identifies the log API/SDK v0.21.0 migration from log.Value/log.KeyValue to attribute.Value/attribute.KeyValue.",
              "States that pathless otlptracehttp.WithEndpointURL uses the root path / in v1.45.0 and explicitly includes /v1/traces in the configured URL (or uses WithURLPath) to preserve the intended OTLP trace route.",
              "States that v1.45.0 HTTP exporters interpret integer Retry-After values as seconds and support HTTP-date values, while v1.44.0 mistakenly treated integers as nanoseconds and ignored HTTP dates.",
              "Does not claim that the code was compiled, an exporter contacted, or live behavior validated; proposes a safe local compile or test path instead."
            ]
          }
        ]
      }
      
    • results.md 2.9 KB
      # `go-v145-upgrade-review` harness results
      
      Run on 2026-08-14 with Codex CLI 0.147.0 driving `gpt-5.6-sol` through ChatGPT
      authentication. Every retained repetition used a fresh isolated `HOME` and `CODEX_HOME`,
      read-only sandbox, identical prompt and tool access, and a five-minute orchestration ceiling.
      Codex reported no marginal API billing.
      
      The deterministic grader marked a repetition passing only when all four expectations in
      `evals.json` were present in its final answer. The selected repetitions are the earliest valid
      fresh repetitions for each arm and were not chosen by outcome.
      
      | Arm | Selected attempts | Pass | Fail | Unknown |
      |---|---|---:|---:|---:|
      | Target skill withheld | `withheld-4`, `withheld-5`, `withheld-6` | 0 | 3 | 0 |
      | Current `origin/main` skill | `current-1`, `current-2`, `current-5` | 1 | 2 | 0 |
      | Proposed skill | `proposed-14`, `proposed-15`, `proposed-16` | 3 | 0 | 0 |
      
      ## Per-attempt grading
      
      `A` is the log `attribute.Value` migration, `B` is the explicit OTLP HTTP trace path,
      `C` is corrected `Retry-After` behavior, and `D` is safe local verification.
      
      | Attempt | A | B | C | D | Result |
      |---|---|---|---|---|---|
      | `withheld-4` | pass | pass | pass | fail | fail |
      | `withheld-5` | pass | pass | pass | fail | fail |
      | `withheld-6` | pass | pass | pass | fail | fail |
      | `current-1` | pass | pass | pass | pass | pass |
      | `current-2` | pass | pass | pass | fail | fail |
      | `current-5` | pass | pass | pass | fail | fail |
      | `proposed-14` | pass | pass | pass | pass | pass |
      | `proposed-15` | pass | pass | pass | pass | pass |
      | `proposed-16` | pass | pass | pass | pass | pass |
      
      The three factual upgrade expectations were saturated in every arm. The measurable lift was
      safe verification: the proposed skill consistently concluded with `go mod tidy -diff`,
      `go build ./...`, `go test ./...`, and a disposable local receiver for exporter behavior.
      
      ## Preserved invalid and superseded attempts
      
      - `withheld-1`: unknown; the JSONL transcript ended before a final answer.
      - `withheld-2`: invalid; harness initialization failed before a model turn due temporary quota.
      - `current-3`: invalid; no final answer was retained.
      - `current-4`: invalid; harness initialization failed before a model turn due temporary quota.
      - `proposed-2`, `proposed-11`, and `proposed-13`: invalid; temporary quota interrupted output before a final answer.
      - `proposed-1`, `proposed-3`, `proposed-4`, `proposed-6`, `proposed-7`, `proposed-8`,
        `proposed-9`, `proposed-10`, and `proposed-12`:
        valid results against superseded proposed revisions, retained during iteration but excluded
        because they do not describe the final head.
      - `proposed-5`: invalidated because the revised skill copy was not loaded.
      
      Raw harness transcripts contained local absolute paths and runtime internals, so this sanitized
      public summary replaces them. The final answers were reviewed to confirm that they contained no
      credentials, customer data, or private repository content.
      
  • references
    • api.md 10.2 KB
      # OpenTelemetry Go API
      
      **Module**: `go.opentelemetry.io/otel`
      
      ## Import Paths
      ```go
      import (
          "go.opentelemetry.io/otel"                    // Global API
          "go.opentelemetry.io/otel/trace"              // Tracing API
          "go.opentelemetry.io/otel/metric"             // Metrics API
          "go.opentelemetry.io/otel/log"                // Logs API (stable v1.x since v1.47.0)
          "go.opentelemetry.io/otel/attribute"          // Attributes
          "go.opentelemetry.io/otel/codes"              // Status codes
          "go.opentelemetry.io/otel/propagation"        // Context propagation
          "go.opentelemetry.io/otel/baggage"            // Baggage API
      )
      ```
      
      ## Global API Access
      
      ### Provider Registration
      ```go
      // Set global providers (typically in main())
      otel.SetTracerProvider(tracerProvider)
      otel.SetMeterProvider(meterProvider)
      otel.SetLoggerProvider(loggerProvider) // v1.47.0+; replaces deprecated otel/log/global
      otel.SetTextMapPropagator(propagator)
      
      // Get providers from global
      tp := otel.GetTracerProvider()
      mp := otel.GetMeterProvider()
      lp := otel.GetLoggerProvider()
      ```
      
      ### Convenience Accessors
      ```go
      // Get a tracer from the global TracerProvider
      tracer := otel.Tracer("github.com/user/pkg")
      
      // Get a meter from the global MeterProvider
      meter := otel.Meter("github.com/user/pkg")
      
      // Get a logger from the global LoggerProvider (v1.47.0+)
      logger := otel.Logger("github.com/user/pkg")
      ```
      
      ### Error Handling
      ```go
      // Set global error handler
      otel.SetErrorHandler(otel.ErrorHandlerFunc(func(err error) {
          // Log or handle errors
      }))
      ```
      
      ## Tracing API
      
      ### Core Types
      ```go
      trace.TracerProvider    // Creates Tracers
      trace.Tracer           // Creates Spans
      trace.Span             // Represents a unit of work
      trace.SpanContext      // Immutable span identifier
      trace.TraceID          // [16]byte unique trace identifier
      trace.SpanID           // [8]byte unique span identifier
      ```
      
      ### Creating Tracers and Spans
      ```go
      // Get tracer (use import path as name)
      tracer := otel.Tracer("github.com/user/pkg")
      
      // Start span with automatic context propagation
      ctx, span := tracer.Start(ctx, "verb object",
          trace.WithAttributes(
              attribute.String("key", "value"),
          ),
          trace.WithSpanKind(trace.SpanKindServer),
      )
      defer span.End()
      
      // Add attributes during execution
      span.SetAttributes(attribute.Int64("items.count", 42))
      
      // Set status on error
      if err != nil {
          span.SetStatus(codes.Error, err.Error())
      }
      ```
      
      ### Span Options
      ```go
      trace.WithAttributes(attrs...)      // Initial attributes
      trace.WithSpanKind(kind)            // Server, Client, Producer, Consumer, Internal
      trace.WithNewRoot()                 // Start new trace
      trace.WithLinks(links...)           // Link to other spans
      ```
      
      ### Context Utilities
      ```go
      // Extract span from context
      span := trace.SpanFromContext(ctx)
      
      // Get current span context
      sc := span.SpanContext()
      traceID := sc.TraceID()
      spanID := sc.SpanID()
      ```
      
      ## Metrics API
      
      ### Core Types
      ```go
      metric.MeterProvider    // Creates Meters
      metric.Meter           // Creates Instruments
      // Synchronous instruments
      metric.Int64Counter
      metric.Float64Counter
      metric.Int64UpDownCounter
      metric.Float64UpDownCounter
      metric.Int64Histogram
      metric.Float64Histogram
      metric.Int64Gauge
      metric.Float64Gauge
      // Asynchronous instruments (callbacks)
      metric.Int64ObservableCounter
      metric.Float64ObservableCounter
      metric.Int64ObservableUpDownCounter
      metric.Float64ObservableUpDownCounter
      metric.Int64ObservableGauge
      metric.Float64ObservableGauge
      ```
      
      ### Creating Meters and Instruments
      ```go
      // Get meter (use import path as name)
      meter := otel.Meter("github.com/user/pkg")
      
      // Counter - monotonic sum
      counter, _ := meter.Int64Counter("requests.total",
          metric.WithDescription("Total requests"),
          metric.WithUnit("1"),
      )
      counter.Add(ctx, 1,
          metric.WithAttributes(attribute.String("method", "GET")))
      
      // UpDownCounter - non-monotonic sum
      updown, _ := meter.Int64UpDownCounter("queue.size")
      updown.Add(ctx, -1)
      
      // Histogram - distribution of values
      histogram, _ := meter.Float64Histogram("request.duration",
          metric.WithUnit("ms"),
      )
      histogram.Record(ctx, 123.45)
      
      // Gauge - current value
      gauge, _ := meter.Float64Gauge("cpu.usage",
          metric.WithUnit("%"),
      )
      gauge.Record(ctx, 75.5)
      
      // Observable (async) gauge with callback
      _, _ = meter.Int64ObservableGauge("memory.usage",
          metric.WithCallback(func(ctx context.Context, o metric.Int64Observer) error {
              o.Observe(getCurrentMemory())
              return nil
          }),
      )
      
      // Check if an instrument is enabled before expensive operations (v1.40.0+)
      // Available on all synchronous instruments: Counter, UpDownCounter, Histogram, Gauge
      if counter.Enabled(ctx) {
          // Only compute expensive value if the instrument will record
          value := computeExpensiveMetric()
          counter.Add(ctx, value, metric.WithAttributes(attribute.String("method", "GET")))
      }
      ```
      
      ## Attributes
      
      ### Creating Attributes
      ```go
      // Typed attribute constructors
      attribute.String("key", "value")
      attribute.Int64("count", 42)
      attribute.Float64("ratio", 0.95)
      attribute.Bool("enabled", true)
      attribute.StringSlice("tags", []string{"a", "b"})
      attribute.ByteSlice("payload", []byte("data")) // v1.44.0+
      attribute.Int64Slice("ids", []int64{1, 2, 3})
      attribute.Float64Slice("values", []float64{1.1, 2.2})
      attribute.BoolSlice("flags", []bool{true, false})
      attribute.Slice("nested", attribute.StringValue("a"), attribute.Int64Value(1)) // v1.44.0+
      attribute.Map("labels", attribute.String("region", "eu")) // v1.45.0+
      ```
      
      ### Attribute Sets
      ```go
      // Create reusable attribute set
      attrs := attribute.NewSet(
          attribute.String("service.name", "api"),
          attribute.String("service.version", "1.0.0"),
      )
      
      // Filter attributes
      filtered, excluded := attrs.Filter(func(kv attribute.KeyValue) bool {
          return kv.Key != "service.version"
      })
      _ = excluded // attributes removed by the filter
      ```
      
      For incremental filtering or deduplication, `attribute.NewHasher()` computes the same
      `Distinct` identifier as `Set.Equivalent()` without first constructing a set. Supply attributes
      to `Write` in ascending key order with no duplicates (retain the last value for duplicate keys),
      call `Distinct`, and use `Reset` before reusing the hasher. Its zero value is not usable.
      
      ## Propagation
      
      ### Context Propagation
      ```go
      // Create composite propagator
      propagator := propagation.NewCompositeTextMapPropagator(
          propagation.TraceContext{},  // W3C Trace Context
      )
      
      // Set global propagator
      otel.SetTextMapPropagator(propagator)
      
      // Extract from HTTP headers
      ctx = propagator.Extract(ctx, propagation.HeaderCarrier(r.Header))
      
      // Inject into HTTP headers
      propagator.Inject(ctx, propagation.HeaderCarrier(w.Header()))
      ```
      
      ## Logs API
      
      As of core v1.47.0 (2026-10-02), `otel/log` and `otel/sdk/log` are **stable** and share the
      v1.x line with traces and metrics. The OTLP/stdout log exporters and the `logtest` helpers
      remain on a separate v0.x line (see the module-versioning table in SKILL.md). The Logs API
      primarily provides a bridge for existing logging libraries.
      
      `go.opentelemetry.io/otel/log/global` is deprecated since v1.47.0; use `otel.Logger`,
      `otel.GetLoggerProvider`, and `otel.SetLoggerProvider` instead. The contrib log bridges
      (v0.21.0+, contrib v1.47.0) default to `otel.GetLoggerProvider()`.
      
      ### Core Types
      ```go
      log.LoggerProvider     // Creates Loggers
      log.Logger             // Emits log records
      log.Record             // Log record with attributes
      attribute.Value        // Log body value (v0.21.0+)
      log.Severity           // Log severity level
      ```
      
      ### Creating Loggers and Emitting Records
      ```go
      // Get logger (use import path as name)
      logger := otel.Logger("github.com/user/pkg")
      
      // Optionally skip expensive record construction. Do not cache the result: it can change.
      // EnabledParameters is a plain struct; set fields directly.
      params := log.EnabledParameters{Severity: log.SeverityInfo}
      if logger.Enabled(ctx, params) {
          var rec log.Record
          rec.SetTimestamp(time.Now())
          rec.SetSeverity(log.SeverityInfo)
          rec.SetBody(attribute.StringValue("User logged in"))
          rec.AddAttributes(
              attribute.String("user.id", userID),
              attribute.String("session.id", sessionID),
          )
          logger.Emit(ctx, rec)
      }
      
      // Attach an error to a log record (otel/log v0.18.0+; released with core v1.42.0)
      // The SDK automatically sets exception attributes (exception.type, exception.message)
      var rec log.Record
      rec.SetSeverity(log.SeverityError)
      rec.SetBody(attribute.StringValue("operation failed"))
      rec.SetErr(err) // Attaches error; SDK sets exception attributes automatically
      logger.Emit(ctx, rec)
      
      // Read back the error from a record
      if recordErr := rec.Err(); recordErr != nil {
          // Handle the error
      }
      ```
      
      ### Severity Levels
      ```go
      log.SeverityTrace1 through log.SeverityTrace4
      log.SeverityDebug1 through log.SeverityDebug4
      log.SeverityInfo1 through log.SeverityInfo4
      log.SeverityWarn1 through log.SeverityWarn4
      log.SeverityError1 through log.SeverityError4
      log.SeverityFatal1 through log.SeverityFatal4
      ```
      
      ### Log Values
      ```go
      attribute.StringValue("text")
      attribute.IntValue(42)
      attribute.Int64Value(int64(42))
      attribute.Float64Value(3.14)
      attribute.BoolValue(true)
      attribute.ByteSliceValue([]byte("data"))
      attribute.SliceValue(attribute.StringValue("a"), attribute.StringValue("b"))
      attribute.MapValue(attribute.String("key", "value"))
      ```
      
      ## Logging Bridges
      
      Logging bridges connect existing logging libraries to OpenTelemetry.
      
      > **Note (bridge v0.10.0):** otelzap and otelslog emit `code.function` with the full package path-qualified function name (e.g., `github.com/user/pkg.MyFunc`). The `code.namespace` attribute is no longer emitted.
      
      ```go
      import (
          // For zap logging
          "go.opentelemetry.io/contrib/bridges/otelzap"
      
          // For slog logging (Go 1.21+)
          "go.opentelemetry.io/contrib/bridges/otelslog"
      
          // For logrus logging
          "go.opentelemetry.io/contrib/bridges/otellogrus"
      
          // For logr logging
          "go.opentelemetry.io/contrib/bridges/otellogr"
      )
      ```
      
      ### Example: zap bridge
      ```go
      // Create zap logger with OpenTelemetry bridge
      core := zapcore.NewTee(
          zapcore.NewCore(encoder, os.Stdout, zapcore.InfoLevel),
          otelzap.NewCore("my-service", otelzap.WithLoggerProvider(loggerProvider)),
      )
      logger := zap.New(core)
      ```
      
      ### Example: slog bridge
      ```go
      // Create slog handler with OpenTelemetry bridge
      handler := otelslog.NewHandler("my-service", otelslog.WithLoggerProvider(loggerProvider))
      logger := slog.New(handler)
      ```
      
    • breaking-changes.md 9.2 KB
      # Go OpenTelemetry Breaking Changes & Gotchas
      
      Audit reference for upgrading existing Go OpenTelemetry code. For current SDK/contrib
      version selection, fetch from the Sources of Truth table in the `otel-go` skill (or
      `go.opentelemetry.io/otel` and `go.opentelemetry.io/contrib` release tags directly).
      
      After applying an upgrade, verify it locally with `go mod tidy -diff`, `go build ./...`,
      and `go test ./...`. For exporter URL or retry behavior, use an `httptest.Server` or
      another disposable local receiver; never probe a deployment endpoint merely to confirm
      the SDK migration.
      
      ## API Deprecations
      
      - `go.opentelemetry.io/contrib/config` was removed in contrib v1.35.0. Use `go.opentelemetry.io/contrib/otelconf` instead.
      - `WithMetricAttributesFn` deprecated in otelhttp v0.66.0 (contrib v1.41.0 release). Use `Labeler` instead.
      - otelgrpc's deprecated client/server interceptors were removed in v0.60.0. Use stats handlers (`NewServerHandler` / `NewClientHandler`).
      - `attribute.INVALID` deprecated (core v1.43.0) — an empty value is now valid; use `attribute.EMPTY` instead.
      - `attribute.Value.Emit` deprecated (core v1.44.0). Use `attribute.Value.String` instead.
      - `sdk/log.WithExportBufferSize` is deprecated and has no effect as of log SDK v0.21.0 (core v1.45.0 release); the batch processor no longer keeps a separate export-request buffer.
      - `OTEL_EXPERIMENTAL_CONFIG_FILE` is no longer supported by root `otelconf` v0.23.0+ (contrib v1.43.0+). Use `OTEL_CONFIG_FILE`.
      - `go.opentelemetry.io/contrib/instrumentation/github.com/labstack/echo/otelecho` was deprecated in v0.71.0 (contrib v1.46.0) and removed in v0.72.0 (contrib v1.47.0). Use `github.com/labstack/echo-opentelemetry`.
      - `go.opentelemetry.io/otel/log/global` is deprecated (core v1.47.0). Use `otel.Logger`, `otel.GetLoggerProvider`, and `otel.SetLoggerProvider` from `go.opentelemetry.io/otel`.
      - Deprecated as no longer supported in contrib v1.47.0, with no contrib replacement: `instrumentation/github.com/aws/aws-sdk-go-v2/otelaws` (v0.72.0), `propagators/aws`, and `samplers/probability/consistent`.
      - otelhttp v0.72.0 deprecates `ReadBytesKey`, `ReadErrorKey`, `WroteBytesKey`, and `WriteErrorKey`; `WithMessageEvents` no longer emits `http.read_bytes` / `http.wrote_bytes`. Total body sizes remain on the server span as `http.request.body.size` / `http.response.body.size`.
      
      ## Removed APIs (instrumentation v0.65.0; contrib v1.40.0 release)
      
      - otelhttp removed `DefaultClient`, `Get`, `Head`, `Post`, `PostForm`, `WithPublicEndpoint`, `WithRouteTag`. Always create a custom client with `otelhttp.NewTransport`.
      - HTTP instrumentation sets the `error.type` attribute instead of `exception` span events.
      
      ## Semantic Convention Renames (instrumentation v0.65.0; contrib v1.40.0 release)
      
      RPC attribute changes:
      
      - `rpc.system` → `rpc.system.name`
      - `rpc.method` + `rpc.service` merged into `rpc.method`
      - `rpc.client.duration` / `rpc.server.duration` → `rpc.client.call.duration` / `rpc.server.call.duration` (unit changed to seconds)
      - `rpc.grpc.status_code` → `rpc.response.status_code`
      
      ## otelgrpc v0.67.0 (contrib v1.42.0 release)
      
      - No longer emits `rpc.message` span events, `rpc.*.request/response.size` metrics, or `network.*` attributes — even with `WithMessageEvents`.
      
      ## Metric SDK cardinality limit (core v1.44.0)
      
      - `sdk/metric` now enforces a **default cardinality limit of 2000** series per instrument. Series past the limit are dropped into an overflow series (`otel.metric.overflow=true`). Breaking change from the previous unlimited default. Restore unlimited with `sdkmetric.WithCardinalityLimit(0)`; the `OTEL_GO_X_CARDINALITY_LIMIT` env var is deprecated. Per-instrument-kind limits: `sdkmetric.WithCardinalityLimitSelector` (v1.43.0).
      
      ## OTLP exporter request-size cap (core v1.44.0)
      
      - All OTLP exporters cap requests at **64 MiB** by default (applied before compression); oversized requests become non-retryable errors. Configure with `WithMaxRequestSize`.
      
      ## Baggage limits (core v1.44.0)
      
      - The 8192-byte baggage size limit is now enforced during extraction/parsing (`otel/baggage`, `otel/propagation`): baggage strings over the limit are rejected outright, while malformed members are skipped — valid members are retained and an error is reported.
      
      ## Log value API replacement (log API/SDK v0.21.0; core v1.45.0 release)
      
      - Log bodies and record attributes now use `go.opentelemetry.io/otel/attribute.Value` and `attribute.KeyValue`. The former `log.Kind`, `log.Value`, `log.KeyValue`, their constructors, and conversion helpers were removed. Replace `log.StringValue(...)` / `log.String(...)` with `attribute.StringValue(...)` / `attribute.String(...)` (and the corresponding typed constructors).
      - `sdk/log/logtest.RecordFactory` no longer has `AttributeValueLengthLimit` or `AttributeCountLimit`; test records now keep attribute limits disabled.
      
      ## OTLP HTTP endpoint URLs (core v1.45.0)
      
      - `otlptracehttp.WithEndpointURL` and `otlpmetrichttp.WithEndpointURL` no longer append `/v1/traces` or `/v1/metrics` when the URL has no path. They now use `/`, matching signal-specific endpoint environment variables and the log exporter. Join the signal path explicitly to preserve the old behavior.
      
      ## Core v1.47.0 (logs API/SDK v1.47.0; log exporters v0.23.0)
      
      - `go.opentelemetry.io/otel/log` and `go.opentelemetry.io/otel/sdk/log` are stable and now tagged on the v1.x line (`@v1.47.0`); the OTLP/stdout log exporters and `logtest` modules continue on v0.x (v0.23.0).
      - Requires Go 1.26 or later (Go 1.25 support dropped in core and contrib v1.47.0).
      - Nested attribute values are capped at depth 64 by default (tuning: [performance.md](performance.md#span-attribute-limits)).
      - The experimental `OTEL_GO_X_METRIC_EXPORT_BATCH_SIZE` variable was removed; use `sdkmetric.WithMaxExportBatchSize` on the `PeriodicReader`.
      - Empty `OTEL_TRACES_SAMPLER` / `OTEL_TRACES_SAMPLER_ARG` values are treated as unset.
      - gRPC OTLP exporters ignore HTTP URL paths when deriving targets from `OTEL_EXPORTER_OTLP_ENDPOINT` / signal-specific endpoint variables.
      
      ## otelhttp client metrics labeler (instrumentation v0.72.0; contrib v1.47.0 release)
      
      - Client metrics no longer include attributes from the server-side `Labeler`. Code that used `ContextWithLabeler` to add custom *client* metric attributes must switch to `ContextWithClientLabeler` (read back with `ClientLabelerFromContext`). Server-side `ContextWithLabeler` / `LabelerFromContext` is unchanged.
      
      ## Log exporter shutdown (log SDK v0.22.0; core v1.46.0 release)
      
      - `sdk/log` adds `ErrExporterShutdown`. The built-in stdout and OTLP log exporters now return it when `Export` is called after `Shutdown`; custom exporters should do the same. Calls to `Shutdown` or `ForceFlush` after shutdown remain no-ops that return `nil`.
      
      ## HTTP instrumentation behaviour (instrumentation v0.69.0; contrib v1.44.0 release)
      
      - Unknown or empty HTTP methods now report `_OTHER` instead of `GET` across all HTTP instrumentations (otelhttp, otelmux).
      - The default server span name is now `{method} {route}` (e.g. `GET /foo/{id}`) when a route is available, or `{method}` otherwise — conforming to HTTP semconv.
      
      In otelhttp v0.70.0 (contrib v1.45.0), client spans and request metrics derive `network.protocol.name` / `network.protocol.version` from the negotiated response protocol rather than the request's initial `Proto` value.
      
      ## Removed / renamed contrib APIs (v1.43.0–v1.44.0 release lines)
      
      - otelgrpc removed the deprecated `WithSpanOptions` option in v0.69.0.
      - `otelconf`: experimental config types moved from `otelconf` to the `otelconf/x` subpackage in v0.23.0. The `host` resource detector no longer sets `host.id` in v0.23.0.
      - otelgrpc added `OTEL_SEMCONV_STABILITY_OPT_IN` (values `rpc` default, `rpc/dup`, `rpc/old`) in v0.68.0 to stage RPC semconv migration.
      - Contrib v1.45.0 updates instrumentation and detectors to semconv v1.43.0; use the exact versioned semconv package and its generated migration file rather than mixing helpers from older versions.
      
      ## Log bridges attach errors via SetErr (bridge v0.18.0–v0.19.0)
      
      - otelslog, otelzap, otellogrus, and otellogr now record fields implementing `error` (e.g. `zap.Error`, `slog` error values) via `log.Record.SetErr` instead of emitting them as plain or `exception.*` attributes. The SDK then derives the `exception.*` attributes from the record error.
      
      ## Behavioural Notes
      
      - `trace.SpanFromContext()` never returns nil — no nil checks or `IsRecording()` guards needed.
      - `span.RecordError()` and `span.AddEvent()` are still present in the released tracing API; do not remove existing uses as deprecated APIs.
      - `log.Record.SetErr(err)` (`otel/log` v0.18.0; released with core v1.42.0) — the SDK automatically sets `exception.type` and `exception.message` attributes.
      
      ## Resolved Gotchas (historical reference)
      
      - **Propagator not installed by `otelconf`** ([open-telemetry/opentelemetry-go-contrib#6712](https://github.com/open-telemetry/opentelemetry-go-contrib/issues/6712)) — fixed in `otelconf v0.20.0` (released 2026-02-02). The root `otelconf` package now exposes `sdk.Propagator()`; install via `otel.SetTextMapPropagator(sdk.Propagator())`. The schema-pinned `otelconf/v0.3.0` subpackage still requires manual installation: `otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(propagation.TraceContext{}, propagation.Baggage{}))`.
      
    • compile-time-instrumentation.md 10.2 KB
      # Compile-time instrumentation with `otelc`
      
      `otelc` (`go.opentelemetry.io/otelc`, current stable release **v1.1.0**) instruments Go applications
      with OpenTelemetry **at compile time**. It wraps the Go toolchain via the compiler's `-toolexec`
      hook and injects trampoline hooks (linked with `//go:linkname`) into target functions, so
      instrumentation is baked into the binary — **no source-code changes**, and it reaches third-party
      dependencies and stdlib packages you don't own. v1.0.0 is retracted because `otelc pin` generated
      the pre-v1 module paths in consumer `go.mod` files; use v1.0.1 or later.
      
      ## When to use it (vs. the rest of this skill)
      
      | Approach | Reach for it when |
      |---|---|
      | **`otelc`** (this file) | You can rebuild the app and want zero source changes and automatic coverage of dependencies/stdlib you don't control. The injected OpenTelemetry SDK and instrumentation still have runtime cost. |
      | **Hand-written SDK + contrib libraries** ([`instrumentation-libraries.md`](instrumentation-libraries.md), [`api.md`](api.md)) | You need explicit, fine-grained control over spans/metrics, or you cannot change the build toolchain. |
      
      `otelc` is **not** [`opentelemetry-go-instrumentation`](https://github.com/open-telemetry/opentelemetry-go-instrumentation),
      which is a separate **eBPF/uprobe runtime** auto-instrumentation project. `otelc` is
      compile-time / build-time.
      
      ## Requirements
      
      - **Go 1.25+** toolchain (`go 1.25.0` in the project's `go.mod`).
      - **Go 1.24+** for the tool-dependency workflow (`go get -tool` / `go tool otelc`).
      - Operates on the module containing your `go.mod`.
      
      ## Usage modes
      
      All three produce an instrumented binary; they differ in how `otelc` wires into the build.
      
      ```bash
      # Mode 1 — wrap the build (simplest). Prefix the normal go command.
      otelc go build -o bin/app .
      otelc go test ./...
      
      # Mode 2 — tool dependency (Go 1.24+, reproducible: version tracked in go.mod)
      go get -tool go.opentelemetry.io/otelc/tool/cmd/otelc
      go tool otelc go build -o bin/app .
      
      # Mode 3 — toolexec drop-in via GOFLAGS (build command owned by a Makefile/CI)
      otelc setup                                             # run once; re-run when deps change
      export GOFLAGS="${GOFLAGS} '-toolexec=otelc toolexec'"  # single quotes are required
      go build -o myapp .
      ```
      
      Obtain the binary by building from source (`make build` in the upstream repo), `go install
      go.opentelemetry.io/otelc/tool/cmd/otelc@latest`, or the tool-dependency mode above.
      
      **Zero-config:** with no instrumentation file present, `otelc go build` analyzes the dependency
      graph, generates a temporary config for the build, and cleans up. Upgrading `otelc` can change what
      gets instrumented, so pin the `otelc` version and inspect `.otelc-build/matched.json` in auditable
      builds. Committing `otelc pin` output is not yet supported (see below).
      
      ## Subcommands
      
      | Subcommand | Purpose |
      |---|---|
      | `otelc go …` | Instrument and run the go toolchain; everything after `go` is forwarded verbatim. |
      | `otelc setup` | Analyze the module, generate instrumentation sources, and write the matched rule set to `.otelc-build/` (needed before Mode 3). |
      | `otelc pin` | Create/update the local-workflow `otel.instrumentation.go`. Flags: `--prune` (default on), `--validate`, `--generate`. |
      | `otelc cleanup` | Delete `.otelc-build/` and generated files. |
      | `otelc version` | Print the tool version (`--verbose` adds the Go runtime version). |
      | `otelc toolexec …` | Hidden interceptor invoked by `-toolexec=otelc toolexec`; never call it directly. |
      
      **Flag ordering:** global flags (`--rules`, `--debug`/`-d`, `--work-dir`/`-w`) must come **before**
      the `go` subcommand. `otelc go` and `otelc toolexec` skip flag parsing, so anything after them goes
      straight to the Go toolchain — `otelc go build --rules …` sends `--rules` to `go build` and fails.
      
      ## Supported libraries (v1)
      
      Verified against the upstream `instrumentation/` tree. The active set is `otelc`'s embedded bundle;
      confirm against the tree when a version changes.
      
      | Library | Signals |
      |---|---|
      | `net/http` (client & server) | HTTP spans |
      | `google.golang.org/grpc` (client & server) | RPC spans |
      | `database/sql` | DB client spans |
      | `github.com/gin-gonic/gin` | HTTP server spans |
      | `github.com/redis/go-redis/v9` | Redis DB spans |
      | `go.mongodb.org/mongo-driver` (v1/v2) | MongoDB DB spans |
      | `k8s.io/client-go` | K8s resource spans |
      | `github.com/openai/openai-go` (v1/v2/v3) | GenAI spans |
      | `github.com/anthropics/anthropic-sdk-go` | GenAI spans |
      | `github.com/segmentio/kafka-go` (consumer & producer) | Kafka messaging spans |
      | `github.com/aws/aws-sdk-go-v2` | AWS SDK client spans |
      | `github.com/linode/linodego/v2` | HTTP client spans and metrics |
      | `log`, `log/slog`, `github.com/sirupsen/logrus` | Trace/span ID log correlation |
      | Go runtime | Runtime metrics |
      
      ## Selecting & configuring instrumentation
      
      **Rule sources, highest priority first — there is NO merging; each source entirely replaces the
      ones below it:**
      
      | Priority | Source |
      |---|---|
      | 1 | `OTELC_RULES` env var (file / dir / comma-separated list) |
      | 2 | `--rules` flag (same format; used only when `OTELC_RULES` is unset) |
      | 3 | Tool file `otel.instrumentation.go` (alias `otelc.tool.go`) |
      | 4 | Embedded default bundle |
      
      This is the top cause of "nothing got instrumented": an `OTELC_RULES`/`--rules` override silently
      masks the tool file and the embedded bundle. `--rules`/`OTELC_RULES` are for dev/debugging. The
      tool file is the intended explicit model, but `otelc` has not published the instrumentation
      submodules: `otelc pin` adds local `replace` directives into `.otelc-build/`. Treat its output as
      a local workflow, not a source-controlled production configuration.
      
      **Local explicit selection** uses the standard Go `tools.go` blank-import pattern in a
      module-scoped file next to `go.mod`. Let `otelc pin` generate and manage this file:
      
      ```go
      //go:build tools
      
      package tools
      
      import (
      	_ "go.opentelemetry.io/otelc/instrumentation/net/http/server"
      	_ "go.opentelemetry.io/otelc/instrumentation/google.golang.org/grpc/client"
      	_ "go.opentelemetry.io/otelc/instrumentation/google.golang.org/grpc/server"
      )
      ```
      
      Only blank (`_`) imports are allowed. Do not hand-copy the parent
      `instrumentation/google.golang.org/grpc` import shown in an upstream protocol example: it
      is not a Go package; the client and server are separate instrumentation modules.
      
      **Runtime tuning:** instrumented binaries embed an auto-initialized SDK that reads the standard
      [OTel SDK env vars](https://opentelemetry.io/docs/specs/otel/configuration/sdk-environment-variables/)
      (`OTEL_EXPORTER_OTLP_ENDPOINT`, `OTEL_TRACES_SAMPLER`, `OTEL_SERVICE_NAME`, …). There is no
      `otelc`-specific exporter or sampler destination config. `OTEL_SDK_DISABLED=true` (case-insensitive)
      disables the injected SDK. `otelc` exposes these runtime controls:
      
      | Variable | Behavior |
      |---|---|
      | `OTEL_GO_ENABLED_INSTRUMENTATIONS` | Comma-separated, case-insensitive allowlist of compiled instrumentation names. |
      | `OTEL_GO_DISABLED_INSTRUMENTATIONS` | Comma-separated denylist, applied after the allowlist. |
      | `OTEL_GLS_MAX_SPANS` | Per-goroutine live span-stack limit; default `1000`. |
      | `OTEL_GLS_MAX_SPAN_STATES` | Shared span-lifecycle map limit; default `100000`, evicting the oldest state when full. |
      | `OTEL_LOG_LEVEL` | `debug`, `info` (default), `warn`, or `error` for `otelc` runtime logs. |
      | `OTEL_GO_SIMPLE_SPAN_PROCESSOR` | Exactly lowercase `true` selects `SimpleSpanProcessor`; other spellings are ignored. |
      
      The enable/disable lists only gate instrumentation already compiled into the binary; build-time
      selection still comes from the rule sources above.
      
      **Verify what matched:** `.otelc-build/matched.json` lists every rule that matched; `[]` means
      nothing matched (and `otelc` prints `Warning: no instrumentation will be applied`). Enable
      `--debug`/`OTELC_DEBUG=1` for `.otelc-build/debug.log`.
      
      ## Rule schema (authoring)
      
      Custom rules and new-library instrumentation use YAML rules with `target` (import path; globs and
      `$root` supported), optional `version`, `where`/`where.file` predicates, and a `do` action. The
      eight rule types: `inject_hooks` (function hook), `add_struct_fields`, `inject_code`, `wrap_call`,
      `expand_directive`, `add_file`, `assign_value`, and `set_fields` (composite literals). See the
      fetch table for the full grammar.
      
      **v1.1.0 custom-rule migration:** function template variables are fields on Go's `text/template`
      dot. Use `{{ .FuncName }}`, `{{ .FuncArgument 0 }}`, and corresponding dotted forms; the pre-v1.1
      `{{ FuncName }}` spelling is no longer valid.
      
      ## Sources of truth
      
      `otelc` ships stable and evolves; fetch upstream docs rather than trusting a snapshot. Repo:
      `open-telemetry/opentelemetry-go-compile-instrumentation` (module `go.opentelemetry.io/otelc`).
      
      | Fact / task | Fetch |
      |---|---|
      | Latest `otelc` release tag | `gh api repos/open-telemetry/opentelemetry-go-compile-instrumentation/releases/latest -q '.tag_name'` |
      | Install, usage modes, managing instrumentations | `WebFetch https://raw.githubusercontent.com/open-telemetry/opentelemetry-go-compile-instrumentation/main/docs/getting-started.md` |
      | Full rule schema, glob grammar, and predicates | `WebFetch https://raw.githubusercontent.com/open-telemetry/opentelemetry-go-compile-instrumentation/main/docs/rules.md` |
      | Scope, filtering, precedence, runtime tuning, verification | `WebFetch https://raw.githubusercontent.com/open-telemetry/opentelemetry-go-compile-instrumentation/main/docs/configuration.md` |
      | Declaring instrumentations via `otel.instrumentation.go` | `WebFetch https://raw.githubusercontent.com/open-telemetry/opentelemetry-go-compile-instrumentation/main/docs/external-configuration.md` |
      | Add instrumentation for a new library; semconv | `WebFetch https://raw.githubusercontent.com/open-telemetry/opentelemetry-go-compile-instrumentation/main/docs/instrument-guide.md` |
      | Internals (`-toolexec`, trampolines, `//go:linkname`, GLS) | `WebFetch https://raw.githubusercontent.com/open-telemetry/opentelemetry-go-compile-instrumentation/main/docs/implementation.md` |
      | Diagnose why instrumentation was not applied | `WebFetch https://raw.githubusercontent.com/open-telemetry/opentelemetry-go-compile-instrumentation/main/docs/troubleshooting.md` |
      | Current supported-library set | List the `instrumentation/` tree in the repo (each leaf ships its own rules). |
      
    • declarative-setup.md 4.5 KB
      # Go SDK Setup with otelconf
      
      Configure the OpenTelemetry SDK in Go via `otelconf`, the declarative YAML configuration
      package.
      
      For the YAML configuration schema, load the `otel-declarative-config` skill.
      
      ## Sources of Truth
      
      For YAML schema details, fetch the upstream sources listed in the `otel-declarative-config`
      skill. For Go-specific facts:
      
      | Fact | Fetch |
      |---|---|
      | `otelconf` CHANGELOG (which schema rc each release supports, breaking changes) | `WebFetch https://raw.githubusercontent.com/open-telemetry/opentelemetry-go-contrib/main/CHANGELOG.md` |
      | Current API surface (`SDK` methods, options) | `WebFetch https://pkg.go.dev/go.opentelemetry.io/contrib/otelconf` |
      | Schema tag the generated types come from (`genjsonschema` target) | `WebFetch https://raw.githubusercontent.com/open-telemetry/opentelemetry-go-contrib/otelconf/<tag>/Makefile` |
      | Latest `go.opentelemetry.io/otel` core release | `gh api repos/open-telemetry/opentelemetry-go/releases/latest -q '.tag_name'` |
      
      ## Choosing an Import Path
      
      The `otelconf` repo ships two flavors of the package:
      
      - **Root** `go.opentelemetry.io/contrib/otelconf` — trails the schema repository. Verified
        2026-10-15: `v0.27.0` targets schema `v1.0.0-rc.3` (`file_format: "1.0-rc.3"`) while the schema
        repository is at `v1.2.0` (owner: the `otel-declarative-config`
        [parsers table](../../otel-declarative-config/SKILL.md)). Includes `sdk.Propagator()` for installing
        propagators from YAML.
      - **Schema-pinned** `go.opentelemetry.io/contrib/otelconf/v0.3.0` — frozen at file_format
        `"0.3"` (a 2024 schema). Has no `Propagator()` method; propagators must be installed
        manually via `otel.SetTextMapPropagator(...)`.
      
      Newer `otelconf` releases may exist; check the selected tag's Makefile schema tag before deploying.
      For "latest", resolve the tag once with the `otelconf` tag command in this skill's `SKILL.md`.
      
      ## Migration notes
      
      - `go.opentelemetry.io/contrib/config` was removed in contrib v1.35.0. Use `go.opentelemetry.io/contrib/otelconf`.
      - In the root package, use `OTEL_CONFIG_FILE`; `OTEL_EXPERIMENTAL_CONFIG_FILE` is rejected by
        current released `otelconf`.
      - Starting with root `otelconf` v0.25.0 (contrib v1.45.0), the resolved resource includes the SDK's default resource attributes; account for those when comparing telemetry across this boundary.
      - If migrating from the schema-pinned `otelconf/v0.3.0` import, the YAML must also migrate to
        the new schema (fetch `examples/otel-sdk-migration-config.yaml` from the schema repo for
        before/after pairs).
      
      ## Dependencies
      
      ```go
      import (
          "go.opentelemetry.io/otel"
          "go.opentelemetry.io/otel/propagation"
          semconv "go.opentelemetry.io/otel/semconv/v1.43.0"
          otelconf "go.opentelemetry.io/contrib/otelconf"
      )
      ```
      
      ## YAML Config
      
      For the canonical structure, fetch `examples/otel-sdk-config.yaml` from the schema repo
      (see the `otel-declarative-config` skill's Sources of Truth). The minimal example below
      illustrates the Go-specific quirk; the exact `file_format` string and exporter field names
      should come from the selected `otelconf` module's package fixtures/source.
      
      ```yaml
      # file_format: use the exact literal accepted by the selected otelconf module
      resource:
        attributes:
          - name: service.name
            value: "${SERVICE_NAME:-my-service}"
          - name: deployment.environment.name
            value: "${DEPLOY_ENV:-development}"
      
      # Tracer/meter/logger provider blocks: structure per the canonical example.
      # The root otelconf parser resolves ${VAR}, ${env:VAR}, and ${VAR:-default}
      # before parsing YAML. Use $${VAR} when the literal text must be preserved.
      ```
      
      ## Key API Facts
      
      - **`otelconf.ParseYAML([]byte) (*OpenTelemetryConfiguration, error)`** parses a YAML config file.
      - **`otelconf.NewSDK(opts ...) (SDK, error)`** builds the SDK from a parsed configuration. Options include `WithContext(ctx)` and `WithOpenTelemetryConfiguration(conf OpenTelemetryConfiguration)`; after `ParseYAML`, pass `*conf` because `ParseYAML` returns `*OpenTelemetryConfiguration`.
      - **`sdk.TracerProvider()`, `sdk.MeterProvider()`, `sdk.LoggerProvider()`** return the configured providers.
      - **`sdk.Propagator()`** (root package, `otelconf v0.20.0+`, fixes [open-telemetry/opentelemetry-go-contrib#6712](https://github.com/open-telemetry/opentelemetry-go-contrib/issues/6712)) returns the propagator built from the YAML `propagator:` block. Install it via `otel.SetTextMapPropagator(sdk.Propagator())`. Not available on the schema-pinned `otelconf/v0.3.0` subpackage.
      - **`sdk.Shutdown(ctx) error`** flushes and closes all providers.
      
    • instrumentation-libraries.md 15.2 KB
      # OpenTelemetry Go Instrumentation Libraries
      
      ## Detecting Existing Instrumentation
      
      Before adding manual instrumentation, check for these imports and middleware patterns in the codebase:
      
      ```go
      import (
          // HTTP frameworks
          "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
          "go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin"
          "go.opentelemetry.io/contrib/instrumentation/github.com/gorilla/mux/otelmux"
          "go.opentelemetry.io/contrib/instrumentation/github.com/labstack/echo/otelecho" // removed in v0.72.0
      
          // gRPC
          "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
      
          // Logging bridges
          "go.opentelemetry.io/contrib/bridges/otelzap"
          "go.opentelemetry.io/contrib/bridges/otelslog"
          "go.opentelemetry.io/contrib/bridges/otellogrus"
      )
      ```
      
      If these middleware/wrapper patterns are present, spans are already being created:
      
      ```go
      handler := otelhttp.NewHandler(mux, "service-name")       // net/http
      router.Use(otelgin.Middleware("service-name"))             // gin (also emits HTTP metrics)
      router.Use(otelmux.Middleware("service-name"))             // gorilla/mux (also emits HTTP metrics)
      ```
      
      ## Context Propagation (read first)
      
      In Go, spans link to their parent **only** through `context.Context`. An instrumentation library
      installs correctly and still emits *detached* spans if the active context never reaches the call. A
      SERVER span living in `c.Request.Context()` (gin) or `r.Context()` (net/http) parents a downstream
      DB/client span **only if that exact context is passed into it**.
      
      > **Symptom:** DB or client spans show up as CLIENT-kind *roots* in their own traces, disconnected
      > from the request. This means trace context was dropped at an internal boundary.
      
      The usual cause is structural, not a missing option:
      
      ```go
      // ANTI-PATTERN: global handle + ctx-less call → span parented to context.Background()
      db := common.GetDB()             // package-level *gorm.DB
      db.Where("id = ?", id).First(&u) // no ctx → detached CLIENT-root span
      
      // CORRECT: thread the request context from the handler down to the call
      func (h *Handler) GetUser(c *gin.Context) {
          u, err := h.users.Find(c.Request.Context(), id) // ctx flows in
      }
      func (r *UserRepo) Find(ctx context.Context, id int) (*User, error) {
          var u User
          return &u, r.db.WithContext(ctx).First(&u, id).Error // GORM links via WithContext(ctx)
      }
      ```
      
      A data layer built on a global `*gorm.DB` (or `*sql.DB`) plus functions that don't accept `ctx`
      **cannot** produce connected traces — the call signatures have to carry `ctx`. Rules:
      
      - Every DB/client/outbound call runs with the incoming request's context.
      - For GORM, call `db.WithContext(ctx)` on every query; for `database/sql`, use the `...Context`
        methods (`QueryContext`, `ExecContext`).
      - Never call instrumented clients with `context.Background()` or `context.TODO()` on a request path.
      
      ## Library Catalog
      
      ### HTTP
      
      | Library | Package |
      |---------|---------|
      | net/http | `go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp` |
      | Gin | `go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin` |
      | Gorilla mux | `go.opentelemetry.io/contrib/instrumentation/github.com/gorilla/mux/otelmux` |
      | Echo | `github.com/labstack/echo-opentelemetry` (contrib `otelecho` was removed in v0.72.0; v0.71.0 is its last release). |
      | go-restful | `go.opentelemetry.io/contrib/instrumentation/github.com/emicklei/go-restful/otelrestful` |
      
      ```go
      // HTTP server with automatic instrumentation
      func setupHTTPServer() *http.Server {
          mux := http.NewServeMux()
          mux.HandleFunc("/users", handleUsers)
      
          // Automatic span creation and context propagation
          handler := otelhttp.NewHandler(mux, "user-service")
      
          return &http.Server{
              Addr:    ":8080",
              Handler: handler,
          }
      }
      
      // HTTP client with automatic instrumentation
      // Note: otelhttp.DefaultClient, Get, Head, Post, PostForm were removed in v0.65.0
      // Always create a custom client with otelhttp.NewTransport instead
      func setupHTTPClient() *http.Client {
          return &http.Client{
              Transport: otelhttp.NewTransport(http.DefaultTransport),
          }
      }
      ```
      
      ### gRPC
      
      | Library | Package |
      |---------|---------|
      | gRPC | `go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc` |
      
      ```go
      // gRPC server with automatic instrumentation (prefer stats handlers over interceptors)
      func setupGRPCServer() *grpc.Server {
          return grpc.NewServer(
              grpc.StatsHandler(otelgrpc.NewServerHandler()),
          )
      }
      
      // gRPC client with automatic instrumentation
      func setupGRPCClient(target string) (*grpc.ClientConn, error) {
          return grpc.NewClient(target,
              grpc.WithStatsHandler(otelgrpc.NewClientHandler()),
          )
      }
      
      // Override default span kind in gRPC (otelgrpc v0.66.0+)
      func setupGRPCServerWithSpanKind() *grpc.Server {
          return grpc.NewServer(
              grpc.StatsHandler(otelgrpc.NewServerHandler(
                  otelgrpc.WithSpanKind(trace.SpanKindInternal),
              )),
          )
      }
      ```
      
      ### Database
      
      | Library | Package |
      |---------|---------|
      | database/sql | No package in current `go.opentelemetry.io/contrib`; check the OpenTelemetry registry for third-party instrumentation or instrument manually with `...Context` methods. |
      | MongoDB | `go.opentelemetry.io/contrib/instrumentation/go.mongodb.org/mongo-driver/mongo/otelmongo` |
      | GORM | `gorm.io/plugin/opentelemetry/tracing` (registered via `db.Use(tracing.NewPlugin(...))`) |
      
      > **PII in `db.query.text`:** the GORM plugin records the executed statement with bound parameter
      > values **inlined** by default, so a query like `INSERT INTO users (email, ...) VALUES ('a@b.com', ...)`
      > leaks the literal email onto the span. Disable query-variable capture so the statement is
      > parameterized:
      >
      > ```go
      > db.Use(tracing.NewPlugin(tracing.WithoutQueryVariables()))
      > // db.query.text becomes: INSERT INTO "users" (...) VALUES (?, ?, ?)
      > ```
      >
      > For services that handle user data, the same trap applies to other SQL instrumentation that
      > captures full statement text; verify the selected package's option to
      > disable or redact query text. Keep parameter values out of `db.query.text` before export. If the
      > selected instrumentation cannot sanitize or parameterize query text, disable query-text capture;
      > downstream redaction is defense-in-depth.
      
      ### AWS
      
      | Library | Package |
      |---------|---------|
      | AWS SDK v2 | `go.opentelemetry.io/contrib/instrumentation/github.com/aws/aws-sdk-go-v2/otelaws` (deprecated in v0.72.0 as no longer supported; no contrib replacement) |
      | Lambda | `go.opentelemetry.io/contrib/instrumentation/github.com/aws/aws-lambda-go/otellambda` |
      
      ### Logging Bridges
      
      | Logger | Package |
      |--------|---------|
      | zap | `go.opentelemetry.io/contrib/bridges/otelzap` |
      | slog | `go.opentelemetry.io/contrib/bridges/otelslog` |
      | logrus | `go.opentelemetry.io/contrib/bridges/otellogrus` |
      | logr | `go.opentelemetry.io/contrib/bridges/otellogr` |
      
      > **Logging bridge change (bridge v0.10.0):** otelzap and otelslog now emit `code.function` with the full package path-qualified function name (e.g., `github.com/user/pkg.MyFunc`) instead of just the function name. The `code.namespace` attribute is no longer emitted.
      
      ```go
      // zap bridge setup
      import (
          "go.opentelemetry.io/contrib/bridges/otelzap"
          "go.opentelemetry.io/otel"
          "go.uber.org/zap"
          "go.uber.org/zap/zapcore"
      )
      
      func setupLogger() *zap.Logger {
          // Create a tee core that logs to both stdout and OpenTelemetry
          core := zapcore.NewTee(
              zapcore.NewCore(
                  zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig()),
                  zapcore.AddSync(os.Stdout),
                  zapcore.InfoLevel,
              ),
              otelzap.NewCore("myservice",
                  otelzap.WithLoggerProvider(otel.GetLoggerProvider())),
          )
          return zap.New(core)
      }
      ```
      
      ```go
      // slog bridge setup (Go 1.21+)
      import (
          "log/slog"
          "go.opentelemetry.io/contrib/bridges/otelslog"
          "go.opentelemetry.io/otel"
      )
      
      func setupLogger() *slog.Logger {
          handler := otelslog.NewHandler("myservice",
              otelslog.WithLoggerProvider(otel.GetLoggerProvider()))
          return slog.New(handler)
      }
      ```
      
      ```go
      // logrus bridge setup
      import (
          "github.com/sirupsen/logrus"
          "go.opentelemetry.io/contrib/bridges/otellogrus"
          "go.opentelemetry.io/otel"
      )
      
      func setupLogger() {
          logrus.AddHook(otellogrus.NewHook("myservice",
              otellogrus.WithLoggerProvider(otel.GetLoggerProvider())))
      }
      ```
      
      ### Resource Detectors
      
      | Cloud | Package |
      |-------|---------|
      | AWS EC2 | `go.opentelemetry.io/contrib/detectors/aws/ec2/v2` |
      | AWS ECS | `go.opentelemetry.io/contrib/detectors/aws/ecs` |
      | AWS EKS | `go.opentelemetry.io/contrib/detectors/aws/eks` |
      | AWS Lambda | `go.opentelemetry.io/contrib/detectors/aws/lambda` |
      | GCP | `go.opentelemetry.io/contrib/detectors/gcp` |
      | Azure VM | `go.opentelemetry.io/contrib/detectors/azure/azurevm` |
      
      ### Propagators
      
      | Propagator | Package |
      |------------|---------|
      | Environment carrier | `go.opentelemetry.io/contrib/propagators/envcar` (new in v0.67.0) |
      
      ## Manual Instrumentation Patterns
      
      Use these patterns when no contrib instrumentation library exists for the target.
      
      ### HTTP Client with Semconv Attributes
      
      ```go
      func (c *CustomClient) CallExternalAPI(ctx context.Context, endpoint string) error {
          ctx, span := c.tracer.Start(ctx, "GET",
              trace.WithSpanKind(trace.SpanKindClient),
              trace.WithAttributes(
                  semconv.HTTPRequestMethodGet,
                  semconv.URLFull(endpoint)))
          defer span.End()
      
          req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
          if err != nil {
              return fmt.Errorf("creating request: %w", err)
          }
      
          resp, err := c.httpClient.Do(req)
          if err != nil {
              return fmt.Errorf("making request: %w", err)
          }
          defer resp.Body.Close()
      
          span.SetAttributes(semconv.HTTPResponseStatusCode(resp.StatusCode))
          return nil
      }
      ```
      
      ### Database Operation with Semconv
      
      ```go
      func (r *Repository) CreateUser(ctx context.Context, userData *UserData) (*User, error) {
          // semconv helpers below track go.opentelemetry.io/otel/semconv/v1.43.0
          ctx, span := r.tracer.Start(ctx, "INSERT users",
              trace.WithAttributes(
                  semconv.DBSystemNamePostgreSQL,        // db.system.name (was DBSystem/db.system)
                  semconv.DBNamespace(r.dbName),
                  semconv.DBOperationName("INSERT"),      // db.operation.name (was DBOperation)
                  semconv.DBCollectionName("users")))     // db.collection.name (was DBSQLTable/db.sql.table)
          defer span.End()
      
          query := `INSERT INTO users (email, name, tier) VALUES ($1, $2, $3) RETURNING id, created_at`
      
          var user User
          err := r.db.QueryRow(ctx, query, userData.Email, userData.Name, userData.Tier).
              Scan(&user.ID, &user.CreatedAt)
      
          if err != nil {
              span.SetAttributes(attribute.String("error.type", classifyDBError(err)))
              return nil, fmt.Errorf("inserting user: %w", err)
          }
      
          span.SetAttributes(
              attribute.String("user.id", user.ID),
              attribute.String("user.tier", userData.Tier),
          )
      
          return &user, nil
      }
      
      func classifyDBError(err error) string {
          if strings.Contains(err.Error(), "unique constraint") {
              return "duplicate_key"
          }
          if strings.Contains(err.Error(), "connection") {
              return "connection_error"
          }
          return "unknown"
      }
      ```
      
      ### Message Queue Producer/Consumer with Semconv
      
      ```go
      func (p *Publisher) PublishOrderEvent(ctx context.Context, order *Order) error {
          ctx, span := p.tracer.Start(ctx, "send queue.orders",
              trace.WithSpanKind(trace.SpanKindProducer),
              trace.WithAttributes(
                  semconv.MessagingSystemKey.String("nats"), // messaging.system (no NATS enum const)
                  semconv.MessagingOperationTypeSend,        // was MessagingOperationTypePublish
                  semconv.MessagingDestinationName("orders"),
                  attribute.String("order.id", order.ID)))
          defer span.End()
      
          message, err := json.Marshal(order)
          if err != nil {
              return fmt.Errorf("marshaling order: %w", err)
          }
      
          if err := p.client.Publish("orders", message); err != nil {
              return fmt.Errorf("publishing message: %w", err)
          }
      
          span.SetAttributes(attribute.Int("message.size", len(message)))
      
          return nil
      }
      
      func (c *Consumer) HandleOrderEvent(ctx context.Context, msg []byte) error {
          ctx, span := c.tracer.Start(ctx, "consume queue.orders",
              trace.WithSpanKind(trace.SpanKindConsumer),
              trace.WithAttributes(
                  semconv.MessagingSystemKey.String("nats"), // messaging.system (no NATS enum const)
                  semconv.MessagingOperationTypeReceive,
                  semconv.MessagingDestinationName("orders")))
          defer span.End()
      
          var order Order
          if err := json.Unmarshal(msg, &order); err != nil {
              span.SetStatus(codes.Error, "message parsing failed")
              return fmt.Errorf("unmarshaling order: %w", err)
          }
      
          span.SetAttributes(attribute.String("order.id", order.ID))
      
          if err := c.processOrder(ctx, &order); err != nil {
              return fmt.Errorf("processing order: %w", err)
          }
      
          return nil
      }
      ```
      
      ### Background Job
      
      ```go
      func (w *Worker) ProcessBatch(ctx context.Context, batchID string) error {
          ctx, span := w.tracer.Start(ctx, "process batch",
              trace.WithAttributes(
                  attribute.String("batch.id", batchID),
                  attribute.String("worker.id", w.id)))
          defer span.End()
      
          items, err := w.repo.GetBatchItems(ctx, batchID)
          if err != nil {
              return fmt.Errorf("fetching batch items: %w", err)
          }
      
          span.SetAttributes(attribute.Int("batch.size", len(items)))
      
          processed := 0
          for _, item := range items {
              if err := w.processItem(ctx, item); err != nil {
                  var rec log.Record
                  rec.SetTimestamp(time.Now())
                  rec.SetSeverity(log.SeverityWarn)
                  rec.SetBody(attribute.StringValue("item processing failed"))
                  rec.AddAttributes(
                      attribute.String("item.id", item.ID),
                  )
                  rec.SetErr(err)
                  w.logger.Emit(ctx, rec)
                  continue
              }
              processed++
          }
      
          span.SetAttributes(
              attribute.Int("batch.processed", processed),
              attribute.Int("batch.failed", len(items)-processed),
          )
      
          return nil
      }
      ```
      
      ## Enriching Spans from Instrumentation Libraries
      
      When an instrumentation library (otelhttp, otelgin, otelmux, etc.) already creates spans, use `trace.SpanFromContext(ctx)` to add attributes to the existing span. Do not create a new span.
      
      ```go
      func (h *Handler) CreateOrder(w http.ResponseWriter, r *http.Request) {
          // Span already created by instrumentation library (otelhttp, otelgin, etc.)
          ctx := r.Context()
      
          // Add business attributes to existing span
          span := trace.SpanFromContext(ctx)
          span.SetAttributes(
              attribute.String("business.operation", "order_creation"),
              attribute.String("customer.tier", extractCustomerTier(r)))
      
          order, err := h.service.CreateOrder(ctx, extractOrderData(r))
          if err != nil {
              span.SetStatus(codes.Error, "order creation failed")
              http.Error(w, "Failed to create order", http.StatusInternalServerError)
              return
          }
      
          span.SetAttributes(attribute.String("order.id", order.ID))
      
          w.Header().Set("Content-Type", "application/json")
          json.NewEncoder(w).Encode(order)
      }
      ```
      
    • performance.md 18.2 KB
      # OpenTelemetry Go Performance Tuning
      
      Performance tuning reference for OpenTelemetry Go SDK. Covers sampling, batch processing, metric readers, attribute allocation, exporter configuration, and pipeline reliability.
      
      ---
      
      ## Performance Impact by Signal
      
      | Signal | Unsampled Overhead | Sampled Overhead | Primary Cost |
      |--------|-------------------|------------------|--------------|
      | Traces | Near-zero (noop span) | Moderate | Allocations, export I/O |
      | Metrics | N/A (always collected) | N/A | Aggregation, cardinality |
      | Logs | Near-zero if `Enabled()` false | Low-moderate | Serialization, export I/O |
      
      ## Default Configuration Values
      
      | Parameter | Default | Environment Variable |
      |-----------|---------|---------------------|
      | Batch span queue size | 2048 | `OTEL_BSP_MAX_QUEUE_SIZE` |
      | Batch span export size | 512 | `OTEL_BSP_MAX_EXPORT_BATCH_SIZE` |
      | Batch schedule delay | 5s | `OTEL_BSP_SCHEDULE_DELAY` |
      | Batch export timeout | 30s | `OTEL_BSP_EXPORT_TIMEOUT` |
      | Span attribute limit | 128 | `OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT` |
      | Span event limit | 128 | `OTEL_SPAN_EVENT_COUNT_LIMIT` |
      | Span link limit | 128 | `OTEL_SPAN_LINK_COUNT_LIMIT` |
      | Attribute value length limit | Unlimited | `OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT` |
      | Metric export interval | 60s | `OTEL_METRIC_EXPORT_INTERVAL` |
      | Metric export timeout | 30s | `OTEL_METRIC_EXPORT_TIMEOUT` |
      | OTLP export timeout | 10s | `OTEL_EXPORTER_OTLP_TIMEOUT` |
      
      ---
      
      ## Sampling
      
      Sampling is the single most impactful performance lever for traces. Dropped spans do not record
      attributes, events, or links, though the application still pays to construct values and options it
      passes to `Start`.
      
      ### Head Sampling Configuration
      
      Configure a sampler at the `TracerProvider` level to decide before span processing whether to record:
      
      ```go
      // Sample 10% of traces
      sampler := sdktrace.TraceIDRatioBased(0.1)
      
      // Composite: respect upstream sampling decisions, ratio-sample the rest
      sampler := sdktrace.ParentBased(
          sdktrace.TraceIDRatioBased(0.1),
          // Remote parent sampled -> always sample (honor upstream)
          // Remote parent not sampled -> never sample
      )
      
      tp := sdktrace.NewTracerProvider(
          sdktrace.WithSampler(sampler),
          sdktrace.WithBatcher(exporter),
      )
      ```
      
      ### AlwaysRecord Sampler (v1.40.0+)
      
      The `AlwaysRecord` sampler wraps another sampler and ensures spans are always recorded (attributes,
      events, and status are captured) and passed to in-process span processors even when the inner
      sampler decides `Drop`. Standard `SimpleSpanProcessor` and `BatchSpanProcessor` exporters still
      skip `RecordOnly` spans, so this is useful for in-process processing such as span-to-metrics, not
      for sending unsampled spans to a Collector.
      
      ```go
      // Record all spans in-process; standard processors export only the sampled 10%
      sampler := sdktrace.AlwaysRecord(sdktrace.TraceIDRatioBased(0.1))
      ```
      
      ### Sampling Decision Impact
      
      ```
      AlwaysSample       -> Full span lifecycle: allocation + recording + export
      AlwaysRecord(0.1)  -> All spans recorded in-process, 10% exported by standard processors
      TraceIDRatioBased(0.1) -> 90% of spans become noop (near-zero cost)
      NeverSample        -> All spans noop (useful for load testing)
      ```
      
      `AlwaysSample` records and exports every span. `TraceIDRatioBased(0.1)` makes 90% of spans noop with near-zero cost. `ParentBased` wrapping respects upstream sampling decisions. `NeverSample` makes all spans noop, which is useful for benchmarking application-only performance.
      
      > **Tail sampling**: For a Collector to decide from complete traces (error status, latency
      > thresholds), configure the SDK to sample and export all candidate spans, then use the Collector's
      > tail sampling processor. `AlwaysRecord` does not send its `RecordOnly` spans through the standard
      > SDK span processors.
      
      ---
      
      ## Batch Processor Tuning
      
      The `BatchSpanProcessor` buffers spans in a queue and exports them asynchronously in batches.
      
      ### How It Works
      
      ```
      Application goroutine          BatchSpanProcessor goroutine
             |                                |
         span.End() --enqueue-->  channel queue (MaxQueueSize)
             |                                |
             |                         timer fires (ScheduleDelay)
             |                         OR batch full (MaxExportBatchSize)
             |                                |
             |                        --export batch--> Exporter
      ```
      
      ### Tuning for Throughput
      
      For high-volume services (>10k spans/second):
      
      ```go
      bsp := sdktrace.NewBatchSpanProcessor(exporter,
          sdktrace.WithMaxQueueSize(4096),       // Absorb bursts (default: 2048)
          sdktrace.WithMaxExportBatchSize(1024), // Fewer network calls (default: 512)
          sdktrace.WithBatchTimeout(10*time.Second), // Allow larger batches (default: 5s)
      )
      ```
      
      ### Tuning for Latency
      
      For services where trace delivery speed matters (debugging, alerting):
      
      ```go
      bsp := sdktrace.NewBatchSpanProcessor(exporter,
          sdktrace.WithMaxExportBatchSize(128),       // Export smaller batches faster
          sdktrace.WithBatchTimeout(2*time.Second),   // Export more frequently
      )
      ```
      
      ### Queue Full Behavior
      
      When the queue fills, new spans are dropped silently by default. Telemetry loss is preferable to application slowdown in this design.
      
      `WithBlockOnQueueFull()` changes this behavior to backpressure the application goroutine, blocking on `span.End()` until queue space is available.
      
      With experimental SDK self-observability enabled via `OTEL_GO_X_OBSERVABILITY=true`, the SDK
      exposes queue size/capacity and processed-span metrics. This feature may change incompatibly.
      
      ### SimpleSpanProcessor
      
      `SimpleSpanProcessor` exports spans synchronously on `span.End()`. It adds latency to every span ending. It is deterministic, which makes it useful for tests and development. Short-lived CLI tools may also use it to ensure spans are exported before exit.
      
      ```go
      ssp := sdktrace.NewSimpleSpanProcessor(exporter)
      ```
      
      ---
      
      ## Metric Reader Tuning
      
      ### PeriodicReader Configuration
      
      The `PeriodicReader` collects and exports metrics on a fixed interval.
      
      ```go
      reader := sdkmetric.NewPeriodicReader(exporter,
          sdkmetric.WithInterval(30*time.Second), // More frequent than default 60s
          sdkmetric.WithTimeout(15*time.Second),  // Timeout per export
      )
      ```
      
      Interval tradeoffs:
      - 60s (default): Standard for dashboards and alerting
      - 15-30s: Near-real-time monitoring, higher export overhead
      - 5-10s: High-frequency use cases, significant overhead
      
      The reader internally uses `sync.Pool` to recycle `ResourceMetrics` objects, reducing GC pressure during collection cycles.
      
      ### Views for Cardinality Control
      
      Views filter attributes before aggregation, reducing the number of unique time series.
      
      #### Attribute Filter
      
      ```go
      // Drop a high-cardinality attribute from a specific metric
      dropUserID := sdkmetric.NewView(
          sdkmetric.Instrument{Name: "http.server.request.duration"},
          sdkmetric.Stream{
              AttributeFilter: func(kv attribute.KeyValue) bool {
                  return kv.Key != "user.id" // Exclude user.id from aggregation
              },
          },
      )
      ```
      
      An allowlist-style filter keeps only specified attributes:
      
      ```go
      sdkmetric.Stream{
          AttributeFilter: func(kv attribute.KeyValue) bool {
              return kv.Key == "http.request.method" || kv.Key == "http.response.status_code"
          },
      }
      ```
      
      #### Drop Metric
      
      ```go
      // Drop an entire metric
      dropMetric := sdkmetric.NewView(
          sdkmetric.Instrument{Name: "runtime.*"},
          sdkmetric.Stream{Aggregation: sdkmetric.AggregationDrop{}},
      )
      ```
      
      #### Applying Views
      
      ```go
      mp := sdkmetric.NewMeterProvider(
          sdkmetric.WithReader(reader),
          sdkmetric.WithView(dropUserID, dropMetric),
      )
      ```
      
      Metric attribute cardinality is the product of unique values across all recorded attributes. Attributes like user IDs or request IDs are unbounded and produce one time series per unique value. Attributes like HTTP method (~10 values) or status code (~50 values) are bounded.
      
      > **Default cardinality limit (v1.44.0+):** `sdk/metric` now enforces a default cardinality
      > limit of **2000** series per instrument (spec-compliant). Series beyond the limit are dropped
      > and aggregated into an overflow series marked `attribute.Bool("otel.metric.overflow", true)`.
      > This is a breaking change from the previous unlimited default. Override globally with the
      > MeterProvider option `sdkmetric.WithCardinalityLimit(n)` (zero or negative disables the
      > limit), or per instrument kind with the reader option
      > `sdkmetric.WithCardinalityLimitSelector` (v1.43.0+). Views remain the correct tool for
      > *shaping* cardinality; the limit is a backstop.
      
      ---
      
      ## Attribute Allocation Patterns
      
      Attribute options can be a significant source of allocations in instrumented code.
      
      ### Pre-allocating Attribute Sets
      
      For attributes used across many measurements, construct an attribute set and measurement option
      once:
      
      ```go
      var (
          successfulGET = attribute.NewSet(
              attribute.String("http.request.method", "GET"),
              attribute.Int("http.response.status_code", 200),
          )
          successfulGETOption = metric.WithAttributeSet(successfulGET)
      )
      
      func handleRequest(ctx context.Context, method string, status int) {
          counter.Add(ctx, 1, successfulGETOption)
      }
      ```
      
      Reusing `metric.WithAttributeSet(...)` avoids rebuilding and deduplicating the same attribute set
      for every measurement. Benchmark dynamic cases before adding caches or pools.
      
      ### Slice Capacity
      
      When building dynamic attribute sets, pre-allocating slice capacity eliminates repeated reallocations:
      
      ```go
      attrs := make([]attribute.KeyValue, 0, 4)
      attrs = append(attrs, attribute.String("service.name", svcName))
      attrs = append(attrs, attribute.String("deployment.environment.name", env))
      if region != "" {
          attrs = append(attrs, attribute.String("cloud.region", region))
      }
      ```
      
      A `nil` slice (`var attrs []attribute.KeyValue`) reallocates on the first and subsequent appends as it grows.
      
      ### Span Attribute Limits
      
      The SDK enforces configurable limits on span attributes, events, and links. New span attributes
      beyond the count limit are dropped; new events and links replace the oldest item (FIFO):
      
      ```go
      tp := sdktrace.NewTracerProvider(
          sdktrace.WithSpanLimits(sdktrace.SpanLimits{
              AttributeCountLimit:         64,  // Default: 128
              EventCountLimit:             32,  // Default: 128
              LinkCountLimit:              16,  // Default: 128
              AttributePerEventCountLimit: 16,  // Default: 128
              AttributePerLinkCountLimit:  16,  // Default: 128
              AttributeValueLengthLimit:   256, // Default: Unlimited. Truncates long strings.
          }),
      )
      ```
      
      `AttributeValueLengthLimit` truncates strings, string-slice elements, byte slices, and those values
      nested in slice attributes that would otherwise consume unbounded memory.
      
      Since v1.47.0, nested (slice/map) attribute values are also capped at a depth of 64 by default
      (`DefaultAttributeValueDepthLimit`); tune with `sdktrace.WithAttributeValueDepthLimit` or
      `SpanLimits.AttributeValueDepthLimit`, and `sdklog.WithAttributeValueDepthLimit` for logs.
      
      ---
      
      ## Exporter Configuration
      
      ### gRPC vs HTTP
      
      | Aspect | gRPC | HTTP/protobuf |
      |--------|------|---------------|
      | Connection overhead | Single long-lived connection | New connection per export (or HTTP/2 reuse) |
      | Default port | 4317 | 4318 |
      | Compression | Optional gzip | Optional gzip |
      | Best for | Persistent connections, high throughput | Firewalls, load balancers, simpler infra |
      
      ### Compression
      
      Compression reduces bandwidth at the cost of CPU.
      
      gRPC:
      
      ```go
      import "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
      
      exporter, err := otlptracegrpc.New(ctx,
          otlptracegrpc.WithCompressor("gzip"),
      )
      ```
      
      HTTP:
      
      ```go
      import "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp"
      
      exporter, err := otlptracehttp.New(ctx,
          otlptracehttp.WithCompression(otlptracehttp.GzipCompression),
      )
      ```
      
      ### OTLP/HTTP trace encoding
      
      The trace HTTP exporter supports both OTLP protobuf (the default) and OTLP JSON. Select JSON
      with `otlptracehttp.WithEncoding(otlptracehttp.EncodingJSON)` or set
      `OTEL_EXPORTER_OTLP_TRACES_PROTOCOL=http/json` (the signal-specific variable takes precedence
      over `OTEL_EXPORTER_OTLP_PROTOCOL`). This released option is specific to `otlptracehttp`; do not
      assume the metric and log HTTP exporters expose the same encoding option.
      
      ### Retry Defaults
      
      The OTLP exporters use exponential backoff with jitter:
      
      | Parameter | Default |
      |-----------|---------|
      | Enabled | true |
      | Initial interval | 5s |
      | Max interval | 30s |
      | Max elapsed time | 1min |
      
      gRPC retries honor server pushback. Since v1.45.0 the HTTP exporters correctly interpret
      integer `Retry-After` values as seconds and also support HTTP-date values. On v1.44.0, integer
      values were mistakenly treated as nanoseconds and HTTP-date values were ignored.
      
      ### Request Size Cap (v1.44.0+)
      
      All OTLP exporters (trace/metric/log, gRPC and HTTP) cap the request payload at **64 MiB** by
      default. The limit is applied *before* compression; oversized requests are treated as
      non-retryable errors and dropped. Tune with `WithMaxRequestSize(bytes)` on the exporter —
      zero or negative disables the limit entirely (not recommended).
      
      ### Timeout Tuning
      
      ```go
      // Lower timeout: fail fast
      exporter, err := otlptracegrpc.New(ctx,
          otlptracegrpc.WithTimeout(5*time.Second), // Default: 10s
      )
      
      // Higher timeout: allow more time for large batches
      exporter, err := otlptracegrpc.New(ctx,
          otlptracegrpc.WithTimeout(30*time.Second),
      )
      ```
      
      ---
      
      ## Log Signal Performance
      
      ### Enabled() Early Exit
      
      The Logs API supports an optional `Enabled()` check for skipping expensive log construction when
      the log level is below threshold or when a processor filters it. Call it at each emission where
      the optimization matters; its result can change and should not be cached:
      
      ```go
      logger := otel.Logger("my-service") // v1.47.0+; replaces deprecated otel/log/global
      
      if logger.Enabled(ctx, log.EnabledParameters{Severity: log.SeverityInfo}) {
          var rec log.Record
          rec.SetBody(attribute.StringValue(expensiveStringBuild()))
          rec.SetSeverity(log.SeverityInfo)
          logger.Emit(ctx, rec)
      }
      ```
      
      ### log.Record Value Type
      
      `log.Record` is a struct value type (not an interface). Passing it by value keeps it stack-allocated, avoiding heap allocation:
      
      ```go
      var rec log.Record
      rec.SetBody(attribute.StringValue("operation completed"))
      rec.SetSeverity(log.SeverityInfo)
      rec.AddAttributes(attribute.String("component", "handler"))
      logger.Emit(ctx, rec)
      ```
      
      ---
      
      ## Context Propagation Performance
      
      ### Extract Once per Boundary
      
      W3C Trace Context propagation involves parsing and formatting fixed-format headers. The overhead is minimal. Extract once per request boundary:
      
      ```go
      ctx = otel.GetTextMapPropagator().Extract(ctx, propagation.HeaderCarrier(r.Header))
      ```
      
      Extracting multiple times for the same request performs redundant parsing with no benefit.
      
      ### Baggage Overhead
      
      Baggage is propagated across all service boundaries via HTTP headers. Every key-value pair in baggage adds to every outgoing request's header size:
      
      ```go
      bag, _ := baggage.New(
          baggageMember("tenant.id", tenantID),
      )
      ctx = baggage.ContextWithBaggage(ctx, bag)
      ```
      
      Large values (e.g., full request bodies) or PII in baggage are propagated to every downstream service in every request.
      
      ---
      
      ## Graceful Shutdown
      
      Proper shutdown ensures buffered telemetry is flushed before the process exits. Shutdown order: providers first (which flush their processors), then exporters.
      
      ```go
      ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
      defer cancel()
      
      if err := tp.Shutdown(ctx); err != nil {
          slog.Error("trace provider shutdown failed", "error", err)
      }
      if err := mp.Shutdown(ctx); err != nil {
          slog.Error("metric provider shutdown failed", "error", err)
      }
      if err := lp.Shutdown(ctx); err != nil {
          slog.Error("log provider shutdown failed", "error", err)
      }
      ```
      
      ---
      
      ## Telemetry Pipeline Reliability
      
      The OpenTelemetry SDK is designed so that telemetry failures do not crash or block the application:
      
      - **Span creation never returns an error** -- it returns a noop span on failure
      - **Metric recording never returns an error** -- measurements are silently dropped on failure
      - **Export failures are retried** -- then dropped after max elapsed time
      - **Queue overflow drops spans** -- the application is not blocked (unless `WithBlockOnQueueFull` is used)
      
      ---
      
      ## Monitoring the Pipeline
      
      The released SDK and OTLP exporters expose experimental self-observability metrics when
      `OTEL_GO_X_OBSERVABILITY=true`. They use the global `MeterProvider` and may change incompatibly:
      
      | Metric | Meaning |
      |--------|---------|
      | `otel.sdk.processor.span.queue.size` | Current BatchSpanProcessor queue depth |
      | `otel.sdk.processor.span.processed` | Spans processed; `error.type=queue_full` identifies queue drops |
      | `otel.sdk.exporter.span.exported` | Spans whose export finished; `error.type` distinguishes failures |
      | Exporter errors in logs | Export failures with error details |
      
      Since v1.46.0, simple span and log processors record their `processed` metric when a record is
      submitted to the exporter, before export completes, and do not derive `error.type` from the export
      outcome. Do not interpret that processor metric as proof of successful export.
      
      ### ForceFlush
      
      `ForceFlush` synchronously exports all buffered data:
      
      ```go
      if err := tp.ForceFlush(ctx); err != nil {
          slog.Warn("failed to flush traces", "error", err)
      }
      ```
      
      As of v1.42.0, `TracerProvider.ForceFlush` iterates through all `SpanProcessor` instances and joins their errors together, instead of stopping at the first error. All processors get a chance to flush even if one fails.
      
      `ForceFlush` in the request hot path synchronously exports all buffered data, adding latency to that request.
      
      ---
      
      ## Synchronous Instrument Enabled()
      
      As of v1.40.0, all synchronous metric instruments (`Int64Counter`, `Float64Counter`, `Int64UpDownCounter`, `Float64UpDownCounter`, `Int64Histogram`, `Float64Histogram`, `Int64Gauge`, `Float64Gauge`) expose an `Enabled()` method. This allows skipping expensive metric value computation when the instrument is not recording:
      
      ```go
      histogram, _ := meter.Float64Histogram("expensive.metric")
      
      attrs := metric.WithAttributes(attribute.String("region", region))
      if histogram.Enabled(ctx) {
          value := computeExpensiveValue() // Only called when instrument is active
          histogram.Record(ctx, value, attrs)
      }
      ```
      
      This is the metric equivalent of `Logger.Enabled()` for logs. When combined with Views that drop metrics, `Enabled()` returns `false` for dropped instruments.
      
  • SKILL.md 5.9 KB
    ---
    name: otel-go
    description: OpenTelemetry in Go — SDK setup, API surface, breaking changes, contrib instrumentation libraries (otelhttp, otelgrpc, otelmongo), compile-time zero-code instrumentation (otelc), and performance tuning. Use when adding, reviewing, or configuring OpenTelemetry in a Go service. Triggers on "setup otel in go", "go telemetry", "go tracing", "otelconf go", "otelhttp", "otelgrpc", "TracerProvider go", "MeterProvider go", "otelc", "compile-time instrumentation go", "zero-code go instrumentation", "go build instrumentation", or any Go-related OTel question.
    ---
    
    # OpenTelemetry in Go
    
    Entry point for OpenTelemetry mechanics in Go services. Load a reference below based on the
    task; each reference is self-contained.
    
    ## References
    
    | File | Use when |
    |---|---|
    | [`references/declarative-setup.md`](references/declarative-setup.md) | Configuring the SDK via `otelconf` and YAML: providers, propagators, shutdown, env-var substitution. |
    | [`references/api.md`](references/api.md) | Looking up import paths, global API access, tracer/meter/logger usage, attributes, propagation, log bridges (zap, slog). |
    | [`references/instrumentation-libraries.md`](references/instrumentation-libraries.md) | Picking or wiring contrib libraries (otelhttp, otelgrpc, database, AWS, message queues, propagators, resource detectors), and writing manual instrumentation that follows semconv. |
    | [`references/performance.md`](references/performance.md) | Tuning sampling, batch processor, metric reader, exporter compression/retry, attribute allocation, log `Enabled()` short-circuiting, graceful shutdown. |
    | [`references/breaking-changes.md`](references/breaking-changes.md) | Auditing existing code for deprecated calls, renamed semantic conventions, and removed APIs across recent SDK / contrib releases. |
    | [`references/compile-time-instrumentation.md`](references/compile-time-instrumentation.md) | Zero-code, compile-time instrumentation with `otelc`: usage modes (`otelc go build`, tool dependency, toolexec drop-in), subcommands, supported libraries, rule sources/precedence, and pinning via `otel.instrumentation.go`. |
    
    For upgrade reviews, always finish with a safe local verification path (`go mod tidy -diff`,
    `go build ./...`, and `go test ./...`). Test exporter URL or retry changes against a disposable
    local receiver, never a deployment endpoint.
    
    ## Module versioning — read before adding dependencies
    
    opentelemetry-go is split into **independently versioned module groups**. They do NOT
    share one version number. Assuming they do is the most common cause of broken builds
    and version churn:
    
    | Module group | Example modules | Version line |
    |---|---|---|
    | Stable (traces, metrics, logs API/SDK) | `go.opentelemetry.io/otel`, `otel/sdk`, `otel/trace`, `otel/metric`, `otel/log`, `otel/sdk/log`, OTLP trace/metric exporters | **v1.x** (e.g. v1.47.0; logs API/SDK joined at v1.47.0) |
    | Log exporters and test helpers | `otel/exporters/otlp/otlplog/otlploghttp`, `.../otlploggrpc`, `otel/exporters/stdout/stdoutlog`, `otel/log/logtest`, `otel/sdk/log/logtest` | **v0.x** (separate, lower line, e.g. v0.23.0) |
    | Contrib instrumentation | `contrib/instrumentation/net/http/otelhttp`, `.../otelgrpc` | **v0.x** (separate line, e.g. v0.72.0) |
    | Contrib log bridges | `contrib/bridges/otelslog`, `otelzap`, `otellogrus`, `otellogr` | **v0.x** |
    
    **The trap:** pinning every module to the core version (e.g. `go get go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp@v1.47.0`)
    fails — log exporter, log test-helper, and bridge modules have no v1.x tag. Hand-picking and re-guessing each `@vX`
    is the churn to avoid.
    
    **Do this instead** — add each module with `@latest` and let Go resolve a compatible set:
    
    ```bash
    go get go.opentelemetry.io/otel@latest go.opentelemetry.io/otel/sdk@latest
    # logs (API/SDK are v1.x; log exporters stay on a v0.x line — do NOT force the core version):
    go get go.opentelemetry.io/otel/log@latest go.opentelemetry.io/otel/sdk/log@latest \
           go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp@latest
    # contrib (instrumentation and bridges each resolve to their own v0.x line):
    go get go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp@latest \
           go.opentelemetry.io/contrib/bridges/otelslog@latest
    go mod tidy && go build ./...
    ```
    
    If exact versions are required, fetch each module group's tag from its own source
    (see below) — never infer one group's version from another's.
    
    ## Sources of Truth
    
    For YAML schema details, fetch the upstream sources listed in the `otel-declarative-config` skill.
    For Go-specific facts:
    
    | Fact | Fetch |
    |---|---|
    | Latest `go.opentelemetry.io/otel` core release | `gh api repos/open-telemetry/opentelemetry-go/releases/latest -q '.tag_name'` |
    | Latest `go.opentelemetry.io/contrib` release | `gh api repos/open-telemetry/opentelemetry-go-contrib/releases/latest -q '.tag_name'` |
    | Latest `otelconf` module tag | the version-sorted tag command below the table |
    | Latest Go semconv package | Search the selected core release's CHANGELOG for `Add the go.opentelemetry.io/otel/semconv/`; do not infer package availability from the upstream semantic-conventions release. Offline: `semconv/v1.43.0` in otel v1.47.0, verified 2026-10-15 (record: the `otel-semantic-conventions` skill's `references/sdk-semconv-packages.md`). |
    | `otel-go` CHANGELOG | `WebFetch https://raw.githubusercontent.com/open-telemetry/opentelemetry-go/main/CHANGELOG.md` |
    | `otel-go-contrib` CHANGELOG | `WebFetch https://raw.githubusercontent.com/open-telemetry/opentelemetry-go-contrib/main/CHANGELOG.md` |
    
    ```bash
    gh api repos/open-telemetry/opentelemetry-go-contrib/git/matching-refs/tags/otelconf -q '.[].ref' | sort -V | tail -1
    ```
    
    ## Cross-References
    
    - Schema-level facts: `otel-declarative-config` skill (language-agnostic YAML schema sources).
    - SDK version selection across languages: `otel-sdk-versions` skill.
    - Semantic conventions lookup: `otel-semantic-conventions` skill.
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related