zerologでCloud Logging用の構造化ログを作る

Google Cloud上でアプリケーションを動かす際に、[Cloud Loggingが求める構造](https://cloud.google.com/logging/docs/structured-logging)を使うと、Cloud TraceやError Reportingなどと相性がいいので、Cloud Logging用にログの形式を設定する。

**logger側**
```go
package log

import (
	"context"
	"fmt"
	"os"
	"path"
	"runtime"
	"strconv"

	"cloud.google.com/go/compute/metadata"
	"github.com/rs/zerolog"
	"github.com/rs/zerolog/log"
	"go.opentelemetry.io/otel/trace"
)

// Google Cloud Project ID
var projectID string

// init Loggerの初期設定を行う。
func init() {
	projectID, _ = metadata.ProjectID()
	log.Logger = zerolog.New(os.Stdout).
		Hook(zerolog.HookFunc(withTrace)).
		With().Timestamp().
		Logger()
	zerolog.LevelFieldName = "severity"
	zerolog.LevelWarnValue = "warning"
	zerolog.TimestampFieldName = "timestamp"
	zerolog.ErrorStackFieldName = "stack_trace"
	zerolog.ErrorStackMarshaler = func(err error) any {
		return fmt.Sprintf("%+v", err)
	}
}

// InfoCtx Infoレベルのログイベントを生成する
func InfoCtx(ctx context.Context) *zerolog.Event {
	return withSourceLocation(log.Info().Ctx(ctx))
}

// WarnCtx Warningレベルのログイベントを生成する
func WarnCtx(ctx context.Context) *zerolog.Event {
	return withSourceLocation(log.Warn().Ctx(ctx))
}

// ErrorCtx Errorレベルのログイベントを生成する
func ErrorCtx(ctx context.Context, err error) *zerolog.Event {
	return withSourceLocation(log.Error().Stack().Ctx(ctx)).Err(err)
}

// withSourceLocation ログにソースコードの情報を追加する
// event.Caller()は、オブジェクトではなく、文字列しか書き込めないため利用しない
func withSourceLocation(event *zerolog.Event) *zerolog.Event {
	if pc, file, line, ok := runtime.Caller(2); ok {
		return event.Dict("logging.googleapis.com/sourceLocation", zerolog.Dict().
			Str("file", file).
			Str("line", strconv.Itoa(line)).
			Str("function", runtime.FuncForPC(pc).Name()),
		)
	}
	return event
}

// withTrace ログにTrace情報を追加する
func withTrace(event *zerolog.Event, _ zerolog.Level, _ string) {
	if sc := trace.SpanContextFromContext(event.GetCtx()); sc.IsValid() {
		trace.SpanFromContext(event.GetCtx())
		var tracePrefix string
		if projectID != "" {
			tracePrefix = path.Join("projects", projectID, "traces")
		}
		event.
			Str("logging.googleapis.com/spanId", sc.SpanID().String()).
			Str("logging.googleapis.com/trace", path.Join(tracePrefix, sc.TraceID().String())).
			Bool("logging.googleapis.com/trace_sampled", sc.IsSampled())
	}
}
```

**呼び出し側**
```go
package main

import (
	"context"

	"example/log"
	"github.com/cockroachdb/errors"
	sdktrace "go.opentelemetry.io/otel/sdk/trace"
	"go.opentelemetry.io/otel/trace"
)

var tracer trace.Tracer

func init() {
	otel.SetTextMapPropagator(autoprop.NewTextMapPropagator())
	tp := sdktrace.NewTracerProvider()
	otel.SetTracerProvider(tp)
	tracer = tp.Tracer("example")
}

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

func demo(ctx context.Context) {
	ctx, span := tracer.Start(ctx, "call demo")
	defer span.End()
	log.InfoCtx(ctx).Msg("OK!")
	log.ErrorCtx(ctx).Err(errors.New("demo error")).Msg("Oops!")
}
```

**実行結果**
```json
{"severity":"info","logging.googleapis.com/sourceLocation":{"file":"/path/to/file.go","line":"24","func":"main.demo"},"logging.googleapis.com/spanId":"9f36d9191b99a8e7","logging.googleapis.com/trace":"4c7141227918cb6973472e205d2a6d03","logging.googleapis.com/trace_sampled":"true","timestamp":"2024-04-26T00:29:48+09:00","message":"OK!"}
{"severity":"error","logging.googleapis.com/sourceLocation":{"file":"/path/to/file.go","line":"25","func":"main.demo"},"stack_trace":"demo error\n(1) attached stack trace\n  -- stack trace:\n  | main.demo\n  | \t/path/to/file.go:25\n  | main.main\n  | \t/path/to/file.go:18\n  | (略)","error":"demo error","logging.googleapis.com/spanId":"9f36d9191b99a8e7","logging.googleapis.com/trace":"4c7141227918cb6973472e205d2a6d03","logging.googleapis.com/trace_sampled":"true","timestamp":"2024-04-26T00:29:48+09:00","message":"Oops!"}
```