otel

package
v3.0.0-next.11 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 23, 2026 License: MIT Imports: 21 Imported by: 0

README

OpenTelemetry Integration

Go Reference

Unified OpenTelemetry configuration, instrumentation, and logging utilities for the pkg library ecosystem.

Overview

The otel package provides centralized OpenTelemetry configuration that enables observability across all library packages. It supports the three pillars of observability:

  • Traces - Distributed tracing for request flows
  • Metrics - Performance and health measurements
  • Logs - Structured logging via OpenTelemetry standard

Since v3, the former logging package is absorbed into otel: global zerolog bootstrap (otel.Initialize, otel.InitializeWithFile, otel.ContextLogger) and OTLP logger providers (otel.NewLoggerProviderWithOptions) live here.

Features

  • Unified Configuration: Single config object for all telemetry pillars, built with functional options
  • Selective Enablement: Enable only the telemetry you need
  • No-op by Default: Zero overhead when providers are not configured
  • Layer Instrumentation: otel.Layers.Start*() spans with integrated, correlated logging
  • Standard Logging Helper: OTel-aware logging with automatic trace correlation
  • Global Logger Bootstrap: zerolog global logger setup with console and/or file output
  • OTLP Logging Support: Export logs to OpenTelemetry collectors with flexible options
  • Granular Log Levels: Fine-grained control over log verbosity (debug, info, warn, error, none)
  • Graceful Shutdown: Proper resource cleanup

Installation

go get github.com/jasoet/pkg/v3/otel

Quick Start

Basic Configuration
import "github.com/jasoet/pkg/v3/otel"

// Create unified OTel config (compile-checked: ExampleNewConfig)
otelConfig := otel.NewConfig("my-service",
    otel.WithTracerProvider(tracerProvider), // optional
    otel.WithMeterProvider(meterProvider),   // optional
    otel.WithServiceVersion("1.0.0"))

// Use with library packages via their OTelConfig field / WithOTelConfig option

// Cleanup on shutdown
defer otelConfig.Shutdown(context.Background())
Selective Telemetry

Enable only what you need:

// Tracing only (default logging disabled)
cfg := otel.NewConfig("my-service",
    otel.WithTracerProvider(tracerProvider),
    otel.WithoutLogging())

// Metrics only (default logging disabled)
cfg := otel.NewConfig("my-service",
    otel.WithMeterProvider(meterProvider),
    otel.WithoutLogging())

// All three pillars
cfg := otel.NewConfig("my-service",
    otel.WithTracerProvider(tracerProvider),
    otel.WithMeterProvider(meterProvider),
    otel.WithLoggerProvider(loggerProvider))
Custom Logger Provider

Use otel.NewLoggerProviderWithOptions for better formatting and automatic trace correlation (compile-checked: ExampleNewLoggerProviderWithOptions):

import "github.com/jasoet/pkg/v3/otel"

loggerProvider, err := otel.NewLoggerProviderWithOptions("my-service",
    otel.WithLogLevel(otel.LogLevelDebug))
if err != nil {
    panic(err)
}

cfg := otel.NewConfig("my-service",
    otel.WithTracerProvider(tracerProvider),
    otel.WithLoggerProvider(loggerProvider))
OTLP Logging with Flexible Options

Console output is enabled by default; add an OTLP endpoint to also export logs to a collector:

import "github.com/jasoet/pkg/v3/otel"

// OTLP logging with console output (local development)
loggerProvider, err := otel.NewLoggerProviderWithOptions(
    "my-service",
    otel.WithOTLPEndpoint("https://localhost:4318", true), // insecure for local
    otel.WithConsoleOutput(true),
    otel.WithLogLevel(otel.LogLevelInfo),
)

// OTLP-only logging (production)
loggerProvider, err = otel.NewLoggerProviderWithOptions(
    "my-service",
    otel.WithOTLPEndpoint("https://otel-collector.prod:4318", false), // secure
    otel.WithConsoleOutput(false), // disable console in prod
    otel.WithLogLevel(otel.LogLevelWarn),
)

Note: this package uses otlploghttp, so OTLP endpoints are full URLs with scheme.

Global Logger Bootstrap

For plain (non-OTel) logging, initialize the global zerolog logger once at startup (compile-checked: ExampleInitialize):

import "github.com/jasoet/pkg/v3/otel"

// Console-only global logger at info level (debug=true for debug level + caller)
err := otel.Initialize("my-service", false)

// Console + file output
closer, err := otel.InitializeWithFile("my-service", true,
    otel.OutputConsole|otel.OutputFile,
    &otel.FileConfig{Path: "app.log"})
if err != nil {
    log.Fatal(err)
}
defer closer.Close()

// Component-scoped logger derived from the global logger
logger := otel.ContextLogger(ctx, "repository")

Global log records are written to stderr (console) and/or the configured file.

Configuration API

Functional Options
Option Description
NewConfig(name, opts...) Create config with service name, default logger, and options
WithTracerProvider(tp) Enable distributed tracing
WithMeterProvider(mp) Enable metrics collection
WithLoggerProvider(lp) Set custom logger provider
WithServiceVersion(v) Set service version
WithoutTracing() Disable tracing
WithoutMetrics() Disable metrics
WithoutLogging() Disable default stdout logging
Helper Methods
// Check what's enabled
cfg.IsTracingEnabled()  // bool
cfg.IsMetricsEnabled()  // bool
cfg.IsLoggingEnabled()  // bool

// Get instrumentation components
tracer := cfg.GetTracer("scope-name")   // Returns no-op if disabled
meter := cfg.GetMeter("scope-name")     // Returns no-op if disabled
logger := cfg.GetLogger("scope-name")   // Returns no-op if disabled

// Context management (recommended)
ctx = otel.ContextWithConfig(ctx, cfg)  // Store config in context
cfg = otel.ConfigFromContext(ctx)       // Retrieve config from context

// Cleanup
cfg.Shutdown(context.Background())
Logger Provider Options

Create flexible logger providers with NewLoggerProviderWithOptions:

Option Description
WithOTLPEndpoint(endpoint, insecure) Enable OTLP log export to collector (full URL with scheme)
WithConsoleOutput(enabled) Enable/disable console logging (default: true)
WithLogLevel(level) Set log level: LogLevelDebug, LogLevelInfo, LogLevelWarn, LogLevelError, LogLevelNone

Log Level Priority:

  1. Explicit WithLogLevel() (highest priority)
  2. Default to info level

Examples:

import "github.com/jasoet/pkg/v3/otel"

// Default info level
provider, _ := otel.NewLoggerProviderWithOptions("service")

// Debug mode (all logs)
provider, _ = otel.NewLoggerProviderWithOptions("service",
    otel.WithLogLevel(otel.LogLevelDebug))

// OTLP + console for development
provider, _ = otel.NewLoggerProviderWithOptions("service",
    otel.WithOTLPEndpoint("https://localhost:4318", true),
    otel.WithConsoleOutput(true),
    otel.WithLogLevel(otel.LogLevelDebug))

// OTLP-only for production
provider, _ = otel.NewLoggerProviderWithOptions("service",
    otel.WithOTLPEndpoint("https://collector:4318", false),
    otel.WithConsoleOutput(false),
    otel.WithLogLevel(otel.LogLevelInfo))

Standard Logging Helper

The otel package provides LogHelper for OTel-aware logging with automatic log-span correlation (compile-checked: Example_optionalFunctionParameter):

import "github.com/jasoet/pkg/v3/otel"

// Create a logger (uses OTel when configured, falls back to zerolog otherwise)
logger := otel.NewLogHelper(ctx, otelConfig, "github.com/jasoet/pkg/v3/mypackage", "mypackage.DoWork")

// Log with automatic trace_id/span_id injection (when OTel is enabled)
logger.Debug("Starting work", otel.F("workerId", 123))
logger.Info("Work completed", otel.F("duration", elapsed))
logger.Error(err, "Work failed", otel.F("workerId", 123))

Benefits:

  • Automatic trace_id/span_id injection when OTel is configured
  • Graceful fallback to zerolog when OTel is not configured
  • Consistent API across all packages
  • Errors automatically recorded in active spans

See helper.go for full documentation.

Layer Instrumentation

otel.Layers provides five starters — StartHandler, StartMiddleware, StartOperations, StartService, StartRepository — each returning a LayerContext with both a span and a correlated logger (compile-checked: Example_layerContextIntegration, Example_middlewareLayer):

// Fields passed here are automatically included in all log calls
lc := otel.Layers.StartService(ctx, "user", "CreateUser",
    otel.F("user.id", "12345"))
defer lc.End()

lc.Logger.Info("Creating user", otel.F("email", "user@example.com"))

if err := repo.Save(lc.Context(), data); err != nil {
    return lc.Error(err, "save failed")
}
lc.Success("User created")

Note: Success sets the span status to codes.Ok; per the OTel specification the status description is dropped for Ok, so the message appears in the log but not on the span.

Context-Based Config Propagation

The recommended pattern for passing OTel config through your application layers is to store it in the context once at the entry point (compile-checked: Example_withOTelConfig, Example_layerPropagation):

import "github.com/jasoet/pkg/v3/otel"

// At the HTTP handler entry point
func (h *Handler) HandleRequest(c echo.Context) error {
    // Store config in context once
    ctx := otel.ContextWithConfig(c.Request().Context(), h.otelConfig)

    // Config automatically available to all nested operations
    return h.service.ProcessRequest(ctx, req)
}

// In service layer - no need to pass config explicitly
func (s *Service) ProcessRequest(ctx context.Context, req Request) error {
    lc := otel.Layers.StartService(ctx, "user", "ProcessRequest",
        otel.F("request.id", req.ID))
    defer lc.End()

    lc.Logger.Info("Processing request")

    return s.repo.Save(lc.Context(), data)
}

Benefits:

  • Set config once at entry point, available everywhere
  • No need to pass config as parameter through all layers
  • Natural propagation through context (like span data)
  • Logger always available (zerolog fallback when no config — see Example_withoutOTelConfig)
  • Fields automatically included in all log calls

Config is optional but recommended for production; you can adopt it gradually (see Example_gradualOTelAdoption, Example_configOptionalButRecommended).

API Pattern:

// Store config in context (once at entry point)
ctx = otel.ContextWithConfig(ctx, cfg)

// Create layer contexts - all return both Span and Logger
lc := otel.Layers.StartHandler(ctx, "user", "GetUser", otel.F("http.method", "GET"))
lc := otel.Layers.StartMiddleware(ctx, "auth", "ValidateToken", otel.F("token.type", "JWT"))
lc := otel.Layers.StartOperations(ctx, "user", "ProcessQueue", otel.F("queue.name", queue))
lc := otel.Layers.StartService(ctx, "user", "CreateUser", otel.F("user.email", email))
lc := otel.Layers.StartRepository(ctx, "user", "FindByID", otel.F("user.id", id))

// All log calls automatically include the fields
lc.Logger.Info("Processing")                    // Includes all fields
lc.Logger.Debug("Details", otel.F("extra", val)) // Adds extra field
lc.Error(err, "Failed")                         // Includes all fields
lc.Success("Done")                              // Includes all fields

// Get logger from span (config retrieved automatically)
span := otel.StartSpan(ctx, "service.user", "DoWork")
logger := span.Logger("service.user") // No config parameter needed

Using with Library Packages

Create one *otel.Config and inject it into each package's configuration. Every instrumented package exposes either an OTelConfig *otel.Config config field or a WithOTelConfig(cfg) option:

import "github.com/jasoet/pkg/v3/otel"

otelConfig := otel.NewConfig("my-app",
    otel.WithTracerProvider(tracerProvider),
    otel.WithMeterProvider(meterProvider))

// server.Config{OTelConfig: otelConfig, ...}
// grpc.WithOTelConfig(otelConfig)
// db config with OTelConfig field
// rest.WithOTelConfig(otelConfig)

See each package's README for its exact wiring (server, grpc, db, rest, temporal, docker, argo).

Complete Example

See the fullstack OTel example for a complete application demonstrating all three telemetry pillars across multiple packages.

Testing

# Run tests (includes Output-verified examples)
go test ./otel -v

# With coverage
go test ./otel -cover
Test Utilities

Use no-op providers for testing:

import (
    "github.com/jasoet/pkg/v3/otel"
    noopm "go.opentelemetry.io/otel/metric/noop"
    noopt "go.opentelemetry.io/otel/trace/noop"
)

func TestMyCode(t *testing.T) {
    cfg := otel.NewConfig("test-service",
        otel.WithTracerProvider(noopt.NewTracerProvider()),
        otel.WithMeterProvider(noopm.NewMeterProvider()),
        otel.WithoutLogging())

    // Test your code with cfg
}

Best Practices

1. Create Once, Share Everywhere
// Good: Single config shared across packages
otelConfig := otel.NewConfig("my-service",
    otel.WithTracerProvider(tp),
    otel.WithMeterProvider(mp))
2. Always Shutdown
// Good: Graceful shutdown
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

if err := otelConfig.Shutdown(ctx); err != nil {
    log.Printf("OTel shutdown error: %v", err)
}
3. Check Before Using
// Good: Check enablement
if cfg.IsTracingEnabled() {
    tracer := cfg.GetTracer("my-scope")
    // Use tracer
}
4. Use LogHelper for Consistent Logging
// Good: Use otel.LogHelper for automatic log-span correlation
logger := otel.NewLogHelper(ctx, otelConfig, "github.com/jasoet/pkg/v3/mypackage", "mypackage.DoWork")
logger.Info("Work completed", otel.F("duration", elapsed))

Architecture

Design Principles
  1. Zero Dependencies: Only depends on OTel SDK (no custom exporters)
  2. No-op Safety: Nil providers result in no-op implementations
  3. Lazy Initialization: Providers created only when needed
  4. Immutable Config: Thread-safe after creation
Package Structure
otel/
├── config.go                       # Config struct and functional options
├── config_test.go                  # Config tests
├── options_test.go                 # Functional options tests
├── bootstrap.go                    # Global zerolog logger bootstrap (Initialize, ContextLogger)
├── bootstrap_test.go               # Bootstrap tests
├── logging.go                      # OTLP logger provider with flexible options
├── logging_test.go                 # Logger provider tests
├── helper.go                       # Standard logging helper with OTel integration
├── helper_test.go                  # LogHelper tests
├── instrumentation.go              # Span/layer instrumentation utilities
├── instrumentation_test.go         # Instrumentation tests
├── instrumentation_behavior_test.go # Behavioral tests with in-memory exporter
├── examples_test.go                # Compile-checked, Output-verified examples
├── instrumentation_example_test.go # Compile-checked, Output-verified examples
└── doc.go                          # Package documentation

Troubleshooting

No Telemetry Data

Problem: Not seeing traces/metrics/logs

Solutions:

// 1. Check if enabled
fmt.Println("Tracing:", cfg.IsTracingEnabled())
fmt.Println("Metrics:", cfg.IsMetricsEnabled())
fmt.Println("Logging:", cfg.IsLoggingEnabled())

// 2. Verify providers are set
if cfg.TracerProvider == nil {
    // Tracing will be no-op
}

// 3. Ensure shutdown is called
defer cfg.Shutdown(context.Background())
Default Logger Too Verbose

Problem: Stdout logger creating too much output

Solution:

// Disable default logger
cfg := otel.NewConfig("my-service", otel.WithoutLogging())

// Or use custom logger
cfg := otel.NewConfig("my-service",
    otel.WithLoggerProvider(myLoggerProvider))
Provider Already Registered

Problem: Global provider conflicts

Solution: This package doesn't use global providers - it returns scoped instruments from GetTracer(), GetMeter(), and GetLogger().

Version Compatibility

  • OpenTelemetry: v1.38.0+
  • Go: 1.25+
  • pkg library: v3.0.0+

Migration from v2

v3 absorbs the logging package into otel and switches Config construction to functional options:

// v2
import "github.com/jasoet/pkg/v2/logging"
err := logging.Initialize("my-service", false)
cfg := otel.NewConfig("my-service").WithServiceVersion("1.0.0")

// v3
import "github.com/jasoet/pkg/v3/otel"
err := otel.Initialize("my-service", false)
cfg := otel.NewConfig("my-service", otel.WithServiceVersion("1.0.0"))

See the v3 audit backlog for the full list of changes; a complete migration guide ships with v3.0.0.

  • server - HTTP server with automatic tracing
  • grpc - gRPC server with automatic instrumentation
  • db - Database with query tracing
  • rest - REST client with distributed tracing

License

MIT License - see LICENSE for details.

Documentation

Overview

Package otel provides OpenTelemetry instrumentation utilities for github.com/jasoet/pkg/v3.

This package offers:

  • Centralized configuration for traces, metrics, and logs
  • Library-specific semantic conventions
  • No-op implementations when telemetry is disabled
  • Integrated span and logging with automatic correlation
  • Layer-aware instrumentation (Handler, Middleware, Operations, Service, Repository)

Configuration

Create an otel.Config with NewConfig and functional options:

cfg := otel.NewConfig("my-service",
    otel.WithServiceVersion("1.0.0"),
    otel.WithTracerProvider(tracerProvider),  // optional
    otel.WithMeterProvider(meterProvider),    // optional
    otel.WithLoggerProvider(loggerProvider),  // optional
)

Then pass this config to package configurations (server.Config, grpc options, etc.).

Telemetry Pillars

Enable any combination of:

  • Traces (distributed tracing)
  • Metrics (measurements and aggregations)
  • Logs (structured log export via OpenTelemetry standard)

Each pillar is independently controlled by setting its provider. Nil providers result in no-op implementations with zero overhead.

Unified Layer Instrumentation

Use LayerContext for simplified span + logging with automatic correlation:

// Service layer example
lc := otel.Layers.StartService(ctx, "user", "CreateUser",
    otel.F("user_id", userID))
defer lc.End()

lc.Logger.Info("Creating user", otel.F("email", email))
if err := repo.Save(lc.Context(), data); err != nil {
    return lc.Error(err, "save failed")
}
return lc.Success("User created")

Available layers: StartHandler, StartMiddleware, StartOperations, StartService, StartRepository

Standard Logging Helper

This package provides otel.LogHelper for OTel-aware logging that automatically correlates logs with traces. It uses OTel LoggerProvider when available, otherwise falls back to zerolog. Logs automatically include trace_id and span_id when a span is active. See helper.go for details.

Example (ConfigOptionalButRecommended)

Example_configOptionalButRecommended demonstrates that config is optional but recommended.

package main

import (
	"context"
	"fmt"

	"github.com/jasoet/pkg/v3/otel"
)

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

	// Option 1: Without config (uses zerolog fallback)
	simpleApp := otel.Layers.StartService(ctx, "simple", "DoWork")
	defer simpleApp.End()
	// Logger available with zerolog fallback
	simpleApp.Logger.Info("Simple app - basic logging")

	// Option 2: With config (full observability)
	cfg := otel.NewConfig("production-service")
	ctx = otel.ContextWithConfig(ctx, cfg)
	productionApp := otel.Layers.StartService(ctx, "user", "ProcessOrder")
	defer productionApp.End()
	// Logger with OTel integration for production
	productionApp.Logger.Info("Production app - full observability")

	// Recommendation: Always pass config for production to enable:
	// - Automatic trace correlation
	// - Service name in logs
	// - Easy tracing integration later
	// - Consistent log formatting

	fmt.Println("Both patterns work, config recommended for production")

}
Output:
Both patterns work, config recommended for production
Example (GradualOTelAdoption)

Example_gradualOTelAdoption shows how to add OTel config to an existing app.

package main

import (
	"context"
	"fmt"

	"github.com/jasoet/pkg/v3/otel"
)

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

	// Phase 1: Start without config (uses zerolog fallback)
	lc1 := otel.Layers.StartService(ctx, "user", "CreateUser")
	defer lc1.End()
	// Logger uses zerolog fallback
	lc1.Logger.Info("Phase 1: Basic logging")

	// Phase 2: Add OTel config via context
	cfg := otel.NewConfig("my-service")
	ctx = otel.ContextWithConfig(ctx, cfg)
	// Later: cfg = otel.NewConfig("my-service", otel.WithTracerProvider(tp))

	lc2 := otel.Layers.StartService(ctx, "user", "CreateUser")
	defer lc2.End()
	// Logger now uses OTel with trace correlation
	lc2.Logger.Info("Phase 2: OTel integration added")

	fmt.Println("Gradual OTel adoption completed")

}
Output:
Gradual OTel adoption completed
Example (LayerContextIntegration)

Example demonstrates the new integrated span-logging features

package main

import (
	"context"
	"errors"
	"fmt"

	"github.com/jasoet/pkg/v3/otel"
)

func main() {
	// Setup OTel config
	cfg := otel.NewConfig("example-service")

	// Store config in context for automatic propagation
	ctx := otel.ContextWithConfig(context.Background(), cfg)

	// Example 1: Using LayerContext for simplified span + logging
	// Fields passed here are automatically included in all log calls
	fmt.Println("=== Example 1: LayerContext ===")
	lc := otel.Layers.StartService(ctx, "user", "CreateUser",
		otel.F("user.id", "12345"))
	defer lc.End()

	// Logger is always available, fields auto-included
	lc.Logger.Info("Creating user", otel.F("email", "user@example.com"))
	// Simulate success - user.id="12345" automatically included
	lc.Success("User created successfully")

	// Example 2: SpanHelper with Logger() method
	fmt.Println("\n=== Example 2: SpanHelper.Logger() ===")
	span := otel.StartSpan(ctx, "service.order", "ProcessOrder",
		otel.WithAttribute("order.id", "ORD-123"))
	defer span.End()

	// Logger is always available
	logger := span.Logger("service.order")
	logger.Info("Processing order", otel.F("items", 3))

	// Example 3: LogEvent for dual span events + logs
	fmt.Println("\n=== Example 3: LogEvent ===")
	span2 := otel.StartSpan(ctx, "service.cache", "GetFromCache",
		otel.WithAttribute("cache.key", "user:123"))
	defer span2.End()

	logger2 := span2.Logger("service.cache")
	span2.LogEvent(logger2, "cache.miss",
		otel.F("key", "user:123"),
		otel.F("reason", "expired"))

	// Example 4: Error handling with LayerContext
	fmt.Println("\n=== Example 4: Error Handling ===")
	lc2 := otel.Layers.StartRepository(ctx, "user", "FindByID",
		otel.F("user.id", "999"))
	defer lc2.End()

	// Simulate error
	err := errors.New("user not found")
	_ = lc2.Error(err, "failed to find user", otel.F("user.id", "999"))

	// Example 5: All five layers (config propagates automatically via context)
	// Fields are automatically included in all log calls for each layer
	fmt.Println("\n=== Example 5: All Layers ===")

	// Middleware layer (auth, CORS, rate limiting, etc.)
	middlewareCtx := otel.Layers.StartMiddleware(ctx, "auth", "ValidateToken",
		otel.F("http.path", "/api/users"),
		otel.F("http.method", "GET"))
	defer middlewareCtx.End()
	middlewareCtx.Logger.Info("Validating authentication token")

	// Handler layer (config available from middleware.Context())
	handlerCtx := otel.Layers.StartHandler(middlewareCtx.Context(), "user", "GetUser",
		otel.F("http.method", "GET"))
	defer handlerCtx.End()
	handlerCtx.Logger.Info("Handling request")

	// Operations layer (config available from handler.Context())
	opsCtx := otel.Layers.StartOperations(handlerCtx.Context(), "user", "ProcessUserRequest")
	defer opsCtx.End()
	opsCtx.Logger.Info("Orchestrating user request")

	// Service layer
	serviceCtx := otel.Layers.StartService(opsCtx.Context(), "user", "GetUser",
		otel.F("user.id", "123"))
	defer serviceCtx.End()
	serviceCtx.Logger.Info("Fetching user data")

	// Repository layer
	repoCtx := otel.Layers.StartRepository(serviceCtx.Context(), "user", "FindByID",
		otel.F("user.id", "123"),
		otel.F("db.operation", "select"))
	defer repoCtx.End()
	repoCtx.Logger.Debug("Querying database")
	repoCtx.Success("User found")

	fmt.Println("\nAll examples completed successfully")

}
Output:
=== Example 1: LayerContext ===

=== Example 2: SpanHelper.Logger() ===

=== Example 3: LogEvent ===

=== Example 4: Error Handling ===

=== Example 5: All Layers ===

All examples completed successfully
Example (LayerPropagation)

Example_layerPropagation demonstrates context propagation through layers. Config stored in context once is automatically available to all nested layers. Fields passed to each layer are automatically included in all log calls.

package main

import (
	"context"
	"fmt"

	"github.com/jasoet/pkg/v3/otel"
)

func main() {
	// Single config instance stored in context once
	cfg := otel.NewConfig("my-service")
	ctx := otel.ContextWithConfig(context.Background(), cfg)

	// Handler layer - receives HTTP request
	// Fields automatically included in all handler logs
	handler := otel.Layers.StartHandler(ctx, "user", "CreateUser",
		otel.F("http.method", "POST"),
		otel.F("http.path", "/users"))
	defer handler.End()

	// Logs include: layer="handler", http.method="POST", http.path="/users"
	handler.Logger.Info("HTTP request received")

	// Operations layer - config automatically available from context
	ops := otel.Layers.StartOperations(handler.Context(), "user", "CreateUserOperation")
	defer ops.End()

	// Logs include: layer="operations"
	ops.Logger.Info("Validating request")

	// Service layer - config still available
	service := otel.Layers.StartService(ops.Context(), "user", "CreateUser",
		otel.F("user.email", "user@example.com"))
	defer service.End()

	// Logs include: layer="service", user.email="user@example.com"
	service.Logger.Info("Creating user entity")

	// Repository layer - config still available
	repo := otel.Layers.StartRepository(service.Context(), "user", "Insert",
		otel.F("db.operation", "insert"),
		otel.F("db.table", "users"))
	defer repo.End()

	// Logs include: layer="repository", db.operation="insert", db.table="users"
	repo.Logger.Info("Inserting into database")
	repo.Success("User inserted")

	// All logs will be correlated with trace_id and span_id
	fmt.Println("Request completed with full trace")

}
Output:
Request completed with full trace
Example (LogHelperSpanAccessor)

Example showing LogHelper.Span() accessor

package main

import (
	"context"

	"github.com/jasoet/pkg/v3/otel"
)

func main() {
	cfg := otel.NewConfig("test-service")
	ctx := otel.ContextWithConfig(context.Background(), cfg)

	span := otel.StartSpan(ctx, "service.test", "Operation")
	defer span.End()

	logger := span.Logger("service.test")

	// Access span from logger
	if logger != nil && logger.Span().IsRecording() {
		logger.Info("Span is active")
	}

	// Log records are written to stderr; stdout stays empty.

}
Example (MiddlewareLayer)

Example showing middleware layer instrumentation

package main

import (
	"context"
	"errors"
	"fmt"

	"github.com/jasoet/pkg/v3/otel"
)

func main() {
	cfg := otel.NewConfig("api-service")
	ctx := otel.ContextWithConfig(context.Background(), cfg)

	fmt.Println("=== Middleware Layer Examples ===")

	// Example 1: Authentication middleware
	// Fields automatically included in all auth logs
	fmt.Println("\n--- Authentication Middleware ---")
	authCtx := otel.Layers.StartMiddleware(ctx, "auth", "ValidateToken",
		otel.F("http.path", "/api/users"),
		otel.F("http.method", "GET"))
	defer authCtx.End()

	authCtx.Logger.Info("Checking authorization header")
	// Simulate successful auth - http.path and http.method auto-included
	authCtx.Success("Token validated", otel.F("user.id", "user-123"))

	// Example 2: CORS middleware
	fmt.Println("\n--- CORS Middleware ---")
	corsCtx := otel.Layers.StartMiddleware(ctx, "cors", "SetHeaders",
		otel.F("origin", "https://example.com"))
	defer corsCtx.End()

	corsCtx.Logger.Info("Setting CORS headers")
	corsCtx.Success("CORS headers configured")

	// Example 3: Rate limiting middleware with error
	fmt.Println("\n--- Rate Limiting Middleware ---")
	rateLimitCtx := otel.Layers.StartMiddleware(ctx, "ratelimit", "CheckLimit",
		otel.F("client.ip", "192.168.1.100"),
		otel.F("endpoint", "/api/data"))
	defer rateLimitCtx.End()

	rateLimitCtx.Logger.Warn("Rate limit exceeded", otel.F("limit", 100))
	err := errors.New("rate limit exceeded")
	_ = rateLimitCtx.Error(err, "request throttled", otel.F("retry_after", "60s"))

	// Example 4: Middleware chain with context propagation
	fmt.Println("\n--- Middleware Chain ---")
	mw1Ctx := otel.Layers.StartMiddleware(ctx, "logging", "RequestLogger")
	defer mw1Ctx.End()
	mw1Ctx.Logger.Info("Incoming request", otel.F("request.id", "req-456"))

	// Next middleware gets context from previous one
	mw2Ctx := otel.Layers.StartMiddleware(mw1Ctx.Context(), "validation", "ValidateInput")
	defer mw2Ctx.End()
	mw2Ctx.Logger.Info("Validating request body")
	mw2Ctx.Success("Validation passed")
	mw1Ctx.Success("Request logged")

	fmt.Println("\nAll middleware examples completed")

}
Output:
=== Middleware Layer Examples ===

--- Authentication Middleware ---

--- CORS Middleware ---

--- Rate Limiting Middleware ---

--- Middleware Chain ---

All middleware examples completed
Example (OptionalFunctionParameter)

Example showing optional function parameter

package main

import (
	"context"

	"github.com/jasoet/pkg/v3/otel"
)

func main() {
	cfg := otel.NewConfig("test-service")
	ctx := context.Background()

	// With function name
	logger1 := otel.NewLogHelper(ctx, cfg, "mypackage", "MyFunction")
	logger1.Info("Message with function", otel.F("key", "value"))

	// Without function name (when used with spans)
	logger2 := otel.NewLogHelper(ctx, cfg, "mypackage", "")
	logger2.Info("Message without function", otel.F("key", "value"))

	// Log records are written to stderr; stdout stays empty.

}
Example (WithOTelConfig)

Example_withOTelConfig demonstrates full OTel integration with tracing and structured logging.

package main

import (
	"context"
	"fmt"

	"github.com/jasoet/pkg/v3/otel"
)

func main() {
	// Create OTel config with default logger (zerolog-based with OTel integration)
	cfg := otel.NewConfig("my-service")

	// Add TracerProvider here if you have one
	// cfg = otel.NewConfig("my-service", otel.WithTracerProvider(tracerProvider))

	// Store config in context for automatic propagation
	ctx := otel.ContextWithConfig(context.Background(), cfg)

	// Fields passed here are automatically included in all log calls
	lc := otel.Layers.StartService(ctx, "user", "CreateUser",
		otel.F("user.id", "12345"))
	defer lc.End()

	// Logs include trace_id/span_id automatically when spans are active
	// Fields "layer=service" and "user.id=12345" are automatically included
	lc.Logger.Info("Creating user with OTel", otel.F("email", "user@example.com"))

	lc.Success("User created successfully")

	fmt.Println("OTel integration active")

}
Output:
OTel integration active
Example (WithoutOTelConfig)

Example_withoutOTelConfig demonstrates span creation without OTel configuration. Logger is always available with zerolog fallback when no config is in context.

package main

import (
	"context"
	"errors"
	"fmt"

	"github.com/jasoet/pkg/v3/otel"
)

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

	// No config in context - spans work, logger uses zerolog fallback
	// Fields passed here are automatically included in all log calls
	lc := otel.Layers.StartService(ctx, "user", "CreateUser",
		otel.F("user.id", "12345"))
	defer lc.End()

	// Logger is always available (zerolog fallback without OTel config)
	// All logs automatically include: layer="service", user.id="12345"
	lc.Logger.Info("Creating user")

	// Span tracking still works
	lc.Span.AddAttribute("status", "processing")

	// Error handling works (recorded in span and log)
	err := errors.New("validation failed")
	if err != nil {
		_ = lc.Error(err, "User creation failed")
		fmt.Println("Error recorded in span and log")
		return
	}

	lc.Success("User created successfully")

}
Output:
Error recorded in span and log

Index

Examples

Constants

This section is empty.

Variables

View Source
var Layers = &LayeredSpanHelper{}

Layers provides convenience methods for creating layer-specific spans

Functions

func ContextLogger

func ContextLogger(ctx context.Context, component string) zerolog.Logger

ContextLogger creates a component-scoped logger from the global logger. The context is associated with the logger for use by zerolog hooks that read from context (e.g., trace correlation), but context.WithValue entries are not automatically extracted into log fields.

Note: ContextLogger creates a new logger instance on every call. Callers in hot paths should cache the returned logger rather than calling this per-request.

Note: Console color output is controlled by zerolog.ConsoleWriter.NoColor; callers can configure TTY detection via the Output option on zerolog.ConsoleWriter directly.

Parameters:

  • ctx: Context associated with the logger (for hooks and cancellation, not value extraction)
  • component: Name of the component, added as a field to all log entries

Returns:

  • A zerolog.Logger instance with the component field and associated context

func ContextWithConfig

func ContextWithConfig(ctx context.Context, cfg *Config) context.Context

ContextWithConfig stores the OTel config in the context. This allows nested layers to access the config without explicit parameter passing.

Example:

func (h *Handler) Handle(c echo.Context) error {
    ctx := otel.ContextWithConfig(c.Request().Context(), cfg)
    return h.service.DoWork(ctx) // Service can now access config
}

func Initialize

func Initialize(serviceName string, debug bool) error

Initialize sets up the zerolog global logger with standard fields for console-only output. This function should be called once at the start of your application. After calling Initialize, you can use zerolog's log package functions directly (log.Debug(), log.Info(), etc.) or create component-specific loggers with ContextLogger.

This is a convenience wrapper around InitializeWithFile for console-only output. For file output or multiple outputs, use InitializeWithFile directly.

PID is included in every log entry for process identification in non-containerized environments where multiple instances of the same service may run on the same host.

Parameters:

  • serviceName: Name of the service, added as a field to all log entries. Defaults to "unknown" when empty.
  • debug: If true, sets log level to Debug, otherwise Info

Returns an error if the logger cannot be initialized.

Example

ExampleInitialize demonstrates bootstrapping the global zerolog logger for plain (non-OTel) logging. Log records are written to stderr.

package main

import (
	"fmt"

	"github.com/jasoet/pkg/v3/otel"
)

func main() {
	// Console-only global logger at info level.
	if err := otel.Initialize("my-service", false); err != nil {
		panic(err)
	}

	fmt.Println("global logger initialized")

}
Output:
global logger initialized

func InitializeWithFile

func InitializeWithFile(serviceName string, debug bool, output OutputDestination, fileConfig *FileConfig) (io.Closer, error)

InitializeWithFile sets up the zerolog global logger with flexible output options. Supports console output, file output, or both simultaneously.

When file output is enabled, the returned io.Closer must be closed by the caller to release the file handle (typically via defer). When only console output is used, the returned closer is nil.

Parameters:

  • serviceName: Name of the service, added as a field to all log entries
  • debug: If true, sets log level to Debug, otherwise Info
  • output: Output destination flags (OutputConsole, OutputFile, or both combined with |)
  • fileConfig: File configuration (required if OutputFile is specified, can be nil otherwise)

Returns an io.Closer (non-nil when file output is enabled) and an error if configuration is invalid.

Example:

// Console only
_, err := InitializeWithFile("my-service", true, OutputConsole, nil)

// File only
closer, err := InitializeWithFile("my-service", false, OutputFile, &FileConfig{Path: "app.log"})
if err != nil { log.Fatal(err) }
defer closer.Close()

// Both console and file
closer, err := InitializeWithFile("my-service", true, OutputConsole|OutputFile, &FileConfig{Path: "app.log"})
if err != nil { log.Fatal(err) }
defer closer.Close()

func NewLoggerProviderWithOptions

func NewLoggerProviderWithOptions(serviceName string, opts ...LoggerProviderOption) (log.LoggerProvider, error)

NewLoggerProviderWithOptions creates a LoggerProvider with flexible options. It supports both console output (zerolog) and OTLP export, or both simultaneously.

Console output uses a synchronous processor (SimpleProcessor) intentionally, so that log records are written immediately and are visible without buffering. This is appropriate for development and for ensuring log visibility on process exit. OTLP export uses a BatchProcessor for efficiency.

Parameters:

  • serviceName: Name of the service
  • opts: Optional configuration options

Returns:

  • A log.LoggerProvider configured according to the options
  • An error if OTLP exporter creation fails

Example:

provider, err := otel.NewLoggerProviderWithOptions("my-service",
    otel.WithLogLevel(otel.LogLevelDebug),
    otel.WithOTLPEndpoint("https://localhost:4318", true),
    otel.WithConsoleOutput(true))
Example

ExampleNewLoggerProviderWithOptions demonstrates creating a console-only logger provider with a custom log level and attaching it to a Config.

package main

import (
	"fmt"

	"github.com/jasoet/pkg/v3/otel"
)

func main() {
	// Console output is enabled by default; no OTLP collector required.
	provider, err := otel.NewLoggerProviderWithOptions("my-service",
		otel.WithLogLevel(otel.LogLevelDebug))
	if err != nil {
		panic(err)
	}

	cfg := otel.NewConfig("my-service", otel.WithLoggerProvider(provider))

	fmt.Println("logging enabled:", cfg.IsLoggingEnabled())

}
Output:
logging enabled: true

Types

type Config

type Config struct {
	// TracerProvider for distributed tracing
	// If nil, tracing will be disabled (no-op tracer)
	TracerProvider trace.TracerProvider

	// MeterProvider for metrics collection
	// If nil, metrics will be disabled (no-op meter)
	MeterProvider metric.MeterProvider

	// LoggerProvider for structured logging via OTel
	// Defaults to zerolog-based provider when using NewConfig()
	// Set to nil explicitly to disable logging
	LoggerProvider log.LoggerProvider

	// ServiceName identifies the service in telemetry data
	ServiceName string

	// ServiceVersion identifies the service version
	ServiceVersion string
}

Config holds OpenTelemetry configuration for instrumentation. TracerProvider and MeterProvider are optional - nil values result in no-op implementations. LoggerProvider defaults to zerolog-based provider when using NewConfig().

Config is constructed via NewConfig with functional options. Treat it as read-only after construction.

func ConfigFromContext

func ConfigFromContext(ctx context.Context) *Config

ConfigFromContext retrieves the OTel config from context. Returns nil if no config is stored in the context.

Example:

func (s *Service) DoWork(ctx context.Context) error {
    cfg := otel.ConfigFromContext(ctx)
    if cfg != nil {
        span := otel.StartSpan(ctx, "service", "DoWork")
        defer span.End()
        logger := span.Logger("service")
        // ... use logger
    }
}

func NewConfig

func NewConfig(serviceName string, opts ...Option) *Config

NewConfig creates a new OpenTelemetry configuration with default LoggerProvider. The default LoggerProvider uses zerolog with automatic log-span correlation for production use. Pass options to add providers, set the service version, or disable signals.

Example:

cfg := otel.NewConfig("my-service",
    otel.WithTracerProvider(tp),
    otel.WithMeterProvider(mp))

For custom logger configuration:

lp, _ := otel.NewLoggerProviderWithOptions("my-service",
    otel.WithLogLevel(otel.LogLevelDebug)) // enable debug mode
cfg := otel.NewConfig("my-service", otel.WithLoggerProvider(lp))
Example

ExampleNewConfig demonstrates creating a Config with functional options. Without provider options, tracing and metrics are no-op while logging defaults to a zerolog-based provider.

package main

import (
	"fmt"

	"github.com/jasoet/pkg/v3/otel"
)

func main() {
	cfg := otel.NewConfig("my-service",
		otel.WithServiceVersion("1.0.0"))

	fmt.Println("service:", cfg.ServiceName)
	fmt.Println("version:", cfg.ServiceVersion)
	fmt.Println("logging enabled:", cfg.IsLoggingEnabled())
	fmt.Println("tracing enabled:", cfg.IsTracingEnabled())
	fmt.Println("metrics enabled:", cfg.IsMetricsEnabled())

}
Output:
service: my-service
version: 1.0.0
logging enabled: true
tracing enabled: false
metrics enabled: false

func (*Config) GetLogger

func (c *Config) GetLogger(scopeName string, opts ...log.LoggerOption) log.Logger

GetLogger returns a logger for the given instrumentation scope. Returns a no-op logger if logging is not configured.

func (*Config) GetMeter

func (c *Config) GetMeter(scopeName string, opts ...metric.MeterOption) metric.Meter

GetMeter returns a meter for the given instrumentation scope. Returns a no-op meter if metrics are not configured.

func (*Config) GetTracer

func (c *Config) GetTracer(scopeName string, opts ...trace.TracerOption) trace.Tracer

GetTracer returns a tracer for the given instrumentation scope. Returns a no-op tracer if tracing is not configured.

func (*Config) IsLoggingEnabled

func (c *Config) IsLoggingEnabled() bool

IsLoggingEnabled returns true if OTel logging is configured

func (*Config) IsMetricsEnabled

func (c *Config) IsMetricsEnabled() bool

IsMetricsEnabled returns true if metrics collection is configured

func (*Config) IsTracingEnabled

func (c *Config) IsTracingEnabled() bool

IsTracingEnabled returns true if tracing is configured

func (*Config) Shutdown

func (c *Config) Shutdown(ctx context.Context) error

Shutdown gracefully shuts down all configured providers (tracer, meter, logger). Call this when your application exits to flush any pending telemetry. Returns a combined error if any provider shutdown fails.

type Field

type Field struct {
	Key   string
	Value any
}

Field represents a key-value pair for structured logging. Use the F() function to create fields for type-safe logging.

func F

func F(key string, value any) Field

F creates a Field for structured logging. This provides a type-safe, readable way to add context to log messages.

Example:

logger.Info("User logged in", F("user_id", 123), F("email", "user@example.com"))

type FileConfig

type FileConfig struct {
	Path string // Log file path (required when OutputFile is used)
}

FileConfig configures file-based logging. File rotation should be managed by OS tools like logrotate.

type LayerContext

type LayerContext struct {
	Span   *SpanHelper
	Logger *LogHelper
}

LayerContext provides unified access to both span and logger for a layer operation. This combines span tracing and logging with automatic correlation. Base fields are automatically included in Error() and Success() log calls.

func (*LayerContext) Context

func (lc *LayerContext) Context() context.Context

Context returns the span's context for passing to child operations.

func (*LayerContext) End

func (lc *LayerContext) End()

End finishes the span. Always defer this after creating a LayerContext.

func (*LayerContext) Error

func (lc *LayerContext) Error(err error, msg string, fields ...Field) error

Error records an error to both span and logs, then returns the error. Base fields from StartX are automatically included in the log via the Logger. Additional fields are also added as span attributes for correlation.

Example:

if err := repo.Save(lc.Context(), data); err != nil {
    return lc.Error(err, "failed to save", F("id", id))
}

func (*LayerContext) Success

func (lc *LayerContext) Success(msg string, fields ...Field)

Success marks the operation as successful in both span and logs. Base fields from StartX are automatically included in the log via the Logger. Additional fields are also added as span attributes for correlation. The span status is set to codes.Ok; per the OTel specification the status description is dropped for Ok, so the message appears only in the log.

Example:

lc.Success("User created successfully", F("user_id", userID))

type LayeredSpanHelper

type LayeredSpanHelper struct{}

LayeredSpanHelper provides convenience methods for common span patterns across handler, service, and repository layers with consistent naming and attributes. All methods return LayerContext which provides both span and logger for unified tracing and logging with automatic correlation.

func (*LayeredSpanHelper) StartHandler

func (l *LayeredSpanHelper) StartHandler(ctx context.Context, component, operation string, fields ...Field) *LayerContext

StartHandler creates a LayerContext for HTTP handler layer operations. Combines span and logger with automatic correlation.

Example:

func (h *EventHandler) Create(c echo.Context) error {
    lc := otel.Layers.StartHandler(c.Request().Context(), "event", "Create",
        F("event.type", eventType))
    defer lc.End()

    lc.Logger.Info("Creating event", F("user_id", userID))
    if err := h.service.Create(lc.Context(), req); err != nil {
        return lc.Error(err, "failed to create event")
    }
    return lc.Success("Event created")
}

func (*LayeredSpanHelper) StartMiddleware

func (l *LayeredSpanHelper) StartMiddleware(ctx context.Context, component, operation string, fields ...Field) *LayerContext

StartMiddleware creates a LayerContext for middleware layer operations. Combines span and logger with automatic correlation.

Example:

func AuthMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
    return func(c echo.Context) error {
        lc := otel.Layers.StartMiddleware(c.Request().Context(), "auth", "ValidateToken",
            F("http.path", c.Path()),
            F("http.method", c.Request().Method))
        defer lc.End()

        lc.Logger.Info("Validating authentication token")
        token := c.Request().Header.Get("Authorization")
        if token == "" {
            return lc.Error(errors.New("missing token"), "authentication failed")
        }

        // Pass updated context to next handler
        c.SetRequest(c.Request().WithContext(lc.Context()))
        if err := next(c); err != nil {
            return lc.Error(err, "request failed")
        }
        return lc.Success("Request processed successfully")
    }
}

func (*LayeredSpanHelper) StartOperations

func (l *LayeredSpanHelper) StartOperations(ctx context.Context, component, operation string, fields ...Field) *LayerContext

StartOperations creates a LayerContext for operations layer orchestration. Combines span and logger with automatic correlation.

Example:

func (o *EventOps) ProcessQueue(ctx context.Context, queueName string) error {
    lc := otel.Layers.StartOperations(ctx, "event", "ProcessQueue",
        F("queue.name", queueName))
    defer lc.End()

    lc.Logger.Info("Processing queue")
    if err := o.service.Process(lc.Context()); err != nil {
        return lc.Error(err, "failed to process queue")
    }
    return lc.Success("Queue processed")
}

func (*LayeredSpanHelper) StartRepository

func (l *LayeredSpanHelper) StartRepository(ctx context.Context, component, operation string, fields ...Field) *LayerContext

StartRepository creates a LayerContext for repository layer data access. Combines span and logger with automatic correlation.

Example:

func (r *EventRepository) FindByID(ctx context.Context, eventID string) (*Event, error) {
    lc := otel.Layers.StartRepository(ctx, "event", "FindByID",
        F("event.id", eventID),
        F("db.operation", "select"))
    defer lc.End()

    lc.Logger.Debug("Querying database")
    event, err := r.db.QueryRow(lc.Context(), query, eventID)
    if err != nil {
        return nil, lc.Error(err, "query failed")
    }
    lc.Success("Event found")
    return event, nil
}

func (*LayeredSpanHelper) StartService

func (l *LayeredSpanHelper) StartService(ctx context.Context, component, operation string, fields ...Field) *LayerContext

StartService creates a LayerContext for service layer business logic. Combines span and logger with automatic correlation.

Example:

func (s *EventService) CancelEvent(ctx context.Context, eventID string) error {
    lc := otel.Layers.StartService(ctx, "event", "CancelEvent",
        F("event.id", eventID))
    defer lc.End()

    lc.Logger.Info("Canceling event")
    if err := s.repo.Update(lc.Context(), data); err != nil {
        return lc.Error(err, "failed to update event")
    }
    return lc.Success("Event canceled")
}

type LogHelper

type LogHelper struct {
	// contains filtered or unexported fields
}

LogHelper provides OTel-aware logging that automatically correlates logs with traces. It uses OTel logging when available (with automatic trace_id/span_id injection), otherwise falls back to plain zerolog.

This is the standard logging pattern for all packages in github.com/jasoet/pkg/v3:

  • When OTel is configured: uses OTel LoggerProvider for automatic log-span correlation
  • When OTel is not configured: falls back to zerolog

Usage:

logger := otel.NewLogHelper(ctx, cfg, "scope-name", "function-name")
logger.Debug("message", "key", "value")
logger.Info("message", "key", "value")
logger.Error(err, "message", "key", "value")

func NewLogHelper

func NewLogHelper(ctx context.Context, config *Config, scopeName, function string) *LogHelper

NewLogHelper creates a logger that uses OTel when available, zerolog otherwise. When OTel is enabled, logs are automatically correlated with active spans.

Note: ctx is captured at construction time. The LogHelper always uses this context for trace correlation and log emission. If the active span changes after construction, create a new LogHelper with the updated context.

Parameters:

  • ctx: Context for trace correlation (captured at construction time)
  • config: OTel configuration (can be nil for zerolog-only mode)
  • scopeName: OpenTelemetry scope name (e.g., "github.com/jasoet/pkg/v3/argo")
  • function: Function name to include in logs (optional, can be empty string)

Example:

// With OTel configured and function name
logger := otel.NewLogHelper(ctx, otelConfig, "github.com/jasoet/pkg/v3/mypackage", "mypackage.DoWork")
logger.Debug("Starting work", F("workerId", 123))

// Without function name (when used with spans)
logger := otel.NewLogHelper(ctx, otelConfig, "service.user", "")
logger.Info("Work completed")

// Without OTel (falls back to zerolog)
logger := otel.NewLogHelper(ctx, nil, "", "mypackage.DoWork")
logger.Info("Work completed")

func (*LogHelper) Debug

func (h *LogHelper) Debug(msg string, fields ...Field)

Debug logs a debug-level message with optional fields. If OTel is enabled, automatically adds trace_id and span_id.

Example:

logger.Debug("Processing request", F("request_id", reqID), F("user", userID))

func (*LogHelper) Error

func (h *LogHelper) Error(err error, msg string, fields ...Field)

Error logs an error-level message with optional fields. Also sets span status to error if a span is active.

Example:

logger.Error(err, "Failed to process request", F("request_id", reqID), F("attempt", 3))

func (*LogHelper) Info

func (h *LogHelper) Info(msg string, fields ...Field)

Info logs an info-level message with optional fields. If OTel is enabled, automatically adds trace_id and span_id.

Example:

logger.Info("User logged in", F("user_id", 123), F("role", "admin"))

func (*LogHelper) Span

func (h *LogHelper) Span() trace.Span

Span returns the active span from the logger's context. Returns a non-nil span even if no span is active (use span.IsRecording() to check).

Example:

span := logger.Span()
if span.IsRecording() {
    span.AddEvent("custom.event")
}

func (*LogHelper) Warn

func (h *LogHelper) Warn(msg string, fields ...Field)

Warn logs a warning-level message with optional fields. If OTel is enabled, automatically adds trace_id and span_id.

Example:

logger.Warn("Rate limit approaching", F("current", 95), F("limit", 100))

func (*LogHelper) WithFields

func (h *LogHelper) WithFields(fields ...Field) *LogHelper

WithFields returns a new LogHelper with additional base fields. These fields will be automatically included in every log call.

Example:

logger := otel.NewLogHelper(ctx, cfg, "service.user", "").
    WithFields(F("user.id", userID), F("action", "create"))
logger.Info("Processing request") // Includes user.id and action

type LogLevel

type LogLevel string

LogLevel represents the logging level for the console/OTel log pipeline. Trace and Fatal levels are intentionally excluded: Trace is not supported by zerolog natively, and Fatal triggers os.Exit which is unsuitable for library use.

const (
	LogLevelDebug LogLevel = "debug"
	LogLevelInfo  LogLevel = "info"
	LogLevelWarn  LogLevel = "warn"
	LogLevelError LogLevel = "error"
	LogLevelNone  LogLevel = "none"
)

type LoggerProviderOption

type LoggerProviderOption func(*loggerProviderConfig)

LoggerProviderOption configures LoggerProvider behavior

func WithConsoleOutput

func WithConsoleOutput(enabled bool) LoggerProviderOption

WithConsoleOutput enables console logging alongside OTLP

func WithLogLevel

func WithLogLevel(level LogLevel) LoggerProviderOption

WithLogLevel sets the log level for console output Valid levels: "debug", "info", "warn", "error", "none" If not specified, defaults to "info"

func WithOTLPEndpoint

func WithOTLPEndpoint(endpoint string, insecure bool) LoggerProviderOption

WithOTLPEndpoint enables OTLP log export. The endpoint format depends on the exporter protocol:

This package uses otlploghttp, so provide a full URL with scheme.

type Option

type Option func(*Config)

Option configures a Config during construction via NewConfig.

func WithLoggerProvider

func WithLoggerProvider(lp log.LoggerProvider) Option

WithLoggerProvider sets a custom LoggerProvider, replacing the default stdout logger.

func WithMeterProvider

func WithMeterProvider(mp metric.MeterProvider) Option

WithMeterProvider sets the meter provider; passing nil disables metrics (falls back to no-op).

func WithServiceVersion

func WithServiceVersion(version string) Option

WithServiceVersion sets the service version for telemetry data.

func WithTracerProvider

func WithTracerProvider(tp trace.TracerProvider) Option

WithTracerProvider sets the tracer provider; passing nil disables tracing (falls back to no-op).

func WithoutLogging

func WithoutLogging() Option

WithoutLogging disables the default logging by setting LoggerProvider to nil.

func WithoutMetrics

func WithoutMetrics() Option

WithoutMetrics disables metrics by setting MeterProvider to nil.

func WithoutTracing

func WithoutTracing() Option

WithoutTracing disables tracing by setting TracerProvider to nil.

type OutputDestination

type OutputDestination int

OutputDestination defines where logs should be written. Multiple destinations can be combined using bitwise OR.

const (
	OutputConsole OutputDestination = 1 << 0 // Output to console (stderr)
	OutputFile    OutputDestination = 1 << 1 // Output to file
)

type SpanHelper

type SpanHelper struct {
	// contains filtered or unexported fields
}

SpanHelper provides a convenient way to create and manage spans with automatic error handling and status management. It's designed for use in service and repository layers where consistent span instrumentation is needed.

Usage pattern:

func (s *Service) DoWork(ctx context.Context, id string) error {
    span := otel.StartSpan(ctx, "service.example", "Service.DoWork",
        otel.WithAttribute("entity.id", id))
    defer span.End()

    // IMPORTANT: Use span.Context() for child operations to maintain trace correlation
    if err := s.repository.Save(span.Context(), data); err != nil {
        return span.Error(err, "failed to save data")
    }

    return span.Success()
}

func StartSpan

func StartSpan(ctx context.Context, tracerName, operationName string, opts ...SpanOption) *SpanHelper

StartSpan creates a new span with the given tracer name and operation name. The tracer name should follow the pattern "layer.component" (e.g., "service.event", "repository.ticket"). The operation name should be descriptive (e.g., "EventService.CancelEvent", "TicketRepository.FindByID").

This is the recommended way to create spans in service and repository layers.

Example:

// In service layer
span := otel.StartSpan(ctx, "service.event", "EventService.CancelEvent",
    otel.WithAttribute("event.id", eventID))
defer span.End()

// In repository layer
span := otel.StartSpan(ctx, "repository.event", "EventRepository.FindByID",
    otel.WithAttribute("event.id", eventID),
    otel.WithAttribute("db.operation", "select"))
defer span.End()

func (*SpanHelper) AddAttribute

func (h *SpanHelper) AddAttribute(key string, value any) *SpanHelper

AddAttribute adds a single attribute to the span.

func (*SpanHelper) AddAttributes

func (h *SpanHelper) AddAttributes(fields ...Field) *SpanHelper

AddAttributes adds multiple attributes to the span.

func (*SpanHelper) AddEvent

func (h *SpanHelper) AddEvent(name string, fields ...Field) *SpanHelper

AddEvent adds an event to the span with optional attributes.

Example:

span.AddEvent("cache.hit", F("key", cacheKey), F("ttl", ttl))

func (*SpanHelper) Context

func (h *SpanHelper) Context() context.Context

Context returns the context with the span attached. Use this when calling child functions that need the span context.

func (*SpanHelper) End

func (h *SpanHelper) End()

End finishes the span. Always defer this after creating a span.

Example:

span := otel.StartSpan(ctx, "service", "Operation")
defer span.End()

func (*SpanHelper) Error

func (h *SpanHelper) Error(err error, message string) error

Error records an error and sets the span status to error. Returns the error unchanged for easy error propagation.

Example:

if err := doWork(); err != nil {
    return span.Error(err, "work failed")
}

func (*SpanHelper) FunctionLogger

func (h *SpanHelper) FunctionLogger(scopeName string, function string) *LogHelper

FunctionLogger creates a LogHelper that is automatically correlated with this span. Returns a LogHelper with the default zerolog logger if no config is stored in the context. Use ContextWithConfig() to store config in context before creating spans.

Example:

ctx = otel.ContextWithConfig(ctx, cfg)
span := otel.StartSpan(ctx, "service.user", "CreateUser",
    otel.WithAttribute("user.id", userID))
defer span.End()

logger := span.FunctionLogger("service.user","function.name")
logger.Info("Creating user", F("email", email))

func (*SpanHelper) LogEvent

func (h *SpanHelper) LogEvent(logger *LogHelper, eventName string, fields ...Field) *SpanHelper

LogEvent creates both a span event and a log entry for better correlation. This is useful for significant events that should appear in both traces and logs.

Example:

logger := span.Logger("service.cache")
span.LogEvent(logger, "cache.miss",
    F("key", cacheKey),
    F("reason", "expired"))

func (*SpanHelper) Logger

func (h *SpanHelper) Logger(scopeName string) *LogHelper

Logger creates a LogHelper that is automatically correlated with this span. Returns a LogHelper with the default zerolog logger if no config is stored in the context. Use ContextWithConfig() to store config in context before creating spans.

Example:

ctx = otel.ContextWithConfig(ctx, cfg)
span := otel.StartSpan(ctx, "service.user", "CreateUser",
    otel.WithAttribute("user.id", userID))
defer span.End()

logger := span.Logger("service.user")

logger.Info("Creating user", F("email", email))

func (*SpanHelper) Span

func (h *SpanHelper) Span() trace.Span

Span returns the underlying trace.Span for advanced usage.

func (*SpanHelper) Success

func (h *SpanHelper) Success(message string)

Success marks the span as successful with an optional message. This is optional but provides explicit success signaling. Note: per the OTel specification, the status description is dropped for codes.Ok, so the message is not retained on the span.

Example:

span.Success("operation completed")

type SpanOption

type SpanOption func(*spanConfig)

SpanOption allows customizing span creation

func WithAttribute

func WithAttribute(key string, value any) SpanOption

WithAttribute adds an attribute to the span

func WithAttributes

func WithAttributes(fields ...Field) SpanOption

WithAttributes adds multiple attributes to the span

func WithSpanKind

func WithSpanKind(kind trace.SpanKind) SpanOption

WithSpanKind sets the span kind (Internal, Server, Client, Producer, Consumer)

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL