This guide explains how to forward logs from Zap to SigNoz.
Requirements
- Go Modules: Ensure your project is using the following Go modules.
go get go.opentelemetry.io/contrib/bridges/otelzap
go get go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp
go get go.opentelemetry.io/otel/trace
go get go.uber.org/zap
We import the required packages:
otelzap: The bridge for Zap logger to work with OpenTelemetry.otlploghttp: The OTLP exporter for logs over HTTP.otel/trace: Used to read the active span'strace_idandspan_idfor log-trace correlation.zap: The high-performance logging library.
Define Endpoint and Access Token
You need to provide
otlpEndpoint: The URL of your SigNoz Cloud OTLP collector endpoint.accessToken: The token required to authenticate against SigNoz Cloud. This is typically provided in your account dashboard.
otlpEndpoint := "<signoz-client-url>"
accessToken := "<my-access-token>"
Create HTTP Headers for Authorization
To send data securely to the SigNoz Cloud, the access token is included in the Authorization header
headers := map[string]string{
"Authorization": "Bearer " + accessToken,
}
Create Exporter
ctx := context.Background()
exporter, err := otlploghttp.New(ctx,
otlploghttp.WithEndpoint(otlpEndpoint),
otlploghttp.WithHeaders(headers),
)
if err != nil {
fmt.Println(err)
}
Here, we create an OTLP HTTP exporter. This is responsible for exporting log records to an OTLP endpoint (like the OpenTelemetry collector).
Log Processor
processor := log.NewBatchProcessor(exporter)
A batch processor is used to efficiently group and export log records. The processor ensures that log records are buffered and sent in batches to the OTLP endpoint.
Logger Provider
provider := log.NewLoggerProvider(
log.WithProcessor(processor),
)
defer func() {
err := provider.Shutdown(context.Background())
if err != nil {
fmt.Println(err)
}
}()
The LoggerProvider serves as the core log provider, which connects the log pipeline (processor) to the Zap logger. This ensures that logs generated by your application will pass through OpenTelemetry’s logging mechanisms.
It's important to properly shut down the LoggerProvider to ensure that no logs are left unprocessed before the application exits.
Zap Logger Initialisation
logger := zap.New(otelzap.NewCore("my/pkg/name", otelzap.WithLoggerProvider(provider)))
defer logger.Sync()
We initialise a Zap logger, but instead of using Zap’s native logging core, we create an OpenTelemetry-based core (otelzap.NewCore). This core connects Zap’s logging functionality to the OpenTelemetry log pipeline, ensuring logs are exported.
"my/pkg/name": Specifies the package name or source of the logs, useful for debugging.otelzap.WithLoggerProvider(provider): Connects the previously created OpenTelemetryLoggerProvider.
Logging Messages
logger.Info("something really cool")
The logger.Info method logs messages. In this example, instead of writing logs to standard output, they are sent to the OTLP endpoint.
Trace With Context
To correlate logs with traces, extract trace_id and span_id from the active span on the incoming context and log them as explicit fields:
if traceID, spanID, ok := GetTraceIDAndSpanID(r.Context()); ok {
logger.Info("handling request",
zap.String("trace_id", traceID),
zap.String("span_id", spanID),
)
}
Where GetTraceIDAndSpanID reads the current span from context via go.opentelemetry.io/otel/trace:
import "go.opentelemetry.io/otel/trace"
func GetTraceIDAndSpanID(ctx context.Context) (string, string, bool) {
sc := trace.SpanFromContext(ctx).SpanContext()
if sc.HasTraceID() && sc.HasSpanID() {
return sc.TraceID().String(), sc.SpanID().String(), true
}
return "", "", false
}
Prefer extracting trace_id and span_id as explicit string fields over dumping the whole context (for example, zap.Any("context", ctx)). Contexts can carry unrelated values (auth tokens, request metadata, deadlines) that could be serialized into a log field, and the two IDs are all SigNoz needs for log-to-trace correlation.
Call the logger from a scope that already has an active span (for example, an HTTP handler that has been instrumented with otelhttp, or any function running under tracer.Start(ctx, ...)). Using context.Background() will not produce a trace or span ID. For more details, see Correlate Traces and Logs.
logging.go
package logging
import (
"context"
"fmt"
"net/http"
"go.opentelemetry.io/contrib/bridges/otelzap"
"go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp"
"go.opentelemetry.io/otel/sdk/log"
"go.opentelemetry.io/otel/trace"
"go.uber.org/zap"
)
func Example() {
otlpEndpoint := "<signoz-client-url>"
accessToken := "<my-access-token>"
// ctx is used only to initialize the exporter; it does not carry any span,
// so logs emitted with this context will not be correlated with a trace.
ctx := context.Background()
headers := map[string]string{
"Authorization": "Bearer " + accessToken,
}
// Create an exporter that will emit log records.
exporter, err := otlploghttp.New(ctx,
otlploghttp.WithEndpoint(otlpEndpoint),
otlploghttp.WithHeaders(headers),
)
if err != nil {
fmt.Println(err)
}
// Create a log record processor pipeline.
processor := log.NewBatchProcessor(exporter)
// Create a logger provider.
// You can pass this instance directly when creating a log bridge.
provider := log.NewLoggerProvider(
log.WithProcessor(processor),
)
defer func() {
err := provider.Shutdown(context.Background())
if err != nil {
fmt.Println(err)
}
}()
// Initialize a zap logger with the otelzap bridge core.
logger := zap.New(otelzap.NewCore("my/pkg/name", otelzap.WithLoggerProvider(provider)))
defer logger.Sync()
// You can now use your logger in your code.
logger.Info("something really cool")
logger.Info("Hello from Zap logger!")
}
// GetTraceIDAndSpanID extracts trace and span IDs from the active span on ctx.
// Returns empty strings and false if no recording span is present.
func GetTraceIDAndSpanID(ctx context.Context) (string, string, bool) {
sc := trace.SpanFromContext(ctx).SpanContext()
if sc.HasTraceID() && sc.HasSpanID() {
return sc.TraceID().String(), sc.SpanID().String(), true
}
return "", "", false
}
// Handler shows the recommended correlation pattern: extract trace_id and
// span_id from the request context and log them as explicit fields.
func Handler(w http.ResponseWriter, r *http.Request, logger *zap.Logger) {
fields := []zap.Field{}
if traceID, spanID, ok := GetTraceIDAndSpanID(r.Context()); ok {
fields = append(fields,
zap.String("trace_id", traceID),
zap.String("span_id", spanID),
)
}
logger.Info("handling request", fields...)
}
If using the k8s-infra chart (which auto-collects container logs), disable log collection for this application to prevent duplicates.
Output
For looking at logs, login into signoz cloud and navigate into logs tab to view the logs.

Requirements
- Go Modules: Ensure your project is using the following Go modules.
go get go.opentelemetry.io/contrib/bridges/otelzap
go get go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp
go get go.opentelemetry.io/otel/trace
go get go.uber.org/zap
We import the required packages:
otelzap: The bridge for Zap logger to work with OpenTelemetry.otlploghttp: The OTLP exporter for logs over HTTP.otel/trace: Used to read the active span'strace_idandspan_idfor log-trace correlation.zap: The high-performance logging library.
Define Endpoint and Access Token
You need to provide
otlpEndpoint: The URL of your SigNoz Cloud OTLP collector endpoint.
otlpEndpoint := "<signoz-client-url>" // For local this is usually 0.0.0.0:4318
Create Exporter
ctx := context.Background()
exporter, err := otlploghttp.New(ctx,
otlploghttp.WithEndpoint(otlpEndpoint),
otlploghttp.WithInsecure(),
)
if err != nil {
fmt.Println(err)
}
Here, we create an OTLP HTTP exporter. This is responsible for exporting log records to an OTLP endpoint (like the OpenTelemetry collector).
Log Processor
processor := log.NewBatchProcessor(exporter)
A batch processor is used to efficiently group and export log records. The processor ensures that log records are buffered and sent in batches to the OTLP endpoint.
Logger Provider
provider := log.NewLoggerProvider(
log.WithProcessor(processor),
)
defer func() {
err := provider.Shutdown(context.Background())
if err != nil {
fmt.Println(err)
}
}()
The LoggerProvider serves as the core log provider, which connects the log pipeline (processor) to the Zap logger. This ensures that logs generated by your application will pass through OpenTelemetry’s logging mechanisms.
It's important to properly shut down the LoggerProvider to ensure that no logs are left unprocessed before the application exits.
Zap Logger Initialisation
logger := zap.New(otelzap.NewCore("my/pkg/name", otelzap.WithLoggerProvider(provider)))
defer logger.Sync()
We initialise a Zap logger, but instead of using Zap’s native logging core, we create an OpenTelemetry-based core (otelzap.NewCore). This core connects Zap’s logging functionality to the OpenTelemetry log pipeline, ensuring logs are exported.
"my/pkg/name": Specifies the package name or source of the logs, useful for debugging.otelzap.WithLoggerProvider(provider): Connects the previously created OpenTelemetryLoggerProvider.
Logging Messages
logger.Info("something really cool")
The logger.Info method logs messages. In this example, instead of writing logs to standard output, they are sent to the OTLP endpoint.
Trace With Context
To correlate logs with traces, extract trace_id and span_id from the active span on the incoming context and log them as explicit fields:
if traceID, spanID, ok := GetTraceIDAndSpanID(r.Context()); ok {
logger.Info("handling request",
zap.String("trace_id", traceID),
zap.String("span_id", spanID),
)
}
Where GetTraceIDAndSpanID reads the current span from context via go.opentelemetry.io/otel/trace:
import "go.opentelemetry.io/otel/trace"
func GetTraceIDAndSpanID(ctx context.Context) (string, string, bool) {
sc := trace.SpanFromContext(ctx).SpanContext()
if sc.HasTraceID() && sc.HasSpanID() {
return sc.TraceID().String(), sc.SpanID().String(), true
}
return "", "", false
}
Prefer extracting trace_id and span_id as explicit string fields over dumping the whole context (for example, zap.Any("context", ctx)). Contexts can carry unrelated values (auth tokens, request metadata, deadlines) that could be serialized into a log field, and the two IDs are all SigNoz needs for log-to-trace correlation.
Call the logger from a scope that already has an active span (for example, an HTTP handler that has been instrumented with otelhttp, or any function running under tracer.Start(ctx, ...)). Using context.Background() will not produce a trace or span ID. For more details, see Correlate Traces and Logs.
logging.go
package logging
import (
"context"
"fmt"
"net/http"
"go.opentelemetry.io/contrib/bridges/otelzap"
"go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp"
"go.opentelemetry.io/otel/sdk/log"
"go.opentelemetry.io/otel/trace"
"go.uber.org/zap"
)
func Example() {
otlpEndpoint := "<signoz-client-url>" // For local instance, this is usually 0.0.0.0:4318
// ctx is used only to initialize the exporter; it does not carry any span,
// so logs emitted with this context will not be correlated with a trace.
ctx := context.Background()
// Create an exporter that will emit log records.
exporter, err := otlploghttp.New(ctx,
otlploghttp.WithEndpoint(otlpEndpoint),
otlploghttp.WithInsecure(),
)
if err != nil {
fmt.Println(err)
}
// Create a log record processor pipeline.
processor := log.NewBatchProcessor(exporter)
// Create a logger provider.
// You can pass this instance directly when creating a log bridge.
provider := log.NewLoggerProvider(
log.WithProcessor(processor),
)
defer func() {
err := provider.Shutdown(context.Background())
if err != nil {
fmt.Println(err)
}
}()
// Initialize a zap logger with the otelzap bridge core.
logger := zap.New(otelzap.NewCore("my/pkg/name", otelzap.WithLoggerProvider(provider)))
defer logger.Sync()
// You can now use your logger in your code.
logger.Info("something really cool")
logger.Info("Hello from Zap logger!")
}
// GetTraceIDAndSpanID extracts trace and span IDs from the active span on ctx.
// Returns empty strings and false if no recording span is present.
func GetTraceIDAndSpanID(ctx context.Context) (string, string, bool) {
sc := trace.SpanFromContext(ctx).SpanContext()
if sc.HasTraceID() && sc.HasSpanID() {
return sc.TraceID().String(), sc.SpanID().String(), true
}
return "", "", false
}
// Handler shows the recommended correlation pattern: extract trace_id and
// span_id from the request context and log them as explicit fields.
func Handler(w http.ResponseWriter, r *http.Request, logger *zap.Logger) {
fields := []zap.Field{}
if traceID, spanID, ok := GetTraceIDAndSpanID(r.Context()); ok {
fields = append(fields,
zap.String("trace_id", traceID),
zap.String("span_id", spanID),
)
}
logger.Info("handling request", fields...)
}
If using the k8s-infra chart (which auto-collects container logs), disable log collection for this application to prevent duplicates.
Output
For looking at logs, login into signoz cloud and navigate into logs tab to view the logs.
