OpenTelemetry Zap [otelzap]: Golang Logging Bridge Setup & Examples

Vladimir Mihailenco
March 30, 2026
7 min read

OpenTelemetry Zap (otelzap) is a bridge for Uber's Zap structured logging library that integrates with OpenTelemetry. It records Zap log messages as events on the active span, letting you correlate logs with distributed traces. You can optionally enable WithTraceIDField(true) and WithSpanIDField(true) to inject trace/span IDs directly into Zap's structured output. This guide covers otelzap setup, global logger configuration, and Golang OTel logging best practices.

Quick Setup

Install otelzap and wrap your existing Zap logger:

shell
go get github.com/uptrace/opentelemetry-go-extra/otelzap
go
import (
    "go.uber.org/zap"
    "github.com/uptrace/opentelemetry-go-extra/otelzap"
)

log := otelzap.New(zap.Must(zap.NewProduction()))
log.Ctx(ctx).Info("request processed", zap.String("user_id", "12345"))

Now all Zap log calls with a context automatically include OpenTelemetry trace context.

What is OpenTelemetry?

OpenTelemetry is an open-source observability framework that aims to standardize and simplify the collection, processing, and export of telemetry data from applications and systems.

OpenTelemetry supports multiple programming languages and platforms, making it suitable for a wide range of applications and environments.

OpenTelemetry enables developers to instrument their code and collect telemetry data, which can then be exported to various OpenTelemetry backends or observability platforms for analysis and visualization. For details on log collection, see OpenTelemetry logs.

What is otelzap?

otelzap is a thin wrapper around Zap that extends it with context-aware methods. When you call log.Ctx(ctx).Info(...) or log.InfoContext(ctx, ...), otelzap extracts the active span from the context and records the log message as a span event with all structured fields attached. If the context has no span, it falls back to standard Zap behavior with zero additional overhead.

Installation

Install the otelzap package:

shell
go get github.com/uptrace/opentelemetry-go-extra/otelzap

Basic Usage

Create an otelzap.Logger by wrapping an existing Zap logger, then pass a context to propagate the active span:

go
import (
    "go.uber.org/zap"
    "github.com/uptrace/opentelemetry-go-extra/otelzap"
)

// Wrap zap logger to extend Zap with API that accepts a context.Context.
log := otelzap.New(zap.NewExample())

// Pass ctx to propagate the span.
log.Ctx(ctx).Error("hello from zap",
    zap.Error(errors.New("hello world")),
    zap.String("foo", "bar"))

// Alternatively, use the Context suffix variant.
log.ErrorContext(ctx, "hello from zap",
    zap.Error(errors.New("hello world")),
    zap.String("foo", "bar"))

Both variants are fast and don't allocate.

Resources:

Global Logger

Just like Zap, otelzap provides a global logger that can be set with otelzap.ReplaceGlobals:

go
package main

import (
    "context"

    "go.uber.org/zap"
    "github.com/uptrace/opentelemetry-go-extra/otelzap"
)

func main() {
    logger := otelzap.New(zap.Must(zap.NewProduction()))
    defer logger.Sync()

    undo := otelzap.ReplaceGlobals(logger)
    defer undo()

    otelzap.L().Info("replaced zap's global loggers")
    otelzap.Ctx(context.TODO()).Info("... and with context")
}

Sugared Logger

You can also use the sugared logger API for a more relaxed, printf-style interface:

go
log := otelzap.New(zap.NewExample())
sugar := log.Sugar()

sugar.Ctx(ctx).Infow("failed to fetch URL",
    // Structured context as loosely typed key-value pairs.
    "url", url,
    "attempt", 3,
    "backoff", time.Second,
)
sugar.InfowContext(ctx, "failed to fetch URL",
    "url", url,
    "attempt", 3,
    "backoff", time.Second,
)

sugar.Ctx(ctx).Infof("Failed to fetch URL: %s", url)
sugar.InfofContext(ctx, "Failed to fetch URL: %s", url)

Complete Golang OTel Logging Example

Here's a full example showing otelzap integration with OpenTelemetry tracing using uptrace-go:

go
package main

import (
    "context"
    "errors"
    "fmt"

    "go.uber.org/zap"
    "github.com/uptrace/opentelemetry-go-extra/otelzap"
    "github.com/uptrace/uptrace-go/uptrace"
    "go.opentelemetry.io/otel"
    "go.opentelemetry.io/otel/trace"
)

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

    // Initialize OpenTelemetry
    uptrace.ConfigureOpentelemetry(
        uptrace.WithServiceName("zap-example"),
    )
    defer uptrace.Shutdown(ctx)

    // Create otelzap logger and set as global
    logger := otelzap.New(zap.Must(zap.NewProduction()))
    defer logger.Sync()

    undo := otelzap.ReplaceGlobals(logger)
    defer undo()

    // Create tracer for instrumenting operations
    tracer := otel.Tracer("zap-example")

    // Start a traced operation
    ctx, span := tracer.Start(ctx, "process-request")
    defer span.End()

    // Structured logging with trace context
    otelzap.Ctx(ctx).Info("Processing request",
        zap.String("user_id", "12345"),
        zap.String("action", "login"))

    // Log errors with full context
    otelzap.Ctx(ctx).Error("Authentication failed",
        zap.Error(errors.New("invalid_credentials")),
        zap.String("locale", "en_US"))

    // View trace in Uptrace
    fmt.Println("Trace URL:", uptrace.TraceURL(trace.SpanFromContext(ctx)))
}

Key Benefits:

  • Logs automatically include trace_id and span_id from context
  • Correlate logs with distributed traces across microservices
  • Compatible with all OpenTelemetry-compliant backends
  • No vendor lock-in - works with Uptrace, Jaeger, Datadog, etc.

Golang OTel Logging Best Practices

Use Context-Aware Logging

Always pass context to include trace information in your logs:

go
// Good: Includes trace context
otelzap.Ctx(ctx).Info("User logged in", zap.String("user_id", userId))

// Bad: Missing trace context, no correlation with traces
logger.Info("User logged in", zap.String("user_id", userId))

Structured Fields for Filtering

Use Zap's typed fields for structured logging to enable powerful filtering and avoid runtime type reflection:

go
otelzap.Ctx(ctx).Info("Database query executed",
    zap.String("query_type", "SELECT"),
    zap.String("table", "users"),
    zap.Int64("duration_ms", 45),
    zap.Int("rows_affected", 1),
)

Log Levels Strategy

  • Debug: Detailed information for debugging (disabled in production)
  • Info: General operational events
  • Warn: Potentially harmful situations
  • Error: Errors that should be investigated
go
// Development: verbose logging
cfg := zap.NewDevelopmentConfig()
cfg.Level = zap.NewAtomicLevelAt(zap.DebugLevel)
devLogger, _ := cfg.Build()
log := otelzap.New(devLogger)

// Production: info and above only
prodLogger, _ := zap.NewProduction()
log := otelzap.New(prodLogger)

Zap vs Slog Comparison

Zap remains one of the most popular Golang logging libraries, while log/slog was introduced in Go 1.21 as the standard library solution. Here's how they compare:

FeatureZapslog (standard library)Benefit
Structured loggingYesYesMachine-readable logs
Log levelsDebug, Info, Warn, ErrorDebug, Info, Warn, ErrorBetter filtering
PerformanceVery fast, zero-allocationFastZap optimized for hot paths
Typed fieldsYes (zap.String, etc.)Loosely typed key-valueZap catches type errors at build
Sugared APIYesNoFlexible printf-style logging
Standard libraryThird-party (Uber)Built-in since Go 1.21Slog has no dependencies
OTel integrationotelzap (community)otelslog (official)Both support trace correlation
Handler interfaceZap Coreslog.HandlerBoth are extensible

When to use Zap: Choose Zap if you need maximum performance, typed field safety, or already have Zap in your codebase. The sugared logger API is convenient for rapid development.

When to use slog: Choose slog for new projects that prefer standard library solutions with zero external dependencies, or if you want the officially supported OpenTelemetry bridge.

What is Uptrace?

Uptrace is a OpenTelemetry APM that supports distributed tracing, metrics, and logs. You can use it to monitor applications and troubleshoot issues.

Uptrace Overview

Uptrace comes with an intuitive query builder, rich dashboards, alerting rules with notifications, and integrations for most languages and frameworks.

Uptrace can process billions of spans and metrics on a single server and allows you to monitor your applications at 10x lower cost.

In just a few minutes, you can try Uptrace by visiting the cloud demo (no login required) or running it locally with Docker. The source code is available on GitHub.

FAQ

What is otelzap? otelzap is a community-maintained OpenTelemetry bridge for Uber's Zap logging library, available at github.com/uptrace/opentelemetry-go-extra/otelzap. It wraps a standard *zap.Logger and adds context-aware methods (Ctx, InfoContext, etc.) that automatically record log messages as span events with trace and span IDs for correlation with distributed traces.

How does otelzap differ from otelslog? otelzap wraps Uber's Zap library and records logs as span events on the active trace, while otelslog is the official OpenTelemetry bridge for Go's standard log/slog package. otelzap uses Zap's typed field API (zap.String, zap.Int) for zero-allocation logging, whereas otelslog uses slog's loosely typed key-value pairs. Choose otelzap if you already use Zap; choose otelslog for new projects preferring the standard library.

Does otelzap work without an active span? Yes. If the context passed to log.Ctx(ctx) does not contain an active span, otelzap falls back to standard Zap logging behavior with no additional overhead. Log messages are still written to Zap's configured output (console, file, etc.) but are not recorded as OpenTelemetry span events.

How to migrate from plain Zap to otelzap? Replace zap.NewProduction() with otelzap.New(zap.NewProduction()), then change logger.Info(...) calls to logger.Ctx(ctx).Info(...) to pass trace context. For the global logger, replace zap.ReplaceGlobals() with otelzap.ReplaceGlobals() and use otelzap.L() and otelzap.Ctx(ctx) instead of zap.L().

What is the performance impact of otelzap? otelzap adds minimal overhead. When no span is present in the context, the wrapper performs a nil check and delegates directly to Zap with near-zero cost. When a span is active, it records the log message as a span event, which is a lightweight operation. Zap's zero-allocation typed fields (zap.String, zap.Int) are preserved through the wrapper, maintaining Zap's performance advantages.

What's next?

OpenTelemetry Zap is a valuable tool for improving the observability of Golang applications that already use Zap for logging. By correlating logs with OpenTelemetry trace context, you can gain a deeper understanding of your application's behavior and more effectively diagnose problems. For comprehensive Go instrumentation, see the OpenTelemetry Go guide. Compare with slog for the standard library approach, or Logrus for another popular logging library.