API / internal /infrastructure /logging /structured.go
sshinmen's picture
Update HF Spaces deployment with latest changes
2e6b65c
Raw
History Blame Contribute Delete
7.93 kB
// 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
}