324 lines
8.2 KiB
Go
324 lines
8.2 KiB
Go
// Package log provides structured logging with context support for request tracing.
|
|
package log
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"sync"
|
|
"time"
|
|
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
// contextKey is a type for context keys to avoid collisions.
|
|
type contextKey string
|
|
|
|
const (
|
|
// correlationIDKey is the context key for correlation ID.
|
|
correlationIDKey contextKey = "correlation_id"
|
|
// operationKey is the context key for operation name.
|
|
operationKey contextKey = "operation"
|
|
// startTimeKey is the context key for operation start time.
|
|
startTimeKey contextKey = "start_time"
|
|
)
|
|
|
|
var (
|
|
// correlationIDGenerator provides thread-safe ID generation.
|
|
correlationIDGenerator = &idGenerator{}
|
|
)
|
|
|
|
// idGenerator generates unique correlation IDs.
|
|
type idGenerator struct {
|
|
mu sync.Mutex
|
|
seq uint64
|
|
}
|
|
|
|
// Generate creates a new unique correlation ID.
|
|
func (g *idGenerator) Generate() string {
|
|
g.mu.Lock()
|
|
defer g.mu.Unlock()
|
|
g.seq++
|
|
return time.Now().Format("20060102-150405") + "-" + string(rune(g.seq))
|
|
}
|
|
|
|
// WithCorrelationID adds a correlation ID to the context for request tracing.
|
|
// If no ID is provided, a new one will be generated.
|
|
func WithCorrelationID(ctx context.Context, id string) context.Context {
|
|
if id == "" {
|
|
id = correlationIDGenerator.Generate()
|
|
}
|
|
return context.WithValue(ctx, correlationIDKey, id)
|
|
}
|
|
|
|
// WithOperation adds an operation name to the context.
|
|
func WithOperation(ctx context.Context, operation string) context.Context {
|
|
return context.WithValue(ctx, operationKey, operation)
|
|
}
|
|
|
|
// WithStartTime adds operation start time to the context.
|
|
func WithStartTime(ctx context.Context) context.Context {
|
|
return context.WithValue(ctx, startTimeKey, time.Now())
|
|
}
|
|
|
|
// GetCorrelationID retrieves the correlation ID from context.
|
|
func GetCorrelationID(ctx context.Context) string {
|
|
if ctx == nil {
|
|
return ""
|
|
}
|
|
if id, ok := ctx.Value(correlationIDKey).(string); ok {
|
|
return id
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// GetOperation retrieves the operation name from context.
|
|
func GetOperation(ctx context.Context) string {
|
|
if ctx == nil {
|
|
return ""
|
|
}
|
|
if op, ok := ctx.Value(operationKey).(string); ok {
|
|
return op
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// GetStartTime retrieves the operation start time from context.
|
|
func GetStartTime(ctx context.Context) time.Time {
|
|
if ctx == nil {
|
|
return time.Time{}
|
|
}
|
|
if t, ok := ctx.Value(startTimeKey).(time.Time); ok {
|
|
return t
|
|
}
|
|
return time.Time{}
|
|
}
|
|
|
|
// Duration returns the elapsed time since the operation started.
|
|
// Returns 0 if no start time is set in context.
|
|
func Duration(ctx context.Context) time.Duration {
|
|
start := GetStartTime(ctx)
|
|
if start.IsZero() {
|
|
return 0
|
|
}
|
|
return time.Since(start)
|
|
}
|
|
|
|
// Logger provides request-scoped logging with context fields.
|
|
type Logger struct {
|
|
*zap.Logger
|
|
ctx context.Context
|
|
}
|
|
|
|
// WithContext creates a new Logger with context fields.
|
|
func WithContext(ctx context.Context) *Logger {
|
|
return &Logger{
|
|
Logger: Default(),
|
|
ctx: ctx,
|
|
}
|
|
}
|
|
|
|
// WithLogger creates a new Logger with a specific zap logger and context.
|
|
func WithLogger(logger *zap.Logger, ctx context.Context) *Logger {
|
|
return &Logger{
|
|
Logger: logger,
|
|
ctx: ctx,
|
|
}
|
|
}
|
|
|
|
// contextFields returns zap fields from context values.
|
|
func (l *Logger) contextFields() []zap.Field {
|
|
if l.ctx == nil {
|
|
return nil
|
|
}
|
|
|
|
var fields []zap.Field
|
|
|
|
if id := GetCorrelationID(l.ctx); id != "" {
|
|
fields = append(fields, zap.String("correlation_id", id))
|
|
}
|
|
|
|
if op := GetOperation(l.ctx); op != "" {
|
|
fields = append(fields, zap.String("operation", op))
|
|
}
|
|
|
|
if start := GetStartTime(l.ctx); !start.IsZero() {
|
|
fields = append(fields, zap.Duration("duration", time.Since(start)))
|
|
}
|
|
|
|
return fields
|
|
}
|
|
|
|
// Debug logs a message at debug level with context fields.
|
|
func (l *Logger) Debug(msg string, fields ...zap.Field) {
|
|
allFields := append(l.contextFields(), fields...)
|
|
l.Logger.Debug(msg, allFields...)
|
|
}
|
|
|
|
// Info logs a message at info level with context fields.
|
|
func (l *Logger) Info(msg string, fields ...zap.Field) {
|
|
allFields := append(l.contextFields(), fields...)
|
|
l.Logger.Info(msg, allFields...)
|
|
}
|
|
|
|
// Warn logs a message at warn level with context fields.
|
|
func (l *Logger) Warn(msg string, fields ...zap.Field) {
|
|
allFields := append(l.contextFields(), fields...)
|
|
l.Logger.Warn(msg, allFields...)
|
|
}
|
|
|
|
// Error logs a message at error level with context fields.
|
|
func (l *Logger) Error(msg string, fields ...zap.Field) {
|
|
allFields := append(l.contextFields(), fields...)
|
|
l.Logger.Error(msg, allFields...)
|
|
}
|
|
|
|
// Fatal logs a message at fatal level with context fields.
|
|
func (l *Logger) Fatal(msg string, fields ...zap.Field) {
|
|
allFields := append(l.contextFields(), fields...)
|
|
l.Logger.Fatal(msg, allFields...)
|
|
}
|
|
|
|
// Panic logs a message at panic level with context fields.
|
|
func (l *Logger) Panic(msg string, fields ...zap.Field) {
|
|
allFields := append(l.contextFields(), fields...)
|
|
l.Logger.Panic(msg, allFields...)
|
|
}
|
|
|
|
// Debugf logs a formatted message at debug level with context fields.
|
|
func (l *Logger) Debugf(format string, args ...any) {
|
|
l.Debug(fmt.Sprintf(format, args...))
|
|
}
|
|
|
|
// Infof logs a formatted message at info level with context fields.
|
|
func (l *Logger) Infof(format string, args ...any) {
|
|
l.Info(fmt.Sprintf(format, args...))
|
|
}
|
|
|
|
// Warnf logs a formatted message at warn level with context fields.
|
|
func (l *Logger) Warnf(format string, args ...any) {
|
|
l.Warn(fmt.Sprintf(format, args...))
|
|
}
|
|
|
|
// Errorf logs a formatted message at error level with context fields.
|
|
func (l *Logger) Errorf(format string, args ...any) {
|
|
l.Error(fmt.Sprintf(format, args...))
|
|
}
|
|
|
|
// Fatalf logs a formatted message at fatal level with context fields.
|
|
func (l *Logger) Fatalf(format string, args ...any) {
|
|
l.Fatal(fmt.Sprintf(format, args...))
|
|
}
|
|
|
|
// Panicf logs a formatted message at panic level with context fields.
|
|
func (l *Logger) Panicf(format string, args ...any) {
|
|
l.Panic(fmt.Sprintf(format, args...))
|
|
}
|
|
|
|
// With creates a child logger with additional fields.
|
|
func (l *Logger) With(fields ...zap.Field) *Logger {
|
|
return &Logger{
|
|
Logger: l.Logger.With(fields...),
|
|
ctx: l.ctx,
|
|
}
|
|
}
|
|
|
|
// WithError creates an error log entry with error details.
|
|
func (l *Logger) WithError(err error) *ErrorEntry {
|
|
return &ErrorEntry{
|
|
Logger: l,
|
|
err: err,
|
|
}
|
|
}
|
|
|
|
// ErrorEntry provides structured error logging.
|
|
type ErrorEntry struct {
|
|
*Logger
|
|
err error
|
|
}
|
|
|
|
// Log logs the error with additional context.
|
|
func (e *ErrorEntry) Log(msg string) {
|
|
if e.err == nil {
|
|
e.Error(msg)
|
|
return
|
|
}
|
|
|
|
fields := []zap.Field{
|
|
zap.Error(e.err),
|
|
zap.String("error_type", fmt.Sprintf("%T", e.err)),
|
|
}
|
|
|
|
e.Error(msg, fields...)
|
|
}
|
|
|
|
// LogWithStatus logs the error with HTTP status code.
|
|
func (e *ErrorEntry) LogWithStatus(msg string, statusCode int) {
|
|
if e.err == nil {
|
|
e.Error(msg, zap.Int("status_code", statusCode))
|
|
return
|
|
}
|
|
|
|
fields := []zap.Field{
|
|
zap.Error(e.err),
|
|
zap.String("error_type", fmt.Sprintf("%T", e.err)),
|
|
zap.Int("status_code", statusCode),
|
|
}
|
|
|
|
e.Error(msg, fields...)
|
|
}
|
|
|
|
// OperationLogger provides a convenient way to log operation execution.
|
|
type OperationLogger struct {
|
|
logger *Logger
|
|
operation string
|
|
start time.Time
|
|
}
|
|
|
|
// StartOperation begins logging an operation with timing.
|
|
func StartOperation(ctx context.Context, operation string) *OperationLogger {
|
|
logger := WithContext(WithOperation(ctx, operation))
|
|
logger.ctx = WithStartTime(logger.ctx)
|
|
|
|
return &OperationLogger{
|
|
logger: logger,
|
|
operation: operation,
|
|
start: time.Now(),
|
|
}
|
|
}
|
|
|
|
// Start begins the operation logging with a start message.
|
|
func (o *OperationLogger) Start(msg string) {
|
|
o.logger.Info(msg, zap.String("phase", "start"))
|
|
}
|
|
|
|
// Success logs a successful operation completion.
|
|
func (o *OperationLogger) Success(msg string) {
|
|
duration := time.Since(o.start)
|
|
o.logger.Info(msg,
|
|
zap.String("phase", "complete"),
|
|
zap.String("status", "success"),
|
|
zap.Duration("duration", duration),
|
|
)
|
|
}
|
|
|
|
// Failure logs a failed operation.
|
|
func (o *OperationLogger) Failure(msg string, err error) {
|
|
duration := time.Since(o.start)
|
|
fields := []zap.Field{
|
|
zap.String("phase", "complete"),
|
|
zap.String("status", "failure"),
|
|
zap.Duration("duration", duration),
|
|
}
|
|
|
|
if err != nil {
|
|
fields = append(fields, zap.Error(err))
|
|
}
|
|
|
|
o.logger.Error(msg, fields...)
|
|
}
|
|
|
|
// Duration returns the elapsed time since operation started.
|
|
func (o *OperationLogger) Duration() time.Duration {
|
|
return time.Since(o.start)
|
|
}
|