UNPKG

@dudousxd/nestjs-telescope

Version:

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

173 lines 7.72 kB
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