@dudousxd/nestjs-telescope
Version:
Laravel Telescope-style observability console for NestJS — core: watchers, recorder, correlation, SQLite store, headless API.
140 lines • 5.91 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/metrics/server-stats.service.ts
import * as perfHooks from 'node:perf_hooks';
import { Inject, Injectable, Optional } from '@nestjs/common';
import { TELESCOPE_CONFIG } from '../nest/telescope.options.js';
const DEFAULT_MAX_SAMPLES = 120;
const BYTES_PER_MB = 1024 * 1024;
const NS_PER_MS = 1e6;
const US_PER_MS = 1e3;
function toMb(bytes) {
return Math.round((bytes / BYTES_PER_MB) * 100) / 100;
}
/**
* Point-in-time snapshot of the Node process health (memory, CPU, uptime,
* event-loop delay). The event-loop histogram is started once at construction
* and read per request so its mean reflects the whole process lifetime. When
* `perf_hooks.monitorEventLoopDelay` is unavailable, the delay degrades to
* `null` rather than throwing — every other field is always present.
*/
let ServerStatsService = class ServerStatsService {
config;
loopMonitor;
maxSamples;
/** CPU/mem history ring buffer (oldest first). */
samples = [];
/** Previous (cpuUsage, wall ms) for deriving the per-interval cpuPercent. */
lastCpu = null;
lastSampleMs = 0;
constructor(config, options) {
this.config = config;
this.loopMonitor = startEventLoopMonitor();
this.maxSamples = Math.max(1, Math.floor(options?.maxSamples ?? DEFAULT_MAX_SAMPLES));
}
onApplicationShutdown() {
this.loopMonitor?.disable();
}
getStats() {
const memory = process.memoryUsage();
const cpu = process.cpuUsage();
const eventLoopDelayMs = this.loopMonitor === null ? null : roundMs(this.loopMonitor.mean / NS_PER_MS);
this.recordSample(memory, cpu, eventLoopDelayMs);
return {
uptimeSec: Math.round(process.uptime() * 100) / 100,
memory: {
rssMb: toMb(memory.rss),
heapUsedMb: toMb(memory.heapUsed),
heapTotalMb: toMb(memory.heapTotal),
},
cpu: {
userMs: roundMs(cpu.user / US_PER_MS),
systemMs: roundMs(cpu.system / US_PER_MS),
},
eventLoopDelayMs,
instanceId: this.config.instanceId,
};
}
/** The CPU/mem history captured so far (oldest first), for the dashboard's
* CPU/mem-history card. Cheap copy of the ring buffer. */
getHistory() {
return { samples: [...this.samples] };
}
/**
* Append one history sample, deriving cpuPercent from the CPU-time delta since
* the previous sample over the wall-clock interval (percent of ONE core). The
* first sample has no prior interval, so its cpuPercent is 0. Evicts the oldest
* once the ring buffer exceeds its cap.
*/
recordSample(memory, cpu, eventLoopDelayMs) {
const nowMs = Date.now();
let cpuPercent = 0;
if (this.lastCpu !== null && this.lastSampleMs > 0) {
const cpuDeltaUs = cpu.user - this.lastCpu.user + (cpu.system - this.lastCpu.system);
const wallDeltaMs = nowMs - this.lastSampleMs;
if (wallDeltaMs > 0) {
// cpuDeltaUs is microseconds of CPU; wallDeltaMs*1000 is the interval in
// microseconds. The ratio is the fraction of one core, ×100 for percent.
cpuPercent = Math.max(0, Math.round((cpuDeltaUs / (wallDeltaMs * US_PER_MS)) * 10000) / 100);
}
}
this.lastCpu = cpu;
this.lastSampleMs = nowMs;
this.samples.push({
atMs: nowMs,
rssMb: toMb(memory.rss),
heapUsedMb: toMb(memory.heapUsed),
cpuPercent,
eventLoopDelayMs,
});
while (this.samples.length > this.maxSamples)
this.samples.shift();
}
};
ServerStatsService = __decorate([
Injectable(),
__param(0, Inject(TELESCOPE_CONFIG)),
__param(1, Optional()),
__metadata("design:paramtypes", [Object, Object])
], ServerStatsService);
export { ServerStatsService };
function roundMs(value) {
return Number.isFinite(value) ? Math.round(value * 1000) / 1000 : 0;
}
/** Start an enabled event-loop-delay monitor, or `null` if perf_hooks lacks it. */
function startEventLoopMonitor() {
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;
}
}
/** 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) || !('mean' in value))
return false;
return (typeof value.enable === 'function' &&
typeof value.disable === 'function' &&
typeof value.mean === 'number');
}
//# sourceMappingURL=server-stats.service.js.map