Golang 日志库 Zap 如何自定义输出目标?

Zap(https://github.com/uber-go/zap)是一个功能非常丰富、易于使用的 Golang日志库。记录日志时,一般会选择输出到控制台(stdout和stderr)或者输出到文件或者同时输出到两者。zap 库也提供了对应的选项。

指定日志输出目标

zap 配置项中(zap.Config), 使用 OutputPaths 和 ErrorOutputPaths 可以指定日志输出目标,首先看下这个配置项的定义和说明:

// OutputPaths is a list of URLs or file paths to write logging output to.
    // See Open for details.
    OutputPaths []string `json:"outputPaths" yaml:"outputPaths"`
    // ErrorOutputPaths is a list of URLs to write internal logger errors to.
    // The default is standard error.
    //
    // Note that this setting only affects internal errors; for sample code that
    // sends error-level logs to a different location from info- and debug-level
    // logs, see the package-level AdvancedConfiguration example.
    ErrorOutputPaths []string `json:"errorOutputPaths" yaml:"errorOutputPaths"`

OutputPaths:是一个字符串类型的切片,可以指定URL、文件路径或者 stdout,默认是stdout。

ErrorOutputPaths:也是一个字符串类型的切片,和 OutputPaths 类似,默认是stderr。

看个例子

package main

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

func main() {
    cfg := zap.NewProductionConfig()
    cfg.OutputPaths = []string{"stdout"}
    cfg.ErrorOutputPaths = []string{"stderr"}
    cfg.EncoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
    logger, err := cfg.Build()
    if err != nil {
        panic("init logger error")
    }
    sugar := Logger.Sugar()

    sugar.Info("这是一条测试日志")
}

这个例子是将日志输出到控制台,如果想也把日志输出到文件一份,修改下第 10 和 11 行,代码如下:

cfg.OutputPaths = []string{"stdout", "./test.log"}
cfg.ErrorOutputPaths = []string{"stderr", "./test.log"}

如果只想输出到文件,代码如下:

cfg.OutputPaths = []string{"./test.log"}
cfg.ErrorOutputPaths = []string{"./test.log"}

自定义日志输出方式

可以使用 zap.RegisterSink 函数来注册自定义的的输出方式,该函数第二个参数为一个自定义的工厂函数, 接收一个 *url.URL 类型的指针, 返回一个 Sink 类型和一个 error,所以需要实现 Sink 接口。看一个具体的示例,自定义了一个通过 http 协议输出到指定接口的输出方式:

package main

import (
    "bytes"
    "errors"
    "fmt"
    "go.uber.org/zap"
    "go.uber.org/zap/zapcore"
    "net/http"
    "net/url"
)

type Http struct {
    Remote *url.URL
}

func (h Http) Sync() error {
    return nil
}

func (h Http) Close() error {
    return nil
}

func (h Http) Write(p []byte) (n int, err error) {
    req, err := http.NewRequest("POST", h.Remote.String(), bytes.NewBuffer(p))
    if err != nil {
        return 0, nil
    }
    req.Header.Set("Content-Type", "application/json")
    client := &http.Client{}
    resp, err := client.Do(req)
    if err != nil {
        return 0, nil
    }
    defer resp.Body.Close()
    if resp.StatusCode != 200 {
        return -1, errors.New("request failed")
    }
    return len(p), nil
}

// 实现工厂函数
func httpSink(url *url.URL) (sink zap.Sink, err error) {
    h := Http{Remote: url}
    return h, nil
}

func main() {
    // 注册自定义方法
    if err := zap.RegisterSink("Http", httpSink); err != nil {
        fmt.Println(err)
        return
    }

    cfg := zap.NewProductionConfig()
    cfg.OutputPaths = []string{"stdout", "http://127.0.0.1/log"}
    cfg.ErrorOutputPaths = []string{"stderr", "http://127.0.0.1/log"}
    cfg.EncoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder

    logger, err := cfg.Build()
    if err != nil {
        panic(err)
    }

    logger.Sugar().Info("这是一条测试日志")
}

然后启动一个服务端,代码如下:

package main

import (
    "bytes"
    "io/ioutil"
    "log"
    "net/http"
)

func main() {
    http.HandleFunc("/log", func(w http.ResponseWriter, r *http.Request) {
        body, _ := ioutil.ReadAll(r.Body)
        log.Println("日志内容内容: ", bytes.NewBuffer(body).String())
    })

    log.Fatal(http.ListenAndServe(":80", nil))
}

然后运行打印日志的代码,可以看到服务端接收到了日志。