package log import ( "context" "fmt" "io" "log/slog" "os" "path" "path/filepath" "runtime" "strings" "sync" "time" "github.com/zeromicro/go-zero/core/logx" "go.opentelemetry.io/otel/trace" "gopkg.in/natefinch/lumberjack.v2" ) var workdir, _ = filepath.Abs(filepath.Dir(os.Args[0])) type Logger struct { *slog.Logger } func SetDefault(logger *Logger) { slog.SetDefault(logger.Logger) } func NewLogger(conf LogConf) *Logger { var log *slog.Logger logLevel := slog.LevelInfo switch strings.ToLower(conf.Level) { case "error": logLevel = slog.LevelError case "warn": logLevel = slog.LevelWarn case "info": logLevel = slog.LevelInfo case "debug": logLevel = slog.LevelDebug } if !path.IsAbs(conf.Filename) { conf.Filename = filepath.Join(workdir, conf.Filename) } // 按天切割日志:每天一个文件,命名格式 bj_power_mes-YYYY-MM-DD.log // 每天内仍?lumberjack 按大小(MaxSize)滚动,保留 MaxBackups 个备? dw := newDailyWriter(conf) var writer io.Writer = dw if conf.Console { writer = io.MultiWriter(writer, os.Stdout) } log = slog.New(NewContextHandler(writer, conf.Format, &slog.HandlerOptions{ Level: logLevel, AddSource: conf.AddSource, ReplaceAttr: func(_ []string, attr slog.Attr) slog.Attr { switch attr.Key { case "caller": return slog.Attr{} case slog.SourceKey: if source, ok := attr.Value.Any().(*slog.Source); ok { if source.File == "" { return slog.Attr{} } return slog.String(slog.SourceKey, " "+filepath.Base(source.File)+fmt.Sprintf(":%d", source.Line)) } case slog.TimeKey: return slog.String(slog.TimeKey, attr.Value.Time().Format(conf.TimeFormat)) } return attr }, })) return &Logger{ Logger: log, } } func (l *Logger) Alert(v any) { l.Logger.Info("%v", v) } func (l *Logger) Close() error { return nil } func (l *Logger) buildAttrs(fields []logx.LogField) []slog.Attr { attrs := make([]slog.Attr, 0, len(fields)) for _, field := range fields { attrs = append(attrs, slog.Attr{ Key: field.Key, Value: slog.AnyValue(field.Value), }) } return attrs } func (l *Logger) DebugCtx(ctx context.Context, v ...any) { if !l.Enabled(ctx, slog.LevelDebug) { return } var pcs [1]uintptr // skip [runtime.Callers, this function, this function's caller] runtime.Callers(2, pcs[:]) pc := pcs[0] r := slog.NewRecord(time.Now(), slog.LevelDebug, fmt.Sprint(v...), pc) var attrs []slog.Attr spanCtx := trace.SpanContextFromContext(ctx) if spanCtx.HasTraceID() { attrs = append(attrs, slog.String("trace", spanCtx.TraceID().String())) } if spanCtx.HasSpanID() { attrs = append(attrs, slog.String("span", spanCtx.SpanID().String())) } r.AddAttrs(attrs...) if ctx == nil { ctx = context.Background() } _ = l.Handler().Handle(ctx, r) } func (l *Logger) log(ctx context.Context, level slog.Level, msg string, args ...any) { if !l.Enabled(ctx, level) { return } var pc uintptr var pcs [1]uintptr // skip [runtime.Callers, this function, this function's caller] runtime.Callers(5, pcs[:]) pc = pcs[0] r := slog.NewRecord(time.Now(), level, msg, pc) r.Add(args...) if ctx == nil { ctx = context.Background() } _ = l.Handler().Handle(ctx, r) } func (l *Logger) logAttrs(ctx context.Context, level slog.Level, msg string, attrs ...slog.Attr) { if !l.Enabled(ctx, level) { return } var pcs [1]uintptr // skip [runtime.Callers, this function, this function's caller] runtime.Callers(5, pcs[:]) pc := pcs[0] r := slog.NewRecord(time.Now(), level, msg, pc) r.AddAttrs(attrs...) if ctx == nil { ctx = context.Background() } _ = l.Handler().Handle(ctx, r) } func (l *Logger) Debug(v any, fields ...logx.LogField) { attrs := l.buildAttrs(fields) l.logAttrs(context.Background(), slog.LevelDebug, fmt.Sprintf("%v", v), attrs...) } func (l *Logger) Error(v any, fields ...logx.LogField) { attrs := l.buildAttrs(fields) l.logAttrs(context.Background(), slog.LevelError, fmt.Sprintf("%v", v), attrs...) } // skipHttpPollLog 过滤高频轮询日志:前端轮询接口(成功 GET)每秒数次刷屏, // 无排障价值直接丢弃;写操作(POST/PUT/DELETE)与?2xx 错误响应保留? func skipHttpPollLog(v any) bool { msg, ok := v.(string) if !ok || !strings.HasPrefix(msg, "[HTTP]") { return false } // 格式: [HTTP] 200 - GET /api/v1/jobs - 127.0.0.1:49864 - UA parts := strings.SplitN(msg, " - ", 3) if len(parts) < 3 { return false } code := strings.TrimSpace(strings.TrimPrefix(parts[0], "[HTTP]")) if len(code) < 1 || code[0] != '2' { return false } return strings.HasPrefix(parts[1], "GET ") } func (l *Logger) Info(v any, fields ...logx.LogField) { if skipHttpPollLog(v) { return } attrs := l.buildAttrs(fields) l.logAttrs(context.Background(), slog.LevelInfo, fmt.Sprintf("%v", v), attrs...) } func (l *Logger) Severe(v any) { l.log(context.Background(), slog.LevelError, fmt.Sprintf("%v", v)) } func (l *Logger) Slow(v any, fields ...logx.LogField) { if skipHttpPollLog(v) { return } attrs := l.buildAttrs(fields) l.logAttrs(context.Background(), slog.LevelWarn, fmt.Sprintf("%v", v), attrs...) } func (l *Logger) Stack(v any) { l.log(context.Background(), slog.LevelError, fmt.Sprintf("%v", v)) } func (l *Logger) Stat(v any, fields ...logx.LogField) { attrs := l.buildAttrs(fields) l.logAttrs(context.Background(), slog.LevelInfo, fmt.Sprintf("%v", v), attrs...) } // dailyWriter 按天切割日志写入器? // 每次 Write 时检查当前日期,跨天时自动切换文件? // 每天内由 lumberjack 按大小滚动,保留 MaxBackups 个备份? type dailyWriter struct { mu sync.Mutex conf LogConf baseDir string baseName string // 不含扩展名的基础文件名,?"bj_power_mes" ext string // 扩展名,?".log" currentDay string lj *lumberjack.Logger } func newDailyWriter(conf LogConf) *dailyWriter { dir := filepath.Dir(conf.Filename) ext := filepath.Ext(conf.Filename) base := strings.TrimSuffix(filepath.Base(conf.Filename), ext) // 确保日志目录存在 if err := os.MkdirAll(dir, 0755); err != nil { // 创建目录失败时回退到可执行文件目录,避免程序启动失? slog.Error("log: 创建日志目录失败,回退到工作目?, "dir", dir, "error", err) dir = workdir } return &dailyWriter{ conf: conf, baseDir: dir, baseName: base, ext: ext, } } func (w *dailyWriter) Write(p []byte) (n int, err error) { w.mu.Lock() defer w.mu.Unlock() day := time.Now().Format("2006-01-02") if day != w.currentDay { // 跨天:关闭旧 writer,创建新文件 if w.lj != nil { w.lj.Close() } filename := filepath.Join(w.baseDir, w.baseName+"-"+day+w.ext) w.lj = &lumberjack.Logger{ Filename: filename, MaxSize: w.conf.MaxSize, MaxBackups: w.conf.MaxBackups, MaxAge: w.conf.MaxAge, Compress: w.conf.Compress, } w.currentDay = day } return w.lj.Write(p) }