@dudousxd/nestjs-telescope
Version:
Laravel Telescope-style observability console for NestJS — core: watchers, recorder, correlation, SQLite store, headless API.
158 lines • 8.01 kB
TypeScript
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