1 import { readdir } from 'fs-extra'
2 import { constants, PerformanceObserver } from 'perf_hooks'
3 import * as process from 'process'
4 import { Meter, ObservableResult } from '@opentelemetry/api-metrics'
5 import { ExplicitBucketHistogramAggregation, MeterProvider } from '@opentelemetry/sdk-metrics-base'
6 import { View } from '@opentelemetry/sdk-metrics-base/build/src/view/View'
7 import { logger } from '@server/helpers/logger'
9 // Thanks to https://github.com/siimon/prom-client
10 // We took their logic and adapter it for opentelemetry
11 // Try to keep consistency with their metric name/description so it's easier to process (grafana dashboard template etc)
13 export class NodeJSObserversBuilder {
15 constructor (private readonly meter: Meter, private readonly meterProvider: MeterProvider) {
19 this.buildCPUObserver()
20 this.buildMemoryObserver()
22 this.buildHandlesObserver()
23 this.buildFileDescriptorsObserver()
25 this.buildGCObserver()
26 this.buildEventLoopLagObserver()
28 this.buildLibUVActiveRequestsObserver()
29 this.buildActiveResourcesObserver()
32 private buildCPUObserver () {
33 const cpuTotal = this.meter.createObservableCounter('process_cpu_seconds_total', {
34 description: 'Total user and system CPU time spent in seconds.'
36 const cpuUser = this.meter.createObservableCounter('process_cpu_user_seconds_total', {
37 description: 'Total user CPU time spent in seconds.'
39 const cpuSystem = this.meter.createObservableCounter('process_cpu_system_seconds_total', {
40 description: 'Total system CPU time spent in seconds.'
43 let lastCpuUsage = process.cpuUsage()
45 this.meter.addBatchObservableCallback(observableResult => {
46 const cpuUsage = process.cpuUsage()
48 const userUsageMicros = cpuUsage.user - lastCpuUsage.user
49 const systemUsageMicros = cpuUsage.system - lastCpuUsage.system
51 lastCpuUsage = cpuUsage
53 observableResult.observe(cpuTotal, (userUsageMicros + systemUsageMicros) / 1e6)
54 observableResult.observe(cpuUser, userUsageMicros / 1e6)
55 observableResult.observe(cpuSystem, systemUsageMicros / 1e6)
57 }, [ cpuTotal, cpuUser, cpuSystem ])
60 private buildMemoryObserver () {
61 this.meter.createObservableGauge('nodejs_memory_usage_bytes', {
63 }).addCallback(observableResult => {
64 const current = process.memoryUsage()
66 observableResult.observe(current.heapTotal, { memoryType: 'heapTotal' })
67 observableResult.observe(current.heapUsed, { memoryType: 'heapUsed' })
68 observableResult.observe(current.arrayBuffers, { memoryType: 'arrayBuffers' })
69 observableResult.observe(current.external, { memoryType: 'external' })
70 observableResult.observe(current.rss, { memoryType: 'rss' })
74 private buildHandlesObserver () {
75 if (typeof (process as any)._getActiveHandles !== 'function') return
77 this.meter.createObservableGauge('nodejs_active_handles_total', {
78 description: 'Total number of active handles.'
79 }).addCallback(observableResult => {
80 const handles = (process as any)._getActiveHandles()
82 observableResult.observe(handles.length)
86 private buildGCObserver () {
88 [constants.NODE_PERFORMANCE_GC_MAJOR]: 'major',
89 [constants.NODE_PERFORMANCE_GC_MINOR]: 'minor',
90 [constants.NODE_PERFORMANCE_GC_INCREMENTAL]: 'incremental',
91 [constants.NODE_PERFORMANCE_GC_WEAKCB]: 'weakcb'
94 this.meterProvider.addView(
95 new View({ aggregation: new ExplicitBucketHistogramAggregation([ 0.001, 0.01, 0.1, 1, 2, 5 ]) }),
96 { instrument: { name: 'nodejs_gc_duration_seconds' } }
99 const histogram = this.meter.createHistogram('nodejs_gc_duration_seconds', {
100 description: 'Garbage collection duration by kind, one of major, minor, incremental or weakcb'
103 const obs = new PerformanceObserver(list => {
104 const entry = list.getEntries()[0]
106 // Node < 16 uses entry.kind
107 // Node >= 16 uses entry.detail.kind
108 // See: https://nodejs.org/docs/latest-v16.x/api/deprecations.html#deprecations_dep0152_extension_performanceentry_properties
109 const kind = (entry as any).detail
110 ? kinds[(entry as any).detail.kind]
113 // Convert duration from milliseconds to seconds
114 histogram.record(entry.duration / 1000, {
119 obs.observe({ entryTypes: [ 'gc' ] })
122 private buildEventLoopLagObserver () {
123 const reportEventloopLag = (start: [ number, number ], observableResult: ObservableResult, res: () => void) => {
124 const delta = process.hrtime(start)
125 const nanosec = delta[0] * 1e9 + delta[1]
126 const seconds = nanosec / 1e9
128 observableResult.observe(seconds)
133 this.meter.createObservableGauge('nodejs_eventloop_lag_seconds', {
134 description: 'Lag of event loop in seconds.'
135 }).addCallback(observableResult => {
136 return new Promise(res => {
137 const start = process.hrtime()
139 setImmediate(reportEventloopLag, start, observableResult, res)
144 private buildFileDescriptorsObserver () {
145 this.meter.createObservableGauge('process_open_fds', {
146 description: 'Number of open file descriptors.'
147 }).addCallback(async observableResult => {
149 const fds = await readdir('/proc/self/fd')
150 observableResult.observe(fds.length - 1)
152 logger.debug('Cannot list file descriptors of current process for OpenTelemetry.', { err })
157 private buildLibUVActiveRequestsObserver () {
158 if (typeof (process as any)._getActiveRequests !== 'function') return
160 this.meter.createObservableGauge('nodejs_active_requests_total', {
161 description: 'Total number of active libuv requests.'
162 }).addCallback(observableResult => {
163 const requests = (process as any)._getActiveRequests()
165 observableResult.observe(requests.length)
169 private buildActiveResourcesObserver () {
170 if (typeof (process as any).getActiveResourcesInfo !== 'function') return
172 const grouped = this.meter.createObservableCounter('nodejs_active_resources', {
173 description: 'Number of active resources that are currently keeping the event loop alive, grouped by async resource type.'
175 const total = this.meter.createObservableCounter('nodejs_active_resources_total', {
176 description: 'Total number of active resources.'
179 this.meter.addBatchObservableCallback(observableResult => {
180 const resources = (process as any).getActiveResourcesInfo()
184 for (let i = 0; i < resources.length; i++) {
185 const resource = resources[i]
187 if (data[resource] === undefined) data[resource] = 0
191 for (const type of Object.keys(data)) {
192 observableResult.observe(grouped, data[type], { type })
195 observableResult.observe(total, resources.length)
196 }, [ grouped, total ])