301 lines
7.2 KiB
Go
301 lines
7.2 KiB
Go
package logger
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/sirupsen/logrus"
|
|
)
|
|
|
|
// Logger defines the interface for logging
|
|
type Logger interface {
|
|
Debug(msg string, args ...any)
|
|
Info(msg string, args ...any)
|
|
Warn(msg string, args ...any)
|
|
Error(msg string, args ...any)
|
|
Time(msg string, args ...any)
|
|
|
|
// Progress logging for operations
|
|
StartOperation(name string) OperationLogger
|
|
}
|
|
|
|
// OperationLogger tracks timing for operations
|
|
type OperationLogger interface {
|
|
Update(msg string, args ...any)
|
|
Complete(msg string, args ...any)
|
|
Fail(msg string, args ...any)
|
|
}
|
|
|
|
// logger implements Logger interface using logrus
|
|
type logger struct {
|
|
logrus *logrus.Logger
|
|
level logrus.Level
|
|
format string
|
|
}
|
|
|
|
// operationLogger tracks a single operation
|
|
type operationLogger struct {
|
|
name string
|
|
startTime time.Time
|
|
parent *logger
|
|
}
|
|
|
|
// New creates a new logger
|
|
func New(level, format string) Logger {
|
|
var logLevel logrus.Level
|
|
switch strings.ToLower(level) {
|
|
case "debug":
|
|
logLevel = logrus.DebugLevel
|
|
case "info":
|
|
logLevel = logrus.InfoLevel
|
|
case "warn", "warning":
|
|
logLevel = logrus.WarnLevel
|
|
case "error":
|
|
logLevel = logrus.ErrorLevel
|
|
default:
|
|
logLevel = logrus.InfoLevel
|
|
}
|
|
|
|
l := logrus.New()
|
|
l.SetLevel(logLevel)
|
|
l.SetOutput(os.Stdout)
|
|
|
|
switch strings.ToLower(format) {
|
|
case "json":
|
|
l.SetFormatter(&logrus.JSONFormatter{})
|
|
default:
|
|
// Use custom clean formatter for human-readable output
|
|
l.SetFormatter(&CleanFormatter{})
|
|
}
|
|
|
|
return &logger{
|
|
logrus: l,
|
|
level: logLevel,
|
|
format: format,
|
|
}
|
|
}
|
|
|
|
func (l *logger) Debug(msg string, args ...any) {
|
|
l.logWithFields(logrus.DebugLevel, msg, args...)
|
|
}
|
|
|
|
func (l *logger) Info(msg string, args ...any) {
|
|
l.logWithFields(logrus.InfoLevel, msg, args...)
|
|
}
|
|
|
|
func (l *logger) Warn(msg string, args ...any) {
|
|
l.logWithFields(logrus.WarnLevel, msg, args...)
|
|
}
|
|
|
|
func (l *logger) Error(msg string, args ...any) {
|
|
l.logWithFields(logrus.ErrorLevel, msg, args...)
|
|
}
|
|
|
|
func (l *logger) Time(msg string, args ...any) {
|
|
// Time logs are always at info level with special formatting
|
|
l.logWithFields(logrus.InfoLevel, "[TIME] "+msg, args...)
|
|
}
|
|
|
|
func (l *logger) StartOperation(name string) OperationLogger {
|
|
return &operationLogger{
|
|
name: name,
|
|
startTime: time.Now(),
|
|
parent: l,
|
|
}
|
|
}
|
|
|
|
func (ol *operationLogger) Update(msg string, args ...any) {
|
|
elapsed := time.Since(ol.startTime)
|
|
ol.parent.Info(fmt.Sprintf("[%s] %s", ol.name, msg),
|
|
append(args, "elapsed", elapsed.String())...)
|
|
}
|
|
|
|
func (ol *operationLogger) Complete(msg string, args ...any) {
|
|
elapsed := time.Since(ol.startTime)
|
|
ol.parent.Info(fmt.Sprintf("[%s] COMPLETED: %s", ol.name, msg),
|
|
append(args, "duration", formatDuration(elapsed))...)
|
|
}
|
|
|
|
func (ol *operationLogger) Fail(msg string, args ...any) {
|
|
elapsed := time.Since(ol.startTime)
|
|
ol.parent.Error(fmt.Sprintf("[%s] FAILED: %s", ol.name, msg),
|
|
append(args, "duration", formatDuration(elapsed))...)
|
|
}
|
|
|
|
// logWithFields forwards log messages with structured fields to logrus
|
|
func (l *logger) logWithFields(level logrus.Level, msg string, args ...any) {
|
|
if l == nil || l.logrus == nil {
|
|
return
|
|
}
|
|
|
|
fields := fieldsFromArgs(args...)
|
|
entry := l.logrus.WithFields(fields)
|
|
|
|
switch level {
|
|
case logrus.DebugLevel:
|
|
entry.Debug(msg)
|
|
case logrus.WarnLevel:
|
|
entry.Warn(msg)
|
|
case logrus.ErrorLevel:
|
|
entry.Error(msg)
|
|
default:
|
|
entry.Info(msg)
|
|
}
|
|
}
|
|
|
|
// fieldsFromArgs converts variadic key/value pairs into logrus fields
|
|
func fieldsFromArgs(args ...any) logrus.Fields {
|
|
fields := logrus.Fields{}
|
|
|
|
for i := 0; i < len(args); {
|
|
if i+1 < len(args) {
|
|
if key, ok := args[i].(string); ok {
|
|
fields[key] = args[i+1]
|
|
i += 2
|
|
continue
|
|
}
|
|
}
|
|
|
|
fields[fmt.Sprintf("arg%d", i)] = args[i]
|
|
i++
|
|
}
|
|
|
|
return fields
|
|
}
|
|
|
|
// formatDuration formats duration in human-readable format
|
|
func formatDuration(d time.Duration) string {
|
|
if d < time.Minute {
|
|
return fmt.Sprintf("%.1fs", d.Seconds())
|
|
} else if d < time.Hour {
|
|
minutes := int(d.Minutes())
|
|
seconds := int(d.Seconds()) % 60
|
|
return fmt.Sprintf("%dm %ds", minutes, seconds)
|
|
} else {
|
|
hours := int(d.Hours())
|
|
minutes := int(d.Minutes()) % 60
|
|
seconds := int(d.Seconds()) % 60
|
|
return fmt.Sprintf("%dh %dm %ds", hours, minutes, seconds)
|
|
}
|
|
}
|
|
|
|
// CleanFormatter formats log entries in a clean, human-readable format
|
|
type CleanFormatter struct{}
|
|
|
|
// Format implements logrus.Formatter interface
|
|
func (f *CleanFormatter) Format(entry *logrus.Entry) ([]byte, error) {
|
|
timestamp := entry.Time.Format("2006-01-02T15:04:05")
|
|
|
|
// Color codes for different log levels
|
|
var levelColor, levelText string
|
|
switch entry.Level {
|
|
case logrus.DebugLevel:
|
|
levelColor = "\033[36m" // Cyan
|
|
levelText = "DEBUG"
|
|
case logrus.InfoLevel:
|
|
levelColor = "\033[32m" // Green
|
|
levelText = "INFO "
|
|
case logrus.WarnLevel:
|
|
levelColor = "\033[33m" // Yellow
|
|
levelText = "WARN "
|
|
case logrus.ErrorLevel:
|
|
levelColor = "\033[31m" // Red
|
|
levelText = "ERROR"
|
|
default:
|
|
levelColor = "\033[0m" // Reset
|
|
levelText = "INFO "
|
|
}
|
|
resetColor := "\033[0m"
|
|
|
|
// Build the message with perfectly aligned columns
|
|
var output strings.Builder
|
|
|
|
// Column 1: Level (with color, fixed width 5 chars)
|
|
output.WriteString(levelColor)
|
|
output.WriteString(levelText)
|
|
output.WriteString(resetColor)
|
|
output.WriteString(" ")
|
|
|
|
// Column 2: Timestamp (fixed format)
|
|
output.WriteString("[")
|
|
output.WriteString(timestamp)
|
|
output.WriteString("] ")
|
|
|
|
// Column 3: Message
|
|
output.WriteString(entry.Message)
|
|
|
|
// Append important fields in a clean format (skip internal/redundant fields)
|
|
if len(entry.Data) > 0 {
|
|
// Only show truly important fields, skip verbose ones
|
|
for k, v := range entry.Data {
|
|
// Skip noisy internal fields and redundant message field
|
|
if k == "elapsed" || k == "operation_id" || k == "step" || k == "timestamp" || k == "message" {
|
|
continue
|
|
}
|
|
|
|
// Format duration nicely at the end
|
|
if k == "duration" {
|
|
if str, ok := v.(string); ok {
|
|
output.WriteString(fmt.Sprintf(" (%s)", str))
|
|
}
|
|
continue
|
|
}
|
|
|
|
// Only show critical fields (driver, errors, etc)
|
|
if k == "driver" || k == "max_conns" || k == "error" || k == "database" {
|
|
output.WriteString(fmt.Sprintf(" %s=%v", k, v))
|
|
}
|
|
}
|
|
}
|
|
|
|
output.WriteString("\n")
|
|
return []byte(output.String()), nil
|
|
}
|
|
|
|
// FileLogger creates a logger that writes to both stdout and a file
|
|
func FileLogger(level, format, filename string) (Logger, error) {
|
|
var logLevel logrus.Level
|
|
switch strings.ToLower(level) {
|
|
case "debug":
|
|
logLevel = logrus.DebugLevel
|
|
case "info":
|
|
logLevel = logrus.InfoLevel
|
|
case "warn", "warning":
|
|
logLevel = logrus.WarnLevel
|
|
case "error":
|
|
logLevel = logrus.ErrorLevel
|
|
default:
|
|
logLevel = logrus.InfoLevel
|
|
}
|
|
|
|
// Open log file
|
|
file, err := os.OpenFile(filename, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to open log file: %w", err)
|
|
}
|
|
|
|
// Create multi-writer (stdout + file)
|
|
multiWriter := io.MultiWriter(os.Stdout, file)
|
|
|
|
l := logrus.New()
|
|
l.SetLevel(logLevel)
|
|
l.SetOutput(multiWriter)
|
|
|
|
switch strings.ToLower(format) {
|
|
case "json":
|
|
l.SetFormatter(&logrus.JSONFormatter{})
|
|
default:
|
|
// Use custom clean formatter for human-readable output
|
|
l.SetFormatter(&CleanFormatter{})
|
|
}
|
|
|
|
return &logger{
|
|
logrus: l,
|
|
level: logLevel,
|
|
format: format,
|
|
}, nil
|
|
}
|