[Go] 基于 Zap 与 ELK 的日志分析实践

6,220 阅读2分钟

Zap 是 Uber 开源的一款高性能日志工具。

本篇文章实现的 ELK 架构如下图,通过定制化 Zap 实现多输出源,同时将日志输出到 Console (Standard IO) 与 MQ 中,再配置 Logstash Input 使其读取 MQ 中的日志并写入 ES 中,最后在 Kibana 中展示。

本文使用 Redis 作为 MQ 实现,可替换为其他 MQ。

image.png

定制化 Zap

笔者理解的 zap logger 组件如下图,通过创建不同的 zapcore 可以实现多格式,多级别的日志输出。

image.png

我们首先创建 logger,其只有一个输出到 console 的 zapcore。

func NewLogger() *zap.Logger {
	// 限制日志输出级别, >= DebugLevel 会打印所有级别的日志
	// 生产环境中一般使用 >= ErrorLevel
	lowPriority := zap.LevelEnablerFunc(func(lv zapcore.Level) bool {
		return lv >= zapcore.DebugLevel
	})

	// 使用 JSON 格式日志
	jsonEnc := zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig())
	stdCore := zapcore.NewCore(jsonEnc, zapcore.Lock(os.Stdout), lowPriority)

	// logger 输出到 console 且标识调用代码行
	return zap.New(stdCore).WithOptions(zap.AddCaller())
}

func main() {
	logger := NewLogger()

	logger.Info("test logger info", zap.String("hello", "logger"))
}

日志输出格式:

{"level":"info","ts":1578372154.69565,"caller":"hello/main.go:28","msg":"test logger info","hello":"logger"}

接下来添加同步日志到 redis 的 zapcore,LevelEnablerEncoder 不变,通过 zapcore.AddSync 可以将一个实现 io.Writer 接口的对象转为 zap 需要的 WriteSyncer

import (
	"os"

	"github.com/go-redis/redis"
	"go.uber.org/zap"
	"go.uber.org/zap/zapcore"
)

func NewRedisWriter(key string, cli *redis.Client) *redisWriter{
	return &redisWriter{
		cli: cli, listKey: key,
	}
}
// 为 logger 提供写入 redis 队列的 io 接口
type redisWriter struct {
	cli *redis.Client
	listKey string
}

func (w *redisWriter) Write(p []byte) (int, error) {
	n, err := w.cli.RPush(w.listKey, p).Result()
	return int(n), err
}

func NewLogger(writer *redisWriter) *zap.Logger {
	// 限制日志输出级别, >= DebugLevel 会打印所有级别的日志
	// 生产环境中一般使用 >= ErrorLevel
	lowPriority := zap.LevelEnablerFunc(func(lv zapcore.Level) bool {
		return lv >= zapcore.DebugLevel
	})

	// 使用 JSON 格式日志
	jsonEnc := zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig())
	stdCore := zapcore.NewCore(jsonEnc, zapcore.Lock(os.Stdout), lowPriority)

	// addSync 将 io.Writer 装饰为 WriteSyncer
	// 故只需要一个实现 io.Writer 接口的对象即可
	syncer := zapcore.AddSync(writer)
	redisCore := zapcore.NewCore(jsonEnc, syncer, lowPriority)

	// 集成多个 core
	core := zapcore.NewTee(stdCore, redisCore)

	// logger 输出到 console 且标识调用代码行
	return zap.New(core).WithOptions(zap.AddCaller())
}

func main() {
	cli := redis.NewClient(&redis.Options{
		Addr: "127.0.0.1:6379",
	})
	writer := NewRedisWriter("log_list", cli)
	logger := NewLogger(writer)

	logger.Info("test logger info", zap.String("hello", "logger"))
}

在 Redis 中查看是否 push:

127.0.0.1:6379> lpop log_list
"{\"level\":\"info\",\"ts\":1578373888.516468,\"caller\":\"hello/main.go:58\",\"msg\":\"test logger info\",\"hello\":\"logger\"}\n"

搭建 elk 环境

这个 repo 包含了一套非常方便搭建的 elk 环境,使用 docker 与 docker-compose 直接启动即可,并且支持通过 yml 修改配置。

git clone https://github.com/deviantony/docker-elk.git

修改 logstash input ,配置文件位置 docker-elk/logstash/pipeline/logstash.conf

input {
	tcp {
		port => 5000
	}
	redis {
		data_type => "list"
		key => "log_list"
		host => "127.0.0.1"
		port => 6379
		db => 0
		threads => 2
	}
}

修改后启动 elk ,访问 Kibana (http://localhost:5601),默认用户名:elastic,密码:changeme

docker-compose up

首次启动需要创建 Index Pattern,可以由 Home 右下方 Manage and Administer the Elastic Stack 的 Index Patterns 进入。

image.png

输入正则使匹配 logstash

image.png

image.png

选择 timestamp 作为 filter

image.png

创建后回到 Discover 页面

image.png

调用 logger 输出日志,可以在 Kibana 上看到。

image.png

总结

Zap 和 Elk 还有很多强大的功能,此处仅展示最基本的使用,仅供参考。