@dudousxd/nestjs-telescope
Version:
Laravel Telescope-style observability console for NestJS — core: watchers, recorder, correlation, SQLite store, headless API.
173 lines • 7.72 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); }
};
var TelescopeOverloadGuard_1;
// packages/core/src/nest/telescope-overload-guard.service.ts
import * as perfHooks from 'node:perf_hooks';
import { Inject, Injectable, Logger, } from '@nestjs/common';
import { TELESCOPE_OPTIONS } from './telescope.options.js';
import { TelescopeService } from './telescope.service.js';
/** Default p99 event-loop lag (ms) that pauses capture when crossed. */
const DEFAULT_MAX_EVENT_LOOP_LAG_MS = 200;
/**
* Default startup grace (ms): how long after the guard arms it discards its
* measurement windows without pausing/logging. Bootstrap blocks the event loop
* synchronously (DI wiring, migrations, codegen) and that stall accumulates in
* the very first window — without a grace the guard would trip on a transient
* that has nothing to do with live load. ~5s covers a typical Nest bootstrap;
* during it there is little traffic to protect anyway.
*/
const DEFAULT_STARTUP_GRACE_MS = 5_000;
/** How often the guard samples the lag histogram. */
const SAMPLE_INTERVAL_MS = 1_000;
const NS_PER_MS = 1e6;
/** Structural guard: confirms a value exposes the monitor surface we read. */
function isEventLoopDelayMonitor(value) {
if (typeof value !== 'object' || value === null)
return false;
if (!('enable' in value) || !('disable' in value))
return false;
if (!('reset' in value) || !('percentile' in value))
return false;
return (typeof value.enable === 'function' &&
typeof value.disable === 'function' &&
typeof value.reset === 'function' &&
typeof value.percentile === 'function');
}
/** Resolve the configured threshold, or `null` when overload protection is off. */
function resolveMaxLagMs(option) {
// Default ON at 200ms. `false` disables; an object tunes the threshold.
if (option === false)
return null;
if (option === undefined || option === true)
return DEFAULT_MAX_EVENT_LOOP_LAG_MS;
return option.maxEventLoopLagMs ?? DEFAULT_MAX_EVENT_LOOP_LAG_MS;
}
/** Resolve the startup grace (ms), defaulting when unset. Never negative. */
function resolveStartupGraceMs(option) {
if (option === false)
return 0;
if (option === undefined || option === true)
return DEFAULT_STARTUP_GRACE_MS;
return Math.max(0, option.startupGraceMs ?? DEFAULT_STARTUP_GRACE_MS);
}
/**
* Overhead guard / overload protection. Samples the process event-loop delay
* histogram (`perf_hooks.monitorEventLoopDelay`) on an interval and PAUSES the
* Recorder when the p99 lag exceeds the configured threshold, resuming once it
* recovers — so a telescope under load can never amplify an incident.
*
* A startup grace (default ~5s) discards the first measurement windows so the
* synchronous bootstrap stall (DI wiring, migrations, codegen blocking the loop)
* never trips the guard on a transient — protection arms once the loop settles.
*
* Degrades to a no-op when `perf_hooks.monitorEventLoopDelay` is unavailable or
* when `overloadProtection: false`. The sampling interval is unref'd so it never
* keeps the host's event loop alive.
*/
let TelescopeOverloadGuard = TelescopeOverloadGuard_1 = class TelescopeOverloadGuard {
options;
service;
logger = new Logger(TelescopeOverloadGuard_1.name);
maxLagMs;
/**
* Number of leading sample windows still to discard for the startup grace.
* Derived from `startupGraceMs` over the sample interval; counts down on every
* sample taken while still warming up, after which the guard judges normally.
*/
warmupSamplesRemaining;
monitor = null;
timer = null;
constructor(options, service) {
this.options = options;
this.service = service;
this.maxLagMs = resolveMaxLagMs(this.options.overloadProtection);
// Round UP so a sub-interval grace still discards at least one window (the
// one that holds the bootstrap stall). 0ms ⇒ 0 windows ⇒ no grace.
this.warmupSamplesRemaining = Math.ceil(resolveStartupGraceMs(this.options.overloadProtection) / SAMPLE_INTERVAL_MS);
}
onModuleInit() {
if (this.maxLagMs === null)
return;
this.monitor = startMonitor();
if (this.monitor === null)
return;
this.timer = setInterval(() => this.sample(), SAMPLE_INTERVAL_MS);
this.timer.unref?.();
}
onApplicationShutdown() {
if (this.timer) {
clearInterval(this.timer);
this.timer = null;
}
this.monitor?.disable();
this.monitor = null;
}
/** Read the rolling p99 lag and pause/resume the Recorder around the threshold. */
sample() {
const monitor = this.monitor;
const maxLagMs = this.maxLagMs;
if (monitor === null || maxLagMs === null)
return;
let p99Ms;
try {
p99Ms = monitor.percentile(99) / NS_PER_MS;
}
catch {
return; // A misbehaving monitor must never crash the host.
}
// Reset the histogram each cycle so the decision reflects the RECENT window
// (a per-process accumulation would never recover once lag spiked).
monitor.reset();
// Startup grace: discard the leading window(s) — they carry the bootstrap
// event-loop stall (DI/migrations/codegen), not live load. Reset above still
// ran, so the NEXT judged window starts clean.
if (this.warmupSamplesRemaining > 0) {
this.warmupSamplesRemaining -= 1;
return;
}
if (p99Ms >= maxLagMs) {
if (!this.service.isPaused) {
this.logger.warn(`Event-loop p99 lag ${p99Ms.toFixed(0)}ms >= ${maxLagMs}ms — pausing Telescope capture.`);
this.service.pause();
}
}
else if (this.service.isPaused) {
this.logger.log(`Event-loop p99 lag ${p99Ms.toFixed(0)}ms recovered — resuming Telescope capture.`);
this.service.resume();
}
}
};
TelescopeOverloadGuard = TelescopeOverloadGuard_1 = __decorate([
Injectable(),
__param(0, Inject(TELESCOPE_OPTIONS)),
__param(1, Inject(TelescopeService)),
__metadata("design:paramtypes", [Object, TelescopeService])
], TelescopeOverloadGuard);
export { TelescopeOverloadGuard };
/** Start an enabled event-loop-delay monitor, or `null` if perf_hooks lacks it. */
function startMonitor() {
const factory = perfHooks.monitorEventLoopDelay;
if (typeof factory !== 'function')
return null;
try {
const monitor = factory();
if (!isEventLoopDelayMonitor(monitor))
return null;
monitor.enable();
return monitor;
}
catch {
return null;
}
}
//# sourceMappingURL=telescope-overload-guard.service.js.map