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.
}
Output:
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.
}
Output:
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 ¶
- Variables
- func ContextLogger(ctx context.Context, component string) zerolog.Logger
- func ContextWithConfig(ctx context.Context, cfg *Config) context.Context
- func Initialize(serviceName string, debug bool) error
- func InitializeWithFile(serviceName string, debug bool, output OutputDestination, ...) (io.Closer, error)
- func NewLoggerProviderWithOptions(serviceName string, opts ...LoggerProviderOption) (log.LoggerProvider, error)
- type Config
- func (c *Config) GetLogger(scopeName string, opts ...log.LoggerOption) log.Logger
- func (c *Config) GetMeter(scopeName string, opts ...metric.MeterOption) metric.Meter
- func (c *Config) GetTracer(scopeName string, opts ...trace.TracerOption) trace.Tracer
- func (c *Config) IsLoggingEnabled() bool
- func (c *Config) IsMetricsEnabled() bool
- func (c *Config) IsTracingEnabled() bool
- func (c *Config) Shutdown(ctx context.Context) error
- type Field
- type FileConfig
- type LayerContext
- type LayeredSpanHelper
- func (l *LayeredSpanHelper) StartHandler(ctx context.Context, component, operation string, fields ...Field) *LayerContext
- func (l *LayeredSpanHelper) StartMiddleware(ctx context.Context, component, operation string, fields ...Field) *LayerContext
- func (l *LayeredSpanHelper) StartOperations(ctx context.Context, component, operation string, fields ...Field) *LayerContext
- func (l *LayeredSpanHelper) StartRepository(ctx context.Context, component, operation string, fields ...Field) *LayerContext
- func (l *LayeredSpanHelper) StartService(ctx context.Context, component, operation string, fields ...Field) *LayerContext
- type LogHelper
- func (h *LogHelper) Debug(msg string, fields ...Field)
- func (h *LogHelper) Error(err error, msg string, fields ...Field)
- func (h *LogHelper) Info(msg string, fields ...Field)
- func (h *LogHelper) Span() trace.Span
- func (h *LogHelper) Warn(msg string, fields ...Field)
- func (h *LogHelper) WithFields(fields ...Field) *LogHelper
- type LogLevel
- type LoggerProviderOption
- type Option
- type OutputDestination
- type SpanHelper
- func (h *SpanHelper) AddAttribute(key string, value any) *SpanHelper
- func (h *SpanHelper) AddAttributes(fields ...Field) *SpanHelper
- func (h *SpanHelper) AddEvent(name string, fields ...Field) *SpanHelper
- func (h *SpanHelper) Context() context.Context
- func (h *SpanHelper) End()
- func (h *SpanHelper) Error(err error, message string) error
- func (h *SpanHelper) FunctionLogger(scopeName string, function string) *LogHelper
- func (h *SpanHelper) LogEvent(logger *LogHelper, eventName string, fields ...Field) *SpanHelper
- func (h *SpanHelper) Logger(scopeName string) *LogHelper
- func (h *SpanHelper) Span() trace.Span
- func (h *SpanHelper) Success(message string)
- type SpanOption
Examples ¶
- Package (ConfigOptionalButRecommended)
- Package (GradualOTelAdoption)
- Package (LayerContextIntegration)
- Package (LayerPropagation)
- Package (LogHelperSpanAccessor)
- Package (MiddlewareLayer)
- Package (OptionalFunctionParameter)
- Package (WithOTelConfig)
- Package (WithoutOTelConfig)
- Initialize
- NewConfig
- NewLoggerProviderWithOptions
Constants ¶
This section is empty.
Variables ¶
var Layers = &LayeredSpanHelper{}
Layers provides convenience methods for creating layer-specific spans
Functions ¶
func ContextLogger ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
GetLogger returns a logger for the given instrumentation scope. Returns a no-op logger if logging is not configured.
func (*Config) GetMeter ¶
GetMeter returns a meter for the given instrumentation scope. Returns a no-op meter if metrics are not configured.
func (*Config) GetTracer ¶
GetTracer returns a tracer for the given instrumentation scope. Returns a no-op tracer if tracing is not configured.
func (*Config) IsLoggingEnabled ¶
IsLoggingEnabled returns true if OTel logging is configured
func (*Config) IsMetricsEnabled ¶
IsMetricsEnabled returns true if metrics collection is configured
func (*Config) IsTracingEnabled ¶
IsTracingEnabled returns true if tracing is configured
type Field ¶
Field represents a key-value pair for structured logging. Use the F() function to create fields for type-safe logging.
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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.
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:
- HTTP (otlploghttp): full URL, e.g. "https://collector.example.com:4318"
- gRPC (otlploggrpc): host:port without scheme, e.g. "collector.example.com:4317"
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 ¶
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)