@dudousxd/nestjs-telescope
Version:
Laravel Telescope-style observability console for NestJS — core: watchers, recorder, correlation, SQLite store, headless API.
99 lines • 5.24 kB
JavaScript
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
var __param = (this && this.__param) || function (paramIndex, decorator) {
return function (target, key) { decorator(target, key, paramIndex); }
};
// packages/core/src/pulse/pulse.service.ts
import { Inject, Injectable, Optional } from '@nestjs/common';
import { collectEntriesInWindow } from '../metrics/collect-window.js';
import { TELESCOPE_STORAGE } from '../nest/telescope.options.js';
import { aggregatePulse, finalizePulse } from './pulse-summary.js';
const DEFAULT_TOP_N = 5;
const DEFAULT_N_PLUS_ONE_THRESHOLD = 5;
const DEFAULT_SLOW_ROUTE_MIN_COUNT = 1;
/**
* Default p99 (ms) a route must reach to count as a slow-route hotspot. Matches
* the `slow` request-tag threshold (`SLOW_THRESHOLD_MS` in tagging/tagger.ts)
* and the HttpClientWatcher `slowMs` default, so "hotspot" lines up with the
* `slow` tag used everywhere else. See {@link PulseOptions.slowRouteMs}.
*/
const DEFAULT_SLOW_ROUTE_MS = 1000;
/** Computes a health snapshot by aggregating stored entries over a time window.
* Reads the store (source of truth) on demand — no live counters. */
let PulseService = class PulseService {
storage;
pageSize;
scanCap;
topN;
nPlusOneThreshold;
slowRouteMinCount;
slowRouteMs;
constructor(storage, options) {
this.storage = storage;
this.pageSize = options?.pageSize;
this.scanCap = options?.scanCap;
this.topN = options?.topN ?? DEFAULT_TOP_N;
this.nPlusOneThreshold = options?.nPlusOneThreshold ?? DEFAULT_N_PLUS_ONE_THRESHOLD;
this.slowRouteMinCount = options?.slowRouteMinCount ?? DEFAULT_SLOW_ROUTE_MIN_COUNT;
this.slowRouteMs = options?.slowRouteMs ?? DEFAULT_SLOW_ROUTE_MS;
}
async getHealth(windowMs) {
if (!Number.isFinite(windowMs) || windowMs <= 0) {
throw new RangeError(`windowMs must be a positive, finite number (got ${windowMs}).`);
}
const windowEnd = new Date();
const windowStart = new Date(windowEnd.getTime() - windowMs);
// Pass 1: scan the whole window over content-less columns only. This is the
// heavy loop (every entry in the window), so omitting the content blob is
// where the time is saved.
const { entries, scanned, truncated } = await collectEntriesInWindow(this.storage, { after: windowStart, omitContent: true }, {
...(this.pageSize !== undefined ? { pageSize: this.pageSize } : {}),
...(this.scanCap !== undefined ? { scanCap: this.scanCap } : {}),
});
const aggregates = aggregatePulse(entries, windowStart, windowEnd, {
topN: this.topN,
nPlusOneThreshold: this.nPlusOneThreshold,
slowRouteMinCount: this.slowRouteMinCount,
slowRouteMs: this.slowRouteMs,
});
// Pass 2: hydrate content for ONLY the handful of displayed rows — the top-N
// slowest, one representative per reported exception family, and one per
// reported N+1 family. Bounded by topN, so at most a few dozen reads.
const contentById = await this.hydrate(aggregates.hydrationIds);
const summary = finalizePulse(aggregates, (id) => contentById.has(id) ? contentById.get(id) : undefined);
return { ...summary, scanned, truncated };
}
/** Fetch content for the deduplicated set of ids the pulse output displays. */
async hydrate(ids) {
const uniqueIds = new Set([...ids.slowest, ...ids.exceptions, ...ids.nPlusOne]);
uniqueIds.delete('');
const contentById = new Map();
if (uniqueIds.size === 0)
return contentById;
// ONE batched query for every displayed id, instead of N per-id find() round-
// trips. Each find() hit the remote DB separately; this collapses them into a
// single fetch (e.g. `id IN (...)` / MGET) — the pulse latency win on a remote
// store. `limit` is sized to the id count so the page can hold all of them.
const idList = [...uniqueIds];
const page = await this.storage.get({ ids: idList, limit: idList.length });
for (const entry of page.data) {
contentById.set(entry.id, entry.content);
}
return contentById;
}
};
PulseService = __decorate([
Injectable(),
__param(0, Inject(TELESCOPE_STORAGE)),
__param(1, Optional()),
__metadata("design:paramtypes", [Object, Object])
], PulseService);
export { PulseService };
//# sourceMappingURL=pulse.service.js.map