otel

package
v2.5.0 Latest Latest
Warning

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

Go to latest
Published: Nov 15, 2025 License: MIT Imports: 18 Imported by: 0

README

OpenTelemetry Integration

Go Reference

Unified OpenTelemetry v2 configuration and instrumentation 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

Features

  • Unified Configuration: Single config object for all telemetry pillars
  • Selective Enablement: Enable only the telemetry you need
  • No-op by Default: Zero overhead when providers are not configured
  • Method Chaining: Fluent API for configuration
  • Standard Logging Helper: OTel-aware logging with automatic trace correlation
  • Graceful Shutdown: Proper resource cleanup

Installation

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

Quick Start

Basic Configuration
package main

import (
    "context"
    "github.com/jasoet/pkg/v2/otel"
    "go.opentelemetry.io/otel/sdk/trace"
    "go.opentelemetry.io/otel/sdk/metric"
)

func main() {
    // Create tracer and meter providers (your setup)
    tracerProvider := trace.NewTracerProvider(/* ... */)
    meterProvider := metric.NewMeterProvider(/* ... */)

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

    // Use with library packages
    // server.Start(server.Config{OTelConfig: otelConfig, ...})
    // db.Pool(db.Config{OTelConfig: otelConfig, ...})

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

Enable only what you need:

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

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

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

Use the logging package for better formatting and automatic trace correlation:

import (
    "github.com/jasoet/pkg/v2/logging"
    "github.com/jasoet/pkg/v2/otel"
)

// Production-ready logger with trace correlation
loggerProvider := logging.NewLoggerProvider("my-service", false)

cfg := otel.NewConfig("my-service").
    WithTracerProvider(tracerProvider).
    WithMeterProvider(meterProvider).
    WithLoggerProvider(loggerProvider)

Configuration API

Config Struct
type Config struct {
    TracerProvider trace.TracerProvider  // nil = no tracing
    MeterProvider  metric.MeterProvider  // nil = no metrics
    LoggerProvider log.LoggerProvider    // nil = no OTel logs
    ServiceName    string
    ServiceVersion string
}
Builder Methods
Method Description
NewConfig(name) Create config with service name and default logger
WithTracerProvider(tp) Enable distributed tracing
WithMeterProvider(mp) Enable metrics collection
WithLoggerProvider(lp) Set custom logger provider
WithServiceVersion(v) Set service version
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

// Cleanup
cfg.Shutdown(context.Background())

Standard Logging Helper

The otel package provides LogHelper for OTel-aware logging with automatic log-span correlation:

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

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

// Log with automatic trace_id/span_id injection (when OTel is enabled)
logger.Debug("Starting work", "workerId", 123)
logger.Info("Work completed", "duration", elapsed)
logger.Error(err, "Work failed", "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.

Integration Examples

HTTP Server
import (
    "github.com/jasoet/pkg/v2/otel"
    "github.com/jasoet/pkg/v2/server"
)

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

server.Start(server.Config{
    Port:       8080,
    OTelConfig: otelConfig,
})
gRPC Server
import (
    "github.com/jasoet/pkg/v2/otel"
    "github.com/jasoet/pkg/v2/grpc"
)

otelConfig := otel.NewConfig("my-grpc-service").
    WithTracerProvider(tracerProvider).
    WithMeterProvider(meterProvider)

grpcServer := grpc.NewServer(
    grpc.NewConfig("my-service", 9090).
        WithOTelConfig(otelConfig),
)
Database
import (
    "github.com/jasoet/pkg/v2/otel"
    "github.com/jasoet/pkg/v2/db"
)

otelConfig := otel.NewConfig("my-db-service").
    WithTracerProvider(tracerProvider).
    WithMeterProvider(meterProvider)

pool, _ := db.ConnectionConfig{
    DbType:     db.Postgresql,
    Host:       "localhost",
    OTelConfig: otelConfig,
}.Pool()

// All queries are automatically traced
pool.Find(&users)
REST Client
import (
    "github.com/jasoet/pkg/v2/otel"
    "github.com/jasoet/pkg/v2/rest"
)

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

client := rest.NewClient(rest.ClientConfig{
    BaseURL:    "https://api.example.com",
    OTelConfig: otelConfig,
})

// Requests are automatically traced
client.Get("/users", &result)

Complete Example

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

Testing

The package includes comprehensive tests with 97.1% coverage:

# Run tests
go test ./otel -v

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

Use no-op providers for testing:

import (
    "github.com/jasoet/pkg/v2/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").
        WithTracerProvider(noopt.NewTracerProvider()).
        WithMeterProvider(noopm.NewMeterProvider()).
        WithoutLogging()

    // Test your code with cfg
}

Best Practices

1. Create Once, Share Everywhere
// ✅ Good: Single config shared across packages
otelConfig := otel.NewConfig("my-service").
    WithTracerProvider(tp).
    WithMeterProvider(mp)

serverCfg := server.Config{OTelConfig: otelConfig}
dbCfg := db.Config{OTelConfig: otelConfig}
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/v2/mypackage", "mypackage.DoWork")
logger.Info("Work completed", "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 builder methods
├── helper.go        # Standard logging helper with OTel integration
├── helper_test.go   # LogHelper tests
├── doc.go          # Package documentation
└── config_test.go  # Config tests

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").WithoutLogging()

// Or use custom logger
cfg := otel.NewConfig("my-service").
    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: v2.0.0+

Migration from v1

v2 uses OpenTelemetry v2 API:

// v1 (OTel v1)
import "go.opentelemetry.io/otel"
tracer := otel.Tracer("my-scope")

// v2 (OTel v2)
import "github.com/jasoet/pkg/v2/otel"
cfg := otel.NewConfig("my-service").WithTracerProvider(tp)
tracer := cfg.GetTracer("my-scope")

See VERSIONING_GUIDE.md for complete migration guide.

  • logging - Structured logging with OTel integration
  • 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/v2.

This package offers:

  • Centralized configuration for traces, metrics, and logs
  • Library-specific semantic conventions
  • No-op implementations when telemetry is disabled

Configuration

Create an otel.Config with the desired providers:

cfg := &otel.Config{
    TracerProvider: tracerProvider,  // optional
    MeterProvider:  meterProvider,   // optional
    LoggerProvider: loggerProvider,  // optional
    ServiceName:    "my-service",
    ServiceVersion: "1.0.0",
}

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.

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. See helper.go for details.

Index

Constants

View Source
const (
	LogLevelDebug = logging.LogLevelDebug
	LogLevelInfo  = logging.LogLevelInfo
	LogLevelWarn  = logging.LogLevelWarn
	LogLevelError = logging.LogLevelError
	LogLevelNone  = logging.LogLevelNone
)

Re-export LogLevel constants from logging package

Variables

This section is empty.

Functions

func NewLoggerProviderWithOptions added in v2.4.8

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

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

Parameters:

  • serviceName: Name of the service
  • debug: If true, sets log level to Debug, otherwise Info
  • 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", false,
    otel.WithOTLPEndpoint("localhost:4318", true),
    otel.WithConsoleOutput(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().

func NewConfig

func NewConfig(serviceName string) *Config

NewConfig creates a new OpenTelemetry configuration with default LoggerProvider. The default LoggerProvider uses zerolog with automatic log-span correlation for production use. Use With* methods to add TracerProvider and MeterProvider.

Example:

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

For custom logger configuration:

import "github.com/jasoet/pkg/v2/logging"
cfg := &otel.Config{
    ServiceName:    "my-service",
    LoggerProvider: logging.NewLoggerProvider("my-service", true), // enable debug mode
}
cfg.WithTracerProvider(tp).WithMeterProvider(mp)

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 Call this when your application exits to flush any pending telemetry

func (*Config) WithLoggerProvider

func (c *Config) WithLoggerProvider(lp log.LoggerProvider) *Config

WithLoggerProvider sets a custom LoggerProvider, replacing the default stdout logger

func (*Config) WithMeterProvider

func (c *Config) WithMeterProvider(mp metric.MeterProvider) *Config

WithMeterProvider sets the MeterProvider for metrics collection

func (*Config) WithServiceVersion

func (c *Config) WithServiceVersion(version string) *Config

WithServiceVersion sets the service version for telemetry data

func (*Config) WithTracerProvider

func (c *Config) WithTracerProvider(tp trace.TracerProvider) *Config

WithTracerProvider sets the TracerProvider for distributed tracing

func (*Config) WithoutLogging

func (c *Config) WithoutLogging() *Config

WithoutLogging disables the default logging by setting LoggerProvider to nil

type Field added in v2.2.1

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 added in v2.2.1

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 LogHelper added in v2.2.0

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

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/v2:

  • 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 added in v2.2.0

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.

Parameters:

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

Example:

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

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

func (*LogHelper) Debug added in v2.2.0

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 added in v2.2.0

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 added in v2.2.0

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) Warn added in v2.2.0

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))

type LogLevel added in v2.4.9

type LogLevel = logging.LogLevel

LogLevel is an alias for logging.LogLevel for convenience

type LoggerProviderOption added in v2.4.8

type LoggerProviderOption func(*loggerProviderConfig)

LoggerProviderOption configures LoggerProvider behavior

func WithConsoleOutput added in v2.4.8

func WithConsoleOutput(enabled bool) LoggerProviderOption

WithConsoleOutput enables console logging alongside OTLP

func WithLogLevel added in v2.4.9

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" (or "debug" if debug parameter is true)

func WithOTLPEndpoint added in v2.4.8

func WithOTLPEndpoint(endpoint string, insecure bool) LoggerProviderOption

WithOTLPEndpoint enables OTLP log export

Jump to

Keyboard shortcuts

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