mirror of
https://github.com/hanzoai/o11y.git
synced 2026-10-15 12:58:33 +00:00
Re-sync the entire SigNoz-derived Go tree to a clean fork of latest SigNoz main (upstream 3e6339019), eliminating the version-skew that broke `go build ./...` (mixed-era packages, ~14 imported-but-absent packages, partial signoz->o11y / clickhouse->datastore rebrands, self-import cycle, duplicate package decls). - pkg/ + cmd/ wholesale replaced with latest SigNoz; imports rewritten github.com/SigNoz/signoz/ -> github.com/hanzoai/o11y/. - Adopt SigNoz's CURRENT structure (resourcefilter->telemetryresourcefilter, integrationtypes->cloudintegrationtypes, roletypes/grammar/times folded in upstream); old paths dropped. - Fork deps repathed to hanzoai + bumped: signoz-otel-collector v0.144.6, clickhouse-go-mock v0.14.1, govaluate v0.1.0; expr-lang/expr => hanzoai/expr v1.17.8 (replace). go.mod reconciled from latest SigNoz; go.sum re-tidied. - ee/ + cmd/enterprise stripped — o11y stays MIT-only; community edition imports no ee packages. - Preserved o11y additions: module path github.com/hanzoai/o11y, mount.go (cloud.Register/zip mount adapter, HIP-0106), NOTICE + LICENSE (SigNoz §4c attribution retained; upstream pkg files are headerless per SigNoz convention). - Reverted o11y cosmetic renames (o11y*/datastore*) and experimental originals (authz/iamauthz, billing, zapreceiver, zapmetricreceiver) to SigNoz canonical to kill the skew; snapshots retained in git history for re-layering. go build ./... = exit 0. go vet clean except benign "unreachable code" in generated ANTLR parser files (inherited from upstream).
359 lines
9.0 KiB
Go
359 lines
9.0 KiB
Go
package rules
|
|
|
|
import (
|
|
"context"
|
|
"log/slog"
|
|
"runtime/debug"
|
|
"sort"
|
|
"sync"
|
|
"time"
|
|
|
|
opentracing "github.com/opentracing/opentracing-go"
|
|
|
|
"github.com/hanzoai/o11y/pkg/errors"
|
|
"github.com/hanzoai/o11y/pkg/types/authtypes"
|
|
"github.com/hanzoai/o11y/pkg/types/ctxtypes"
|
|
"github.com/hanzoai/o11y/pkg/types/ruletypes"
|
|
)
|
|
|
|
// RuleTask holds a rule (with composite queries)
|
|
// and evaluates the rule at a given frequency
|
|
type RuleTask struct {
|
|
name string
|
|
file string
|
|
frequency time.Duration
|
|
rules []Rule
|
|
opts *ManagerOptions
|
|
logger *slog.Logger
|
|
mtx sync.Mutex
|
|
evaluationDuration time.Duration
|
|
evaluationTime time.Duration
|
|
lastEvaluation time.Time
|
|
|
|
done chan struct{}
|
|
terminated chan struct{}
|
|
|
|
pause bool
|
|
notify NotifyFunc
|
|
}
|
|
|
|
const DefaultFrequency = 1 * time.Minute
|
|
|
|
// NewRuleTask makes a new RuleTask with the given name, options, and rules.
|
|
func NewRuleTask(name, file string, frequency time.Duration, rules []Rule, opts *ManagerOptions, notify NotifyFunc) *RuleTask {
|
|
if frequency == 0 {
|
|
frequency = DefaultFrequency
|
|
}
|
|
opts.Logger.Info("initiating a new rule task", "name", name, "frequency", frequency)
|
|
|
|
return &RuleTask{
|
|
name: name,
|
|
file: file,
|
|
pause: false,
|
|
frequency: frequency,
|
|
rules: rules,
|
|
opts: opts,
|
|
logger: opts.Logger,
|
|
done: make(chan struct{}),
|
|
terminated: make(chan struct{}),
|
|
notify: notify,
|
|
}
|
|
}
|
|
|
|
// Name returns the group name.
|
|
func (g *RuleTask) Name() string { return g.name }
|
|
|
|
// Key returns the group key
|
|
// TODO(jatinderjit): remove (unused)?
|
|
func (g *RuleTask) Key() string {
|
|
return g.name + ";" + g.file
|
|
}
|
|
|
|
// Name returns the group name.
|
|
func (g *RuleTask) Type() TaskType { return TaskTypeCh }
|
|
|
|
// Rules returns the group's rules.
|
|
func (g *RuleTask) Rules() []Rule { return g.rules }
|
|
|
|
// Interval returns the group's interval.
|
|
// TODO(jatinderjit): remove (unused)?
|
|
func (g *RuleTask) Interval() time.Duration { return g.frequency }
|
|
|
|
func (g *RuleTask) Pause(b bool) {
|
|
g.mtx.Lock()
|
|
defer g.mtx.Unlock()
|
|
g.pause = b
|
|
}
|
|
|
|
type QueryOrigin struct{}
|
|
|
|
func NewQueryOriginContext(ctx context.Context, data map[string]any) context.Context {
|
|
return context.WithValue(ctx, QueryOrigin{}, data)
|
|
}
|
|
|
|
func (g *RuleTask) Run(ctx context.Context) {
|
|
defer close(g.terminated)
|
|
|
|
// Wait an initial amount to have consistently slotted intervals.
|
|
evalTimestamp := g.EvalTimestamp(time.Now().UnixNano()).Add(g.frequency)
|
|
g.logger.DebugContext(ctx, "group run to begin at", "eval_timestamp", evalTimestamp)
|
|
select {
|
|
case <-time.After(time.Until(evalTimestamp)):
|
|
case <-g.done:
|
|
return
|
|
}
|
|
|
|
ctx = NewQueryOriginContext(ctx, map[string]any{
|
|
"ruleRuleTask": map[string]string{
|
|
"name": g.Name(),
|
|
},
|
|
})
|
|
|
|
iter := func() {
|
|
if g.pause {
|
|
// todo(amol): remove in memory active alerts
|
|
// and last series state
|
|
return
|
|
}
|
|
start := time.Now()
|
|
g.Eval(ctx, evalTimestamp)
|
|
timeSinceStart := time.Since(start)
|
|
|
|
g.setEvaluationTime(timeSinceStart)
|
|
g.setLastEvaluation(start)
|
|
}
|
|
|
|
// The assumption here is that since the ticker was started after having
|
|
// waited for `evalTimestamp` to pass, the ticks will trigger soon
|
|
// after each `evalTimestamp + N * g.frequency` occurrence.
|
|
tick := time.NewTicker(g.frequency)
|
|
defer tick.Stop()
|
|
|
|
iter()
|
|
|
|
// let the group iterate and run
|
|
for {
|
|
select {
|
|
case <-g.done:
|
|
return
|
|
default:
|
|
select {
|
|
case <-g.done:
|
|
return
|
|
case <-tick.C:
|
|
missed := (time.Since(evalTimestamp) / g.frequency) - 1
|
|
evalTimestamp = evalTimestamp.Add((missed + 1) * g.frequency)
|
|
iter()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (g *RuleTask) Stop() {
|
|
close(g.done)
|
|
<-g.terminated
|
|
}
|
|
|
|
func (g *RuleTask) hash() uint64 {
|
|
l := ruletypes.New(
|
|
ruletypes.Label{Name: "name", Value: g.name},
|
|
)
|
|
return l.Hash()
|
|
}
|
|
|
|
// ThresholdRules returns the list of the group's threshold rules.
|
|
func (g *RuleTask) ThresholdRules() []*ThresholdRule {
|
|
g.mtx.Lock()
|
|
defer g.mtx.Unlock()
|
|
var alerts []*ThresholdRule
|
|
for _, rule := range g.rules {
|
|
if tr, ok := rule.(*ThresholdRule); ok {
|
|
alerts = append(alerts, tr)
|
|
}
|
|
}
|
|
sort.Slice(alerts, func(i, j int) bool {
|
|
return alerts[i].State().Severity() > alerts[j].State().Severity() ||
|
|
(alerts[i].State() == alerts[j].State() &&
|
|
alerts[i].Name() < alerts[j].Name())
|
|
})
|
|
return alerts
|
|
}
|
|
|
|
// HasAlertingRules returns true if the group contains at least one AlertingRule.
|
|
func (g *RuleTask) HasAlertingRules() bool {
|
|
g.mtx.Lock()
|
|
defer g.mtx.Unlock()
|
|
|
|
for _, rule := range g.rules {
|
|
if _, ok := rule.(*ThresholdRule); ok {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// GetEvaluationDuration returns the time in seconds it took to evaluate the rule group.
|
|
func (g *RuleTask) GetEvaluationDuration() time.Duration {
|
|
g.mtx.Lock()
|
|
defer g.mtx.Unlock()
|
|
return g.evaluationDuration
|
|
}
|
|
|
|
// SetEvaluationDuration sets the time in seconds the last evaluation took.
|
|
func (g *RuleTask) SetEvaluationDuration(dur time.Duration) {
|
|
g.mtx.Lock()
|
|
defer g.mtx.Unlock()
|
|
g.evaluationDuration = dur
|
|
}
|
|
|
|
// GetEvaluationTime returns the time in seconds it took to evaluate the rule group.
|
|
func (g *RuleTask) GetEvaluationTime() time.Duration {
|
|
g.mtx.Lock()
|
|
defer g.mtx.Unlock()
|
|
return g.evaluationTime
|
|
}
|
|
|
|
// setEvaluationTime sets the time in seconds the last evaluation took.
|
|
func (g *RuleTask) setEvaluationTime(dur time.Duration) {
|
|
g.mtx.Lock()
|
|
defer g.mtx.Unlock()
|
|
g.evaluationTime = dur
|
|
}
|
|
|
|
// GetLastEvaluation returns the time the last evaluation of the rule group took place.
|
|
func (g *RuleTask) GetLastEvaluation() time.Time {
|
|
g.mtx.Lock()
|
|
defer g.mtx.Unlock()
|
|
return g.lastEvaluation
|
|
}
|
|
|
|
// setLastEvaluation updates evaluationTimestamp to the timestamp of when the rule group was last evaluated.
|
|
func (g *RuleTask) setLastEvaluation(ts time.Time) {
|
|
g.mtx.Lock()
|
|
defer g.mtx.Unlock()
|
|
g.lastEvaluation = ts
|
|
}
|
|
|
|
// EvalTimestamp returns the immediately preceding consistently slotted evaluation time.
|
|
func (g *RuleTask) EvalTimestamp(startTime int64) time.Time {
|
|
var (
|
|
offset = int64(g.hash() % uint64(g.frequency))
|
|
adjNow = startTime - offset
|
|
base = adjNow - (adjNow % int64(g.frequency))
|
|
)
|
|
|
|
return time.Unix(0, base+offset).UTC()
|
|
}
|
|
|
|
func nameAndLabels(rule Rule) string {
|
|
return rule.Name() + rule.Labels().String()
|
|
}
|
|
|
|
// CopyState copies the alerting rule and staleness related state from the given group.
|
|
//
|
|
// Rules are matched based on their name and labels. If there are duplicates, the
|
|
// first is matched with the first, second with the second etc.
|
|
func (g *RuleTask) CopyState(fromTask Task) error {
|
|
from, ok := fromTask.(*RuleTask)
|
|
if !ok {
|
|
return errors.NewInternalf(errors.CodeInternal, "invalid from task for copy")
|
|
}
|
|
g.evaluationTime = from.evaluationTime
|
|
g.lastEvaluation = from.lastEvaluation
|
|
|
|
ruleMap := make(map[string][]int, len(from.rules))
|
|
|
|
for fi, fromRule := range from.rules {
|
|
nameAndLabels := nameAndLabels(fromRule)
|
|
l := ruleMap[nameAndLabels]
|
|
ruleMap[nameAndLabels] = append(l, fi)
|
|
}
|
|
|
|
for _, rule := range g.rules {
|
|
nameAndLabels := nameAndLabels(rule)
|
|
indexes := ruleMap[nameAndLabels]
|
|
if len(indexes) == 0 {
|
|
continue
|
|
}
|
|
fi := indexes[0]
|
|
ruleMap[nameAndLabels] = indexes[1:]
|
|
|
|
ar, ok := rule.(*ThresholdRule)
|
|
if !ok {
|
|
continue
|
|
}
|
|
far, ok := from.rules[fi].(*ThresholdRule)
|
|
if !ok {
|
|
continue
|
|
}
|
|
|
|
for fp, a := range far.Active {
|
|
ar.Active[fp] = a
|
|
}
|
|
ar.handledRestart = far.handledRestart
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Eval runs a single evaluation cycle in which all rules are evaluated sequentially.
|
|
func (g *RuleTask) Eval(ctx context.Context, ts time.Time) {
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
g.logger.ErrorContext(
|
|
ctx, "panic during rule evaluation", slog.Any("panic", r),
|
|
slog.String("stack", string(debug.Stack())),
|
|
)
|
|
}
|
|
}()
|
|
|
|
g.logger.DebugContext(ctx, "rule task eval started", "name", g.name, "start_time", ts)
|
|
|
|
for i, rule := range g.rules {
|
|
if rule == nil {
|
|
continue
|
|
}
|
|
|
|
select {
|
|
case <-g.done:
|
|
return
|
|
default:
|
|
}
|
|
|
|
func(i int, rule Rule) {
|
|
sp, ctx := opentracing.StartSpanFromContext(ctx, "rule")
|
|
|
|
sp.SetTag("name", rule.Name())
|
|
defer func(t time.Time) {
|
|
sp.Finish()
|
|
|
|
since := time.Since(t)
|
|
rule.SetEvaluationDuration(since)
|
|
rule.SetEvaluationTimestamp(t)
|
|
}(time.Now())
|
|
|
|
comment := ctxtypes.CommentFromContext(ctx)
|
|
comment.Set("rule_id", rule.ID())
|
|
comment.Set("identn_provider", authtypes.IdentNProviderInternal.StringValue())
|
|
ctx = ctxtypes.NewContextWithComment(ctx, comment)
|
|
|
|
_, err := rule.Eval(ctx, ts)
|
|
if err != nil {
|
|
rule.SetHealth(ruletypes.HealthBad)
|
|
rule.SetLastError(err)
|
|
|
|
g.logger.WarnContext(ctx, "evaluating rule failed", slog.String("rule.id", rule.ID()), errors.Attr(err))
|
|
|
|
// Canceled queries are intentional termination of queries. This normally
|
|
// happens on shutdown and thus we skip logging of any errors here.
|
|
//! if _, ok := err.(promql.ErrQueryCanceled); !ok {
|
|
// level.Warn(g.logger).Log("msg", "Evaluating rule failed", "rule", rule, "err", err)
|
|
//}
|
|
return
|
|
}
|
|
|
|
rule.SendAlerts(ctx, ts, g.opts.ResendDelay, g.frequency, g.notify)
|
|
}(i, rule)
|
|
}
|
|
}
|