duyong mac 5 kuukautta sitten
vanhempi
commit
c3cc723770
12 muutettua tiedostoa jossa 518 lisäystä ja 2 poistoa
  1. 2 2
      .gitignore
  2. 44 0
      examples/logs/main.go
  3. BIN
      logs/doc/img.png
  4. BIN
      logs/doc/img_1.png
  5. BIN
      logs/doc/img_2.png
  6. BIN
      logs/doc/img_3.png
  7. BIN
      logs/doc/img_4.png
  8. 137 0
      logs/doc/readme.md
  9. 15 0
      logs/encoder.go
  10. 206 0
      logs/log.go
  11. 23 0
      logs/logger.go
  12. 91 0
      logs/types.go

+ 2 - 2
.gitignore

@@ -15,12 +15,12 @@
 .fleet/
 .DS_Store
 **/bin
-**/logs
+
 **/docs
 **/mbtest*
 **/.idea
 
 /vendor/
 /wb
-/logs/
+
 test.log

+ 44 - 0
examples/logs/main.go

@@ -0,0 +1,44 @@
+package main
+
+import (
+	"context"
+	"errors"
+	"fmt"
+	"gogs.tyduyong.com/duyong/dy-pkg/app/options"
+	"gogs.tyduyong.com/duyong/dy-pkg/logs"
+)
+
+func main() {
+	simple()
+	use()
+}
+
+func simple() {
+	logs.Infof("this is a test log, message: %s", "good")
+	logs.Debug("this is debug message", logs.String("name", "duyong"))
+	logs.Debugf("this is debugf message:%s", "debugf")
+	logs.Error("this is error message", logs.Err(errors.New("name not found")))
+	fmt.Println(123)
+}
+
+func use() {
+	opts := &options.LogOptions{
+		Level:            "debug",
+		Format:           "console",
+		EnableColor:      true,
+		DisableCaller:    true,
+		OutputPaths:      []string{"test.log", "stdout"},
+		ErrorOutputPaths: []string{"stdout"}, // 这个路径指的是zap内部错误时输出的位置。而不是zap打印error日志的输出位置。
+	}
+	logs.Init(opts)
+	defer logs.Flush()
+
+	// 这个地方可以定义中间件
+	ctx := context.WithValue(context.Background(), logs.KeyRequestID, "123456")
+	ctx = context.WithValue(ctx, logs.KeyUsername, "adu")
+
+	logs.L(ctx).Debug("this is debug message", logs.String("name", "duyong"))
+	logs.L(ctx).Info("this is info message", logs.String("name", "duyong"))
+	logs.L(ctx).Error("this is error message", logs.String("name", "duyong"))
+	logs.L(ctx).Fatal("this is fatal message", logs.String("name", "duyong"))
+}

BIN
logs/doc/img.png


BIN
logs/doc/img_1.png


BIN
logs/doc/img_2.png


BIN
logs/doc/img_3.png


BIN
logs/doc/img_4.png


+ 137 - 0
logs/doc/readme.md

@@ -0,0 +1,137 @@
+# 日志包设计理念
+
+## 1. 如何设计日志包
+
+## 1.1. 基础功能
+
+### 1.1.1 支持基本的日志信息
+
+日志包需要支持基本的日志信息,包括时间戳、文件名、行号、日志级别和日志信息
+### 1.1.2 支持不同的日志级别
+
+通常一个日志包至少要实现 6 个级
+
+![](img.png)
+
+![](img_1.png)
+
+### 1.1.3 支持自定义配置
+
+不同的运行环境,需要不同的日志输出配置
+例如:开发测试环境为了能够方便地 Debug,需要设置日志级别为 Debug 级别; 现网环境为了提高应用程序的性能,则需要设置日志级别为 Info 级别。
+又比如,现网环境为了方便日志采集,通常会输出 JSON 格式的日志;
+开发测试环境为了方便查看日志,会输出 TEXT 格式的日志。
+
+### 1.1.4 支持输出到标准输出和文件
+
+日志总是要被读的,要么输出到标准输出,供开发者实时读取,要么保存到文件,供开发者日后查看。输出到标准输出和保存到文件是一个日志包最基本的功能。
+
+## 1.2 高级功能
+
+### 1.2.1 支持多种日志格式
+
+一个日志包至少需要提供以下两种格式:
+
+* TEXT 格式:TEXT 格式的日志具有良好的可读性,可以方便我们在开发联调阶段查看日志
+* JSON 格式:JSON 格式的日志可以记录更详细的信息,日志中包含一些通用的或自定义的字段,可供日后的查询、分析使用,而且可以很方便地供 filebeat、logstash 这类日志采集工具采集并上报
+
+### 1.2.2 能够按级别分类输出
+
+为了能够快速定位到需要的日志,一个比较好的做法是将日志按级别分类输出,至少错误级别的日志可以输出到独立的文件中。这样,出现问题时,可以直接查找错误文件定位问题.
+
+### 1.2.3 支持结构化日志
+
+结构化日志(Structured Logging),就是使用 JSON 或者其他编码方式使日志结构化,这样可以方便后续使用 Filebeat、Logstash Shipper 等各种工具,对日志进行采集、过滤、分析和查找
+
+### 1.2.4 支持日志轮转
+
+为了防止日志把磁盘空间占满,导致服务器或者程序异常,就需要确保日志大小达到一定量级时,对日志进行切割、压缩,并转存。
+
+[lumberjack](https://github.com/natefinch/lumberjack) 可以支持按大小和日期归档日志
+
+[file-rotatelogs](https://github.com/lestrrat-go/file-rotatelogs)支持按小时数进行日志切割
+
+对于日志轮转功能,不建议在日志包中添加,因为这会增加日志包的复杂度,我更建议的做法是借助其他的工具来实现日志轮转。
+例如,在 Linux 系统中可以使用 Logrotate 来轮转日志。Logrotate 功能强大,是一个专业的日志轮转工具。
+
+### 1.2.5 具备 Hook 能力
+
+Hook 能力可以使我们对日志内容进行自定义处理。例如,当某个级别的日志产生时,发送邮件或者调用告警接口进行告警。
+
+## 1.3 可选功能
+
+### 1.3.1 支持颜色输出
+
+在开发、测试时开启颜色输出,不同级别的日志会被不同颜色标识,这样我们可以很轻松地发现一些 Error、Warn 级别的日志,方便开发调试。发布到生产环境时,可以关闭颜色输出,以提高性能。
+
+### 1.3.2 兼容标准库 log 包
+
+
+### 1.3.3 支持输出到不同的位置
+
+在分布式系统中,一个服务会被部署在多台机器上,这时候如果我们要查看日志,就需要分别登录不同的机器查看,非常麻烦。我们更希望将日志统一投递到 Elasticsearch 上,在 Elasticsearch 上查看日志。
+
+我们还可能需要从日志中分析某个接口的调用次数、某个用户的请求次数等信息,这就需要我们能够对日志进行处理。一般的做法是将日志投递到 Kafka,数据处理服务消费 Kafka 中保存的日志,从而分析出调用次数等信息。
+
+![](img_2.png)
+
+如果日志不支持投递到不同的下游组件,例如 Elasticsearch、Kafka、Fluentd、Logstash 等位置,也可以通过 Filebeat 采集磁盘上的日志文件,进而投递到下游组件。
+
+# 2. 涉及日志包需要关注的点
+
+* 高性能:因为我们要在代码中频繁调用日志包,记录日志,所以日志包的性能是首先要考虑的点,一个性能很差的日志包必然会导致整个应用性能很差。
+* 并发安全:Go 应用程序会大量使用 Go 语言的并发特性,也就意味着需要并发地记录日志,这就需要日志包是并发安全的。
+* 插件化能力:日志包应该能提供一些插件化的能力,比如允许开发者自定义输出格式,自定义存储位置,自定义错误发生时的行为(例如 告警、发邮件等)。插件化的能力不是必需的,因为日志自身的特性就能满足绝大部分的使用需求,例如:输出格式支持 JSON 和 TEXT,存储位置支持标准输出和文件,日志监控可以通过一些旁路系统来实现。
+* 日志参数控制:日志包应该能够灵活地进行配置,初始化时配置或者程序运行时配置。例如:初始化配置可以通过 Init 函数完成,运行时配置可以通过 SetOptions / SetLevel 等函数来完成。
+
+# 3. 如何记录日志
+
+日志并不是越多越好,在实际开发中,经常会遇到一大堆无用的日志,却没有我们需要的日志;或者有效的日志被大量无用的日志淹没,查找起来非常困难。
+
+一个优秀的日志包可以协助我们更好地记录、查看和分析日志,但是如何记录日志决定了我们能否获取到有用的信息。日志包是工具,日志记录才是灵魂。这里,我就来详细讲讲如何记录日志。
+
+## 3.1 在何处打印日志
+
+* 在分支语句处打印日志。在分支语句处打印日志,可以判断出代码走了哪个分支,有助于判断请求的下一跳,继而继续排查问题。
+* 写操作必须打印日志。写操作最可能会引起比较严重的业务故障,写操作打印日志,可以在出问题时找到关键信息。
+* 在循环中打印日志要慎重。如果循环次数过多,会导致打印大量的日志,严重拖累代码的性能,建议的办法是在循环中记录要点,在循环外面总结打印出来。
+* 在错误产生的最原始位置打印日志。对于嵌套的 Error,可在 Error 产生的最初位置打印 Error 日志,上层如果不需要添加必要的信息,可以直接返回下层的 Error
+
+## 3.2 在哪个日志级别打印日志
+
+![](img_3.png)
+
+## 3.3 如何记录日志内容?
+
+* 在记录日志时,不要输出一些敏感信息,例如密码、密钥等。
+* 日志内容应该小写字母开头,以英文点号 . 结尾,例如 log.Info("update user function called.")
+* 为了提高性能,尽可能使用明确的类型,例如使用 log.Warnf("init datastore: %s", err.Error()) 而非 log.Warnf("init datastore: %v", err) 。
+* 根据需要,日志最好包含两个信息。一个是请求 ID(RequestID),是每次请求的唯一 ID,便于从海量日志中过滤出某次请求的日志,可以将请求 ID 放在请求的通用日志字段中。另一个是用户和行为,用于标识谁做了什么。
+* 不要将日志记录在错误的日志级别上。
+
+## 4. 分布式日志解决方案(EFK/ELK)
+
+在实际 Go 项目开发中,为了实现高可用,同一个服务至少需要部署两个实例,通过轮询的负载均衡策略转发请求。
+另外,一个应用又可能包含多个服务。假设我们的应用包含两个服务,每个服务部署两个实例,如果应用出故障,我们可能需要登陆 4(2 x 2)台服务器查看本地的日志文件,定位问题,非常麻烦,会增加故障恢复时间。
+所以在真实的企业场景中,我们会将这些日志统一收集并展示。
+
+在业界,日志的收集、处理和展示,早已经有了一套十分流行的日志解决方案:EFK(Elasticsearch + Filebeat + Kibana)或者 ELK(Elasticsearch + Logstash + Kibana),EFK 可以理解为 ELK 的演进版,把日志收集组件从 Logstash 替换成了 Filebeat。
+用 Filebeat 替换 Logstash,主要原因是 Filebeat 更轻量级,占用的资源更少。关于日志处理架构,你可以参考这张图。
+
+![](img_4.png)
+
+通过 log 包将日志记录在本地文件中(*.log 文件),再通过 Shipper 收集到 Kafka 中。Shipper 可以根据需要灵活选择,常见的 Shipper 有 Logstash Shipper、Flume、Fluentd、Filebeat。其中 Filebeat 
+和 Logstash Shipper 用得最多。Shipper 没有直接将日志投递到 Logstash indexer,或者 Elasticsearch,是因为 Kafka 能够支持更大的吞吐量,起到削峰填谷的作用。
+
+Kafka 中的日志消息会被 Logstash indexer 消费,处理后投递到 Elasticsearch 中存储起来。Elasticsearch 是实时全文搜索和分析引擎,提供搜集、分析、存储数据三大功能。Elasticsearch 中存储的日志,可以通过 Kibana 提供的图形界面来展示。
+Kibana 是一个基于 Web 的图形界面,用于搜索、分析和可视化存储在 Elasticsearch 中的日志数据。
+
+
+Logstash 负责采集、转换和过滤日志。它支持几乎任何类型的日志,包括系统日志、错误日志和自定义应用程序日志。
+Logstash 又分为 Logstash Shipper 和 Logstash indexer。其中,Logstash Shipper 监控并收集日志,并将日志内容发送到 Logstash indexer,然后 Logstash indexer 过滤日志,并将日志提交给 Elasticsearch。    
+
+# 5. 本日志包实现
+
+该 log 包是基于 go.uber.org/zap 包封装而来的
+
+    

+ 15 - 0
logs/encoder.go

@@ -0,0 +1,15 @@
+package logs
+
+import (
+	"time"
+
+	"go.uber.org/zap/zapcore"
+)
+
+func timeEncoder(t time.Time, enc zapcore.PrimitiveArrayEncoder) {
+	enc.AppendString(t.Format("2006-01-02 15:04:05.000"))
+}
+
+func milliSecondsDurationEncoder(d time.Duration, enc zapcore.PrimitiveArrayEncoder) {
+	enc.AppendFloat64(float64(d) / float64(time.Millisecond))
+}

+ 206 - 0
logs/log.go

@@ -0,0 +1,206 @@
+package logs
+
+import (
+	"context"
+	"go.uber.org/zap"
+	"go.uber.org/zap/zapcore"
+	"gogs.tyduyong.com/duyong/dy-pkg/app/options"
+	"sync"
+)
+
+var (
+	std = New(options.NewLogOptions())
+	mu  sync.Mutex
+)
+
+func Init(opts *options.LogOptions) {
+	mu.Lock()
+	defer mu.Unlock()
+	std = New(opts)
+}
+
+func New(opts *options.LogOptions) *zapLogger {
+	if opts == nil {
+		opts = options.NewLogOptions()
+	}
+
+	var zapLevel zapcore.Level
+	if err := zapLevel.UnmarshalText([]byte(opts.Level)); err != nil {
+		zapLevel = zapcore.InfoLevel
+	}
+	encodeLevel := zapcore.CapitalLevelEncoder
+	// when output to local path, with color is forbidden
+	if opts.Format == "console" && opts.EnableColor {
+		encodeLevel = zapcore.CapitalColorLevelEncoder
+	}
+
+	encoderConfig := zapcore.EncoderConfig{
+		MessageKey:     "message",
+		LevelKey:       "level",
+		TimeKey:        "timestamp",
+		NameKey:        "logger",
+		CallerKey:      "caller",
+		StacktraceKey:  "stacktrace",
+		LineEnding:     zapcore.DefaultLineEnding,
+		EncodeLevel:    encodeLevel,
+		EncodeTime:     timeEncoder,
+		EncodeDuration: milliSecondsDurationEncoder,
+		EncodeCaller:   zapcore.ShortCallerEncoder,
+	}
+
+	loggerConfig := &zap.Config{
+		Level:             zap.NewAtomicLevelAt(zapLevel),
+		Development:       opts.Development,
+		DisableCaller:     opts.DisableCaller,
+		DisableStacktrace: opts.DisableStacktrace,
+		Sampling: &zap.SamplingConfig{
+			Initial:    100,
+			Thereafter: 100,
+		},
+		Encoding:         opts.Format,
+		EncoderConfig:    encoderConfig,
+		OutputPaths:      opts.OutputPaths,
+		ErrorOutputPaths: opts.ErrorOutputPaths,
+	}
+
+	var err error
+	l, err := loggerConfig.Build(zap.AddStacktrace(zapcore.PanicLevel), zap.AddCallerSkip(1))
+	if err != nil {
+		panic(err)
+	}
+	logger := &zapLogger{
+		zapLogger: l.Named(opts.Name),
+	}
+
+	zap.RedirectStdLog(l)
+
+	return logger
+}
+
+type zapLogger struct {
+	zapLogger *zap.Logger
+}
+
+func Debug(msg string, fields ...Field) {
+	std.zapLogger.Debug(msg, fields...)
+}
+
+func (l *zapLogger) Debug(msg string, fields ...Field) {
+	l.zapLogger.Debug(msg, fields...)
+}
+
+func Debugf(format string, v ...interface{}) {
+	std.zapLogger.Sugar().Debugf(format, v...)
+}
+
+func (l *zapLogger) Debugf(format string, v ...interface{}) {
+	l.zapLogger.Sugar().Debugf(format, v...)
+}
+
+func Info(msg string, fields ...Field) {
+	std.zapLogger.Info(msg, fields...)
+}
+
+func (l *zapLogger) Info(msg string, fields ...Field) {
+	l.zapLogger.Info(msg, fields...)
+}
+
+func Infof(format string, v ...interface{}) {
+	std.zapLogger.Sugar().Infof(format, v...)
+}
+
+func (l *zapLogger) Infof(format string, v ...interface{}) {
+	l.zapLogger.Sugar().Infof(format, v...)
+}
+
+func Warn(msg string, fields ...Field) {
+	std.zapLogger.Warn(msg, fields...)
+}
+
+func (l *zapLogger) Warn(msg string, fields ...Field) {
+	l.zapLogger.Warn(msg, fields...)
+}
+
+func Warnf(format string, v ...interface{}) {
+	std.zapLogger.Sugar().Warnf(format, v...)
+}
+
+func (l *zapLogger) Warnf(format string, v ...interface{}) {
+	l.zapLogger.Sugar().Warnf(format, v...)
+}
+
+func Error(msg string, fields ...Field) {
+	std.zapLogger.Error(msg, fields...)
+}
+
+func (l *zapLogger) Error(msg string, fields ...Field) {
+	l.zapLogger.Error(msg, fields...)
+}
+
+func Errorf(format string, v ...interface{}) {
+	std.zapLogger.Sugar().Errorf(format, v...)
+}
+
+func (l *zapLogger) Errorf(format string, v ...interface{}) {
+	l.zapLogger.Sugar().Errorf(format, v...)
+}
+
+func Panic(msg string, fields ...Field) {
+	std.zapLogger.Panic(msg, fields...)
+}
+
+func (l *zapLogger) Panic(msg string, fields ...Field) {
+	l.zapLogger.Panic(msg, fields...)
+}
+
+func Panicf(format string, v ...interface{}) {
+	std.zapLogger.Sugar().Panicf(format, v...)
+}
+
+func (l *zapLogger) Panicf(format string, v ...interface{}) {
+	l.zapLogger.Sugar().Panicf(format, v...)
+}
+
+func Fatal(msg string, fields ...Field) {
+	std.zapLogger.Fatal(msg, fields...)
+}
+
+func (l *zapLogger) Fatal(msg string, fields ...Field) {
+	l.zapLogger.Fatal(msg, fields...)
+}
+
+func Fatalf(format string, v ...interface{}) {
+	std.zapLogger.Sugar().Fatalf(format, v...)
+}
+
+func (l *zapLogger) Fatalf(format string, v ...interface{}) {
+	l.zapLogger.Sugar().Fatalf(format, v...)
+}
+
+func L(ctx context.Context) *zapLogger {
+	return std.L(ctx)
+}
+
+func (l *zapLogger) L(ctx context.Context) *zapLogger {
+	lg := l.clone()
+
+	if requestID := ctx.Value(KeyRequestID); requestID != nil {
+		lg.zapLogger = lg.zapLogger.With(zap.Any(KeyRequestID, requestID))
+	}
+	if username := ctx.Value(KeyUsername); username != nil {
+		lg.zapLogger = lg.zapLogger.With(zap.Any(KeyUsername, username))
+	}
+
+	return lg
+}
+
+func (l *zapLogger) clone() *zapLogger {
+	c := *l
+	return &c
+}
+
+func Flush() { std.Flush() }
+
+func (l *zapLogger) Flush() {
+	_ = l.zapLogger.Sync()
+}

+ 23 - 0
logs/logger.go

@@ -0,0 +1,23 @@
+package logs
+
+type Logger interface {
+	Debug(msg string, fields ...Field)
+	Debugf(format string, v ...interface{})
+
+	Info(msg string, fields ...Field)
+	Infof(format string, v ...interface{})
+
+	Warn(msg string, fields ...Field)
+	Warnf(format string, v ...interface{})
+
+	Error(msg string, fields ...Field)
+	Errorf(format string, v ...interface{})
+
+	Panic(msg string, fields ...Field)
+	Panicf(format string, v ...interface{})
+
+	Fatal(msg string, fields ...Field)
+	Fatalf(format string, v ...interface{})
+
+	Flush()
+}

+ 91 - 0
logs/types.go

@@ -0,0 +1,91 @@
+package logs
+
+import (
+	"go.uber.org/zap"
+	"go.uber.org/zap/zapcore"
+)
+
+// 通用的日志字段
+const (
+	KeyRequestID   string = "requestID"
+	KeyUsername    string = "username"
+	KeyWatcherName string = "watcher"
+)
+
+// Field zap字段别名
+type Field = zapcore.Field
+
+// Level zap日志级别别名
+type Level = zapcore.Level
+
+var (
+	// DebugLevel logs are typically voluminous, and are usually disabled in
+	// production.
+	DebugLevel = zapcore.DebugLevel
+	// InfoLevel is the default logging priority.
+	InfoLevel = zapcore.InfoLevel
+	// WarnLevel logs are more important than Info, but don't need individual
+	// human review.
+	WarnLevel = zapcore.WarnLevel
+	// ErrorLevel logs are high-priority. If an application is running smoothly,
+	// it shouldn't generate any error-level logs.
+	ErrorLevel = zapcore.ErrorLevel
+	// PanicLevel logs a message, then panics.
+	PanicLevel = zapcore.PanicLevel
+	// FatalLevel logs a message, then calls os.Exit(1).
+	FatalLevel = zapcore.FatalLevel
+)
+
+// Alias for zap type functions.
+var (
+	Any         = zap.Any
+	Array       = zap.Array
+	Object      = zap.Object
+	Binary      = zap.Binary
+	Bool        = zap.Bool
+	Bools       = zap.Bools
+	ByteString  = zap.ByteString
+	ByteStrings = zap.ByteStrings
+	Complex64   = zap.Complex64
+	Complex64s  = zap.Complex64s
+	Complex128  = zap.Complex128
+	Complex128s = zap.Complex128s
+	Duration    = zap.Duration
+	Durations   = zap.Durations
+	Err         = zap.Error
+	Errors      = zap.Errors
+	Float32     = zap.Float32
+	Float32s    = zap.Float32s
+	Float64     = zap.Float64
+	Float64s    = zap.Float64s
+	Int         = zap.Int
+	Ints        = zap.Ints
+	Int8        = zap.Int8
+	Int8s       = zap.Int8s
+	Int16       = zap.Int16
+	Int16s      = zap.Int16s
+	Int32       = zap.Int32
+	Int32s      = zap.Int32s
+	Int64       = zap.Int64
+	Int64s      = zap.Int64s
+	Namespace   = zap.Namespace
+	Reflect     = zap.Reflect
+	Stack       = zap.Stack
+	String      = zap.String
+	Stringer    = zap.Stringer
+	Strings     = zap.Strings
+	Time        = zap.Time
+	Times       = zap.Times
+	Uint        = zap.Uint
+	Uints       = zap.Uints
+	Uint8       = zap.Uint8
+	Uint8s      = zap.Uint8s
+	Uint16      = zap.Uint16
+	Uint16s     = zap.Uint16s
+	Uint32      = zap.Uint32
+	Uint32s     = zap.Uint32s
+	Uint64      = zap.Uint64
+	Uint64s     = zap.Uint64s
+	Uintptr     = zap.Uintptr
+	Uintptrs    = zap.Uintptrs
+)