mcpField
G
golang-observability-opentelemetry

golang-observability-opentelemetry

github.com/bobmatnyc開発78415 回インストール15 週間前に更新
リポジトリ

golang-observability-opentelemetry について

OpenTelemetry、Prometheus、slogを使った、Go開発者向けの可観測性ガイドです。トレース、メトリクス、ログの相関に加え、ヘルスチェックやグレースフルシャットダウンの実装例を扱います。

主な機能

  1. 01OpenTelemetryによる分散トレース
  2. 02Prometheusのメトリクスとエンドポイント
  3. 03相関ID付き構造化slogログ
  4. 04HTTP/gRPCの計装パターン
  5. 05ヘルスチェックとグレースフルシャットダウン

活用シーン

  1. 01マイクロサービスに本番可観測性を追加
  2. 02サービス間のリクエスト追跡と性能問題の調査
  3. 03ログ、トレース、メトリクスを関連付けたデバッグ

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/bobmatnyc/claude-mpm-skills --skill golang-observability-opentelemetry

インストール後、Claude Code で/golang-observability-opentelemetryを入力して読み込まれたか確認します。

同梱ファイルmetadata.json

詳細

OpenTelemetry を使用した Go のオブザーバビリティ

概要

現代の Go アプリケーションでは、トレース、メトリクス、ログという 3 つの柱を通じた包括的なオブザーバビリティが求められます。OpenTelemetry はベンダーに依存しない分散トレーシングの計装を提供し、Prometheus は強力なメトリクス収集機能を備え、Go の slog パッケージ(1.21 以降)はオーバーヘッドを最小限に抑えた構造化ログを実現します。

主な機能:

  • 🔍 OpenTelemetry:コンテキスト伝播を備えた分散トレーシング
  • 📊 Prometheus:/metrics エンドポイントを使用したメトリクス収集
  • 📝 構造化ログ:JSON 形式と相関 ID に対応した slog
  • 🎯 自動計装:HTTP/gRPC ミドルウェアのパターン
  • 💚 ヘルスチェック:Kubernetes 対応のレディネス/ライブネスプローブ
  • 🔄 グレースフルシャットダウン:エクスポーターの正常なシャットダウンとシグナル処理

このスキルを使用する場面

以下の場合にこのスキルを有効にしてください:

  • 本番環境のオブザーバビリティのためにマイクロサービスを計装する
  • サービス境界を越えた分散トレーシングを設定する
  • Prometheus/Grafana を使用した運用ダッシュボードを作成する
  • 本番環境のパフォーマンス問題やボトルネックをデバッグする
  • SLO を実装し、SLI を監視する
  • 既存の Go アプリケーションにオブザーバビリティを追加する
  • デバッグのためにログ、トレース、メトリクスを関連付ける

オブザーバビリティの基本原則

3 つの柱
  1. トレース:分散システム全体にわたるリクエストの流れを把握する
  2. メトリクス:システムの動作とパフォーマンスを経時的に測定する
  3. ログ:デバッグや監査のために個々のイベントを記録する
相関戦略

3 つの柱すべてで共通の識別子を共有する必要があります:

  • トレース ID:1 つのリクエスト内のすべての操作を関連付ける
  • スパン ID:トレース内の特定の操作を識別する
  • リクエスト ID:ログをトレースおよびメトリクスと関連付ける

OpenTelemetry の統合

インストール
go get go.opentelemetry.io/otel
go get go.opentelemetry.io/otel/sdk
go get go.opentelemetry.io/otel/exporters/jaeger
go get go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp
基本設定
package main

import (
    "context"
    "log"

    "go.opentelemetry.io/otel"
    "go.opentelemetry.io/otel/exporters/jaeger"
    "go.opentelemetry.io/otel/sdk/resource"
    sdktrace "go.opentelemetry.io/otel/sdk/trace"
    semconv "go.opentelemetry.io/otel/semconv/v1.21.0"
)

func initTracer(serviceName string) (*sdktrace.TracerProvider, error) {
    // Create Jaeger exporter
    exporter, err := jaeger.New(jaeger.WithCollectorEndpoint(
        jaeger.WithEndpoint("http://localhost:14268/api/traces"),
    ))
    if err != nil {
        return nil, err
    }

    // Create resource with service name
    res, err := resource.Merge(
        resource.Default(),
        resource.NewWithAttributes(
            semconv.SchemaURL,
            semconv.ServiceName(serviceName),
            semconv.ServiceVersion("1.0.0"),
        ),
    )
    if err != nil {
        return nil, err
    }

    // Create tracer provider
    tp := sdktrace.NewTracerProvider(
        sdktrace.WithBatcher(exporter),
        sdktrace.WithResource(res),
        sdktrace.WithSampler(sdktrace.AlwaysSample()), // Use probability sampler in production
    )

    otel.SetTracerProvider(tp)
    return tp, nil
}

func main() {
    tp, err := initTracer("order-service")
    if err != nil {
        log.Fatal(err)
    }
    defer func() {
        if err := tp.Shutdown(context.Background()); err != nil {
            log.Printf("Error shutting down tracer: %v", err)
        }
    }()

    // Application code...
}
スパンの作成
import (
    "context"

    "go.opentelemetry.io/otel"
    "go.opentelemetry.io/otel/attribute"
    "go.opentelemetry.io/otel/codes"
    "go.opentelemetry.io/otel/trace"
)

func ProcessOrder(ctx context.Context, order Order) error {
    tracer := otel.Tracer("order-service")
    ctx, span := tracer.Start(ctx, "ProcessOrder")
    defer span.End()

    // Add attributes
    span.SetAttributes(
        attribute.String("order.id", order.ID),
        attribute.Int("order.items", len(order.Items)),
        attribute.Float64("order.total", order.Total),
    )

    // Validate order (creates child span)
    if err := validateOrder(ctx, order); err != nil {
        span.RecordError(err)
        span.SetStatus(codes.Error, "validation failed")
        return err
    }

    // Fulfill order
    if err := fulfillOrder(ctx, order); err != nil {
        span.RecordError(err)
        span.SetStatus(codes.Error, "fulfillment failed")
        return err
    }

    span.SetStatus(codes.Ok, "order processed successfully")
    return nil
}

func validateOrder(ctx context.Context, order Order) error {
    _, span := otel.Tracer("order-service").Start(ctx, "validateOrder")
    defer span.End()

    // Validation logic...
    return nil
}
HTTP ミドルウェアの計装
import (
    "net/http"

    "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
)

func main() {
    // Wrap handler with automatic tracing
    handler := http.HandlerFunc(orderHandler)
    wrappedHandler := otelhttp.NewHandler(handler, "order-handler")

    http.Handle("/orders", wrappedHandler)
    http.ListenAndServe(":8080", nil)
}

// Manual instrumentation for more control
func orderHandler(w http.ResponseWriter, r *http.Request) {
    ctx := r.Context()
    tracer := otel.Tracer("order-service")

    ctx, span := tracer.Start(ctx, "orderHandler")
    defer span.End()

    // Extract order ID from request
    orderID := r.URL.Query().Get("id")
    span.SetAttributes(attribute.String("order.id", orderID))

    // Process order with propagated context
    order, err := fetchOrder(ctx, orderID)
    if err != nil {
        span.RecordError(err)
        http.Error(w, "Order not found", http.StatusNotFound)
        return
    }

    // ... handle response
}

Prometheus メトリクス

インストール
go get github.com/prometheus/client_golang/prometheus
go get github.com/prometheus/client_golang/prometheus/promhttp
メトリクスの種類とパターン
package metrics

import (
    "github.com/prometheus/client_golang/prometheus"
    "github.com/prometheus/client_golang/prometheus/promauto"
)

var (
    // Counter: Monotonically increasing value
    httpRequestsTotal = promauto.NewCounterVec(
        prometheus.CounterOpts{
            Name: "http_requests_total",
            Help: "Total number of HTTP requests",
        },
        []string{"method", "path", "status"},
    )

    // Gauge: Value that can go up or down
    activeConnections = promauto.NewGauge(
        prometheus.GaugeOpts{
            Name: "active_connections",
            Help: "Number of active connections",
        },
    )

    // Histogram: Observations bucketed by value
    httpRequestDuration = promauto.NewHistogramVec(
        prometheus.HistogramOpts{
            Name:    "http_request_duration_seconds",
            Help:    "HTTP request duration in seconds",
            Buckets: prometheus.DefBuckets, // [0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10]
        },
        []string{"method", "path"},
    )

    // Summary: Similar to histogram but calculates quantiles
    dbQueryDuration = promauto.NewSummaryVec(
        prometheus.SummaryOpts{
            Name:       "db_query_duration_seconds",
            Help:       "Database query duration",
            Objectives: map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.99: 0.001},
        },
        []string{"query_type"},
    )
)
メトリクスミドルウェア
import (
    "net/http"
    "strconv"
    "time"

    "github.com/prometheus/client_golang/prometheus/promhttp"
)

// Metrics middleware that instruments all HTTP handlers
func MetricsMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()

        // Track active connections
        activeConnections.Inc()
        defer activeConnections.Dec()

        // Wrap response writer to capture status code
        rw := &responseWriter{ResponseWriter: w, statusCode: http.StatusOK}

        // Call next handler
        next.ServeHTTP(rw, r)

        // Record metrics
        duration := time.Since(start).Seconds()
        httpRequestDuration.WithLabelValues(r.Method, r.URL.Path).Observe(duration)
        httpRequestsTotal.WithLabelValues(r.Method, r.URL.Path, strconv.Itoa(rw.statusCode)).Inc()
    })
}

type responseWriter struct {
    http.ResponseWriter
    statusCode int
}

func (rw *responseWriter) WriteHeader(code int) {
    rw.statusCode = code
    rw.ResponseWriter.WriteHeader(code)
}

// Expose metrics endpoint
func main() {
    http.Handle("/metrics", promhttp.Handler())

    handler := MetricsMiddleware(http.HandlerFunc(orderHandler))
    http.Handle("/orders", handler)

    http.ListenAndServe(":8080", nil)
}
カスタムメトリクスの例
func ProcessPayment(ctx context.Context, payment Payment) error {
    timer := prometheus.NewTimer(dbQueryDuration.WithLabelValues("payment_insert"))
    defer timer.ObserveDuration()

    // Process payment
    if err := db.InsertPayment(payment); err != nil {
        httpRequestsTotal.WithLabelValues("POST", "/payments", "500").Inc()
        return err
    }

    httpRequestsTotal.WithLabelValues("POST", "/payments", "200").Inc()
    return nil
}

slog による構造化ログ

基本設定(Go 1.21 以降)
package main

import (
    "context"
    "log/slog"
    "os"
)

func initLogger() *slog.Logger {
    // JSON logger for production
    handler := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
        Level: slog.LevelInfo,
        AddSource: true, // Include file:line information
    })

    logger := slog.New(handler)
    slog.SetDefault(logger) // Set as default logger
    return logger
}

func main() {
    logger := initLogger()

    logger.Info("service starting",
        "service", "order-service",
        "version", "1.0.0",
        "port", 8080,
    )
}
コンテキスト対応ロギング
import (
    "context"
    "log/slog"

    "go.opentelemetry.io/otel/trace"
)

// Add trace context to logger
func LoggerWithTrace(ctx context.Context) *slog.Logger {
    span := trace.SpanFromContext(ctx)
    spanCtx := span.SpanContext()

    return slog.With(
        "trace_id", spanCtx.TraceID().String(),
        "span_id", spanCtx.SpanID().String(),
    )
}

func HandleRequest(ctx context.Context, req Request) error {
    logger := LoggerWithTrace(ctx)

    logger.Info("processing request",
        "request_id", req.ID,
        "method", req.Method,
        "path", req.Path,
    )

    if err := processRequest(ctx, req); err != nil {
        logger.Error("request failed",
            "error", err,
            "duration_ms", time.Since(req.StartTime).Milliseconds(),
        )
        return err
    }

    logger.Info("request completed successfully",
        "duration_ms", time.Since(req.StartTime).Milliseconds(),
    )
    return nil
}
ログレベルと構造化フィールド
func ProcessOrder(ctx context.Context, order Order) error {
    logger := LoggerWithTrace(ctx).With(
        "order_id", order.ID,
        "user_id", order.UserID,
    )

    logger.Debug("validating order", "items", len(order.Items))

    if len(order.Items) == 0 {
        logger.Warn("empty order received")
        return ErrEmptyOrder
    }

    logger.Info("order validation passed")

    if err := fulfillOrder(ctx, order); err != nil {
        logger.Error("fulfillment failed",
            "error", err,
            slog.Group("order_details",
                "total", order.Total,
                "items", len(order.Items),
            ),
        )
        return err
    }

    logger.Info("order processed successfully",
        "total", order.Total,
    )
    return nil
}

ヘルスチェックとグレースフルシャットダウン

ヘルスチェックエンドポイント
import (
    "context"
    "database/sql"
    "encoding/json"
    "net/http"
    "time"
)

type HealthChecker struct {
    db *sql.DB
    // Add other dependencies
}

type HealthStatus struct {
    Status      string            `json:"status"`
    Version     string            `json:"version"`
    Checks      map[string]string `json:"checks"`
    Timestamp   time.Time         `json:"timestamp"`
}

// Liveness probe - is the app running?
func (hc *HealthChecker) LivenessHandler(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(http.StatusOK)
    json.NewEncoder(w).Encode(map[string]string{
        "status": "alive",
    })
}

// Readiness probe - is the app ready to serve traffic?
func (hc *HealthChecker) ReadinessHandler(w http.ResponseWriter, r *http.Request) {
    ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
    defer cancel()

    status := HealthStatus{
        Status:    "ready",
        Version:   "1.0.0",
        Checks:    make(map[string]string),
        Timestamp: time.Now(),
    }

    // Check database
    if err := hc.db.PingContext(ctx); err != nil {
        status.Status = "not_ready"
        status.Checks["database"] = "unhealthy: " + err.Error()
        w.WriteHeader(http.StatusServiceUnavailable)
    } else {
        status.Checks["database"] = "healthy"
    }

    // Add more dependency checks (Redis, external APIs, etc.)

    w.Header().Set("Content-Type", "application/json")
    if status.Status == "ready" {
        w.WriteHeader(http.StatusOK)
    }
    json.NewEncoder(w).Encode(status)
}
グレースフルシャットダウン
import (
    "context"
    "net/http"
    "os"
    "os/signal"
    "syscall"
    "time"
)

func main() {
    // Initialize tracer
    tp, err := initTracer("order-service")
    if err != nil {
        log.Fatal(err)
    }

    // Setup HTTP server
    server := &http.Server{
        Addr:    ":8080",
        Handler: setupRoutes(),
    }

    // Channel for shutdown signals
    shutdown := make(chan os.Signal, 1)
    signal.Notify(shutdown, os.Interrupt, syscall.SIGTERM)

    // Start server in goroutine
    go func() {
        slog.Info("server starting", "port", 8080)
        if err := server.ListenAndServe(); err != http.ErrServerClosed {
            log.Fatal(err)
        }
    }()

    // Wait for shutdown signal
    <-shutdown
    slog.Info("shutdown signal received")

    // Create shutdown context with timeout
    ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
    defer cancel()

    // Shutdown HTTP server
    slog.Info("shutting down HTTP server")
    if err := server.Shutdown(ctx); err != nil {
        slog.Error("HTTP server shutdown error", "error", err)
    }

    // Shutdown tracer provider (flush spans)
    slog.Info("shutting down tracer")
    if err := tp.Shutdown(ctx); err != nil {
        slog.Error("tracer shutdown error", "error", err)
    }

    slog.Info("shutdown complete")
}

計装の完全な例

package main

import (
    "context"
    "database/sql"
    "log/slog"
    "net/http"
    "os"
    "time"

    "github.com/prometheus/client_golang/prometheus/promhttp"
    "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
    "go.opentelemetry.io/otel"
    "go.opentelemetry.io/otel/attribute"
)

type Server struct {
    db     *sql.DB
    logger *slog.Logger
}

func (s *Server) orderHandler(w http.ResponseWriter, r *http.Request) {
    ctx := r.Context()

    // Get tracer and create span
    tracer := otel.Tracer("order-service")
    ctx, span := tracer.Start(ctx, "orderHandler")
    defer span.End()

    // Create context-aware logger with trace ID
    logger := s.logger.With(
        "trace_id", span.SpanContext().TraceID().String(),
        "request_id", r.Header.Get("X-Request-ID"),
    )

    orderID := r.URL.Query().Get("id")
    span.SetAttributes(attribute.String("order.id", orderID))

    logger.Info("fetching order", "order_id", orderID)

    // Fetch order from database
    order, err := s.fetchOrder(ctx, orderID)
    if err != nil {
        span.RecordError(err)
        logger.Error("failed to fetch order", "error", err)
        http.Error(w, "Order not found", http.StatusNotFound)
        return
    }

    logger.Info("order fetched successfully",
        "order_id", orderID,
        "items", len(order.Items),
    )

    // Return order as JSON
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(order)
}

func (s *Server) fetchOrder(ctx context.Context, orderID string) (*Order, error) {
    _, span := otel.Tracer("order-service").Start(ctx, "fetchOrder")
    defer span.End()

    // Time database query
    start := time.Now()

    var order Order
    err := s.db.QueryRowContext(ctx, "SELECT * FROM orders WHERE id = ?", orderID).Scan(&order)

    duration := time.Since(start).Seconds()
    dbQueryDuration.WithLabelValues("select_order").Observe(duration)

    return &order, err
}

func setupRoutes(s *Server, hc *HealthChecker) http.Handler {
    mux := http.NewServeMux()

    // Health endpoints (no tracing needed)
    mux.HandleFunc("/health", hc.LivenessHandler)
    mux.HandleFunc("/ready", hc.ReadinessHandler)
    mux.Handle("/metrics", promhttp.Handler())

    // Business endpoints (with tracing)
    orderHandler := http.HandlerFunc(s.orderHandler)
    mux.Handle("/orders", otelhttp.NewHandler(orderHandler, "orders"))

    // Wrap everything with metrics middleware
    return MetricsMiddleware(mux)
}

判断ツリー

OpenTelemetry を使用する場面

OpenTelemetry を使用する場合:

  • 複数のサービスで構成される分散システムを構築する
  • サービス境界を越えてリクエストをトレースする必要がある
  • マイクロサービスのパフォーマンス問題をデバッグする
  • ベンダーに依存しないオブザーバビリティが必要(バックエンドを簡単に切り替えられる)
  • トレース、メトリクス、ログ間の相関が必要である

OpenTelemetry を使用しない場合:

  • シンプルなモノリシックアプリケーションを構築する
  • パフォーマンスのオーバーヘッドが重大な問題となる(サンプリングを検討してください)
  • チームにオブザーバビリティのインフラストラクチャ(Jaeger、Zipkin)がない
Prometheus を使用する場面

Prometheus を使用する場合:

  • 監視とアラートに時系列メトリクスが必要である
  • 運用ダッシュボード(Grafana)を構築する
  • SLO の達成状況を測定するために SLI を計測する
  • ビジネスメトリクス(リクエスト数/秒、コンバージョン率)を追跡する
  • Kubernetes/コンテナ化された環境である

Prometheus を使用しない場合:

  • カーディナリティの高いメトリクスが必要である(Prometheus には上限があります)
  • メトリクスを長期間保存する必要がある(Thanos/Cortex を使用してください)
  • プッシュ型メトリクスが必要である(Prometheus はプル型です)
slog を使用する場面

slog を使用する場合:

  • Go 1.21 以降のプロジェクトである(標準ライブラリで、依存関係が不要)
  • JSON 出力による構造化ログが必要である
  • 割り当てを最小限に抑えた高性能ロギングが必要である
  • ログ集約システム(Loki、ELK)と統合する

slog を使用しない場合:

  • Go < 1.21 である(代わりに zap または zerolog を使用してください)
  • 複雑なログルーティングやフィルタリングが必要である(zap を使用してください)
  • 非常に特定の機能(監査証跡など)が必要である
サンプリング戦略の決定

常にサンプリングする場合:

  • 開発/ステージング環境
  • 総トラフィックが 100 リクエスト/秒未満
  • 特定の問題をデバッグする場合

確率的サンプリングを行う場合:

  • トラフィックが中程度の本番環境(100~10K リクエスト/秒)
  • 通常のサンプルレート:1~10%

テールベースサンプリングを行う場合:

  • トラフィックの多い本番環境(>10K リクエスト/秒)
  • エラーと処理の遅いリクエストのみをサンプリング
  • テールサンプリングプロセッサー(OpenTelemetry Collector)が必要

避けるべきアンチパターン

❌ コンテキストを伝播しない

誤り:トレースコンテキストを途切れさせる

func processOrder(order Order) error {
    // Creates new context, loses trace!
    ctx := context.Background()
    return validateOrder(ctx, order)
}

正しい方法:呼び出しチェーン全体でコンテキストを伝播する

func processOrder(ctx context.Context, order Order) error {
    // Propagates trace context
    return validateOrder(ctx, order)
}
❌ カーディナリティの爆発

誤り:ラベル値に上限がない

// user_id can have millions of values!
httpRequests.WithLabelValues(r.Method, r.URL.Path, userID).Inc()

正しい方法:上限のあるラベルを使用する

// Only method and path (bounded values)
httpRequests.WithLabelValues(r.Method, r.URL.Path).Inc()
// Track user-specific metrics separately if needed
❌ 機密データをログに記録する

誤り:個人識別情報(PII)やシークレットを露出させる

logger.Info("user login",
    "email", user.Email,        // PII!
    "password", user.Password,  // CRITICAL!
    "token", authToken,         // SECRET!
)

正しい方法:機密情報をマスキングする

logger.Info("user login",
    "user_id", user.ID,  // Safe identifier
    "method", "password",
)
❌ スパンを終了しない

誤り:スパンによってメモリリークが発生する

func processOrder(ctx context.Context) error {
    ctx, span := tracer.Start(ctx, "processOrder")
    // Missing defer span.End()!

    if err := validate(); err != nil {
        return err  // Span never closed!
    }

    return nil
}

正しい方法:常に span.End() を defer で呼び出す

func processOrder(ctx context.Context) error {
    ctx, span := tracer.Start(ctx, "processOrder")
    defer span.End()  // Always runs

    if err := validate(); err != nil {
        span.RecordError(err)
        return err
    }

    return nil
}
❌ メトリクスを同期的にエクスポートする

誤り:メトリクスのエクスポートでリクエストをブロックする

// Synchronous export blocks HTTP handler
exporter := jaeger.New(jaeger.WithCollectorEndpoint(...))
tp := sdktrace.NewTracerProvider(
    sdktrace.WithSyncer(exporter),  // BAD: Synchronous!
)

正しい方法:非同期エクスポートにはバッチ処理を使用する

// Batching exports asynchronously
tp := sdktrace.NewTracerProvider(
    sdktrace.WithBatcher(exporter),  // GOOD: Async batching
)
❌ グレースフルシャットダウンがない

誤り:シャットダウン時にトレースが失われる

func main() {
    tp, _ := initTracer("service")
    // Missing shutdown - spans lost!
    http.ListenAndServe(":8080", nil)
}

正しい方法:エクスポーターを適切にシャットダウンする

func main() {
    tp, _ := initTracer("service")
    defer tp.Shutdown(context.Background())

    // Handle signals and graceful shutdown
    server.ListenAndServe()
}

ベストプラクティス

  1. コンテキストの伝播:呼び出しチェーン全体で常に context.Context を渡します
  2. 上限のあるラベル:メトリクスのラベルのカーディナリティを 1000 通り未満に抑えます
  3. サンプリング:トラフィックの多い本番環境では確率的サンプリングを使用します
  4. 相関 ID:ログに trace_id を含めて相関付けに利用します
  5. ヘルスチェック:/health(ライブネス)と /ready(レディネス)の両方を実装します
  6. グレースフルシャットダウン:終了前にトレースとメトリクスをフラッシュします
  7. エラーの記録:自動エラー追跡には span.RecordError() を使用します
  8. メトリクスの命名:Prometheus の命名規則(_total、_seconds)に従います
  9. ログレベル:適切なレベル(Debug、Info、Warn、Error)を使用します
  10. 自動計装:可能な場合は HTTP/gRPC にミドルウェアを使用します

メトリクスの命名規則

Prometheus のベストプラクティスに従います。

カウンターメトリクス(常に増加):

  • http_requests_total(http_requests ではない)
  • payment_transactions_total
  • errors_total

ゲージメトリクス(増減する):

  • active_connections
  • queue_size
  • memory_usage_bytes

ヒストグラム/サマリーメトリクス(観測値):

  • http_request_duration_seconds(_milliseconds ではない)
  • db_query_duration_seconds
  • response_size_bytes

ラベルの命名:

  • http_method ではなく method を使用します
  • status_code や http_status ではなく status を使用します
  • camelCase ではなく snake_case を使用します

リソース

公式ドキュメント:

最近のガイド(2025年):

  • 「Go のオブザーバビリティ:2025 年に現場のエンジニアが語ること」(Quesma Blog)
  • 「OpenTelemetry メトリクスによる Go アプリの監視」(Better Stack、2025年)
  • Prometheus のベストプラクティス:https://prometheus.io/docs/practices/naming/

関連スキル:

  • golang-web-frameworks:HTTP サーバーのパターンとミドルウェア
  • golang-testing-strategies:計装されたコードのテスト
  • verification-before-completion:オブザーバビリティの設定を検証する

クイックリファレンス

OpenTelemetry を初期化する
tp, _ := initTracer("service-name")
defer tp.Shutdown(context.Background())
スパンを作成する
ctx, span := otel.Tracer("name").Start(ctx, "operation")
defer span.End()
span.SetAttributes(attribute.String("key", "value"))
メトリクスを定義する
counter := promauto.NewCounterVec(opts, []string{"label"})
histogram := promauto.NewHistogramVec(opts, []string{"label"})
構造化ログ
logger := slog.With("trace_id", traceID)
logger.Info("message", "key", value)
ヘルスチェック
http.HandleFunc("/health", livenessHandler)
http.HandleFunc("/ready", readinessHandler)

トークンの推定数:~5,000 トークン(エントリーポイント+全コンテンツ) バージョン:1.0.0 最終更新日:2025-12-03

コメント