# Golang Observability

> When to activate: Go logging with slog, structured logging, metrics with Prometheus, distributed tracing with OpenTelemetry

- Skill: `mattakushi432/golang-observability` (Agent Skill)
- Install (CLI): `npx skillmds@latest add mattakushi432/golang-observability`
- Raw SKILL.md: https://api.skillmd.com/api/skills/mattakushi432/golang-observability/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: Mattakushi432 (https://skillmd.com/u/mattakushi432)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/mattakushi432/golang-observability

---


# Go Observability Patterns

## Structured Logging with slog (Go 1.21+)

```go
import "log/slog"

func main() {
    // JSON handler for production
    logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
        Level:     slog.LevelInfo,
        AddSource: true,
    }))
    slog.SetDefault(logger)

    // Structured fields
    slog.Info("server started", "port", 8080, "env", "production")
    slog.Error("request failed", "err", err, "method", r.Method, "path", r.URL.Path)
    slog.Warn("rate limit exceeded", "ip", clientIP, "limit", rps)
}

// Logger with context (request-scoped)
func withRequestLogger(ctx context.Context, requestID string) *slog.Logger {
    return slog.Default().With("requestID", requestID)
}

// Middleware that injects logger into context
func loggingMiddleware(logger *slog.Logger) func(http.Handler) http.Handler {
    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            reqLogger := logger.With(
                "requestID", r.Header.Get("X-Request-ID"),
                "method", r.Method,
                "path", r.URL.Path,
            )
            ctx := context.WithValue(r.Context(), loggerKey{}, reqLogger)
            start := time.Now()
            next.ServeHTTP(w, r.WithContext(ctx))
            reqLogger.Info("request completed", "duration", time.Since(start))
        })
    }
}

func LoggerFromContext(ctx context.Context) *slog.Logger {
    if l, ok := ctx.Value(loggerKey{}).(*slog.Logger); ok { return l }
    return slog.Default()
}
```

## Prometheus Metrics

```go
import (
    "github.com/prometheus/client_golang/prometheus"
    "github.com/prometheus/client_golang/prometheus/promauto"
    "github.com/prometheus/client_golang/prometheus/promhttp"
)

var (
    httpRequestsTotal = promauto.NewCounterVec(prometheus.CounterOpts{
        Name: "http_requests_total",
        Help: "Total HTTP requests",
    }, []string{"method", "path", "status"})

    httpRequestDuration = promauto.NewHistogramVec(prometheus.HistogramOpts{
        Name:    "http_request_duration_seconds",
        Help:    "HTTP request duration",
        Buckets: prometheus.DefBuckets,
    }, []string{"method", "path"})

    activeConnections = promauto.NewGauge(prometheus.GaugeOpts{
        Name: "active_connections",
        Help: "Number of active connections",
    })
)

// Instrumented response writer
type statusRecorder struct {
    http.ResponseWriter
    status int
}

func (r *statusRecorder) WriteHeader(status int) {
    r.status = status
    r.ResponseWriter.WriteHeader(status)
}

func metricsMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        rec := &statusRecorder{ResponseWriter: w, status: 200}
        start := time.Now()
        activeConnections.Inc()
        defer activeConnections.Dec()

        next.ServeHTTP(rec, r)

        httpRequestsTotal.WithLabelValues(r.Method, r.URL.Path, strconv.Itoa(rec.status)).Inc()
        httpRequestDuration.WithLabelValues(r.Method, r.URL.Path).Observe(time.Since(start).Seconds())
    })
}

// Expose metrics endpoint
mux.Handle("/metrics", promhttp.Handler())
```

## OpenTelemetry Tracing

```go
import (
    "go.opentelemetry.io/otel"
    "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
    "go.opentelemetry.io/otel/sdk/trace"
)

func initTracer(endpoint string) (func(context.Context) error, error) {
    exporter, err := otlptracegrpc.New(context.Background(),
        otlptracegrpc.WithEndpoint(endpoint),
        otlptracegrpc.WithInsecure(),
    )
    if err != nil { return nil, err }

    tp := trace.NewTracerProvider(
        trace.WithBatcher(exporter),
        trace.WithResource(resource.NewWithAttributes(
            semconv.SchemaURL,
            semconv.ServiceName("my-service"),
            semconv.ServiceVersion("1.0.0"),
        )),
    )
    otel.SetTracerProvider(tp)
    return tp.Shutdown, nil
}

// Instrument a function
var tracer = otel.Tracer("my-service")

func (s *Service) ProcessOrder(ctx context.Context, orderID string) error {
    ctx, span := tracer.Start(ctx, "ProcessOrder")
    defer span.End()

    span.SetAttributes(attribute.String("order.id", orderID))

    if err := s.repo.GetOrder(ctx, orderID); err != nil {
        span.RecordError(err)
        span.SetStatus(codes.Error, err.Error())
        return err
    }
    return nil
}
```

## Common Anti-Patterns

- **`log.Printf` in new code** — use `slog` for structured, level-aware logging
- **Logging inside library packages** — accept a logger via interface; don't use global logger in libs
- **High-cardinality label values in Prometheus** — never use user IDs or paths with variables as labels
- **Not sampling traces** — 100% trace sampling in high-traffic services is expensive; use head-based sampling
- **Missing span propagation** — always pass `ctx` through your call stack to maintain trace context

