feat: add Workbench observability rule summaries
This commit is contained in:
@@ -87,6 +87,8 @@ export interface HwlabRuntimeObservabilitySpec {
|
||||
readonly prometheusOperator: boolean;
|
||||
readonly metricsEndpoint?: HwlabRuntimeObservabilityMetricsEndpointSpec;
|
||||
readonly workbench?: HwlabRuntimeObservabilityWorkbenchSpec;
|
||||
readonly recordingRules: readonly HwlabRuntimeObservabilityRecordingRuleSpec[];
|
||||
readonly warningAlerts: readonly HwlabRuntimeObservabilityWarningAlertSpec[];
|
||||
}
|
||||
|
||||
export interface HwlabRuntimeObservabilityMetricsEndpointSpec {
|
||||
@@ -102,12 +104,34 @@ export interface HwlabRuntimeObservabilityMetricsEndpointSpec {
|
||||
export interface HwlabRuntimeObservabilityWorkbenchSpec {
|
||||
readonly enabled: boolean;
|
||||
readonly summaryPath: string;
|
||||
readonly lowSampleThreshold: number;
|
||||
readonly metricPrefixes: readonly string[];
|
||||
readonly requiredSeries: readonly string[];
|
||||
readonly backendLabelDenylist: readonly string[];
|
||||
readonly maxUnknownEventLines: number;
|
||||
}
|
||||
|
||||
export interface HwlabRuntimeObservabilityRecordingRuleSpec {
|
||||
readonly id: string;
|
||||
readonly metric: string;
|
||||
readonly sourceMetric: string;
|
||||
readonly quantile: number;
|
||||
readonly window: string;
|
||||
readonly minSamples: number;
|
||||
readonly groupBy: readonly string[];
|
||||
readonly matchLabels: Record<string, string>;
|
||||
}
|
||||
|
||||
export interface HwlabRuntimeObservabilityWarningAlertSpec {
|
||||
readonly id: string;
|
||||
readonly ruleId: string;
|
||||
readonly severity: "warning";
|
||||
readonly thresholdSeconds: number;
|
||||
readonly minSamples: number;
|
||||
readonly for: string;
|
||||
readonly matchLabels: Record<string, string>;
|
||||
}
|
||||
|
||||
export interface HwlabRuntimeImageRewriteSpec {
|
||||
readonly source: string;
|
||||
readonly target: string;
|
||||
@@ -295,6 +319,12 @@ function nonNegativeIntegerField(obj: Record<string, unknown>, key: string, path
|
||||
return value;
|
||||
}
|
||||
|
||||
function positiveNumberField(obj: Record<string, unknown>, key: string, path: string): number {
|
||||
const value = obj[key];
|
||||
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) throw new Error(`${path}.${key} must be a positive number`);
|
||||
return value;
|
||||
}
|
||||
|
||||
function sortedRecordEntries(value: unknown, path: string): Array<[string, Record<string, unknown>]> {
|
||||
return Object.entries(asRecord(value, path)).map(([key, item]) => [key, asRecord(item, `${path}.${key}`)]);
|
||||
}
|
||||
@@ -584,10 +614,18 @@ function publicExposureConfig(value: unknown, path: string): HwlabRuntimePublicE
|
||||
|
||||
function observabilityConfig(value: unknown, path: string): HwlabRuntimeObservabilitySpec {
|
||||
const raw = asRecord(value, path);
|
||||
const recordingRules = observabilityRecordingRulesConfig(raw.recordingRules, `${path}.recordingRules`);
|
||||
const warningAlerts = observabilityWarningAlertsConfig(raw.warningAlerts, `${path}.warningAlerts`);
|
||||
const recordingRuleIds = new Set(recordingRules.map((rule) => rule.id));
|
||||
for (const alert of warningAlerts) {
|
||||
if (!recordingRuleIds.has(alert.ruleId)) throw new Error(`${path}.warningAlerts.${alert.id}.ruleId must reference a recordingRules id`);
|
||||
}
|
||||
return {
|
||||
prometheusOperator: booleanField(raw, "prometheusOperator", path),
|
||||
metricsEndpoint: observabilityMetricsEndpointConfig(raw.metricsEndpoint, `${path}.metricsEndpoint`),
|
||||
workbench: observabilityWorkbenchConfig(raw.workbench, `${path}.workbench`),
|
||||
recordingRules,
|
||||
warningAlerts,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -617,6 +655,7 @@ function observabilityWorkbenchConfig(value: unknown, path: string): HwlabRuntim
|
||||
return {
|
||||
enabled: booleanField(raw, "enabled", path),
|
||||
summaryPath: stringField(raw, "summaryPath", path),
|
||||
lowSampleThreshold: numberField(raw, "lowSampleThreshold", path),
|
||||
metricPrefixes: stringArrayField(raw, "metricPrefixes", path),
|
||||
requiredSeries: stringArrayField(raw, "requiredSeries", path),
|
||||
backendLabelDenylist: stringArrayField(raw, "backendLabelDenylist", path),
|
||||
@@ -624,6 +663,47 @@ function observabilityWorkbenchConfig(value: unknown, path: string): HwlabRuntim
|
||||
};
|
||||
}
|
||||
|
||||
function observabilityRecordingRulesConfig(value: unknown, path: string): HwlabRuntimeObservabilityRecordingRuleSpec[] {
|
||||
if (value === undefined) return [];
|
||||
if (!Array.isArray(value)) throw new Error(`${path} must be an array`);
|
||||
return value.map((item, index) => {
|
||||
const itemPath = `${path}[${index}]`;
|
||||
const raw = asRecord(item, itemPath);
|
||||
const quantile = positiveNumberField(raw, "quantile", itemPath);
|
||||
if (quantile >= 1) throw new Error(`${itemPath}.quantile must be less than 1`);
|
||||
return {
|
||||
id: stringField(raw, "id", itemPath),
|
||||
metric: stringField(raw, "metric", itemPath),
|
||||
sourceMetric: stringField(raw, "sourceMetric", itemPath),
|
||||
quantile,
|
||||
window: stringField(raw, "window", itemPath),
|
||||
minSamples: numberField(raw, "minSamples", itemPath),
|
||||
groupBy: stringArrayField(raw, "groupBy", itemPath),
|
||||
matchLabels: optionalStringRecord(raw.matchLabels, `${itemPath}.matchLabels`),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function observabilityWarningAlertsConfig(value: unknown, path: string): HwlabRuntimeObservabilityWarningAlertSpec[] {
|
||||
if (value === undefined) return [];
|
||||
if (!Array.isArray(value)) throw new Error(`${path} must be an array`);
|
||||
return value.map((item, index) => {
|
||||
const itemPath = `${path}[${index}]`;
|
||||
const raw = asRecord(item, itemPath);
|
||||
const severity = stringField(raw, "severity", itemPath);
|
||||
if (severity !== "warning") throw new Error(`${itemPath}.severity must be warning`);
|
||||
return {
|
||||
id: stringField(raw, "id", itemPath),
|
||||
ruleId: stringField(raw, "ruleId", itemPath),
|
||||
severity,
|
||||
thresholdSeconds: positiveNumberField(raw, "thresholdSeconds", itemPath),
|
||||
minSamples: numberField(raw, "minSamples", itemPath),
|
||||
for: stringField(raw, "for", itemPath),
|
||||
matchLabels: optionalStringRecord(raw.matchLabels, `${itemPath}.matchLabels`),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function runtimeImageRewritesConfig(value: unknown, path: string): HwlabRuntimeImageRewriteSpec[] {
|
||||
if (value === undefined) return [];
|
||||
if (!Array.isArray(value)) throw new Error(`${path} must be an array`);
|
||||
|
||||
Reference in New Issue
Block a user