package logger import ( "context" "git.dcentral.systems/toolz/goplt/pkg/logger" "go.opentelemetry.io/otel/trace" "go.uber.org/zap" "go.uber.org/zap/zapcore" ) // Note: contextKey constants are defined in middleware.go to avoid duplication // zapLogger implements the Logger interface using zap. type zapLogger struct { zap *zap.Logger } // NewZapLogger creates a new zap-based logger. // The format parameter determines the output format: // - "json": JSON format (production) // - "console": Human-readable format (development) func NewZapLogger(level string, format string) (logger.Logger, error) { var zapConfig zap.Config var zapLevel zapcore.Level // Parse log level if err := zapLevel.UnmarshalText([]byte(level)); err != nil { zapLevel = zapcore.InfoLevel } // Configure based on format if format == "json" { zapConfig = zap.NewProductionConfig() } else { zapConfig = zap.NewDevelopmentConfig() } zapConfig.Level = zap.NewAtomicLevelAt(zapLevel) zapConfig.EncoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder z, err := zapConfig.Build() if err != nil { return nil, err } return &zapLogger{zap: z}, nil } // Debug logs a message at debug level. func (zl *zapLogger) Debug(msg string, fields ...logger.Field) { zl.zap.Debug(msg, convertFields(fields)...) } // Info logs a message at info level. func (zl *zapLogger) Info(msg string, fields ...logger.Field) { zl.zap.Info(msg, convertFields(fields)...) } // Warn logs a message at warning level. func (zl *zapLogger) Warn(msg string, fields ...logger.Field) { zl.zap.Warn(msg, convertFields(fields)...) } // Error logs a message at error level. func (zl *zapLogger) Error(msg string, fields ...logger.Field) { zl.zap.Error(msg, convertFields(fields)...) } // With creates a child logger with the specified fields. func (zl *zapLogger) With(fields ...logger.Field) logger.Logger { return &zapLogger{ zap: zl.zap.With(convertFields(fields)...), } } // WithContext creates a child logger with fields extracted from context. func (zl *zapLogger) WithContext(ctx context.Context) logger.Logger { fields := make([]logger.Field, 0) // Extract request ID from context if requestID, ok := ctx.Value(requestIDKey).(string); ok && requestID != "" { fields = append(fields, zap.String("request_id", requestID)) } // Extract user ID from context if userID, ok := ctx.Value(userIDKey).(string); ok && userID != "" { fields = append(fields, zap.String("user_id", userID)) } // Extract trace ID from OpenTelemetry context span := trace.SpanFromContext(ctx) if span.SpanContext().IsValid() { traceID := span.SpanContext().TraceID().String() spanID := span.SpanContext().SpanID().String() if traceID != "" { fields = append(fields, zap.String("trace_id", traceID)) } if spanID != "" { fields = append(fields, zap.String("span_id", spanID)) } } if len(fields) == 0 { return zl } return &zapLogger{ zap: zl.zap.With(convertFields(fields)...), } } // convertFields converts logger.Field to zap.Field. // Since Field is an alias for zap.Field, we can cast directly. func convertFields(fields []logger.Field) []zap.Field { if len(fields) == 0 { return nil } zapFields := make([]zap.Field, 0, len(fields)) for _, f := range fields { // Type assert to zap.Field if zf, ok := f.(zap.Field); ok { zapFields = append(zapFields, zf) } else { // Fallback: convert to Any field zapFields = append(zapFields, zap.Any("field", f)) } } return zapFields } // RequestIDKey returns the context key for request ID. // This is exported so modules can use it to set request IDs in context. func RequestIDKey() ContextKey { return requestIDKey } // UserIDKey returns the context key for user ID. // This is exported so modules can use it to set user IDs in context. func UserIDKey() ContextKey { return userIDKey }