相比起 zap 日志库,logrus 性能会低一些,但在很多场景,并不需要太高性能,而 logrus 库的自定义能力较为强大,可以任意定义日志格式,较为方便。

package main

import (
    "context"
    "fmt"
    "github.com/sirupsen/logrus"
    "io"
    "os"
    "path/filepath"
    "runtime"
    "strings"
)

const logPath = "./log.txt"

var log = New()

func New() *Logger {
    return &Logger{logger: logrus.New()}
}

type Logger struct {
    ctx    context.Context
    logger *logrus.Logger
}

func (l *Logger) SetFormatter(formatter logrus.Formatter) {
    l.logger.SetFormatter(formatter)
}

func (l *Logger) SetOutput(output io.Writer) {
    l.logger.SetOutput(output)
}

func (l *Logger) SetLevel(level logrus.Level) {
    l.logger.SetLevel(level)
}

func (l *Logger) WithContext(ctx context.Context) *Logger {
    return &Logger{ctx: ctx, logger: l.logger}
}

func (l *Logger) Debug(format string, args ...any) {
    l.log(logrus.DebugLevel, format, args...)
}

func (l *Logger) Info(format string, args ...any) {
    l.log(logrus.InfoLevel, format, args...)
}

func (l *Logger) Warn(format string, args ...any) {
    l.log(logrus.WarnLevel, format, args...)
}

func (l *Logger) Error(format string, args ...any) {
    l.log(logrus.ErrorLevel, format, args...)
}

func (l *Logger) Fatal(format string, args ...any) {
    l.log(logrus.FatalLevel, format, args...)
}

func (l *Logger) log(level logrus.Level, format string, args ...any) {
    format = fmt.Sprintf("[%s] %s", GetCallerShort(3), format)
    l.logger.WithContext(l.ctx).Logf(level, format, args...)
}

type Formatter struct{}

func (f *Formatter) Format(entry *logrus.Entry) ([]byte, error) {
    pid := os.Getpid()
    time := entry.Time.Format("2006-01-02T15:04:05.000000")
    level := strings.ToUpper(entry.Level.String())
    traceId := GetTraceIdFromCtx(entry.Context)

    content := fmt.Sprintf("%s %s %d %s %s\n", time, level, pid, traceId, entry.Message)
    return []byte(content), nil
}

// GetCallerShort 获取调用点文件名 + 行号
func GetCallerShort(skip int) string {
    _, file, line, ok := runtime.Caller(skip + 1)
    if !ok {
       return ""
    }
    _, file = filepath.Split(file)
    return fmt.Sprintf("%s:%d", file, line)
}

// CtxSetTraceId 在 ctx 中设置 trace id
func CtxSetTraceId(ctx context.Context) context.Context {
    return context.WithValue(ctx, "trace_id", "id1")
}

// GetTraceIdFromCtx 打印日志时,从 ctx 中获取 trace id 打印
func GetTraceIdFromCtx(ctx context.Context) string {
    if ctx == nil {
       return "-"
    }

    val := ctx.Value("trace_id")
    if traceId, ok := val.(string); ok {
       return fmt.Sprintf("trace-%s", traceId)
    } else {
       return "-"
    }
}

func init() {
    f, err := os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o640)
    if err != nil {
       panic(err)
    }

    log.SetFormatter(&Formatter{})
    log.SetOutput(io.MultiWriter(os.Stdout, f))
    log.SetLevel(logrus.DebugLevel)
}

func main() {
    ctx := context.Background()
    ctx = CtxSetTraceId(ctx)
    log.WithContext(ctx).Info("main failed: %s", "detail")
}