Files
sub2api-cn-relay-manager/internal/log/log.go
phamnazage-jpg cf7dd35e1d feat(log): B-03 日志轮转配置 - 添加 lumberjack 支持
- 添加 lumberjack.v2 依赖实现日志轮转
- 支持配置文件输出(stdout/stderr/file)
- 支持文件轮转(100MB/3备份/7天/压缩)
- 添加 Config 结构体灵活配置
- 添加完整测试用例

测试验证:
- TestInitWithConfig PASS
- TestInitWithConfigFileOutput PASS
- TestDefaultConfig PASS
- 全量日志测试通过
2026-06-01 22:06:56 +08:00

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