Skip to content

Commit 3b7c9a0

Browse files
authored
feat(stats): getStats, server report channels (#78)
* index, alias typings for stats * options, add hashed publicSessionId, context driven channel names * server.helpers, add portValid helper * server.http, fix falsy port check * server.stats, initialize for server, basic health, transport reports * server, activate stat reports, expose getStats with channel names * stats, use lazy node diagnostic_channel publish
1 parent 562b4a6 commit 3b7c9a0

21 files changed

Lines changed: 799 additions & 32 deletions

src/__tests__/__snapshots__/options.defaults.test.ts.snap

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,12 @@ exports[`options defaults should return specific properties: defaults 1`] = `
5959
---
6060
6161
",
62+
"stats": {
63+
"reportIntervalMs": {
64+
"health": 30000,
65+
"transport": 10000,
66+
},
67+
},
6268
"toolMemoOptions": {
6369
"fetchDocs": {
6470
"cacheErrors": false,

src/__tests__/__snapshots__/options.test.ts.snap

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,9 @@ exports[`parseCliOptions should attempt to parse args with --http and --port 1`]
102102
exports[`parseCliOptions should attempt to parse args with --http and invalid --port 1`] = `
103103
{
104104
"docsHost": false,
105-
"http": {},
105+
"http": {
106+
"port": 0,
107+
},
106108
"isHttp": true,
107109
"logging": {
108110
"level": "info",

src/__tests__/__snapshots__/server.test.ts.snap

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ exports[`runServer should allow server to be stopped, http stop server: diagnost
66
[
77
"Server logging enabled.",
88
],
9+
[
10+
"Server stats enabled.",
11+
],
912
[
1013
"No external tools loaded.",
1114
],
@@ -38,6 +41,9 @@ exports[`runServer should allow server to be stopped, stdio stop server: diagnos
3841
[
3942
"Server logging enabled.",
4043
],
44+
[
45+
"Server stats enabled.",
46+
],
4147
[
4248
"No external tools loaded.",
4349
],
@@ -70,6 +76,9 @@ exports[`runServer should attempt to run server, create transport, connect, and
7076
[
7177
"Server logging enabled.",
7278
],
79+
[
80+
"Server stats enabled.",
81+
],
7382
[
7483
"No external tools loaded.",
7584
],
@@ -107,6 +116,9 @@ exports[`runServer should attempt to run server, disable SIGINT handler: diagnos
107116
[
108117
"Server logging enabled.",
109118
],
119+
[
120+
"Server stats enabled.",
121+
],
110122
[
111123
"No external tools loaded.",
112124
],
@@ -139,6 +151,9 @@ exports[`runServer should attempt to run server, enable SIGINT handler explicitl
139151
[
140152
"Server logging enabled.",
141153
],
154+
[
155+
"Server stats enabled.",
156+
],
142157
[
143158
"No external tools loaded.",
144159
],
@@ -176,6 +191,9 @@ exports[`runServer should attempt to run server, register a tool: diagnostics 1`
176191
[
177192
"Server logging enabled.",
178193
],
194+
[
195+
"Server stats enabled.",
196+
],
179197
[
180198
"No external tools loaded.",
181199
],
@@ -224,6 +242,9 @@ exports[`runServer should attempt to run server, register multiple tools: diagno
224242
[
225243
"Server logging enabled.",
226244
],
245+
[
246+
"Server stats enabled.",
247+
],
227248
[
228249
"No external tools loaded.",
229250
],
@@ -282,6 +303,9 @@ exports[`runServer should attempt to run server, use custom options: diagnostics
282303
[
283304
"Server logging enabled.",
284305
],
306+
[
307+
"Server stats enabled.",
308+
],
285309
[
286310
"No external tools loaded.",
287311
],
@@ -319,6 +343,9 @@ exports[`runServer should attempt to run server, use default tools, http: diagno
319343
[
320344
"Server logging enabled.",
321345
],
346+
[
347+
"Server stats enabled.",
348+
],
322349
[
323350
"No external tools loaded.",
324351
],
@@ -369,6 +396,9 @@ exports[`runServer should attempt to run server, use default tools, stdio: diagn
369396
[
370397
"Server logging enabled.",
371398
],
399+
[
400+
"Server stats enabled.",
401+
],
372402
[
373403
"No external tools loaded.",
374404
],

src/__tests__/index.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ describe('main', () => {
5555
const mockServerInstance = {
5656
stop: jest.fn().mockResolvedValue(undefined),
5757
isRunning: jest.fn().mockReturnValue(true),
58+
getStats: jest.fn().mockReturnValue({}),
5859
onLog: jest.fn()
5960
};
6061

src/__tests__/server.helpers.test.ts

Lines changed: 77 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,13 @@
1-
import { freezeObject, generateHash, hashCode, isPlainObject, isPromise, isReferenceLike, mergeObjects } from '../server.helpers';
1+
import {
2+
freezeObject,
3+
generateHash,
4+
hashCode,
5+
isPlainObject,
6+
isPromise,
7+
isReferenceLike,
8+
mergeObjects,
9+
portValid
10+
} from '../server.helpers';
211

312
describe('freezeObject', () => {
413
it.each([
@@ -598,3 +607,70 @@ describe('mergeObjects', () => {
598607
expect((Object.prototype as any).polluted).toBeUndefined();
599608
});
600609
});
610+
611+
describe('portValid', () => {
612+
it.each([
613+
{
614+
description: 'valid',
615+
port: 8080,
616+
expected: 8080
617+
},
618+
{
619+
description: 'zero',
620+
port: 0,
621+
expected: 0
622+
},
623+
{
624+
description: 'upper-range',
625+
port: 65535,
626+
expected: 65535
627+
},
628+
{
629+
description: 'out-of-range',
630+
port: 10_0000,
631+
expected: undefined
632+
},
633+
{
634+
description: 'out-of-range negative',
635+
port: -10_0000,
636+
expected: undefined
637+
},
638+
{
639+
description: 'string',
640+
port: '9000',
641+
expected: 9000
642+
},
643+
{
644+
description: 'empty string',
645+
port: '',
646+
expected: undefined
647+
},
648+
{
649+
description: 'NaN',
650+
port: NaN,
651+
expected: undefined
652+
},
653+
{
654+
description: 'float',
655+
port: 1.088,
656+
expected: undefined
657+
},
658+
{
659+
description: 'out-of-range float',
660+
port: -1.088,
661+
expected: undefined
662+
},
663+
{
664+
description: 'undefined',
665+
port: undefined,
666+
expected: undefined
667+
},
668+
{
669+
description: 'null',
670+
port: null,
671+
expected: undefined
672+
}
673+
])('should validate a port, $description', ({ port, expected }) => {
674+
expect(portValid(port)).toBe(expected);
675+
});
676+
});

src/__tests__/server.stats.test.ts

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import diagnostics_channel from 'node:diagnostics_channel';
2+
import { healthReport, statsReport, transportReport, createServerStats } from '../server.stats';
3+
import { getStatsOptions } from '../options.context';
4+
5+
describe('healthReport', () => {
6+
const statsOptions = getStatsOptions();
7+
8+
it('should generate a health report', () => {
9+
const type = 'health';
10+
const channelName = statsOptions.channels[type];
11+
const channel = diagnostics_channel.channel(channelName);
12+
const handler = jest.fn();
13+
14+
channel.subscribe(handler);
15+
16+
const report = healthReport(statsOptions);
17+
18+
expect(Object.keys(handler.mock.calls[0][0])).toEqual(expect.arrayContaining(['timestamp', 'type', 'memory', 'uptime']));
19+
20+
clearTimeout(report);
21+
});
22+
});
23+
24+
describe('statsReport', () => {
25+
const statsOptions = getStatsOptions();
26+
27+
it.each([
28+
{ description: 'stdio', httpPort: undefined },
29+
{ description: 'http', httpPort: 3030 }
30+
])('should generate a stats report, $description', ({ httpPort }) => {
31+
const report = statsReport({ httpPort }, statsOptions);
32+
33+
expect(Object.keys(report)).toEqual(expect.arrayContaining(['timestamp', 'reports']));
34+
expect(Object.keys(report.reports.transport).includes('port')).toBe(httpPort !== undefined);
35+
36+
expect(report.reports.transport.channelId).toBe(statsOptions.channels.transport);
37+
expect(report.reports.health.channelId).toBe(statsOptions.channels.health);
38+
expect(report.reports.traffic.channelId).toBe(statsOptions.channels.traffic);
39+
});
40+
});
41+
42+
describe('transportReport', () => {
43+
const statsOptions = getStatsOptions();
44+
45+
it('should generate a transport report', () => {
46+
const type = 'transport';
47+
const channelName = statsOptions.channels[type];
48+
const channel = diagnostics_channel.channel(channelName);
49+
const handler = jest.fn();
50+
51+
channel.subscribe(handler);
52+
53+
const report = transportReport({ httpPort: 9999 }, statsOptions);
54+
55+
expect(Object.keys(handler.mock.calls[0][0])).toEqual(expect.arrayContaining(['timestamp', 'type', 'method', 'port']));
56+
57+
clearTimeout(report);
58+
});
59+
});
60+
61+
describe('createServerStats', () => {
62+
const statsOptions = getStatsOptions();
63+
64+
beforeEach(() => {
65+
jest.useFakeTimers();
66+
});
67+
68+
afterEach(() => {
69+
jest.useRealTimers();
70+
});
71+
72+
it('should resolve stats promise after setStats is called', async () => {
73+
const tracker = createServerStats(statsOptions, { isHttp: true } as any);
74+
const httpHandle = { port: 9999, close: jest.fn() };
75+
76+
tracker.setStats(httpHandle as any);
77+
78+
const stats = await tracker.getStats();
79+
80+
expect(stats.reports.transport.port).toBe(9999);
81+
expect(stats.reports.transport.method).toBe('http');
82+
83+
tracker.unsubscribe();
84+
});
85+
86+
it('should correctly clean up timers on unsubscribe', () => {
87+
const tracker = createServerStats();
88+
const spy = jest.spyOn(global, 'clearTimeout');
89+
90+
tracker.unsubscribe();
91+
92+
expect(spy).toHaveBeenCalledTimes(1);
93+
});
94+
});

src/__tests__/server.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ describe('runServer', () => {
4747
// Mock HTTP transport
4848
mockClose = jest.fn().mockResolvedValue(undefined);
4949
mockHttpHandle = {
50+
port: 0,
5051
close: mockClose
5152
};
5253

0 commit comments

Comments
 (0)