4. 值班机器人Trace接入实战:在机器人核心流程中插入Span

本章将指导你在值班机器人的三个核心流程——接收告警、决策、执行动作——中手动插入 OpenTelemetry Span,构建端到端追踪链路。通过 Trace,你可以精确追踪每一条告警从入站到动作执行的完整生命周期,快速定位延迟瓶颈或逻辑错误。

4.1 核心流程Span设计

在接入 Trace 前,需要先定义 Span 的层级结构与属性。下表列出了三个核心阶段对应的 Span 名称、职责及关键属性:

阶段 Span 名称 职责 关键属性(Attributes)
接收告警 alert.receive 解析入站告警源(Prometheus、Grafana、自研监控) alert.source, alert.severity, alert.name
决策 alert.decision 匹配规则引擎、查询历史、判断是否需要执行动作 decision.rule_id, decision.match_result, decision.duration_ms
执行动作 alert.action.execute 调用外部系统(Jira、PagerDuty、Webhook、自愈脚本) action.type, action.target, action.status
设计原则:每个 Span 的 name 使用 层级.子模块 命名规范,便于在 Jaeger 或 Grafana Tempo 中按前缀搜索。属性中必须包含 alert.id 作为关联键,确保同一告警的所有 Span 可被串联。

4.2 端到端追踪架构图

下图展示了值班机器人内部 Span 的嵌套关系与上下文传播路径:

值班机器人端到端追踪架构 值班机器人 Trace 架构:Span 嵌套与上下文传播 告警源 Prometheus / Grafana alert.receive 解析告警 / 提取属性 alert.id, alert.severity alert.decision 规则匹配 / 历史查询 decision.rule_id alert.action.execute 调用外部系统 action.type, action.status 上下文传播机制: 每个 Span 通过 OpenTelemetry Context 自动传递 Trace ID 和 Span ID。即使决策阶段调用外部缓存或数据库,子 Span 仍会继承父 Span 的 Trace ID。 alert.decision.cache 查询历史告警 alert.decision.rule 匹配规则引擎 alert.action.http 调用 Webhook 接收阶段 决策阶段 执行阶段 子 Span HTTP 调用

4.3 Python 代码实战:插入 Span

以下代码演示如何在 Python 值班机器人中,使用 OpenTelemetry SDK 手动创建嵌套 Span。假设你已经配置了全局 TracerProvider 并导出到 Jaeger 或 Tempo。

from opentelemetry import trace
from opentelemetry.trace import Status, StatusCode
import time

tracer = trace.get_tracer("alert-bot", "1.0.0")

def process_alert(alert_dict: dict):
    """值班机器人核心流程:接收 -> 决策 -> 执行"""
    # 创建根 Span:接收告警
    with tracer.start_as_current_span("alert.receive") as receive_span:
        receive_span.set_attribute("alert.id", alert_dict.get("id"))
        receive_span.set_attribute("alert.source", alert_dict.get("source", "unknown"))
        receive_span.set_attribute("alert.severity", alert_dict.get("severity", "info"))

        # 模拟告警解析耗时
        time.sleep(0.05)

        # 创建子 Span:决策阶段
        with tracer.start_as_current_span("alert.decision") as decision_span:
            decision_span.set_attribute("decision.rule_id", alert_dict.get("rule_id", "default"))
            decision_span.set_attribute("decision.match_result", "true")

            # 决策内部子 Span:查询历史缓存
            with tracer.start_as_current_span("alert.decision.cache") as cache_span:
                cache_span.set_attribute("cache.hit", "true")
                time.sleep(0.02)

            # 决策内部子 Span:规则匹配
            with tracer.start_as_current_span("alert.decision.rule") as rule_span:
                rule_span.set_attribute("rule.evaluation", "pass")
                time.sleep(0.03)

            decision_span.set_attribute("decision.duration_ms", 50)

        # 创建子 Span:执行动作
        with tracer.start_as_current_span("alert.action.execute") as action_span:
            action_span.set_attribute("action.type", "webhook")
            action_span.set_attribute("action.target", "https://hooks.example.com/alert")

            # 模拟 HTTP 调用
            try:
                # 实际调用外部系统
                time.sleep(0.1)
                action_span.set_attribute("action.status", "success")
                action_span.set_status(Status(StatusCode.OK))
            except Exception as e:
                action_span.set_attribute("action.status", "failed")
                action_span.set_attribute("action.error", str(e))
                action_span.set_status(Status(StatusCode.ERROR, str(e)))

        # 根 Span 记录总耗时
        receive_span.set_attribute("alert.total_duration_ms", 200)

# 模拟一条告警
alert = {
    "id": "alert-001",
    "source": "prometheus",
    "severity": "critical",
    "rule_id": "cpu_high_95"
}
process_alert(alert)
关键点:
  • 使用 tracer.start_as_current_span 上下文管理器自动处理 Span 的开始与结束。
  • 子 Span 自动继承父 Span 的 Trace ID,无需手动传递 Context。
  • 通过 set_attribute 添加业务维度信息,便于后续检索与过滤。
  • 异常时调用 set_status(StatusCode.ERROR) 标记 Span 为失败状态。

4.4 Go 代码实战:插入 Span

Go 版本的实现同样遵循相同的 Span 层级结构。以下代码使用 OpenTelemetry Go SDK:

package main

import (
    "context"
    "fmt"
    "time"

    "go.opentelemetry.io/otel"
    "go.opentelemetry.io/otel/attribute"
    "go.opentelemetry.io/otel/trace"
)

var tracer = otel.Tracer("alert-bot", trace.WithInstrumentationVersion("1.0.0"))

type Alert struct {
    ID       string
    Source   string
    Severity string
    RuleID   string
}

func ProcessAlert(ctx context.Context, alert Alert) {
    // 根 Span:接收告警
    ctx, receiveSpan := tracer.Start(ctx, "alert.receive",
        trace.WithAttributes(
            attribute.String("alert.id", alert.ID),
            attribute.String("alert.source", alert.Source),
            attribute.String("alert.severity", alert.Severity),
        ),
    )
    defer receiveSpan.End()

    time.Sleep(50 * time.Millisecond)

    // 子 Span:决策阶段
    ctx, decisionSpan := tracer.Start(ctx, "alert.decision",
        trace.WithAttributes(
            attribute.String("decision.rule_id", alert.RuleID),
            attribute.String("decision.match_result", "true"),
        ),
    )

    // 决策内部子 Span:查询缓存
    _, cacheSpan := tracer.Start(ctx, "alert.decision.cache",
        trace.WithAttributes(attribute.Bool("cache.hit", true)),
    )
    time.Sleep(20 * time.Millisecond)
    cacheSpan.End()

    // 决策内部子 Span:规则匹配
    _, ruleSpan := tracer.Start(ctx, "alert.decision.rule",
        trace.WithAttributes(attribute.String("rule.evaluation", "pass")),
    )
    time.Sleep(30 * time.Millisecond)
    ruleSpan.End()

    decisionSpan.SetAttributes(attribute.Int64("decision.duration_ms", 50))
    decisionSpan.End()

    // 子 Span:执行动作
    _, actionSpan := tracer.Start(ctx, "alert.action.execute",
        trace.WithAttributes(
            attribute.String("action.type", "webhook"),
            attribute.String("action.target", "https://hooks.example.com/alert"),
        ),
    )

    // 模拟 HTTP 调用
    time.Sleep(100 * time.Millisecond)
    actionSpan.SetAttributes(attribute.String("action.status", "success"))
    actionSpan.End()

    receiveSpan.SetAttributes(attribute.Int64("alert.total_duration_ms", 200))
    fmt.Printf("Processed alert %s\n", alert.ID)
}

func main() {
    // 假设已初始化全局 TracerProvider
    ctx := context.Background()
    alert := Alert{
        ID:       "alert-002",
        Source:   "grafana",
        Severity: "warning",
        RuleID:   "mem_high_80",
    }
    ProcessAlert(ctx, alert)
}
避坑指南:
  • Go 中必须显式传递 context.Context,否则子 Span 无法继承父 Span 的 Trace ID。
  • 不要忘记调用 defer span.End() 或手动 span.End(),否则 Span 会一直处于打开状态,导致追踪数据不完整。
  • 属性值尽量使用强类型方法(如 attribute.String、attribute.Int64),避免运行时类型转换开销。

4.5 验证 Trace 接入效果

完成代码接入后,通过以下步骤验证 Trace 是否正确生成:

  1. 启动导出器:确保 OTLP gRPC 或 HTTP 导出器已配置,并指向 Jaeger Collector 或 Grafana Tempo。
  2. 触发告警:向值班机器人发送一条测试告警(例如通过 curl 模拟 Prometheus Webhook)。
  3. 查询 Trace:在 Jaeger UI 或 Grafana Explore 中搜索 alert.receive,按 Trace ID 查看完整链路。
  4. 检查 Span 属性:确认每个 Span 都携带了预期的属性(alert.id、decision.rule_id、action.status 等)。
  5. 验证嵌套关系:展开 Trace 视图,确认 alert.decision.cache 和 alert.decision.rule 是 alert.decision 的子 Span。
快速验证命令:使用 otel-cli 或 grpcurl 直接向 OTLP 端点发送测试 Span,无需启动完整机器人。例如:otel-cli span --service alert-bot --name "alert.receive" --attrs "alert.id=test-001"

4.6 常见问题与排查

问题现象 可能原因 解决方案
Trace 中只有根 Span,没有子 Span Context 未正确传递,或子 Span 使用了不同的 Tracer 检查是否使用同一个 tracer 实例,并确保 ctx 在父子 Span 间传递
Span 属性缺失或为空 属性设置时机在 Span 结束后,或属性键拼写错误 在 span.End() 之前设置属性,并核对属性名是否与查询条件一致
Trace 未出现在 Jaeger/Tempo 中 导出器配置错误、网络不通、采样率过低 检查 OTLP 端点地址、防火墙规则,并确认采样率 >= 1(调试时可设为 1.0)
Span 耗时显示为 0 未正确调用 span.End(),或使用了 Start 但不带 WithTimestamp 确保每个 Span 都调用了 End(),并检查 SDK 版本是否支持自动计时

通过本章实战,你已成功在值班机器人核心流程中插入 Span,实现了从告警接收到动作执行的端到端追踪。下一阶段将基于这些 Trace 数据构建故障定位 Dashboard,并配置基于 Span 延迟的告警规则。