导语:可观测性是"日志、Trace、Metrics"三位一体
很多团队把可观测性等同于"打日志",于是线上出问题时:日志像豆腐渣、Trace 缺失、指标迟到——三个信号对不上,排障全凭运气。真正的可观测性是 Logging(日志)、Tracing(链路)、Metrics(指标) 三种信号的协同:Metrics 告诉你"出了什么问题",Tracing 告诉你"在哪条链路出问题",Logging 告诉你"问题现场的细节"。
Go 生态在这条路上已经相当成熟:标准库 log/slog 在 Go 1.21 落地了结构化日志;go.opentelemetry.io/otel 提供了标准化的 Trace 与 Metrics 接入。本文把三块拼成一套可落地的工程方案。
一句话总结:Go 可观测性 = slog 打结构化日志 + OpenTelemetry 打链路与指标 + 一套"日志、Trace、Metrics 用同一 traceID 关联"的协同体系。
1. 从 log 到 slog:结构化日志的演进
1.1 传统 log 的痛点
// 传统 log:只有消息,没有结构,机器无法过滤与聚合
log.Printf("user %d login failed from %s", userID, ip)
// 传统 log 是"面向人类阅读",无法被查询引擎结构化检索
// 线上排查:grep 无法精确过滤字段,日志量一大就失控
1.2 slog 的基本用法
import "log/slog"
// 默认 JSON Handler:输出结构化字段
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
logger.Info("user login", "userID", 42, "ip", "10.0.0.1")
// 输出:{"time":"...","level":"INFO","msg":"user login","userID":42,"ip":"10.0.0.1"}
// 文本 Handler:开发期更易读
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
logger.Warn("cache miss", "key", "hot-item", "cost_ms", 12.3)
log/slog 从 Go 1.21 进入标准库,提供了 Info、Warn、Error、Debug 四个级别,每个调用用 key/value 成对追加结构化字段。机器可解析,人也可读。
1.3 与旧 log 的兼容
// 旧代码仍可用 log 包,但输出被重定向到 slog
slog.SetDefault(logger)
log.SetOutput(slog.Default().Handler().Writer()) // 老 log 也走结构化
// 或直接把标准 logger 替换成 slog
log.SetFlags(0) // 去掉前缀,交给 slog 管理
一句话总结:slog 把"人读的消息"升级为"机器可解析的结构化事件",级别、时间、字段全是结构化数据,是日志体系的地基。
2. slog 核心 API 与 Handler 自定义
2.1 全局默认 logger 与 Logger 注入
// 包级:设置全局默认
slog.SetDefault(slog.New(slog.NewJSONHandler(os.Stdout, nil)))
// 依赖注入:把 *slog.Logger 作为依赖传入,便于测试与替换
type Service struct {
log *slog.Logger
}
func NewService(log *slog.Logger) *Service {
return &Service{log: log.With("service", "user-svc")} // 公共字段一次附加
}
2.2 HandlerOptions 与级别过滤
logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelInfo, // 只记录 Info 及以上,Debug 被过滤
// AddSource: true, // 附加调用位置(file:line)
ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
// 自定义:把 level 改成大写,或裁剪敏感字段
if a.Key == slog.LevelKey {
a.Value = slog.StringValue(strings.ToUpper(a.Value.String()))
}
return a
},
}))
// 运行时调整级别(配合配置文件或环境变量)
var lvl = new(slog.LevelVar)
logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: lvl}))
lvl.Set(slog.LevelDebug) // 按需切换到 Debug 排障
2.3 自定义 Handler:接第三方日志系统
// 自定义 Handler 只需实现三个方法,slog 负责上层调度
type myHandler struct {
next slog.Handler
}
func (h *myHandler) Enabled(ctx context.Context, l slog.Level) bool {
return h.next.Enabled(ctx, l)
}
func (h *myHandler) Handle(ctx context.Context, r slog.Record) error {
// 在这里把 slog.Record 转成自己格式:加 traceID、采样、缓冲发送
// ...
return h.next.Handle(ctx, r)
}
func (h *myHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
return &myHandler{next: h.next.WithAttrs(attrs)}
}
func (h *myHandler) WithGroup(name string) slog.Handler {
return &myHandler{next: h.next.WithGroup(name)}
}
一句话总结:
slog.Logger全局可设、依赖可注,HandlerOptions 控制级别与格式,自定义 Handler 让你把日志接到任何第三方平台。
3. 日志级别、采样与上下文关联
3.1 级别语义与生产规则
□ Debug —— 开发排障细节,生产默认关闭,按需动态打开
□ Info —— 业务关键事件:登录、下单、状态变更
□ Warn —— 可恢复的异常:重试、降级、慢查询
□ Error —— 影响功能的错误:必须记录堆栈与上下文
□ 原则:Info 别刷屏(每条 Info 都该有查询价值),Error 必带可排障字段
3.2 上下文关联:把请求元数据带进每条日志
// 方式1:With 派生 logger,在处理器入口建立"请求级 logger"
func handler(log *slog.Logger, w http.ResponseWriter, r *http.Request) {
reqLog := log.With(
"traceID", r.Header.Get("X-Trace-ID"),
"path", r.URL.Path,
"method", r.Method,
)
reqLog.Info("request start")
// 后续该请求的所有日志都自动带 traceID/path/method
reqLog.Error("upstream timeout", "service", "pay", "cost_ms", 2500)
}
3.3 日志采样:高流量下的取舍
// 高频日志(如每个请求的访问日志)在高流量下会淹没存储
// 方案1:按 traceID 哈希采样 —— 同一请求的日志要么全留要么全丢
func sampled(traceID string) bool {
sum := 0
for _, c := range traceID {
sum += int(c)
}
return sum%100 < 10 // 保留 10%
}
// 方案2:按错误级别强制保留,Info 采样、Error 全留
if logLevel == slog.LevelError || sampled(traceID) {
// 记录
}
一句话总结:级别语义决定"记什么",请求级 logger 决定"每条日志带什么上下文",采样决定"存多少"——三者协同才能控制日志成本与价值。
4. OpenTelemetry Trace 集成
4.1 初始化 TracerProvider
import (
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp"
sdkTrace "go.opentelemetry.io/otel/sdk/trace"
)
func setupOTel() (*otlptracehttp.Exporter, error) {
// 通过 OTLP/HTTP 导出到 Collector
exporter, err := otlptracehttp.New(context.Background())
if err != nil {
return nil, err
}
tp := sdkTrace.NewTracerProvider(
sdkTrace.WithBatcher(exporter),
sdkTrace.WithSampler(sdkTrace.AlwaysSample()),
)
otel.SetTracerProvider(tp)
return exporter, nil
}
4.2 在业务代码中打 Span
func HandleRequest(ctx context.Context) error {
tracer := otel.Tracer("user-service")
ctx, span := tracer.Start(ctx, "HandleRequest") // 创建一个 Span
defer span.End()
// 记录 Span 级结构化属性
span.SetAttributes(attribute.String("endpoint", "/v1/user"))
// 调用下游:ctx 传递让 Span 成为父子链
if err := callPayment(ctx); err != nil {
span.RecordError(err) // 记录错误但不一定结束 Span
span.SetStatus(codes.Error, err.Error())
return err
}
return nil
}
4.3 TraceID 注入日志:让 Log 与 Trace 对齐
// 关键:把 traceID 从 Span 取出,注入 slog —— 日志与链路就能对上
func logWithTrace(log *slog.Logger, ctx context.Context) *slog.Logger {
span := trace.SpanFromContext(ctx)
return log.With(
"traceID", span.SpanContext().TraceID().String(),
"spanID", span.SpanContext().SpanID().String(),
)
}
// 使用:handler 里先取 ctx(可能来自 span),再打日志
ctx, span := tracer.Start(r.Context(), "handler")
defer span.End()
logWithTrace(logger, ctx).Info("user loaded", "userID", 42)
// 日志里的 traceID 与链路系统里的 traceID 完全一致
一句话总结:OpenTelemetry 用 TracerProvider 统一导出、用 Span 记录链路节点,最关键的是把 traceID 注入日志——日志与 Trace 才能互跳排查。
5. Metrics 指标与运行时监控
5.1 三种基本指标类型
import (
"go.opentelemetry.io/otel/metric"
"go.opentelemetry.io/otel"
)
var meter = otel.Meter("app.metrics")
// Counter:只增不减的累计计数(请求数、错误数)
reqCounter, _ := meter.Int64Counter("http.requests.total")
// Histogram:分布统计(延迟、大小)
latencyHist, _ := meter.Float64Histogram("http.request.duration")
// Gauge:可增可减的瞬时值(当前连接数、队列长度)
connGauge, _ := meter.Int64ObservableGauge("http.conn.current")
// 使用
func trackRequest(method, path string, dur time.Duration, code int) {
reqCounter.Add(context.Background(), 1, metric.WithAttributes(
attribute.String("method", method),
attribute.String("path", path),
attribute.Int("status", code),
))
latencyHist.Record(context.Background(), dur.Seconds())
}
5.2 Go runtime 指标
// Go runtime 自带可观测指标,接入 OpenTelemetry 后自动上报
// 常见指标:
// go.goroutines 当前 goroutine 数
// go.gc.count / go.gc.duration GC 次数与耗时
// process.memory.rss 进程常驻内存
// process.cpu.utilization CPU 使用率
import (
"go.opentelemetry.io/contrib/instruntime/runtime"
)
runtime.Start(context.Background()) // 一行接入 runtime 指标
5.3 RED / USE 方法论选指标
□ RED(面向请求型服务):
Rate —— 每秒请求数
Errors —— 每秒错误数
Duration —— 请求延迟分布(P50/P95/P99)
□ USE(面向资源型组件):
Utilization —— 使用率(CPU/内存/磁盘)
Saturation —— 饱和度(队列长度、goroutine 堆积)
Errors —— 错误计数(重试、超时次数)
□ 指标要少而精:每个指标都对应一个"能否直接触发告警"的决策
一句话总结:Metrics 用 Counter/Histogram/Gauge 三种原语表达"量",RED/USE 方法论指导"选哪些量",runtime 指标一键接入补全系统视角。
6. 生产日志体系与告警设计
6.1 日志、Trace、Metrics 的关联设计
┌─ 告警触发条件(Metrics)
│ P99 延迟 > 500ms 持续 5 分钟
│ │
│ ▼
│ 打开对应服务面板(Metrics + Trace 过滤)
│ │
│ ▼
│ 按 traceID 进入单条链路(Tracing)
│ │
│ ▼
│ 链路上某 Span 的日志详情(Logging,含 traceID)
└─ 完成排障
三者的纽带就是 traceID:指标异常 → 链路定位 → 日志取证,一条 traceID 串起全部。
6.2 结构化字段命名规范
□ 统一 key:traceID、spanID、service、env、host、version
□ 业务字段用驼峰:userID、orderID、costMs —— 保持可查询性
□ 敏感字段脱敏:token、password 绝不落日志
□ 统一时间格式:RFC3339,日志与 Trace 时间轴对齐
□ 保留堆栈:Error 级必须带 stack,Warn 级视情况
6.3 告警设计原则
□ 告警必须有"可执行的下一步"——没有行动的告警就是噪音
□ 用 Metrics 触发(持续条件),不用单条日志触发(容易抖动)
□ 分级:Warning(人工关注)→ Critical(自动动作/值班)
□ 避免告警风暴:Error 级别采样 + 聚合窗口(如 5 分钟错误率)
□ 每条告警附 traceID 查询链接,值班人员点开即可排障
一句话总结:生产可观测性是"traceID 关联三信号 + 结构化命名规范 + Metrics 驱动告警",让排障从"猜"变成"沿链路取证"。
7. 总结
| 信号 | Go 工具 | 一句要义 |
|---|---|---|
| Logging | log/slog | 结构化 key/value,机器可解析 |
| 日志级别 | slog 级别 + LevelVar | Debug 关、Error 必带堆栈 |
| 上下文 | request logger | traceID/path/method 常驻每条日志 |
| Tracing | OpenTelemetry SDK | Span 记录链路节点,父子传递 |
| Metrics | otel metric API | Counter/Histogram/Gauge 三种原语 |
| 运行时监控 | runtime 指标 | goroutine/GC/内存一键接入 |
| 关联 | traceID 注入 | 日志、Trace、Metrics 用同一 ID 串起 |
| 告警 | Metrics 持续条件 | 可执行、分级、防风暴 |
落地记住六件事:全部日志用 slog 结构化输出、请求级 logger 常驻 traceID、Span 必须 defer End 且错误要 RecordError、指标按 RED/USE 选型而非随意埋、Error 日志必带堆栈与上下文、告警用 Metrics 持续条件触发并附 traceID 查询链接。把 Logging、Tracing、Metrics 当作一套协同体系来设计,你的 Go 服务才能在任何故障面前"有据可查、有链可追、有数可依"。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。