Repository: prometheus/prometheus
File: rules/group.go (Go)

Code before:
	g.staleSeries = from.staleSeries
	for fi, fromRule := range from.rules {
		nameAndLabels := nameAndLabels(fromRule)
		l := ruleMap[nameAndLabels]
		if len(l) != 0 {
			for _, series := range from.seriesInPreviousEval[fi] {
				g.staleSeries = append(g.staleSeries, series)
			}
		}
	}
}

// Eval runs a single evaluation cycle in which all rules are evaluated sequentially.
// Rules can be evaluated concurrently if the `concurrent-rule-eval` feature flag is enabled.
func (g *Group) Eval(ctx context.Context, ts time.Time) {
	var (
		samplesTotal    atomic.Float64
		ruleQueryOffset = g.QueryOffset()
	)
	eval := func(i int, rule Rule, cleanup func()) {
		// Check if the group has been stopped.
		select {
		case <-g.done:
			return
		default:
		}

		if cleanup != nil {
			defer cleanup()
		}

		logger := g.logger.With("name", rule.Name(), "index", i)
		ctx, sp := otel.Tracer("").Start(ctx, "rule")
		sp.SetAttributes(attribute.String("name", rule.Name()))
		defer func(t time.Time) {
			sp.End()

			since := time.Since(t)
			g.metrics.EvalDuration.Observe(since.Seconds())
			rule.SetEvaluationDuration(since)
			rule.SetEvaluationTimestamp(t)
		}(time.Now())

		if sp.SpanContext().IsSampled() && sp.SpanContext().HasTraceID() {
			logger = logger.With("trace_id", sp.SpanContext().TraceID())
		}

		g.metrics.EvalTotal.WithLabelValues(GroupKey(g.File(), g.Name())).Inc()

		vector, err := rule.Eval(ctx, ruleQueryOffset, ts, g.opts.QueryFunc, g.opts.ExternalURL, g.Limit())
		if err != nil {

Review comment (bug):
This should go outside of this loop or we'll execute:

```
g.metrics.GroupSamples.WithLabelValues(GroupKey(g.File(), g.Name())).Set(samplesTotal.Load())
g.cleanupStaleSeries(ctx, ts)
```

With incorrect metrics given that no queries were executed as the group as stopped.

Code after:
	g.staleSeries = from.staleSeries
	for fi, fromRule := range from.rules {
		nameAndLabels := nameAndLabels(fromRule)
		l := ruleMap[nameAndLabels]
		if len(l) != 0 {
			for _, series := range from.seriesInPreviousEval[fi] {
				g.staleSeries = append(g.staleSeries, series)
			}
		}
	}
}

// Eval runs a single evaluation cycle in which all rules are evaluated sequentially.
// Rules can be evaluated concurrently if the `concurrent-rule-eval` feature flag is enabled.
func (g *Group) Eval(ctx context.Context, ts time.Time) {
	var (
		samplesTotal    atomic.Float64
		ruleQueryOffset = g.QueryOffset()
	)
	eval := func(i int, rule Rule, cleanup func()) {
		if cleanup != nil {
			defer cleanup()
		}

		logger := g.logger.With("name", rule.Name(), "index", i)
		ctx, sp := otel.Tracer("").Start(ctx, "rule")
		sp.SetAttributes(attribute.String("name", rule.Name()))
		defer func(t time.Time) {
			sp.End()

			since := time.Since(t)
			g.metrics.EvalDuration.Observe(since.Seconds())
			rule.SetEvaluationDuration(since)
			rule.SetEvaluationTimestamp(t)
		}(time.Now())

		if sp.SpanContext().IsSampled() && sp.SpanContext().HasTraceID() {
			logger = logger.With("trace_id", sp.SpanContext().TraceID())
		}

		g.metrics.EvalTotal.WithLabelValues(GroupKey(g.File(), g.Name())).Inc()

		vector, err := rule.Eval(ctx, ruleQueryOffset, ts, g.opts.QueryFunc, g.opts.ExternalURL, g.Limit())
		if err != nil {
			rule.SetHealth(HealthBad)
			rule.SetLastError(err)
			sp.SetStatus(codes.Error, err.Error())
			g.metrics.EvalFailures.WithLabelValues(GroupKey(g.File(), g.Name())).Inc()

			// Canceled queries are intentional termination of queries. This normally
			// happens on shutdown and thus we skip logging of any errors here.