@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.
- package/.turbo/turbo-build.log +1 -1
- package/AGENTS.md +8 -1
- package/README.md +44 -0
- package/dist/init.js +33 -0
- package/dist/init.js.map +2 -2
- package/dist/modules/telemetry/__integration__/TC-TELEMETRY-002.spec.js +4 -0
- package/dist/modules/telemetry/__integration__/TC-TELEMETRY-002.spec.js.map +2 -2
- package/dist/provider/otlp-provider.js +24 -2
- package/dist/provider/otlp-provider.js.map +2 -2
- package/dist/runtime-metrics.js +132 -0
- package/dist/runtime-metrics.js.map +7 -0
- package/package.json +2 -2
- package/src/__tests__/env-load-order.test.ts +74 -1
- package/src/__tests__/otlp-integration.test.ts +34 -4
- package/src/__tests__/runtime-metrics.test.ts +239 -0
- package/src/init.ts +33 -0
- package/src/modules/telemetry/__integration__/TC-TELEMETRY-002.spec.ts +6 -0
- package/src/provider/otlp-provider.ts +18 -1
- package/src/runtime-metrics.ts +179 -0
|
@@ -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
|
+
}
|