UNPKG

@dudousxd/nestjs-telescope

Version:

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

187 lines 7.56 kB
import { type Entry } from '../entry/entry.js'; export interface SlowEntry { id: string; type: string; durationMs: number; label: string; batchId: string; } export interface ExceptionGroup { familyHash: string; class: string; message: string; count: number; lastSeen: string; } export interface NPlusOneHotspot { familyHash: string; sql: string; /** Worst (max) repetition count of this family within a single request/batch. */ perRequest: number; /** Number of distinct requests/batches where this family tripped the threshold. */ requests: number; /** Sum of repetition counts across those requests. */ total: number; /** * Total duration (ms) spent across ALL occurrences of this family in the * window — the time "wasted" in the loop. Lets the dashboard weight an N+1 by * cost, not just repetition count (a 200×1ms loop vs a 6×80ms loop). Derived * from the content-less `durationMs` column, so it needs no hydration. */ totalDurationMs: number; /** One batch id (the worst) to deep-link to. */ sampleBatchId: string; } /** * A consistently-slow endpoint, aggregated by route family. The `route` IS the * normalized `familyHash` (e.g. "GET /api/base/:id/mel"), so it doubles as the * label — fully derived from content-less columns, no hydration. */ export interface SlowRouteHotspot { /** The normalized route family — equals the request entry's `familyHash`. */ route: string; count: number; p99: number; p50: number; } /** * A user's share of the load in the window — request count and the total time * spent serving them. The `user` is the id from the request's `user:<id>` tag. * Modelled on Laravel Pulse's "Usage" card. */ export interface UserLoad { user: string; count: number; totalDurationMs: number; } export interface PulseSummary { windowStart: string; windowEnd: string; windowMs: number; counts: Record<string, number>; slowest: SlowEntry[]; topExceptions: ExceptionGroup[]; nPlusOne: NPlusOneHotspot[]; slowRoutes: SlowRouteHotspot[]; slowOutgoing: SlowRouteHotspot[]; /** Slowest job families (by p99), the queue analogue of `slowRoutes`. */ slowJobs: SlowRouteHotspot[]; /** Top users by total request time in the window. */ loadByUser: UserLoad[]; } export interface PulseOptions { topN: number; nPlusOneThreshold: number; /** Minimum request count for a route to qualify as a slow-route hotspot. */ slowRouteMinCount: number; /** * Minimum p99 (ms) for a route family to count as a slow-route hotspot. A * route only surfaces here when its p99 is **>= slowRouteMs** — a hotspot is a * route that is *actually slow*, not merely the slowest of an otherwise-healthy * set. Without this gate, "Slow request hotspots" is a pure top-N p99 ranking, * so on a quiet host it surfaces e.g. `/health` at 18ms and reads as a false * alarm. The default (1000) matches the `slow` request tag threshold * (`SLOW_THRESHOLD_MS` in tagging/tagger.ts) and the HttpClientWatcher's * `slowMs` default, so "hotspot" means the same thing here as the `slow` tag * does everywhere else. Applies to both incoming slow-route and outgoing * slow-HTTP hotspots (both are p99 route rankings). */ slowRouteMs: number; } /** * The exact set of entry ids whose `content` the final pulse output displays. * Everything else aggregates over content-less columns, so a caller can run a * content-less primary scan and then hydrate only THESE ids: * - `slowest`: the top-N slowest entries (labels come from content). * - `exceptions`: one representative per reported exception family (class/message). * - `nPlusOne`: one representative query entry per reported N+1 family (sql). */ export interface PulseHydrationIds { slowest: string[]; exceptions: string[]; nPlusOne: string[]; } /** A content lookup for a previously-identified id; returns the hydrated content * or undefined when the entry could not be re-read (e.g. since pruned). */ export type HydrateContent = (id: string) => unknown; interface SlowCandidate { id: string; type: string; durationMs: number; batchId: string; } interface ExceptionAccumulator { /** A representative entry id to hydrate class/message from. */ representativeId: string; count: number; lastSeen: Date; } interface ExceptionGroupAggregate extends ExceptionAccumulator { familyHash: string; } interface NPlusOneAccumulator { familyHash: string; perRequest: number; requests: number; total: number; totalDurationMs: number; sampleBatchId: string; /** A representative query entry id to hydrate the sql from. */ representativeId: string; } /** * What `summarizePulse` derives from the content-less columns alone, BEFORE any * content hydration: counts, the ranked slowest candidates, exception groups * (without class/message), and N+1 hotspots (without sql). The pulse service * hydrates the ids in {@link hydrationIds} and calls {@link finalizePulse}. */ export interface PulseAggregates { windowStart: Date; windowEnd: Date; options: PulseOptions; counts: Record<string, number>; slowest: SlowCandidate[]; exceptions: ExceptionGroupAggregate[]; nPlusOne: NPlusOneAccumulator[]; /** * Slow-route hotspots, already final: the route IS the familyHash and the * stats come from content-less columns, so no hydration is required. */ slowRoutes: SlowRouteHotspot[]; /** * Slow outgoing-HTTP hotspots, already final: the `route` IS the http_client * familyHash (method + host + normalized path) and the stats come from * content-less columns, so no hydration is required. */ slowOutgoing: SlowRouteHotspot[]; /** Slowest job families, already final (familyHash is the label). */ slowJobs: SlowRouteHotspot[]; /** Top users by request time, already final (no hydration needed). */ loadByUser: UserLoad[]; hydrationIds: PulseHydrationIds; } /** * Pass 1: aggregate the windowed entries over their content-less columns only. * Reads `type`, `durationMs`, `familyHash`, `batchId`, `createdAt`, `sequence` * — never `content`. Produces the ranked/sliced aggregates plus the exact ids * whose content the final output needs. */ export declare function aggregatePulse(entries: Entry[], windowStart: Date, windowEnd: Date, options: PulseOptions): PulseAggregates; /** * Pass 2: build the final {@link PulseSummary}, reading content for ONLY the few * displayed rows via the `hydrate` lookup. `hydrate(id)` returns the entry's * content (or undefined if it could not be re-read). */ export declare function finalizePulse(aggregates: PulseAggregates, hydrate: HydrateContent): PulseSummary; /** * Summarize stored entries into a health snapshot: per-type counts, slowest * entries, top exceptions, and N+1 hotspots aggregated by query family. Pure: * callers fetch the windowed entries (createdAt is not re-checked here). * * When the entries carry their `content` (the in-process / single-pass path), * labels/class/message/sql resolve directly from each entry. The two-pass * content-less path uses {@link aggregatePulse} + {@link finalizePulse} instead. */ export declare function summarizePulse(entries: Entry[], windowStart: Date, windowEnd: Date, options: PulseOptions): PulseSummary; export {}; //# sourceMappingURL=pulse-summary.d.ts.map