mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-09-04 06:35:29 +00:00
feat: add service-scoped stack alert rules (#1681)
* feat: add service-scoped stack alert rules
Stack alerts can target one Compose service or all services. Breach timers
are per container and cooldowns are per service so a healthy sibling no
longer clears another container's timer or silences a different service.
* fix: gate remote scoped alert creates without losing the body
Remote hops skip JSON parsing so the proxy stream stays pipeable, which
left service_name invisible to the capability gate. Buffer POST /alerts
bodies for inspection, fail closed when the remote lacks the capability,
and rewrite the buffered bytes on forward. Restore alert-panel alt text
to match the unchanged screenshot.
* fix: bound remote alert body buffer and reject encoded JSON
Cap proxied POST /alerts buffering at the local 100KB JSON limit with
structured 413 cleanup, reject non-identity Content-Encoding with 415 so
compressed scoped bodies cannot bypass the mixed-version gate, and cover
oversized, chunked, and gzip regressions.
* fix: harden service-scoped alert delete, cooldown, and proxy gates
Reject non-digit alert ids, dual-write last_fired_at for rollback safety,
gate cooldown on persisted notification history, fail-fast oversized proxy
bodies with 413, and clarify Not in compose UI semantics.
* test: expect dispatchAlert persisted result in crash-safety cases
Update notification-routing assertions for the new { persisted } return
shape so CI matches the cooldown-gating contract.
This commit is contained in:
@@ -324,6 +324,7 @@ describe('DatabaseService - stack alerts CRUD', () => {
|
||||
it('adds and retrieves stack alerts', () => {
|
||||
db.addStackAlert({
|
||||
stack_name: 'alert-stack',
|
||||
service_name: null,
|
||||
metric: 'cpu_percent',
|
||||
operator: '>',
|
||||
threshold: 80,
|
||||
@@ -339,11 +340,27 @@ describe('DatabaseService - stack alerts CRUD', () => {
|
||||
expect(found.threshold).toBe(80);
|
||||
expect(found.duration_mins).toBe(5);
|
||||
expect(found.cooldown_mins).toBe(15);
|
||||
expect(found.service_name).toBeNull();
|
||||
});
|
||||
|
||||
it('persists a named service_name', () => {
|
||||
const created = db.addStackAlert({
|
||||
stack_name: 'scoped-stack',
|
||||
service_name: 'api.web',
|
||||
metric: 'cpu_percent',
|
||||
operator: '>',
|
||||
threshold: 80,
|
||||
duration_mins: 5,
|
||||
cooldown_mins: 15,
|
||||
});
|
||||
expect(created.service_name).toBe('api.web');
|
||||
expect(db.getStackAlerts('scoped-stack')[0].service_name).toBe('api.web');
|
||||
});
|
||||
|
||||
it('filters alerts by stack name', () => {
|
||||
db.addStackAlert({
|
||||
stack_name: 'filter-stack-a',
|
||||
service_name: null,
|
||||
metric: 'memory_mb',
|
||||
operator: '>=',
|
||||
threshold: 512,
|
||||
@@ -352,6 +369,7 @@ describe('DatabaseService - stack alerts CRUD', () => {
|
||||
});
|
||||
db.addStackAlert({
|
||||
stack_name: 'filter-stack-b',
|
||||
service_name: null,
|
||||
metric: 'cpu_percent',
|
||||
operator: '>',
|
||||
threshold: 90,
|
||||
@@ -371,6 +389,7 @@ describe('DatabaseService - stack alerts CRUD', () => {
|
||||
it('updates last_fired_at timestamp', () => {
|
||||
db.addStackAlert({
|
||||
stack_name: 'fired-stack',
|
||||
service_name: null,
|
||||
metric: 'net_rx',
|
||||
operator: '>',
|
||||
threshold: 100,
|
||||
@@ -381,29 +400,55 @@ describe('DatabaseService - stack alerts CRUD', () => {
|
||||
const alerts = db.getStackAlerts('fired-stack');
|
||||
const alert = alerts[0];
|
||||
const fireTime = Date.now();
|
||||
db.updateStackAlertLastFired(alert.id, fireTime);
|
||||
db.updateStackAlertLastFired(alert.id!, fireTime);
|
||||
|
||||
const updated = db.getStackAlerts('fired-stack');
|
||||
expect(updated[0].last_fired_at).toBe(fireTime);
|
||||
});
|
||||
|
||||
it('deletes an alert by id', () => {
|
||||
db.addStackAlert({
|
||||
it('upserts and reads distinct per-service cooldowns', () => {
|
||||
const alert = db.addStackAlert({
|
||||
stack_name: 'cooldown-stack',
|
||||
service_name: null,
|
||||
metric: 'cpu_percent',
|
||||
operator: '>',
|
||||
threshold: 80,
|
||||
duration_mins: 1,
|
||||
cooldown_mins: 10,
|
||||
});
|
||||
const id = alert.id!;
|
||||
expect(db.hasAnyStackAlertServiceCooldown(id)).toBe(false);
|
||||
|
||||
db.upsertStackAlertServiceCooldown(id, 'api', 1000);
|
||||
db.upsertStackAlertServiceCooldown(id, 'database', 2000);
|
||||
|
||||
expect(db.getStackAlertServiceCooldown(id, 'api')).toBe(1000);
|
||||
expect(db.getStackAlertServiceCooldown(id, 'database')).toBe(2000);
|
||||
expect(db.hasAnyStackAlertServiceCooldown(id)).toBe(true);
|
||||
|
||||
db.upsertStackAlertServiceCooldown(id, 'api', 3000);
|
||||
expect(db.getStackAlertServiceCooldown(id, 'api')).toBe(3000);
|
||||
});
|
||||
|
||||
it('deletes an alert by id and removes child cooldown rows', () => {
|
||||
const alert = db.addStackAlert({
|
||||
stack_name: 'delete-stack',
|
||||
service_name: null,
|
||||
metric: 'memory_percent',
|
||||
operator: '>',
|
||||
threshold: 95,
|
||||
duration_mins: 1,
|
||||
cooldown_mins: 5,
|
||||
});
|
||||
const id = alert.id!;
|
||||
db.upsertStackAlertServiceCooldown(id, 'api', Date.now());
|
||||
expect(db.hasAnyStackAlertServiceCooldown(id)).toBe(true);
|
||||
|
||||
const before = db.getStackAlerts('delete-stack');
|
||||
expect(before.length).toBe(1);
|
||||
db.deleteStackAlert(id);
|
||||
|
||||
db.deleteStackAlert(before[0].id);
|
||||
|
||||
const after = db.getStackAlerts('delete-stack');
|
||||
expect(after.length).toBe(0);
|
||||
expect(db.getStackAlerts('delete-stack').length).toBe(0);
|
||||
expect(db.hasAnyStackAlertServiceCooldown(id)).toBe(false);
|
||||
expect(db.getStackAlertServiceCooldown(id, 'api')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user