β–ΆπŸ—Ί Alerting Pipeline
  rule files (*.yml / *.yaml)
        β”‚
        β–Ό
  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
  β”‚           rules.Manager                       β”‚  rules/manager.go:99
  β”‚  β€’ ParseFiles() β†’ []Group                     β”‚
  β”‚  β€’ one goroutine per Group                    β”‚
  β”‚  β€’ ticker fires every group.interval          β”‚
  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                         β”‚ per Group
                         β–Ό
  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
  β”‚           rules.Group.Eval()                  β”‚  rules/group.go:504
  β”‚  β€’ evaluates all rules in the group           β”‚
  β”‚  β€’ respects query_offset                      β”‚
  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
              β”‚                       β”‚
              β–Ό                       β–Ό
  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
  β”‚  AlertingRule     β”‚    β”‚  RecordingRule             β”‚
  β”‚  rules/alerting.goβ”‚    β”‚  rules/recording.go        β”‚
  β”‚                   β”‚    β”‚                            β”‚
  β”‚ 1. exec PromQL    β”‚    β”‚ 1. exec PromQL             β”‚
  β”‚ 2. each result    β”‚    β”‚ 2. rename metric label     β”‚
  β”‚    sample β†’       β”‚    β”‚ 3. Append() to storage     β”‚
  β”‚    alert instance β”‚    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
  β”‚ 3. state machine  β”‚
  │    inactive→      │
  │    pending→       │
  β”‚    firing         β”‚
  β”‚ 4. write ALERTS{} β”‚
  β”‚    to storage     β”‚
  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜
              β”‚  firing alerts
              β–Ό
  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
  β”‚           notifier.Manager                    β”‚  notifier/manager.go:53
  β”‚  β€’ SendAlerts() called after each Group eval  β”‚
  β”‚  β€’ batches alerts (default 256 per request)   β”‚
  β”‚  β€’ discovers Alertmanager endpoints via SD    β”‚
  β”‚  β€’ HTTP POST /api/v1/alerts                   β”‚
  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
              β”‚
              β–Ό
  Alertmanager  (dedup, group, route, silence, inhibit)
β–ΆπŸ“‹ rules.Manager β€” Lifecycle
rules/manager.go β€” Manager struct L99
type Manager struct {
    opts   *ManagerOptions
    groups map[string]*Group  // key: "filepath;groupname"
    mtx    sync.RWMutex
    block  chan struct{}   // blocks until rule files are loaded
    done   chan struct{}
    ...
}

// ManagerOptions wires dependencies:
type ManagerOptions struct {
    Appendable storage.Appendable  // write recording rules + ALERTS
    Queryable  storage.Queryable   // read for rule PromQL
    QueryFunc  QueryFunc           // calls promql.Engine
    NotifyFunc NotifyFunc          // deliver alerts to notifier
    Context    context.Context
    ExternalURL *url.URL
    Logger     *slog.Logger
    ...
}

On config reload, Manager.Update() diffs old and new groups. Groups with the same name and same rules are kept running (their evaluation offset is preserved). New or changed groups are started fresh.

▢⏱ Group.Eval() β€” Per-Interval Evaluation
rules/group.go β€” Group struct L45
type Group struct {
    name          string
    file          string
    interval      time.Duration      // eval_interval
    queryOffset   *time.Duration     // query_offset
    rules         []Rule
    seriesInPreviousEval []map[string]labels.Labels
    staleSeries   []labels.Labels
    opts          *ManagerOptions
    mtx           sync.Mutex
    evaluationTime time.Duration     // last eval duration
    ...
}
rules/group.go β€” Group.Eval() L504
func (g *Group) Eval(ctx context.Context, ts time.Time) {
    for i, rule := range g.rules {
        // Execute PromQL query via QueryFunc.
        vector, err := rule.Eval(ctx, g.QueryOffset(), ts,
                                 g.opts.QueryFunc, g.opts.ExternalURL, g.Limit())

        // For alerting rules: update alert state machine.
        // For recording rules: append resulting vector to storage.

        // Track which series were produced; stale-mark any that vanished.
    }

    // After all rules: call NotifyFunc with firing alerts.
    g.opts.NotifyFunc(ctx, g.file, firingAlerts...)
}

Evaluation Timestamp

The evaluation timestamp ts is the wall-clock time of the tick, optionally shifted back by query_offset. This allows rules to query "slightly in the past" to avoid reading data that hasn't been fully scraped yet.

β–ΆπŸ”” AlertingRule β€” State Machine
rules/alerting.go β€” AlertingRule struct L116
type AlertingRule struct {
    name        string
    vector      parser.Expr     // the PromQL expression
    holdDuration time.Duration  // "for:" clause
    labels      labels.Labels   // extra labels to attach
    annotations labels.Labels   // human-readable annotations
    externalURL *url.URL
    active      map[uint64]*Alert  // fingerprint β†’ alert state
    mtx         sync.Mutex
    ...
}

type Alert struct {
    State       AlertState   // StateInactive / StatePending / StateFiring
    Labels      labels.Labels
    Annotations labels.Labels
    Value       float64      // the sample value that triggered
    ActiveAt    time.Time    // when alert first became Pending
    FiredAt     time.Time    // when alert transitioned to Firing
    ResolvedAt  time.Time    // when alert resolved
    LastSentAt  time.Time    // last time sent to Alertmanager
    ValidUntil  time.Time    // expiry if not re-evaluated
    ...
}

Alert State Machine

  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”      PromQL returns sample     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
  β”‚  Inactive   β”‚  ─────────────────────────────▢ β”‚   Pending   β”‚
  β”‚  (no data   β”‚                                 β”‚  (waiting   β”‚
  β”‚   or false) β”‚  ◀─────────────────────────── β”‚  for: dur)  β”‚
  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜     PromQL returns no sample    β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜
                                                         β”‚ holdDuration elapsed
                                                         β–Ό
                                                  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                                                  β”‚   Firing    β”‚
                                                  β”‚ (sent to    β”‚
                                                  β”‚  Alertmgr)  β”‚
                                                  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                                         β”‚ PromQL returns no sample
                                                         β–Ό
                                                  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                                                  β”‚  Inactive   β”‚
                                                  β”‚ (resolved   β”‚
                                                  β”‚  sent once) β”‚
                                                  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Synthetic Metrics Written to Storage

// ALERTS metric (one per active alert instance):
ALERTS{
    alertname="MyAlert",
    alertstate="firing",    // or "pending"
    <rule labels>,
    <alert labels>,
} = 1

// ALERTS_FOR_STATE metric (tracks "for:" start time):
ALERTS_FOR_STATE{
    alertname="MyAlert",
    <rule labels>,
} = <unix timestamp when alert became pending>

These are written to fanoutStorage exactly like scraped metrics, making alert state queryable via PromQL.

β–ΆπŸ–Š RecordingRule β€” Precomputed Aggregates
rules/recording.go recording.go
// Example rule:
// - record: job:http_requests:rate5m
//   expr: sum by (job) (rate(http_requests_total[5m]))

func (rule *RecordingRule) Eval(ctx context.Context, ...) (promql.Vector, error) {
    vector, err := queryFunc(ctx, rule.vector.String(), t)
    // Rename __name__ label to rule.name on each sample.
    for i := range vector {
        vector[i].Metric = labelsutil.ReplaceOrAddLabel(
            vector[i].Metric, labels.MetricName, rule.name)
    }
    return vector, err
}

// The caller (Group.Eval) appends the vector to storage:
app := opts.Appendable.Appender(ctx)
for _, s := range vector {
    app.Append(0, s.Metric, s.T, s.F)
}
app.Commit()
Recording rules are the primary way to pre-aggregate expensive queries. The stored result is a regular metric, queryable at sub-interval precision isn't possible β€” it has the same granularity as the evaluation interval.
β–ΆπŸ“£ Notifier β€” Alertmanager Delivery
notifier/manager.go β€” Manager struct L53
type Manager struct {
    opts  *Options
    queue []*Alert          // buffered alerts pending delivery
    more  chan struct{}      // signal: new alerts in queue
    mtx   sync.RWMutex

    // alertmanagers discovered via SD (separate discovery manager).
    alertmanagers map[string]*alertmanagerSet
    ...
}

// Delivery loop (simplified):
func (n *Manager) run() {
    for {
        select {
        case <-n.more:
            n.sendAll(n.nextBatch())
        }
    }
}

// sendAll sends the batch to all discovered Alertmanager endpoints.
// Each endpoint is tried independently; failure for one does not
// block delivery to others.

Alert Enrichment

Before sending, each alert is enriched:

  1. External labels from --external-labels added (do not override alert labels)
  2. Alertmanager URL added to GeneratorURL field
  3. Annotations templated using Go text/template with $value, $labels available
  4. Batched into chunks of maxBatchSize (default 256)
  5. POST to https://<alertmanager>/api/v2/alerts

Alertmanager Discovery

The notifier uses its own discovery.Manager instance to watch the alerting.alertmanagers config. Alertmanager endpoints can be discovered via any SD mechanism, just like scrape targets.

The alert queue has a maximum size (QueueCapacity, default 10 000). If Alertmanager is unreachable for too long and the queue fills, older alerts are dropped. Alert counts are tracked in prometheus_notifications_dropped_total.
β–ΆπŸ“„ Rule File Format
groups:
  - name: example                   # group name (must be unique per file)
    interval: 1m                    # override global evaluation_interval
    query_offset: 30s               # evaluate 30s in the past
    limit: 10                       # max alerts per rule (0 = unlimited)
    rules:

      # Alerting rule
      - alert: HighErrorRate
        expr: |
          rate(http_requests_total{status=~"5.."}[5m])
          / rate(http_requests_total[5m]) > 0.05
        for: 5m                     # must be true for 5m before firing
        keep_firing_for: 2m         # stay firing at least 2m after resolving
        labels:
          severity: critical
          team: backend
        annotations:
          summary: "High error rate on {{ $labels.job }}"
          description: "Error rate is {{ $value | humanizePercentage }}"

      # Recording rule
      - record: job:http_request_duration:p99
        expr: histogram_quantile(0.99, sum by (job, le) (
                rate(http_request_duration_seconds_bucket[5m])
              ))