第一章:Go语言在可观测性领域的核心定位与架构价值
Go语言凭借其原生并发模型、轻量级goroutine调度、静态编译输出及极低的运行时开销,天然契合可观测性系统对高吞吐、低延迟、强稳定性的严苛要求。在分布式追踪、指标采集与日志聚合等核心场景中,Go已成为Prometheus、OpenTelemetry Collector、Jaeger Agent、Grafana Agent等主流可观测性组件的首选实现语言。
为什么可观测性基础设施偏爱Go
- 启动快、内存稳:单二进制可执行文件无依赖,容器内秒级启动;GC停顿通常控制在毫秒级,避免采样抖动干扰真实业务链路;
- 并发即原语:
net/http服务器默认为每个请求分配goroutine,天然支撑高并发指标拉取(如Prometheus每15s批量抓取数百实例); - 交叉编译友好:
GOOS=linux GOARCH=arm64 go build -o collector-arm64 .可一键构建边缘设备兼容版本,适配K8s DaemonSet或IoT边缘节点部署。
典型可观测性组件的Go实践特征
| 组件类型 | Go关键能力体现 | 实际效果示例 |
|---|---|---|
| 指标采集器 | sync.Pool复用metrics buffer |
减少30% GC压力,QPS提升2.1倍(实测于10k target环境) |
| 分布式追踪Agent | context.Context贯穿span生命周期 |
精确传播traceID,避免跨goroutine丢失上下文 |
| 日志转发器 | bufio.Scanner + io.Pipe流式处理 |
支持TB级日志实时解析,内存占用恒定 |
快速验证Go可观测性服务的最小可行性
以下代码启动一个暴露标准Prometheus指标端点的HTTP服务:
package main
import (
"net/http"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
func main() {
// 定义自定义计数器
httpRequests := prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "http_requests_total",
Help: "Total number of HTTP requests.",
},
[]string{"method", "status"},
)
prometheus.MustRegister(httpRequests)
// 记录一次请求
http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
httpRequests.WithLabelValues(r.Method, "200").Inc()
w.WriteHeader(http.StatusOK)
w.Write([]byte("OK"))
})
// 暴露/metrics端点
http.Handle("/metrics", promhttp.Handler())
http.ListenAndServe(":2112", nil) // Prometheus默认拉取端口
}
执行后访问 curl http://localhost:2112/metrics 即可获得符合OpenMetrics规范的文本格式指标,无缝接入任意Prometheus生态。
第二章:用Go开发Prometheus Exporter的完整实践
2.1 Exporter设计原理与OpenMetrics规范深度解析
Exporter本质是将第三方系统指标转换为Prometheus可采集格式的适配层,核心职责是指标发现、采集、转换与暴露。
数据同步机制
采用拉取(Pull)模型,由Prometheus定期HTTP GET /metrics端点。Exporter需保证响应符合OpenMetrics文本格式规范。
OpenMetrics兼容要点
- 必须使用
# TYPE和# HELP注释行 - 指标名称须符合
[a-zA-Z_:][a-zA-Z0-9_:]*正则 - 时间序列须含标签对,如
http_requests_total{method="GET",status="200"} 12345
# 示例:标准OpenMetrics响应生成片段
def render_metrics():
yield '# HELP http_requests_total Total HTTP requests handled'
yield '# TYPE http_requests_total counter'
yield 'http_requests_total{method="POST",status="500"} 42' # 标签键值必须加引号
逻辑分析:
yield逐行输出确保流式响应;method和status为必需标签,其值必须为合法字符串字面量(双引号包裹),否则违反OpenMetrics v1.0.0语法。
| 特性 | Prometheus原生 | OpenMetrics v1.0 |
|---|---|---|
单位注释 # UNIT |
❌ 不支持 | ✅ 支持 |
| 指标类型扩展(info) | ❌ | ✅ |
graph TD
A[目标系统API] --> B[Exporter采集器]
B --> C[指标映射规则]
C --> D[OpenMetrics文本序列化]
D --> E[HTTP响应体]
2.2 自定义指标建模:Gauge、Counter、Histogram的Go实现与语义对齐
Prometheus 客户端库为 Go 提供了语义明确的指标原语。正确选择类型是语义对齐的第一步:
- Counter:只增不减,适用于请求总数、错误累计等单调递增场景
- Gauge:可增可减,适合当前活跃连接数、内存使用量等瞬时状态
- Histogram:按预设桶(bucket)统计分布,用于响应延迟、处理时长等观测
// 初始化三类核心指标(注册到默认注册器)
var (
httpRequestsTotal = prometheus.NewCounterVec(
prometheus.CounterOpts{Namespace: "app", Name: "http_requests_total"},
[]string{"method", "status"},
)
memUsageBytes = prometheus.NewGauge(prometheus.GaugeOpts{
Namespace: "app", Name: "memory_usage_bytes",
Help: "Current resident memory in bytes",
})
reqLatency = prometheus.NewHistogram(prometheus.HistogramOpts{
Namespace: "app", Name: "request_latency_seconds",
Buckets: prometheus.ExponentialBuckets(0.01, 2, 8), // 0.01s ~ 2.56s
})
)
CounterVec支持多维标签聚合;Gauge直接调用Set()或Add();Histogram通过Observe(float64)记录样本——三者底层序列化格式与 Prometheus 服务端解析逻辑严格对齐。
| 指标类型 | 重置行为 | 适用聚合函数 | 典型 PromQL 查询 |
|---|---|---|---|
| Counter | 不支持重置(服务重启后需配合 rate()) |
rate(), increase() |
rate(app_http_requests_total[5m]) |
| Gauge | 可任意写入新值 | avg(), max(), last() |
avg(app_memory_usage_bytes) |
| Histogram | 桶计数独立累加 | histogram_quantile() |
histogram_quantile(0.95, sum(rate(app_request_latency_seconds_bucket[5m])) by (le)) |
2.3 高效采集逻辑:并发控制、采样策略与资源隔离机制
并发控制:动态线程池管理
采用 ScheduledThreadPoolExecutor 实现自适应并发度调节,依据实时队列积压量动态伸缩核心线程数:
// 基于监控指标的弹性线程池(核心参数说明)
ScheduledThreadPoolExecutor collectorPool =
new ScheduledThreadPoolExecutor(
minThreads, // 最小并发数(默认4),保障低负载时资源节约
maxThreads, // 峰值并发上限(默认32),防止单任务耗尽CPU
60L, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(1024), // 有界队列,触发背压而非OOM
new ThreadFactoryBuilder().setNameFormat("collector-%d").build()
);
该设计避免静态线程池在流量突增时吞吐骤降,同时通过有界队列强制上游限流。
采样策略对比
| 策略 | 适用场景 | 丢弃率可控性 | 实现复杂度 |
|---|---|---|---|
| 时间窗口随机 | 日志类弱序数据 | 中 | 低 |
| 一致性哈希采样 | 需保Key分布均匀场景 | 高 | 中 |
| 自适应速率限制 | 流量波动剧烈系统 | 高 | 高 |
资源隔离机制
graph TD
A[采集任务] --> B{资源调度器}
B -->|高优先级API| C[专用CPU核组]
B -->|日志批量任务| D[独立内存配额]
B -->|第三方插件| E[沙箱进程+ cgroups 限制]
2.4 HTTP暴露层构建:自定义/metrics路由、TLS/BasicAuth集成与健康检查端点
自定义 /metrics 路由
使用 Prometheus 客户端库暴露结构化指标:
http.Handle("/metrics", promhttp.Handler())
该行将标准指标处理器挂载到 /metrics,自动聚合注册的 Counter、Gauge 等指标;promhttp.Handler() 默认启用 Content-Type: text/plain; version=0.0.4,兼容所有 Prometheus 抓取器。
TLS 与 BasicAuth 集成
需组合中间件保障传输与访问安全:
| 中间件类型 | 作用 | 启用方式 |
|---|---|---|
http.ListenAndServeTLS |
启用 HTTPS | 提供 cert.pem 与 key.pem |
basic-auth middleware |
用户认证 | 拦截 /metrics 和 /healthz |
健康检查端点
http.HandleFunc("/healthz", func(w http.ResponseWriter, r *request.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("ok"))
})
此轻量端点返回 200 OK,供 Kubernetes Liveness Probe 或负载均衡器周期性探测;无依赖校验,确保快速响应。
graph TD
A[HTTP Server] --> B[/healthz]
A --> C[/metrics]
B --> D[Status OK]
C --> E[Prometheus Format]
E --> F[TLS + BasicAuth]
2.5 生产就绪能力:动态配置热加载、指标元数据注入与Exporter生命周期管理
生产环境要求监控组件具备零停机演进能力。核心支撑来自三重机制协同:
动态配置热加载
基于 fsnotify 监听 YAML 配置变更,触发 Reload() 接口重建采集器实例:
func (e *Exporter) Reload(cfg *Config) error {
e.mu.Lock()
defer e.mu.Unlock()
e.cfg = cfg // 原子替换配置引用
e.resetCollectors() // 清理旧指标注册表
return nil
}
e.cfg 引用更新确保线程安全;resetCollectors() 触发 prometheus.Unregister() 避免重复注册冲突。
指标元数据注入
通过 Desc 构造时注入 ConstLabels 与 VariableLabels,实现业务维度自动打标:
| 字段 | 用途 | 示例 |
|---|---|---|
job |
服务角色标识 | "api-gateway" |
env |
环境上下文 | "prod" |
Exporter 生命周期管理
graph TD
A[Init] --> B[Start: 启动采集协程]
B --> C{Config Changed?}
C -->|Yes| D[Reload: 安全切换]
C -->|No| E[Normal Collect]
D --> E
- 所有采集 goroutine 使用
context.WithCancel统一控制; Stop()方法阻塞等待所有采集任务 graceful shutdown。
第三章:基于Go构建轻量级Trace Collector的工程实践
3.1 OpenTelemetry Protocol(OTLP)接收器的Go原生实现
OTLP 接收器是 OpenTelemetry Collector 的核心数据入口,其 Go 原生实现基于 go.opentelemetry.io/collector/receiver/otlpreceiver,深度集成 protobuf 解析与 zstd/gzip 解压缩能力。
数据同步机制
接收器采用无锁通道 + 批处理协程模型:
// 启动 OTLP gRPC 服务端
srv := grpc.NewServer(
grpc.MaxRecvMsgSize(16 * 1024 * 1024), // 支持最大 16MB 请求
grpc.ChainUnaryInterceptor(authInterceptor),
)
otlpGRPC := otlpreceiver.NewFactory().CreateDefaultConfig()
otlpGRPC.(*config).Protocols.GRPC.Endpoint = "0.0.0.0:4317"
MaxRecvMsgSize显式放宽限制以兼容大型 TraceSpan 批次;authInterceptor可插拔认证逻辑,不影响协议层解码路径。
核心组件职责对比
| 组件 | 职责 | 是否可配置 |
|---|---|---|
otlpgrpc |
gRPC 传输层绑定与 TLS 协商 | ✅ |
otlphttp |
HTTP/1.1 + JSON/Protobuf 多格式路由 | ✅ |
exporterhelper |
队列、重试、超时封装 | ✅ |
graph TD
A[OTLP gRPC Request] --> B[Protobuf Unmarshal]
B --> C[Validation & Normalization]
C --> D[Signal Routing: Traces/Metrics/Logs]
D --> E[Export Pipeline]
3.2 Trace数据批处理与缓冲策略:内存队列、背压控制与持久化落盘
Trace数据洪流需在吞吐与稳定性间取得精妙平衡。内存队列作为首道缓冲,常采用无锁环形队列(如 Disruptor)降低GC压力;当写入速率持续超过消费能力,背压机制触发降级——拒绝新Span或动态缩减采样率。
数据同步机制
落盘前批量压缩并序列化为 Protocol Buffer:
// 批量写入磁盘前的缓冲组装
List<Span> batch = memoryQueue.drainTo(new ArrayList<>(MAX_BATCH_SIZE));
if (!batch.isEmpty()) {
byte[] packed = SpanBatch.newBuilder().addAllSpans(batch).build().toByteArray();
diskWriter.appendAsync(packed); // 异步落盘,避免阻塞采集线程
}
逻辑分析:drainTo 原子清空队列,避免迭代器并发异常;MAX_BATCH_SIZE=512 平衡延迟与IO效率;appendAsync 封装于独立IO线程池,解耦采集与存储路径。
背压响应策略对比
| 策略 | 触发条件 | 影响面 | 实现复杂度 |
|---|---|---|---|
| 采样率动态下调 | 队列填充率 > 90% 持续5s | 全局精度损失 | 中 |
| 写入拒绝 | 队列已满且等待超时 | 局部Span丢失 | 低 |
| 限速写入 | 持久化延迟 > 200ms | 采集端吞吐下降 | 高 |
graph TD
A[Trace采集线程] -->|生产Span| B[无锁内存队列]
B --> C{队列水位 > 85%?}
C -->|是| D[触发背压控制器]
D --> E[动态调整采样率]
D --> F[通知下游限速]
B -->|定期批量| G[压缩+序列化]
G --> H[异步落盘线程池]
3.3 采样决策引擎:基于速率、标签、服务拓扑的可插拔采样器设计
采样决策引擎是可观测性数据降噪的核心,需在高吞吐下兼顾精度与灵活性。其设计围绕三个正交维度:全局/局部速率控制、业务语义标签(如 env:prod, error:true)、实时服务拓扑关系(如调用链深度、下游服务SLA)。
可插拔策略注册机制
type Sampler interface {
Sample(ctx context.Context, span *Span) bool
}
// 注册示例:按标签+拓扑联合采样
registry.Register("tag-aware-topo", func(cfg map[string]any) Sampler {
return &TagAwareTopoSampler{
errorRate: cfg["error_rate"].(float64), // 错误span强制100%采样
maxDepth: int(cfg["max_depth"].(float64)), // 拓扑深度阈值
criticalSvc: cfg["critical_svc"].([]string), // 关键服务白名单
}
})
该注册模式解耦策略实现与调度逻辑;errorRate保障故障可观测性,maxDepth避免长链爆炸,criticalSvc确保核心路径全量捕获。
策略优先级与组合规则
| 策略类型 | 触发条件 | 优先级 | 是否可组合 |
|---|---|---|---|
| 恒定速率采样 | 全局QPS > 10k | 高 | 否 |
| 标签匹配采样 | env=staging 或 debug=true |
中 | 是 |
| 拓扑感知采样 | 调用下游含 payment-svc |
低 | 是 |
决策流程
graph TD
A[接收Span] --> B{标签匹配?}
B -->|是| C[应用TagSampler]
B -->|否| D{是否在关键拓扑路径?}
D -->|是| E[应用TopoSampler]
D -->|否| F[回退至RateSampler]
C & E & F --> G[返回采样结果]
第四章:Go驱动的Log Aggregator高可用架构实现
4.1 日志协议适配层:Syslog、JSON Lines、Fluentd Forward Protocol的统一接入
日志协议适配层是可观测性平台的“协议翻译中枢”,屏蔽下游采集端的语义差异,输出标准化事件流。
协议特征对比
| 协议 | 传输方式 | 结构化程度 | 认证/加密 | 典型场景 |
|---|---|---|---|---|
| Syslog (RFC 5424) | UDP/TCP | 弱(需解析PRI、timestamp、hostname等字段) | 可选TLS | 网络设备、传统OS |
| JSON Lines | HTTP/TCP | 强(每行一个合法JSON对象) | 推荐HTTPS | 云原生应用、CI/CD流水线 |
| Fluentd Forward | TCP + MessagePack | 强(含tag、time、record嵌套结构) | 支持TLS + auth | Fluentd生态链路 |
统一接入核心逻辑(Go伪代码)
func adaptLog(payload []byte, protocol string) (Event, error) {
switch protocol {
case "syslog":
return parseRFC5424(payload) // 提取structured-data、msg、app-name
case "jsonl":
return parseJSONLines(payload) // 验证JSON有效性,补全缺失字段如@timestamp
case "fluentd":
return decodeForward(payload) // 解包MessagePack,提取tag/time/record并归一化为通用schema
}
}
该函数实现协议识别→格式解析→字段映射→时间戳对齐→元数据注入五步归一化流程,确保下游处理器仅消费统一 Event{Timestamp, Tags, Fields, Raw} 结构。
4.2 结构化日志处理流水线:Parser、Filter、Enricher的Pipeline式编排
日志处理不再依赖单体解析器,而是通过可插拔、职责分离的组件链式协作。
核心组件职责划分
- Parser:将原始文本(如
{"ts":"2024-06-01T12:34:56Z","level":"INFO","msg":"user login"})反序列化为结构化事件对象 - Filter:基于表达式(如
event.level != "DEBUG")丢弃无关日志 - Enricher:注入上下文字段(如
host_ip,service_name,trace_id)
流水线执行流程
graph TD
A[Raw Log Line] --> B[Parser]
B --> C[Filter]
C --> D[Enricher]
D --> E[Structured Event]
示例代码(Go风格Pipeline构建)
pipeline := NewPipeline().
WithParser(JSONParser{}).
WithFilter(LevelFilter{Levels: []string{"INFO", "WARN", "ERROR"}}).
WithEnricher(HostEnricher{}, TraceEnricher{})
JSONParser{}:支持RFC 7468兼容的JSON/NDJSON格式;自动推导时间戳字段ts或@timestampLevelFilter:白名单机制,避免运行时正则匹配开销HostEnricher:从环境变量读取HOSTNAME并注入为host.name字段
| 组件 | 输入类型 | 输出类型 | 可配置性 |
|---|---|---|---|
| Parser | []byte |
*Event |
高(格式、时区、字段映射) |
| Filter | *Event |
*Event or nil |
中(布尔表达式、字段路径) |
| Enricher | *Event |
*Event |
高(外部API、缓存策略) |
4.3 日志路由与分发:基于字段/正则/服务标识的多目的地异步投递
日志投递需兼顾灵活性与性能,核心在于解耦路由决策与传输执行。
路由策略优先级模型
- 字段匹配(如
service: "auth-api")→ 最高效,直查哈希表 - 正则匹配(如
level =~ /^(ERROR|FATAL)$/)→ 适配动态模式 - 服务标识兜底(如
k8s.pod.labels.app)→ 支持自动发现
异步分发流水线
# routes.yaml 示例
routes:
- match: { service: "payment-gateway" }
destinations: [kafka://logs-prod, elasticsearch://alerting]
- match_regexp: { message: ".*timeout.*504.*" }
destinations: [slack://oncall, prometheus://alert_counter]
该配置声明式定义路由规则;match 为精确字段匹配,match_regexp 触发 JIT 编译的正则引擎;每个匹配项异步扇出至多个目标,由独立 worker 池并行推送,避免阻塞主采集线程。
投递可靠性保障
| 机制 | 说明 |
|---|---|
| 批量缓冲 | 200ms/1MB 双触发阈值 |
| 本地磁盘暂存 | WAL 日志防止进程崩溃丢数据 |
| 目标健康探测 | 自动熔断异常 endpoint 并降级 |
graph TD
A[原始日志] --> B{路由引擎}
B -->|service=auth| C[Kafka]
B -->|ERROR regex| D[Slack + Prometheus]
B -->|default| E[Cloud Storage]
4.4 存储抽象与后端对接:本地文件轮转、Elasticsearch Bulk API与Loki Push API封装
存储抽象层需统一处理异构后端写入逻辑,核心聚焦三类目标:本地磁盘的可靠持久化、ES的高吞吐索引、Loki的流式日志推送。
数据同步机制
采用策略模式封装不同后端:
- 本地轮转:基于
time.Ticker触发os.Rename+os.Create实现按小时切分 - ES Bulk:批量构造
[]elastic.BulkIndexRequest,控制size <= 10MB且count <= 500 - Loki:序列化为
PushRequest,按stream分组并签名X-Scope-OrgID
// Loki推送封装示例(含租户隔离)
func (l *LokiClient) Push(ctx context.Context, streams []logproto.Stream) error {
req := &logproto.PushRequest{Streams: streams}
data, _ := proto.Marshal(req)
return l.client.Post(ctx, "/loki/api/v1/push", "application/x-protobuf", bytes.NewReader(data))
}
该函数将日志流序列化为 Protocol Buffer 二进制格式,通过 HTTP POST 提交至 Loki /loki/api/v1/push 端点;X-Scope-OrgID 头由 client 自动注入,实现多租户隔离。
| 后端类型 | 批量粒度 | 超时设置 | 错误重试 |
|---|---|---|---|
| 本地文件 | 单文件(≤100MB) | 无 | 不适用 |
| Elasticsearch | ≤500 条或 ≤10MB | 30s | 指数退避(3次) |
| Loki | ≤1MB / 请求 | 10s | 幂等重试(5次) |
graph TD
A[日志事件] --> B{抽象路由}
B -->|本地模式| C[RotateWriter]
B -->|ES模式| D[BulkIndexer]
B -->|Loki模式| E[LokiPusher]
C --> F[按小时切分+GZIP压缩]
D --> G[JSON序列化+HTTP批提交]
E --> H[Protobuf序列化+gRPC兼容HTTP]
第五章:开源项目结构全景与可观测平台协同演进路径
现代云原生开源项目已不再孤立演进,其目录结构、构建契约与发布规范正深度耦合可观测性能力。以 CNCF 毕业项目 Prometheus 为例,其标准仓库结构中 ./cmd/prometheus/ 下的主程序明确嵌入了 /metrics /debug/pprof /healthz 三类内置端点,且通过 promhttp.HandlerFor(registry, promhttp.HandlerOpts{}) 将指标导出逻辑直接绑定至 HTTP 路由层——这种“结构即可观测”的设计范式已成为主流。
核心组件与指标生命周期对齐
开源项目的模块划分(如 pkg/storage/, pkg/web/, pkg/rules/)天然对应可观测性关注域:
pkg/storage/输出prometheus_tsdb_head_series_created_total等时序存储指标;pkg/rules/暴露prometheus_rule_evaluations_total及prometheus_rule_evaluation_duration_seconds;pkg/web/提供promhttp_metric_handler_requests_total和响应延迟直方图。
这种映射非人工配置,而是通过 Go 的init()函数自动注册指标变量,确保代码结构变更即触发指标拓扑更新。
构建流水线嵌入可观测性验证
以下 GitHub Actions 片段展示了如何在 CI 阶段强制校验可观测性契约:
- name: Validate /metrics endpoint
run: |
docker run -d --name prom-test -p 9090:9090 prom/prometheus:v2.47.1
sleep 5
curl -s http://localhost:9090/metrics | grep -q "prometheus_build_info" || exit 1
docker stop prom-test
该检查被纳入 make test 流程,任何破坏指标端点可用性的 PR 将被自动拦截。
多维度可观测性协同演进矩阵
| 项目阶段 | 开源结构特征 | 对应可观测能力 | 协同机制示例 |
|---|---|---|---|
| 初始化 | config/config.go 定义全局配置 |
prometheus_config_last_reload_success_timestamp_seconds |
配置加载失败时自动触发 config_reload_failed 告警 |
| 运行时扩展 | plugin/ 目录支持动态插件加载 |
prometheus_plugin_loads_total |
插件启动超时 3s 自动上报 plugin_init_duration_seconds{quantile="0.99"} |
| 版本升级 | CHANGELOG.md 语义化版本记录 |
prometheus_version_info{version="2.47.1"} |
Grafana 仪表盘通过 version_info 标签自动过滤旧版指标 |
社区驱动的可观测性治理实践
Kubernetes SIG Instrumentation 维护的 instrumentation-guidelines 明确要求:所有新增 controller 必须在 pkg/controller/<name>/controller.go 中实现 metrics.Register() 接口,并将指标命名空间限定为 kubernetes_<component>_<metric_name>。该规范已落地于 ClusterAutoscaler v1.28+,其 autoscaler_node_group_size_changes_total 指标可直接关联到 pkg/autoscaler/cluster/ 目录下的节点扩缩容核心逻辑。
动态服务发现与指标元数据同步
Prometheus 的 file_sd_configs 与 Consul 服务发现集成后,其 __meta_consul_tags 标签会自动注入至指标标签集,而开源项目 consul-k8s 在 helm/charts/consul/templates/_helpers.tpl 中预定义 consul_service_tags 模板函数,确保 Kubernetes Service 的 consul-tags annotation 被准确映射为 __meta_consul_tags,形成从 Helm Chart 结构 → Consul 元数据 → Prometheus 标签的全链路一致性。
这种结构与可观测性的共生关系正在重塑开源协作边界:当 pkg/trace/ 目录被添加时,OpenTelemetry Collector 的 otelcol_exporter_send_failed_total 指标便自动出现在贡献者本地 make dev 启动的调试环境中。
