Go 可观测性

日志、指标、链路追踪三大支柱在 Go 服务中的分工;与 context、slog、pprof 及 OpenTelemetry 的工程关系。

#type / concept #status / growing #tech / dev #resource / go

[!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":...}

结合场景再看三个关注点

  1. 结构化日志(key-value),不是 fmt.Sprintf 长字符串。
  2. 尊重请求 ctx,取消要向上反映。
  3. 示例未上完整 OTel;生产在中间件统一注入 trace 与 RED 指标。

核心概念与准确模型

三大支柱

支柱问题Go 常见实现后端例子
Logs事件细节log/slog、zerolog 等Loki、ELK
Metrics聚合健康与 SLOPrometheus client、OTel metricsPrometheus、Mimir
Traces请求拓扑与跨服务延迟OpenTelemetry GoJaeger、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 大海式日志冲垮存储

实践:

  1. 错误带 err 与稳定 code
  2. 禁止打密钥与 PII
  3. 高基数 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

边界情况

  1. 高基数标签(user_id 当 label)撑爆时序库。
  2. 同步打日志阻塞请求路径。
  3. 追踪 100% 采样拖垮热路径与后端。
  4. 三套 ID 不统一(request/trace/span)无法关联。
  5. 仅 stdout 无采集,容器重启即失证。
  6. 可观测性代码错误被忽略:export 失败应有兜底指标。

常见误区

[!warning] 常见误区:日志越多越可观测 错误:Debug 刷屏。
正确:结构化、可采样、可关联、有字段规范。

[!warning] 常见误区:有仪表盘就有 SLO 错误:只看 CPU。
正确:用户旅程延迟与错误预算。

[!warning] 常见误区:追踪替代日志 错误:span 里塞大段业务正文。
正确:span 记拓扑与时序;细节放日志并挂 trace id。

[!warning] 常见误区:本地 pprof 等于生产可观测 错误:只在开发机剖析。
正确:持续信号 + 按需深潜剖析(鉴权)。

工程实践

  1. 中间件统一:生成/解析 request id 与 trace,记录 status、duration。
  2. 错误包装保留栈或错误码,日志一次、上游少噪声(go-error-wrapping)。
  3. 资源语义约定service.namedeployment.environment(OTel semconv)。
  4. 优雅停机:flush exporter、关 listener(go-graceful-shutdown)。
  5. 示例与文档:runbook 写“告警 → 哪个面板 → 哪条查询”。
  6. 测试:指标名契约测试;handler 测日志字段可用 hook。
  7. 安全:admin/pprof/metrics 端点网络隔离与鉴权。
  8. 先标准库 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

自测题

概念题

  1. logs/metrics/traces 各最擅长什么?
  2. 为什么 user_id 不适合做 Prometheus label?
  3. context 在可观测性里起什么作用?

代码推理题

handler 忽略 r.Context() 做固定 10s 睡眠,客户端 1s 超时断开。指标与日志可能出现什么误导?

工程思考题

微服务 A→B→C,仅 A 有指标。如何分阶段补齐仍能先定位慢段?

参考答案

展开
  1. 日志:细节事件;指标:聚合与告警;追踪:跨服务路径与延迟分解。
  2. 高基数导致时间序列爆炸。
  3. 传播取消与 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 loggingGo pprof
  • 本篇状态:已深化
创建于 2026/6/25 更新于 2026/7/15