292 lines
7.0 KiB
Go
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,
|
|
})
|
|
}
|