2025-06-05 21:21:28 +07:00

292 lines
7.0 KiB
Go

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,
})
}