150 lines
6.8 KiB
TypeScript
150 lines
6.8 KiB
TypeScript
|
|
/**
|
||
|
|
* Alerting option lists and enum types used by the rule editor.
|
||
|
|
*
|
||
|
|
* These are **string-literal unions mirrored by hand from the backend Java
|
||
|
|
* enums**. Why not derived from `schema.d.ts`? Springdoc emits polymorphic
|
||
|
|
* `@JsonSubTypes` conditions with two conflicting `kind` discriminators (the
|
||
|
|
* class-name literal + the Jackson enum), whose intersection is `never` —
|
||
|
|
* indexed access like `RouteMetricCondition['comparator']` resolves to
|
||
|
|
* `never` and silently breaks `Record<T, string>` exhaustiveness. Until the
|
||
|
|
* OpenAPI shape is fixed upstream, we declare the unions here.
|
||
|
|
*
|
||
|
|
* How to keep this file honest:
|
||
|
|
* 1. Regenerate the schema (`npm run generate-api:live` or `generate-api`).
|
||
|
|
* 2. Search `src/api/schema.d.ts` for the enum unions on the relevant
|
||
|
|
* condition type (e.g. `comparator?: "GT" | "GTE" | …`).
|
||
|
|
* 3. Update the matching union below.
|
||
|
|
* 4. `enums.test.ts` dumps every exported `*_OPTIONS` array as an inline
|
||
|
|
* snapshot — the diff in a PR shows exactly which values / labels moved.
|
||
|
|
*
|
||
|
|
* Fields whose backend type is `String` (agent state, log level, deployment
|
||
|
|
* states, exchange filter status, JVM metric names) aren't in here at all —
|
||
|
|
* springdoc emits them as open-ended strings. Follow-up: add
|
||
|
|
* `@Schema(allowableValues = …)` on the Java record components, then migrate
|
||
|
|
* those string literals into this file too.
|
||
|
|
*
|
||
|
|
* Condition-kind-wide enums (ConditionKind, Severity, TargetKind) are
|
||
|
|
* derived from `schema.d.ts`, because they appear on flat fields that
|
||
|
|
* springdoc types correctly.
|
||
|
|
*/
|
||
|
|
import type { components } from '../../api/schema';
|
||
|
|
|
||
|
|
type AlertRuleRequest = components['schemas']['AlertRuleRequest'];
|
||
|
|
type AlertRuleTarget = components['schemas']['AlertRuleTarget'];
|
||
|
|
|
||
|
|
// Derived — these schema fields are correctly typed (flat, no polymorphism).
|
||
|
|
export type ConditionKind = NonNullable<AlertRuleRequest['conditionKind']>;
|
||
|
|
export type Severity = NonNullable<AlertRuleRequest['severity']>;
|
||
|
|
export type TargetKind = NonNullable<AlertRuleTarget['kind']>;
|
||
|
|
|
||
|
|
// Manual — schema's polymorphic condition types resolve to `never`.
|
||
|
|
// Mirrors: cameleer-server-core RouteMetric, Comparator, AggregationOp enums
|
||
|
|
// and ExchangeMatchCondition.fireMode.
|
||
|
|
export type RouteMetric = 'ERROR_RATE' | 'AVG_DURATION_MS' | 'P99_LATENCY_MS' | 'THROUGHPUT' | 'ERROR_COUNT';
|
||
|
|
export type Comparator = 'GT' | 'GTE' | 'LT' | 'LTE' | 'EQ';
|
||
|
|
export type JvmAggregation = 'MAX' | 'MIN' | 'AVG' | 'LATEST';
|
||
|
|
export type ExchangeFireMode = 'PER_EXCHANGE' | 'COUNT_IN_WINDOW';
|
||
|
|
|
||
|
|
export interface Option<T extends string> { value: T; label: string }
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Build a dropdown option array from a `Record<T, string>` label map.
|
||
|
|
*
|
||
|
|
* Declaration order is load-bearing: ES2015+ guarantees `Object.keys` yields
|
||
|
|
* string-keyed entries in insertion order, which is what the user sees.
|
||
|
|
*
|
||
|
|
* Hidden values stay in the `Record<T, string>` map (so `Record`-exhaustiveness
|
||
|
|
* still pins labels to the union) and in the type `T` (so rules carrying a
|
||
|
|
* hidden value round-trip fine through save/load) — they're simply filtered
|
||
|
|
* out of the visible option array.
|
||
|
|
*/
|
||
|
|
function toOptions<T extends string>(labels: Record<T, string>, hidden?: readonly T[]): Option<T>[] {
|
||
|
|
const skip: ReadonlySet<T> = new Set(hidden ?? []);
|
||
|
|
return (Object.keys(labels) as T[])
|
||
|
|
.filter((value) => !skip.has(value))
|
||
|
|
.map((value) => ({ value, label: labels[value] }));
|
||
|
|
}
|
||
|
|
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
// Label maps — declaration order = dropdown order.
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
|
||
|
|
const CONDITION_KIND_LABELS: Record<ConditionKind, string> = {
|
||
|
|
ROUTE_METRIC: 'Route metric (error rate, latency, throughput)',
|
||
|
|
EXCHANGE_MATCH: 'Exchange match (specific failures)',
|
||
|
|
AGENT_STATE: 'Agent state (DEAD / STALE)',
|
||
|
|
DEPLOYMENT_STATE: 'Deployment state (FAILED / DEGRADED)',
|
||
|
|
LOG_PATTERN: 'Log pattern (count of matching logs)',
|
||
|
|
JVM_METRIC: 'JVM metric (heap, GC, inflight)',
|
||
|
|
};
|
||
|
|
|
||
|
|
const SEVERITY_LABELS: Record<Severity, string> = {
|
||
|
|
CRITICAL: 'Critical',
|
||
|
|
WARNING: 'Warning',
|
||
|
|
INFO: 'Info',
|
||
|
|
};
|
||
|
|
|
||
|
|
const ROUTE_METRIC_LABELS: Record<RouteMetric, string> = {
|
||
|
|
ERROR_RATE: 'Error rate',
|
||
|
|
P99_LATENCY_MS: 'P99 latency (ms)',
|
||
|
|
AVG_DURATION_MS: 'Avg duration (ms)',
|
||
|
|
THROUGHPUT: 'Throughput (msg/s)',
|
||
|
|
ERROR_COUNT: 'Error count',
|
||
|
|
};
|
||
|
|
|
||
|
|
const COMPARATOR_LABELS: Record<Comparator, string> = {
|
||
|
|
GT: '>',
|
||
|
|
GTE: '\u2265',
|
||
|
|
LT: '<',
|
||
|
|
LTE: '\u2264',
|
||
|
|
EQ: '=',
|
||
|
|
};
|
||
|
|
|
||
|
|
// Previous UI ordering (MAX, AVG, MIN) preserved; LATEST is declared last
|
||
|
|
// and hidden — see comment on JVM_AGGREGATION_HIDDEN below.
|
||
|
|
const JVM_AGGREGATION_LABELS: Record<JvmAggregation, string> = {
|
||
|
|
MAX: 'MAX',
|
||
|
|
AVG: 'AVG',
|
||
|
|
MIN: 'MIN',
|
||
|
|
LATEST: 'LATEST',
|
||
|
|
};
|
||
|
|
|
||
|
|
const EXCHANGE_FIRE_MODE_LABELS: Record<ExchangeFireMode, string> = {
|
||
|
|
PER_EXCHANGE: 'One alert per matching exchange',
|
||
|
|
COUNT_IN_WINDOW: 'Threshold: N matches in window',
|
||
|
|
};
|
||
|
|
|
||
|
|
const TARGET_KIND_LABELS: Record<TargetKind, string> = {
|
||
|
|
USER: 'User',
|
||
|
|
GROUP: 'Group',
|
||
|
|
ROLE: 'Role',
|
||
|
|
};
|
||
|
|
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
// Hidden values — legal on the wire (saved/loaded rules round-trip fine) but
|
||
|
|
// intentionally not surfaced in dropdowns. Document *why* — silent omission
|
||
|
|
// is exactly what caused the original drift problem.
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
|
||
|
|
const COMPARATOR_HIDDEN: readonly Comparator[] = [
|
||
|
|
'EQ', // Exact equality on floating-point metrics is rarely useful and noisy
|
||
|
|
// — users should pick GT/LT with a sensible threshold instead.
|
||
|
|
];
|
||
|
|
|
||
|
|
const JVM_AGGREGATION_HIDDEN: readonly JvmAggregation[] = [
|
||
|
|
'LATEST', // Point-in-time reads belong on a metric dashboard, not an alert
|
||
|
|
// rule — a windowed MAX/MIN/AVG is what you want for alerting.
|
||
|
|
];
|
||
|
|
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
// Exported option arrays (visible in dropdowns).
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
|
||
|
|
export const CONDITION_KIND_OPTIONS: Option<ConditionKind>[] = toOptions(CONDITION_KIND_LABELS);
|
||
|
|
export const SEVERITY_OPTIONS: Option<Severity>[] = toOptions(SEVERITY_LABELS);
|
||
|
|
export const ROUTE_METRIC_OPTIONS: Option<RouteMetric>[] = toOptions(ROUTE_METRIC_LABELS);
|
||
|
|
export const COMPARATOR_OPTIONS: Option<Comparator>[] = toOptions(COMPARATOR_LABELS, COMPARATOR_HIDDEN);
|
||
|
|
export const JVM_AGGREGATION_OPTIONS: Option<JvmAggregation>[] = toOptions(JVM_AGGREGATION_LABELS, JVM_AGGREGATION_HIDDEN);
|
||
|
|
export const EXCHANGE_FIRE_MODE_OPTIONS: Option<ExchangeFireMode>[] = toOptions(EXCHANGE_FIRE_MODE_LABELS);
|
||
|
|
export const TARGET_KIND_OPTIONS: Option<TargetKind>[] = toOptions(TARGET_KIND_LABELS);
|