跳转至

可观测性体系——指标、追踪、日志、健康检查

ZeroBot 基因:ZeroBot 无任何可观测性基础设施(无指标、无追踪、日志仅限简单打印)。这是 Remilia 从零独立设计的能力。参阅 11-zerobot-inspiration.md

设计理念

可观测性不是事后补丁,而是框架的一等公民。Remilia 提供了四维可观测性:

    Metrics (Prometheus)
         │
    什么是发生?──── Tracing (OpenTelemetry)
         │
    谁在发生?────── Logging (zerolog)
         │
    还活着吗?────── Health Check

每层回答不同的问题,数据互补而不重叠。

1. Prometheus 指标

自定义注册表隔离

func NewMetricsCollector(namespace string) *Collector {
    return NewMetricsCollectorWithRegistry(namespace, prometheus.NewRegistry())
}

每个 Collector 使用独立的 prometheus.Registry,避免同一进程多次调用时因 metric 名称重复导致 panic。这在多引擎测试场景或同一进程中运行多个 Bot 实例时至关重要。

指标分类

事件层指标

eventProcessed *prometheus.CounterVec   // 按事件类型 + 状态(成功/失败)
eventDropped   *prometheus.CounterVec   // 丢弃原因(限流/熔断/超时)
eventLatency   *prometheus.HistogramVec // 延迟分布(P50/P90/P99)

业务层指标

commandInvocations *prometheus.CounterVec  // 按命令名 + 状态
messageSent        *prometheus.CounterVec  // 按消息类型 + 状态
messageLatency     *prometheus.HistogramVec
botUptime          prometheus.Gauge         // 1=运行中, 0=停止

中间件层指标

retryAttempts  *prometheus.CounterVec   // 重试次数
retrySuccesses prometheus.Counter       // 重试成功
retryFailures  prometheus.Counter       // 重试失败
deadLetterQueueSize    prometheus.Gauge  // 死信队列积压

插件层指标

pluginHandlers *prometheus.GaugeVec    // 各插件的 Handler 数
pluginMatchers *prometheus.GaugeVec    // 各插件的 Matcher 数
pluginLoadTime *prometheus.HistogramVec
pluginUnloadTime *prometheus.HistogramVec

平台层指标

platformAdapterUp     *prometheus.GaugeVec  // 适配器运行状态
platformDisconnects   *prometheus.CounterVec // 断连次数
platformAdapterErrors *prometheus.CounterVec // 致命错误

端点暴露

mc := metrics.NewMetricsCollector("mybot")
http.Handle("/metrics", promhttp.HandlerFor(mc.Registry(), promhttp.HandlerOpts{}))

2. OpenTelemetry 分布式追踪

架构

import (
    "go.opentelemetry.io/otel"
    "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp"
    "go.opentelemetry.io/otel/sdk/trace"
)

使用 OTLP HTTP Exporter 将 Trace 发送到兼容 OpenTelemetry 的后端(Jaeger、Tempo、Datadog 等)。

自适应采样

type AdaptiveSampler struct {
    mu          sync.RWMutex
    targetTPS   float64          // 目标每秒采样数
    currentRate float64          // 当前采样率(0.0 ~ 1.0)
    // 动态调整
    sampleCount atomic.Int64
    windowStart time.Time
}

func (s *AdaptiveSampler) ShouldSample(parameters trace.SamplingParameters) trace.SamplingResult {
    // 根据当前 TPS 动态调整采样率
    // 高负载时自动降采样,低负载时全采样
    elapsed := time.Since(s.windowStart)
    tps := float64(s.sampleCount.Load()) / elapsed.Seconds()

    if tps > s.targetTPS {
        s.currentRate = max(0.01, s.currentRate * 0.9) // 降采样
    } else {
        s.currentRate = min(1.0, s.currentRate * 1.1) // 升采样
    }
    // ...
}

在大量事件处理时,自适应采样避免了 Trace 数据量过大导致的后端压力。

Span 注入

中间件 Tracing() 自动为每个事件处理链创建 Span:

func Tracing() eventctx.Middleware {
    return func(next eventctx.Handler) eventctx.Handler {
        return func(ctx *eventctx.Context) error {
            tracer := otel.Tracer("remilia")
            _, span := tracer.Start(ctx, "handle_event")
            defer span.End()

            span.SetAttributes(
                attribute.String("event.type", string(ctx.GetEventType())),
                attribute.String("platform", ctx.GetEventPlatform()),
            )

            return next(ctx)
        }
    }
}

3. 结构化日志

基于 rs/zerolog(零分配 JSON 日志器)实现:

import "github.com/rs/zerolog"

logger.WithFields(logger.Fields{
    "event_type": ctx.GetEventType(),
    "platform":   ctx.GetEventPlatform(),
    "latency":    time.Since(start),
    "matcher":    m.Source,
}).Info("event processed")

零分配设计:zerolog 在热路径上避免堆分配,适合高性能场景。

多输出:支持同时输出到 stdout、文件、syslog 等。

分级控制:开发环境用 ConsoleWriter(彩色可读),生产环境用 JSON 格式。

4. 健康检查

type Check struct {
    checkers []Checker  // 聚合多个检查器
    mu       sync.RWMutex
}

type Checker interface {
    Name() string
    Check(ctx context.Context) CheckResult
}

type CheckResult struct {
    Status  Status  // Healthy / Degraded / Unhealthy
    Message string
    Details map[string]any
}

Bot 内置检查器

// 适配器健康检查
type AdapterHealthChecker struct {
    adapter platform.Adapter
}

func (c *AdapterHealthChecker) Check(ctx context.Context) CheckResult {
    if c.adapter.IsRunning() {
        return CheckResult{Status: Healthy}
    }
    return CheckResult{Status: Unhealthy, Message: "adapter not running"}
}

// 引擎健康检查
type EngineHealthChecker struct {
    engine *engine.Engine
}

// Bot 状态检查
type BotStatusChecker struct {
    bot *Bot
}

聚合响应

func (b *Bot) Health() health.CheckResponse {
    ctx, cancel := context.WithTimeout(b.Context(), 5*time.Second)
    defer cancel()
    return b.health.Check(ctx)
}

// 响应示例
{
    "status": "healthy",
    "uptime": "12h34m56s",
    "checks": [
        {"name": "qq-adapter", "status": "healthy"},
        {"name": "engine", "status": "healthy"},
        {"name": "bot-status", "status": "healthy"}
    ]
}

HTTP 端点

// 通过 infra/server 暴露
http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
    result := bot.Health()
    json.NewEncoder(w).Encode(result)
})

5. pprof 性能分析

内置 pprof 服务器,通过 WithPprof 选项启用:

bot, err := remilia.NewBotBuilder().
    WithPlatformAdapter(adapter).
    WithPprof(":6060").
    Build()

自动在 Bot 启动时启动 pprof,停止时优雅关闭。

完整集成示例

func setupObservability(bot *remilia.Bot) {
    // 1. Prometheus 指标
    mc := metrics.NewMetricsCollector("remilia")
    bot.Engine().SetMetricsCollector(mc)
    http.Handle("/metrics", promhttp.HandlerFor(mc.Registry(), promhttp.HandlerOpts{}))

    // 2. OpenTelemetry 追踪
    tp := initTracerProvider()
    otel.SetTracerProvider(tp)

    // 3. 结构化日志
    logger.SetLevel(logger.InfoLevel)

    // 4. 健康检查端点
    http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
        json.NewEncoder(w).Encode(bot.Health())
    })

    // 5. 启动 HTTP Server
    go http.ListenAndServe(":9090", nil)
}

迭代过程

V0:logrus + 基础指标

初始版本使用 sirupsen/logrus 作为日志库,指标使用在根包定义的简单结构体:

// V0 — logrus + 基础 metrics
import "github.com/sirupsen/logrus"

var log = logrus.New()

func (e *Engine) DoSomething() {
    log.WithField("key", "value").Info("doing something")
}
// V0 metrics.go — 直接在根包
type MetricsCollector struct {
    // 使用全局 prometheus 注册表
    eventProcessed *prometheus.CounterVec
}

问题: - logrus 在热路径上分配较多(entry 对象、interface{} 转换),在 475K msg/s 下 GC 压力大 - 使用全局 Prometheus 注册表:同一进程运行两个 Bot 实例时,metric 名称冲突导致 panic - 指标收集器没有统一的管理方式——Engine、middleware、插件各自注册,容易重复 - 健康检查简陋——只有一个 IsRunning() 方法

V1:zerolog 替换 logrus(零分配日志)

决定将 logrus 替换为 rs/zerolog,后者在设计上就是零分配:

// V1 — zerolog 代码(commit 69aecda)
import "github.com/rs/zerolog"

// zerolog 的 Logger 值类型(可拷贝),热路径上不分配
logger.WithFields(logger.Fields{
    "event_type": ctx.GetEventType(),
    "latency":    time.Since(start),
}).Info("event processed")

对比

方面 logrus zerolog
热路径分配 每次调用分配 Entry + map 零分配(值类型 + 链式调用)
序列化 反射 类型化写入
性能(单条日志) ~1-2 μs ~100-300 ns
接口风格 可变参数 WithField(k, v) 链式 .Str().Int().Msg()

这一替换使热路径日志开销降低约 10 倍

V2:自定义 Prometheus Registry 替代全局注册表

// V1 方式 — 全局注册表,多实例必 panic
func NewMetricsCollector(namespace string) *Collector {
    factory := promauto.With(prometheus.DefaultRegisterer)
    factory.NewCounterVec(...) // 第二次调用时 panic: "duplicate metrics collector registration"
}
// V2(当前)— 独立 Registry
func NewMetricsCollector(namespace string) *Collector {
    registry := prometheus.NewRegistry()
    return NewMetricsCollectorWithRegistry(namespace, registry)
}

// 暴露时使用自定义的 Handler
http.Handle("/metrics", promhttp.HandlerFor(mc.Registry(), promhttp.HandlerOpts{}))

同时将指标收集器通过 infra/metrics 包统一管理,消除各模块各自注册的问题:

// 统一的指标收集器
type Collector struct {
    registry  prometheus.Registerer  // 自定义注册表

    // 事件层
    eventProcessed *prometheus.CounterVec
    eventDropped   *prometheus.CounterVec
    eventLatency   *prometheus.HistogramVec

    // 中间件层
    retryAttempts  *prometheus.CounterVec
    deadLetterQueueSize prometheus.Gauge

    // 插件层
    pluginHandlers *prometheus.GaugeVec
    pluginLoadTime *prometheus.HistogramVec

    // 平台层
    platformAdapterUp     *prometheus.GaugeVec
    platformDisconnects   *prometheus.CounterVec
}

V3:健康检查框架化 + 自适应追踪采样

从 Bot 中提取通用的健康检查框架:

// V0 健康的检查 — Bot 方法
func (b *Bot) IsHealthy() bool {
    return b.wh.IsRunning() && b.engine != nil
}
// V3(当前)— 框架化
type Check struct {
    checkers []Checker  // 聚合多个检查器
}

// 各模块实现 Checker 接口
health.AddChecker(NewAdapterHealthChecker(adapter))
health.AddChecker(health.NewEngineHealthChecker(engine))
health.AddChecker(NewBotStatusChecker(bot))

追踪系统引入了自适应采样——在高负载时自动降采样以避免压垮 Trace 后端:

type AdaptiveSampler struct {
    targetTPS   float64   // 目标每秒采样数
    currentRate float64   // 动态调整的采样率
}

func (s *AdaptiveSampler) ShouldSample() bool {
    tps := s.measureTPS()
    if tps > s.targetTPS {
        s.currentRate *= 0.9  // 降采样
    } else {
        s.currentRate = min(1.0, s.currentRate * 1.1)  // 升采样
    }
    return rand.Float64() < s.currentRate
}

TPO 配置从 Zipkin Exporter 迁移到 OTLP HTTP Exporter(OTEL 标准协议),与更广泛的生态兼容。

迭代历程

版本 核心变化 解决的问题
V0 logrus + 全局 Prometheus + 简陋健康检查 快速实现
V1 zerolog 替换 logrus 热路径日志零分配,降低 GC 压力
V2 自定义 Prometheus Registry + infra/metrics 统一管理 多实例冲突、分散注册
V3(当前) 健康检查框架化 + 自适应追踪采样 + OTLP 标准 聚合检查、自适应数据量

设计权衡

维度 选择 理由
指标 独立 Prometheus Registry 避免多实例冲突
追踪 自适应采样 控制数据量,降低后端成本
日志 zerolog 零分配 高性能场景必须
健康 聚合多检查器 单一端点获取全貌
分析 内置 pprof 开箱即用调试