日志插件

glog 插件实现。

zap 日志插件

zap Option

internal/logger/zap_option.go

package logger

import (
    "go.uber.org/zap"
    "go.uber.org/zap/zapcore"
)

type ZapOption func(o *zapOption)

type zapOption struct {
    encoder zapcore.EncoderConfig
    level   zap.AtomicLevel
    opts    []zap.Option
    ws      []zapcore.WriteSyncer
}

func Encoder(encoder zapcore.EncoderConfig) ZapOption {
    return func(o *zapOption) { o.encoder = encoder }
}

func Level(level zap.AtomicLevel) ZapOption {
    return func(o *zapOption) { o.level = level }
}

func Option(opts ...zap.Option) ZapOption {
    return func(o *zapOption) { o.opts = opts }
}

func WriteSyncer(ws ...zapcore.WriteSyncer) ZapOption {
    return func(o *zapOption) { o.ws = ws }
}

zap Logger

internal/logger/zap.go

package logger

import (
    "fmt"
    "github.com/camry/g/glog"
    "go.uber.org/zap"
    "go.uber.org/zap/zapcore"
)

var _ glog.Logger = (*ZapLogger)(nil)

// ZapLogger 实现 glog.Logger
type ZapLogger struct {
    log *zap.Logger
}

// NewZapLogger 返回 zap 日志记录器。
func NewZapLogger(opts ...ZapOption) *ZapLogger {
    opt := &zapOption{}
    for _, o := range opts {
        o(opt)
    }
    core := zapcore.NewCore(
        zapcore.NewConsoleEncoder(opt.encoder),
        zapcore.NewMultiWriteSyncer(opt.ws...),
        opt.level,
    )
    zapLogger := zap.New(core, opt.opts...)
    return &ZapLogger{log: zapLogger}
}

// Log 实现 glog.Logger 接口。
func (l *ZapLogger) Log(level glog.Level, keyvals ...any) error {
    if len(keyvals) == 0 || len(keyvals)%2 != 0 {
        l.log.Warn(fmt.Sprint("keyvals must appear in pairs: ", keyvals))
        return nil
    }
    // Zap.Field 出现 keyvals 对时使用。
    var data []zap.Field
    for i := 0; i < len(keyvals); i += 2 {
        data = append(data, zap.Any(fmt.Sprint(keyvals[i]), fmt.Sprint(keyvals[i+1])))
    }
    switch level {
    case glog.LevelDebug:
        l.log.Debug("", data...)
    case glog.LevelInfo:
        l.log.Info("", data...)
    case glog.LevelWarn:
        l.log.Warn("", data...)
    case glog.LevelError:
        l.log.Error("", data...)
    }
    return nil
}

func (l *ZapLogger) Close() error {
    return l.log.Sync()
}

gorm 日志插件

gorm Logger

package logger

import (
    "context"
    "errors"
    "fmt"
    "time"

    "github.com/camry/g/glog"
    "gorm.io/gorm/logger"
    "gorm.io/gorm/utils"
)

type GormLogger struct {
    *logger.Config
    GLog *glog.Helper

    infoStr, warnStr, errStr            string
    traceStr, traceErrStr, traceWarnStr string
}

func NewGormLogger(gl glog.Logger, config *logger.Config) *GormLogger {
    var (
        infoStr      = "%s\n[info] "
        warnStr      = "%s\n[warn] "
        errStr       = "%s\n[error] "
        traceStr     = "%s\n[%.3fms] [rows:%v] %s"
        traceWarnStr = "%s %s\n[%.3fms] [rows:%v] %s"
        traceErrStr  = "%s %s\n[%.3fms] [rows:%v] %s"
    )

    if config.Colorful {
        infoStr = logger.Green + "%s\n" + logger.Reset + logger.Green + "[info] " + logger.Reset
        warnStr = logger.BlueBold + "%s\n" + logger.Reset + logger.Magenta + "[warn] " + logger.Reset
        errStr = logger.Magenta + "%s\n" + logger.Reset + logger.Red + "[error] " + logger.Reset
        traceStr = logger.Green + "%s\n" + logger.Reset + logger.Yellow + "[%.3fms] " + logger.BlueBold + "[rows:%v]" + logger.Reset + " %s"
        traceWarnStr = logger.Green + "%s " + logger.Yellow + "%s\n" + logger.Reset + logger.RedBold + "[%.3fms] " + logger.Yellow + "[rows:%v]" + logger.Magenta + " %s" + logger.Reset
        traceErrStr = logger.RedBold + "%s " + logger.MagentaBold + "%s\n" + logger.Reset + logger.Yellow + "[%.3fms] " + logger.BlueBold + "[rows:%v]" + logger.Reset + " %s"
    }

    return &GormLogger{
        GLog:         glog.NewHelper(gl),
        Config:       config,
        infoStr:      infoStr,
        warnStr:      warnStr,
        errStr:       errStr,
        traceStr:     traceStr,
        traceWarnStr: traceWarnStr,
        traceErrStr:  traceErrStr,
    }
}

func (l *GormLogger) LogMode(level logger.LogLevel) logger.Interface {
    newLogger := *l
    newLogger.LogLevel = level
    return &newLogger
}

func (l *GormLogger) Info(ctx context.Context, msg string, i ...any) {
    if l.LogLevel >= logger.Info {
        l.GLog.Infof(l.infoStr+msg, i...)
    }
}

func (l *GormLogger) Warn(ctx context.Context, msg string, i ...any) {
    if l.LogLevel >= logger.Warn {
        l.GLog.Warnf(l.warnStr+msg, i...)
    }
}

func (l *GormLogger) Error(ctx context.Context, msg string, i ...any) {
    if l.LogLevel >= logger.Error {
        l.GLog.Errorf(l.errStr+msg, i...)
    }
}

func (l *GormLogger) Trace(ctx context.Context, begin time.Time, fc func() (sql string, rowsAffected int64), err error) {
    if l.LogLevel <= logger.Silent {
        return
    }

    elapsed := time.Since(begin)
    switch {
    case err != nil && l.LogLevel >= logger.Error && (!errors.Is(err, logger.ErrRecordNotFound) || !l.IgnoreRecordNotFoundError):
        sql, rows := fc()
        if rows == -1 {
            l.GLog.Errorf(l.traceErrStr, utils.FileWithLineNum(), err, float64(elapsed.Nanoseconds())/1e6, "-", sql)
        } else {
            l.GLog.Errorf(l.traceErrStr, utils.FileWithLineNum(), err, float64(elapsed.Nanoseconds())/1e6, rows, sql)
        }
    case elapsed > l.SlowThreshold && l.SlowThreshold != 0 && l.LogLevel >= logger.Warn:
        sql, rows := fc()
        slowLog := fmt.Sprintf("SLOW SQL >= %v", l.SlowThreshold)
        if rows == -1 {
            l.GLog.Warnf(l.traceWarnStr, utils.FileWithLineNum(), slowLog, float64(elapsed.Nanoseconds())/1e6, "-", sql)
        } else {
            l.GLog.Warnf(l.traceWarnStr, utils.FileWithLineNum(), slowLog, float64(elapsed.Nanoseconds())/1e6, rows, sql)
        }
    case l.LogLevel == logger.Info:
        sql, rows := fc()
        if rows == -1 {
            l.GLog.Infof(l.traceStr, utils.FileWithLineNum(), float64(elapsed.Nanoseconds())/1e6, "-", sql)
        } else {
            l.GLog.Infof(l.traceStr, utils.FileWithLineNum(), float64(elapsed.Nanoseconds())/1e6, rows, sql)
        }
    }
}

gorm 配置

func NewDb(cfg *config.Config, l glog.Logger) *gorm.DB {
    dsn := fmt.Sprintf(`%s:%s@tcp(%s:%d)/%s?charset=%s&parseTime=True&loc=Local`,
        cfg.Db.User, cfg.Db.Pass, cfg.Db.Host, cfg.Db.Port, cfg.Db.Name, cfg.Db.Charset,
    )
    db, err := gorm.Open(mysql.New(mysql.Config{
        DSN:                       dsn,   // DSN data source name
        DefaultStringSize:         256,   // string 类型字段的默认长度
        DisableDatetimePrecision:  true,  // 禁用 datetime 精度,MySQL 5.6 之前的数据库不支持
        DontSupportRenameIndex:    true,  // 重命名索引时采用删除并新建的方式,MySQL 5.7 之前的数据库和 MariaDB 不支持重命名索引
        DontSupportRenameColumn:   true,  // 用 `change` 重命名列,MySQL 8 之前的数据库和 MariaDB 不支持重命名列
        SkipInitializeWithVersion: false, // 根据当前 MySQL 版本自动配置
    }), &gorm.Config{
        Logger: logger.NewGormLogger(l, &gormLogger.Config{
            SlowThreshold:             200 * time.Millisecond,
            LogLevel:                  gormLogger.Warn,
            IgnoreRecordNotFoundError: false,
            Colorful:                  false,
        }),                           // 设置日志处理器
        SkipDefaultTransaction: true, // 禁用默认事务
        PrepareStmt:            true, // 缓存预编译语句
    })
    if err != nil {
        panic(err)
    }
    return db
}

gin 日志中间件

Gin Logger 中间件

package middleware

import (
    "fmt"
    "net/http"
    "runtime/debug"
    "time"

    "github.com/camry/g/glog"
    "github.com/gin-gonic/gin"
)

func GinLogger() gin.HandlerFunc {
    return func(c *gin.Context) {
        start := time.Now()
        c.Next()

        req := c.Request
        res := c.Writer

        fields := []any{
            "remote_ip", c.ClientIP(),
            "latency", time.Now().Sub(start),
            "host", req.Host,
            "request", fmt.Sprintf("%s %s", req.Method, req.RequestURI),
            "status", res.Status(),
            "size", res.Size(),
            "user_agent", req.UserAgent(),
            "errors", c.Errors.ByType(gin.ErrorTypePrivate).String(),
            "raw", req.URL.RawQuery,
        }

        n := res.Status()
        switch {
        case n >= 500:
            fields = append(fields, "msg", "Server error")
            glog.Errorw(fields...)
        case n >= 400:
            fields = append(fields, "msg", "Client error")
            glog.Warnw(fields...)
        case n >= 300:
            fields = append(fields, "msg", "Redirection")
            glog.Infow(fields...)
        default:
            fields = append(fields, "msg", "Success")
            glog.Infow(fields...)
        }
    }
}

func GinRecover() gin.HandlerFunc {
    return func(c *gin.Context) {
        defer func() {
            if r := recover(); r != nil {
                glog.Errorf(`panic: %v`, r)
                glog.Errorf(`stack: %v`, string(debug.Stack()))
                debug.PrintStack()
                c.String(http.StatusInternalServerError, "Internal Server Error")
                c.AbortWithStatus(http.StatusInternalServerError)
            }
        }()
        c.Next()
    }
}

Gin 路由中间件配置

func NewRouter(cfg *config.Config, logger glog.Logger, sn *snowflake.Node, bt *table.BattleTable) *gin.Engine {
    gin.SetMode(cfg.App.Env)

    router := gin.Default()

    router.Use(middleware.GinLogger(), middleware.GinRecover())

    homeC := controllers.NewHome(cfg, logger, bt, sn)

    router.GET("/", homeC.Index)
    router.GET("/home/debug", homeC.Debug)

    return router
}