Posted in

Golang可观测性入门黑洞:从log打点到metrics暴露再到trace透传,实习生常漏掉的4个关键hook点

第一章: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_number
  • body(必须为 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_number operator 根据 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 覆盖。

从 Consensus 到容错,持续探索分布式系统的本质。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注