@lakutata/monitor 3.0.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/LICENSE +23 -0
  2. package/README.md +114 -0
  3. package/dist/cjs/exports/Monitor.d.ts +9 -0
  4. package/dist/cjs/exports/Monitor.js +28 -0
  5. package/dist/cjs/monitor/CpuMonitor.d.ts +60 -0
  6. package/dist/cjs/monitor/CpuMonitor.js +96 -0
  7. package/dist/cjs/monitor/EventLoopMonitor.d.ts +65 -0
  8. package/dist/cjs/monitor/EventLoopMonitor.js +92 -0
  9. package/dist/cjs/monitor/HttpRequestMonitor.d.ts +61 -0
  10. package/dist/cjs/monitor/HttpRequestMonitor.js +92 -0
  11. package/dist/cjs/monitor/MemoryMonitor.d.ts +61 -0
  12. package/dist/cjs/monitor/MemoryMonitor.js +152 -0
  13. package/dist/cjs/monitor/interfaces/ICpuMonitorStatistics.d.ts +40 -0
  14. package/dist/cjs/monitor/interfaces/ICpuMonitorStatistics.js +2 -0
  15. package/dist/cjs/monitor/interfaces/IEventLoopMonitorStatistics.d.ts +45 -0
  16. package/dist/cjs/monitor/interfaces/IEventLoopMonitorStatistics.js +2 -0
  17. package/dist/cjs/monitor/interfaces/IHttpRequestMonitorStatistics.d.ts +43 -0
  18. package/dist/cjs/monitor/interfaces/IHttpRequestMonitorStatistics.js +2 -0
  19. package/dist/cjs/monitor/interfaces/IMemoryMonitorStatistics.d.ts +124 -0
  20. package/dist/cjs/monitor/interfaces/IMemoryMonitorStatistics.js +2 -0
  21. package/dist/cjs/monitor/interfaces/IMonitor.d.ts +10 -0
  22. package/dist/cjs/monitor/interfaces/IMonitor.js +2 -0
  23. package/dist/cjs/monitor/lib/Samples.d.ts +57 -0
  24. package/dist/cjs/monitor/lib/Samples.js +119 -0
  25. package/dist/cjs/package.json +1 -0
  26. package/dist/esm/exports/Monitor.js +9 -0
  27. package/dist/esm/monitor/CpuMonitor.js +92 -0
  28. package/dist/esm/monitor/EventLoopMonitor.js +88 -0
  29. package/dist/esm/monitor/HttpRequestMonitor.js +88 -0
  30. package/dist/esm/monitor/MemoryMonitor.js +115 -0
  31. package/dist/esm/monitor/interfaces/ICpuMonitorStatistics.js +1 -0
  32. package/dist/esm/monitor/interfaces/IEventLoopMonitorStatistics.js +1 -0
  33. package/dist/esm/monitor/interfaces/IHttpRequestMonitorStatistics.js +1 -0
  34. package/dist/esm/monitor/interfaces/IMemoryMonitorStatistics.js +1 -0
  35. package/dist/esm/monitor/interfaces/IMonitor.js +1 -0
  36. package/dist/esm/monitor/lib/Samples.js +114 -0
  37. package/dist/types/exports/Monitor.d.ts +9 -0
  38. package/dist/types/monitor/CpuMonitor.d.ts +60 -0
  39. package/dist/types/monitor/EventLoopMonitor.d.ts +65 -0
  40. package/dist/types/monitor/HttpRequestMonitor.d.ts +61 -0
  41. package/dist/types/monitor/MemoryMonitor.d.ts +61 -0
  42. package/dist/types/monitor/interfaces/ICpuMonitorStatistics.d.ts +40 -0
  43. package/dist/types/monitor/interfaces/IEventLoopMonitorStatistics.d.ts +45 -0
  44. package/dist/types/monitor/interfaces/IHttpRequestMonitorStatistics.d.ts +43 -0
  45. package/dist/types/monitor/interfaces/IMemoryMonitorStatistics.d.ts +124 -0
  46. package/dist/types/monitor/interfaces/IMonitor.d.ts +10 -0
  47. package/dist/types/monitor/lib/Samples.d.ts +57 -0
  48. package/package.json +44 -0
@@ -0,0 +1,9 @@
1
+ export * from '../monitor/CpuMonitor.js';
2
+ export * from '../monitor/EventLoopMonitor.js';
3
+ export * from '../monitor/HttpRequestMonitor.js';
4
+ export * from '../monitor/MemoryMonitor.js';
5
+ export * from '../monitor/interfaces/IMonitor.js';
6
+ export * from '../monitor/interfaces/ICpuMonitorStatistics.js';
7
+ export * from '../monitor/interfaces/IEventLoopMonitorStatistics.js';
8
+ export * from '../monitor/interfaces/IHttpRequestMonitorStatistics.js';
9
+ export * from '../monitor/interfaces/IMemoryMonitorStatistics.js';
@@ -0,0 +1,92 @@
1
+ import { Component } from '@lakutata/core';
2
+ import { round, Samples } from './lib/Samples.js';
3
+ /**
4
+ * The CPU monitor: measures the CPU time the process uses (user plus system, all its threads), as a percentage of one
5
+ * core: 100 is one core fully busy, and a process using several cores (worker threads, the libuv thread pool) goes
6
+ * above 100. It takes a sample every second, each the average usage over that second, and keeps the statistics of
7
+ * every sample since it was created or last {@link CpuMonitor.reset}: the latest, the minimum, the maximum, the mean
8
+ * and percentiles, in constant memory.
9
+ *
10
+ * Declare it in the `components` of the application (`{class: CpuMonitor}`) and get it with `@Inject` or
11
+ * `getObject()`; it is a singleton. It starts sampling when it is created: list it in `bootstrap` to measure from the
12
+ * launch. The statistics are all 0 until the first sample, one second after its creation. Its timer does not keep the
13
+ * process alive, and stops when the component is destroyed.
14
+ * @example
15
+ * ```typescript
16
+ * import {Application} from 'lakutata'
17
+ * import {CpuMonitor, type ICpuMonitorStatistics} from 'lakutata/com/monitor'
18
+ *
19
+ * Application
20
+ * .run(() => ({
21
+ * id: 'cpu.app',
22
+ * name: 'Cpu',
23
+ * components: {
24
+ * cpu: {class: CpuMonitor}
25
+ * },
26
+ * //Created at the launch, so that it samples from the start
27
+ * bootstrap: ['cpu']
28
+ * }))
29
+ * .onLaunched(async (app: Application): Promise<void> => {
30
+ * const cpu: CpuMonitor = await app.getObject('cpu')
31
+ * setInterval((): void => {
32
+ * const statistics: ICpuMonitorStatistics = cpu.statistics
33
+ * console.log(`CPU ${statistics.usage}% (p95 ${statistics.usageP95}%, max ${statistics.usageMax}%)`)
34
+ * }, 60000)
35
+ * })
36
+ * ```
37
+ */
38
+ export class CpuMonitor extends Component {
39
+ #fractionDigits = 5;
40
+ #intervalDelay = 1000;
41
+ #samples = new Samples(1e5);
42
+ #sampleInterval;
43
+ #previousUsage = process.cpuUsage();
44
+ #previousTime = process.hrtime.bigint();
45
+ /**
46
+ * The statistics of the CPU samples since the monitor was created or reset, computed at each read: the latest
47
+ * usage and the minimum, maximum, mean and percentiles (P50 to P99) of the samples, in percent of one core, rounded
48
+ * to 5 decimals. The percentiles are approximate (3 significant digits). All 0 before the first sample.
49
+ */
50
+ get statistics() {
51
+ return {
52
+ usage: round(this.#samples.latest, this.#fractionDigits),
53
+ usageMin: round(this.#samples.min, this.#fractionDigits),
54
+ usageMax: round(this.#samples.max, this.#fractionDigits),
55
+ usageAvg: round(this.#samples.mean, this.#fractionDigits),
56
+ usageP50: round(this.#samples.percentile(50), this.#fractionDigits),
57
+ usageP90: round(this.#samples.percentile(90), this.#fractionDigits),
58
+ usageP95: round(this.#samples.percentile(95), this.#fractionDigits),
59
+ usageP99: round(this.#samples.percentile(99), this.#fractionDigits)
60
+ };
61
+ }
62
+ /**
63
+ * Take a sample: the CPU usage since the previous sample (user plus system time over the elapsed time, in percent of
64
+ * one core). Called every second.
65
+ * @protected
66
+ */
67
+ sampleCpuUsage() {
68
+ const now = process.hrtime.bigint();
69
+ const usage = process.cpuUsage();
70
+ const cpuMicroseconds = (usage.user - this.#previousUsage.user) + (usage.system - this.#previousUsage.system);
71
+ const elapsedMicroseconds = Number(now - this.#previousTime) / 1000;
72
+ this.#previousUsage = usage;
73
+ this.#previousTime = now;
74
+ if (elapsedMicroseconds > 0)
75
+ this.#samples.record(cpuMicroseconds / elapsedMicroseconds * 100);
76
+ }
77
+ async init() {
78
+ this.#previousUsage = process.cpuUsage();
79
+ this.#previousTime = process.hrtime.bigint();
80
+ this.#sampleInterval = setInterval(() => this.sampleCpuUsage(), this.#intervalDelay).unref();
81
+ }
82
+ async destroy() {
83
+ clearInterval(this.#sampleInterval);
84
+ }
85
+ /**
86
+ * Forget the samples: the statistics are 0 until the next sample, then cover the samples taken since. Use it to
87
+ * measure a period, such as between two reports.
88
+ */
89
+ reset() {
90
+ this.#samples.reset();
91
+ }
92
+ }
@@ -0,0 +1,88 @@
1
+ import { monitorEventLoopDelay, performance } from 'node:perf_hooks';
2
+ import { Component } from '@lakutata/core';
3
+ /**
4
+ * The event loop monitor: measures how late the event loop of the main thread runs its timers (the event loop delay,
5
+ * in milliseconds, with Node's `monitorEventLoopDelay` at a 1 ms resolution) and how busy it is (its utilization, in
6
+ * percent of the time). A growing delay means synchronous work (CPU-bound code, synchronous I/O, large JSON) blocks the
7
+ * other requests; a utilization near 100 means the loop has no idle time left.
8
+ *
9
+ * Both cover the time since the monitor was created or last {@link EventLoopMonitor.reset}: the delays are a
10
+ * histogram of every measure, the utilization is the share of that whole time the loop was busy. Declare it in the
11
+ * `components` of the application (`{class: EventLoopMonitor}`) and get it with `@Inject` or `getObject()`; it is a
12
+ * singleton. It starts measuring when it is created: list it in `bootstrap` to measure from the launch; it stops when
13
+ * the component is destroyed.
14
+ * @example
15
+ * ```typescript
16
+ * import {Application} from 'lakutata'
17
+ * import {EventLoopMonitor, type IEventLoopMonitorStatistics} from 'lakutata/com/monitor'
18
+ *
19
+ * Application
20
+ * .run(() => ({
21
+ * id: 'loop.app',
22
+ * name: 'Loop',
23
+ * components: {
24
+ * eventLoop: {class: EventLoopMonitor}
25
+ * },
26
+ * bootstrap: ['eventLoop']
27
+ * }))
28
+ * .onLaunched(async (app: Application): Promise<void> => {
29
+ * const eventLoop: EventLoopMonitor = await app.getObject('eventLoop')
30
+ * setInterval((): void => {
31
+ * const statistics: IEventLoopMonitorStatistics = eventLoop.statistics
32
+ * if (statistics.p99 > 100) console.log(`The event loop lags: p99 ${statistics.p99} ms, ${statistics.utilRate}% busy`)
33
+ * //Each report covers the last minute
34
+ * eventLoop.reset()
35
+ * }, 60000)
36
+ * })
37
+ * ```
38
+ */
39
+ export class EventLoopMonitor extends Component {
40
+ constructor() {
41
+ super(...arguments);
42
+ /**
43
+ * The histogram of the event loop delays, in nanoseconds, sampled every millisecond while enabled (from the
44
+ * monitor's initialization to its destruction).
45
+ */
46
+ this.histogram = monitorEventLoopDelay({ resolution: 1 });
47
+ /**
48
+ * The event loop utilization when the monitor was created or reset: the start of the period that
49
+ * {@link IEventLoopMonitorStatistics.utilRate} covers.
50
+ */
51
+ this.initUtil = performance.eventLoopUtilization();
52
+ }
53
+ /**
54
+ * The event loop statistics since the monitor was created or reset, computed at each read: the minimum, maximum,
55
+ * mean, standard deviation and percentiles of the delays in milliseconds (not rounded; all 0 before the first
56
+ * measure), and the utilization in percent.
57
+ */
58
+ get statistics() {
59
+ //Without sample yet, the histogram gives no meaningful value
60
+ const sampled = this.histogram.count > 0;
61
+ const delay = (nanoseconds) => sampled ? nanoseconds / 1e6 : 0;
62
+ return {
63
+ min: delay(this.histogram.min),
64
+ max: delay(this.histogram.max),
65
+ avg: delay(this.histogram.mean),
66
+ stdDev: delay(this.histogram.stddev),
67
+ p50: delay(this.histogram.percentile(50)),
68
+ p90: delay(this.histogram.percentile(90)),
69
+ p95: delay(this.histogram.percentile(95)),
70
+ p99: delay(this.histogram.percentile(99)),
71
+ utilRate: performance.eventLoopUtilization(this.initUtil).utilization * 100
72
+ };
73
+ }
74
+ async init() {
75
+ this.histogram.enable();
76
+ }
77
+ async destroy() {
78
+ this.histogram.disable();
79
+ }
80
+ /**
81
+ * Forget the delays and restart the utilization: the statistics then cover the time since this call. Use it to
82
+ * measure a period, such as between two reports.
83
+ */
84
+ reset() {
85
+ this.histogram.reset();
86
+ this.initUtil = performance.eventLoopUtilization();
87
+ }
88
+ }
@@ -0,0 +1,88 @@
1
+ import { PerformanceObserver } from 'node:perf_hooks';
2
+ import { Component } from '@lakutata/core';
3
+ import { round, Samples } from './lib/Samples.js';
4
+ /**
5
+ * The HTTP request monitor: measures how long the HTTP requests received by the process take, in milliseconds, from
6
+ * their arrival to the end of their response, through the performance entries of `node:http` (so every server built on
7
+ * it is counted: the Express and Fastify adapters, and any other server of the process). The requests the process
8
+ * sends are not counted, nor are HTTP/2 requests.
9
+ *
10
+ * It keeps the count and the statistics of the durations since it was created or last {@link HttpRequestMonitor.reset},
11
+ * in constant memory. The entries reach it in batches, shortly after the responses end, so a request just answered may
12
+ * not be counted yet. Declare it in the `components` of the application (`{class: HttpRequestMonitor}`) and get it with
13
+ * `@Inject` or `getObject()`; it is a singleton. It only sees the requests ending after its creation: list it in
14
+ * `bootstrap` to measure from the launch; it stops when the component is destroyed.
15
+ * @example
16
+ * ```typescript
17
+ * import {Application} from 'lakutata'
18
+ * import {HttpRequestMonitor, type IHttpRequestMonitorStatistics} from 'lakutata/com/monitor'
19
+ *
20
+ * Application
21
+ * .run(() => ({
22
+ * id: 'http.app',
23
+ * name: 'Http',
24
+ * components: {
25
+ * httpRequests: {class: HttpRequestMonitor}
26
+ * },
27
+ * bootstrap: ['httpRequests']
28
+ * }))
29
+ * .onLaunched(async (app: Application): Promise<void> => {
30
+ * const httpRequests: HttpRequestMonitor = await app.getObject('httpRequests')
31
+ * setInterval((): void => {
32
+ * const statistics: IHttpRequestMonitorStatistics = httpRequests.statistics
33
+ * console.log(`${statistics.count} requests, avg ${statistics.avg} ms, p99 ${statistics.p99} ms`)
34
+ * httpRequests.reset()
35
+ * }, 60000)
36
+ * })
37
+ * ```
38
+ */
39
+ export class HttpRequestMonitor extends Component {
40
+ constructor() {
41
+ super(...arguments);
42
+ this.#fractionDigits = 2;
43
+ this.#durations = new Samples(100);
44
+ /**
45
+ * The observer of the `http` performance entries, recording the duration of each received request (the
46
+ * `HttpRequest` entries); observing from the monitor's initialization to its destruction.
47
+ */
48
+ this.observer = new PerformanceObserver((list) => {
49
+ //Every request of the batch, the received ones only (HttpClient entries are the requests sent)
50
+ for (const entry of list.getEntriesByName('HttpRequest'))
51
+ this.#durations.record(entry.duration);
52
+ });
53
+ }
54
+ #fractionDigits;
55
+ #durations;
56
+ /**
57
+ * The statistics of the request durations since the monitor was created or reset, computed at each read: the
58
+ * number of requests, and the minimum, maximum, mean, standard deviation and percentiles of their durations in
59
+ * milliseconds, rounded to 2 decimals. The percentiles are approximate (3 significant digits). All 0 before the first
60
+ * request.
61
+ */
62
+ get statistics() {
63
+ return {
64
+ count: this.#durations.count,
65
+ min: round(this.#durations.min, this.#fractionDigits),
66
+ max: round(this.#durations.max, this.#fractionDigits),
67
+ avg: round(this.#durations.mean, this.#fractionDigits),
68
+ stdDev: round(this.#durations.stdDev, this.#fractionDigits),
69
+ p50: round(this.#durations.percentile(50), this.#fractionDigits),
70
+ p90: round(this.#durations.percentile(90), this.#fractionDigits),
71
+ p95: round(this.#durations.percentile(95), this.#fractionDigits),
72
+ p99: round(this.#durations.percentile(99), this.#fractionDigits)
73
+ };
74
+ }
75
+ async init() {
76
+ this.observer.observe({ entryTypes: ['http'] });
77
+ }
78
+ async destroy() {
79
+ this.observer.disconnect();
80
+ }
81
+ /**
82
+ * Forget the requests: the count and the statistics restart from 0. Use it to measure a period, such as between
83
+ * two reports.
84
+ */
85
+ reset() {
86
+ this.#durations.reset();
87
+ }
88
+ }
@@ -0,0 +1,115 @@
1
+ import * as os from 'node:os';
2
+ import { Component } from '@lakutata/core';
3
+ import { round, Samples } from './lib/Samples.js';
4
+ /**
5
+ * The memory monitor: measures the memory of the process, in bytes, with `process.memoryUsage()`: its resident set
6
+ * (the physical memory it occupies, `rss`), its V8 heap (`heapTotal` reserved, `heapUsed` occupied by the JavaScript
7
+ * objects) and its external memory (the C++ objects bound to JavaScript ones, the `Buffer`s included). It takes a
8
+ * sample when it is created then every second, and keeps the statistics of every sample since it was created or last
9
+ * {@link MemoryMonitor.reset}: the minimum, the maximum, the mean and percentiles, in constant memory.
10
+ *
11
+ * Declare it in the `components` of the application (`{class: MemoryMonitor}`) and get it with `@Inject` or
12
+ * `getObject()`; it is a singleton. It starts sampling when it is created: list it in `bootstrap` to measure from the
13
+ * launch. Its timer does not keep the process alive, and stops when the component is destroyed.
14
+ * @example
15
+ * ```typescript
16
+ * import {Application} from 'lakutata'
17
+ * import {MemoryMonitor, type IMemoryMonitorStatistics} from 'lakutata/com/monitor'
18
+ *
19
+ * Application
20
+ * .run(() => ({
21
+ * id: 'memory.app',
22
+ * name: 'Memory',
23
+ * components: {
24
+ * memory: {class: MemoryMonitor}
25
+ * },
26
+ * bootstrap: ['memory']
27
+ * }))
28
+ * .onLaunched(async (app: Application): Promise<void> => {
29
+ * const memory: MemoryMonitor = await app.getObject('memory')
30
+ * setInterval((): void => {
31
+ * const statistics: IMemoryMonitorStatistics = memory.statistics
32
+ * const megabytes = (bytes: number): string => (bytes / 1048576).toFixed(1)
33
+ * console.log(`RSS ${megabytes(statistics.physicalUsed)} MB (${statistics.physicalUsage}% of the host), heap ${statistics.heapUsage}% used`)
34
+ * }, 60000)
35
+ * })
36
+ * ```
37
+ */
38
+ export class MemoryMonitor extends Component {
39
+ #fractionDigits = 2;
40
+ #intervalDelay = 1000;
41
+ #totalMemorySize = os.totalmem();
42
+ #physical = new Samples();
43
+ #heap = new Samples();
44
+ #external = new Samples();
45
+ #latest = process.memoryUsage();
46
+ #sampleInterval;
47
+ /**
48
+ * The memory statistics, computed at each read: the latest sample (`physicalUsed`, `heapTotal`, `heapUsed`,
49
+ * `externalUsed`, in bytes, and the percentages `physicalUsage` and `heapUsage`, rounded to 2 decimals), the host's
50
+ * total memory, and the minimum, maximum, mean (rounded to 2 decimals) and percentiles of the samples since the
51
+ * monitor was created or reset. The percentiles are approximate (3 significant digits). The statistics of the
52
+ * samples are 0 after a reset until the next sample.
53
+ */
54
+ get statistics() {
55
+ const latest = this.#latest;
56
+ return {
57
+ physicalTotal: this.#totalMemorySize,
58
+ physicalUsed: latest.rss,
59
+ physicalUsage: round(latest.rss / this.#totalMemorySize * 100, this.#fractionDigits),
60
+ physicalUsedMin: this.#physical.min,
61
+ physicalUsedMax: this.#physical.max,
62
+ physicalUsedAvg: round(this.#physical.mean, this.#fractionDigits),
63
+ physicalUsedP50: this.#physical.percentile(50),
64
+ physicalUsedP90: this.#physical.percentile(90),
65
+ physicalUsedP95: this.#physical.percentile(95),
66
+ physicalUsedP99: this.#physical.percentile(99),
67
+ heapTotal: latest.heapTotal,
68
+ heapUsed: latest.heapUsed,
69
+ heapUsage: round(latest.heapTotal ? latest.heapUsed / latest.heapTotal * 100 : 0, this.#fractionDigits),
70
+ heapUsedMin: this.#heap.min,
71
+ heapUsedMax: this.#heap.max,
72
+ heapUsedAvg: round(this.#heap.mean, this.#fractionDigits),
73
+ heapUsedP50: this.#heap.percentile(50),
74
+ heapUsedP90: this.#heap.percentile(90),
75
+ heapUsedP95: this.#heap.percentile(95),
76
+ heapUsedP99: this.#heap.percentile(99),
77
+ externalUsed: latest.external,
78
+ externalUsedMin: this.#external.min,
79
+ externalUsedMax: this.#external.max,
80
+ externalUsedAvg: round(this.#external.mean, this.#fractionDigits),
81
+ externalUsedP50: this.#external.percentile(50),
82
+ externalUsedP90: this.#external.percentile(90),
83
+ externalUsedP95: this.#external.percentile(95),
84
+ externalUsedP99: this.#external.percentile(99)
85
+ };
86
+ }
87
+ /**
88
+ * Take a sample of the memory of the process: it becomes the latest usage and is added to the statistics. Called
89
+ * when the monitor is created, then every second.
90
+ * @protected
91
+ */
92
+ sampleMemoryUsage() {
93
+ const usage = process.memoryUsage();
94
+ this.#latest = usage;
95
+ this.#physical.record(usage.rss);
96
+ this.#heap.record(usage.heapUsed);
97
+ this.#external.record(usage.external);
98
+ }
99
+ async init() {
100
+ this.sampleMemoryUsage();
101
+ this.#sampleInterval = setInterval(() => this.sampleMemoryUsage(), this.#intervalDelay).unref();
102
+ }
103
+ async destroy() {
104
+ clearInterval(this.#sampleInterval);
105
+ }
106
+ /**
107
+ * Forget the samples: the minimum, maximum, mean and percentiles are 0 until the next sample, then cover the samples
108
+ * taken since. The latest usage is kept.
109
+ */
110
+ reset() {
111
+ this.#physical.reset();
112
+ this.#heap.reset();
113
+ this.#external.reset();
114
+ }
115
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,114 @@
1
+ import { createHistogram } from 'node:perf_hooks';
2
+ /**
3
+ * The statistics of the samples of a measure, in a constant memory: the latest value, the minimum, the maximum, the
4
+ * mean and the standard deviation are exact, the percentiles come from a histogram (3 significant digits)
5
+ */
6
+ export class Samples {
7
+ /**
8
+ * The histogram of the percentiles: it records integers from 1, so a value is recorded multiplied by the scale,
9
+ * plus 1 (a value may be 0)
10
+ */
11
+ #histogram = createHistogram();
12
+ #count = 0;
13
+ #latest = 0;
14
+ #min = 0;
15
+ #max = 0;
16
+ #mean = 0;
17
+ //The sum of the squares of the differences from the mean (Welford's algorithm)
18
+ #m2 = 0;
19
+ /**
20
+ * @param scale the factor of the precision of the percentiles (100 keeps 2 decimals)
21
+ */
22
+ constructor(scale = 1) {
23
+ this.scale = scale;
24
+ }
25
+ /**
26
+ * Record a sample (a negative value is recorded as 0)
27
+ * @param value
28
+ */
29
+ record(value) {
30
+ const sample = Number.isFinite(value) && value > 0 ? value : 0;
31
+ this.#histogram.record(Math.round(sample * this.scale) + 1);
32
+ this.#count++;
33
+ this.#latest = sample;
34
+ if (this.#count === 1) {
35
+ this.#min = sample;
36
+ this.#max = sample;
37
+ }
38
+ else {
39
+ if (sample < this.#min)
40
+ this.#min = sample;
41
+ if (sample > this.#max)
42
+ this.#max = sample;
43
+ }
44
+ const delta = sample - this.#mean;
45
+ this.#mean += delta / this.#count;
46
+ this.#m2 += delta * (sample - this.#mean);
47
+ }
48
+ /**
49
+ * The number of samples
50
+ */
51
+ get count() {
52
+ return this.#count;
53
+ }
54
+ /**
55
+ * The latest sample, 0 without sample
56
+ */
57
+ get latest() {
58
+ return this.#latest;
59
+ }
60
+ /**
61
+ * The minimum, 0 without sample
62
+ */
63
+ get min() {
64
+ return this.#min;
65
+ }
66
+ /**
67
+ * The maximum, 0 without sample
68
+ */
69
+ get max() {
70
+ return this.#max;
71
+ }
72
+ /**
73
+ * The mean, 0 without sample
74
+ */
75
+ get mean() {
76
+ return this.#mean;
77
+ }
78
+ /**
79
+ * The (population) standard deviation, 0 without sample
80
+ */
81
+ get stdDev() {
82
+ return this.#count ? Math.sqrt(this.#m2 / this.#count) : 0;
83
+ }
84
+ /**
85
+ * A percentile, 0 without sample
86
+ * @param percentile from 0 to 100
87
+ */
88
+ percentile(percentile) {
89
+ if (!this.#count)
90
+ return 0;
91
+ return Math.min(Math.max((this.#histogram.percentile(percentile) - 1) / this.scale, this.#min), this.#max);
92
+ }
93
+ /**
94
+ * Forget the samples
95
+ */
96
+ reset() {
97
+ this.#histogram.reset();
98
+ this.#count = 0;
99
+ this.#latest = 0;
100
+ this.#min = 0;
101
+ this.#max = 0;
102
+ this.#mean = 0;
103
+ this.#m2 = 0;
104
+ }
105
+ }
106
+ /**
107
+ * Round a number to a number of decimals
108
+ * @param value
109
+ * @param fractionDigits
110
+ * @constructor
111
+ */
112
+ export function round(value, fractionDigits) {
113
+ return parseFloat(value.toFixed(fractionDigits));
114
+ }
@@ -0,0 +1,9 @@
1
+ export * from '../monitor/CpuMonitor.js';
2
+ export * from '../monitor/EventLoopMonitor.js';
3
+ export * from '../monitor/HttpRequestMonitor.js';
4
+ export * from '../monitor/MemoryMonitor.js';
5
+ export * from '../monitor/interfaces/IMonitor.js';
6
+ export * from '../monitor/interfaces/ICpuMonitorStatistics.js';
7
+ export * from '../monitor/interfaces/IEventLoopMonitorStatistics.js';
8
+ export * from '../monitor/interfaces/IHttpRequestMonitorStatistics.js';
9
+ export * from '../monitor/interfaces/IMemoryMonitorStatistics.js';
@@ -0,0 +1,60 @@
1
+ import { Component } from '@lakutata/core';
2
+ import { IMonitor } from './interfaces/IMonitor.js';
3
+ import { ICpuMonitorStatistics } from './interfaces/ICpuMonitorStatistics.js';
4
+ /**
5
+ * The CPU monitor: measures the CPU time the process uses (user plus system, all its threads), as a percentage of one
6
+ * core: 100 is one core fully busy, and a process using several cores (worker threads, the libuv thread pool) goes
7
+ * above 100. It takes a sample every second, each the average usage over that second, and keeps the statistics of
8
+ * every sample since it was created or last {@link CpuMonitor.reset}: the latest, the minimum, the maximum, the mean
9
+ * and percentiles, in constant memory.
10
+ *
11
+ * Declare it in the `components` of the application (`{class: CpuMonitor}`) and get it with `@Inject` or
12
+ * `getObject()`; it is a singleton. It starts sampling when it is created: list it in `bootstrap` to measure from the
13
+ * launch. The statistics are all 0 until the first sample, one second after its creation. Its timer does not keep the
14
+ * process alive, and stops when the component is destroyed.
15
+ * @example
16
+ * ```typescript
17
+ * import {Application} from 'lakutata'
18
+ * import {CpuMonitor, type ICpuMonitorStatistics} from 'lakutata/com/monitor'
19
+ *
20
+ * Application
21
+ * .run(() => ({
22
+ * id: 'cpu.app',
23
+ * name: 'Cpu',
24
+ * components: {
25
+ * cpu: {class: CpuMonitor}
26
+ * },
27
+ * //Created at the launch, so that it samples from the start
28
+ * bootstrap: ['cpu']
29
+ * }))
30
+ * .onLaunched(async (app: Application): Promise<void> => {
31
+ * const cpu: CpuMonitor = await app.getObject('cpu')
32
+ * setInterval((): void => {
33
+ * const statistics: ICpuMonitorStatistics = cpu.statistics
34
+ * console.log(`CPU ${statistics.usage}% (p95 ${statistics.usageP95}%, max ${statistics.usageMax}%)`)
35
+ * }, 60000)
36
+ * })
37
+ * ```
38
+ */
39
+ export declare class CpuMonitor extends Component implements IMonitor<ICpuMonitorStatistics> {
40
+ #private;
41
+ /**
42
+ * The statistics of the CPU samples since the monitor was created or reset, computed at each read: the latest
43
+ * usage and the minimum, maximum, mean and percentiles (P50 to P99) of the samples, in percent of one core, rounded
44
+ * to 5 decimals. The percentiles are approximate (3 significant digits). All 0 before the first sample.
45
+ */
46
+ get statistics(): ICpuMonitorStatistics;
47
+ /**
48
+ * Take a sample: the CPU usage since the previous sample (user plus system time over the elapsed time, in percent of
49
+ * one core). Called every second.
50
+ * @protected
51
+ */
52
+ protected sampleCpuUsage(): void;
53
+ protected init(): Promise<void>;
54
+ protected destroy(): Promise<void>;
55
+ /**
56
+ * Forget the samples: the statistics are 0 until the next sample, then cover the samples taken since. Use it to
57
+ * measure a period, such as between two reports.
58
+ */
59
+ reset(): void;
60
+ }
@@ -0,0 +1,65 @@
1
+ import { type EventLoopUtilization, type IntervalHistogram } from 'node:perf_hooks';
2
+ import { Component } from '@lakutata/core';
3
+ import { IEventLoopMonitorStatistics } from './interfaces/IEventLoopMonitorStatistics.js';
4
+ import { IMonitor } from './interfaces/IMonitor.js';
5
+ /**
6
+ * The event loop monitor: measures how late the event loop of the main thread runs its timers (the event loop delay,
7
+ * in milliseconds, with Node's `monitorEventLoopDelay` at a 1 ms resolution) and how busy it is (its utilization, in
8
+ * percent of the time). A growing delay means synchronous work (CPU-bound code, synchronous I/O, large JSON) blocks the
9
+ * other requests; a utilization near 100 means the loop has no idle time left.
10
+ *
11
+ * Both cover the time since the monitor was created or last {@link EventLoopMonitor.reset}: the delays are a
12
+ * histogram of every measure, the utilization is the share of that whole time the loop was busy. Declare it in the
13
+ * `components` of the application (`{class: EventLoopMonitor}`) and get it with `@Inject` or `getObject()`; it is a
14
+ * singleton. It starts measuring when it is created: list it in `bootstrap` to measure from the launch; it stops when
15
+ * the component is destroyed.
16
+ * @example
17
+ * ```typescript
18
+ * import {Application} from 'lakutata'
19
+ * import {EventLoopMonitor, type IEventLoopMonitorStatistics} from 'lakutata/com/monitor'
20
+ *
21
+ * Application
22
+ * .run(() => ({
23
+ * id: 'loop.app',
24
+ * name: 'Loop',
25
+ * components: {
26
+ * eventLoop: {class: EventLoopMonitor}
27
+ * },
28
+ * bootstrap: ['eventLoop']
29
+ * }))
30
+ * .onLaunched(async (app: Application): Promise<void> => {
31
+ * const eventLoop: EventLoopMonitor = await app.getObject('eventLoop')
32
+ * setInterval((): void => {
33
+ * const statistics: IEventLoopMonitorStatistics = eventLoop.statistics
34
+ * if (statistics.p99 > 100) console.log(`The event loop lags: p99 ${statistics.p99} ms, ${statistics.utilRate}% busy`)
35
+ * //Each report covers the last minute
36
+ * eventLoop.reset()
37
+ * }, 60000)
38
+ * })
39
+ * ```
40
+ */
41
+ export declare class EventLoopMonitor extends Component implements IMonitor<IEventLoopMonitorStatistics> {
42
+ /**
43
+ * The histogram of the event loop delays, in nanoseconds, sampled every millisecond while enabled (from the
44
+ * monitor's initialization to its destruction).
45
+ */
46
+ protected readonly histogram: IntervalHistogram;
47
+ /**
48
+ * The event loop utilization when the monitor was created or reset: the start of the period that
49
+ * {@link IEventLoopMonitorStatistics.utilRate} covers.
50
+ */
51
+ protected initUtil: EventLoopUtilization;
52
+ /**
53
+ * The event loop statistics since the monitor was created or reset, computed at each read: the minimum, maximum,
54
+ * mean, standard deviation and percentiles of the delays in milliseconds (not rounded; all 0 before the first
55
+ * measure), and the utilization in percent.
56
+ */
57
+ get statistics(): IEventLoopMonitorStatistics;
58
+ protected init(): Promise<void>;
59
+ protected destroy(): Promise<void>;
60
+ /**
61
+ * Forget the delays and restart the utilization: the statistics then cover the time since this call. Use it to
62
+ * measure a period, such as between two reports.
63
+ */
64
+ reset(): void;
65
+ }