lesismal / nbio

Pure Go 1000k+ connections solution, support tls/http1.x/websocket and basically compatible with net/http, with high-performance and low memory cost, non-blocking, event-driven, easy-to-use.
MIT License
2.22k stars 155 forks source link

custom Logger #305

Closed liwnn closed 1 year ago

liwnn commented 1 year ago

想把nbio的logger接入zaplog,代码如下

zapLog := logger.NewZap(logger.Level(cfg.Log.Level))
logging.SetLogger(zapLog)

但是zaplogger没有实现SetLevel(lvl int)接口,所以没法接入。看下能否去掉SetLevel接口?

日志级别应该是日志实现者的职责,不应该强制设置。因为日志级别的字段类型不同,也很难统一设置。

lesismal commented 1 year ago

提交了一版到这个分支,估计最近会合并到master和release: https://github.com/lesismal/nbio/commit/5d0c6a2daac7c3d356df8015e7e676467114bd9c

可以先用这个分支测试,或者应用层自己先封装个结构体实现 Logger :

type UserLogger struct {
    fsetLevel func(lvl int)
    fdebug    func(format string, v ...interface{})
    finfo     func(format string, v ...interface{})
    fwarn     func(format string, v ...interface{})
    ferror    func(format string, v ...interface{})
}

然后初始化把这个logger字段设置为zaplog的func,setLevel设置个空函数 func(int) {},这样应该也是可以的

lesismal commented 1 year ago

或者把Output设置为你们的writer也可以,但nbio部分的日志还是nbio自己的格式:

logging.Output = yourLogWriter
liwnn commented 1 year ago

或者把Output设置为你们的writer也可以,但nbio部分的日志还是nbio自己的格式:

logging.Output = yourLogWriter

ok,先用这种方式

lesismal commented 1 year ago

发布了 v1.3.16 , 去掉了 SetLevel

liwnn commented 1 year ago

不好意思,之前漏看了logger接口。参考了zap、grpc这些库的logger接口:

Debugf(format string, v ...interface{})
Debug(v ...interface{})

带format的函数名都会有加上f。可以自己重新定义适配nbio,不过我觉得还是跟这些库适配下比较好。您评估下,觉得没必要就给close掉

lesismal commented 1 year ago

我前面两层楼说的方法,都可以用户层自己进行适配实现的。

没有必要向其他库看齐,否则又有一个其他知名的库跟你提到的库不一样,我得怎么看齐呢?

用户自己能适配的就自己适配下,不能指望基础设施都向用户需求看齐,否则标准库的很多东西得天天接需求改造。

lesismal commented 1 year ago

有些基础设施的库,直接用标准库的 log,但标准库 log 也跟 zap 这些不一样。

另一个知名库 zerolog 还有这样用法: log.Info().Msg("hello world")

logrus 有 log.SetLevel(log.WarnLevel)

不能只以你们自己项目的习惯用法来要求基础设施啊!

兄弟,我有点后悔今天对 Logger 的修改啊,不能随便听你们的需求!

下个版本我revert回去

:joy::joy:

liwnn commented 1 year ago

哈哈,把你带沟里去了。我看了下logrus,他也是这样的

func (logger *Logger) Debug(args ...interface{}) {
    logger.Log(DebugLevel, args...)
}
func (logger *Logger) Debugf(format string, args ...interface{}) {
    logger.Logf(DebugLevel, format, args...)
}

go的标准库log:

func Printf(format string, v ...any) {

感觉这是大家的一个默认规则,带f的都有format。我觉可以像大多数库看齐

liwnn commented 1 year ago

有些基础设施的库,直接用标准库的 log,但标准库 log 也跟 zap 这些不一样。

另一个知名库 zerolog 还有这样用法: log.Info().Msg("hello world")

logrus 有 log.SetLevel(log.WarnLevel)

不能只以你们自己项目的习惯用法来要求基础设施啊!

兄弟,我有点后悔今天对 Logger 的修改啊,不能随便听你们的需求!

下个版本我revert回去

😂😂

这个我就不太认同的,logrus带level,是因为它是日志库的具体实现。同样的zap、包括nbio默认库都需要有setlevel函数。但是logging是一个抽象接口,只需要能打印日志就行。具体的是setlevel或者有些日志需要flush,logging就不管了

liwnn commented 1 year ago

这个其实影响不大,我觉的像大多数知名库看齐,方便即插即用。可以适配,这个就先关了

lesismal commented 1 year ago

感觉这是大家的一个默认规则,带f的都有format。我觉可以像大多数库看齐

这个我就不太认同的,logrus带level,是因为它是日志库的具体实现。同样的zap、包括nbio默认库都需要有setlevel函数。但是logging是一个抽象接口,只需要能打印日志就行。具体的是setlevel或者有些日志需要flush,logging就不管了

需求不一样,nbio的需求是打印自己这一层的日志,一些用处不大的debug日志之类的,方便用户通过 SetLevel 屏蔽,所以才内部区分成 Debug/Info 这些级别。format 是为了 nbio 自己日志的方便。

而其他那些专业日志库,是面向日志这种“业务”本身,所以它们要考虑性能、结构化等各种,提供给用户的接口也需要你说的所谓的标准,但这不适用于 nbio。

就像标准库有些地方也会有日志,但并不会向你说的这些日志库看齐、连 Level 都不区分的。

web框架echo里提供了Debug/Debugf进行区分: https://github.com/labstack/gommon/blob/master/log/log.go#L157

我个人觉得基础库的日志格式,还是带 format 的更简洁明了些。 nbio里的日志不是特别多,把 f 省去了,即使区分 Debug和Debugf的实现,也是调用 Debugf。 然后用户还是要来适配 Debugf、还是带format。 但这些都可以通过我上面说的两种方案来实现,想与其他库结构化之类的格式兼容,就自己Wrap个中间层实现Logger定义设置下,如果不需要就 SetOutput 就可以了

如果与其他库适配,虽然方便即插即用了,但不带format、日志就不那么好看了。 如果用 Printf,就与我上面说的方便用户屏蔽不需要的Debug日志之类的冲突。

所以下个版本我再把SetLevel弄回来

应用层该做适配的,由应用层自己实施,没什么难度。

liwnn commented 1 year ago

但是我觉得,只是把Debug改名成Debugf,就可以适配大多数日志库,而不用让使用者自己去适配。整理是利大于弊的

lesismal commented 1 year ago

结构化日志也并不是所有项目的需求。 上了ELK之类的日志系统,如果遇到有业务故障需要编码通过结构化日志来自动处理,这种场景下,结构化日志优势,但前提也是编写日志代码有较好得设计规范和实现。 但更多的项目,甚至是在用ELK这些的,还是在用format为主的日志,包括上了ELK这些的,很多还是format了的、只是全文搜索关键字比较方便。 绝大多数项目对业务可用性要求没那么高、或者说不是每一个模块对业务可用性都那么高,对结构化日志的要求就更低了,因为单就可读性来讲,结构化的比format的要差一些。 还有基础设施类的,网关、代理,,大家都是format这种

liwnn commented 1 year ago

我的意思是保持用带format的形式。但是函数名改成Debugf、Infof、Warnf、Errof。这样可以直接适配其他日志

type Logger interface {
    Debugf(format string, v ...interface{})
    Infof(format string, v ...interface{})
    Warnf(format string, v ...interface{})
    Errorf(format string, v ...interface{})
}

不是说去掉format,感觉说的不是一个频道

liwnn commented 1 year ago

这样仅仅把函数名字改下,就可以同大多数日志库一致。而且成本也很低

lesismal commented 1 year ago

不是说去掉format,感觉说的不是一个频道

"但是我觉得,只是把Debug改名成Debugf,就可以适配大多数日志库,而不用让使用者自己去适配。整理是利大于弊的“ 你说这句的时候我正在编辑这楼下面的那段,并不是回复你这一楼的,而是接着我前面楼层

lesismal commented 1 year ago

这样仅仅把函数名字改下,就可以同大多数日志库一致。而且成本也很低

这就和旧版本不兼容性了,别人的项目也有设置 Logger 替换 nbio 这个默认 Logger 的,修改了别人更新nbio就不兼容了。不能只考虑你的需求啊

还是前面说的那样,你可以自己Wrap一个logger实现我这几个蹩脚的接口就可以了

liwnn commented 1 year ago

这样仅仅把函数名字改下,就可以同大多数日志库一致。而且成本也很低

这就和旧版本不兼容性了,别人的项目也有设置 Logger 替换 nbio 这个默认 Logger 的,修改了别人更新nbio就不兼容了。不能只考虑你的需求啊

还是前面说的那样,你可以自己Wrap一个logger实现我这几个蹩脚的接口就可以了

好的,兼容性确实是个问题,那就我自己适配

lesismal commented 1 year ago

好的,兼容性确实是个问题,那就我自己适配

很多基础设施,是没有统一标准的,比如你同时依赖多个基础设施,这几个设施依赖了不同的日志实现,很可能这几个日志实现不一样,那只能应用层自己做兼容、不可能要求每个基础设施都去按照你们项目习惯使用的那个日志库去实现。

还有比如对net.Conn的实现,一些网络协议时没有实现net.Conn的比如websocket、QUIC。 所以我另一个项目arpc里,为了支持多种协议做transport层,去自己封装一层来实现net.Conn: https://github.com/lesismal/arpc/tree/master/extension/protocol

lesismal commented 1 year ago

还有比如这个例子: https://github.com/lesismal/nbio/issues/263

gin、echo的 ResponseWriter,因为它们要考虑 SEO 所以支持 template 为主,所以都没有支持 sendfile 这种 zero copy ,但对静态资源服务就性能不友好了,而且各个框架各自实现都不一样,只能应用层按各个框架各自方式来处理它们

中间层才是王道

lesismal commented 1 year ago

睡了睡了,晚安了

liwnn commented 1 year ago

晚安,感谢大佬解答。希望nbio越来越火

lesismal commented 1 year ago

我又想了下,或许可以这样实现老版本兼容:

nbio自己先添加一个中间层的 thirdLogger:

// third_logger.go
package logging

type thirdLogger struct {
    setLevel func(l int)
    debug    func(format string, v ...interface{})
    info     func(format string, v ...interface{})
    warn     func(format string, v ...interface{})
    err      func(format string, v ...interface{})
}

// SetLevel sets logs priority.
func (l *thirdLogger) SetLevel(lvl int) {
    l.setLevel(lvl)
}

// Debug uses fmt.Printf to log a message at LevelDebug.
func (l *thirdLogger) Debug(format string, v ...interface{}) {
    l.debug(format, v...)
}

// Info uses fmt.Printf to log a message at LevelInfo.
func (l *thirdLogger) Info(format string, v ...interface{}) {
    l.info(format, v...)
}

// Warn uses fmt.Printf to log a message at LevelWarn.
func (l *thirdLogger) Warn(format string, v ...interface{}) {
    l.warn(format, v...)
}

// Error uses fmt.Printf to log a message at LevelWarn.
func (l *thirdLogger) Error(format string, v ...interface{}) {
    l.err(format, v...)
}

SetLogger 方法里面做兼容:


// Logger defines log interface.
type Logger interface {
    SetLevel(l int)
    Debug(format string, v ...interface{})
    Info(format string, v ...interface{})
    Warn(format string, v ...interface{})
    Error(format string, v ...interface{})
}

// SetLogger sets default logger.
func SetLogger(v interface{}) {
    if l, ok := v.(Logger); ok {
        DefaultLogger = l
        return
    }

    l := &thirdLogger{}
    if i, ok := v.(interface {
        Debugf(format string, v ...interface{})
        Infof(format string, v ...interface{})
        Warnf(format string, v ...interface{})
        Errorf(format string, v ...interface{})
    }); ok {
        l.debug = i.Debugf
        l.info = i.Infof
        l.warn = i.Warnf
        l.err = i.Errorf
    } else {
        ilogger := &logger{level: LevelInfo}
        l.debug = ilogger.Debug
        l.info = ilogger.Info
        l.warn = ilogger.Warn
        l.err = ilogger.Error
        l.setLevel = ilogger.SetLevel
    }
    if i, ok := v.(interface {
        SetLevel(l int)
    }); ok {
        l.setLevel = i.SetLevel
    }

    DefaultLogger = l
}
lesismal commented 1 year ago

这样即使三方库使用了 SetLogger,参数类型时 interface{} 了也能兼容到别人的调用,也能把别人的这些 f 系列函数的 Logger 接口设置进来

type Logger interface {
    Debugf(format string, v ...interface{})
    Infof(format string, v ...interface{})
    Warnf(format string, v ...interface{})
    Errorf(format string, v ...interface{})
}

完整的: logging.zip

lesismal commented 1 year ago

还是算了,这样的话代码就太奇怪了

liwnn commented 1 year ago

ok, ok。确实不好改,得项目开始就定义好。

liwnn commented 1 year ago

https://github.com/grpc/grpc-go/blob/master/grpclog/loggerv2.go grpc的做法,加了个V2。然后把原先的标记为Deprecated

lesismal commented 1 year ago

不改了,应用层自己适配下,其他人有这样做的: https://github.com/inaneverb/ekaweb/blob/master/framework/nbio/logging_bridge.go

grpc那个V2,如果用户想设置自己的logger还是要实现 LoggerV2,这里有个 V(l int) bool 未必每个logger框架都有、用户还是要自己适配: https://github.com/grpc/grpc-go/blob/master/grpclog/loggerv2.go#L66

lesismal commented 1 year ago

echo框架的 Logger 也是有 SetLevel 的: https://github.com/labstack/echo/blob/master/log.go#L17

按照你们习惯使用的日志库,如果你们使用 echo 框架那是不是也需要让它去改一下 Logger 定义? 实际上没必要的,应用层自己来适配吧,别用几家的定义去约束其他家的定义。

liwnn commented 1 year ago

ok, ok.我已经做了适配了,就是突然想到的

lesismal commented 1 year ago

gin的一些日志更霸道,根本不给你自己搞格式的机会,只能output: https://github.com/gin-gonic/gin/blob/master/debug.go#L50 https://github.com/gin-gonic/gin/blob/master/logger.go#L267

lesismal commented 1 year ago

标准库不是slog嘛,只能等标准库的slog越来越证明成熟适合生产后,大家统一换到slog就好了,否则日志这玩意一家一个样子,没有完全一样的。 标准库的 log.Printf 自动换行的,那还要 log.Println 也是完全多余啊(省了format性能倒是略好)。。。所以我就没打算遵循标准库原来的 log 或者其他日志库。我这一组不带f,纯是自己只有一种日志需要、带上 f 的名字太丑,就粗暴这样搞了,留给用户可配置的方式了用户想咋弄都行的

lesismal commented 1 year ago

对,等标准库 slog 666 了,我换到 slog 去,哈哈哈

liwnn commented 1 year ago

主要是现在有个问题,不同的库都有自己的logging接口。有的Infof带格式,有的Info带格式,适配起来有些麻烦。只能等个能统一标准的

lesismal commented 1 year ago

这事只能等标准库了,任何非官方都难做到一统江湖。

fasthttp这个最简洁不能再简洁了。。 https://github.com/valyala/fasthttp/blob/master/server.go#L856

liwnn commented 1 year ago

休息了,晚安

liwnn commented 1 year ago

这事只能等标准库了,任何非官方都难做到一统江湖。

fasthttp这个最简洁不能再简洁了。。 https://github.com/valyala/fasthttp/blob/master/server.go#L856

确实需要标准库,现在只能舍弃一部分日志行号了。因为带f跟不带f的不兼容。写两个文件可读性又差,代码行还可以接受。 image

lesismal commented 1 year ago

确实需要标准库,现在只能舍弃一部分日志行号了。因为带f跟不带f的不兼容。写两个文件可读性又差,代码行还可以接受。

行号不至于啊,日志库一般都支持设置stack depth的,zap的我看了下,弄个单独的 sugar 给nbio,这个sugar用 AddCallerSkip 就可以了: https://github.com/uber-go/zap/issues/1070#issuecomment-1078681368

测试文件名 log.go,完整代码:

// log.go
package main

import (
    "context"
    "fmt"
    "net/http"
    "os"
    "os/signal"
    "time"

    "github.com/lesismal/nbio/logging"
    "github.com/lesismal/nbio/nbhttp"
    "go.uber.org/zap"
)

func main() {
    logger, _ := zap.NewProduction()

    // setup nbio logger
    nbioSugar := logger.Sugar().WithOptions(zap.AddCallerSkip(2))
    logging.SetLogger(newNBIOLogger(nbioSugar))
    defer nbioSugar.Sync()

    // Your sugar logger
    sugar := logger.Sugar()
    defer sugar.Sync()

    sugar.Infof("App log 111")

    mux := &http.ServeMux{}
    mux.HandleFunc("/now", onTime)
    engine := nbhttp.NewEngine(nbhttp.Config{
        Network: "tcp",
        Addrs:   []string{"localhost:8080"},
        Handler: mux,
    })

    err := engine.Start()
    if err != nil {
        fmt.Printf("nbio.Start failed: %v\n", err)
        return
    }

    interrupt := make(chan os.Signal, 1)
    signal.Notify(interrupt, os.Interrupt)
    <-interrupt
    ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
    defer cancel()
    engine.Shutdown(ctx)
}

func onTime(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte(time.Now().Format("20060102 15:04:05")))
}

type nbioLogger struct {
    sugar *zap.SugaredLogger
}

func (logger *nbioLogger) SetLevel(_ int) {
    // Do nothing. Nbio package does not call this method.
}

func (logger *nbioLogger) Debug(format string, v ...interface{}) {
    logger.sugar.Debugf(format, v...)
}

func (logger *nbioLogger) Info(format string, v ...interface{}) {
    logger.sugar.Infof(format, v...)
}

func (logger *nbioLogger) Warn(format string, v ...interface{}) {
    logger.sugar.Warnf(format, v...)
}

func (logger *nbioLogger) Error(format string, v ...interface{}) {
    logger.sugar.Errorf(format, v...)
}

func newNBIOLogger(sugar *zap.SugaredLogger) *nbioLogger {
    return &nbioLogger{sugar: sugar}
}

output:

{"level":"info","ts":1684896533.5934606,"caller":"log/log.go:28","msg":"App log 111"}
{"level":"info","ts":1684896533.593972,"caller":"nbio@v1.3.16/engine_std.go:102","msg":"NBIO[NB] start"}
{"level":"info","ts":1684896533.6130466,"caller":"nbhttp/engine.go:378","msg":"Serve HTTP On: [tcp@127.0.0.1:8080]"}
lesismal commented 1 year ago

如果不支持设置栈深度,那三方日志库就不能被封装日志接口了、文件都得错乱,所以这应该是各个知名日志库的基本功能。

liwnn commented 1 year ago

如果不支持设置栈深度,那三方日志库就不能被封装日志接口了、文件都得错乱,所以这应该是各个知名日志库的基本功能。

因为还有另外的库也用了zaplog,它兼容zaplog,而nbio包了一层,所以深度不一样的。不过您这么一说我知道怎么搞了,我再包一层适配另外的库,让它们深度一样就解决了。

lesismal commented 1 year ago

因为还有另外的库也用了zaplog,它兼容zaplog,而nbio包了一层,所以深度不一样的。不过您这么一说我知道怎么搞了,我再包一层适配另外的库,让它们深度一样就解决了。

不同的库做适配,用不同的sugarlogger,每个sugarlogger设置自己的深度就可以了 我示例代码里,Your sugar就是zap默认的深度,nbioSugar就是深度+2(包装的这个Logger一层、nbio.logging.Info这些包函数一层,一共是2) 你跑下我的示例代码,看下output,nbio里的日志是打印了正常的文件行号的,app自己的sugar也是打印了正确的行号的:

/ setup nbio logger // 给nbio单独一个sugar、深度+2
nbioSugar := logger.Sugar().WithOptions(zap.AddCallerSkip(2))
logging.SetLogger(newNBIOLogger(nbioSugar))
defer nbioSugar.Sync()

// Your sugar logger // 你们项目自己的sugar,不用更改深度
sugar := logger.Sugar()
defer sugar.Sync()

// Other sugar logger ,如果也需要适配其他库,再弄个sugar给它就可以了
// ...

output:

// 这个是测试代码的正确文件行号
{"level":"info","ts":1684896533.5934606,"caller":"log/log.go:28","msg":"App log 111"}

// 这两个都是nbio正确的文件行号
{"level":"info","ts":1684896533.593972,"caller":"nbio@v1.3.16/engine_std.go:102","msg":"NBIO[NB] start"}
{"level":"info","ts":1684896533.6130466,"caller":"nbhttp/engine.go:378","msg":"Serve HTTP On: [tcp@127.0.0.1:8080]"}

你先看下我的示例代码就懂了

liwnn commented 1 year ago

明白了,感谢

lesismal commented 1 year ago

好嘞,不客气