From 18cacb33ee3f10b48a606a9aa2d4d0383f2cbda9 Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Mon, 20 Apr 2026 18:04:17 +0200 Subject: [PATCH] docs(alerting): align @JsonTypeInfo spec with shipped code MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Design spec and Plan 02 described AlertCondition polymorphism as Id.DEDUCTION, but the code that shipped in PR #140 uses Id.NAME with property="kind" and include=EXISTING_PROPERTY. The `kind` field is real on every subtype and the DB stores it in a separate column (condition_kind), so reading the discriminator directly is simpler than deduction — update the docs to match. Also add `"kind"` to the example JSON payloads so they match on-wire reality. OutboundAuth (Plan 01) correctly still uses Id.DEDUCTION and is unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../plans/2026-04-19-alerting-02-backend.md | 21 +++++------ .../specs/2026-04-19-alerting-design.md | 36 ++++++++++--------- 2 files changed, 31 insertions(+), 26 deletions(-) diff --git a/docs/superpowers/plans/2026-04-19-alerting-02-backend.md b/docs/superpowers/plans/2026-04-19-alerting-02-backend.md index 1a36efe2..656a0bb9 100644 --- a/docs/superpowers/plans/2026-04-19-alerting-02-backend.md +++ b/docs/superpowers/plans/2026-04-19-alerting-02-backend.md @@ -32,7 +32,7 @@ mvn clean compile # confirm Plan 01 code compiles as baseline |---|---| | `AlertingProperties.java` | Not here — see app module. | | `AlertRule.java` | Immutable record: id, environmentId, name, description, severity, enabled, conditionKind, condition, evaluationIntervalSeconds, forDurationSeconds, reNotifyMinutes, notificationTitleTmpl, notificationMessageTmpl, webhooks, targets, nextEvaluationAt, claimedBy, claimedUntil, evalState, audit fields. | -| `AlertCondition.java` | Sealed interface; Jackson DEDUCTION polymorphism root. | +| `AlertCondition.java` | Sealed interface; Jackson `kind`-based polymorphism root (Id.NAME + EXISTING_PROPERTY). | | `RouteMetricCondition.java` | Record: scope, metric, comparator, threshold, windowSeconds. | | `ExchangeMatchCondition.java` | Record: scope, filter, fireMode, threshold, windowSeconds, perExchangeLingerSeconds. | | `AgentStateCondition.java` | Record: scope, state, forSeconds. | @@ -126,7 +126,7 @@ mvn clean compile # confirm Plan 01 code compiles as baseline - **One commit per task.** Commit messages: `feat(alerting): …`, `test(alerting): …`, `fix(alerting): …`, `chore(alerting): …`, `docs(alerting): …`. - **Tenant invariant.** Every ClickHouse query and Postgres table referencing observability data filters by `tenantId` (injected via `AlertingBeanConfig` from `cameleer.server.tenant.id`). - **No `FINAL`** on the two new CH count methods — alerting tolerates brief duplicate counts. -- **Jackson polymorphism** via `@JsonTypeInfo(use = DEDUCTION)` with `@JsonSubTypes` on `AlertCondition`. +- **Jackson polymorphism** via `@JsonTypeInfo(use = Id.NAME, property = "kind", include = EXISTING_PROPERTY)` with `@JsonSubTypes` on `AlertCondition`. - **Pure `core/`, Spring-only in `app/`.** No `@Component`, `@Service`, or `@Scheduled` annotations in `cameleer-server-core`. - **Claim polling.** `FOR UPDATE SKIP LOCKED` + `claimed_by` / `claimed_until` with 30 s TTL. - **Instance id** for claim ownership: use `InetAddress.getLocalHost().getHostName() + ":" + processPid()`; exposed as a bean `"alertingInstanceId"` of type `String`. @@ -403,7 +403,7 @@ git commit -m "feat(alerting): add ALERT_RULE_CHANGE + ALERT_SILENCE_CHANGE audi ## Phase 2 — Core domain model -Each task in this phase adds a small, focused set of pure-Java records and enums under `cameleer-server-core/src/main/java/com/cameleer/server/core/alerting/`. All records use canonical constructors with explicit `@NotNull`-style defensive copying only for mutable collections (`List.copyOf`, `Map.copyOf`). Jackson polymorphism is handled by `@JsonTypeInfo(use = DEDUCTION)` on `AlertCondition`. +Each task in this phase adds a small, focused set of pure-Java records and enums under `cameleer-server-core/src/main/java/com/cameleer/server/core/alerting/`. All records use canonical constructors with explicit `@NotNull`-style defensive copying only for mutable collections (`List.copyOf`, `Map.copyOf`). Jackson polymorphism is handled by `@JsonTypeInfo(use = Id.NAME, property = "kind", include = EXISTING_PROPERTY)` on `AlertCondition` — the subtype is read from the existing `kind` field each record exposes. ### Task 3: Enums + `AlertScope` @@ -606,14 +606,15 @@ package com.cameleer.server.core.alerting; import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonTypeInfo; -@JsonTypeInfo(use = JsonTypeInfo.Id.DEDUCTION) +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "kind", + include = JsonTypeInfo.As.EXISTING_PROPERTY, visible = true) @JsonSubTypes({ - @JsonSubTypes.Type(RouteMetricCondition.class), - @JsonSubTypes.Type(ExchangeMatchCondition.class), - @JsonSubTypes.Type(AgentStateCondition.class), - @JsonSubTypes.Type(DeploymentStateCondition.class), - @JsonSubTypes.Type(LogPatternCondition.class), - @JsonSubTypes.Type(JvmMetricCondition.class) + @JsonSubTypes.Type(value = RouteMetricCondition.class, name = "ROUTE_METRIC"), + @JsonSubTypes.Type(value = ExchangeMatchCondition.class, name = "EXCHANGE_MATCH"), + @JsonSubTypes.Type(value = AgentStateCondition.class, name = "AGENT_STATE"), + @JsonSubTypes.Type(value = DeploymentStateCondition.class, name = "DEPLOYMENT_STATE"), + @JsonSubTypes.Type(value = LogPatternCondition.class, name = "LOG_PATTERN"), + @JsonSubTypes.Type(value = JvmMetricCondition.class, name = "JVM_METRIC") }) public sealed interface AlertCondition permits RouteMetricCondition, ExchangeMatchCondition, AgentStateCondition, diff --git a/docs/superpowers/specs/2026-04-19-alerting-design.md b/docs/superpowers/specs/2026-04-19-alerting-design.md index 8cab2837..a92994e8 100644 --- a/docs/superpowers/specs/2026-04-19-alerting-design.md +++ b/docs/superpowers/specs/2026-04-19-alerting-design.md @@ -286,7 +286,7 @@ CREATE TABLE alert_rules ( enabled boolean NOT NULL DEFAULT true, condition_kind condition_kind_enum NOT NULL, - condition jsonb NOT NULL, -- sealed-subtype payload, Jackson-DEDUCTION polymorphic + condition jsonb NOT NULL, -- sealed-subtype payload, Jackson polymorphic on `kind` evaluation_interval_seconds int NOT NULL DEFAULT 60 CHECK (evaluation_interval_seconds >= 5), for_duration_seconds int NOT NULL DEFAULT 0 CHECK (for_duration_seconds >= 0), @@ -423,14 +423,15 @@ outbound_connections (delete) — blocked by FK from rules.webhooks JSONB ### Jackson polymorphism for conditions ```java -@JsonTypeInfo(use = JsonTypeInfo.Id.DEDUCTION) +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "kind", + include = JsonTypeInfo.As.EXISTING_PROPERTY, visible = true) @JsonSubTypes({ - @Type(RouteMetricCondition.class), - @Type(ExchangeMatchCondition.class), - @Type(AgentStateCondition.class), - @Type(DeploymentStateCondition.class), - @Type(LogPatternCondition.class), - @Type(JvmMetricCondition.class), + @Type(value = RouteMetricCondition.class, name = "ROUTE_METRIC"), + @Type(value = ExchangeMatchCondition.class, name = "EXCHANGE_MATCH"), + @Type(value = AgentStateCondition.class, name = "AGENT_STATE"), + @Type(value = DeploymentStateCondition.class, name = "DEPLOYMENT_STATE"), + @Type(value = LogPatternCondition.class, name = "LOG_PATTERN"), + @Type(value = JvmMetricCondition.class, name = "JVM_METRIC"), }) public sealed interface AlertCondition permits RouteMetricCondition, ExchangeMatchCondition, AgentStateCondition, @@ -439,37 +440,40 @@ public sealed interface AlertCondition permits } ``` -Jackson deduces the subtype from the set of present fields. Bean Validation (`@Valid`) on each record validates at the controller boundary. +Each payload carries its own `kind` field, which Jackson reads (`EXISTING_PROPERTY`) to pick the subtype and the record still exposes as `ConditionKind kind()`. Bean Validation (`@Valid`) on each record validates at the controller boundary. Example condition payloads: ```json // ROUTE_METRIC -{ "scope": {"appSlug":"orders","routeId":"route-1"}, +{ "kind": "ROUTE_METRIC", + "scope": {"appSlug":"orders","routeId":"route-1"}, "metric": "P99_LATENCY_MS", "comparator": "GT", "threshold": 2000, "windowSeconds": 300 } // EXCHANGE_MATCH — PER_EXCHANGE -{ "scope": {"appSlug":"orders"}, +{ "kind": "EXCHANGE_MATCH", + "scope": {"appSlug":"orders"}, "filter": {"status":"FAILED","attributes":{"type":"payment"}}, "fireMode": "PER_EXCHANGE", "perExchangeLingerSeconds": 300 } // EXCHANGE_MATCH — COUNT_IN_WINDOW -{ "scope": {"appSlug":"orders"}, +{ "kind": "EXCHANGE_MATCH", + "scope": {"appSlug":"orders"}, "filter": {"status":"FAILED"}, "fireMode": "COUNT_IN_WINDOW", "threshold": 5, "windowSeconds": 900 } // AGENT_STATE -{ "scope": {"appSlug":"orders"}, "state": "DEAD", "forSeconds": 60 } +{ "kind": "AGENT_STATE", "scope": {"appSlug":"orders"}, "state": "DEAD", "forSeconds": 60 } // DEPLOYMENT_STATE -{ "scope": {"appSlug":"orders"}, "states": ["FAILED","DEGRADED"] } +{ "kind": "DEPLOYMENT_STATE", "scope": {"appSlug":"orders"}, "states": ["FAILED","DEGRADED"] } // LOG_PATTERN -{ "scope": {"appSlug":"orders"}, "level": "ERROR", +{ "kind": "LOG_PATTERN", "scope": {"appSlug":"orders"}, "level": "ERROR", "pattern": "TimeoutException", "threshold": 5, "windowSeconds": 900 } // JVM_METRIC -{ "scope": {"appSlug":"orders"}, "metric": "heap_used_percent", +{ "kind": "JVM_METRIC", "scope": {"appSlug":"orders"}, "metric": "heap_used_percent", "aggregation": "MAX", "comparator": "GT", "threshold": 90, "windowSeconds": 300 } ```