agentsclimarketplace

Ollygarden otel go setup

Skill ollygarden/skills/skills/ollygarden-otel-go-setup

OllyGarden's opinionated AI agent skills for OpenTelemetry and observability workflows

Install
npx -y skills add ollygarden/skills --skill ollygarden-otel-go-setup

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

What its author says it does

Copied from the file, not written here

Ollygarden's recommended pattern for setting up the OpenTelemetry SDK in Go services using otelconf. Covers project structure, the Providers struct, no-op fallback, runtime attribute injection, and the zap log bridge. Use when adding OTel to a Go project, structuring telemetry code, or reviewing an existing setup — including when DB spans show up as trace roots or GORM/database spans are disconnected from HTTP spans. Triggers on "go otel setup", "go telemetry pattern", "Providers struct go otel", "gorm WithContext", "root client span go".

SKILL.md

13.4 KB, as published. Nobody here has run it

Go SDK Setup Conventions

Setup Checklist — verify every item before you finish

Setup is not done when the SDK boots. Each unchecked item below produces a specific telemetry-quality finding in production; work through all of them.

  • Thread the request context into the data layer. Every database, HTTP-client, and queue call on a request path must receive the request's ctx — never context.Background() and never a bare global handle. With GORM this means db.WithContext(ctx) at every query site, with the context passed handler → service → model:

    // handler: take ctx from the framework request
    articles, err := svc.ListArticles(c.Request.Context(), filter)
    
    // service: every method takes ctx as its first parameter
    func (s *Service) ListArticles(ctx context.Context, f Filter) ([]Article, error) {
        return s.repo.List(ctx, f)
    }
    
    // repo/model: the only layer touching *gorm.DB
    func (r *Repo) List(ctx context.Context, f Filter) ([]Article, error) {
        var out []Article
        return out, r.db.WithContext(ctx).Where("tag = ?", f.Tag).Find(&out).Error
    }
    

    Verify by auditing every request-path DB call: each must receive the request context — via db.WithContext(ctx), a per-request gorm.Session{Context: ctx}, or database/sql's QueryContext/ExecContext. A quick spot-check is grep -rn "WithContext" --include='*.go' . (zero hits on a GORM codebase is a strong signal the context is not threaded), but the grep alone is not proof — wrappers and reused *gorm.DB handles hide call sites, so walk the request paths. Without the request context, DB spans become detached CLIENT-kind trace roots instead of children of the HTTP span (Root Client Span finding). Refactoring existing ctx-less signatures across layers is part of setup, not optional follow-up.

  • Never record SQL parameter values — on any signal. Bound values must not appear in db.query.text, SQL logs, database/sql instrumentation attributes, or custom spans; only ? placeholders are acceptable. With the GORM OTel plugin specifically, pass tracing.WithoutQueryVariables(). Raw values in any of these leak PII (Critical PII Leakage finding).

  • Configure the SDK declaratively, not in code. Exporters, processors, sampling, and signal wiring live in an external YAML file (configs/otel.yaml) parsed with otelconf — use the Setup Pattern below, not hand-constructed exporter/provider code. Operators must be able to change the telemetry setup without recompiling, and the app must fall back to no-op providers when the file is absent.

  • Inject service.instance.id (a per-process UUID) alongside service.version, as the setup pattern below does programmatically (Missing service.instance.id finding).

  • Keep the resource lean. service.name, service.version, service.instance.id, and deployment.environment.name — that is the full set. Do not add resource.WithOS(), resource.WithProcess(), resource.WithHost(), or equivalent detectors: os.* and process.* resource attributes are discouraged (Discouraged Resource Attribute finding).

  • Honor the standard OTEL_* environment variables end-to-end. OTEL_EXPORTER_OTLP_*, OTEL_SERVICE_NAME, and OTEL_RESOURCE_ATTRIBUTES must all take effect at runtime. Do not invent custom environment variables (DEPLOYMENT_ENVIRONMENT, SERVICE_VERSION, ...) for values the standard variables already express, and never overwrite an attribute supplied via OTEL_RESOURCE_ATTRIBUTES with a code-level default — a hardcoded fallback like deployment.environment.name = "development" silently clobbers the deployment's real environment and misfiles every signal the service emits.

Required: Verification Report

Setup is not complete until you produce this report. It is a table with one row per checklist item above. Fill each row with artifacts from THIS run — the marker value you sent, an excerpt of the exported span dump, a trace id, the config value you changed. Never a restatement of the requirement, never a bare "done".

The table below is an illustrative example, not a report you can submit: every value in it is a placeholder showing the expected shape of evidence. Replace every cell with your own run's artifacts. If you did not run a check, write GAP — not run in that row and leave it visible — a missing or hand-waved row is itself a finding.

Example (illustrative values — replace every cell with your own run's evidence):

ItemCheck performedObserved evidence
Context threaded to data layertraced a request end-to-end and inspected the exported spansDB span is a CHILD of the HTTP server span (same trace id 4bf9…), no parentless CLIENT-kind roots
No SQL parameter values on any signalran a query with marker value MARKER_7f3a, inspected the exported DB span, SQL logs, and DB metricsdb.query.text shows only ? placeholders; MARKER_7f3a appears in no span, log, or metric
SDK configured declarativelychanged a value in configs/otel.yaml (e.g. the sampler ratio) and restarted without recompiling; renamed the file to confirm fallbacknew sampling behavior took effect from the file alone; with the file absent the app logged the no-op warning and still ran
service.instance.id injecteddumped the exported resource across two process startsservice.instance.id present as a UUID, and it differs between the two boots
Resource kept leandumped the exported resource attributesexactly service.name, service.version, service.instance.id, deployment.environment.name; no os.* or process.* keys
Standard OTEL_* honoredbooted with OTEL_SERVICE_NAME and OTEL_RESOURCE_ATTRIBUTES set to non-defaults, and OTEL_EXPORTER_OTLP_ENDPOINT pointed at a marker collectorservice.name/deployment.environment.name carry the supplied values on exported telemetry, with no code-level default overwriting them; the marker collector's log / receipt confirms telemetry arrived at the overridden endpoint (the endpoint is a destination, evidenced there, not on the spans)

A row you cannot fill with observed evidence is a visible gap — that item is not done. Do not delete the row, copy these example values, or write "N/A" to hide it; go run the check and record what you actually saw.

Recommended import path

For new code, use the root otelconf package (go.opentelemetry.io/contrib/otelconf) — it tracks the current schema and includes the propagator-from-YAML fix. The schema-pinned otelconf/v0.3.0 subpackage is for keeping existing configs unchanged.

Project Structure

internal/telemetry/
├── const.go          # Service scope and telemetry constants
├── setup.go          # SDK initialization (code below)
├── providers.go      # Provider management utilities
└── carriers.go       # Custom propagation carriers (if needed)
configs/
└── otel.yaml         # Declarative configuration

Setup Pattern

The core setup reads a YAML config file, injects runtime attributes, and creates an SDK instance that provides all three providers (tracer, meter, logger) plus a propagator.

package telemetry

import (
    "context"
    "errors"
    "fmt"
    "os"

    "github.com/google/uuid"
    "go.opentelemetry.io/contrib/bridges/otelzap"
    otelconf "go.opentelemetry.io/contrib/otelconf"
    "go.opentelemetry.io/otel"
    "go.opentelemetry.io/otel/log"
    "go.opentelemetry.io/otel/log/global"
    "go.opentelemetry.io/otel/metric"
    "go.opentelemetry.io/otel/propagation"
    semconv "go.opentelemetry.io/otel/semconv/v1.41.0"
    "go.opentelemetry.io/otel/trace"
    "go.uber.org/zap"
    "go.uber.org/zap/zapcore"
)

type Providers struct {
    TracerProvider trace.TracerProvider
    MeterProvider  metric.MeterProvider
    LoggerProvider log.LoggerProvider
    Logger         *zap.Logger
    Closer         func(ctx context.Context) error
}

func SetupTelemetry(ctx context.Context, serviceName, version, configFile string) (*Providers, error) {
    providers, sdk, err := providersFromConfig(ctx, serviceName, version, configFile)
    if err != nil {
        return nil, err
    }

    otel.SetTracerProvider(providers.TracerProvider)
    otel.SetMeterProvider(providers.MeterProvider)
    global.SetLoggerProvider(providers.LoggerProvider)

    if sdk != nil {
        otel.SetTextMapPropagator(sdk.Propagator())
    } else {
        otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(
            propagation.TraceContext{},
            propagation.Baggage{},
        ))
    }

    return providers, nil
}

func providersFromConfig(ctx context.Context, scope, version, cfgFile string) (*Providers, *otelconf.SDK, error) {
    b, err := os.ReadFile(cfgFile)
    if err != nil {
        if errors.Is(err, os.ErrNotExist) {
            logger := zap.Must(zap.NewProduction())
            logger.Warn("OpenTelemetry config file not found, using no-op providers",
                zap.String("config_file", cfgFile))
            return &Providers{
                TracerProvider: trace.NewNoOpTracerProvider(),
                MeterProvider:  metric.NewNoOpMeterProvider(),
                LoggerProvider: log.NewNoOpLoggerProvider(),
                Logger:         logger,
                Closer:         func(ctx context.Context) error { return nil },
            }, nil, nil
        }
        return nil, nil, fmt.Errorf("failed to read config file %s: %w", cfgFile, err)
    }

    b = []byte(os.ExpandEnv(string(b)))

    conf, err := otelconf.ParseYAML(b)
    if err != nil {
        return nil, nil, err
    }

    if conf.Resource == nil {
        conf.Resource = &otelconf.Resource{}
    }
    if conf.Resource.Attributes == nil {
        conf.Resource.Attributes = []otelconf.AttributeNameValue{}
    }
    conf.Resource.Attributes = insertAttribute(conf.Resource.Attributes,
        string(semconv.ServiceVersionKey), version)
    conf.Resource.Attributes = insertAttribute(conf.Resource.Attributes,
        string(semconv.ServiceInstanceIDKey), uuid.New().String())

    sdk, err := otelconf.NewSDK(
        otelconf.WithContext(ctx),
        otelconf.WithOpenTelemetryConfiguration(*conf),
    )
    if err != nil {
        return nil, nil, err
    }

    core := zapcore.NewTee(
        zapcore.NewCore(
            zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig()),
            zapcore.AddSync(os.Stdout),
            zapcore.InfoLevel,
        ),
        otelzap.NewCore(scope, otelzap.WithLoggerProvider(global.GetLoggerProvider())),
    )

    return &Providers{
        TracerProvider: sdk.TracerProvider(),
        MeterProvider:  sdk.MeterProvider(),
        LoggerProvider: sdk.LoggerProvider(),
        Logger:         zap.New(core),
        Closer:         sdk.Shutdown,
    }, &sdk, nil
}

func insertAttribute(attrs []otelconf.AttributeNameValue, name, value string) []otelconf.AttributeNameValue {
    for _, attr := range attrs {
        if attr.Name == name {
            return attrs
        }
    }
    return append(attrs, otelconf.AttributeNameValue{Name: name, Value: value})
}

Main Integration

func main() {
    ctx := context.Background()

    providers, err := telemetry.SetupTelemetry(ctx,
        telemetry.ServiceName,
        telemetry.ServiceVersion,
        "configs/otel.yaml")
    if err != nil {
        log.Fatalf("Failed to setup telemetry: %v", err)
    }

    defer func() {
        shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
        defer cancel()
        if err := providers.Closer(shutdownCtx); err != nil {
            providers.Logger.Error("Failed to shutdown telemetry", zap.Error(err))
        }
    }()

    tracer := otel.Tracer(telemetry.Scope)
    meter := otel.Meter(telemetry.Scope)

    // Application logic...
}

Key Details

  • No-op fallback: If the config file doesn't exist, the setup returns no-op providers instead of failing. The application runs without telemetry.
  • Runtime attributes: service.version and service.instance.id are injected programmatically because they vary per deployment, not per environment.
  • Zap bridge: The otelzap bridge sends structured logs to the OTel LoggerProvider, enabling log correlation with traces. Stdout JSON output is preserved via a tee.
  • 10-second shutdown timeout: Bounds shutdown so a hung exporter cannot block process exit.

Cross-References

  • Reference: otel-go skill — references/declarative-setup.md for otelconf fetch table, import path facts, schema version mapping; references/breaking-changes.md for SDK/contrib upgrade audits; references/instrumentation-libraries.md for wiring DB/HTTP/gRPC libraries, threading context.Context into the data layer (avoid detached CLIENT-root DB spans), and keeping PII out of db.query.text.
  • General conventions: ollygarden-otel-declarative-config — anti-patterns and common YAML patterns.

Keep looking

Skills are one crate of 328,083. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.