Logging & Tracing — Zap + OpenTelemetry
| Field | Value |
|---|---|
| Type | Agent Reference |
| Source | ~/.copilot/agents/_refs/go-backend-engineer/logging-and-tracing.md |
| Description | Not specified |
Source Content
Logging & Tracing — Zap + OpenTelemetry
Org standard: Zap for structured logging, OpenTelemetry for distributed tracing. The two are correlated through request_id and trace_id carried in context.Context.
Zap logger setup (internal/telemetry/logger.go)
package telemetry
import ( "context" "go.uber.org/zap" "go.uber.org/zap/zapcore")
type contextKey string
const loggerKey contextKey = "logger"
type Config struct { ServiceName string ServiceVersion string Environment string Level string}
var globalLogger *zap.Logger
func InitLogger(cfg Config) error { var zapConfig zap.Config if cfg.Environment == "production" { zapConfig = zap.NewProductionConfig() zapConfig.EncoderConfig.TimeKey = "timestamp" zapConfig.EncoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder } else { zapConfig = zap.NewDevelopmentConfig() zapConfig.EncoderConfig.EncodeLevel = zapcore.CapitalColorLevelEncoder }
level, err := zapcore.ParseLevel(cfg.Level) if err != nil { level = zapcore.InfoLevel } zapConfig.Level = zap.NewAtomicLevelAt(level)
logger, err := zapConfig.Build( zap.AddCallerSkip(1), zap.AddStacktrace(zapcore.ErrorLevel), ) if err != nil { return err } globalLogger = logger.With( zap.String("service_name", cfg.ServiceName), zap.String("service_version", cfg.ServiceVersion), zap.String("environment", cfg.Environment), ) return nil}
func GetLogger() *zap.Logger { if globalLogger == nil { globalLogger, _ = zap.NewProduction() } return globalLogger}
func Sync() error { if globalLogger != nil { return globalLogger.Sync() } return nil}ADR-compliant request context
Every request gets a logger seeded with: request_id, trace_id, session_id, plus tenant, user, auth objects when available.
type TenantContext struct { ID, Slug, Plan, Region, DatabaseShard, Status string}type UserContext struct{ ID, Role string }type AuthContext struct { Method string APIKeyID *string SSOProvider *string ImpersonationUID *string}
func WithRequestContext( ctx context.Context, requestID, traceID, sessionID string, tenant *TenantContext, user *UserContext, auth *AuthContext,) context.Context { logger := GetLogger().With( zap.String("request_id", requestID), zap.String("trace_id", traceID), zap.String("session_id", sessionID), ) if tenant != nil { logger = logger.With(zap.Object("tenant", zapcore.ObjectMarshalerFunc(func(enc zapcore.ObjectEncoder) error { enc.AddString("id", tenant.ID) enc.AddString("slug", tenant.Slug) enc.AddString("plan", tenant.Plan) enc.AddString("region", tenant.Region) if tenant.DatabaseShard != "" { enc.AddString("database_shard", tenant.DatabaseShard) } enc.AddString("status", tenant.Status) return nil }))) } // similar blocks for user and auth ... return context.WithValue(ctx, loggerKey, logger)}
func FromContext(ctx context.Context) *zap.Logger { if logger, ok := ctx.Value(loggerKey).(*zap.Logger); ok { return logger } return GetLogger()}
func WithFields(ctx context.Context, fields ...zap.Field) context.Context { return context.WithValue(ctx, loggerKey, FromContext(ctx).With(fields...))}OpenTelemetry tracing (internal/telemetry/tracing.go)
package telemetry
import ( "context" "fmt" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc" "go.opentelemetry.io/otel/propagation" "go.opentelemetry.io/otel/sdk/resource" sdktrace "go.opentelemetry.io/otel/sdk/trace" semconv "go.opentelemetry.io/otel/semconv/v1.21.0" "go.opentelemetry.io/otel/trace" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure")
var tracer trace.Tracer
func InitTracing(serviceName, serviceVersion, otlpEndpoint string) (func(), error) { ctx := context.Background() conn, err := grpc.DialContext(ctx, otlpEndpoint, grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithBlock()) if err != nil { return nil, fmt.Errorf("dial: %w", err) } exporter, err := otlptracegrpc.New(ctx, otlptracegrpc.WithGRPCConn(conn)) if err != nil { return nil, fmt.Errorf("exporter: %w", err) } res, err := resource.New(ctx, resource.WithAttributes( semconv.ServiceName(serviceName), semconv.ServiceVersion(serviceVersion), )) if err != nil { return nil, err } tp := sdktrace.NewTracerProvider( sdktrace.WithBatcher(exporter), sdktrace.WithResource(res), sdktrace.WithSampler(sdktrace.AlwaysSample()), ) otel.SetTracerProvider(tp) otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator( propagation.TraceContext{}, propagation.Baggage{}, )) tracer = tp.Tracer(serviceName) return func() { _ = tp.Shutdown(context.Background()) }, nil}
func StartSpan(ctx context.Context, name string, attrs ...attribute.KeyValue) (context.Context, trace.Span) { return tracer.Start(ctx, name, trace.WithAttributes(attrs...))}
func RecordError(ctx context.Context, err error) { if span := trace.SpanFromContext(ctx); span != nil && err != nil { span.RecordError(err) }}Fiber middleware that wires both
internal/api/middleware/logging.go — generates request_id, opens a span, attaches a tenant/user/auth-aware logger to c.UserContext(), then logs request_started/request_completed and records Prometheus counters/histograms. See observability.md for the metrics side.
Logging conventions
- Always log
action: <verb_phrase>as a string field — easier to filter in Loki. Infofor normal lifecycle,Erroronly for things that need a human.- Never log secrets, tokens, raw passwords, or full request bodies.
- Prefer
zap.Error(err)overzap.String("err", err.Error())(preserves stack).