feat(alerting): AGENT_LIFECYCLE condition kind with per-subject fire mode
Allows alert rules to fire on agent-lifecycle events — REGISTERED, RE_REGISTERED, DEREGISTERED, WENT_STALE, WENT_DEAD, RECOVERED — rather than only on current state. Each matching `(agent, eventType, timestamp)` becomes its own ackable AlertInstance, so outages on distinct agents are independently routable. Core: - New `ConditionKind.AGENT_LIFECYCLE` + `AgentLifecycleCondition` record (scope, eventTypes, withinSeconds). Compact ctor rejects empty eventTypes and withinSeconds<1. - Strict allowlist enum `AgentLifecycleEventType` (six entries matching the server-emitted types in `AgentRegistrationController` and `AgentLifecycleMonitor`). Custom agent-emitted event types tracked in backlog issue #145. - `AgentEventRepository.findInWindow(env, appSlug, agentId, eventTypes, from, to, limit)` — new read path ordered `(timestamp ASC, insert_id ASC)` used by the evaluator. Implemented on `ClickHouseAgentEventRepository` with tenant + env filter mandatory. App: - `AgentLifecycleEvaluator` queries events in the last `withinSeconds` window and returns `EvalResult.Batch` with one `Firing` per row. Every Firing carries a canonical `_subjectFingerprint` of `"<agentId>:<eventType>:<tsMillis>"` in context plus `agent` / `event` subtrees for Mustache templating. - `NotificationContextBuilder` gains an `AGENT_LIFECYCLE` branch that exposes `{{agent.id}}`, `{{agent.app}}`, `{{event.type}}`, `{{event.timestamp}}`, `{{event.detail}}`. - Validation is delegated to the record compact ctor + enum at Jackson deserialization time — matches the existing policy of keeping controller validators focused on env-scoped / SQL-injection concerns. Schema: - V16 migration generalises the V15 per-exchange discriminator on `alert_instances_open_rule_uq` to prefer `_subjectFingerprint` with a fallback to the legacy `exchange.id` expression. Scalar kinds still resolve to `''` and keep one-open-per-rule. Duplicate-key path in `PostgresAlertInstanceRepository.save` is unchanged — the index is the deduper. UI: - New `AgentLifecycleForm.tsx` wizard form with multi-select chips for the six allowed event types + `withinSeconds` input. Wired into `ConditionStep`, `form-state` (validation + defaults: WENT_DEAD, 300 s), and `enums.ts` options. Tests in `enums.test.ts` pin the new option array. - `alert-variables.ts` registers `{{agent.app}}`, `{{event.type}}`, `{{event.timestamp}}`, `{{event.detail}}` leaves for the new kind, and extends `agent.id`'s availability list to include `AGENT_LIFECYCLE`. Tests (all passing): - 5 new JSON-roundtrip cases on `AlertConditionJsonTest` (positive + empty/zero/unknown-type rejection). - 5 new evaluator unit tests on `AgentLifecycleEvaluatorTest` (empty window, multi-agent fingerprint shape, scope forwarding, missing env). - `NotificationContextBuilderTest` switch now covers the new kind. - 119 alerting unit tests + 71 UI tests green. Docs: `.claude/rules/{core,app,ui}` and CLAUDE.md migration list updated.
This commit is contained in:
@@ -42,6 +42,16 @@ export const ALERT_VARIABLES: AlertVariable[] = [
|
||||
{ path: 'app.id', type: 'uuid', description: 'App UUID', sampleValue: '33333333-...',
|
||||
availableForKinds: ['ROUTE_METRIC', 'EXCHANGE_MATCH', 'AGENT_STATE', 'DEPLOYMENT_STATE', 'LOG_PATTERN', 'JVM_METRIC'], mayBeNull: true },
|
||||
|
||||
// AGENT_LIFECYCLE — agent + event subtree (distinct from AGENT_STATE's agent.* leaves)
|
||||
{ path: 'agent.app', type: 'string', description: 'Agent app slug', sampleValue: 'orders',
|
||||
availableForKinds: ['AGENT_LIFECYCLE'] },
|
||||
{ path: 'event.type', type: 'string', description: 'Lifecycle event type', sampleValue: 'WENT_DEAD',
|
||||
availableForKinds: ['AGENT_LIFECYCLE'] },
|
||||
{ path: 'event.timestamp', type: 'Instant', description: 'When the event happened', sampleValue: '2026-04-20T14:33:10Z',
|
||||
availableForKinds: ['AGENT_LIFECYCLE'] },
|
||||
{ path: 'event.detail', type: 'string', description: 'Free-text event detail', sampleValue: 'orders-0 STALE -> DEAD',
|
||||
availableForKinds: ['AGENT_LIFECYCLE'], mayBeNull: true },
|
||||
|
||||
// ROUTE_METRIC + EXCHANGE_MATCH share route.*
|
||||
{ path: 'route.id', type: 'string', description: 'Route ID', sampleValue: 'route-1',
|
||||
availableForKinds: ['ROUTE_METRIC', 'EXCHANGE_MATCH'] },
|
||||
@@ -56,7 +66,7 @@ export const ALERT_VARIABLES: AlertVariable[] = [
|
||||
|
||||
// AGENT_STATE + JVM_METRIC share agent.id/name; AGENT_STATE adds agent.state
|
||||
{ path: 'agent.id', type: 'string', description: 'Agent instance ID', sampleValue: 'prod-orders-0',
|
||||
availableForKinds: ['AGENT_STATE', 'JVM_METRIC'] },
|
||||
availableForKinds: ['AGENT_STATE', 'AGENT_LIFECYCLE', 'JVM_METRIC'] },
|
||||
{ path: 'agent.name', type: 'string', description: 'Agent display name', sampleValue: 'orders-0',
|
||||
availableForKinds: ['AGENT_STATE', 'JVM_METRIC'] },
|
||||
{ path: 'agent.state', type: 'string', description: 'Agent state', sampleValue: 'DEAD',
|
||||
|
||||
Reference in New Issue
Block a user