OpenTelemetry Zap [otelzap]: Golang Logging Bridge Setup & Examples
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:
go get github.com/uptrace/opentelemetry-go-extra/otelzap
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:
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:
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:
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:
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:
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:
// 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:
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 eventsWarn: Potentially harmful situationsError: Errors that should be investigated
// 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:
| Feature | Zap | slog (standard library) | Benefit |
|---|---|---|---|
| Structured logging | Yes | Yes | Machine-readable logs |
| Log levels | Debug, Info, Warn, Error | Debug, Info, Warn, Error | Better filtering |
| Performance | Very fast, zero-allocation | Fast | Zap optimized for hot paths |
| Typed fields | Yes (zap.String, etc.) | Loosely typed key-value | Zap catches type errors at build |
| Sugared API | Yes | No | Flexible printf-style logging |
| Standard library | Third-party (Uber) | Built-in since Go 1.21 | Slog has no dependencies |
| OTel integration | otelzap (community) | otelslog (official) | Both support trace correlation |
| Handler interface | Zap Core | slog.Handler | Both 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 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.