第一章:Golang可观测性全景图与实习生认知误区
可观测性不是日志、指标、追踪三者的简单拼凑,而是系统在未知故障场景下可被理解与推理的能力。对刚接触生产级 Go 服务的实习生而言,常见误区包括:将 log.Printf 当作可观测性入口、误以为 Prometheus 自动采集所有性能数据、认为 OpenTelemetry SDK 开箱即用无需配置传播器与导出器。
什么是真正的可观测性三角
- Metrics(指标):聚合的、有维度的数值快照(如
http_requests_total{method="POST",status="500"}),用于趋势分析与告警; - Logs(日志):结构化事件记录(推荐使用
zap或zerolog输出 JSON),需携带 trace_id 以关联请求生命周期; - Traces(追踪):跨服务调用的时序链路(如 HTTP → gRPC → DB),依赖上下文传播(
context.WithValue不足以支撑分布式追踪)。
实习生高频误操作示例
- ❌ 直接在 handler 中写
log.Println("user created")—— 缺失结构化字段与 trace_id 关联; - ❌ 配置 Prometheus 仅抓取
/metrics,却未注册runtime.MemStats或自定义业务指标; - ❌ 使用
otelhttp.NewHandler但未初始化全局 tracer 和 propagator。
快速验证基础可观测性是否就绪
以下代码片段可嵌入 main.go 进行本地验证:
import (
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/exporters/stdout/stdouttrace"
"go.opentelemetry.io/otel/sdk/trace"
"go.opentelemetry.io/otel/propagation"
)
func initTracer() {
exporter, _ := stdouttrace.New(stdouttrace.WithPrettyPrint()) // 本地调试输出 trace 到终端
tp := trace.NewTracerProvider(trace.WithSyncer(exporter))
otel.SetTracerProvider(tp)
otel.SetTextMapPropagator(propagation.TraceContext{}) // 启用 W3C TraceContext 传播
}
执行后发起一次 HTTP 请求(如 curl http://localhost:8080/api/users),若终端打印出带 trace_id 和 span_id 的 JSON 日志,则传播链路已初步打通。
| 误区类型 | 正确实践 |
|---|---|
| 日志无上下文 | 使用 logger.With(zap.String("trace_id", span.SpanContext().TraceID().String())) |
| 指标未命名规范 | 遵循 Prometheus 命名约定:namespace_subsystem_metric_name |
| 追踪丢失跨度 | 所有 goroutine 启动前须 ctx = trace.ContextWithSpan(ctx, span) |
第二章:Log打点的四大隐形陷阱与正确Hook实践
2.1 日志上下文传递缺失:requestID与traceID未绑定导致链路断裂
在微服务调用中,requestID(业务请求标识)与 traceID(分布式追踪标识)常被独立生成,造成日志无法跨服务关联。
数据同步机制
典型错误示例:
// 错误:requestID 与 traceID 分离生成
String requestId = UUID.randomUUID().toString(); // 业务层生成
Tracer tracer = GlobalTracer.get();
Span span = tracer.buildSpan("service-a").start();
String traceId = span.context().toTraceId(); // OpenTracing 生成
// ❌ 二者无映射关系,ELK 中无法 join
逻辑分析:requestId 由 Web 层注入,traceId 由 APM SDK 自动注入,中间缺少 MDC.put("traceId", traceId) 与 MDC.put("requestId", requestId) 的显式绑定;参数 traceId 为 16 进制字符串(如 4a7d3f1e8b9c0a2d),需确保其在 HTTP Header(如 X-B3-TraceId)与日志 MDC 中一致。
根本原因归类
- 未统一上下文传播协议(如 W3C Trace Context)
- 日志框架(Logback/Log4j2)未集成
TraceContextPropagator
| 组件 | 是否透传 requestID | 是否透传 traceID | 是否双向绑定 |
|---|---|---|---|
| Spring Cloud Gateway | ✅ | ✅ | ❌ |
| Feign Client | ❌ | ✅ | ❌ |
| SLF4J + MDC | ❌ | ❌ | ❌ |
graph TD
A[Client Request] --> B[Gateway: 生成 requestId]
B --> C[Feign: 仅透传 traceID]
C --> D[Service-B: 日志丢失 requestId]
D --> E[ELK 查询断裂]
2.2 结构化日志误用:字符串拼接替代field注入引发解析失效
日志结构失焦的典型表现
当开发者用 log.Info("user " + uid + " failed login at " + time.Now().String()) 替代字段化记录,原始结构被扁平化为不可索引的纯文本。
错误示例与后果
// ❌ 反模式:字符串拼接破坏结构
log.Info("login_failed user_id=" + userID + " status=401 ip=" + ip)
// ✅ 正确:显式字段注入
log.Info("login failed", "user_id", userID, "status_code", 401, "client_ip", ip)
逻辑分析:前者生成无分隔符、无类型标记的字符串(如 "login_failed user_id=U123 status=401 ip=192.168.1.5"),导致日志采集器(如 Filebeat)无法提取 status_code 字段;后者通过键值对序列化,保障 JSON 解析时字段可映射。
解析失败影响对比
| 场景 | 字段可查询性 | 告警触发能力 | 聚合统计支持 |
|---|---|---|---|
| 字符串拼接 | ❌ | ❌ | ❌ |
| Field 注入 | ✅ | ✅ | ✅ |
graph TD
A[日志写入] --> B{是否使用field注入?}
B -->|否| C[文本流→正则提取→高误判率]
B -->|是| D[结构化JSON→字段直取→零解析开销]
2.3 日志采样策略缺位:高频日志淹没关键错误且无分级降级机制
当系统每秒产生数万条 INFO 级日志时,ERROR 日志常被稀释在滚动日志流中,人工巡检失效,告警响应延迟超 15 分钟。
常见采样陷阱
- 固定频率采样(如每 100 条取 1 条)无视日志语义,可能漏掉连续错误链;
- 全量采集导致存储成本激增,Kafka 分区堆积严重;
- 缺乏基于异常模式的动态权重(如
NullPointerException出现频次突增应自动升权)。
动态采样逻辑示例
// 基于错误类型与上下文热度的加权采样
if (log.level == ERROR && errorPatternCache.get(pattern) > THRESHOLD) {
sampler.sample(1.0); // 关键错误强制全采
} else if (log.level == INFO) {
sampler.sample(Math.min(0.01, 1.0 / Math.sqrt(activeRequests))); // 负载越高,INFO 采样率越低
}
errorPatternCache 统计最近 60 秒同类堆栈哈希频次;activeRequests 反映当前服务压力,实现负载感知降级。
采样策略效果对比
| 策略 | ERROR 捕获率 | 日志体积降幅 | 告警平均延迟 |
|---|---|---|---|
| 全量采集 | 100% | — | 8.2 min |
| 固定 1% 采样 | 43% | 99% | 22.7 min |
| 动态分级采样 | 99.8% | 92% | 1.9 min |
graph TD
A[原始日志流] --> B{按 level & pattern 分类}
B -->|ERROR/CRITICAL| C[100% 透传]
B -->|WARN + 高频 pattern| D[动态提升至 30%]
B -->|INFO/DEBUG| E[负载自适应衰减]
C & D & E --> F[统一日志管道]
2.4 日志生命周期失控:未集成context.Done()导致goroutine泄漏与日志截断
问题根源:阻塞写入无视取消信号
当日志写入器(如 io.MultiWriter 封装的网络/文件输出)未监听 ctx.Done(),goroutine 将永久等待底层 I/O 完成,即使父任务已超时或取消。
func logAsync(ctx context.Context, writer io.Writer, msg string) {
// ❌ 危险:完全忽略 ctx.Done()
_, _ = writer.Write([]byte(msg + "\n")) // 阻塞直至写完或失败
}
该函数无上下文感知能力,一旦 writer 暂挂(如磁盘满、网络抖动),goroutine 永不退出,且后续日志因 channel 缓冲区满而被静默丢弃。
典型泄漏场景对比
| 场景 | 是否响应 cancel | 是否截断日志 | goroutine 状态 |
|---|---|---|---|
原生 log.Printf |
否 | 否 | 阻塞 |
log/slog + context |
是 | 否 | 及时退出 |
| 自定义异步写入器 | 否(默认) | 是 | 泄漏 |
正确模式:带超时的上下文感知写入
func logWithContext(ctx context.Context, writer io.Writer, msg string) error {
done := make(chan error, 1)
go func() { done <- writer.Write([]byte(msg + "\n")) }()
select {
case err := <-done: return err
case <-ctx.Done(): return ctx.Err() // ✅ 主动响应取消
}
}
逻辑分析:启动 goroutine 执行写入并立即返回控制权;主协程通过 select 等待结果或取消信号。ctx.Err() 返回 context.Canceled 或 context.DeadlineExceeded,确保资源可回收。
2.5 日志采集对接盲区:stdout/stderr未对齐OpenTelemetry Log Spec导致Agent丢日志
当容器化应用仅将日志输出到 stdout/stderr,而 OpenTelemetry Collector 的 filelog 或 docker receiver 未启用结构化解析时,原始日志行会缺失 observed_timestamp、severity_number 等必需字段,触发 OTel Log Spec 校验失败,被 silently drop。
数据同步机制
OTel Log Spec 要求每条日志必须包含:
time_unix_nano(或observed_time_unix_nano)severity_numberbody(必须为 string 或 structured value)
典型错误配置
# ❌ 缺失 parser 和 attribute mapping,日志无 severity_number
receivers:
filelog/legacy:
include: ["/var/log/pods/*/*.log"]
start_at: "beginning"
该配置下,Collector 将纯文本行作为
body.string_value注入,但未设置severity_number(默认为SEVERITY_NUMBER_UNSPECIFIED),违反 spec 的MUST级约束,Agent 直接丢弃。
正确映射示例
# ✅ 启用 regex parser 并显式注入 severity
filelog/otlp:
include: ["/var/log/pods/*/*.log"]
operators:
- type: regex_parser
regex: '^(?P<time>\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d+Z) (?P<level>INFO|WARN|ERROR) (?P<msg>.*)$'
parse_to: body
- type: move
from: attributes.level
to: severity_text
- type: severity_number
severity_text: attributes.severity_text
severity_numberoperator 根据severity_text自动映射为SEVERITY_NUMBER_INFO/WARN/ERROR,满足 spec 强制要求。
| 字段 | 是否必需 | OTel Spec 来源 | Agent 行为若缺失 |
|---|---|---|---|
observed_time_unix_nano |
是 | §3.1.1 | 降级为 time_unix_nano,不丢弃 |
severity_number |
是 | §3.2.2 | 立即丢弃整条日志 |
body |
是 | §3.1.3 | 触发 schema validation error |
graph TD
A[容器 stdout 写入] --> B[Collector filelog receiver]
B --> C{是否配置 severity_number 映射?}
C -->|否| D[丢弃:severity_number == UNSPECIFIED]
C -->|是| E[通过校验 → Exporter]
第三章:Metrics暴露的三个反模式及标准埋点实践
3.1 指标命名违反Semantic Conventions:自定义前缀与unit缺失引发Prometheus聚合异常
常见错误命名示例
以下指标违背 OpenMetrics 语义规范:
# ❌ 错误:自定义前缀 + 缺失 unit + 非标准后缀
myapp_http_request_duration_seconds_sum{job="api"}
myapp_db_query_count_total{env="prod"}
myapp_前缀破坏跨服务可聚合性(应统一用http_、process_等标准命名空间);seconds_sum中seconds是 unit,但未按*_duration_seconds格式置于末尾;_total后缀虽正确,但db_query_count缺少unit(应为db_query_duration_seconds_total或db_query_operations_total)。
正确命名对照表
| 错误指标名 | 正确指标名 | 违反项 |
|---|---|---|
myapp_http_request_duration_seconds_sum |
http_request_duration_seconds_sum |
自定义前缀、unit位置错误 |
myapp_db_query_count_total |
db_query_operations_total |
unit缺失、语义模糊 |
聚合异常根源(Mermaid)
graph TD
A[采集端上报 myapp_http_request_duration_seconds_sum] --> B[Prometheus label matching]
B --> C{metric_name 不匹配 http_* 规范}
C --> D[rate()/histogram_quantile() 计算失败]
C --> E[Alertmanager 无法关联标准 SLO 规则]
3.2 Counter误用为Gauge:请求计数未区分success/fail导致SLI计算失真
当监控系统将单一 http_requests_total Counter 同时累加成功与失败请求时,SLI(如 success_rate = success / (success + failure))因缺失正交指标而无法准确计算。
错误实践示例
# ❌ 反模式:用同一Counter混记所有请求
http_requests_total{method="POST",status="200"} 1240
http_requests_total{method="POST",status="500"} 1240 # 同一指标名,仅靠label区分
此写法虽符合Prometheus数据模型,但若下游告警/SLI仪表盘错误地对 status=~"2.*" 求和后除以总量,会因Counter重置、抓取时序错位引入统计偏差——Counter本身不支持“瞬时值差分归因”。
推荐方案:语义化指标命名
| 指标类型 | 正确命名 | 语义说明 |
|---|---|---|
| Counter | http_requests_success_total |
仅记录成功请求累积量 |
| Counter | http_requests_failure_total |
仅记录失败请求累积量 |
| Gauge | http_active_requests |
当前并发请求数(可增可减) |
SLI计算逻辑修复
# ✅ 基于正交Counter的SLI表达式(抗抓取偏移)
rate(http_requests_success_total[1h])
/
(rate(http_requests_success_total[1h]) + rate(http_requests_failure_total[1h]))
注:
rate()自动处理Counter重置与采样对齐,避免手动delta()引入负值风险。
3.3 指标生命周期错配:局部变量注册指标实例造成内存泄漏与重复注册panic
问题根源
Prometheus 客户端库要求 Collector 实例在整个应用生命周期内唯一且稳定。若在函数作用域内创建并注册指标(如 HTTP 处理器中),每次调用将生成新实例,触发重复注册 panic 或长期驻留导致内存泄漏。
典型错误代码
func handleRequest(w http.ResponseWriter, r *http.Request) {
// ❌ 错误:每次请求都新建并注册
counter := promauto.NewCounter(prometheus.CounterOpts{
Name: "http_request_total",
Help: "Total number of HTTP requests",
})
counter.Inc()
}
逻辑分析:promauto.NewCounter 内部调用 Register(),而默认 Registry 不允许同名指标重复注册;同时 counter 无法被 GC(因被全局 registry 强引用),造成内存持续增长。
正确实践
- ✅ 在
init()或main()中一次性注册 - ✅ 使用
promauto.With(registry).NewCounter(...)隔离测试 registry - ✅ 避免闭包捕获指标变量
| 场景 | 是否安全 | 原因 |
|---|---|---|
| 包级变量注册 | ✅ | 单例,生命周期匹配 |
局部变量 + promauto |
❌ | 重复注册 panic |
| 局部变量 + 自定义 registry | ⚠️ | 仅限测试,需手动清理 |
graph TD
A[HTTP Handler 调用] --> B[创建局部指标实例]
B --> C{Registry.Register?}
C -->|已存在同名| D[panic: duplicate metric]
C -->|首次注册| E[指标加入全局 registry]
E --> F[GC 无法回收:强引用链]
第四章:Trace透传的四重网络边界穿透与跨协程Hook实践
4.1 HTTP中间件中context.WithValue覆盖原始span:未使用otelhttp.NewHandler正确注入span
当手动在中间件中调用 context.WithValue(ctx, oteltrace.SpanKey, span),会直接覆盖由 otelhttp.NewHandler 注入的、具备完整生命周期管理的 span,导致 span 上下文断裂。
常见错误写法
func badMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
span := trace.SpanFromContext(ctx) // 此时 span 可能已由 otelhttp 注入
// ❌ 错误:强制覆盖 span,破坏 parent-child 关系
r = r.WithContext(context.WithValue(ctx, oteltrace.SpanKey, span))
next.ServeHTTP(w, r)
})
}
该操作绕过 OpenTelemetry 的 span propagation 机制,使子 span 丢失 trace ID 关联与采样决策上下文。
正确实践对比
| 方式 | 是否维护 span 生命周期 | 是否支持自动注入/提取 | 是否兼容 otelhttp |
|---|---|---|---|
手动 context.WithValue |
❌ 否(覆盖而非嵌套) | ❌ 否(忽略 baggage/tracestate) | ❌ 破坏链路完整性 |
otelhttp.NewHandler |
✅ 是(自动 defer End) | ✅ 是(基于 HTTP header) | ✅ 原生适配 |
根本原因流程
graph TD
A[HTTP 请求进入] --> B[otelhttp.NewHandler 包装]
B --> C[自动提取 traceparent header]
C --> D[创建 child span 并注入 context]
D --> E[中间件调用 next.ServeHTTP]
E --> F[响应返回时自动 End span]
G[手动 WithValue] -.->|覆盖 ctx 中 span| D
G --> H[子 span 失去 parent 引用]
4.2 Goroutine启动时trace上下文丢失:go func() {}中未显式传递context.Background().WithSpan()
问题根源
Go 的 go 语句启动新 goroutine 时不会自动继承父 goroutine 的 context,包括已注入的 trace span。context.WithSpan() 生成的 context 仅在线程内传递,无法跨 goroutine 自动传播。
典型错误示例
span := tracer.StartSpan("parent")
ctx := context.WithValue(context.Background(), spanKey, span) // ❌ 无效:WithValue 不被 otel-go 识别
go func() {
// span 丢失!此处无法获取 parent span 作为 parent
child := tracer.StartSpan("child") // 变成孤立根 span
}()
逻辑分析:
context.WithValue无法被 OpenTelemetry 的otel.GetTextMapPropagator().Inject()感知;必须使用context.WithSpan()(来自go.opentelemetry.io/otel/trace)且需显式传入 goroutine。
正确做法
- ✅ 显式传参:
go func(ctx context.Context) { ... }(parentCtx) - ✅ 使用
otel.TraceProvider().Tracer(...).Start(ctx, ...)自动链接 span
| 方式 | 是否保留 trace 上下文 | 是否推荐 |
|---|---|---|
go func() {}() + context.Background() |
否 | ❌ |
go func(ctx context.Context) {}(parentCtx) |
是 | ✅ |
context.WithValue(ctx, key, span) |
否(非标准传播) | ❌ |
graph TD
A[Parent Goroutine] -->|StartSpan| B[Span S1]
B -->|ctx passed explicitly| C[Child Goroutine]
C -->|StartSpan with ctx| D[Span S2, child of S1]
4.3 异步消息队列(如Kafka/RabbitMQ)中traceID未序列化注入headers或properties字段
当微服务通过消息队列异步通信时,若生产者未将当前Span的traceID显式写入消息元数据,链路追踪将在此处断裂。
数据同步机制
RabbitMQ需通过MessageProperties注入,Kafka则依赖ProducerRecord.headers:
// Kafka 生产端示例
ProducerRecord<String, String> record =
new ProducerRecord<>("order-topic", "order-123");
record.headers().add("traceId", traceId.getBytes(StandardCharsets.UTF_8));
traceId.getBytes()确保二进制安全;若遗漏此步,Consumer端无法从空headers还原调用上下文。
常见疏漏点
- 使用String序列化替代二进制header(导致编码/解码不一致)
- 消息重试后traceID未更新,引发跨请求污染
- Spring Cloud Stream默认不透传MDC,需自定义
ChannelInterceptor
| 组件 | 元数据字段 | 是否默认支持trace透传 |
|---|---|---|
| RabbitMQ | MessageProperties |
否 |
| Kafka | Headers |
否(需手动注入) |
| Pulsar | MessageMetadata |
是(v3.0+) |
graph TD
A[Producer] -->|发送消息| B[Broker]
B -->|投递| C[Consumer]
A -.->|缺失traceId| D[链路断点]
C -.->|无trace上下文| E[监控盲区]
4.4 数据库调用链断裂:SQL driver未启用OpenTracing/OTel插件,DB连接池未透传span context
当应用启用分布式追踪后,数据库访问常成为调用链“断点”——因原生 database/sql 驱动不感知 trace context,且连接池(如 sqlx、gorm)默认不注入 span。
常见断裂原因
- SQL driver 未包装为 OpenTracing/OTel 兼容驱动(如
opentracing/sql或go.opentelemetry.io/instrumentation/database/sql) - 连接获取/释放过程中未显式传递
context.WithValue(ctx, oteltrace.SpanKey{}, span) - ORM 层(如 GORM v1.x)未启用插件模式,或 v2+ 未配置
otelgorm中间件
修复示例(OTel Go)
import "go.opentelemetry.io/instrumentation/database/sql"
// 包装原生驱动
db, _ := sql.Open("mysql", "user:pass@tcp(127.0.0.1:3306)/test")
sql.Register("mysql-otel", &sql.Driver{
Driver: mysql.MySQLDriver{},
Tracer: otelsql.NewTracer(),
})
此代码将 MySQL 驱动注册为带 OTel 自动埋点的
mysql-otel。otelsql.NewTracer()默认捕获query,exec,connect等事件,并从入参context.Context提取并延续 span context;若上下文无 span,则创建独立子 span。
连接池透传关键点
| 组件 | 是否透传 context | 说明 |
|---|---|---|
db.QueryContext |
✅ | 必须使用带 context 的方法 |
sql.Open |
❌ | 仅初始化,不触发 trace |
gorm.Open |
⚠️(v2+需显式配置) | 需 gorm.Config{PrepareStmt: true} + otelgorm.Plugin |
graph TD
A[HTTP Handler] -->|ctx with span| B[Service Logic]
B -->|ctx passed| C[db.QueryContext]
C --> D[otelsql interceptor]
D -->|enriched span| E[MySQL Server]
第五章:可观测性基建落地checklist与实习生自检指南
核心组件部署验证
确保 OpenTelemetry Collector(v0.98+)以 DaemonSet + Deployment 混合模式运行:Node 节点上采集主机指标与日志,中心集群部署处理 traces 的负载均衡实例。执行 kubectl get pods -n observability -l app=otel-collector 验证全部 Pod 处于 Running 状态,且 restarts 为 0;若某 Pod restarts ≥3,需立即检查其日志 kubectl logs -n observability <pod-name> --previous 中是否出现 failed to export trace data: rpc error: code = Unavailable 类错误——这通常指向后端 Jaeger/Tempo 地址配置错误或 TLS 证书过期。
数据链路连通性自测表
实习生每日晨会前须完成以下三链路探活(建议使用 curl + jq 自动化脚本):
| 链路类型 | 测试命令示例 | 预期响应码 | 关键校验点 |
|---|---|---|---|
| Metrics(Prometheus) | curl -s http://prometheus.observability.svc:9090/api/v1/query?query=up{job="kubernetes-pods"} | jq '.status' |
200 | 返回 "success" |
| Logs(Loki) | curl -s "http://loki.observability.svc:3100/loki/api/v1/labels?start=$(date -d '5 minutes ago' +%s)000000000" | jq 'length' |
200 | 响应值 > 0 |
| Traces(Tempo) | curl -sI http://tempo.observability.svc:3200/api/traces/00000000000000000000000000000000 | head -n 1 |
404(非500/timeout) | 证明服务可达且路由正常 |
告警规则有效性验证
在 Prometheus Web UI 中手动触发 KubePodCrashLooping 告警:向测试命名空间部署一个故意失败的 Pod(kubectl apply -f ./test-crash.yaml),等待 2 分钟后执行 curl -s "http://prometheus.observability.svc:9090/api/v1/alerts" | jq '.data.alerts[] | select(.labels.alertname=="KubePodCrashLooping")'。若无输出,检查 alert_rules.yaml 中 for: 2m 是否被误写为 for: 2s,或 Alertmanager 配置中 route.receiver 是否指向了已删除的 Slack webhook。
日志采样率合规检查
通过 Loki 查询语句验证采样策略生效:
count_over_time({namespace="default", container="payment-service"} |~ "ERROR" [1h]) / count_over_time({namespace="default", container="payment-service"} [1h])
结果应稳定在 0.05±0.005(即 5% 采样)。若高于 8%,检查 otel-collector config 中 processors.batch 和 processors.tail_sampling 配置是否冲突——常见错误是同时启用 probabilistic_sampler 和 rate_limiting 导致降采样失效。
实习生自检流程图
flowchart TD
A[启动自检脚本] --> B{Collector Pod 全部 Ready?}
B -->|否| C[检查 ConfigMap 挂载与 env 变量]
B -->|是| D{Prometheus 能查到 kube-state-metrics?}
D -->|否| E[验证 ServiceMonitor 是否 label 匹配]
D -->|是| F{Loki 日志延迟 < 30s?}
F -->|否| G[检查 fluent-bit output 插件 buffer 配置]
F -->|是| H[提交当日 checklog.md 到 /observability/docs]
依赖服务健康快照
运行以下命令生成环境基线快照(保存为 health-snapshot-$(date +%Y%m%d).json):
kubectl get nodes -o wide && \
kubectl get pods -n observability -o wide && \
kubectl exec -n observability deploy/prometheus -c prometheus -- sh -c 'curl -s http://localhost:9090/readyz' && \
kubectl exec -n observability deploy/loki -c loki -- sh -c 'curl -s http://localhost:3100/readyz'
Trace 上下文透传验证
在 Java 微服务中注入 -javaagent:/opt/otel/javaagent.jar 后,调用 /api/v1/orders 接口并提取响应头 X-Trace-ID,随后在 Tempo UI 中搜索该 ID。若结果为空,检查 Spring Boot 应用是否遗漏 spring-cloud-starter-sleuth 依赖,或 application.yml 中 spring.sleuth.enabled: true 是否被 profile 覆盖。
