66 lines
3.7 KiB
TypeScript
66 lines
3.7 KiB
TypeScript
// SPEC: PJ2026-01060505 Workbench Performance draft-2026-06-17-p0.
|
|
// Responsibility: Prometheus/observability expression rendering for HWLAB node runtime.
|
|
import type { HwlabRuntimeObservabilityRecordingRuleSpec, HwlabRuntimeObservabilitySpec, HwlabRuntimeObservabilityWarningAlertSpec } from "./hwlab-node-lanes";
|
|
|
|
export function nodeObservabilityRecordingRuleSummaries(observability: HwlabRuntimeObservabilitySpec): Array<Record<string, unknown>> {
|
|
return observability.recordingRules.map((rule) => ({
|
|
...rule,
|
|
expression: nodeObservabilityRecordingRuleExpression(rule),
|
|
sampleCountExpression: nodeObservabilitySampleCountExpression(rule, rule.matchLabels),
|
|
lowSampleGuardExpression: `${nodeObservabilitySampleCountExpression(rule, rule.matchLabels)} >= ${rule.minSamples}`,
|
|
}));
|
|
}
|
|
|
|
export function nodeObservabilityWarningAlertSummaries(observability: HwlabRuntimeObservabilitySpec): Array<Record<string, unknown>> {
|
|
const recordingRules = new Map(observability.recordingRules.map((rule) => [rule.id, rule]));
|
|
return observability.warningAlerts.map((alert) => {
|
|
const rule = recordingRules.get(alert.ruleId);
|
|
return {
|
|
...alert,
|
|
expression: rule === undefined ? null : nodeObservabilityWarningAlertExpression(rule, alert),
|
|
recordingRule: rule === undefined ? null : rule.metric,
|
|
lowSampleGuardExpression: rule === undefined ? null : `${nodeObservabilitySampleCountExpression(rule, { ...rule.matchLabels, ...alert.matchLabels })} >= ${alert.minSamples}`,
|
|
};
|
|
});
|
|
}
|
|
|
|
export function nodeObservabilityRecordingRuleExpression(rule: HwlabRuntimeObservabilityRecordingRuleSpec): string {
|
|
const bucketGroupBy = uniqueStrings([...rule.groupBy, "le"]);
|
|
return `histogram_quantile(${formatPrometheusNumber(rule.quantile)}, sum by (${bucketGroupBy.join(", ")}) (rate(${prometheusMetricSelector(`${rule.sourceMetric}_bucket`, rule.matchLabels)}[${rule.window}])))`;
|
|
}
|
|
|
|
function nodeObservabilitySampleCountExpression(rule: HwlabRuntimeObservabilityRecordingRuleSpec, matchLabels: Record<string, string>): string {
|
|
const groupBy = uniqueStrings([...rule.groupBy]);
|
|
return `sum by (${groupBy.join(", ")}) (increase(${prometheusMetricSelector(`${rule.sourceMetric}_count`, matchLabels)}[${rule.window}]))`;
|
|
}
|
|
|
|
export function nodeObservabilityWarningAlertExpression(rule: HwlabRuntimeObservabilityRecordingRuleSpec, alert: HwlabRuntimeObservabilityWarningAlertSpec): string {
|
|
const sampleLabels = { ...rule.matchLabels, ...alert.matchLabels };
|
|
const sampleGuard = `${nodeObservabilitySampleCountExpression(rule, sampleLabels)} >= ${alert.minSamples}`;
|
|
const vectorMatch = rule.groupBy.length === 0 ? "" : ` on (${uniqueStrings([...rule.groupBy]).join(", ")})`;
|
|
return `(${prometheusMetricSelector(rule.metric, alert.matchLabels)} > ${formatPrometheusNumber(alert.thresholdSeconds)}) and${vectorMatch} (${sampleGuard})`;
|
|
}
|
|
|
|
function prometheusMetricSelector(metric: string, labels: Record<string, string>): string {
|
|
const selector = prometheusLabelSelector(labels);
|
|
return selector.length === 0 ? metric : `${metric}${selector}`;
|
|
}
|
|
|
|
function prometheusLabelSelector(labels: Record<string, string>): string {
|
|
const entries = Object.entries(labels);
|
|
if (entries.length === 0) return "";
|
|
return `{${entries.map(([key, value]) => `${key}${value.includes("|") ? "=~" : "="}"${escapePrometheusLabelValue(value)}"`).join(",")}}`;
|
|
}
|
|
|
|
function escapePrometheusLabelValue(value: string): string {
|
|
return value.replace(/\\/gu, "\\\\").replace(/"/gu, "\\\"");
|
|
}
|
|
|
|
function formatPrometheusNumber(value: number): string {
|
|
return Number.isInteger(value) ? String(value) : String(value);
|
|
}
|
|
|
|
function uniqueStrings(values: readonly string[]): string[] {
|
|
return [...new Set(values)];
|
|
}
|