Skip to content

Commit bafdb19

Browse files
committed
feat(admin): add in-memory metrics collector foundation
Signed-off-by: ABHAY PANDEY <pandeyabhay967@gmail.com>
1 parent 33f0ba9 commit bafdb19

2 files changed

Lines changed: 144 additions & 0 deletions

File tree

‎src/utils/admin-metrics.ts‎

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import os from 'os'
2+
3+
export interface AdminMetricsSnapshot {
4+
timestamp: number
5+
eventsPerSecond: number
6+
acceptedEvents: number
7+
rejectedEvents: number
8+
activeConnections: number
9+
cpuLoadPercent: number
10+
memoryUsedMb: number
11+
}
12+
13+
export class AdminMetricsCollector {
14+
private acceptedEvents = 0
15+
private rejectedEvents = 0
16+
private readonly activeConnections = new Set<string>()
17+
private readonly eventTimestamps: number[] = []
18+
private previousCpuUsage = process.cpuUsage()
19+
private previousCpuTimeMs = Date.now()
20+
21+
public recordAcceptedEvent(timestamp: number = Date.now()): void {
22+
this.acceptedEvents += 1
23+
this.eventTimestamps.push(timestamp)
24+
this.pruneEventWindow(timestamp)
25+
}
26+
27+
public recordRejectedEvent(timestamp: number = Date.now()): void {
28+
this.rejectedEvents += 1
29+
this.eventTimestamps.push(timestamp)
30+
this.pruneEventWindow(timestamp)
31+
}
32+
33+
public openConnection(connectionId: string): void {
34+
this.activeConnections.add(connectionId)
35+
}
36+
37+
public closeConnection(connectionId: string): void {
38+
this.activeConnections.delete(connectionId)
39+
}
40+
41+
public getSnapshot(timestamp: number = Date.now()): AdminMetricsSnapshot {
42+
this.pruneEventWindow(timestamp)
43+
44+
return {
45+
timestamp,
46+
eventsPerSecond: this.eventTimestamps.length,
47+
acceptedEvents: this.acceptedEvents,
48+
rejectedEvents: this.rejectedEvents,
49+
activeConnections: this.activeConnections.size,
50+
cpuLoadPercent: this.getCpuLoadPercent(timestamp),
51+
memoryUsedMb: Math.round((process.memoryUsage().heapUsed / 1024 / 1024) * 100) / 100,
52+
}
53+
}
54+
55+
private pruneEventWindow(timestamp: number): void {
56+
const minTimestamp = timestamp - 1000
57+
58+
while (this.eventTimestamps.length > 0 && this.eventTimestamps[0] <= minTimestamp) {
59+
this.eventTimestamps.shift()
60+
}
61+
}
62+
63+
private getCpuLoadPercent(timestamp: number): number {
64+
const elapsedMs = timestamp - this.previousCpuTimeMs
65+
if (elapsedMs <= 0) {
66+
return 0
67+
}
68+
69+
const cpuDiff = process.cpuUsage(this.previousCpuUsage)
70+
this.previousCpuUsage = process.cpuUsage()
71+
this.previousCpuTimeMs = timestamp
72+
73+
const cpuTotalMs = (cpuDiff.user + cpuDiff.system) / 1000
74+
const cores = Math.max(os.cpus().length, 1)
75+
const loadPercent = (cpuTotalMs / (elapsedMs * cores)) * 100
76+
77+
return Math.round(Math.max(0, Math.min(loadPercent, 100)) * 100) / 100
78+
}
79+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { expect } from 'chai'
2+
import Sinon from 'sinon'
3+
4+
import { AdminMetricsCollector } from '../../../src/utils/admin-metrics'
5+
6+
describe('AdminMetricsCollector', () => {
7+
let collector: AdminMetricsCollector
8+
let clock: Sinon.SinonFakeTimers
9+
let sandbox: Sinon.SinonSandbox
10+
11+
beforeEach(() => {
12+
sandbox = Sinon.createSandbox()
13+
clock = sandbox.useFakeTimers(10000)
14+
collector = new AdminMetricsCollector()
15+
})
16+
17+
afterEach(() => {
18+
clock.restore()
19+
sandbox.restore()
20+
})
21+
22+
it('returns zeroed snapshot by default', () => {
23+
const snapshot = collector.getSnapshot()
24+
25+
expect(snapshot.eventsPerSecond).to.equal(0)
26+
expect(snapshot.acceptedEvents).to.equal(0)
27+
expect(snapshot.rejectedEvents).to.equal(0)
28+
expect(snapshot.activeConnections).to.equal(0)
29+
expect(snapshot.cpuLoadPercent).to.be.greaterThanOrEqual(0)
30+
expect(snapshot.memoryUsedMb).to.be.greaterThan(0)
31+
})
32+
33+
it('tracks accepted and rejected event counters', () => {
34+
collector.recordAcceptedEvent()
35+
collector.recordAcceptedEvent()
36+
collector.recordRejectedEvent()
37+
38+
const snapshot = collector.getSnapshot()
39+
40+
expect(snapshot.acceptedEvents).to.equal(2)
41+
expect(snapshot.rejectedEvents).to.equal(1)
42+
})
43+
44+
it('tracks events per second in a rolling 1-second window', () => {
45+
collector.recordAcceptedEvent(10000)
46+
collector.recordRejectedEvent(10500)
47+
collector.recordRejectedEvent(10999)
48+
49+
expect(collector.getSnapshot(11000).eventsPerSecond).to.equal(2)
50+
expect(collector.getSnapshot(11500).eventsPerSecond).to.equal(1)
51+
expect(collector.getSnapshot(12000).eventsPerSecond).to.equal(0)
52+
})
53+
54+
it('tracks active websocket connections by id', () => {
55+
collector.openConnection('conn-1')
56+
collector.openConnection('conn-2')
57+
collector.openConnection('conn-1')
58+
59+
expect(collector.getSnapshot().activeConnections).to.equal(2)
60+
61+
collector.closeConnection('conn-2')
62+
63+
expect(collector.getSnapshot().activeConnections).to.equal(1)
64+
})
65+
})

0 commit comments

Comments
 (0)