package logger import ( "fmt" "io" "os" "runtime" "strings" "sync" "time" "github.com/sirupsen/logrus" ) var ( // log is the global logger instance log *logrus.Logger // logMutex ensures thread-safe logger configuration logMutex sync.RWMutex // defaultFields are included in every log entry defaultFields = logrus.Fields{ "app_name": "starter-kit", "env": os.Getenv("APP_ENV"), } ) // Buffer size for async logging const defaultBufferSize = 256 // Fields is a type alias for logrus.Fields type Fields = logrus.Fields // LogConfig holds configuration for the logger type LogConfig struct { Level string `json:"level"` Format string `json:"format"` EnableCaller bool `json:"enable_caller"` BufferSize int `json:"buffer_size"` DisableColor bool `json:"disable_color"` ReportCaller bool `json:"report_caller"` } // Init initializes the logger with the given configuration func Init(config *LogConfig) { logMutex.Lock() defer logMutex.Unlock() // Create new logger instance log = logrus.New() // Set default values if config is nil if config == nil { config = &LogConfig{ Level: "info", Format: "json", EnableCaller: true, BufferSize: defaultBufferSize, DisableColor: false, ReportCaller: true, } } // Set log level setLogLevel(config.Level) // Set log formatter setLogFormatter(config) // Configure output configureOutput() // Add hooks addHooks(config) } // configureOutput sets up the output writers func configureOutput() { // Use async writer if buffer size > 0 log.SetOutput(io.Discard) // Discard logs by default // Create multi-writer for stdout/stderr log.AddHook(&writerHook{ Writer: os.Stdout, LogLevels: []logrus.Level{logrus.InfoLevel, logrus.DebugLevel}, }) log.AddHook(&writerHook{ Writer: os.Stderr, LogLevels: []logrus.Level{logrus.PanicLevel, logrus.FatalLevel, logrus.ErrorLevel, logrus.WarnLevel}, }) } // setLogLevel configures the log level func setLogLevel(level string) { lvl, err := logrus.ParseLevel(strings.ToLower(level)) if err != nil { logrus.Warnf("Invalid log level '%s', defaulting to 'info'", level) lvl = logrus.InfoLevel } log.SetLevel(lvl) } // setLogFormatter configures the log formatter func setLogFormatter(config *LogConfig) { switch strings.ToLower(config.Format) { case "text": log.SetFormatter(&logrus.TextFormatter{ FullTimestamp: true, TimestampFormat: time.RFC3339Nano, DisableColors: config.DisableColor, CallerPrettyfier: callerPrettyfier, }) default: // JSON format log.SetFormatter(&logrus.JSONFormatter{ TimestampFormat: time.RFC3339Nano, DisableTimestamp: false, DisableHTMLEscape: true, CallerPrettyfier: callerPrettyfier, FieldMap: logrus.FieldMap{ logrus.FieldKeyTime: "@timestamp", logrus.FieldKeyLevel: "level", logrus.FieldKeyMsg: "message", logrus.FieldKeyFunc: "caller", }, }) } } // addHooks adds additional hooks to the logger func addHooks(config *LogConfig) { // Add caller info hook if enabled if config.EnableCaller || config.ReportCaller { log.AddHook(&callerHook{}) } // Add default fields hook log.AddHook(&defaultFieldsHook{fields: defaultFields}) } // writerHook sends logs to different writers based on level type writerHook struct { Writer io.Writer LogLevels []logrus.Level } func (hook *writerHook) Fire(entry *logrus.Entry) error { line, err := entry.Logger.Formatter.Format(entry) if err != nil { return err } _, err = hook.Writer.Write(line) return err } func (hook *writerHook) Levels() []logrus.Level { return hook.LogLevels } // callerHook adds caller information to the log entry type callerHook struct{} func (hook *callerHook) Fire(entry *logrus.Entry) error { entry.Data["caller"] = getCaller() return nil } func (hook *callerHook) Levels() []logrus.Level { return logrus.AllLevels } // defaultFieldsHook adds default fields to each log entry type defaultFieldsHook struct { fields logrus.Fields } func (hook *defaultFieldsHook) Fire(entry *logrus.Entry) error { for k, v := range hook.fields { if _, exists := entry.Data[k]; !exists { entry.Data[k] = v } } return nil } func (hook *defaultFieldsHook) Levels() []logrus.Level { return logrus.AllLevels } // getCaller returns the caller's file and line number func getCaller() string { _, file, line, ok := runtime.Caller(5) // Adjust the skip to get the right caller if !ok { return "" } return fmt.Sprintf("%s:%d", file, line) } // callerPrettyfier formats the caller info func callerPrettyfier(f *runtime.Frame) (string, string) { // Get the relative path of the file file := "" if f != nil { file = f.File // Get the last 2 segments of the path parts := strings.Split(file, "/") if len(parts) > 2 { file = strings.Join(parts[len(parts)-2:], "/") } file = fmt.Sprintf("%s:%d", file, f.Line) } // Return empty function name to hide it return "", file } // GetLogger returns the global logger instance func GetLogger() *logrus.Logger { logMutex.RLock() defer logMutex.RUnlock() return log } // SetLevel sets the logging level func SetLevel(level string) { logMutex.Lock() defer logMutex.Unlock() setLogLevel(level) } // WithFields creates an entry with the given fields and includes the caller func WithFields(fields Fields) *logrus.Entry { return log.WithFields(logrus.Fields(fields)).WithField("caller", getCaller()) } // WithError adds an error as a single field and includes the caller func WithError(err error) *logrus.Entry { return log.WithError(err).WithField("caller", getCaller()) } // Debug logs a message at level Debug func Debug(args ...interface{}) { log.WithField("caller", getCaller()).Debug(args...) } // Info logs a message at level Info func Info(args ...interface{}) { log.WithField("caller", getCaller()).Info(args...) } // Infof logs a formatted message at level Info func Infof(format string, args ...interface{}) { log.WithField("caller", getCaller()).Infof(format, args...) } // Warn logs a message at level Warn func Warn(args ...interface{}) { log.WithField("caller", getCaller()).Warn(args...) } // Error logs a message at level Error func Error(args ...interface{}) { log.WithField("caller", getCaller()).Error(args...) } // Errorf logs a formatted message at level Error func Errorf(format string, args ...interface{}) { log.WithField("caller", getCaller()).Errorf(format, args...) } // Fatal logs a message at level Fatal then the process will exit with status set to 1 func Fatal(args ...interface{}) { log.WithField("caller", getCaller()).Fatal(args...) } // Panic logs a message at level Panic and then panics func Panic(args ...interface{}) { log.WithField("caller", getCaller()).Panic(args...) } // Trace logs a message at level Trace func Trace(args ...interface{}) { log.WithField("caller", getCaller()).Trace(args...) } // Initialize logger with default configuration func init() { Init(&LogConfig{ Level: "info", Format: "json", EnableCaller: true, BufferSize: defaultBufferSize, ReportCaller: true, }) }