Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions includes/alarmmanager.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,15 @@ function syslog_alarmmanager_forward(array $alert, array $matches, int $count, b
throw new RuntimeException('source_identity_missing');
}
}
if ((int) $alert['method'] === 0 && !$recovered && !$state && count($matches) > 1) {
$accepted = true;
foreach ($matches as $match) {
if (!syslog_alarmmanager_forward($alert, [$match], 1)) {
$accepted = false;
}
}
return $accepted;
}
usort($matches, static fn($a, $b) => (int) $a['seq'] <=> (int) $b['seq']);
$first = $matches[0];
$identity = hash('sha256', json_encode([$alert['id'], $collector, array_map(static fn($m) => [$m['seq'], $m['logtime']], $matches)], JSON_THROW_ON_ERROR));
Expand Down
37 changes: 37 additions & 0 deletions tests/Unit/AlarmManagerForwardingTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,43 @@ function alarmmanager_test_matches(): array {
expect($first['alarm_key'])->not->toBe($next['alarm_key']);
});

it('submits traditional matches separately with stable identities across batches', function () {
$rule = array_replace(alarmmanager_test_rule(), ['method' => 0]);
$matches = alarmmanager_test_matches();
$matches[] = array_replace($matches[0], ['seq' => 10, 'host' => 'other-router', 'message' => 'second message']);
expect(syslog_alarmmanager_forward($rule, $matches, 2))->toBeTrue();
[$first, $second] = $GLOBALS['alarmmanager_test_events'];
expect($GLOBALS['alarmmanager_test_events'])->toHaveCount(2);
expect($first['transition'])->toBe('occurrence');
expect($first['details'])->not->toContain('second message');
expect($second['device']['hostname'])->toBe('other-router');
expect($second['details'])->not->toContain('fixture');
expect($second['context']['metric_value'])->toBe('1');
expect($first['alarm_key'])->not->toBe($second['alarm_key']);
expect(syslog_alarmmanager_forward($rule, [$matches[0]], 1))->toBeTrue();
expect($GLOBALS['alarmmanager_test_events'][2])->toBe($first);
});

it('attempts every traditional match and retains fallback after partial acceptance', function () {
$rule = array_replace(alarmmanager_test_rule(), ['method' => 0]);
$matches = alarmmanager_test_matches();
$matches[] = array_replace($matches[0], ['seq' => 10]);
test_override('api_plugin_hook_function', static function ($hook, $envelope) {
$GLOBALS['alarmmanager_test_events'][] = $envelope['event'];
return $envelope + ['status' => $envelope['event']['sequence'] === 9 ? 'failed' : 'accepted', 'event_row_id' => 42];
});
expect(syslog_alarmmanager_forward($rule, $matches, 2))->toBeFalse();
expect($GLOBALS['alarmmanager_test_events'])->toHaveCount(2);
});

it('keeps threshold matches grouped', function () {
$matches = alarmmanager_test_matches();
$matches[] = array_replace($matches[0], ['seq' => 10, 'message' => 'second message']);
expect(syslog_alarmmanager_forward(alarmmanager_test_rule(), $matches, 5))->toBeTrue();
expect($GLOBALS['alarmmanager_test_events'])->toHaveCount(1);
expect($GLOBALS['alarmmanager_test_events'][0]['details'])->toContain('fixture', 'second message');
});

it('correlates threshold recovery and records acceptance without resending on local failure', function ($write) {
$rule = alarmmanager_test_rule();
$rule['_threshold_state'] = ['source_poller_id' => 2, 'breach_key' => str_repeat('a', 40)];
Expand Down
13 changes: 13 additions & 0 deletions tests/regression/alarmmanager_forward_test.php
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,20 @@ function check($condition, $message) { $GLOBALS['checks']++; if (!$condition) {
check($second['event_id'] !== $first['event_id'] && $second['alarm_key'] !== $first['alarm_key'], 'Independent occurrence has distinct event and stream');
$empty_state = ['last_sequence' => 0, 'episode_id' => '', 'active' => 0, 'acknowledged' => 0];
check(alarmmanager_transition($empty_state, $first)[1] === 'occurrence' && alarmmanager_transition($empty_state, $second)[1] === 'occurrence', 'Real Alarm Manager lifecycle accepts each independent stream');
$batch = [$matches[0], array_replace($matches[0], ['seq' => 102, 'host' => 'other.example', 'message' => 'second message'])];
$before = count($submitted);
check(syslog_alarmmanager_forward($rule, $batch, 2), 'Traditional batch accepted');
$events = array_column(array_slice($submitted, $before), 'event');
check(count($events) === 2 && $events[0] === $second, 'Each match is a separate occurrence with stable identity across batches');
check($events[1]['device']['hostname'] === 'other.example' && !str_contains($events[1]['details'], 'sensitive test message') && $events[1]['context']['metric_value'] === '1', 'Occurrence contains only its own host and message');
check($events[0]['alarm_key'] !== $events[1]['alarm_key'], 'Batch occurrences use independent streams');
$response = 'failed';
$before = count($submitted);
check(!syslog_alarmmanager_forward($rule, $batch, 2) && count($submitted) === $before + 2, 'Failure retains fallback and still attempts every match');
$response = 'accepted';
$rule['method'] = 1;
$before = count($submitted);
check(syslog_alarmmanager_forward($rule, $batch, 5) && count($submitted) === $before + 1, 'Threshold batch remains one event');
$rule['_threshold_state'] = ['source_poller_id' => 2, 'breach_key' => str_repeat('a', 40)];
check(syslog_alarmmanager_forward($rule, $matches, 5), 'Threshold raise accepted');
$raised = end($submitted)['event'];
Expand Down
Loading