File size: 3,520 Bytes
4e23b01
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
import { cpus, freemem, loadavg, totalmem } from 'node:os';

import type { TelemetryProperties } from './types';

const DEFAULT_INTERVAL_MS = 300_000;
const DEFAULT_WARMUP_SAMPLE_MS = 1_500;
const SYSTEM_METRICS_EVENT = 'system_metrics';

export interface SystemMetricsTrackClient {
  track(event: string, properties?: TelemetryProperties): void;
}

export interface SystemMetricsCollectorOptions {
  readonly client: SystemMetricsTrackClient;
  readonly intervalMs?: number;
  readonly warmupSampleMs?: number | null;
}

export class SystemMetricsCollector {
  private readonly client: SystemMetricsTrackClient;
  private readonly intervalMs: number;
  private readonly warmupSampleMs: number | null;
  private intervalTimer: ReturnType<typeof setInterval> | null = null;
  private warmupTimer: ReturnType<typeof setTimeout> | null = null;
  private previousCpuUsage = process.cpuUsage();
  private previousHrtime = process.hrtime.bigint();
  private readonly processStartedAtSeconds = Math.floor(Date.now() / 1000 - process.uptime());

  constructor(options: SystemMetricsCollectorOptions) {
    this.client = options.client;
    this.intervalMs = options.intervalMs ?? DEFAULT_INTERVAL_MS;
    this.warmupSampleMs =
      options.warmupSampleMs === undefined ? DEFAULT_WARMUP_SAMPLE_MS : options.warmupSampleMs;
  }

  start(): void {
    if (this.intervalTimer !== null) return;

    if (this.warmupSampleMs !== null && this.warmupSampleMs > 0) {
      this.warmupTimer = setTimeout(() => {
        this.warmupTimer = null;
        this.sampleSafely();
      }, this.warmupSampleMs);
      this.warmupTimer.unref?.();
    }

    this.intervalTimer = setInterval(() => {
      this.sampleSafely();
    }, this.intervalMs);
    this.intervalTimer.unref?.();
  }

  stop(): void {
    if (this.warmupTimer !== null) {
      clearTimeout(this.warmupTimer);
      this.warmupTimer = null;
    }
    if (this.intervalTimer !== null) {
      clearInterval(this.intervalTimer);
      this.intervalTimer = null;
    }
  }

  private sampleSafely(): void {
    try {
      this.sample();
    } catch {
      this.stop();
    }
  }

  private sample(): void {
    const now = process.hrtime.bigint();
    const elapsedUs = Number(now - this.previousHrtime) / 1_000;

    const cpu = process.cpuUsage(this.previousCpuUsage);
    this.previousCpuUsage = process.cpuUsage();
    this.previousHrtime = now;

    const mem = process.memoryUsage();
    const constrainedMemory = getConstrainedMemoryBytes();
    const properties: TelemetryProperties = {
      process_started_at: this.processStartedAtSeconds,
      process_uptime_ms: Math.round(process.uptime() * 1000),
      rss_bytes: mem.rss,
      heap_used_bytes: mem.heapUsed,
      heap_total_bytes: mem.heapTotal,
      external_bytes: mem.external,
      array_buffers_bytes: mem.arrayBuffers,
      cpu_user_us: cpu.user,
      cpu_system_us: cpu.system,
      cpu_elapsed_us: Math.round(elapsedUs),
      load_avg_1m: loadavg()[0],
      free_mem_bytes: freemem(),
      total_mem_bytes: totalmem(),
      cpu_count: cpus().length,
    };
    if (constrainedMemory !== undefined) {
      properties['constrained_memory_bytes'] = constrainedMemory;
    }

    this.client.track(SYSTEM_METRICS_EVENT, properties);
  }
}

function getConstrainedMemoryBytes(): number | undefined {
  if (typeof process.constrainedMemory !== 'function') return undefined;
  const value = process.constrainedMemory();
  return Number.isSafeInteger(value) && value >= 0 ? value : undefined;
}