- 添加 lumberjack.v2 依赖实现日志轮转 - 支持配置文件输出(stdout/stderr/file) - 支持文件轮转(100MB/3备份/7天/压缩) - 添加 Config 结构体灵活配置 - 添加完整测试用例 测试验证: - TestInitWithConfig PASS - TestInitWithConfigFileOutput PASS - TestDefaultConfig PASS - 全量日志测试通过
196 lines
4.2 KiB
Go
196 lines
4.2 KiB
Go
// Package log provides structured logging using slog.
|
|
// It supports JSON output, configurable log levels, and sensitive field sanitization.
|
|
package log
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
|
|
"log/slog"
|
|
"gopkg.in/natefinch/lumberjack.v2"
|
|
)
|
|
|
|
var logger *slog.Logger
|
|
|
|
// Config holds logging configuration
|
|
type Config struct {
|
|
Level string
|
|
Output string // "stdout", "stderr", or file path
|
|
Rotation bool // enable file rotation
|
|
MaxSize int // MB
|
|
MaxBackups int
|
|
MaxAge int // days
|
|
Compress bool
|
|
}
|
|
|
|
// DefaultConfig returns default logging configuration
|
|
func DefaultConfig() Config {
|
|
return Config{
|
|
Level: "INFO",
|
|
Output: "stdout",
|
|
Rotation: false,
|
|
MaxSize: 100,
|
|
MaxBackups: 3,
|
|
MaxAge: 7,
|
|
Compress: true,
|
|
}
|
|
}
|
|
|
|
// sensitiveFields contains field names that should be sanitized in logs
|
|
var sensitiveFields = []string{
|
|
"token",
|
|
"password",
|
|
"secret",
|
|
"key",
|
|
"credential",
|
|
"auth",
|
|
"api_key",
|
|
"api_secret",
|
|
"private_key",
|
|
"access_token",
|
|
"refresh_token",
|
|
}
|
|
|
|
// Init initializes the logger with JSON handler and INFO level
|
|
func Init() {
|
|
InitWithLevel("INFO")
|
|
}
|
|
|
|
// InitWithLevel initializes the logger with specified level
|
|
func InitWithLevel(level string) {
|
|
cfg := DefaultConfig()
|
|
cfg.Level = level
|
|
InitWithConfig(cfg)
|
|
}
|
|
|
|
// InitWithConfig initializes the logger with full configuration
|
|
func InitWithConfig(cfg Config) {
|
|
levelVar := parseLevel(cfg.Level)
|
|
|
|
opts := &slog.HandlerOptions{
|
|
Level: levelVar,
|
|
ReplaceAttr: sanitizeAttrs,
|
|
}
|
|
|
|
var handler slog.Handler
|
|
|
|
switch cfg.Output {
|
|
case "stdout":
|
|
handler = slog.NewJSONHandler(os.Stdout, opts)
|
|
case "stderr":
|
|
handler = slog.NewJSONHandler(os.Stderr, opts)
|
|
default:
|
|
// File output with optional rotation
|
|
if cfg.Rotation {
|
|
lumberjackLogger := &lumberjack.Logger{
|
|
Filename: cfg.Output,
|
|
MaxSize: cfg.MaxSize,
|
|
MaxBackups: cfg.MaxBackups,
|
|
MaxAge: cfg.MaxAge,
|
|
Compress: cfg.Compress,
|
|
}
|
|
handler = slog.NewJSONHandler(lumberjackLogger, opts)
|
|
} else {
|
|
file, err := os.OpenFile(cfg.Output, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
handler = slog.NewJSONHandler(file, opts)
|
|
}
|
|
}
|
|
|
|
logger = slog.New(handler)
|
|
slog.SetDefault(logger)
|
|
}
|
|
|
|
// parseLevel parses string level to slog.Level
|
|
func parseLevel(level string) slog.Level {
|
|
switch strings.ToUpper(level) {
|
|
case "DEBUG":
|
|
return slog.LevelDebug
|
|
case "INFO":
|
|
return slog.LevelInfo
|
|
case "WARN", "WARNING":
|
|
return slog.LevelWarn
|
|
case "ERROR":
|
|
return slog.LevelError
|
|
default:
|
|
return slog.LevelInfo
|
|
}
|
|
}
|
|
|
|
// sanitizeAttrs sanitizes sensitive fields in log attributes
|
|
func sanitizeAttrs(groups []string, a slog.Attr) slog.Attr {
|
|
// Check if attribute key contains sensitive field name
|
|
keyLower := strings.ToLower(a.Key)
|
|
for _, field := range sensitiveFields {
|
|
if strings.Contains(keyLower, field) {
|
|
return slog.String(a.Key, "[REDACTED]")
|
|
}
|
|
}
|
|
return a
|
|
}
|
|
|
|
// IsSensitive checks if a field name is sensitive
|
|
func IsSensitive(field string) bool {
|
|
fieldLower := strings.ToLower(field)
|
|
for _, f := range sensitiveFields {
|
|
if strings.Contains(fieldLower, f) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// Info logs an info level message
|
|
func Info(msg string, args ...any) {
|
|
slog.Info(msg, args...)
|
|
}
|
|
|
|
// Error logs an error level message
|
|
func Error(msg string, args ...any) {
|
|
slog.Error(msg, args...)
|
|
}
|
|
|
|
// Debug logs a debug level message
|
|
func Debug(msg string, args ...any) {
|
|
slog.Debug(msg, args...)
|
|
}
|
|
|
|
// Warn logs a warning level message
|
|
func Warn(msg string, args ...any) {
|
|
slog.Warn(msg, args...)
|
|
}
|
|
|
|
// Logger returns the underlying logger
|
|
func Logger() *slog.Logger {
|
|
return logger
|
|
}
|
|
|
|
// WithContext returns a logger with context
|
|
func WithContext(ctx context.Context) *slog.Logger {
|
|
// Extract trace_id from context if present
|
|
if traceID, ok := ctx.Value("trace_id").(string); ok {
|
|
return logger.With("trace_id", traceID)
|
|
}
|
|
return logger
|
|
}
|
|
|
|
// RequestLogger returns a logger with request fields
|
|
func RequestLogger(method, path, clientIP string) *slog.Logger {
|
|
return logger.With(
|
|
"method", method,
|
|
"path", path,
|
|
"client_ip", clientIP,
|
|
"time", time.Now().UTC(),
|
|
)
|
|
}
|
|
|
|
// Fatal logs an error message and exits with code 1
|
|
func Fatal(msg string, args ...any) {
|
|
slog.Error(msg, args...)
|
|
os.Exit(1)
|
|
}
|