Monitoring System 0.1.0
System resource monitoring with pluggable collectors and alerting
Loading...
Searching...
No Matches
alert_triggers.h
Go to the documentation of this file.
1// BSD 3-Clause License
2// Copyright (c) 2021-2025, 🍀☀🌕🌥 🌊
3// See the LICENSE file in the project root for full license information.
4
5#pragma once
6
15#include <algorithm>
16#include <cmath>
17#include <deque>
18#include <memory>
19#include <mutex>
20#include <numeric>
21#include <string>
22#include <vector>
23
24#include "alert_rule.h"
25
26namespace kcenon::monitoring {
27
40
44constexpr const char* comparison_operator_to_string(comparison_operator op) noexcept {
45 switch (op) {
46 case comparison_operator::greater_than: return ">";
48 case comparison_operator::less_than: return "<";
49 case comparison_operator::less_or_equal: return "<=";
50 case comparison_operator::equal: return "==";
51 case comparison_operator::not_equal: return "!=";
52 default: return "?";
53 }
54}
55
77public:
86 double epsilon = 1e-9)
88 , operator_(op)
89 , epsilon_(epsilon) {}
90
91 bool evaluate(double value) const override {
92 switch (operator_) {
94 return value > threshold_;
96 return value >= threshold_ - epsilon_;
98 return value < threshold_;
100 return value <= threshold_ + epsilon_;
102 return std::abs(value - threshold_) <= epsilon_;
104 return std::abs(value - threshold_) > epsilon_;
105 default:
106 return false;
107 }
108 }
109
110 std::string type_name() const override {
111 return "threshold";
112 }
113
114 std::string description() const override {
115 return "value " + std::string(comparison_operator_to_string(operator_)) +
116 " " + std::to_string(threshold_);
117 }
118
122 double threshold() const { return threshold_; }
123
127 comparison_operator op() const { return operator_; }
128
129 // Factory methods for common cases
133 static std::shared_ptr<threshold_trigger> above(double threshold) {
134 return std::make_shared<threshold_trigger>(threshold, comparison_operator::greater_than);
135 }
136
140 static std::shared_ptr<threshold_trigger> above_or_equal(double threshold) {
141 return std::make_shared<threshold_trigger>(threshold, comparison_operator::greater_or_equal);
142 }
143
147 static std::shared_ptr<threshold_trigger> below(double threshold) {
148 return std::make_shared<threshold_trigger>(threshold, comparison_operator::less_than);
149 }
150
154 static std::shared_ptr<threshold_trigger> below_or_equal(double threshold) {
155 return std::make_shared<threshold_trigger>(threshold, comparison_operator::less_or_equal);
156 }
157
161 static std::shared_ptr<class range_trigger> in_range(double min_val, double max_val);
162
166 static std::shared_ptr<class range_trigger> out_of_range(double min_val, double max_val);
167
168private:
171 double epsilon_;
172};
173
179public:
186 range_trigger(double min_value, double max_value, bool inside_range)
187 : min_value_(min_value)
188 , max_value_(max_value)
189 , inside_range_(inside_range) {}
190
191 bool evaluate(double value) const override {
192 bool in_range = (value >= min_value_ && value <= max_value_);
193 return inside_range_ ? in_range : !in_range;
194 }
195
196 std::string type_name() const override {
197 return "range";
198 }
199
200 std::string description() const override {
201 if (inside_range_) {
202 return "value in [" + std::to_string(min_value_) + ", " +
203 std::to_string(max_value_) + "]";
204 }
205 return "value outside [" + std::to_string(min_value_) + ", " +
206 std::to_string(max_value_) + "]";
207 }
208
209private:
213};
214
215inline std::shared_ptr<range_trigger> threshold_trigger::in_range(double min_val, double max_val) {
216 return std::make_shared<range_trigger>(min_val, max_val, true);
217}
218
219inline std::shared_ptr<range_trigger> threshold_trigger::out_of_range(double min_val, double max_val) {
220 return std::make_shared<range_trigger>(min_val, max_val, false);
221}
222
242public:
247 enum class rate_direction {
248 increasing,
249 decreasing,
250 either
251 };
252
260 rate_of_change_trigger(double rate_threshold,
261 std::chrono::milliseconds window,
263 size_t min_samples = 2)
264 : rate_threshold_(rate_threshold)
265 , window_(window)
266 , direction_(direction)
267 , min_samples_(min_samples) {}
268
269 bool evaluate(double value) const override {
270 auto now = std::chrono::steady_clock::now();
271
272 std::lock_guard<std::mutex> lock(mutex_);
273
274 // Add new sample
275 samples_.push_back({value, now});
276
277 // Remove old samples outside window
278 auto cutoff = now - window_;
279 while (!samples_.empty() && samples_.front().timestamp < cutoff) {
280 samples_.pop_front();
281 }
282
283 // Need minimum samples to calculate rate
284 if (samples_.size() < min_samples_) {
285 return false;
286 }
287
288 // Calculate rate of change
289 double rate = calculate_rate();
290
291 switch (direction_) {
293 return rate > rate_threshold_;
295 return rate < -rate_threshold_;
297 return std::abs(rate) > rate_threshold_;
298 default:
299 return false;
300 }
301 }
302
303 std::string type_name() const override {
304 return "rate_of_change";
305 }
306
307 std::string description() const override {
308 std::string dir_str;
309 switch (direction_) {
310 case rate_direction::increasing: dir_str = "increase"; break;
311 case rate_direction::decreasing: dir_str = "decrease"; break;
312 case rate_direction::either: dir_str = "change"; break;
313 }
314 return dir_str + " rate > " + std::to_string(rate_threshold_) +
315 " per " + std::to_string(window_.count()) + "ms";
316 }
317
321 void reset() {
322 std::lock_guard<std::mutex> lock(mutex_);
323 samples_.clear();
324 }
325
326private:
327 struct sample {
328 double value;
329 std::chrono::steady_clock::time_point timestamp;
330 };
331
332 double calculate_rate() const {
333 if (samples_.size() < 2) {
334 return 0.0;
335 }
336
337 // Use linear regression for smoother rate calculation
338 double sum_x = 0.0, sum_y = 0.0, sum_xy = 0.0, sum_xx = 0.0;
339 auto base_time = samples_.front().timestamp;
340
341 for (const auto& s : samples_) {
342 double x = std::chrono::duration<double, std::milli>(
343 s.timestamp - base_time).count();
344 double y = s.value;
345 sum_x += x;
346 sum_y += y;
347 sum_xy += x * y;
348 sum_xx += x * x;
349 }
350
351 double n = static_cast<double>(samples_.size());
352 double denominator = n * sum_xx - sum_x * sum_x;
353
354 if (std::abs(denominator) < 1e-10) {
355 return 0.0;
356 }
357
358 // Slope is rate of change per millisecond
359 double slope = (n * sum_xy - sum_x * sum_y) / denominator;
360
361 // Convert to rate per window
362 return slope * static_cast<double>(window_.count());
363 }
364
366 std::chrono::milliseconds window_;
369
370 mutable std::mutex mutex_;
371 mutable std::deque<sample> samples_;
372};
373
393public:
400 explicit anomaly_trigger(double sensitivity = 3.0,
401 size_t window_size = 100,
402 size_t min_samples = 10)
403 : sensitivity_(sensitivity)
404 , window_size_(window_size)
405 , min_samples_(min_samples) {}
406
407 bool evaluate(double value) const override {
408 std::lock_guard<std::mutex> lock(mutex_);
409
410 // Add to history
411 if (history_.size() >= window_size_) {
412 history_.pop_front();
413 }
414 history_.push_back(value);
415
416 // Need minimum samples for statistics
417 if (history_.size() < min_samples_) {
418 return false;
419 }
420
421 // Calculate statistics
422 double mean_val = mean();
423 double stddev = standard_deviation(mean_val);
424
425 // Avoid division by zero or very small stddev
426 if (stddev < 1e-10) {
427 return false;
428 }
429
430 // Calculate z-score
431 double z_score = std::abs(value - mean_val) / stddev;
432
433 return z_score > sensitivity_;
434 }
435
436 std::string type_name() const override {
437 return "anomaly";
438 }
439
440 std::string description() const override {
441 return "value > " + std::to_string(sensitivity_) + " std devs from mean";
442 }
443
447 double current_mean() const {
448 std::lock_guard<std::mutex> lock(mutex_);
449 return mean();
450 }
451
455 double current_stddev() const {
456 std::lock_guard<std::mutex> lock(mutex_);
457 return standard_deviation(mean());
458 }
459
463 void reset() {
464 std::lock_guard<std::mutex> lock(mutex_);
465 history_.clear();
466 }
467
468private:
469 double mean() const {
470 if (history_.empty()) {
471 return 0.0;
472 }
473 double sum = std::accumulate(history_.begin(), history_.end(), 0.0);
474 return sum / static_cast<double>(history_.size());
475 }
476
477 double standard_deviation(double mean_val) const {
478 if (history_.size() < 2) {
479 return 0.0;
480 }
481 double sq_sum = 0.0;
482 for (double val : history_) {
483 double diff = val - mean_val;
484 sq_sum += diff * diff;
485 }
486 return std::sqrt(sq_sum / static_cast<double>(history_.size() - 1));
487 }
488
492
493 mutable std::mutex mutex_;
494 mutable std::deque<double> history_;
495};
496
502 AND,
503 OR,
504 XOR,
505 NOT
506};
507
529public:
536 std::vector<std::shared_ptr<alert_trigger>> triggers)
537 : operation_(op)
538 , triggers_(std::move(triggers)) {}
539
543 bool evaluate(double value) const override {
544 std::vector<double> values(triggers_.size(), value);
545 return evaluate_multi(values);
546 }
547
553 bool evaluate_multi(const std::vector<double>& values) const {
554 if (triggers_.empty()) {
555 return false;
556 }
557
558 std::vector<bool> results;
559 results.reserve(triggers_.size());
560
561 for (size_t i = 0; i < triggers_.size(); ++i) {
562 double val = (i < values.size()) ? values[i] : values.back();
563 results.push_back(triggers_[i]->evaluate(val));
564 }
565
566 switch (operation_) {
568 return std::all_of(results.begin(), results.end(),
569 [](bool b) { return b; });
571 return std::any_of(results.begin(), results.end(),
572 [](bool b) { return b; });
574 size_t count = std::count(results.begin(), results.end(), true);
575 return count == 1;
576 }
578 return !results.front();
579 default:
580 return false;
581 }
582 }
583
584 std::string type_name() const override {
585 return "composite";
586 }
587
588 std::string description() const override {
589 std::string op_str;
590 switch (operation_) {
591 case composite_operation::AND: op_str = " AND "; break;
592 case composite_operation::OR: op_str = " OR "; break;
593 case composite_operation::XOR: op_str = " XOR "; break;
594 case composite_operation::NOT: return "NOT (" + triggers_.front()->description() + ")";
595 }
596
597 std::string result = "(";
598 for (size_t i = 0; i < triggers_.size(); ++i) {
599 if (i > 0) {
600 result += op_str;
601 }
602 result += triggers_[i]->description();
603 }
604 result += ")";
605 return result;
606 }
607
611 const std::vector<std::shared_ptr<alert_trigger>>& triggers() const {
612 return triggers_;
613 }
614
615 // Factory methods
619 static std::shared_ptr<composite_trigger> all_of(
620 std::vector<std::shared_ptr<alert_trigger>> triggers) {
621 return std::make_shared<composite_trigger>(composite_operation::AND,
622 std::move(triggers));
623 }
624
628 static std::shared_ptr<composite_trigger> any_of(
629 std::vector<std::shared_ptr<alert_trigger>> triggers) {
630 return std::make_shared<composite_trigger>(composite_operation::OR,
631 std::move(triggers));
632 }
633
637 static std::shared_ptr<composite_trigger> invert(
638 std::shared_ptr<alert_trigger> trigger) {
639 return std::make_shared<composite_trigger>(composite_operation::NOT,
640 std::vector<std::shared_ptr<alert_trigger>>{std::move(trigger)});
641 }
642
643private:
645 std::vector<std::shared_ptr<alert_trigger>> triggers_;
646};
647
656public:
661 explicit absent_trigger(std::chrono::milliseconds absent_duration)
662 : absent_duration_(absent_duration) {}
663
664 bool evaluate(double /*value*/) const override {
665 auto now = std::chrono::steady_clock::now();
666
667 std::lock_guard<std::mutex> lock(mutex_);
668
669 auto previous = last_seen_;
670 last_seen_ = now;
671
672 // First evaluation - not absent yet
673 if (previous == std::chrono::steady_clock::time_point{}) {
674 return false;
675 }
676
677 // Check if the gap since previous value exceeds threshold
678 return (now - previous) > absent_duration_;
679 }
680
681 std::string type_name() const override {
682 return "absent";
683 }
684
685 std::string description() const override {
686 return "no data for " + std::to_string(absent_duration_.count()) + "ms";
687 }
688
692 void reset() {
693 std::lock_guard<std::mutex> lock(mutex_);
694 last_seen_ = std::chrono::steady_clock::time_point{};
695 }
696
697private:
698 std::chrono::milliseconds absent_duration_;
699 mutable std::mutex mutex_;
700 mutable std::chrono::steady_clock::time_point last_seen_{};
701};
702
711public:
717 explicit delta_trigger(double delta_threshold, bool absolute = true)
718 : delta_threshold_(delta_threshold)
719 , absolute_(absolute) {}
720
721 bool evaluate(double value) const override {
722 std::lock_guard<std::mutex> lock(mutex_);
723
724 if (!has_previous_) {
725 previous_value_ = value;
726 has_previous_ = true;
727 return false;
728 }
729
730 double delta = value - previous_value_;
731 previous_value_ = value;
732
733 if (absolute_) {
734 return std::abs(delta) > delta_threshold_;
735 }
736 return delta > delta_threshold_;
737 }
738
739 std::string type_name() const override {
740 return "delta";
741 }
742
743 std::string description() const override {
744 if (absolute_) {
745 return "|delta| > " + std::to_string(delta_threshold_);
746 }
747 return "delta > " + std::to_string(delta_threshold_);
748 }
749
753 void reset() {
754 std::lock_guard<std::mutex> lock(mutex_);
755 has_previous_ = false;
756 }
757
758private:
761 mutable std::mutex mutex_;
762 mutable double previous_value_ = 0.0;
763 mutable bool has_previous_ = false;
764};
765
766} // namespace kcenon::monitoring
Alert rule configuration and evaluation.
Trigger when no data is received for a period.
std::chrono::steady_clock::time_point last_seen_
std::string type_name() const override
Get trigger type name.
std::string description() const override
Get human-readable description.
void reset()
Reset last seen timestamp.
bool evaluate(double) const override
Evaluate the trigger condition.
absent_trigger(std::chrono::milliseconds absent_duration)
Construct an absent trigger.
std::chrono::milliseconds absent_duration_
Base class for alert trigger conditions.
Definition alert_rule.h:325
Trigger based on statistical anomaly detection.
double current_mean() const
Get current mean of historical values.
bool evaluate(double value) const override
Evaluate the trigger condition.
anomaly_trigger(double sensitivity=3.0, size_t window_size=100, size_t min_samples=10)
Construct an anomaly trigger.
double current_stddev() const
Get current standard deviation.
std::string description() const override
Get human-readable description.
double standard_deviation(double mean_val) const
std::string type_name() const override
Get trigger type name.
void reset()
Clear historical data.
Combines multiple triggers with logical operations.
bool evaluate(double value) const override
Evaluate with a single value (applies to all triggers)
const std::vector< std::shared_ptr< alert_trigger > > & triggers() const
Get child triggers.
std::string type_name() const override
Get trigger type name.
static std::shared_ptr< composite_trigger > all_of(std::vector< std::shared_ptr< alert_trigger > > triggers)
Create AND composite.
std::vector< std::shared_ptr< alert_trigger > > triggers_
composite_trigger(composite_operation op, std::vector< std::shared_ptr< alert_trigger > > triggers)
Construct a composite trigger.
static std::shared_ptr< composite_trigger > any_of(std::vector< std::shared_ptr< alert_trigger > > triggers)
Create OR composite.
static std::shared_ptr< composite_trigger > invert(std::shared_ptr< alert_trigger > trigger)
Create NOT composite.
bool evaluate_multi(const std::vector< double > &values) const
Evaluate with multiple values (one per trigger)
std::string description() const override
Get human-readable description.
Trigger based on change from previous value.
std::string description() const override
Get human-readable description.
delta_trigger(double delta_threshold, bool absolute=true)
Construct a delta trigger.
std::string type_name() const override
Get trigger type name.
void reset()
Reset previous value.
bool evaluate(double value) const override
Evaluate the trigger condition.
Trigger based on value being within or outside a range.
std::string type_name() const override
Get trigger type name.
bool evaluate(double value) const override
Evaluate the trigger condition.
range_trigger(double min_value, double max_value, bool inside_range)
Construct a range trigger.
std::string description() const override
Get human-readable description.
Trigger based on rate of change of values.
bool evaluate(double value) const override
Evaluate the trigger condition.
rate_direction
Direction of rate change to monitor.
std::string description() const override
Get human-readable description.
void reset()
Clear accumulated samples.
rate_of_change_trigger(double rate_threshold, std::chrono::milliseconds window, rate_direction direction=rate_direction::either, size_t min_samples=2)
Construct a rate of change trigger.
std::string type_name() const override
Get trigger type name.
Trigger based on comparing value against a threshold.
double threshold() const
Get the threshold value.
static std::shared_ptr< threshold_trigger > below_or_equal(double threshold)
Create trigger for value <= threshold.
static std::shared_ptr< threshold_trigger > below(double threshold)
Create trigger for value < threshold.
static std::shared_ptr< class range_trigger > in_range(double min_val, double max_val)
Create trigger for value within range (inclusive)
std::string type_name() const override
Get trigger type name.
std::string description() const override
Get human-readable description.
comparison_operator op() const
Get the comparison operator.
static std::shared_ptr< threshold_trigger > above_or_equal(double threshold)
Create trigger for value >= threshold.
bool evaluate(double value) const override
Evaluate the trigger condition.
static std::shared_ptr< class range_trigger > out_of_range(double min_val, double max_val)
Create trigger for value outside range (exclusive)
threshold_trigger(double threshold, comparison_operator op=comparison_operator::greater_than, double epsilon=1e-9)
Construct a threshold trigger.
static std::shared_ptr< threshold_trigger > above(double threshold)
Create trigger for value > threshold.
comparison_operator
Comparison operators for threshold triggers.
@ equal
value == threshold (with epsilon)
@ not_equal
value != threshold (with epsilon)
composite_operation
Logical operations for combining triggers.
@ NOT
Invert single trigger (uses first trigger only)
@ XOR
Exactly one trigger fires.
constexpr const char * comparison_operator_to_string(comparison_operator op) noexcept
Convert comparison operator to string.
std::chrono::steady_clock::time_point timestamp