@open-mercato/telemetry 0.10.1-develop.7827.1.5c631908e4 → 0.10.1-develop.7836.1.0ef0f453c6

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.
@@ -0,0 +1,179 @@
1
+ import { monitorEventLoopDelay, performance, type EventLoopUtilization } from 'node:perf_hooks'
2
+ import { getHeapSpaceStatistics } from 'node:v8'
3
+ import {
4
+ collectTelemetryMetrics,
5
+ getTelemetryRuntime,
6
+ recordTelemetryMetric,
7
+ } from '@open-mercato/shared/lib/telemetry/runtime'
8
+ import { createLogger } from '@open-mercato/shared/lib/logger'
9
+
10
+ const SAMPLE_INTERVAL_MS = 10_000
11
+ const DELAY_RESOLUTION_MS = 20
12
+ const NANOSECONDS_PER_SECOND = 1_000_000_000
13
+ const GLOBAL_KEY = Symbol.for('@open-mercato/telemetry.runtimeMetrics')
14
+
15
+ export type EventLoopDelayMonitor = {
16
+ enable(): void
17
+ disable(): void
18
+ percentile(percentile: number): number
19
+ reset(): void
20
+ }
21
+
22
+ export type HeapSpaceSample = {
23
+ space_name: string
24
+ space_used_size: number
25
+ }
26
+
27
+ export type RuntimeMetricSources = {
28
+ createEventLoopDelayMonitor(): EventLoopDelayMonitor
29
+ eventLoopUtilization(
30
+ first?: EventLoopUtilization,
31
+ second?: EventLoopUtilization,
32
+ ): EventLoopUtilization
33
+ memoryUsage(): { rss: number }
34
+ heapSpaceStatistics(): HeapSpaceSample[]
35
+ setInterval(handler: () => void, intervalMs: number): ReturnType<typeof setInterval>
36
+ clearInterval(interval: ReturnType<typeof setInterval>): void
37
+ }
38
+
39
+ type ActiveRuntimeSampler = {
40
+ references: number
41
+ dispose(): void
42
+ }
43
+
44
+ type RuntimeMetricsStore = {
45
+ active?: ActiveRuntimeSampler
46
+ }
47
+
48
+ const logger = createLogger('telemetry').child({ component: 'runtime-metrics' })
49
+
50
+ const defaultSources: RuntimeMetricSources = {
51
+ createEventLoopDelayMonitor: () => monitorEventLoopDelay({ resolution: DELAY_RESOLUTION_MS }),
52
+ eventLoopUtilization: (first, second) => performance.eventLoopUtilization(first, second),
53
+ memoryUsage: () => process.memoryUsage(),
54
+ heapSpaceStatistics: () => getHeapSpaceStatistics(),
55
+ setInterval: (handler, intervalMs) => setInterval(handler, intervalMs),
56
+ clearInterval: (interval) => clearInterval(interval),
57
+ }
58
+
59
+ function store(): RuntimeMetricsStore {
60
+ const globalStore = globalThis as unknown as Record<symbol, RuntimeMetricsStore | undefined>
61
+ let current = globalStore[GLOBAL_KEY]
62
+ if (!current) {
63
+ current = {}
64
+ globalStore[GLOBAL_KEY] = current
65
+ }
66
+ return current
67
+ }
68
+
69
+ function secondsFromNanoseconds(value: number): number {
70
+ return Number.isFinite(value) ? Math.max(0, value) / NANOSECONDS_PER_SECOND : 0
71
+ }
72
+
73
+ export function createRuntimeMetricsSampler(
74
+ sources: RuntimeMetricSources = defaultSources,
75
+ ): () => void {
76
+ const delay = sources.createEventLoopDelayMonitor()
77
+ let previousUtilization = sources.eventLoopUtilization()
78
+ delay.enable()
79
+
80
+ const sample = () => {
81
+ try {
82
+ const currentUtilization = sources.eventLoopUtilization()
83
+ const intervalUtilization = sources.eventLoopUtilization(
84
+ currentUtilization,
85
+ previousUtilization,
86
+ )
87
+ previousUtilization = currentUtilization
88
+
89
+ recordTelemetryMetric({
90
+ kind: 'gauge',
91
+ name: 'nodejs.eventloop.utilization',
92
+ value: intervalUtilization.utilization,
93
+ unit: '1',
94
+ })
95
+ for (const percentile of [50, 90, 99]) {
96
+ recordTelemetryMetric({
97
+ kind: 'gauge',
98
+ name: `nodejs.eventloop.delay.p${percentile}`,
99
+ value: secondsFromNanoseconds(delay.percentile(percentile)),
100
+ unit: 's',
101
+ })
102
+ }
103
+
104
+ recordTelemetryMetric({
105
+ kind: 'gauge',
106
+ name: 'process.memory.usage',
107
+ value: sources.memoryUsage().rss,
108
+ unit: 'By',
109
+ })
110
+ for (const heapSpace of sources.heapSpaceStatistics()) {
111
+ recordTelemetryMetric({
112
+ kind: 'gauge',
113
+ name: 'v8js.memory.heap.used',
114
+ value: heapSpace.space_used_size,
115
+ labels: { 'v8js.heap.space.name': heapSpace.space_name },
116
+ unit: 'By',
117
+ })
118
+ }
119
+
120
+ collectTelemetryMetrics((err) => {
121
+ logger.warn('Runtime metric collector failed', { err })
122
+ })
123
+ } catch (err) {
124
+ try {
125
+ getTelemetryRuntime()?.reportError(err, {
126
+ module: 'telemetry',
127
+ code: 'telemetry.runtime_sampling_failed',
128
+ })
129
+ } catch {}
130
+ logger.warn('Runtime metric sampling failed', { err })
131
+ } finally {
132
+ delay.reset()
133
+ }
134
+ }
135
+
136
+ const interval = sources.setInterval(sample, SAMPLE_INTERVAL_MS)
137
+ interval.unref?.()
138
+
139
+ let disposed = false
140
+ return () => {
141
+ if (disposed) return
142
+ disposed = true
143
+ sources.clearInterval(interval)
144
+ delay.disable()
145
+ }
146
+ }
147
+
148
+ export function startRuntimeMetrics(
149
+ sources: RuntimeMetricSources = defaultSources,
150
+ ): () => void {
151
+ const current = store()
152
+ if (!current.active) {
153
+ current.active = {
154
+ references: 0,
155
+ dispose: createRuntimeMetricsSampler(sources),
156
+ }
157
+ }
158
+
159
+ const active = current.active
160
+ active.references += 1
161
+ let released = false
162
+
163
+ return () => {
164
+ if (released) return
165
+ released = true
166
+ active.references -= 1
167
+ if (active.references > 0) return
168
+ active.dispose()
169
+ const latest = store()
170
+ if (latest.active === active) latest.active = undefined
171
+ }
172
+ }
173
+
174
+ /** Test-only: tear down the process-wide sampler regardless of reference count. */
175
+ export function resetRuntimeMetrics(): void {
176
+ const current = store()
177
+ current.active?.dispose()
178
+ current.active = undefined
179
+ }