日志处理:构建可观测的应用

学习 Go 的日志处理,从标准库到结构化日志,构建可观测的应用

日志处理:构建可观测的应用

日志是应用程序的眼睛。良好的日志系统能帮助我们调试问题、监控系统状态、追踪用户行为。Go 提供了多种日志解决方案,从简单的标准库到强大的第三方框架。

今天我们就来全面学习 Go 的日志处理。

标准库 log

Go 的标准库 log 提供了基础的日志功能:

package main

import (
	"log"
	"os"
)

func main() {
	// 基本使用
	log.Println("这是一条普通日志")
	log.Printf("用户 %s 登录了系统", "张三")
	
	// 设置日志前缀
	log.SetPrefix("[MyApp] ")
	log.Println("带前缀的日志")
	
	// 设置日志标志
	log.SetFlags(log.Ldate | log.Ltime | log.Lshortfile)
	log.Println("带日期、时间和文件名的日志")
	
	// 输出到文件
	file, err := os.OpenFile("app.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
	if err != nil {
		log.Fatal("打开日志文件失败:", err)
	}
	defer file.Close()
	
	logger := log.New(file, "[FILE] ", log.LstdFlags)
	logger.Println("这条日志会写入文件")
	
	// Fatal 和 Panic
	// log.Fatal("程序会退出")  // 调用 os.Exit(1)
	// log.Panic("程序会 panic") // 调用 panic()
}

日志标志

const (
	Ldate         = 1 << iota     // 日期:2009/01/23
	Ltime                         // 时间:01:23:23
	Lmicroseconds                 // 微秒:01:23:23.123123
	Llongfile                     // 完整文件名和行号:/a/b/c/d.go:23
	Lshortfile                    // 短文件名和行号:d.go:23
	LUTC                          // 使用 UTC 时间
	Lmsgprefix                    // 将前缀移到消息前面
	LstdFlags     = Ldate | Ltime // 标准标志
)

第三方日志库

标准库 log 功能简单,生产环境通常使用更强大的第三方库。

logrus

logrus 是最流行的结构化日志库:

package main

import (
	"os"
	
	"github.com/sirupsen/logrus"
)

func main() {
	// 基本使用
	logrus.Info("这是一条 Info 日志")
	logrus.Warn("这是一条 Warning 日志")
	logrus.Error("这是一条 Error 日志")
	
	// 带字段的结构化日志
	logrus.WithFields(logrus.Fields{
		"user_id": 123,
		"username": "张三",
		"action":   "login",
	}).Info("用户登录")
	
	// 设置日志级别
	logrus.SetLevel(logrus.DebugLevel)
	logrus.Debug("这条 Debug 日志会显示")
	
	// 设置输出格式
	logrus.SetFormatter(&logrus.JSONFormatter{})
	logrus.Info("JSON 格式的日志")
	
	// 输出到文件
	file, _ := os.OpenFile("app.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
	logrus.SetOutput(file)
	logrus.Info("这条日志会写入文件")
}

zap

zap 是 Uber 开发的高性能日志库:

package main

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

func main() {
	// 创建 logger
	logger, _ := zap.NewProduction()
	defer logger.Sync()
	
	// 基本使用
	logger.Info("这是一条 Info 日志")
	logger.Warn("这是一条 Warning 日志")
	logger.Error("这是一条 Error 日志")
	
	// 带字段
	logger.Info("用户登录",
		zap.Int("user_id", 123),
		zap.String("username", "张三"),
		zap.String("action", "login"),
	)
	
	// 开发环境 logger(更易读)
	devLogger, _ := zap.NewDevelopment()
	devLogger.Info("开发环境日志")
	
	// 自定义配置
	config := zap.Config{
		Level:       zap.NewAtomicLevelAt(zap.DebugLevel),
		Development: true,
		Encoding:    "console",
		OutputPaths: []string{"stdout"},
	}
	customLogger, _ := config.Build()
	customLogger.Debug("自定义配置的日志")
}

zerolog

zerolog 是另一个高性能的选择:

package main

import (
	"os"
	
	"github.com/rs/zerolog"
	"github.com/rs/zerolog/log"
)

func main() {
	// 基本使用
	log.Info().Msg("这是一条 Info 日志")
	log.Warn().Msg("这是一条 Warning 日志")
	log.Error().Msg("这是一条 Error 日志")
	
	// 带字段
	log.Info().
		Int("user_id", 123).
		Str("username", "张三").
		Str("action", "login").
		Msg("用户登录")
	
	// 设置全局级别
	zerolog.SetGlobalLevel(zerolog.DebugLevel)
	log.Debug().Msg("这条 Debug 日志会显示")
	
	// 人类友好的输出
	log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stdout})
	log.Info().Msg("易读的日志格式")
}

结构化日志

结构化日志使用键值对而不是纯文本,便于日志分析和检索:

package main

import (
	"github.com/sirupsen/logrus"
)

type Logger struct {
	*logrus.Entry
}

func NewLogger(component string) *Logger {
	return &Logger{
		Entry: logrus.WithField("component", component),
	}
}

func (l *Logger) WithRequestID(requestID string) *Logger {
	return &Logger{
		Entry: l.WithField("request_id", requestID),
	}
}

func (l *Logger) WithUserID(userID int) *Logger {
	return &Logger{
		Entry: l.WithField("user_id", userID),
	}
}

func main() {
	// 创建带组件标识的 logger
	logger := NewLogger("UserService")
	
	// 在请求处理中添加上下文
	requestLogger := logger.WithRequestID("req-123").WithUserID(456)
	
	requestLogger.Info("开始处理请求")
	// 输出: {"component":"UserService","request_id":"req-123","user_id":456,"level":"info","msg":"开始处理请求"}
	
	// 处理业务逻辑
	requestLogger.WithField("action", "query").Info("查询用户信息")
	
	requestLogger.Info("请求处理完成")
}

日志中间件

在 HTTP 服务器中自动记录请求日志:

package main

import (
	"net/http"
	"time"
	
	"github.com/sirupsen/logrus"
)

type responseWriter struct {
	http.ResponseWriter
	statusCode int
}

func (rw *responseWriter) WriteHeader(code int) {
	rw.statusCode = code
	rw.ResponseWriter.WriteHeader(code)
}

func loggingMiddleware(logger *logrus.Logger) func(http.Handler) http.Handler {
	return func(next http.Handler) http.Handler {
		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			start := time.Now()
			
			// 创建带请求信息的 logger
			reqLogger := logger.WithFields(logrus.Fields{
				"method":     r.Method,
				"path":       r.URL.Path,
				"remote_addr": r.RemoteAddr,
				"user_agent": r.UserAgent(),
			})
			
			reqLogger.Info("请求开始")
			
			// 包装 ResponseWriter 以捕获状态码
			wrapped := &responseWriter{w, http.StatusOK}
			
			// 调用下一个处理器
			next.ServeHTTP(wrapped, r)
			
			// 记录请求完成
			duration := time.Since(start)
			reqLogger.WithFields(logrus.Fields{
				"status":   wrapped.statusCode,
				"duration": duration.String(),
			}).Info("请求完成")
		})
	}
}

func main() {
	logger := logrus.New()
	logger.SetFormatter(&logrus.JSONFormatter{})
	
	mux := http.NewServeMux()
	mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		w.Write([]byte("Hello, World!"))
	})
	
	handler := loggingMiddleware(logger)(mux)
	
	http.ListenAndServe(":8080", handler)
}

日志轮转

使用 lumberjack 实现日志轮转:

package main

import (
	"github.com/sirupsen/logrus"
	"gopkg.in/natefinish/lumberjack.v2"
)

func main() {
	logger := logrus.New()
	
	// 配置日志轮转
	logger.SetOutput(&lumberjack.Logger{
		Filename:   "app.log",
		MaxSize:    100,  // MB
		MaxBackups: 30,   // 保留旧文件数量
		MaxAge:     7,    // 保留天数
		Compress:   true, // 压缩旧文件
	})
	
	logger.SetFormatter(&logrus.JSONFormatter{})
	
	// 使用 logger
	for i := 0; i < 1000; i++ {
		logger.WithField("iteration", i).Info("循环日志")
	}
}

最佳实践

1. 使用合适的日志级别

// Debug:调试信息,开发环境使用
logger.Debug("数据库连接池状态", zap.Int("active", 5), zap.Int("idle", 10))

// Info:正常操作信息
logger.Info("用户登录成功", zap.Int("user_id", 123))

// Warn:警告信息,可能的问题
logger.Warn("数据库查询慢", zap.Duration("duration", 2*time.Second))

// Error:错误信息,但不影响系统运行
logger.Error("发送邮件失败", zap.Error(err), zap.String("email", "user@example.com"))

// Fatal:严重错误,程序会退出
logger.Fatal("数据库连接失败", zap.Error(err))

2. 避免敏感信息

// ❌ 不要记录敏感信息
logger.Info("用户登录", zap.String("password", password))

// ✅ 只记录必要信息
logger.Info("用户登录", zap.Int("user_id", userID), zap.String("ip", clientIP))

3. 使用上下文传播

type contextKey string

const loggerKey contextKey = "logger"

func WithLogger(ctx context.Context, logger *logrus.Entry) context.Context {
	return context.WithValue(ctx, loggerKey, logger)
}

func LoggerFromContext(ctx context.Context) *logrus.Entry {
	logger, ok := ctx.Value(loggerKey).(*logrus.Entry)
	if !ok {
		return logrus.NewEntry(logrus.StandardLogger())
	}
	return logger
}

func handleRequest(w http.ResponseWriter, r *http.Request) {
	// 创建带请求 ID 的 logger
	requestID := r.Header.Get("X-Request-ID")
	logger := logrus.WithField("request_id", requestID)
	
	// 将 logger 放入 context
	ctx := WithLogger(r.Context(), logger)
	
	// 在后续处理中使用
	processRequest(ctx)
}

func processRequest(ctx context.Context) {
	logger := LoggerFromContext(ctx)
	logger.Info("处理请求")
}

4. 性能优化

// ❌ 即使日志级别不够,也会执行字符串拼接
logger.Debug("用户信息: " + user.String())

// ✅ 使用 WithFields 延迟求值
logger.WithField("user", user).Debug("用户信息")

// ✅ 使用 zap 的懒加载
logger.Debug("用户信息", zap.Stringer("user", user))

实战:完整的日志系统

package main

import (
	"context"
	"net/http"
	"os"
	"time"
	
	"github.com/google/uuid"
	"github.com/sirupsen/logrus"
	"gopkg.in/natefinish/lumberjack.v2"
)

type Logger struct {
	*logrus.Logger
}

func NewLogger() *Logger {
	logger := logrus.New()
	
	// 设置输出
	if os.Getenv("ENV") == "production" {
		logger.SetOutput(&lumberjack.Logger{
			Filename:   "/var/log/myapp/app.log",
			MaxSize:    100,
			MaxBackups: 30,
			MaxAge:     7,
			Compress:   true,
		})
		logger.SetFormatter(&logrus.JSONFormatter{})
	} else {
		logger.SetFormatter(&logrus.TextFormatter{
			FullTimestamp: true,
		})
	}
	
	// 设置级别
	level, err := logrus.ParseLevel(os.Getenv("LOG_LEVEL"))
	if err != nil {
		level = logrus.InfoLevel
	}
	logger.SetLevel(level)
	
	return &Logger{logger}
}

func (l *Logger) WithContext(ctx context.Context) *logrus.Entry {
	entry := l.Logger.WithContext(ctx)
	
	if requestID, ok := ctx.Value("request_id").(string); ok {
		entry = entry.WithField("request_id", requestID)
	}
	
	if userID, ok := ctx.Value("user_id").(int); ok {
		entry = entry.WithField("user_id", userID)
	}
	
	return entry
}

type contextKey string

const loggerKey contextKey = "logger"

func LoggingMiddleware(logger *Logger) func(http.Handler) http.Handler {
	return func(next http.Handler) http.Handler {
		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			// 生成请求 ID
			requestID := r.Header.Get("X-Request-ID")
			if requestID == "" {
				requestID = uuid.New().String()
			}
			
			// 创建请求上下文
			ctx := context.WithValue(r.Context(), "request_id", requestID)
			
			// 创建请求 logger
			reqLogger := logger.WithContext(ctx).WithFields(logrus.Fields{
				"method":      r.Method,
				"path":        r.URL.Path,
				"remote_addr": r.RemoteAddr,
			})
			
			// 将 logger 放入 context
			ctx = context.WithValue(ctx, loggerKey, reqLogger)
			
			start := time.Now()
			reqLogger.Info("请求开始")
			
			// 添加请求 ID 到响应头
			w.Header().Set("X-Request-ID", requestID)
			
			// 调用下一个处理器
			next.ServeHTTP(w, r.WithContext(ctx))
			
			duration := time.Since(start)
			reqLogger.WithField("duration", duration.String()).Info("请求完成")
		})
	}
}

var logger = NewLogger()

func handler(w http.ResponseWriter, r *http.Request) {
	// 从 context 获取 logger
	reqLogger := r.Context().Value(loggerKey).(*logrus.Entry)
	
	reqLogger.Info("处理业务逻辑")
	
	// 模拟处理
	time.Sleep(100 * time.Millisecond)
	
	reqLogger.Info("业务逻辑完成")
	
	w.Write([]byte("Hello, World!"))
}

func main() {
	mux := http.NewServeMux()
	mux.HandleFunc("/", handler)
	
	handler := LoggingMiddleware(logger)(mux)
	
	logger.Info("服务器启动在 :8080")
	http.ListenAndServe(":8080", handler)
}

小结

今天我们学习了 Go 的日志处理:

  1. 标准库 log:基础的日志功能
  2. 第三方库:logrus、zap、zerolog
  3. 结构化日志:键值对格式的日志
  4. 日志中间件:自动记录 HTTP 请求
  5. 日志轮转:使用 lumberjack
  6. 最佳实践:级别选择、敏感信息、性能优化

良好的日志系统是构建可观测应用的基础。选择合适的日志库,遵循最佳实践,能让你的应用更易于调试和维护。

练习时间

  1. 实现一个日志聚合系统,收集多个服务的日志
  2. 创建一个日志分析工具,统计错误频率和模式
  3. 实现日志的异步写入,提升性能
  4. 构建一个日志查询 API,支持按时间、级别、字段过滤

我们下篇见!

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页