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