Go 可观测性
日志、指标、链路追踪三大支柱在 Go 服务中的分工;与 context、slog、pprof 及 OpenTelemetry 的工程关系。
[!info] 关联笔记
Go 可观测性
这个概念为什么会出现
进程“在跑”不等于“可运营”。故障时你需要回答:
- 发生了什么(事件与错误上下文)
- 系统现在怎样(红黄金指标、饱和度)
- 这个请求走过哪里(依赖链与延迟分解)
可观测性(observability)不是某一个库,而是让系统外部输出足够信号,使你能从输出推断内部状态。Go 服务通常用日志 + 指标 + 追踪,并与 context 贯穿的请求生命周期对齐;剖析(pprof)则属于更深层的诊断工具箱。
[!abstract] 一句话理解 可观测性 = 用 logs/metrics/traces(常经 OpenTelemetry 等)把请求路径与运行状态变成可查询信号;context 携带请求关联;pprof/trace 用于热点与调度级深潜。
最小可运行示例
先把示例放进业务场景,再看代码:
场景:/work 处理一次任务,结构化日志记成败与耗时
内部 API GET /work 跑一段可取消的工作(查库/调下游的缩影)。
运维需要从 stdout 看到:
- 请求是否成功
- 失败错误是什么
- 花了多久
本示例用标准库 log/slog JSON 日志打点;完整 metrics/traces 工程上在中间件统一接 OpenTelemetry,这里不淹没主线。
package main
import (
"context"
"log/slog"
"net/http"
"os"
"time"
)
func main() {
// JSON Handler:便于采集到 Loki/ELK;字段可检索
log := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelInfo}))
mux := http.NewServeMux()
mux.HandleFunc("/work", func(w http.ResponseWriter, r *http.Request) {
// 请求级 ctx:客户端断开 / 超时会取消下游
ctx := r.Context()
start := time.Now()
// 真实系统:中间件把 request_id / trace_id 注入 ctx 与 log attr
if err := doWork(ctx); err != nil {
// 失败:错误 + 耗时,方便按 err 聚合
log.Error("work failed", "err", err, "dur", time.Since(start))
http.Error(w, "unavailable", 503)
return
}
// 成功:只记必要字段,避免 Debug 刷屏
log.Info("work ok", "dur", time.Since(start))
w.WriteHeader(204)
})
log.Info("listen", "addr", ":8080")
_ = http.ListenAndServe(":8080", mux)
}
// doWork:模拟一次可取消的业务工作。
func doWork(ctx context.Context) error {
select {
case <-time.After(20 * time.Millisecond):
return nil
case <-ctx.Done():
// 客户端已走:别继续占资源
return ctx.Err()
}
}
建议运行:
go run .
# 终端 2:
curl -i http://localhost:8080/work
期望:HTTP 204;服务端 stdout 类似:
{"time":"...","level":"INFO","msg":"listen","addr":":8080"}
{"time":"...","level":"INFO","msg":"work ok","dur":...}
结合场景再看三个关注点
- 结构化日志(key-value),不是
fmt.Sprintf长字符串。 - 尊重请求 ctx,取消要向上反映。
- 示例未上完整 OTel;生产在中间件统一注入 trace 与 RED 指标。
核心概念与准确模型
三大支柱
| 支柱 | 问题 | Go 常见实现 | 后端例子 |
|---|---|---|---|
| Logs | 事件细节 | log/slog、zerolog 等 | Loki、ELK |
| Metrics | 聚合健康与 SLO | Prometheus client、OTel metrics | Prometheus、Mimir |
| Traces | 请求拓扑与跨服务延迟 | OpenTelemetry Go | Jaeger、Tempo、Honeycomb |
互补:指标告警 → 追踪定位慢依赖 → 日志看错误上下文。
与 context 的关系
- 请求入口创建/提取 ctx
- trace span 存入 ctx,子调用
Tracer.Start(ctx, name) - 日志库从 ctx 取
trace_id字段 - 取消时 span 标记错误并结束
没有 ctx 贯穿,追踪会断、日志无法关联。见 go-context。
日志(logs)
Go 1.21+ 标准库 log/slog:
- 结构化 key-value
- Level 与 Handler 可插拔
- 避免
fmt.Sprintf大海式日志冲垮存储
实践:
- 错误带
err与稳定code - 禁止打密钥与 PII
- 高基数 ID 放日志/追踪,不要无限制打进 metrics 标签
指标(metrics)
四类常用:
| 类型 | 用途 |
|---|---|
| Counter | 请求数、错误数 |
| Gauge | 队列长度、goroutine 数 |
| Histogram / Summary | 延迟分布 |
RED(Rate/Errors/Duration)与 USE(Utilization/Saturation/Errors)是设计起点。Go 运行时可暴露:GC、内存、调度——见 runtime/metrics 与 Prometheus exporters。
追踪(traces)
- Span:一段操作
- Trace:span 树
- Propagation:HTTP/gRPC 头部传递 trace context
OpenTelemetry 是当前跨语言事实标准之一:OpenTelemetry Go。采样策略决定成本与代表性。
与 pprof / runtime/trace
| 工具 | 层级 |
|---|---|
| metrics/logs/traces | 服务与请求级运营 |
| go-pprof | 函数热点、堆、goroutine |
runtime/trace | 调度与 STW 时间线 |
可观测性告警说“p99 升高”;pprof 回答“哪个函数”;二者衔接而非互斥。Diagnostics。
健康检查
/healthz、/readyz 是编排信号,不是完整可观测性,但应与依赖探测一致。见 go-health-check-endpoint。
边界情况
- 高基数标签(user_id 当 label)撑爆时序库。
- 同步打日志阻塞请求路径。
- 追踪 100% 采样拖垮热路径与后端。
- 三套 ID 不统一(request/trace/span)无法关联。
- 仅 stdout 无采集,容器重启即失证。
- 可观测性代码错误被忽略:export 失败应有兜底指标。
常见误区
[!warning] 常见误区:日志越多越可观测 错误:Debug 刷屏。
正确:结构化、可采样、可关联、有字段规范。
[!warning] 常见误区:有仪表盘就有 SLO 错误:只看 CPU。
正确:用户旅程延迟与错误预算。
[!warning] 常见误区:追踪替代日志 错误:span 里塞大段业务正文。
正确:span 记拓扑与时序;细节放日志并挂 trace id。
[!warning] 常见误区:本地 pprof 等于生产可观测 错误:只在开发机剖析。
正确:持续信号 + 按需深潜剖析(鉴权)。
工程实践
- 中间件统一:生成/解析 request id 与 trace,记录 status、duration。
- 错误包装保留栈或错误码,日志一次、上游少噪声(go-error-wrapping)。
- 资源语义约定:
service.name、deployment.environment(OTel semconv)。 - 优雅停机:flush exporter、关 listener(go-graceful-shutdown)。
- 示例与文档:runbook 写“告警 → 哪个面板 → 哪条查询”。
- 测试:指标名契约测试;handler 测日志字段可用 hook。
- 安全:admin/pprof/metrics 端点网络隔离与鉴权。
- 先标准库 slog + 基础 metrics,再上完整 OTel,避免一上来全平台吊车。
// 伪代码:HTTP 中间件记录耗时
func withMetrics(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
ww := &statusWriter{ResponseWriter: w, code: 200}
next.ServeHTTP(ww, r)
// requests.WithLabelValues(r.Method, strconv.Itoa(ww.code)).Inc()
// duration.Observe(time.Since(start).Seconds())
_ = start
})
}
可验证实验
实验 1:slog JSON
打一条含 err 的 Error 日志,确认字段可解析。
实验 2:请求取消
客户端断开时 handler 是否记 context.Canceled 而不当 500 风暴。
实验 3:指标
用 Prometheus client 暴露 /metrics,curl 看 counter 递增。
实验 4:追踪(可选)
按 OTel Go getting started 跑最小 span 导出。
实验 5:关联
同一请求的 log 行带 trace_id,在后端跳到对应 trace。
本节总结
- 本质:用三类信号(加诊断工具)推断系统内部状态。
- 关键规则:ctx 贯穿、低基数指标、结构化日志、采样追踪、与 pprof 分工。
- 最易错:高基数、无关联 ID、只采集不行动、端点裸奔。
- 下一步:go-logging 深挖;go-pprof 深潜;服务入口 go-http-middleware。
自测题
概念题
- logs/metrics/traces 各最擅长什么?
- 为什么 user_id 不适合做 Prometheus label?
- context 在可观测性里起什么作用?
代码推理题
handler 忽略 r.Context() 做固定 10s 睡眠,客户端 1s 超时断开。指标与日志可能出现什么误导?
工程思考题
微服务 A→B→C,仅 A 有指标。如何分阶段补齐仍能先定位慢段?
参考答案
展开
- 日志:细节事件;指标:聚合与告警;追踪:跨服务路径与延迟分解。
- 高基数导致时间序列爆炸。
- 传播取消与 trace/request 元数据,连接调用链。
代码题:服务端仍占满 10s 资源;错误率/延迟失真;可能大量客户端取消与服务端成功混杂。
工程题:先在 A 对出站调用打 span/客户端指标;再给 B/C 加同样中间件;用 trace 断点看哪一跳。
延伸阅读与资料来源
| 资料 | 类型 | 支撑 |
|---|---|---|
| Diagnostics | 官方文档 | Go 诊断总览 |
| log/slog | 标准库 | 结构化日志 |
| OpenTelemetry Go | 文档 | traces/metrics/logs 信号 |
| runtime/metrics | 标准库 | 运行时指标 |
| pprof blog | 官方博客 | 剖析衔接 |
| Context blog | 官方博客 | 请求范围传播 |
笔记元信息
- 建议文件名:
go-observability.md - 所属阶段:服务工程 / 诊断
- 建议下一篇:Go logging 或 Go pprof
- 本篇状态:已深化