@dudousxd/nestjs-telescope
Version:
Laravel Telescope-style observability console for NestJS — core: watchers, recorder, correlation, SQLite store, headless API.
62 lines • 2.83 kB
JavaScript
import { durationToMs } from '../config/parse-duration.js';
// packages/core/src/alerts/resolve-alerts.ts
import { webhookChannel } from './alert-channel.js';
const DEFAULT_INTERVAL = '1m';
const DEFAULT_COOLDOWN = '15m';
/**
* Validate + normalize `alerts`. Returns `null` when unconfigured (feature off).
*
* Fails CLOSED at boot:
* - a configured `alerts` with NO destination — neither `channels` nor the
* legacy `webhookUrl` — throws (silently dropping every alert is worse than
* refusing to boot);
* - empty `rules` throws;
* - an unparseable `every`/`cooldown`/rule-`window` duration throws (a typo
* surfaces at boot rather than silently never firing).
*
* The legacy `webhookUrl` is folded into the channel list as a
* {@link webhookChannel}, APPENDED after any explicit `channels`, so the v1
* raw-JSON behavior is preserved exactly while new configs use `channels`.
*/
export function resolveAlerts(alerts) {
if (alerts === undefined) {
return null;
}
if (!Array.isArray(alerts.rules) || alerts.rules.length === 0) {
throw new Error('Telescope alerts: `rules` is required and must be non-empty.');
}
const channels = [...(alerts.channels ?? [])];
// Legacy compat: a top-level `webhookUrl` becomes a raw-JSON webhook channel.
if (typeof alerts.webhookUrl === 'string' && alerts.webhookUrl.trim() !== '') {
channels.push(webhookChannel(alerts.webhookUrl));
}
if (channels.length === 0) {
throw new Error('Telescope alerts: at least one destination is required — set `channels` ' +
'(e.g. slackChannel/webhookChannel/customChannel) or the legacy `webhookUrl`.');
}
for (const rule of alerts.rules) {
if (rule.type === 'exception-rate' ||
rule.type === 'slow-request-rate' ||
rule.type === 'new-exception' ||
rule.type === 'metric-threshold') {
// Throws on an unparseable window — fail closed at boot.
durationToMs(rule.window);
}
// `every-exception`'s window is optional (occurrence-count only); validate it
// when present so a typo fails at boot rather than silently mis-counting.
if (rule.type === 'every-exception' && rule.window !== undefined) {
durationToMs(rule.window);
}
}
return {
channels,
dashboardUrl: typeof alerts.dashboardUrl === 'string' && alerts.dashboardUrl.trim() !== ''
? alerts.dashboardUrl
: null,
intervalMs: durationToMs(alerts.every ?? DEFAULT_INTERVAL),
cooldownMs: durationToMs(alerts.cooldown ?? DEFAULT_COOLDOWN),
rules: alerts.rules,
geoLookup: typeof alerts.geoLookup === 'function' ? alerts.geoLookup : null,
};
}
//# sourceMappingURL=resolve-alerts.js.map