Skip to content

Logging & Tracing — Zap + OpenTelemetry

FieldValue
TypeAgent Reference
Source~/.copilot/agents/_refs/go-backend-engineer/logging-and-tracing.md
DescriptionNot 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.
  • Info for normal lifecycle, Error only for things that need a human.
  • Never log secrets, tokens, raw passwords, or full request bodies.
  • Prefer zap.Error(err) over zap.String("err", err.Error()) (preserves stack).