UNPKG

@dudousxd/nestjs-telescope

Version:

Laravel Telescope-style observability console for NestJS — core: watchers, recorder, correlation, SQLite store, headless API.

268 lines 12.5 kB
import type { AlertChannel } from './alert-channel.js'; /** * A single alerting rule. Each rule is evaluated on every tick of the alerter, * EXCEPT `new-exception` which evaluates per-flush over just-stored exception * entries (so a brand-new error family pages you within a flush interval, not a * full evaluation interval). * * - `exception-rate` — fires when `>= threshold` exception entries were * recorded in the trailing `window`. * - `slow-request-rate` — fires when `>= count` request entries slower than * `thresholdMs` were recorded in the trailing `window`. * - `dropped-entries` — fires when the Recorder's cumulative `droppedCount` * grew by `>= threshold` since the previous evaluation * (a delta, not an absolute — so a once-busy host that * dropped a burst at boot doesn't keep re-firing). * - `new-exception` — fires the FIRST time an exception's `familyHash` is * seen within `window` (a genuinely NEW error family). * Dedup uses the shared {@link StorageProvider} when it * implements `markFamilySeen` (so a family pages ONCE * across a multi-replica deployment), falling back to an * in-memory per-replica seen-map otherwise. * - `every-exception` — fires for EVERY exception (server + browser-reported * `client_exception`), not just brand-new families — * parity with a "notify on every error" setup. Still * rate-limited by the shared `cooldown` PER FAMILY, so a * hot loop of the same error re-pages once per cooldown * rather than on every occurrence; set `cooldown: '0s'` * for a truly uncollapsed stream. The optional `window` * is used only to count occurrences shown on the alert * (default `'1h'`); it does NOT gate firing. * - `metric-threshold` — fires when a COMPUTED metric over the trailing * `window` crosses `threshold` in the `comparator` * direction. Unlike the rate rules (which count events), * this evaluates the SAME aggregates the dashboard shows * — e.g. request p95 latency, or cache hit-rate — so you * can page on "p95 > 800ms" or "hit-rate < 0.8". */ export type AlertRule = { type: 'exception-rate'; window: string; threshold: number; } | { type: 'slow-request-rate'; window: string; thresholdMs: number; count: number; } | { type: 'dropped-entries'; threshold: number; } | { type: 'new-exception'; window: string; } | { type: 'every-exception'; window?: string; } | { type: 'metric-threshold'; /** The computed metric to evaluate (see {@link AlertMetric}). */ metric: AlertMetric; /** Trailing window to aggregate over (e.g. `'5m'`). */ window: string; /** `gte` fires when value >= threshold; `lte` when value <= threshold. */ comparator: 'gte' | 'lte'; /** The threshold the metric is compared against. */ threshold: number; /** * Minimum samples in the window before the rule can fire. Guards against a * single slow request tripping a p95 page on a quiet host. Default 1. */ minSamples?: number; }; /** * The metrics a `metric-threshold` rule can evaluate. Each is derived from the * SAME windowed aggregation the stats/pulse views use, so an alert means exactly * what the dashboard shows. Latency metrics are in ms; `cache-hit-rate` is a * ratio in [0, 1]. */ export type AlertMetric = 'request-p95-ms' | 'request-p99-ms' | 'query-p95-ms' | 'query-p99-ms' | 'cache-hit-rate'; /** * A coarse geo location resolved from a client IP, attached to an exception alert * when a {@link AlertsOptions.geoLookup} hook is configured. Every field is * optional — a partial result (e.g. country only) still renders. Deliberately * dependency-light: the LIB ships no geo database or HTTP client; the host owns * the lookup (and its caching/rate-limiting) and returns this shape. */ export interface AlertGeoLocation { city?: string; region?: string; country?: string; /** ISO 3166-1 alpha-2 (e.g. `US`), used to render a flag emoji. */ countryCode?: string; } /** * Host-supplied resolver from a client IP to a coarse {@link AlertGeoLocation}. * Called ONLY when an exception alert actually fires and carries a `clientIp`, so * the common no-fire path pays nothing. May be sync or async; returning `null` * (or throwing — swallowed) simply omits the Location field. The lib never caches * or rate-limits it — do that in the hook if the provider needs it. */ export type GeoLookup = (ip: string) => AlertGeoLocation | null | Promise<AlertGeoLocation | null>; /** * Pluggable alerting (v2). When set, {@link TelescopeAlerter} evaluates `rules` * and fans each fired alert out to EVERY configured channel concurrently. A * configured `alerts` with no destination (neither `channels` nor the legacy * `webhookUrl`) or empty `rules` is a fail-closed boot error. */ export interface AlertsOptions { /** * Optional IP→geo resolver. When set, a firing exception alert that carries a * `clientIp` is enriched with a coarse {@link AlertGeoLocation} (rendered as a * "Location" field by channels that support it). Kept out of the lib core so * telescope ships no geo dependency — see {@link GeoLookup}. */ geoLookup?: GeoLookup; /** * Delivery destinations. Each fired alert is sent to every channel * concurrently; one channel failing never blocks the others. Use the factory * helpers: `slackChannel(url)`, `webhookChannel(url)`, `customChannel(fn)`. * Optional ONLY for backward compatibility with `webhookUrl` — supply at least * one of `channels` / `webhookUrl`. */ channels?: AlertChannel[]; /** * Legacy single raw-JSON webhook (v1). Still accepted: internally rewritten * into a `webhookChannel(webhookUrl)` and appended to `channels`. Prefer * `channels` for new configs. */ webhookUrl?: string; /** * The host's EXTERNAL Telescope dashboard URL (e.g. * `https://telescope.example.com/telescope/`). When set, channels that support * deep links (Slack) build a link straight to the offending entry, e.g. * `${dashboardUrl}#/entries/exception/${id}`. Optional; without it the Slack * message simply omits the "Open in Telescope" button. */ dashboardUrl?: string; /** * Evaluation cadence as a duration string (e.g. `'1m'`). Default `'1m'`. * Resolved to ms via `durationToMs`. (Interval rules only; `new-exception` * evaluates per-flush.) */ every?: string; /** Re-notify suppression per rule, as a duration string. Default `'15m'`. */ cooldown?: string; /** Rules to evaluate. Must be non-empty when `alerts` is set. */ rules: AlertRule[]; } /** Boot-resolved, validated alerting config (durations normalized to ms). */ export interface ResolvedAlerts { /** All destinations, with any legacy `webhookUrl` already folded in. */ channels: AlertChannel[]; /** External dashboard URL for deep links, or `null` when unset. */ dashboardUrl: string | null; intervalMs: number; cooldownMs: number; rules: AlertRule[]; /** Host IP→geo resolver, or `null` when unconfigured. */ geoLookup: GeoLookup | null; } /** * Rich exception context attached to a `new-exception` alert. Pulled from the * exception entry AND its sibling request entry in the SAME batch (queried by * `batchId` only when the rule actually fires, so the common no-fire path stays * a single in-memory map lookup). Absent on rate-rule alerts. */ export interface ExceptionAlertContext { /** Stable family hash that was first-seen this window. */ familyHash: string; /** Exception class name (e.g. `TypeError`). */ class: string; /** Exception message. */ message: string; /** Truncated stack (first frames), or `null`. */ stack: string | null; /** * Where the error happened: the sibling request's route/uri for a server * exception, or the browser page `url` for a `client_exception`. `null` when * neither is available. */ route: string | null; /** Request method, or `null` (always `null` for a client_exception). */ method: string | null; /** * User-agent string. For a `client_exception` it's the reporting browser's UA; * for a server exception it's the sibling request's `user-agent` header (when * captured). `null` when unavailable. */ userAgent: string | null; /** * `Referer` header of the originating request (server exception) — the page a * user came from. `null` when absent or for a `client_exception` (the browser * report carries its own page `url` in `route`, not a referer). */ referer: string | null; /** * React component stack from an error boundary — present ONLY for a * `client_exception` that supplied one. `null` otherwise. */ componentStack: string | null; /** * Host-defined free-form debugging bag from a `client_exception` (`extra`), or * `null`. Redacted/bounded at record time like any other content. */ extra: Record<string, unknown> | null; /** True when this alert is a browser-reported `client_exception`. */ client: boolean; /** * Originating client IP, or `null` when unknown. For a `client_exception` it's * the browser IP the ingestion controller filled in server-side (`clientIp`); * for a server exception it's the sibling request entry's `ip` (`request.ip` / * the first `x-forwarded-for` hop). Never sourced from an untrusted body. */ clientIp: string | null; /** * Coarse geo location resolved from {@link clientIp}, or `null`. Populated only * when an {@link AlertsOptions.geoLookup} hook is configured AND the alert has a * `clientIp`; otherwise `null`. */ geo: AlertGeoLocation | null; /** Response status code, or `null`. */ statusCode: number | null; /** Request duration (ms), or `null`. */ durationMs: number | null; /** Authenticated user id from a `user:<id>` tag, or `null`. */ user: string | null; /** Times this family was seen in the window (>= 1; 1 on first-occurrence). */ occurrences: number; /** * True when this is the family's first occurrence in the window (`occurrences * === 1`) — a brand-new error rather than a recurrence. Lets channels badge a * new error distinctly from a recurring one. */ isNew: boolean; /** Exception entry id (for the dashboard deep link). */ entryId: string; /** Batch id that ties the exception to its request entry. */ batchId: string; } /** * Shape delivered to channels when a rule fires. BACKWARD COMPATIBLE with the v1 * raw-webhook JSON: every v1 field (`rule`, `value`, `threshold`, `firedAt`, * `instanceId`) is unchanged; new fields are purely additive and optional. */ export interface AlertPayload { rule: AlertRule; /** The measured value that crossed the threshold. */ value: number; /** The rule's threshold (`threshold`/`count`, or `1` for `new-exception`). */ threshold: number; /** ISO-8601 fire time. */ firedAt: string; /** The reporting instance (`config.instanceId`). */ instanceId: string; /** Rich context for `new-exception` alerts; absent for rate rules. */ exception?: ExceptionAlertContext; /** External dashboard URL when configured (lets channels build deep links). */ dashboardUrl?: string; /** * AI-generated probable-cause markdown for a `new-exception` alert, attached * when `ai` is configured in `'auto'` mode AND the diagnosis finished within * the alert's short grace window. Absent when AI is off, the diagnosis was * still running at dispatch time, or it failed — the alert always fires * regardless, AI is purely additive. Channels that render it (Slack) append a * "Probable cause (AI)" section. */ diagnosis?: string; } //# sourceMappingURL=alert-rule.d.ts.map