背景
オブザーバビリティ環境を整備する中で、Go で Google Cloud Logging と Cloud Trace のログとトレースを紐付けることで横断調査しやすいよう対応した際のメモです。
環境
- Go 1.26.2
- zap v1.21.0
- zapdriver v1.3.1
対応
前提
Cloud Logging にはいくつかの予約フィールドがあります。
その中で次のフィールドを設定すると、Cloud Logging と Cloud Trace の関連付けができます。
| JSON キー | 中身 |
|---|---|
logging.googleapis.com/trace |
projects/<PROJECT_ID>/traces/<TRACE_ID>(TRACE_ID 単体でも可) |
logging.googleapis.com/spanId |
16 桁 hex(例: 000000000000004a) |
logging.googleapis.com/trace_sampled |
true / false |
各ロガーで、上記フィールドに必要な情報を付与します。OTel なら値は以下の様に trace.SpanContext から取れます。
sc := span.SpanContext() traceName := fmt.Sprintf("projects/%s/traces/%s", projectID, sc.TraceID().String()) spanID := sc.SpanID().String() sampled := sc.IsSampled()
実装方針
先ほどの値を毎回の Info に 3 フィールドを直書きしても動きますが、本番でそれを愚直に書くと実装漏れが出るので、ロガーのハンドラー層で共通実装すると良いです。
slog と zap での実装例を紹介します。
slog
slogは次のように出力時にHandlerインタフェースのHandle()を通ります。
slog.InfoContext(ctx, "charged", ...) └─ Default().log(ctx, LevelInfo, "charged", ...) ├─ ctx == nil なら context.Background() ├─ Logger.Enabled(ctx, level) │ └─ Handler.Enabled(ctx, level) // false ならここで終了(Handle は呼ばない) ├─ runtime.Callers で PC を取る ├─ slog.NewRecord(now, level, msg, pc) ├─ record.Add(args...) └─ Handler.Handle(ctx, record) // ← ここが spanHandler.Handle ├─ SpanContextFromContext(ctx) で予約キーを AddAttrs └─ 内側の JSONHandler.Handle(ctx, record) └─ commonHandler.handle(record) が JSON を Write
なのでそこに埋め込んであげれば共通実装が可能です。
func setupLogging(projectID string) { h := slog.NewJSONHandler(os.Stdout, nil) slog.SetDefault(slog.New(&spanHandler{ Handler: h, projectID: projectID, })) } type spanHandler struct { slog.Handler projectID string } func (h *spanHandler) Handle(ctx context.Context, r slog.Record) error { if sc := trace.SpanContextFromContext(ctx); sc.IsValid() { r.AddAttrs( slog.String("logging.googleapis.com/trace", fmt.Sprintf("projects/%s/traces/%s", h.projectID, sc.TraceID().String())), slog.String("logging.googleapis.com/spanId", sc.SpanID().String()), slog.Bool("logging.googleapis.com/trace_sampled", sc.IsSampled()), ) } return h.Handler.Handle(ctx, r) } func (h *spanHandler) WithAttrs(attrs []slog.Attr) slog.Handler { return &spanHandler{Handler: h.Handler.WithAttrs(attrs), projectID: h.projectID} } func (h *spanHandler) WithGroup(name string) slog.Handler { return &spanHandler{Handler: h.Handler.WithGroup(name), projectID: h.projectID} }
公式の Go instrumentation sample の実装方法はこちらですね。
注意点として、ctxを引き継げるようslog.Info ではなく slog.InfoContext を使いその時点の ctx を渡します。
Zap
Zap本家のAPIはctxを受け取るシグネチャになっていないので、ヘルパー関数を用意するなど軽くラップしてあげるのが良いでしょう。
またCloud Loggingであれば以前紹介した
のzapdriverにある、zapdriver.TraceContext に任せるとシンプルに書けます。
func gcpTrace(ctx context.Context, projectID string) []zap.Field { sc := trace.SpanContextFromContext(ctx) if !sc.IsValid() { return nil } return zapdriver.TraceContext(sc.TraceID().String(), sc.SpanID().String(), sc.IsSampled(), projectID) } func Info(ctx context.Context, l *zap.Logger, msg string, fields ...zap.Field) { l.Info(msg, append(gcpTrace(ctx, projectID), fields...)...) }
使う時はこんな感じです。
// 呼び出し Info(ctx, logger, "charged") // ヘルパーを使わず直接足す場合 sc := trace.SpanContextFromContext(ctx) logger.Info("charged", zapdriver.TraceContext( sc.TraceID().String(), sc.SpanID().String(), sc.IsSampled(), projectID, )...)
まとめ
Google Cloud Logging と Cloud Trace のログとトレースを関連付ける方法を紹介しました。