Appearance
日志、监控与链路追踪
本篇讲解 go-zero 的可观测性体系,包括日志(logx)、监控(Prometheus)和链路追踪(OpenTelemetry + Jaeger)。可观测性是微服务稳定运行的基础:日志用于排查问题,监控用于发现异常,链路追踪用于定位跨服务调用的性能瓶颈。go-zero 把这三者深度集成到框架中,开发者只需配置即可启用。
一、go-zero 日志
go-zero 的日志包是 logx,位于 core/logx。它提供结构化日志、多级别输出、多种输出目标。
1. logx 包
基本用法:
go
import "github.com/zeromicro/go-zero/core/logx"
logx.Info("hello world")
logx.Infof("user %s logged in", username)
logx.Error("something went wrong")
logx.Errorf("query failed: %v", err)
// 结构化字段
logx.WithFields(logx.Field("userId", 1001), logx.Field("action", "login")).Info("user action")
// 带 context(推荐,自动关联 TraceID)
logx.WithContext(ctx).Info("processing request")
logx.WithContext(ctx).Errorf("call rpc failed: %v", err)2. 结构化日志
logx 默认输出 JSON 格式,方便日志系统(ELK、Loki)采集和查询:
json
{"@timestamp":"2026-08-01T10:00:00.000Z","@level":"info","@caller":"user/logic.go:42","@message":"user login","userId":1001,"trace":"abc123"}字段说明:
@timestamp:时间戳@level:日志级别@caller:调用位置(文件:行号)@message:日志内容trace:TraceID(自动注入)- 其他自定义字段
3. 日志级别
logx 支持以下级别(从低到高):
| 级别 | 方法 | 用途 |
|---|---|---|
debug | logx.Debug | 调试信息,生产环境关闭 |
info | logx.Info | 一般信息,业务关键节点 |
slow | logx.Slow | 慢请求日志(go-zero 自动记录) |
error | logx.Error | 错误,需要关注 |
severe | logx.Severe | 严重错误,影响服务 |
disable | - | 关闭日志 |
设置级别:
yaml
Log:
Level: info # debug / info / error / severe或代码设置:
go
logx.SetLevel(logx.InfoLevel)4. 日志输出:文件 + 控制台
logx 支持多种输出模式,通过配置控制:
yaml
Log:
ServiceName: user-api # 服务名,写入日志
Mode: file # file / console / volume / elasticsearch
Path: logs # 日志目录(Mode=file 时)
Level: info # 日志级别
Encoding: json # json / plain
KeepDays: 7 # 保留天数
Compress: true # 是否压缩
MaxContentLength: 0 # 单条日志最大长度(0 不限制)
Stat: true # 是否记录统计日志
StackCooldownMillis: 100 # 同一堆栈日志冷却时间Mode 说明:
console:输出到控制台(开发环境)file:输出到文件(生产环境)volume:输出到挂载卷(K8s 场景)elasticsearch:直接输出到 ES(需额外配置)
文件输出会按天滚动,文件名格式:logs/user-api.log.20260801。配合 Compress: true 会自动压缩历史日志。
5. 同时输出到文件和控制台
如果需要同时输出到文件和控制台(如容器化部署),可以自定义:
go
import "github.com/zeromicro/go-zero/core/logx"
func initLog() {
// 文件 writer
fileWriter := logx.NewWriter(&fileLogger{path: "logs"})
// 同时写控制台和文件
logx.SetOutputs(logx.NewComboWriter(logx.NewConsoleWriter(), fileWriter))
}
type fileLogger struct {
path string
file *os.File
}
func (l *fileLogger) Write(p []byte) (int, error) {
if l.file == nil {
os.MkdirAll(l.path, 0755)
f, err := os.OpenFile(filepath.Join(l.path, "app.log"),
os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
if err != nil {
return 0, err
}
l.file = f
}
return l.file.Write(p)
}6. 请求日志自动记录
go-zero 的 rest 框架会自动记录每个 HTTP 请求的访问日志,包括:
- 请求方法、路径
- 响应状态码
- 请求耗时
- 客户端 IP
- TraceID
日志示例:
json
{"@timestamp":"...","@level":"info","@caller":"rest/handler.go:78","@message":"[HTTP] 200 - POST /api/v1/user (127.0.0.1) 12.3ms","trace":"abc123","span":"def456"}慢请求会单独记录为 slow 级别:
json
{"@level":"slow","@message":"[HTTP] 200 - GET /api/v1/user/1001 ... 1500ms"}RPC 调用也会自动记录,包括方法名、耗时、错误。
7. 业务日志规范
推荐的日志规范:
go
// ✅ 推荐:带 context,结构化字段
logx.WithContext(l.ctx).WithFields(
logx.Field("userId", userId),
logx.Field("action", "create_order"),
logx.Field("productId", productId),
).Info("order created")
// ✅ 推荐:错误日志带堆栈
logx.WithContext(l.ctx).Errorf("query user failed, id=%d, err=%v", id, err)
// ❌ 避免:无 context,无法关联 TraceID
logx.Infof("user %d login", userId)
// ❌ 避免:敏感信息
logx.Infof("user password: %s", password) // 不要记录密码二、Prometheus 监控
go-zero 内置 Prometheus 指标采集,无需额外开发即可获得关键监控数据。
1. go-zero 内置 Prometheus 指标
配置开启:
yaml
Prometheus:
Host: 0.0.0.0
Port: 9100
Path: /metrics启动后访问 http://localhost:9100/metrics 可以看到指标。
主要指标:
HTTP 服务:
| 指标 | 含义 |
|---|---|
http_server_requests_duration_ms | 请求处理耗时(直方图) |
http_server_requests_total | 请求总数(按 method/path/code) |
http_server_requests_code_total | 按状态码统计 |
go_redis_resource_pool | Redis 连接池状态 |
RPC 服务:
| 指标 | 含义 |
|---|---|
rpc_server_requests_duration_ms | RPC 调用耗时 |
rpc_server_requests_total | RPC 调用总数 |
rpc_client_requests_duration_ms | 客户端调用耗时 |
rpc_client_requests_total | 客户端调用总数 |
运行时:
| 指标 | 含义 |
|---|---|
go_memstats_alloc_bytes | 内存分配 |
go_goroutines | goroutine 数量 |
go_threads | 线程数 |
process_cpu_seconds_total | CPU 使用 |
2. 自定义指标
业务可以自定义 Prometheus 指标:
go
// internal/metrics/metrics.go
package metrics
import (
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
)
var (
// 订单创建计数器
OrderCreatedTotal = promauto.NewCounterVec(
prometheus.CounterOpts{
Name: "order_created_total",
Help: "Total number of orders created",
},
[]string{"status", "type"}, // 成功/失败,订单类型
)
// 订单金额直方图
OrderAmount = promauto.NewHistogramVec(
prometheus.HistogramOpts{
Name: "order_amount",
Help: "Order amount distribution",
Buckets: []float64{10, 50, 100, 500, 1000, 5000},
},
[]string{"currency"},
)
// 在线用户数(Gauge)
OnlineUsers = promauto.NewGauge(
prometheus.GaugeOpts{
Name: "online_users",
Help: "Current online users",
},
)
)在业务代码中使用:
go
// 创建订单
func (l *CreateOrderLogic) CreateOrder(req *types.CreateOrderRequest) (*types.CreateOrderResponse, error) {
err := l.doCreateOrder(req)
if err != nil {
metrics.OrderCreatedTotal.WithLabelValues("failed", req.Type).Inc()
return nil, err
}
metrics.OrderCreatedTotal.WithLabelValues("success", req.Type).Inc()
metrics.OrderAmount.WithLabelValues("CNY").Observe(req.Amount)
return resp, nil
}3. Grafana 仪表盘配置
在 Grafana 中添加 Prometheus 数据源后,创建仪表盘。常用 PromQL:
QPS(每秒请求数):
promql
sum(rate(http_server_requests_total[1m])) by (method, path)P99 延迟:
promql
histogram_quantile(0.99, sum(rate(http_server_requests_duration_ms_bucket[5m])) by (le, path))错误率:
promql
sum(rate(http_server_requests_total{code=~"5.."}[5m])) / sum(rate(http_server_requests_total[5m]))goroutine 数:
promql
go_goroutines{job="user-api"}RPC 调用延迟:
promql
histogram_quantile(0.95, sum(rate(rpc_client_requests_duration_ms_bucket[5m])) by (le, method))Grafana 仪表盘可以参考 go-zero 官方提供的模板,或自己根据业务定制。
4. 告警规则
在 Prometheus 中配置告警规则:
yaml
# prometheus/alerts.yml
groups:
- name: user-api-alerts
rules:
- alert: HighErrorRate
expr: |
sum(rate(http_server_requests_total{job="user-api",code=~"5.."}[5m]))
/ sum(rate(http_server_requests_total{job="user-api"}[5m])) > 0.05
for: 5m
labels:
severity: critical
annotations:
summary: "user-api error rate > 5%"
description: "Error rate is \{\{ $value \}\}% for the last 5 minutes"
- alert: HighLatency
expr: |
histogram_quantile(0.99,
sum(rate(http_server_requests_duration_ms_bucket{job="user-api"}[5m])) by (le)
) > 1000
for: 5m
labels:
severity: warning
annotations:
summary: "user-api P99 latency > 1s"
- alert: ServiceDown
expr: up{job="user-api"} == 0
for: 1m
labels:
severity: critical
annotations:
summary: "user-api is down"注意:Prometheus 告警模板中
\{\{ $value \}\}是 Prometheus 的模板语法,在 YAML 中需正确转义。这里在文档中已经用反斜杠转义,实际配置文件中应写为\{\{ $value \}\}(VitePress 渲染时需要转义花括号)。
配合 Alertmanager 推送到钉钉、企业微信、邮件等渠道。
三、链路追踪
链路追踪(Distributed Tracing)用于追踪一个请求在多个微服务之间的调用链路,是排查跨服务性能问题的利器。
1. go-zero 内置 OpenTelemetry
go-zero 内置 OpenTelemetry 支持,配置开启:
yaml
Telemetry:
Name: user-api # 服务名
Endpoint: http://127.0.0.1:14268/api/traces # Jaeger 上报地址
Sampler: 1.0 # 采样率(0-1)
Batcher: jaeger # jaeger / zipkin / otlpgrpc / otlphttpSampler:采样率,1.0 表示全采样,0.1 表示 10% 采样。生产环境建议 0.01-0.1,避免数据量过大Batcher:上报协议,对应不同的后端
go-zero 会在以下位置自动注入 span:
- HTTP 请求进入
- RPC 调用发起和接收
- SQL 查询
- Redis 操作
2. Jaeger 集成
启动 Jaeger(all-in-one 容器):
bash
docker run -d --name jaeger \
-p 16686:16686 \
-p 4318:4318 \
-p 14268:14268 \
jaegertracing/all-in-one:1.45访问 Jaeger UI:http://localhost:16686
在 Jaeger UI 中:
- 左侧选择服务(如
user-api) - 选择 Operation(如
/api/v1/user/:id) - 点击 Find Traces 查看调用链
- 展开某个 Trace 可以看到完整的调用栈
每个 Span 包含:
- 服务名
- 操作名
- 开始时间、耗时
- Tags(标签)
- Logs(日志事件)
- 父 Span
3. TraceID 传播
go-zero 自动处理 TraceID 的传播:
HTTP 调用:通过 X-Trace-ID 请求头传播
RPC 调用:通过 gRPC metadata 传播
go
// 客户端发起 RPC 调用时,go-zero 自动从 ctx 提取 TraceID 注入 metadata
resp, err := l.svcCtx.UserRpc.GetUser(l.ctx, &pb.GetUserRequest{Id: id})
// 服务端收到请求时,go-zero 自动从 metadata 提取 TraceID 注入 ctx
func (l *GetUserLogic) GetUser(in *pb.GetUserRequest) (*pb.GetUserResponse, error) {
// l.ctx 已包含 TraceID
logx.WithContext(l.ctx).Info("processing") // 日志自动关联 TraceID
// ...
}跨服务调用链:
[Client] → [user-api] → [user-rpc] → [MySQL]
trace=abc trace=abc trace=abc trace=abc
span=1 span=2 span=3 span=4
parent=1 parent=2 parent=3所有 Span 共享同一个 TraceID,形成一棵调用树。
4. API → RPC 链路追踪
下面演示一个完整的跨服务链路追踪。
API 服务配置
yaml
# etc/user-api.yaml
Telemetry:
Name: user-api
Endpoint: http://jaeger:14268/api/traces
Sampler: 1.0
Batcher: jaegerRPC 服务配置
yaml
# etc/user.yaml
Telemetry:
Name: user.rpc
Endpoint: http://jaeger:14268/api/traces
Sampler: 1.0
Batcher: jaegerLogic 调用
go
// user-api/internal/logic/user/getuserlogic.go
func (l *GetUserLogic) GetUser(req *types.GetUserRequest) (*types.GetUserResponse, error) {
logx.WithContext(l.ctx).Infof("get user, id=%s", req.Id)
// 调用 RPC,TraceID 自动传播
rpcResp, err := l.svcCtx.UserRpc.GetUser(l.ctx, &pb.GetUserRequest{Id: id})
if err != nil {
logx.WithContext(l.ctx).Errorf("call rpc failed: %v", err)
return nil, err
}
return &types.GetUserResponse{...}, nil
}在 Jaeger 中查看
发起请求:
bash
curl http://localhost:8888/api/v1/user/1在 Jaeger UI 中可以看到完整的调用链:
user-api /api/v1/user/:id (12ms)
└─ user.rpc /user.User/GetUser (8ms)
└─ mysql SELECT (5ms)每个节点的耗时一目了然,性能瓶颈无处遁形。
5. 自定义 Span
业务可以在关键逻辑中添加自定义 Span:
go
import (
"github.com/zeromicro/go-zero/core/trace"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
)
func (l *GetUserLogic) GetUser(req *types.GetUserRequest) (*types.GetUserResponse, error) {
tracer := otel.Tracer(trace.TraceName)
ctx, span := tracer.Start(l.ctx, "business_logic")
defer span.End()
// 添加标签
span.SetAttributes(
attribute.String("user.id", req.Id),
attribute.Bool("user.vip", true),
)
// 添加事件
span.AddEvent("start_query_db")
user, err := l.svcCtx.UserModel.FindOne(ctx, id)
if err != nil {
span.RecordError(err)
return nil, err
}
span.AddEvent("end_query_db")
return &types.GetUserResponse{...}, nil
}自定义 Span 会显示在 Jaeger 调用链中,帮助定位业务逻辑耗时。
四、日志关联 TraceID
把日志和链路追踪关联起来,是可观测性的关键。通过 TraceID 可以从日志系统跳转到 Jaeger,反之亦然。
1. 自动关联
go-zero 的 logx.WithContext(ctx) 会自动从 ctx 中提取 TraceID 并写入日志:
go
logx.WithContext(l.ctx).Info("processing")
// 输出:{"@message":"processing","trace":"abc123","span":"def456"}trace 和 span 字段就是 TraceID 和 SpanID。
2. 从请求头获取 TraceID
如果没有用 go-zero 的链路追踪,可以手动从请求头获取:
go
func TraceMiddleware(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
traceID := r.Header.Get("X-Trace-Id")
if traceID == "" {
traceID = uuid.New().String()
}
ctx := context.WithValue(r.Context(), "traceID", traceID)
w.Header().Set("X-Trace-Id", traceID)
next(w, r.WithContext(ctx))
}
}
// 在 Logic 中使用
func (l *SomeLogic) DoSomething() {
traceID, _ := l.ctx.Value("traceID").(string)
logx.WithFields(logx.Field("trace", traceID)).Info("doing something")
}3. 在 ELK 中关联
日志采集到 ELK 后,可以通过 TraceID 串联:
- 在 Kibana 中搜索特定 TraceID 的日志
- 看到这个请求在所有服务中的日志
- 点击 TraceID 跳转到 Jaeger 查看调用链
配置 Logstash 解析 trace 字段:
ruby
# logstash.conf
filter {
json {
source => "message"
}
if [trace] {
mutate {
add_field => { "trace_id" => "%{trace}" }
}
}
}4. 请求头返回 TraceID
为了方便排查,可以把 TraceID 返回给客户端:
go
// 在 main.go 中注册全局中间件
server.Use(func(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
traceID := trace.TraceIDFromContext(r.Context())
if traceID != "" {
w.Header().Set("X-Trace-Id", traceID)
}
next(w, r)
}
})客户端报错时提供 TraceID,运维可以快速定位问题。
五、完整示例:可观测的微服务
下面搭建一个完整的可观测微服务,包含日志、监控、链路追踪。
1. 基础设施
yaml
# docker-compose.yml
version: "3.8"
services:
etcd:
image: bitnami/etcd:3.5
ports: ["2379:2379"]
environment:
- ALLOW_NONE_AUTHENTICATION=yes
mysql:
image: mysql:8.0
ports: ["3306:3306"]
environment:
MYSQL_ROOT_PASSWORD: "123456"
MYSQL_DATABASE: "demo"
redis:
image: redis:7-alpine
ports: ["6379:6379"]
jaeger:
image: jaegertracing/all-in-one:1.45
ports:
- "16686:16686"
- "4318:4318"
- "14268:14268"
prometheus:
image: prom/prometheus:v2.45
ports: ["9090:9090"]
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml
grafana:
image: grafana/grafana:10.0
ports: ["3000:3000"]
environment:
- GF_SECURITY_ADMIN_PASSWORD=admin2. Prometheus 配置
yaml
# prometheus.yml
global:
scrape_interval: 15s
scrape_configs:
- job_name: "user-api"
static_configs:
- targets: ["host.docker.internal:9100"]
- job_name: "user-rpc"
static_configs:
- targets: ["host.docker.internal:9101"]3. user-api 配置
yaml
# etc/user-api.yaml
Name: user-api
Host: 0.0.0.0
Port: 8888
UserRpc:
Etcd:
Hosts: ["127.0.0.1:2379"]
Key: user.rpc
NonBlock: true
Timeout: 2000
Log:
ServiceName: user-api
Mode: file
Path: logs
Level: info
Encoding: json
KeepDays: 7
Prometheus:
Host: 0.0.0.0
Port: 9100
Path: /metrics
Telemetry:
Name: user-api
Endpoint: http://127.0.0.1:14268/api/traces
Sampler: 1.0
Batcher: jaeger4. user-rpc 配置
yaml
# etc/user.yaml
Name: user.rpc
ListenOn: 0.0.0.0:8080
Etcd:
Hosts: ["127.0.0.1:2379"]
Key: user.rpc
MySQL:
DataSource: root:123456@tcp(127.0.0.1:3306)/demo?charset=utf8mb4&parseTime=true&loc=Local
Log:
ServiceName: user.rpc
Mode: file
Path: logs
Level: info
Encoding: json
KeepDays: 7
Prometheus:
Host: 0.0.0.0
Port: 9101
Path: /metrics
Telemetry:
Name: user.rpc
Endpoint: http://127.0.0.1:14268/api/traces
Sampler: 1.0
Batcher: jaeger5. 业务代码
go
// user-rpc/internal/logic/getuserlogic.go
func (l *GetUserLogic) GetUser(in *pb.GetUserRequest) (*pb.GetUserResponse, error) {
logx.WithContext(l.ctx).Infof("get user, id=%d", in.Id)
// 查 DB(go-zero 自动记录 SQL span)
user, err := l.svcCtx.UserModel.FindOne(l.ctx, in.Id)
if err != nil {
logx.WithContext(l.ctx).Errorf("query failed: %v", err)
return nil, err
}
if user == nil {
return nil, status.Error(codes.NotFound, "user not found")
}
return &pb.GetUserResponse{
User: &pb.UserInfo{
Id: user.Id,
Name: user.Name,
Email: user.Email,
},
}, nil
}go
// user-api/internal/logic/user/getuserlogic.go
func (l *GetUserLogic) GetUser(req *types.GetUserRequest) (*types.GetUserResponse, error) {
id, _ := strconv.ParseInt(req.Id, 10, 64)
logx.WithContext(l.ctx).Infof("api get user, id=%d", id)
rpcResp, err := l.svcCtx.UserRpc.GetUser(l.ctx, &pb.GetUserRequest{Id: id})
if err != nil {
logx.WithContext(l.ctx).Errorf("call rpc failed: %v", err)
return nil, err
}
return &types.GetUserResponse{
Id: rpcResp.User.Id,
Name: rpcResp.User.Name,
Email: rpcResp.User.Email,
}, nil
}6. 验证可观测性
启动所有服务后,发起请求:
bash
curl http://localhost:8888/api/v1/user/1 -v响应头中会有 X-Trace-Id。
查看日志
bash
# user-api 日志
tail -f logs/user-api.log
# {"@message":"api get user, id=1","trace":"abc123","span":"def456"}
# user-rpc 日志
tail -f logs/user.rpc.log
# {"@message":"get user, id=1","trace":"abc123","span":"ghi789"}两个服务的日志有相同的 trace 字段。
查看监控
访问 Prometheus(http://localhost:9090)查询:
promql
# user-api QPS
rate(http_server_requests_total{job="user-api"}[1m])
# user-rpc P99 延迟
histogram_quantile(0.99, rate(rpc_server_requests_duration_ms_bucket{job="user-rpc"}[5m]))Grafana(http://localhost:3000,admin/admin)添加 Prometheus 数据源,导入仪表盘。
查看链路追踪
访问 Jaeger(http://localhost:16686):
- Service 选择
user-api - Operation 选择
/api/v1/user/:id - Find Traces
- 展开某个 Trace,看到调用链:
user-api: GET /api/v1/user/1 (15ms)
└─ user.rpc: User/GetUser (10ms)
└─ mysql: SELECT (6ms)每个 Span 的耗时、标签、日志事件都可见。
六、可观测性最佳实践
1. 日志规范
- 所有日志带 context(
logx.WithContext) - 关键业务节点都打日志(登录、下单、支付)
- 错误日志带足够上下文(参数、用户ID、错误堆栈)
- 不记录敏感信息(密码、token、身份证)
- 日志级别合理(debug 开发用,error 生产用)
2. 监控规范
- 关注四大黄金指标:
- 延迟(Latency)
- 流量(Traffic)
- 错误(Errors)
- 饱和度(Saturation)
- 设置合理告警阈值,避免告警风暴
- 关键业务指标单独监控(订单数、支付金额)
3. 链路追踪规范
- 采样率合理(生产环境 1-10%)
- 关键业务逻辑加自定义 Span
- Span 名称有意义(业务动词 + 名词)
- Tags 记录业务信息(用户ID、订单ID)
4. 三者关联
- 日志带 TraceID(自动)
- 监控告警带 TraceID(手动从 ctx 提取)
- 故障排查流程:告警 → Grafana 看趋势 → Jaeger 看链路 → ELK 看日志
七、小结
本篇讲解了 go-zero 的可观测性体系。要点回顾:
logx提供结构化日志,支持文件/控制台/ES 输出,自动关联 TraceID- 日志级别:debug/info/slow/error/severe,生产环境用 info 或 error
- go-zero 内置 Prometheus 指标(HTTP/RPC/DB/Redis),配置即可启用
- 自定义指标用
promauto创建 Counter/Histogram/Gauge - 链路追踪基于 OpenTelemetry,支持 Jaeger/Zipkin/OTLP
- TraceID 自动在 HTTP(请求头)和 RPC(metadata)间传播
logx.WithContext自动从 ctx 提取 TraceID 写入日志- 完整示例演示了日志 + 监控 + 链路追踪的端到端可观测性
- 故障排查三件套:Grafana(看趋势)→ Jaeger(看链路)→ ELK(看日志)
下一篇我们将进入项目实战与最佳实践,通过一个完整的电商系统案例把所有知识点串起来。