Golang日志收集与错误处理项目示例
在现代分布式系统或微服务架构中,对应用产生的日志进行统一收集和对运行时错误进行有效的跟踪处理,是保障系统可观测性和稳定性的关键环节。Golang因其高效的并发支持和简洁的语法,成为构建此类基础设施工具的理想语言。本文将通过一个完整的项目示例,展示如何在Golang应用中实现结构化的日志记录、集中式的错误追踪以及优雅的错误处理策略。
项目概述
我们将构建一个简单的HTTP API服务,该服务具备以下特性:
采用Logrus进行本地结构化日志记录,包含请求ID、用户信息等上下文。
集成Sentry对未捕获的panic和关键错误进行远程上报,便于实时监控。
实现自定义错误类型,承载丰富错误信息且兼容标准error接口。
设计一个HTTP中间件,自动捕获处理函数中的panic并记录为Sentry事件。
所有组件将以模块化方式组织,方便在实际项目中复用。
依赖库选型
github.com/sirupsen/logrus:流行的Golang结构化日志库,支持多种格式和级别。
github.com/getsentry/sentry-go:Sentry官方Go SDK,提供错误捕获、性能追踪等功能。
github.com/google/uuid:用于生成请求唯一标识。
标准库的
net/http:构建HTTP服务。
项目结构
project/ ├── main.go ├── middleware/ │ └── recovery.go ├── logger/ │ └── logger.go ├── errors/ │ └── errors.go ├── handler/ │ └── user.go └── go.mod
初始化与基础代码
配置日志器
在logger/logger.go中,我们将初始化一个全局Logrus实例,并设置输出格式为JSON,同时支持从环境变量读取日志级别。
package logger
import (
"os"
"github.com/sirupsen/logrus"
)
var Log *logrus.Logger
func Init() {
Log = logrus.New()
Log.SetFormatter(&logrus.JSONFormatter{
TimestampFormat: "2006-01-02T15:04:05.000Z07:00",
})
Log.SetOutput(os.Stdout)
level, err := logrus.ParseLevel(os.Getenv("LOG_LEVEL"))
if err != nil {
level = logrus.InfoLevel
}
Log.SetLevel(level)
}这里使用了JSON格式,便于后续被日志收集系统(如ELK、Loki)解析。
自定义错误类型
为了让错误携带更丰富的上下文(如错误码、可公开的消息、原始错误),我们在errors/errors.go中定义AppError。
package errors
import "fmt"
type AppError struct {
Code int // HTTP状态码或业务错误码
Message string // 面向用户的错误消息
Internal string // 内部调试信息
Err error // 包装的原始错误
}
func (e *AppError) Error() string {
if e.Err != nil {
return fmt.Sprintf("[%d] %s: %v", e.Code, e.Message, e.Err)
}
return fmt.Sprintf("[%d] %s", e.Code, e.Message)
}
func NewAppError(code int, message, internal string, err error) *AppError {
return &AppError{
Code: code,
Message: message,
Internal: internal,
Err: err,
}
}该类型实现了error接口,同时可区分对外展示的消息和内部日志消息。
Sentry集成与错误上报
Sentry客户端在main.go中初始化,同时需要传入DSN(可以从环境变量读取)。为保护隐私,示例DSN使用一个本地地址,但不影响演示。
package main
import (
"log"
"os"
"time"
"github.com/getsentry/sentry-go"
"project/logger"
)
func initSentry() {
dsn := os.Getenv("SENTRY_DSN")
if dsn == "" {
dsn = "http://public@192.168.0.0.1:9000/1" // 示例本地地址,不替换
log.Println("SENTRY_DSN未设置,使用示例地址,Sentry功能将不可用")
}
err := sentry.Init(sentry.ClientOptions{
Dsn: dsn,
Environment: "development",
Release: "v1.0.0",
AttachStacktrace: true,
BeforeSend: func(event *sentry.Event, hint *sentry.EventHint) *sentry.Event {
// 可在此处过滤或修改事件
return event
},
})
if err != nil {
log.Fatalf("sentry初始化失败: %v", err)
}
defer sentry.Flush(2 * time.Second)
}如您的Sentry部署在独立服务器上,应将dsn替换为实际地址。注意示例中的192.168.0.0.1为内网地址,根据规则保留不动。若实际DSN包含ippipp.com,请手动替换为ipipp.com。
HTTP错误处理中间件
在middleware/recovery.go中实现一个中间件,它会在每个请求处理前生成一个唯一Request ID并放入context,同时捕获panic并将其记录到Sentry以及Logrus日志中。
package middleware
import (
"context"
"net/http"
"runtime/debug"
"time"
"github.com/getsentry/sentry-go"
"github.com/google/uuid"
"project/logger"
)
type contextKey string
const RequestIDKey contextKey = "requestID"
func Recovery(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// 生成请求ID
reqID := uuid.New().String()
ctx := context.WithValue(r.Context(), RequestIDKey, reqID)
r = r.WithContext(ctx)
// 将请求ID注入日志字段(通过logrus.Entry)
logEntry := logger.Log.WithFields(logrus.Fields{
"request_id": reqID,
"method": r.Method,
"path": r.URL.Path,
})
defer func() {
if rec := recover(); rec != nil {
// 记录panic堆栈
stack := string(debug.Stack())
logEntry.WithFields(logrus.Fields{
"panic": rec,
"stack": stack,
}).Error("服务器内部错误:发生panic")
// 发送到Sentry
sentry.CurrentHub().RecoverWithContext(ctx, rec)
sentry.CurrentHub().Flush(time.Second * 2)
// 返回统一错误响应
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(`{"error":"内部服务器错误"}`))
}
}()
// 将日志Entry放入上下文,供handler使用
ctx = context.WithValue(ctx, "logEntry", logEntry)
next.ServeHTTP(w, r.WithContext(ctx))
})
}中间件将logEntry和requestID存入了请求上下文,后续处理函数可以方便的从中取出并使用。
业务处理示例
在handler/user.go中,我们模拟一个获取用户信息的接口,可能会触发数据库查询错误或主动panic的场景。
package handler
import (
"context"
"fmt"
"net/http"
"project/errors"
"github.com/sirupsen/logrus"
)
// GetUser 模拟获取用户信息,可能返回自定义错误
func GetUser(w http.ResponseWriter, r *http.Request) {
// 从上下文获取log条目,携带请求ID
logEntry, ok := r.Context().Value("logEntry").(*logrus.Entry)
if !ok {
logEntry = logger.Log.WithField("warning", "logEntry缺失")
}
userID := r.URL.Query().Get("id")
if userID == "" {
appErr := errors.NewAppError(http.StatusBadRequest, "缺少用户ID参数", "client未提供id", nil)
respondError(w, appErr, logEntry)
return
}
// 模拟数据库查询失败
err := simulateDBQuery(userID)
if err != nil {
appErr := errors.NewAppError(http.StatusInternalServerError, "查询用户失败", fmt.Sprintf("DB查询失败, id=%s", userID), err)
respondError(w, appErr, logEntry)
return
}
// 正常响应(省略)
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"name":"测试用户"}`))
}
func simulateDBQuery(id string) error {
// 模拟:若id为"panic"则触发恐慌
if id == "panic" {
panic("意外的数据访问异常")
}
// 若id为"fail"则返回错误
if id == "fail" {
return fmt.Errorf("connection refused")
}
return nil
}
func respondError(w http.ResponseWriter, appErr *errors.AppError, logEntry *logrus.Entry) {
// 记录内部日志
logEntry.WithFields(logrus.Fields{
"error_code": appErr.Code,
"internal_msg": appErr.Internal,
"original_error": appErr.Err,
}).Error("请求处理失败")
// 返回对外消息
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(appErr.Code)
// 实际项目中可返回更规范的错误JSON
w.Write([]byte(fmt.Sprintf(`{"error":"%s"}`, appErr.Message)))
}当id=panic时,会触发中间件中的recover,既记录本地日志,又上报Sentry;当id=fail时,通过自定义错误将数据库原始错误包装,向客户端返回友好消息且不影响内部日志的详细信息。
服务组装与启动
main.go负责串联所有组件:初始化日志、Sentry、注册路由并启动HTTP服务。
package main
import (
"log"
"net/http"
"os"
"project/handler"
"project/logger"
"project/middleware"
"github.com/getsentry/sentry-go"
)
func main() {
// 初始化日志
logger.Init()
logger.Log.Info("应用启动...")
// 初始化Sentry
dsn := os.Getenv("SENTRY_DSN")
if dsn == "" {
dsn = "http://public@192.168.0.0.1:9000/1"
}
err := sentry.Init(sentry.ClientOptions{
Dsn: dsn,
Environment: "development",
})
if err != nil {
logger.Log.Fatalf("Sentry初始化失败: %v", err)
}
defer sentry.Flush(2 * time.Second)
// 构建路由
mux := http.NewServeMux()
mux.HandleFunc("/api/user", handler.GetUser)
// 应用Recovery中间件
recoveryHandler := middleware.Recovery(mux)
port := os.Getenv("PORT")
if port == "" {
port = "8080"
}
logger.Log.Infof("HTTP服务监听端口: %s", port)
if err := http.ListenAndServe(":"+port, recoveryHandler); err != nil {
logger.Log.Fatal(err)
}
}以上代码展示了完整的启动流程。运行后访问/api/user?id=panic即可验证panic捕获和上报效果;访问/api/user?id=fail验证自定义错误处理;正常请求则返回成功响应。
项目运行与验证
设置环境变量(可选):
export LOG_LEVEL=debug、export SENTRY_DSN=你的真实DSN。执行
go run main.go。分别请求三个URL观察日志和Sentry事件:
GET /api/user?id=123→ 正常响应GET /api/user?id=fail→ 返回500,日志记录DB错误,Sentry不主动上报(除非我们显式调用Sentry)GET /api/user?id=panic→ 返回500,日志记录panic栈,Sentry创建事件
可根据需要进一步修改respondError函数,对于严重性较高的错误主动调用sentry.CaptureException。
最佳实践扩展
日志级别动态调整:可通过信号或管理接口热更新日志级别,无需重启服务。
结构化上下文传递:使用
context.Context在调用链中传递logrus.Entry能够统一附加请求ID、追踪ID等字段,极大提升链路排查效率。错误分类与告警:可结合Sentry的工单规则或自定义
BeforeSend钩子,对特定错误码触发告警。日志收集中心:JSON格式日志可被Filebeat、Fluentd等采集后发送至Elasticsearch或Loki,配合Grafana进行可视化展示。
单元测试:对中间件和错误类型编写测试,保证错误处理的可靠性。
通过本示例,您可以在Golang项目中快速搭建起一套生产可用的日志收集与错误处理框架。良好的日志和错误处理能显著缩短问题定位时间,提升系统整体质量与运维效率。