Monitoring System 0.1.0
System resource monitoring with pluggable collectors and alerting
Loading...
Searching...
No Matches
test_alert_types.cpp
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
17#include <gtest/gtest.h>
19
20#include <chrono>
21#include <thread>
22
23using namespace kcenon::monitoring;
24using namespace std::chrono_literals;
25
26// =============================================================================
27// alert_severity Tests
28// =============================================================================
29
30TEST(AlertSeverityTest, ToStringConversions) {
31 EXPECT_STREQ(alert_severity_to_string(alert_severity::info), "info");
32 EXPECT_STREQ(alert_severity_to_string(alert_severity::warning), "warning");
33 EXPECT_STREQ(alert_severity_to_string(alert_severity::critical), "critical");
34 EXPECT_STREQ(alert_severity_to_string(alert_severity::emergency), "emergency");
35}
36
37TEST(AlertSeverityTest, OrderingByValue) {
38 EXPECT_LT(static_cast<uint8_t>(alert_severity::info),
39 static_cast<uint8_t>(alert_severity::warning));
40 EXPECT_LT(static_cast<uint8_t>(alert_severity::warning),
41 static_cast<uint8_t>(alert_severity::critical));
42 EXPECT_LT(static_cast<uint8_t>(alert_severity::critical),
43 static_cast<uint8_t>(alert_severity::emergency));
44}
45
46// =============================================================================
47// alert_state Tests
48// =============================================================================
49
50TEST(AlertStateTest, ToStringConversions) {
51 EXPECT_STREQ(alert_state_to_string(alert_state::inactive), "inactive");
52 EXPECT_STREQ(alert_state_to_string(alert_state::pending), "pending");
53 EXPECT_STREQ(alert_state_to_string(alert_state::firing), "firing");
54 EXPECT_STREQ(alert_state_to_string(alert_state::resolved), "resolved");
55 EXPECT_STREQ(alert_state_to_string(alert_state::suppressed), "suppressed");
56}
57
58// =============================================================================
59// alert_labels Tests
60// =============================================================================
61
62class AlertLabelsTest : public ::testing::Test {
63protected:
65};
66
67TEST_F(AlertLabelsTest, DefaultConstructionIsEmpty) {
68 EXPECT_TRUE(labels_.labels.empty());
69}
70
71TEST_F(AlertLabelsTest, ConstructFromMap) {
72 std::unordered_map<std::string, std::string> map{
73 {"env", "production"}, {"service", "api"}};
74 alert_labels lbl(map);
75 EXPECT_EQ(lbl.labels.size(), 2u);
76 EXPECT_EQ(lbl.get("env"), "production");
77}
78
80 labels_.set("team", "infra");
81 EXPECT_EQ(labels_.get("team"), "infra");
82}
83
84TEST_F(AlertLabelsTest, GetNonexistentReturnsEmpty) {
85 EXPECT_EQ(labels_.get("missing"), "");
86}
87
88TEST_F(AlertLabelsTest, HasExistingKey) {
89 labels_.set("region", "us-east");
90 EXPECT_TRUE(labels_.has("region"));
91 EXPECT_FALSE(labels_.has("zone"));
92}
93
94TEST_F(AlertLabelsTest, SetOverwritesExisting) {
95 labels_.set("env", "staging");
96 labels_.set("env", "production");
97 EXPECT_EQ(labels_.get("env"), "production");
98}
99
100TEST_F(AlertLabelsTest, FingerprintIsDeterministic) {
101 labels_.set("b", "2");
102 labels_.set("a", "1");
103 auto fp1 = labels_.fingerprint();
104
106 other.set("a", "1");
107 other.set("b", "2");
108 auto fp2 = other.fingerprint();
109
110 // Sorted order: a=1,b=2, regardless of insertion order
111 EXPECT_EQ(fp1, fp2);
112}
113
114TEST_F(AlertLabelsTest, FingerprintDiffersForDifferentValues) {
115 labels_.set("key", "value1");
116 auto fp1 = labels_.fingerprint();
117
119 other.set("key", "value2");
120 auto fp2 = other.fingerprint();
121
122 EXPECT_NE(fp1, fp2);
123}
124
125TEST_F(AlertLabelsTest, EqualityOperator) {
126 labels_.set("a", "1");
127 labels_.set("b", "2");
128
130 other.set("a", "1");
131 other.set("b", "2");
132
133 EXPECT_EQ(labels_, other);
134}
135
136TEST_F(AlertLabelsTest, InequalityWhenDifferent) {
137 labels_.set("a", "1");
138
140 other.set("a", "2");
141
142 EXPECT_FALSE(labels_ == other);
143}
144
145// =============================================================================
146// alert_annotations Tests
147// =============================================================================
148
149TEST(AlertAnnotationsTest, DefaultConstruction) {
151 EXPECT_TRUE(ann.summary.empty());
152 EXPECT_TRUE(ann.description.empty());
153 EXPECT_FALSE(ann.runbook_url.has_value());
154 EXPECT_TRUE(ann.custom.empty());
155}
156
157TEST(AlertAnnotationsTest, ConstructWithSummaryAndDescription) {
158 alert_annotations ann("High CPU", "CPU usage exceeded 80%");
159 EXPECT_EQ(ann.summary, "High CPU");
160 EXPECT_EQ(ann.description, "CPU usage exceeded 80%");
161}
162
163TEST(AlertAnnotationsTest, RunbookUrl) {
165 ann.runbook_url = "https://runbooks.example.com/cpu";
166 EXPECT_TRUE(ann.runbook_url.has_value());
167 EXPECT_EQ(*ann.runbook_url, "https://runbooks.example.com/cpu");
168}
169
170TEST(AlertAnnotationsTest, CustomAnnotations) {
172 ann.custom["dashboard"] = "grafana/cpu";
173 EXPECT_EQ(ann.custom.at("dashboard"), "grafana/cpu");
174}
175
176// =============================================================================
177// alert struct Tests
178// =============================================================================
179
180class AlertTest : public ::testing::Test {
181protected:
183 alert_labels labels;
184 labels.set("service", "api");
185 labels.set("env", "prod");
186 alert a("high_cpu", labels);
187 a.severity = alert_severity::critical;
188 a.value = 95.0;
189 return a;
190 }
191};
192
193TEST_F(AlertTest, DefaultConstruction) {
194 alert a;
195 EXPECT_TRUE(a.name.empty());
196 EXPECT_EQ(a.state, alert_state::inactive);
197 EXPECT_EQ(a.severity, alert_severity::warning);
198 EXPECT_EQ(a.value, 0.0);
199 EXPECT_FALSE(a.started_at.has_value());
200 EXPECT_FALSE(a.resolved_at.has_value());
201}
202
203TEST_F(AlertTest, ConstructWithNameAndLabels) {
204 auto a = create_test_alert();
205 EXPECT_EQ(a.name, "high_cpu");
206 EXPECT_EQ(a.labels.get("service"), "api");
207 EXPECT_EQ(a.severity, alert_severity::critical);
208}
209
210TEST_F(AlertTest, UniqueIds) {
211 alert a1;
212 alert a2;
213 EXPECT_NE(a1.id, a2.id);
214}
215
216TEST_F(AlertTest, FingerprintIncludesNameAndLabels) {
217 auto a = create_test_alert();
218 auto fp = a.fingerprint();
219 EXPECT_FALSE(fp.empty());
220 EXPECT_NE(fp.find("high_cpu"), std::string::npos);
221}
222
223TEST_F(AlertTest, FingerprintConsistency) {
224 auto a1 = create_test_alert();
225 auto a2 = create_test_alert();
226 // Same name + same labels = same fingerprint (for dedup)
227 EXPECT_EQ(a1.fingerprint(), a2.fingerprint());
228}
229
230TEST_F(AlertTest, IsActiveForPendingAndFiring) {
231 alert a;
232 EXPECT_FALSE(a.is_active()); // inactive
233
234 a.state = alert_state::pending;
235 EXPECT_TRUE(a.is_active());
236
237 a.state = alert_state::firing;
238 EXPECT_TRUE(a.is_active());
239
240 a.state = alert_state::resolved;
241 EXPECT_FALSE(a.is_active());
242
243 a.state = alert_state::suppressed;
244 EXPECT_FALSE(a.is_active());
245}
246
247TEST_F(AlertTest, StateDurationIsPositive) {
248 alert a;
249 // Sleep briefly to ensure non-zero duration
250 std::this_thread::sleep_for(1ms);
251 auto dur = a.state_duration();
252 EXPECT_GT(dur.count(), 0);
253}
254
255TEST_F(AlertTest, FiringDurationZeroWhenNotFiring) {
256 alert a;
257 EXPECT_EQ(a.firing_duration().count(), 0);
258}
259
260// =============================================================================
261// alert State Transition Tests
262// =============================================================================
263
264class AlertTransitionTest : public ::testing::Test {
265protected:
267};
268
269TEST_F(AlertTransitionTest, InactiveToLending) {
270 EXPECT_TRUE(a_.transition_to(alert_state::pending));
271 EXPECT_EQ(a_.state, alert_state::pending);
272}
273
274TEST_F(AlertTransitionTest, InactiveToFiringInvalid) {
275 EXPECT_FALSE(a_.transition_to(alert_state::firing));
276 EXPECT_EQ(a_.state, alert_state::inactive);
277}
278
279TEST_F(AlertTransitionTest, InactiveToResolvedInvalid) {
280 EXPECT_FALSE(a_.transition_to(alert_state::resolved));
281 EXPECT_EQ(a_.state, alert_state::inactive);
282}
283
284TEST_F(AlertTransitionTest, PendingToFiring) {
285 a_.transition_to(alert_state::pending);
286 EXPECT_TRUE(a_.transition_to(alert_state::firing));
287 EXPECT_EQ(a_.state, alert_state::firing);
288 EXPECT_TRUE(a_.started_at.has_value());
289}
290
291TEST_F(AlertTransitionTest, PendingToInactive) {
292 a_.transition_to(alert_state::pending);
293 EXPECT_TRUE(a_.transition_to(alert_state::inactive));
294 EXPECT_EQ(a_.state, alert_state::inactive);
295}
296
297TEST_F(AlertTransitionTest, FiringToResolved) {
298 a_.transition_to(alert_state::pending);
299 a_.transition_to(alert_state::firing);
300 EXPECT_TRUE(a_.transition_to(alert_state::resolved));
301 EXPECT_EQ(a_.state, alert_state::resolved);
302 EXPECT_TRUE(a_.resolved_at.has_value());
303}
304
305TEST_F(AlertTransitionTest, FiringToPendingInvalid) {
306 a_.transition_to(alert_state::pending);
307 a_.transition_to(alert_state::firing);
308 EXPECT_FALSE(a_.transition_to(alert_state::pending));
309 EXPECT_EQ(a_.state, alert_state::firing);
310}
311
312TEST_F(AlertTransitionTest, ResolvedToPending) {
313 a_.transition_to(alert_state::pending);
314 a_.transition_to(alert_state::firing);
315 a_.transition_to(alert_state::resolved);
316 EXPECT_TRUE(a_.transition_to(alert_state::pending));
317 EXPECT_EQ(a_.state, alert_state::pending);
318}
319
320TEST_F(AlertTransitionTest, ResolvedToInactive) {
321 a_.transition_to(alert_state::pending);
322 a_.transition_to(alert_state::firing);
323 a_.transition_to(alert_state::resolved);
324 EXPECT_TRUE(a_.transition_to(alert_state::inactive));
325}
326
327TEST_F(AlertTransitionTest, AnyStateToSuppressed) {
328 EXPECT_TRUE(a_.transition_to(alert_state::suppressed));
329 EXPECT_EQ(a_.state, alert_state::suppressed);
330}
331
332TEST_F(AlertTransitionTest, SuppressedToAnyState) {
333 a_.transition_to(alert_state::suppressed);
334
335 // From suppressed, all transitions should be valid
336 EXPECT_TRUE(a_.transition_to(alert_state::firing));
337 EXPECT_EQ(a_.state, alert_state::firing);
338}
339
341 // inactive -> pending -> firing -> resolved -> pending -> firing -> resolved
342 EXPECT_TRUE(a_.transition_to(alert_state::pending));
343 EXPECT_TRUE(a_.transition_to(alert_state::firing));
344 EXPECT_TRUE(a_.transition_to(alert_state::resolved));
345 EXPECT_TRUE(a_.transition_to(alert_state::pending));
346 EXPECT_TRUE(a_.transition_to(alert_state::firing));
347 EXPECT_TRUE(a_.transition_to(alert_state::resolved));
348}
349
350TEST_F(AlertTransitionTest, FiringStartedAtSetOnlyOnce) {
351 a_.transition_to(alert_state::pending);
352 a_.transition_to(alert_state::firing);
353 auto first_started = a_.started_at;
354
355 // Resolve and re-fire
356 a_.transition_to(alert_state::resolved);
357 a_.transition_to(alert_state::pending);
358 a_.transition_to(alert_state::firing);
359
360 // started_at should remain the same (first firing)
361 EXPECT_EQ(a_.started_at, first_started);
362}
363
364TEST_F(AlertTransitionTest, UpdatedAtChangesOnTransition) {
365 auto initial_updated = a_.updated_at;
366 std::this_thread::sleep_for(1ms);
367 a_.transition_to(alert_state::pending);
368 EXPECT_GT(a_.updated_at, initial_updated);
369}
370
371// =============================================================================
372// alert_group Tests
373// =============================================================================
374
375class AlertGroupTest : public ::testing::Test {
376protected:
377 alert_group group_{"test_group"};
378};
379
380TEST_F(AlertGroupTest, DefaultConstruction) {
381 alert_group g;
382 EXPECT_TRUE(g.group_key.empty());
383 EXPECT_TRUE(g.empty());
384 EXPECT_EQ(g.size(), 0u);
385}
386
387TEST_F(AlertGroupTest, ConstructWithKey) {
388 EXPECT_EQ(group_.group_key, "test_group");
389 EXPECT_TRUE(group_.empty());
390}
391
393 alert a("test", alert_labels{});
394 group_.add_alert(a);
395 EXPECT_EQ(group_.size(), 1u);
396 EXPECT_FALSE(group_.empty());
397}
398
399TEST_F(AlertGroupTest, AddMultipleAlerts) {
400 for (int i = 0; i < 5; ++i) {
401 alert a("alert_" + std::to_string(i), alert_labels{});
402 group_.add_alert(a);
403 }
404 EXPECT_EQ(group_.size(), 5u);
405}
406
407TEST_F(AlertGroupTest, MaxSeverityEmptyGroup) {
408 EXPECT_EQ(group_.max_severity(), alert_severity::info);
409}
410
411TEST_F(AlertGroupTest, MaxSeveritySingleAlert) {
412 alert a;
413 a.severity = alert_severity::critical;
414 group_.add_alert(a);
415 EXPECT_EQ(group_.max_severity(), alert_severity::critical);
416}
417
418TEST_F(AlertGroupTest, MaxSeverityMultipleAlerts) {
419 alert a1;
420 a1.severity = alert_severity::info;
421 group_.add_alert(a1);
422
423 alert a2;
424 a2.severity = alert_severity::emergency;
425 group_.add_alert(a2);
426
427 alert a3;
428 a3.severity = alert_severity::warning;
429 group_.add_alert(a3);
430
431 EXPECT_EQ(group_.max_severity(), alert_severity::emergency);
432}
433
434TEST_F(AlertGroupTest, UpdatedAtChangesOnAdd) {
435 auto initial = group_.updated_at;
436 std::this_thread::sleep_for(1ms);
437 alert a;
438 group_.add_alert(a);
439 EXPECT_GT(group_.updated_at, initial);
440}
441
442// =============================================================================
443// alert_silence Tests
444// =============================================================================
445
446class AlertSilenceTest : public ::testing::Test {
447protected:
449};
450
451TEST_F(AlertSilenceTest, DefaultConstructionIsActive) {
452 // Default: starts_at = now, ends_at = now + 1 hour
453 EXPECT_TRUE(silence_.is_active());
454}
455
457 alert_silence s1;
458 alert_silence s2;
459 EXPECT_NE(s1.id, s2.id);
460}
461
462TEST_F(AlertSilenceTest, ExpiredSilenceNotActive) {
463 silence_.starts_at = std::chrono::steady_clock::now() - 2h;
464 silence_.ends_at = std::chrono::steady_clock::now() - 1h;
465 EXPECT_FALSE(silence_.is_active());
466}
467
468TEST_F(AlertSilenceTest, FutureSilenceNotActive) {
469 silence_.starts_at = std::chrono::steady_clock::now() + 1h;
470 silence_.ends_at = std::chrono::steady_clock::now() + 2h;
471 EXPECT_FALSE(silence_.is_active());
472}
473
474TEST_F(AlertSilenceTest, MatchesAlertWithMatchingLabels) {
475 silence_.matchers.set("service", "api");
476
477 alert a;
478 a.labels.set("service", "api");
479 a.labels.set("env", "prod");
480
481 EXPECT_TRUE(silence_.matches(a));
482}
483
484TEST_F(AlertSilenceTest, DoesNotMatchAlertWithDifferentLabels) {
485 silence_.matchers.set("service", "api");
486
487 alert a;
488 a.labels.set("service", "web");
489
490 EXPECT_FALSE(silence_.matches(a));
491}
492
493TEST_F(AlertSilenceTest, DoesNotMatchAlertMissingLabel) {
494 silence_.matchers.set("service", "api");
495
496 alert a;
497 a.labels.set("env", "prod");
498
499 EXPECT_FALSE(silence_.matches(a));
500}
501
502TEST_F(AlertSilenceTest, EmptyMatchersMatchesAll) {
503 // No matcher labels means all alerts match
504 alert a;
505 a.labels.set("anything", "value");
506 EXPECT_TRUE(silence_.matches(a));
507}
508
509TEST_F(AlertSilenceTest, ExpiredSilenceDoesNotMatch) {
510 silence_.matchers.set("service", "api");
511 silence_.starts_at = std::chrono::steady_clock::now() - 2h;
512 silence_.ends_at = std::chrono::steady_clock::now() - 1h;
513
514 alert a;
515 a.labels.set("service", "api");
516
517 EXPECT_FALSE(silence_.matches(a));
518}
519
520TEST_F(AlertSilenceTest, MultipleMatchersMustAllMatch) {
521 silence_.matchers.set("service", "api");
522 silence_.matchers.set("env", "prod");
523
524 alert a1;
525 a1.labels.set("service", "api");
526 a1.labels.set("env", "prod");
527 EXPECT_TRUE(silence_.matches(a1));
528
529 alert a2;
530 a2.labels.set("service", "api");
531 a2.labels.set("env", "staging");
532 EXPECT_FALSE(silence_.matches(a2));
533}
534
535TEST_F(AlertSilenceTest, CommentAndCreatedBy) {
536 silence_.comment = "Maintenance window";
537 silence_.created_by = "admin@example.com";
538 EXPECT_EQ(silence_.comment, "Maintenance window");
539 EXPECT_EQ(silence_.created_by, "admin@example.com");
540}
Core alert data structures for the monitoring system.
alert create_test_alert()
constexpr const char * alert_state_to_string(alert_state state) noexcept
Convert alert state to string.
Definition alert_types.h:82
constexpr const char * alert_severity_to_string(alert_severity severity) noexcept
Convert alert severity to string.
Definition alert_types.h:47
Additional metadata for alert context.
std::string description
Detailed description.
std::unordered_map< std::string, std::string > custom
Custom annotations.
std::string summary
Brief description.
std::optional< std::string > runbook_url
Link to runbook.
Group of related alerts for batch notification.
std::string group_key
Common grouping key.
bool empty() const
Check if group is empty.
size_t size() const
Get count of alerts in the group.
Key-value labels for alert identification and routing.
std::unordered_map< std::string, std::string > labels
void set(const std::string &key, const std::string &value)
Add or update a label.
std::string get(const std::string &key) const
Get a label value.
Silence configuration to suppress alerts.
Core alert data structure.
alert_state state
Current state.
std::optional< std::chrono::steady_clock::time_point > started_at
When firing started.
double value
Current metric value.
std::optional< std::chrono::steady_clock::time_point > resolved_at
When resolved.
alert_severity severity
Alert severity level.
std::chrono::steady_clock::duration state_duration() const
Get duration in current state.
uint64_t id
Unique alert ID.
std::string name
Alert name/identifier.
bool is_active() const
Check if alert is currently active (pending or firing)
alert_labels labels
Identifying labels.
std::chrono::steady_clock::duration firing_duration() const
Get firing duration (if currently firing)
TEST(AlertSeverityTest, ToStringConversions)
TEST_F(AlertLabelsTest, DefaultConstructionIsEmpty)