backend-service/internal/logger/logger.go

347 lines
9.1 KiB
Go
Raw Normal View History

package logger
import (
"context"
"encoding/json"
"fmt"
"log"
"os"
"runtime"
"time"
)
// LogLevel represents the severity level of a log entry
type LogLevel string
const (
DEBUG LogLevel = "DEBUG"
INFO LogLevel = "INFO"
WARN LogLevel = "WARN"
ERROR LogLevel = "ERROR"
FATAL LogLevel = "FATAL"
)
// LogEntry represents a structured log entry
type LogEntry struct {
Timestamp string `json:"timestamp"`
Level LogLevel `json:"level"`
Message string `json:"message"`
Service string `json:"service"`
TraceID string `json:"trace_id,omitempty"`
UserID string `json:"user_id,omitempty"`
Fields map[string]interface{} `json:"fields,omitempty"`
Error *ErrorDetails `json:"error,omitempty"`
Source *SourceLocation `json:"source,omitempty"`
}
// ErrorDetails contains error-specific information
type ErrorDetails struct {
Type string `json:"type"`
Message string `json:"message"`
StackTrace string `json:"stack_trace,omitempty"`
}
// SourceLocation contains source code location information
type SourceLocation struct {
File string `json:"file"`
Line int `json:"line"`
Function string `json:"function"`
}
// Logger provides structured logging capabilities
type Logger struct {
service string
level LogLevel
}
// New creates a new logger instance
func New(service string) *Logger {
return &Logger{
service: service,
level: INFO, // Default level
}
}
// SetLevel sets the minimum log level
func (l *Logger) SetLevel(level LogLevel) {
l.level = level
}
// GetLevel returns the current log level
func (l *Logger) GetLevel() LogLevel {
return l.level
}
// IsLevelEnabled checks if a log level is enabled
func (l *Logger) IsLevelEnabled(level LogLevel) bool {
return l.shouldLog(level)
}
// Debug logs a debug message
func (l *Logger) Debug(message string, fields ...map[string]interface{}) {
if l.shouldLog(DEBUG) {
l.log(DEBUG, message, nil, fields...)
}
}
// Info logs an info message
func (l *Logger) Info(message string, fields ...map[string]interface{}) {
if l.shouldLog(INFO) {
l.log(INFO, message, nil, fields...)
}
}
// Warn logs a warning message
func (l *Logger) Warn(message string, fields ...map[string]interface{}) {
if l.shouldLog(WARN) {
l.log(WARN, message, nil, fields...)
}
}
// Error logs an error message
func (l *Logger) Error(message string, err error, fields ...map[string]interface{}) {
if l.shouldLog(ERROR) {
l.log(ERROR, message, err, fields...)
}
}
// Fatal logs a fatal message and exits
func (l *Logger) Fatal(message string, err error, fields ...map[string]interface{}) {
l.log(FATAL, message, err, fields...)
os.Exit(1)
}
// WithContext creates a logger with context information
func (l *Logger) WithContext(ctx context.Context) *ContextLogger {
return &ContextLogger{
logger: l,
ctx: ctx,
}
}
// WithFields creates a logger with predefined fields
func (l *Logger) WithFields(fields map[string]interface{}) *FieldLogger {
return &FieldLogger{
logger: l,
fields: fields,
}
}
// log performs the actual logging
func (l *Logger) log(level LogLevel, message string, err error, fields ...map[string]interface{}) {
entry := LogEntry{
Timestamp: time.Now().UTC().Format(time.RFC3339),
Level: level,
Message: message,
Service: l.service,
}
// Add fields if provided
if len(fields) > 0 && fields[0] != nil {
entry.Fields = fields[0]
}
// Add error details if provided
if err != nil {
entry.Error = &ErrorDetails{
Type: fmt.Sprintf("%T", err),
Message: err.Error(),
}
// Add stack trace for errors and fatal logs
if level == ERROR || level == FATAL {
entry.Error.StackTrace = getStackTrace()
}
}
// Add source location for errors and fatal logs
if level == ERROR || level == FATAL {
entry.Source = getSourceLocation(3) // Skip 3 frames: log, Error/Fatal, caller
}
// Output the log entry
l.output(entry)
}
// shouldLog checks if the message should be logged based on level
func (l *Logger) shouldLog(level LogLevel) bool {
levels := map[LogLevel]int{
DEBUG: 0,
INFO: 1,
WARN: 2,
ERROR: 3,
FATAL: 4,
}
return levels[level] >= levels[l.level]
}
// output writes the log entry to the output
func (l *Logger) output(entry LogEntry) {
jsonBytes, err := json.Marshal(entry)
if err != nil {
// Fallback to standard logging if JSON marshaling fails
log.Printf("LOGGER_ERROR: Failed to marshal log entry: %v", err)
log.Printf("%s [%s] %s: %s", entry.Timestamp, entry.Level, entry.Service, entry.Message)
return
}
fmt.Println(string(jsonBytes))
}
// getStackTrace returns the current stack trace
func getStackTrace() string {
buf := make([]byte, 4096)
n := runtime.Stack(buf, false)
return string(buf[:n])
}
// getSourceLocation returns the source location information
func getSourceLocation(skip int) *SourceLocation {
pc, file, line, ok := runtime.Caller(skip)
if !ok {
return nil
}
fn := runtime.FuncForPC(pc)
if fn == nil {
return nil
}
return &SourceLocation{
File: file,
Line: line,
Function: fn.Name(),
}
}
// ContextLogger wraps a logger with context information
type ContextLogger struct {
logger *Logger
ctx context.Context
}
// Debug logs a debug message with context
func (cl *ContextLogger) Debug(message string, fields ...map[string]interface{}) {
cl.logWithContext(DEBUG, message, nil, fields...)
}
// Info logs an info message with context
func (cl *ContextLogger) Info(message string, fields ...map[string]interface{}) {
cl.logWithContext(INFO, message, nil, fields...)
}
// Warn logs a warning message with context
func (cl *ContextLogger) Warn(message string, fields ...map[string]interface{}) {
cl.logWithContext(WARN, message, nil, fields...)
}
// Error logs an error message with context
func (cl *ContextLogger) Error(message string, err error, fields ...map[string]interface{}) {
cl.logWithContext(ERROR, message, err, fields...)
}
// logWithContext logs with context information
func (cl *ContextLogger) logWithContext(level LogLevel, message string, err error, fields ...map[string]interface{}) {
// Extract context information
contextFields := make(map[string]interface{})
// Add trace ID if available
if traceID := cl.ctx.Value("trace_id"); traceID != nil {
contextFields["trace_id"] = traceID
}
// Add user ID if available
if userID := cl.ctx.Value("user_id"); userID != nil {
contextFields["user_id"] = userID
}
// Merge with provided fields
if len(fields) > 0 && fields[0] != nil {
for k, v := range fields[0] {
contextFields[k] = v
}
}
cl.logger.log(level, message, err, contextFields)
}
// FieldLogger wraps a logger with predefined fields
type FieldLogger struct {
logger *Logger
fields map[string]interface{}
}
// Debug logs a debug message with predefined fields
func (fl *FieldLogger) Debug(message string, additionalFields ...map[string]interface{}) {
fl.logWithFields(DEBUG, message, nil, additionalFields...)
}
// Info logs an info message with predefined fields
func (fl *FieldLogger) Info(message string, additionalFields ...map[string]interface{}) {
fl.logWithFields(INFO, message, nil, additionalFields...)
}
// Warn logs a warning message with predefined fields
func (fl *FieldLogger) Warn(message string, additionalFields ...map[string]interface{}) {
fl.logWithFields(WARN, message, nil, additionalFields...)
}
// Error logs an error message with predefined fields
func (fl *FieldLogger) Error(message string, err error, additionalFields ...map[string]interface{}) {
fl.logWithFields(ERROR, message, err, additionalFields...)
}
// logWithFields logs with predefined fields
func (fl *FieldLogger) logWithFields(level LogLevel, message string, err error, additionalFields ...map[string]interface{}) {
// Merge predefined fields with additional fields
mergedFields := make(map[string]interface{})
// Add predefined fields
for k, v := range fl.fields {
mergedFields[k] = v
}
// Add additional fields
if len(additionalFields) > 0 && additionalFields[0] != nil {
for k, v := range additionalFields[0] {
mergedFields[k] = v
}
}
fl.logger.log(level, message, err, mergedFields)
}
// Global logger instance
var globalLogger = New("app")
// SetGlobalLevel sets the global logger level
func SetGlobalLevel(level LogLevel) {
globalLogger.SetLevel(level)
}
// Debug logs a debug message using the global logger
func Debug(message string, fields ...map[string]interface{}) {
globalLogger.Debug(message, fields...)
}
// Info logs an info message using the global logger
func Info(message string, fields ...map[string]interface{}) {
globalLogger.Info(message, fields...)
}
// Warn logs a warning message using the global logger
func Warn(message string, fields ...map[string]interface{}) {
globalLogger.Warn(message, fields...)
}
// Error logs an error message using the global logger
func Error(message string, err error, fields ...map[string]interface{}) {
globalLogger.Error(message, err, fields...)
}
// Fatal logs a fatal message using the global logger and exits
func Fatal(message string, err error, fields ...map[string]interface{}) {
globalLogger.Fatal(message, err, fields...)
}