// Package logging provides structured logging infrastructure with correlation ID support. package logging import ( "context" "fmt" "os" "path/filepath" "sync" "time" "github.com/router-for-me/CLIProxyAPI/v6/internal/config" "github.com/router-for-me/CLIProxyAPI/v6/internal/logging" "github.com/sirupsen/logrus" "gopkg.in/natefinch/lumberjack.v2" ) // contextKey is used for storing values in context type contextKey string const ( // CorrelationIDKey is the context key for correlation IDs CorrelationIDKey contextKey = "request_id" // ServiceKey is the context key for service name ServiceKey contextKey = "service_name" // OperationKey is the context key for operation name OperationKey contextKey = "operation_name" ) // StructuredLogger provides structured logging with correlation ID support type StructuredLogger struct { logger *logrus.Logger mu sync.RWMutex logWriter *lumberjack.Logger } // LogEntry represents a structured log entry type LogEntry struct { Timestamp time.Time `json:"timestamp"` Level string `json:"level"` Message string `json:"message"` CorrelationID string `json:"correlation_id,omitempty"` Service string `json:"service,omitempty"` Operation string `json:"operation,omitempty"` Fields map[string]interface{} `json:"fields,omitempty"` } // NewStructuredLogger creates a new structured logger func NewStructuredLogger() *StructuredLogger { logger := logrus.New() logger.SetFormatter(&logrus.JSONFormatter{ TimestampFormat: time.RFC3339Nano, }) logger.SetOutput(os.Stdout) logger.SetLevel(logrus.InfoLevel) return &StructuredLogger{ logger: logger, } } // Configure configures the logger with the given configuration func (l *StructuredLogger) Configure(cfg *config.Config) error { l.mu.Lock() defer l.mu.Unlock() // Set log level if cfg.Debug { l.logger.SetLevel(logrus.DebugLevel) } else { l.logger.SetLevel(logrus.InfoLevel) } // Configure file logging if enabled if cfg.LoggingToFile { logDir := l.resolveLogDirectory(cfg) if err := os.MkdirAll(logDir, 0755); err != nil { return fmt.Errorf("failed to create log directory: %w", err) } logPath := filepath.Join(logDir, "main.log") if l.logWriter != nil { _ = l.logWriter.Close() } l.logWriter = &lumberjack.Logger{ Filename: logPath, MaxSize: 10, // MB MaxBackups: 0, MaxAge: 0, Compress: false, } l.logger.SetOutput(l.logWriter) } else { if l.logWriter != nil { _ = l.logWriter.Close() l.logWriter = nil } l.logger.SetOutput(os.Stdout) } return nil } // resolveLogDirectory determines the log directory from configuration func (l *StructuredLogger) resolveLogDirectory(cfg *config.Config) string { if cfg.AuthDir != "" { return filepath.Join(cfg.AuthDir, "logs") } return "logs" } // Close closes the logger and releases resources func (l *StructuredLogger) Close() error { l.mu.Lock() defer l.mu.Unlock() if l.logWriter != nil { return l.logWriter.Close() } return nil } // WithContext creates a logger entry with context values func (l *StructuredLogger) WithContext(ctx context.Context) *logrus.Entry { l.mu.RLock() defer l.mu.RUnlock() entry := l.logger.WithContext(ctx) // Add correlation ID if present if correlationID := GetCorrelationID(ctx); correlationID != "" { entry = entry.WithField("correlation_id", correlationID) } // Add service name if present if service := GetService(ctx); service != "" { entry = entry.WithField("service", service) } // Add operation name if present if operation := GetOperation(ctx); operation != "" { entry = entry.WithField("operation", operation) } return entry } // WithField adds a field to the logger func (l *StructuredLogger) WithField(key string, value interface{}) *logrus.Entry { l.mu.RLock() defer l.mu.RUnlock() return l.logger.WithField(key, value) } // WithFields adds multiple fields to the logger func (l *StructuredLogger) WithFields(fields map[string]interface{}) *logrus.Entry { l.mu.RLock() defer l.mu.RUnlock() return l.logger.WithFields(fields) } // WithError adds an error to the logger func (l *StructuredLogger) WithError(err error) *logrus.Entry { l.mu.RLock() defer l.mu.RUnlock() return l.logger.WithError(err) } // Debug logs a debug message func (l *StructuredLogger) Debug(ctx context.Context, message string) { l.WithContext(ctx).Debug(message) } // Info logs an info message func (l *StructuredLogger) Info(ctx context.Context, message string) { l.WithContext(ctx).Info(message) } // Warn logs a warning message func (l *StructuredLogger) Warn(ctx context.Context, message string) { l.WithContext(ctx).Warn(message) } // Error logs an error message func (l *StructuredLogger) Error(ctx context.Context, message string, err error) { entry := l.WithContext(ctx) if err != nil { entry = entry.WithError(err) } entry.Error(message) } // Fatal logs a fatal message func (l *StructuredLogger) Fatal(ctx context.Context, message string, err error) { entry := l.WithContext(ctx) if err != nil { entry = entry.WithError(err) } entry.Fatal(message) } // Debugf logs a formatted debug message func (l *StructuredLogger) Debugf(ctx context.Context, format string, args ...interface{}) { l.WithContext(ctx).Debugf(format, args...) } // Infof logs a formatted info message func (l *StructuredLogger) Infof(ctx context.Context, format string, args ...interface{}) { l.WithContext(ctx).Infof(format, args...) } // Warnf logs a formatted warning message func (l *StructuredLogger) Warnf(ctx context.Context, format string, args ...interface{}) { l.WithContext(ctx).Warnf(format, args...) } // Errorf logs a formatted error message func (l *StructuredLogger) Errorf(ctx context.Context, format string, args ...interface{}) { l.WithContext(ctx).Errorf(format, args...) } // WithCorrelationID adds a correlation ID to the context func WithCorrelationID(ctx context.Context, correlationID string) context.Context { return context.WithValue(ctx, CorrelationIDKey, correlationID) } // GetCorrelationID retrieves the correlation ID from the context func GetCorrelationID(ctx context.Context) string { if ctx == nil { return "" } // Try string key first (from Gin middleware) if id, ok := ctx.Value("request_id").(string); ok { return id } // Try typed key if id, ok := ctx.Value(CorrelationIDKey).(string); ok { return id } return "" } // WithService adds a service name to the context func WithService(ctx context.Context, service string) context.Context { return context.WithValue(ctx, ServiceKey, service) } // GetService retrieves the service name from the context func GetService(ctx context.Context) string { if ctx == nil { return "" } if service, ok := ctx.Value(ServiceKey).(string); ok { return service } return "" } // WithOperation adds an operation name to the context func WithOperation(ctx context.Context, operation string) context.Context { return context.WithValue(ctx, OperationKey, operation) } // GetOperation retrieves the operation name from the context func GetOperation(ctx context.Context) string { if ctx == nil { return "" } if operation, ok := ctx.Value(OperationKey).(string); ok { return operation } return "" } // GenerateCorrelationID generates a new correlation ID func GenerateCorrelationID() string { return logging.GenerateRequestID() } // Logger is the global structured logger instance var ( globalLogger *StructuredLogger loggerOnce sync.Once ) // GetLogger returns the global structured logger instance func GetLogger() *StructuredLogger { loggerOnce.Do(func() { globalLogger = NewStructuredLogger() }) return globalLogger } // SetLogger sets the global structured logger instance func SetLogger(logger *StructuredLogger) { globalLogger = logger }