UNPKG

@dudousxd/nestjs-telescope

Version:

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

158 lines 8.01 kB
import { Logger } from '@nestjs/common'; import { type Entry } from '../entry/entry.js'; import type { StorageProvider } from '../storage/storage-provider.js'; import type { ResolvedAlerts } from './alert-rule.js'; export interface TelescopeAlerterDeps { alerts: ResolvedAlerts; storage: StorageProvider; instanceId: string; /** Cumulative Recorder drop count reader (delta-based `dropped-entries` rule). */ droppedCount: () => number; /** Wall-clock seam (ms). Defaults to `Date.now`. */ now?: () => number; /** Cap on tracked error families for the `new-exception` rule (test seam). */ maxFamilies?: number; logger?: Logger; /** * Optional AI-diagnosis lookup for `new-exception` alerts. When set (auto-mode * AI configured), the alerter briefly awaits it for the firing family and, if a * diagnosis is ready within the grace cap, attaches it to the payload as * `diagnosis`. Returning `null` (or being unset) simply means no AI note — the * alert fires either way. Never blocks the alert beyond the hook's own cap. */ diagnosisFor?: (familyHash: string) => Promise<string | null>; } /** * Pluggable-channel alerting (v2). Two evaluation paths: * 1. Interval rules (`exception-rate` / `slow-request-rate` / `dropped-entries`) * run on an unref'd timer over windowed counts. * 2. The `new-exception` rule runs per-flush via {@link evaluateFlush} over the * just-stored exception entries, so a brand-new error family pages within a * flush interval rather than waiting a full evaluation interval. * * Every fired alert fans out to ALL configured channels concurrently; one channel * failing never blocks the others. NEVER throws into the host: a channel failure * is warn-logged (rate-limited per channel) and otherwise swallowed. */ export declare class TelescopeAlerter { private readonly deps; private readonly logger; private readonly now; private timer; /** Per-rule last-fired wall time (index-keyed; rules are a fixed array). */ private readonly lastFiredAt; /** * Per-family last-fired wall time for the flush-driven exception rules. Keyed * by `${ruleType}|${familyHash}` so a `new-exception` and an `every-exception` * rule configured together keep independent cooldown clocks. */ private readonly lastFiredFamily; /** Channels we've already warned about (rate-limit failure logs by name). */ private readonly warnedChannels; /** Previous cumulative droppedCount, for the `dropped-entries` delta. */ private lastDroppedCount; /** Bounded per-replica seen-family map backing the `new-exception` rule. */ private readonly newExceptionTracker; /** Pre-computed `new-exception` rule (if any) so the flush path is cheap. */ private readonly newExceptionRule; /** Pre-computed `every-exception` rule (if any) so the flush path is cheap. */ private readonly everyExceptionRule; constructor(deps: TelescopeAlerterDeps); /** Start the unref'd evaluation interval. Idempotent. */ start(): void; /** Stop the interval (shutdown). Idempotent. */ stop(): void; /** * Evaluate every INTERVAL rule once (the `new-exception` rule is handled by * {@link evaluateFlush}, not here). Each firing rule fans out independently. * Exposed for the timer and for deterministic tests. */ evaluate(): Promise<void>; /** * Per-flush evaluation for the exception rules (`new-exception` / * `every-exception`). Called with the entries a flush just stored. MUST be * cheap: it filters to exception-type entries and does a single bounded-map * lookup per family; the expensive batch-context fetch happens ONLY for a * family that actually fires (and is past cooldown). No-op (zero cost beyond the * early return) when neither rule is configured. Never throws into the host — * failures are swallowed/logged. */ evaluateFlush(storedEntries: Entry[]): Promise<void>; /** * "Is this a NEW error family for the window?" — using the SHARED store dedup * when the provider implements `markFamilySeen` (so a family pages once across a * multi-replica deployment), and the in-memory per-replica tracker otherwise. * A shared-store failure falls back to the local tracker rather than dropping * the alert, so a transient store hiccup degrades to once-per-pod, never to * silent. */ private observeFamily; /** Returns `{ value, threshold }` when an interval rule is firing, else `null`. */ private measure; /** Count entries of `type` recorded in the trailing `window`. */ private countInWindow; /** Count request entries in the window whose `durationMs >= thresholdMs`. */ private countSlowRequests; /** * Compute a `metric-threshold` rule's metric over its window and return * `{ value, threshold }` when it crosses in the comparator direction, else * `null`. Latency metrics read `durationMs` only (content-less); the cache * hit-rate needs the cache content, so it reads WITH content for that metric. */ private measureMetric; /** Aggregate a metric over the window, or `null` when there are no samples. */ private computeMetric; /** * Small windowed read reusing the analytics scan: `omitContent` (alerting only * needs counts + `durationMs`, never payloads) and a tight `scanCap`. */ private readWindow; private inCooldown; /** Composite cooldown key so each flush rule keeps an independent per-family clock. */ private cooldownKey; private familyInCooldown; /** Build the v1-compatible payload for an interval (rate) rule. */ private buildRatePayload; /** * Build the rich exception payload shared by `new-exception` and * `every-exception`. Pulls the exception's own fields, then fetches its batch to * find the sibling REQUEST entry for route/method/status/duration/user context, * counts how many times this family appears in the trailing window, and (when a * `geoLookup` hook is configured) resolves the client IP to a coarse location. * This is the ONLY expensive path and runs only on a real fire. */ private buildExceptionPayload; /** * Resolve a client IP to a coarse {@link AlertGeoLocation} via the host's * `geoLookup` hook. Returns `null` when no hook is configured, no IP is present, * the hook returns `null`, or the hook throws (swallowed — geo is purely * additive and must never break or block an alert beyond the hook's own cost). */ private resolveGeo; /** * Build the server-exception context: the exception's own fields plus its * sibling REQUEST entry (route/method/status/duration/user) from the same batch. */ private buildServerContext; /** * Build the client-exception context from the browser-reported content. There's * no sibling request (the error came straight from the browser), so `route` is * the page `url`, `method`/`statusCode`/`durationMs` are null, and `userAgent` * carries the reporting browser. The user comes from the entry's own * `user:<id>` tag (the controller tagged it at record time). */ private buildClientContext; /** Find the sibling REQUEST entry in the exception's batch (or `null`). */ private findSiblingRequest; /** Count entries of this family AND type in the trailing window (>= 1). */ private countFamilyInWindow; /** * Fan the payload out to every channel concurrently. Each channel's failure is * isolated (`Promise.allSettled`) and warn-logged ONCE per channel name so a * persistently-down destination doesn't flood the logs. Never rejects. */ private dispatch; /** Warn once per channel name; subsequent failures for that channel are silent. */ private warnChannelFailure; } //# sourceMappingURL=telescope-alerter.d.ts.map