Prometheus 源码专题【左扬精讲】—— Prometheus 3.4.0 源码解析:告警规则的定义与执行(基于 AlertingRule)

Prometheus 源码专题【左扬精讲】—— Prometheus 3.4.0 源码解析:告警规则的定义与执行(基于 AlertingRule)

        在 Prometheus 的监控体系中,告警规则(Alerting Rule)是实现异常监控与告警的核心组件。它通过定义 PromQL 查询条件、告警触发阈值、状态转换逻辑等,将监控指标转化为可行动的告警信号。本文将从 Prometheus 3.4.0 源码角度,深入解析告警规则的核心结构体AlertingRule及其执行流程,揭示从规则定义到告警触发的完整链路。

一、告警规则的核心 AlertingRule 结构体(源码解析)

        告警规则的定义在源码中在AlertingRule结构体,其核心代码位于(https://github.com/prometheus/prometheus/blob/v3.4.2/rules/alerting.go#L108)。该结构体封装了告警规则的所有属性,是规则解析、执行、状态管理的基础。

1.1 AlertingRule 结构体定义

// An AlertingRule generates alerts from its vector expression.
// AlertingRule 结构体用于从向量表达式生成告警。
type AlertingRule struct {
	// The name of the alert.
	// 告警的名称,用于唯一标识该告警规则。
	name string
	// The vector expression from which to generate alerts.
	// 用于生成告警的向量表达式,通常是 PromQL 表达式。
	vector parser.Expr
	// The duration for which a labelset needs to persist in the expression
	// output vector before an alert transitions from Pending to Firing state.
	// 一个标签集需要在表达式输出向量中持续存在的时长,之后告警才会从 Pending 状态转变为 Firing 状态。
	holdDuration time.Duration
	// The amount of time that the alert should remain firing after the
	// resolution.
	// 告警在条件解决后应继续保持 Firing 状态的时长,用于防止告警抖动等问题。
	keepFiringFor time.Duration
	// Extra labels to attach to the resulting alert sample vectors.
	// 要附加到生成的告警样本向量上的额外标签,可用于对告警进行分类或添加额外信息。
	labels labels.Labels
	// Non-identifying key/value pairs.
	// 非标识性的键值对,通常用于存储告警的描述信息、运行手册链接等额外信息。
	annotations labels.Labels
	// External labels from the global config.
	// 从全局配置中获取的外部标签,通常用于在不同环境或实例中区分告警。
	externalLabels map[string]string
	// The external URL from the --web.external-url flag.
	// 从 --web.external-url 标志获取的外部 URL,可用于在告警通知中提供额外的链接。
	externalURL string
	// true if old state has been restored. We start persisting samples for ALERT_FOR_STATE
	// only after the restoration.
	// 表示是否已恢复旧状态的布尔值。只有在恢复状态后,才会开始持久化 ALERT_FOR_STATE 的样本。
	restored *atomic.Bool
	// Time in seconds taken to evaluate rule.
	// 评估规则所花费的时间,以秒为单位。
	evaluationDuration *atomic.Duration
	// Timestamp of last evaluation of rule.
	// 规则最后一次评估的时间戳。
	evaluationTimestamp *atomic.Time
	// The health of the alerting rule.
	// 告警规则的健康状态,可能的值有 "unknown", "ok", "err"。
	health *atomic.String
	// The last error seen by the alerting rule.
	// 告警规则最后一次遇到的错误。
	lastError *atomic.Error
	// activeMtx Protects the `active` map.
	// 用于保护 `active` 映射的互斥锁,确保并发访问时的线程安全。
	activeMtx sync.Mutex
	// A map of alerts which are currently active (Pending or Firing), keyed by
	// the fingerprint of the labelset they correspond to.
	// 一个映射,存储当前处于活动状态(Pending 或 Firing)的告警,键为对应标签集的指纹。
	active map[uint64]*Alert

	logger *slog.Logger

	dependenciesMutex sync.RWMutex
	// 依赖于当前规则输出的规则列表。
	dependentRules    []Rule
	// 当前规则所依赖的规则列表。
	dependencyRules   []Rule
}

// NewAlertingRule constructs a new AlertingRule.
// NewAlertingRule 函数用于创建一个新的 AlertingRule 实例。
func NewAlertingRule(
	// 告警的名称。
	name string, 
	// 用于生成告警的向量表达式。
	vec parser.Expr, 
	// 告警从 Pending 状态转变为 Firing 状态所需的持续时长。
	hold time.Duration, 
	// 告警在条件解决后应继续保持 Firing 状态的时长。
	keepFiringFor time.Duration, 
	// 要附加到生成的告警样本向量上的额外标签。
	labels labels.Labels, 
	// 非标识性的键值对,用于存储告警的描述信息等。
	annotations labels.Labels, 
	// 从全局配置中获取的外部标签。
	externalLabels labels.Labels, 
	// 从 --web.external-url 标志获取的外部 URL。
	externalURL string, 
	// 表示是否已恢复旧状态的布尔值。
	restored bool, 
	// 日志记录器。
	logger *slog.Logger, 
) *AlertingRule {
	// 将外部标签转换为映射。
	el := externalLabels.Map()

	return &AlertingRule{
		name:                name,
		vector:              vec,
		holdDuration:        hold,
		keepFiringFor:       keepFiringFor,
		labels:              labels,
		annotations:         annotations,
		externalLabels:      el,
		externalURL:         externalURL,
		active:              map[uint64]*Alert{},
		logger:              logger,
		restored:            atomic.NewBool(restored),
		health:              atomic.NewString(string(HealthUnknown)),
		evaluationTimestamp: atomic.NewTime(time.Time{}),
		evaluationDuration:  atomic.NewDuration(0),
		lastError:           atomic.NewError(nil),
	}
}

1.2 关键字段解析

        AlertingRule:Prometheus 告警系统的核心数据结构,负责封装告警规则的配置元数据、状态转换逻辑及评估上下文。通过分层设计,该结构体将字段划分为状态管理、内容定义、执行监控三大模块,形成了完整的告警生命周期管理体系。

1.2.1、规则标识与表达式

      • name:告警规则名称,用于唯一标识规则,通常在告警列表、通知中作为标识出现。
      • vector:PromQL 向量表达式,定义告警触发条件,直接关联告警内容的生成逻辑。

1.2.2、告警状态管理字段

      • holdDuration:控制告警从 `Pending` 状态转变为 `Firing` 状态所需的持续时长,避免短暂波动触发误报,与告警生命周期管理密切相关。
      • keepFiringFor:告警条件解决后继续保持 `Firing` 状态的时长,用于防止告警频繁恢复/触发(抖动),增强告警稳定性。
      • restored:布尔标志,指示是否已从持久化存储恢复历史状态,影响告警状态持久化的起始时机。
      • active:存储当前活跃告警(`Pending` 或 `Firing`)的映射表,按标签集指纹索引,是状态管理的核心数据结构。

1.2.3、告警内容管理字段

      • labels:附加到告警的键值对标签,用于分类、过滤和聚合,直接影响告警内容的元数据。
      • annotations:非标识性描述信息,如告警摘要、解决建议等,是告警通知内容的重要组成部分。
      • externalLabels:全局配置中的外部标签(如环境、集群信息),用于补充告警上下文,统一不同规则的标识维度。
      • externalURL:外部链接,通常指向告警详情页面或文档,为告警通知提供额外的交互入口。

1.2.4、执行状态与监控字段

      • evaluationDuration:规则评估耗时,反映规则复杂度和性能影响,用于性能监控。
      • evaluationTimestamp:规则最后一次评估的时间戳,用于追踪规则执行频率和时效性。
      • health:规则健康状态(`ok`/`error`/`pending`),反映规则执行的稳定性。
      • lastError:规则执行过程中遇到的最后一个错误,用于故障排查和告警质量监控。

1.2.5、实例化与依赖管理

      • NewAlertingRule:构造函数,用于创建并初始化 AlertingRule 实例,确保必要字段的正确配置。

二、告警规则的执行流程(源码解析)

        在 Prometheus 中,AlertingRule 的执行主要由规则管理器 Manager(位于 https://github.com/prometheus/prometheus/blob/v3.4.2/rules/manager.go#L92 )统一调度。系统会按照设定的评估周期(默认 15 秒,可通过 evaluation_interval 配置调整),定时调用每条规则的 Eval 方法,完成告警规则的评估与状态更新。整个流程可分为以下几个核心阶段:

2.1 规则调度与入口

结构体定位

核心结构体:Manager(rules/manager.go)https://github.com/prometheus/prometheus/blob/v3.4.2/rules/manager.go#L92 

      • 负责统一管理和调度所有的告警规则(AlertingRule:https://github.com/prometheus/prometheus/blob/v3.4.2/rules/manager.go#L415)和记录规则(RecordingRule:https://github.com/prometheus/prometheus/blob/v3.4.2/rules/recording.go#L34)。
      • 其字段 groups map[string]*Group(https://github.com/prometheus/prometheus/blob/v3.4.2/rules/manager.go#L94)存储所有已加载的规则组。

调度主循环

入口方法:Manager.Run()(https://github.com/prometheus/prometheus/blob/v3.4.2/rules/manager.go#L174)

      • 启动规则管理器,内部调用 m.start() (https://github.com/prometheus/prometheus/blob/v3.4.2/rules/manager.go#L180),并阻塞直到 m.done 被关闭。
        // Run starts processing of the rule manager. It is blocking.
        func (m *Manager) Run() {
            // 记录启动信息日志
            m.logger.Info("Starting rule manager...")
            
            // 调用start方法初始化并启动规则管理器的核心组件
            // 该方法会启动后台协程处理规则评估和告警管理
            m.start()
            
            // 阻塞当前协程,直到接收到关闭信号
            // m.done是一个channel,当规则管理器需要关闭时会关闭这个channel
            <-m.done
        }
      • 每个规则组(Group)(https://github.com/prometheus/prometheus/blob/v3.4.2/rules/group.go#L44) 会在独立 goroutine 中运行其 run() (https://github.com/prometheus/prometheus/blob/v3.4.2/rules/group.go#L207)方法。
      • 规则组启动:Group.run(ctx)
        • 通过定时器(time.Ticker),按照 interval(评估周期,默认 15 秒,可配置)定时触发评估。
        • 每次 tick 时,调用 g.evalIterationFunc(ctx, g, evalTimestamp),默认实现为 DefaultEvalIterationFunc。
          源代码太长了,这里进行简化(https://github.com/prometheus/prometheus/blob/v3.4.2/rules/group.go#L207)
          // rules/group.go
          func (g *Group) run(ctx context.Context) {
          	// ...初始化...
          	tick := time.NewTicker(g.interval)
          	defer tick.Stop()
          	for {
          		select {
          		case <-g.done:
          			return
          		case <-tick.C:
          			// 定时触发评估
          			g.evalIterationFunc(ctx, g, evalTimestamp)
          		}
          	}
          }
      • 评估函数:DefaultEvalIterationFunc (https://github.com/prometheus/prometheus/blob/v3.4.2/rules/manager.go#L77)
// DefaultEvalIterationFunc is the default implementation of
// GroupEvalIterationFunc that is periodically invoked to evaluate the rules
// in a group at a given point in time and updates Group state and metrics
// accordingly. Custom GroupEvalIterationFunc implementations are recommended
// to invoke this function as well, to ensure correct Group state and metrics
// are maintained.
func DefaultEvalIterationFunc(ctx context.Context, g *Group, evalTimestamp time.Time) {
    // 增加已调度的规则组迭代次数指标计数
    g.metrics.IterationsScheduled.WithLabelValues(GroupKey(g.file, g.name)).Inc()

    // 记录规则组评估开始时间
    start := time.Now()
    
    // 执行规则组内所有规则的评估   <==  主要看这里
    g.Eval(ctx, evalTimestamp)
    
    // 计算规则组评估耗时
    timeSinceStart := time.Since(start)

    // 记录规则组评估耗时指标(秒为单位)
    g.metrics.IterationDuration.Observe(timeSinceStart.Seconds())
    
    // 更新规则评估总时间(用于计算平均评估时间)
    g.updateRuleEvaluationTimeSum()
    
    // 记录当前规则组评估耗时
    g.setEvaluationTime(timeSinceStart)
    
    // 记录当前规则组评估开始时间
    g.setLastEvaluation(start)
    
    // 记录当前规则组评估使用的时间戳
    g.setLastEvalTimestamp(evalTimestamp)
}

这段代码是 Prometheus 规则组评估的默认实现,主要功能是:

            • 记录规则组评估调度次数指标
            • 执行规则组内所有规则的评估逻辑
            • 记录评估耗时相关指标
            • 更新规则组的评估时间、上次评估时间和评估时间戳等状态信息
            • 为规则组评估提供统一的指标收集和状态管理机制
            • 建议自定义评估函数调用此函数,以确保统一的指标和状态管理

依次调用组内每条规则的 Eval 方法,完成实际的规则评估和状态更新。

// Eval evaluates the rule expression and then creates pending alerts and fires
// or removes previously pending alerts accordingly.
func (r *AlertingRule) Eval(ctx context.Context, queryOffset time.Duration, ts time.Time, query QueryFunc, externalURL *url.URL, limit int) (promql.Vector, error) {
    // 将规则详情添加到上下文中,用于追踪和日志记录
    ctx = NewOriginContext(ctx, NewRuleDetail(r))
    
    // 执行规则表达式查询,获取当前时间点的指标值
    // 使用ts.Add(-queryOffset)进行时间偏移,允许查询过去的数据点
    res, err := query(ctx, r.vector.String(), ts.Add(-queryOffset))
    if err != nil {
        return nil, err
    }

    // 存储当前查询结果中所有样本的指纹,用于后续比较
    resultFPs := map[uint64]struct{}{}

    // 创建标签构建器,用于构建警报标签
    lb := labels.NewBuilder(labels.EmptyLabels())
    sb := labels.NewScratchBuilder(0)
    var vec promql.Vector
    alerts := make(map[uint64]*Alert, len(res))
    
    // 处理查询结果中的每个样本点
    for _, smpl := range res {
        // 提取样本的标签,用于模板渲染
        l := smpl.Metric.Map()

        // 创建模板数据对象,包含标签、外部标签、外部URL和样本值
        tmplData := template.AlertTemplateData(l, r.externalLabels, r.externalURL, smpl)
        
        // 注入一些方便用户使用的模板变量,避免直接使用Go模板语法
        defs := []string{
            "{{$labels := .Labels}}",
            "{{$externalLabels := .ExternalLabels}}",
            "{{$externalURL := .ExternalURL}}",
            "{{$value := .Value}}",
        }

        // 定义模板扩展函数,用于渲染标签和注释中的模板表达式
        expand := func(text string) string {
            tmpl := template.NewTemplateExpander(
                ctx,
                strings.Join(append(defs, text), ""),
                "__alert_"+r.Name(),
                tmplData,
                model.Time(timestamp.FromTime(ts)),
                template.QueryFunc(query),
                externalURL,
                nil,
            )
            result, err := tmpl.Expand()
            if err != nil {
                // 模板扩展失败时,返回错误信息并记录警告日志
                result = fmt.Sprintf("<error expanding template: %s>", err)
                r.logger.Warn("Expanding alert template failed", "err", err, "data", tmplData)
            }
            return result
        }

        // 构建警报标签集
        lb.Reset(smpl.Metric)
        lb.Del(labels.MetricName)  // 移除原始指标名称标签
        
        // 应用规则中定义的标签,使用模板扩展函数处理动态值
        r.labels.Range(func(l labels.Label) {
            lb.Set(l.Name, expand(l.Value))
        })
        // 添加alertname标签,固定为规则名称
        lb.Set(labels.AlertName, r.Name())

        // 构建警报注释集,同样使用模板扩展函数处理动态值
        sb.Reset()
        r.annotations.Range(func(a labels.Label) {
            sb.Add(a.Name, expand(a.Value))
        })
        annotations := sb.Labels()

        // 生成最终的标签集并计算哈希指纹
        lbs := lb.Labels()
        h := lbs.Hash()
        resultFPs[h] = struct{}{}

        // 确保处理后的标签集没有重复项
        if _, ok := alerts[h]; ok {
            return nil, errors.New("vector contains metrics with the same labelset after applying alert labels")
        }

        // 创建新的待处理警报对象
        alerts[h] = &Alert{
            Labels:      lbs,
            Annotations: annotations,
            ActiveAt:    ts,       // 警报首次激活时间
            State:       StatePending,  // 初始状态为待处理
            Value:       smpl.F,    // 记录触发警报的指标值
        }
    }

    // 锁定活动警报映射,确保并发安全
    r.activeMtx.Lock()
    defer r.activeMtx.Unlock()

    // 处理新生成的警报
    for h, a := range alerts {
        // 检查是否已有该标签集的警报
        // 如果有且状态不是非活跃,则更新其值和注释
        if alert, ok := r.active[h]; ok && alert.State != StateInactive {
            alert.Value = a.Value
            alert.Annotations = a.Annotations
            continue
        }

        // 否则添加新警报
        r.active[h] = a
    }

    var numActivePending int
    // 遍历所有活动警报,处理状态转换和过期警报
    for fp, a := range r.active {
        // 如果当前查询结果中没有此警报的指纹,表示该警报已不再触发
        if _, ok := resultFPs[fp]; !ok {
            // 确定是否应继续保持警报触发状态
            var keepFiring bool
            if a.State == StateFiring && r.keepFiringFor > 0 {
                // 记录开始保持触发状态的时间
                if a.KeepFiringSince.IsZero() {
                    a.KeepFiringSince = ts
                }
                // 如果保持触发时间未超过设定值,则继续保持触发
                if ts.Sub(a.KeepFiringSince) < r.keepFiringFor {
                    keepFiring = true
                }
            }

            // 处理待处理警报或已解决且超过保留期的警报
            if a.State == StatePending || (!a.ResolvedAt.IsZero() && ts.Sub(a.ResolvedAt) > resolvedRetention) {
                delete(r.active, fp)  // 从活动警报中移除
            }
            
            // 更新警报状态为非活跃,并记录解决时间
            if a.State != StateInactive && !keepFiring {
                a.State = StateInactive
                a.ResolvedAt = ts
            }
            
            // 如果不需要继续保持触发状态,则跳过后续处理
            if !keepFiring {
                continue
            }
        } else {
            // 警报仍在触发,重置保持触发时间
            a.KeepFiringSince = time.Time{}
        }
        numActivePending++

        // 处理待处理警报:如果持续时间超过holdDuration,则将状态转为触发中
        if a.State == StatePending && ts.Sub(a.ActiveAt) >= r.holdDuration {
            a.State = StateFiring
            a.FiredAt = ts
        }

        // 如果规则已恢复,则生成用于Prometheus查询的样本数据
        if r.restored.Load() {
            vec = append(vec, r.sample(a, ts.Add(-queryOffset)))
            vec = append(vec, r.forStateSample(a, ts.Add(-queryOffset), float64(a.ActiveAt.Unix())))
        }
    }

    // 检查是否超过警报数量限制
    if limit > 0 && numActivePending > limit {
        r.active = map[uint64]*Alert{}  // 清空所有活动警报
        return nil, fmt.Errorf("exceeded limit of %d with %d alerts", limit, numActivePending)
    }

    return vec, nil
}

这段代码实现了 Prometheus 告警规则的评估逻辑,主要功能包括:

        • 执行告警表达式查询,获取当前触发的指标
        • 处理查询结果,构建告警标签和注释
        • 管理告警状态转换(待处理 -> 触发中 -> 已解决)
        • 支持告警保持触发一段时间的机制
        • 生成用于 Prometheus 查询的告警时间序列数据
        • 实现告警数量限制保护机制

2.2 Eval 方法:告警规则评估

2.2.1、结构体与字段

    每条 AlertingRule(告警规则) (https://github.com/prometheus/prometheus/blob/v3.4.2/rules/alerting.go#L108) 都包含一个 PromQL 表达式(vector 字段)。

// An AlertingRule generates alerts from its vector expression.
// AlertingRule 结构体用于从向量表达式生成告警。
type AlertingRule struct {
	// The name of the alert.
	// 告警的名称。
	name string
	// The vector expression from which to generate alerts.
	// 用于生成告警的向量表达式。
	vector parser.Expr
	// The duration for which a labelset needs to persist in the expression
	// output vector before an alert transitions from Pending to Firing state.
	// 标签集需要在表达式输出向量中持续存在的时长,之后告警才会从 Pending 状态转变为 Firing 状态。
	holdDuration time.Duration
	// The amount of time that the alert should remain firing after the
	// resolution.
	// 告警在解决后应继续保持触发状态的时长。
	keepFiringFor time.Duration
	// Extra labels to attach to the resulting alert sample vectors.
	// 要附加到生成的告警样本向量上的额外标签。
	labels labels.Labels
	// Non-identifying key/value pairs.
	// 非标识性的键值对,通常用于存储额外信息。
	annotations labels.Labels
	// External labels from the global config.
	// 来自全局配置的外部标签。
	externalLabels map[string]string
	// The external URL from the --web.external-url flag.
	// 从 --web.external-url 标志指定的外部 URL。
	externalURL string
	// true if old state has been restored. We start persisting samples for ALERT_FOR_STATE
	// only after the restoration.
	// 如果旧状态已恢复,则为 true。只有在恢复后,我们才开始持久化 ALERT_FOR_STATE 的样本。
	restored *atomic.Bool
	// Time in seconds taken to evaluate rule.
	// 评估规则所花费的时间(以秒为单位)。
	evaluationDuration *atomic.Duration
	// Timestamp of last evaluation of rule.
	// 规则最后一次评估的时间戳。
	evaluationTimestamp *atomic.Time
	// The health of the alerting rule.
	// 告警规则的健康状态。
	health *atomic.String
	// The last error seen by the alerting rule.
	// 告警规则遇到的最后一个错误。
	lastError *atomic.Error
	// activeMtx Protects the `active` map.
	// 用于保护 `active` 映射的互斥锁。
	activeMtx sync.Mutex
	// A map of alerts which are currently active (Pending or Firing), keyed by
	// the fingerprint of the labelset they correspond to.
	// 当前处于活动状态(Pending 或 Firing)的告警映射,键为对应标签集的指纹。
	active map[uint64]*Alert

	logger *slog.Logger

	dependenciesMutex sync.RWMutex
	// 依赖此规则的规则列表。
	dependentRules    []Rule
	// 此规则所依赖的规则列表。
	dependencyRules   []Rule
}

// NewAlertingRule constructs a new AlertingRule.
// NewAlertingRule 函数用于构造一个新的 AlertingRule 实例。
func NewAlertingRule(
	// 告警规则的名称。
	name string, 
	// 用于生成告警的向量表达式。
	vec parser.Expr, 
	// 标签集需要在表达式输出向量中持续存在的时长,之后告警才会从 Pending 状态转变为 Firing 状态。
	hold, 
	// 告警在解决后应继续保持触发状态的时长。
	keepFiringFor time.Duration, 
	// 要附加到生成的告警样本向量上的额外标签。
	labels, 
	// 非标识性的键值对,通常用于存储额外信息。
	annotations, 
	// 来自全局配置的外部标签。
	externalLabels labels.Labels, 
	// 从 --web.external-url 标志指定的外部 URL。
	externalURL string, 
	// 如果旧状态已恢复,则为 true。
	restored bool, 
	// 日志记录器。
	logger *slog.Logger, 
) *AlertingRule {
	// 将外部标签转换为映射。
	el := externalLabels.Map()

	// 返回一个新的 AlertingRule 实例。
	return &AlertingRule{
		// 告警规则的名称。
		name:                name,
		// 用于生成告警的向量表达式。
		vector:              vec,
		// 标签集需要在表达式输出向量中持续存在的时长,之后告警才会从 Pending 状态转变为 Firing 状态。
		holdDuration:        hold,
		// 告警在解决后应继续保持触发状态的时长。
		keepFiringFor:       keepFiringFor,
		// 要附加到生成的告警样本向量上的额外标签。
		labels:              labels,
		// 非标识性的键值对,通常用于存储额外信息。
		annotations:         annotations,
		// 来自全局配置的外部标签。
		externalLabels:      el,
		// 从 --web.external-url 标志指定的外部 URL。
		externalURL:         externalURL,
		// 当前处于活动状态(Pending 或 Firing)的告警映射,初始为空。
		active:              map[uint64]*Alert{},
		// 日志记录器。
		logger:              logger,
		// 旧状态恢复标志,初始值由 restored 参数决定。
		restored:            atomic.NewBool(restored),
		// 告警规则的健康状态,初始为未知。
		health:              atomic.NewString(string(HealthUnknown)),
		// 规则最后一次评估的时间戳,初始为空。
		evaluationTimestamp: atomic.NewTime(time.Time{}),
		// 评估规则所花费的时间,初始为 0。
		evaluationDuration:  atomic.NewDuration(0),
		// 告警规则遇到的最后一个错误,初始为 nil。
		lastError:           atomic.NewError(nil),
	}
}

2.2.2、评估入口

在规则评估时,会调用 AlertingRule.Eval (https://github.com/prometheus/prometheus/blob/v3.4.2/rules/alerting.go#L374) 方法。其核心逻辑如下:

// Eval evaluates the rule expression and then creates pending alerts and fires
// or removes previously pending alerts accordingly.
// Eval 方法用于评估规则表达式,然后相应地创建待处理的告警,并触发或移除之前待处理的告警。
func (r *AlertingRule) Eval(ctx context.Context, queryOffset time.Duration, ts time.Time, query QueryFunc, externalURL *url.URL, limit int) (promql.Vector, error) {
	// 将规则详情信息添加到上下文,方便后续跟踪规则执行来源。
	ctx = NewOriginContext(ctx, NewRuleDetail(r))
	// 调用查询函数,执行规则的向量表达式查询,查询时间为当前时间减去查询偏移量。
// 1.通过 QueryFunc 执行 PromQL 查询 <=== 重点逻辑 res, err := query(ctx, r.vector.String(), ts.Add(-queryOffset)) if err != nil { // 如果查询过程中出现错误,直接返回错误。 return nil, err } // Create pending alerts for any new vector elements in the alert expression // or update the expression value for existing elements. // 为告警表达式中的任何新向量元素创建待处理的告警,或者更新现有元素的表达式值。 resultFPs := map[uint64]struct{}{} // 创建一个标签构建器,用于构建标签。 lb := labels.NewBuilder(labels.EmptyLabels()) // 创建一个临时标签构建器,用于构建注解。 sb := labels.NewScratchBuilder(0) // 用于存储最终生成的样本向量。 var vec promql.Vector // 用于存储本次评估生成的告警。 alerts := make(map[uint64]*Alert, len(res)) for _, smpl := range res { // Provide the alert information to the template. // 将样本的标签转换为映射,方便后续模板使用。 l := smpl.Metric.Map() // 创建模板数据,包含标签、外部标签、外部 URL 和样本信息。 tmplData := template.AlertTemplateData(l, r.externalLabels, r.externalURL, smpl) // Inject some convenience variables that are easier to remember for users // who are not used to Go's templating system. // 注入一些方便用户使用的变量,这些变量对于不熟悉 Go 模板系统的用户更容易记忆。 defs := []string{ "{{$labels := .Labels}}", "{{$externalLabels := .ExternalLabels}}", "{{$externalURL := .ExternalURL}}", "{{$value := .Value}}", } // 定义一个展开模板的函数。 expand := func(text string) string { // 创建一个模板扩展器,用于展开模板。 tmpl := template.NewTemplateExpander( ctx, strings.Join(append(defs, text), ""), "__alert_"+r.Name(), tmplData, model.Time(timestamp.FromTime(ts)), template.QueryFunc(query), externalURL, nil, ) // 展开模板。 result, err := tmpl.Expand() if err != nil { // 如果展开模板时出现错误,记录错误信息并返回错误提示。 result = fmt.Sprintf("<error expanding template: %s>", err) r.logger.Warn("Expanding alert template failed", "err", err, "data", tmplData) } return result } // 重置标签构建器,准备构建新的标签。 lb.Reset(smpl.Metric) // 删除原样本中的指标名称标签。 lb.Del(labels.MetricName) // 遍历规则的标签,使用模板展开标签值并设置到标签构建器中。 r.labels.Range(func(l labels.Label) { lb.Set(l.Name, expand(l.Value)) }) // 设置告警名称标签。 lb.Set(labels.AlertName, r.Name()) // 重置临时标签构建器,准备构建注解。 sb.Reset() // 遍历规则的注解,使用模板展开注解值并添加到临时标签构建器中。 r.annotations.Range(func(a labels.Label) { sb.Add(a.Name, expand(a.Value)) }) // 获取构建好的注解标签。 annotations := sb.Labels() // 获取最终构建好的标签。 lbs := lb.Labels() // 计算标签的哈希值。 h := lbs.Hash() // 将哈希值添加到结果指纹集合中。 resultFPs[h] = struct{}{} if _, ok := alerts[h]; ok { // 如果已经存在相同哈希值的告警,说明存在重复的标签集,返回错误。 return nil, errors.New("vector contains metrics with the same labelset after applying alert labels") } // 创建一个新的告警实例。 alerts[h] = &Alert{ Labels: lbs, Annotations: annotations, ActiveAt: ts, State: StatePending, Value: smpl.F, } } // 加锁,保护 active 映射,防止并发修改。 r.activeMtx.Lock() defer r.activeMtx.Unlock() for h, a := range alerts { // Check whether we already have alerting state for the identifying label set. // Update the last value and annotations if so, create a new alert entry otherwise. // 检查是否已经存在该标签集的告警状态。 // 如果存在且告警状态不为非活动状态,则更新最后一个值和注解;否则,创建一个新的告警条目。 if alert, ok := r.active[h]; ok && alert.State != StateInactive { alert.Value = a.Value alert.Annotations = a.Annotations continue } // 如果不存在,则将新告警添加到活动告警映射中。 r.active[h] = a } // 记录当前活动的待处理告警数量。 var numActivePending int // Check if any pending alerts should be removed or fire now. Write out alert timeseries. // 检查是否有任何待处理的告警现在应该被移除或触发。写出告警时间序列。 for fp, a := range r.active { if _, ok := resultFPs[fp]; !ok { // There is no firing alerts for this fingerprint. The alert is no // longer firing. // 该指纹对应的告警不再触发。 // Use keepFiringFor value to determine if the alert should keep // firing. // 使用 keepFiringFor 值来确定告警是否应该继续触发。 var keepFiring bool if a.State == StateFiring && r.keepFiringFor > 0 { if a.KeepFiringSince.IsZero() { a.KeepFiringSince = ts } if ts.Sub(a.KeepFiringSince) < r.keepFiringFor { keepFiring = true } } // If the alert is resolved (was firing but is now inactive) keep it for // at least the retention period. This is important for a number of reasons: // // 1. It allows for Prometheus to be more resilient to network issues that // would otherwise prevent a resolved alert from being reported as resolved // to Alertmanager. // // 2. It helps reduce the chance of resolved notifications being lost if // Alertmanager crashes or restarts between receiving the resolved alert // from Prometheus and sending the resolved notification. This tends to // occur for routes with large Group intervals. // 如果告警已解决(之前处于触发状态但现在处于非活动状态),则至少保留保留期。这很重要,原因如下: // 1. 它使 Prometheus 对网络问题更具弹性,否则这些问题会阻止已解决的告警被报告给 Alertmanager。 // 2. 它有助于减少在 Alertmanager 从 Prometheus 接收已解决的告警并发送已解决的通知之间崩溃或重启时丢失已解决通知的机会。这通常发生在具有大分组间隔的路由中。 if a.State == StatePending || (!a.ResolvedAt.IsZero() && ts.Sub(a.ResolvedAt) > resolvedRetention) { // 如果告警处于待处理状态,或者已解决且超过保留期,则从活动告警映射中删除该告警。 delete(r.active, fp) } if a.State != StateInactive && !keepFiring { // 如果告警不处于非活动状态且不应继续触发,则将其状态设置为非活动状态,并记录解决时间。 a.State = StateInactive a.ResolvedAt = ts } if !keepFiring { // 如果不应继续触发,则跳过后续处理。 continue } } else { // The alert is firing, reset keepFiringSince. // 告警正在触发,重置 keepFiringSince 时间。 a.KeepFiringSince = time.Time{} } // 活动的待处理告警数量加 1。 numActivePending++ if a.State == StatePending && ts.Sub(a.ActiveAt) >= r.holdDuration { // 如果告警处于待处理状态且持续时间超过保持时间,则将其状态设置为触发状态,并记录触发时间。 a.State = StateFiring a.FiredAt = ts } if r.restored.Load() { // 如果规则状态已恢复,则将告警样本和告警持续状态样本添加到结果向量中。 vec = append(vec, r.sample(a, ts.Add(-queryOffset))) vec = append(vec, r.forStateSample(a, ts.Add(-queryOffset), float64(a.ActiveAt.Unix()))) } } if limit > 0 && numActivePending > limit { // 如果设置了告警数量限制且活动的待处理告警数量超过限制,则清空活动告警映射并返回错误。 r.active = map[uint64]*Alert{} return nil, fmt.Errorf("exceeded limit of %d with %d alerts", limit, numActivePending) } // 返回最终生成的样本向量和 nil 错误。 return vec, nil }
      • r.vector.String():将 PromQL 表达式转为字符串。

      • ts.Add(-queryOffset):支持查询历史数据(如有 queryOffset 配置)。

      • query:类型为 QueryFunc,是 Prometheus 查询引擎的抽象。

2.3、查询引擎调用链

QueryFunc 通过 EngineQueryFunc 间接调用 Prometheus 的 PromQL 查询引擎,执行即时查询并处理查询结果。整个查询引擎调用链涉及到查询对象的创建、查询的执行和结果的处理等步骤。

QueryFunc(https://github.com/prometheus/prometheus/blob/v3.4.2/rules/manager.go#L46) 实际上会调用 Prometheus 的 PromQL 查询引擎,执行表达式并返回结果:

// QueryFunc processes PromQL queries.
// QueryFunc 是一个函数类型,用于处理 PromQL 查询。它接收一个上下文、一个查询字符串和一个时间戳作为参数,返回一个 promql.Vector 类型的结果和一个错误。
type QueryFunc func(ctx context.Context, q string, t time.Time) (promql.Vector, error)

// EngineQueryFunc returns a new query function that executes instant queries against
// the given engine.
// It converts scalar into vector results.
// EngineQueryFunc 函数返回一个新的查询函数,该函数使用给定的 PromQL 引擎执行即时查询。
// 它会将查询结果中的标量值转换为向量结果。
func EngineQueryFunc(engine promql.QueryEngine, q storage.Queryable) QueryFunc {
    // 返回一个匿名函数,该函数实现了 QueryFunc 接口。
    return func(ctx context.Context, qs string, t time.Time) (promql.Vector, error) {
        // 使用给定的引擎创建一个即时查询对象。
        q, err := engine.NewInstantQuery(ctx, q, nil, qs, t)
        if err != nil {
            // 如果创建查询对象时出现错误,返回 nil 和错误信息。
            return nil, err
        }
        // 执行查询。
        res := q.Exec(ctx)
        if res.Err != nil {
            // 如果查询执行过程中出现错误,返回 nil 和错误信息。
            return nil, res.Err
        }
        // 根据查询结果的类型进行处理。
        switch v := res.Value.(type) {
        case promql.Vector:
            // 如果结果是向量类型,直接返回该向量。
            return v, nil
        case promql.Scalar:
            // 如果结果是标量类型,将其转换为包含一个样本的向量。
            return promql.Vector{promql.Sample{
                T:      v.T,  // 样本的时间戳
                F:      v.V,  // 样本的值
                Metric: labels.Labels{}, // 样本的标签,这里为空
            }}, nil
        default:
            // 如果结果既不是向量也不是标量类型,返回 nil 和错误信息。
            return nil, errors.New("rule result is not a vector or scalar")
        }
    }
}

下面详细分析 QueryFunc 及其相关函数如何调用 Prometheus 的 PromQL 查询引擎,以及整个查询引擎调用链的工作流程。

2.3.1、QueryFunc 类型定义

type QueryFunc func(ctx context.Context, q string, t time.Time) (promql.Vector, error)

QueryFunc 是一个函数类型,它定义了执行 PromQL 查询的接口。该函数接收一个上下文 ctx、一个查询字符串 q 和一个时间戳 t,并返回一个 promql.Vector 类型的结果和一个错误。

2.3.2、EngineQueryFunc 函数

EngineQueryFunc 函数返回一个实现了 QueryFunc 接口的匿名函数。具体步骤如下:

        1. 创建即时查询对象:调用 engine.NewInstantQuery 方法,使用给定的上下文 ctx、查询字符串 qs 和时间戳 t 创建一个即时查询对象。
        2. 检查错误:如果创建查询对象时出现错误,返回 nil 和错误信息。
        3. 执行查询:调用查询对象的 Exec 方法执行查询,并获取查询结果 res。
        4. 检查查询结果错误:如果查询执行过程中出现错误,返回 nil 和错误信息。
        5. 处理查询结果类型:根据查询结果的类型进行处理:
          1. 如果结果是 promql.Vector 类型,直接返回该向量。
          2. 如果结果是 promql.Scalar 类型,将其转换为包含一个样本的向量并返回。
          3. 如果结果既不是向量也不是标量类型,返回 nil 和错误信息。

2.3.3、查询引擎调用链详细流程

2.3.3.1、调用 EngineQueryFunc 创建查询函数

engine := promql.NewEngine(opts) // 创建 PromQL 引擎
queryable := ... // 获取查询数据源
queryFunc := EngineQueryFunc(engine, queryable) // 创建查询函数 

通过调用 EngineQueryFunc 函数,传入 PromQL 引擎和查询数据源,创建一个实现了 QueryFunc 接口的查询函数 queryFunc。

2.3.3.2、调用查询函数执行查询

ctx := context.Background()
queryString := "up" // PromQL 查询字符串
timestamp := time.Now() // 查询时间戳
result, err := queryFunc(ctx, queryString, timestamp)
if err != nil {
    // 处理错误
}
// 处理查询结果

调用 queryFunc 函数,传入上下文、查询字符串和时间戳,执行 PromQL 查询并获取结果。

2.3.3.3、EngineQueryFunc 内部调用链

        • engine.NewInstantQuery:调用 PromQL 引擎的 NewInstantQuery 方法创建一个即时查询对象。该方法会解析查询字符串,准备查询所需的资源。
        • q.Exec:调用查询对象的 Exec 方法执行查询。该方法会根据查询表达式和时间戳从查询数据源中获取数据,并进行计算和处理。
        • 结果处理:根据查询结果的类型进行处理,将标量结果转换为向量结果或直接返回向量结果。

2.3.4、prometheus/web/api/v1/api.go 中的调用

https://github.com/prometheus/prometheus/blob/v3.4.2/web/api/v1/api.go#L468

qry, err := api.QueryEngine.NewInstantQuery(ctx, api.Queryable, opts, r.FormValue("query"), ts)
if err != nil {
	return invalidParamError(err, "query")
}

// From now on, we must only return with a finalizer in the result (to
// be called by the caller) or call qry.Close ourselves (which is
// required in the case of a panic).
defer func() {
	if result.finalizer == nil {
		qry.Close()
	}
}()

ctx = httputil.ContextFromRequest(ctx, r)

res := qry.Exec(ctx) 

在 API 处理函数中,直接调用 NewInstantQuery 方法创建查询对象,并执行查询。

2.4、查询结果

  • 查询结果 res (https://github.com/prometheus/prometheus/blob/main/rules/manager.go#L53) 是一个 promql.Vector (https://github.com/prometheus/prometheus/blob/main/promql/value.go#L243),即一组样本(Sample(https://github.com/prometheus/prometheus/blob/main/promql/value.go#L195))。
  • 每个样本包含:
    • Metric(https://github.com/prometheus/prometheus/blob/main/promql/value.go#L200):标签集(如 {instance="1.2.3.4:9100", job="node"})
    • F(https://github.com/prometheus/prometheus/blob/main/promql/value.go#L197):数值(float64)
    • T(https://github.com/prometheus/prometheus/blob/main/promql/value.go#L196):时间戳

这些样本就是“当前满足告警条件的所有时间序列”,后续会被用于告警状态判断与实例维护。

posted @ 2025-07-21 11:08  左扬  阅读(64)  评论(0)    收藏  举报