@cocreate/server-telemetry 1.15.0

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/src/index.js ADDED
@@ -0,0 +1,560 @@
1
+ /********************************************************************************
2
+ * Copyright (C) 2023 CoCreate and Contributors.
3
+ *
4
+ * This program is free software: you can redistribute it and/or modify
5
+ * it under the terms of the GNU Affero General Public License as published
6
+ * by the Free Software Foundation, either version 3 of the License, or
7
+ * (at your option) any later version.
8
+ *
9
+ * This program is distributed in the hope that it will be useful,
10
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ * GNU Affero General Public License for more details.
13
+ *
14
+ * You should have received a copy of the GNU Affero General Public License
15
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
16
+ *
17
+ ********************************************************************************/
18
+
19
+ // Commercial Licensing Information:
20
+ // For commercial use of this software without the copyleft provisions of the AGPLv3,
21
+ // you must obtain a commercial license from CoCreate LLC.
22
+ // For details, visit <https://cocreate.app/licenses/> or contact us at sales@cocreate.app.
23
+
24
+ import fs from 'node:fs';
25
+ import os from 'node:os';
26
+ import Config from '@cocreate/config';
27
+ import { getValueFromObject } from '@cocreate/utils';
28
+
29
+
30
+ // ==============================================================================
31
+ // Module-Level Sandbox State (ESM Singleton Container)
32
+ // ==============================================================================
33
+ let server = null; // Reference to the main CoCreateServer instance
34
+ let activeIntervalId = null; // Storage for the core execution loop interval
35
+ let lastCpuInfo = null; // Cache for the previous CPU telemetry sample
36
+ let telemetryConfig = null; // In-memory reference to loaded configuration map
37
+ let dbBuffer = []; // In-memory array to accumulate raw telemetry ticks
38
+ let lastDbFlushTimestamp = Date.now(); // Record of last database persistence execution
39
+ let lastEmergencyAlertTimestamp = 0; // Throttling lock for high-priority OOM warnings
40
+
41
+ // Registry for dynamic telemetry pushed by external modules (e.g. server-mesh)
42
+ const customTelemetryRegistry = {};
43
+
44
+ // Active state snapshot populated on every poll tick
45
+ let currentTelemetry = {
46
+ cpu: { totalUsage: 0, cores: [] },
47
+ memory: { total: 0, available: 0, used: 0, percentage: 0 },
48
+ system: { loadavg: [0, 0, 0], uptime: 0 },
49
+ process: { uptime: 0, heapTotal: 0, heapUsed: 0, rss: 0, external: 0 },
50
+ timestamp: null
51
+ };
52
+
53
+ // Default configurations
54
+ const defaults = {
55
+ collectionInterval: 1000,
56
+ writeInterval: 60000,
57
+ retentionDays: 0, // Default set to 0 (infinite retention, never auto-delete)
58
+ broadcast: true
59
+ };
60
+
61
+
62
+ /**
63
+ * Reads memory allocations using a local-dev fallback if Linux /proc is unavailable.
64
+ * Supports cross-platform safety (macOS and Windows environments).
65
+ *
66
+ * @returns {Object} Telemetry payload containing hardware-allocated details
67
+ */
68
+ export function readMemoryInfo() {
69
+ try {
70
+ if (os.platform() !== 'linux') {
71
+ const totalMemoryBytes = os.totalmem();
72
+ const freeMemoryBytes = os.freemem();
73
+ const usedMemoryBytes = totalMemoryBytes - freeMemoryBytes;
74
+ const usedMemoryPercentage = (usedMemoryBytes / totalMemoryBytes) * 100;
75
+
76
+ return {
77
+ totalMemory: Math.round(totalMemoryBytes / 1024),
78
+ availableMemory: Math.round(freeMemoryBytes / 1024),
79
+ usedMemory: Math.round(usedMemoryBytes / 1024),
80
+ usedMemoryPercentage
81
+ };
82
+ }
83
+
84
+ const memInfoContent = fs.readFileSync('/proc/meminfo', 'utf8');
85
+ const memInfoLines = memInfoContent.split('\n');
86
+ const memInfo = memInfoLines.reduce((info, line) => {
87
+ const parts = line.split(':');
88
+ if (parts.length === 2) {
89
+ info[parts[0].trim()] = parts[1].trim();
90
+ }
91
+ return info;
92
+ }, {});
93
+
94
+ const totalMemory = parseInt(memInfo['MemTotal'], 10);
95
+ const availableMemory = parseInt(memInfo['MemAvailable'] || memInfo['MemFree'], 10);
96
+ const usedMemory = totalMemory - availableMemory;
97
+ const usedMemoryPercentage = (usedMemory / totalMemory) * 100;
98
+
99
+ return {
100
+ totalMemory,
101
+ availableMemory,
102
+ usedMemory,
103
+ usedMemoryPercentage
104
+ };
105
+ } catch (err) {
106
+ console.error('[@cocreate/server-telemetry] Error reading memory info:', err.message);
107
+ return { totalMemory: 0, availableMemory: 0, usedMemory: 0, usedMemoryPercentage: 0 };
108
+ }
109
+ }
110
+
111
+
112
+ /**
113
+ * Reads CPU performance ticks across cores natively or using fallback trackers.
114
+ *
115
+ * @returns {Array} List of CPU core tickers and time allocations
116
+ */
117
+ export function readCpuInfo() {
118
+ try {
119
+ if (os.platform() !== 'linux') {
120
+ const cpus = os.cpus();
121
+ return cpus.map((cpu, index) => {
122
+ const t = cpu.times;
123
+ return {
124
+ core: `cpu${index}`,
125
+ times: [t.user, t.nice, t.sys, t.idle, t.irq]
126
+ };
127
+ });
128
+ }
129
+
130
+ const cpuInfoContent = fs.readFileSync('/proc/stat', 'utf8');
131
+ const cpuLines = cpuInfoContent.split('\n');
132
+ return cpuLines
133
+ .filter(line => line.startsWith('cpu'))
134
+ .map(line => {
135
+ const parts = line.split(/\s+/).filter(Boolean);
136
+ return {
137
+ core: parts[0],
138
+ times: parts.slice(1).map(Number),
139
+ };
140
+ });
141
+ } catch (err) {
142
+ console.error('[@cocreate/server-telemetry] Error reading CPU ticks:', err.message);
143
+ return [];
144
+ }
145
+ }
146
+
147
+
148
+ /**
149
+ * Computes CPU core utilization percentages by comparing differential cycle ticks.
150
+ *
151
+ * @param {Array} cpuInfo1 - First historical CPU sample state
152
+ * @param {Array} cpuInfo2 - Newly captured secondary CPU sample state
153
+ * @returns {Array} Differential usage details per hardware core
154
+ */
155
+ export function calculateCpuUsage(cpuInfo1, cpuInfo2) {
156
+ if (!cpuInfo1 || !cpuInfo1.length || !cpuInfo2 || !cpuInfo2.length) return [];
157
+
158
+ return cpuInfo1.map((coreInfo1) => {
159
+ const coreInfo2 = cpuInfo2.find(c => c.core === coreInfo1.core);
160
+ if (!coreInfo2) return { core: coreInfo1.core, usage: 0 };
161
+
162
+ const idle1 = coreInfo1.times[3] || 0;
163
+ const idle2 = coreInfo2.times[3] || 0;
164
+
165
+ const total1 = coreInfo1.times.reduce((acc, val) => acc + val, 0);
166
+ const total2 = coreInfo2.times.reduce((acc, val) => acc + val, 0);
167
+
168
+ const totalDiff = total2 - total1;
169
+ const idleDiff = idle2 - idle1;
170
+
171
+ return {
172
+ core: coreInfo1.core,
173
+ usage: totalDiff > 0 ? Math.max(0, Math.min(100, (1 - idleDiff / totalDiff) * 100)) : 0,
174
+ };
175
+ });
176
+ }
177
+
178
+
179
+ /**
180
+ * Calculates a dynamic safety memory threshold percentage based on the physical core-to-memory ratio.
181
+ * This is symmetrically checked locally in server-telemetry.js to align the 1-second emergency loops.
182
+ *
183
+ * @returns {number} Calculated maximum safe memory percentage threshold (capped between 50% and 90%)
184
+ */
185
+ function calculateDynamicSafetyThreshold() {
186
+ const coresCount = os.cpus().length;
187
+ const totalMemoryMb = os.totalmem() / 1024 / 1024;
188
+
189
+ const allocationRatePerCoreMs = 15; // Estimated peak allocation speed in MB/sec under sustained load
190
+ const bootWindowSeconds = 60; // Boot delay window for new instances
191
+
192
+ const requiredHeadroomMb = coresCount * allocationRatePerCoreMs * bootWindowSeconds;
193
+ const headroomPercentage = (requiredHeadroomMb / totalMemoryMb) * 100;
194
+
195
+ const maxSafePercentage = Math.round(100 - headroomPercentage);
196
+
197
+ // Safety clamps: Prevent calculated values from getting too extreme
198
+ return Math.max(50, Math.min(90, maxSafePercentage));
199
+ }
200
+
201
+
202
+ /**
203
+ * Dynamic Telemetry Ingestion Protocol.
204
+ * Allows other platform modules (like server-mesh.js) to push specific performance
205
+ * stats directly to the core telemetry registry to be emitted and serialized.
206
+ *
207
+ * @param {string} component - Name of the reporting component (e.g., 'mesh')
208
+ * @param {Object} data - Key/Value pairs of custom tracking attributes
209
+ */
210
+ export function send(component, data) {
211
+ if (!component || typeof component !== 'string' || !data || typeof data !== 'object') {
212
+ return;
213
+ }
214
+ try {
215
+ customTelemetryRegistry[component] = {
216
+ ...customTelemetryRegistry[component],
217
+ ...data,
218
+ timestamp: new Date().toISOString()
219
+ };
220
+ } catch (err) {
221
+ console.error('[@cocreate/server-telemetry] Error capturing custom telemetry transmission:', err.message);
222
+ }
223
+ }
224
+
225
+
226
+ /**
227
+ * Core execution tick. Collects system specs, appends external custom registries,
228
+ * and commits rolls to database buffer limits. Excludes event-emitter or callback leaks.
229
+ */
230
+ export function read() {
231
+ const memory = readMemoryInfo();
232
+ const currentCpuInfo = readCpuInfo();
233
+
234
+ let cpuTotalUsage = 0;
235
+ let coresUsage = [];
236
+
237
+ if (lastCpuInfo) {
238
+ coresUsage = calculateCpuUsage(lastCpuInfo, currentCpuInfo);
239
+ if (coresUsage.length > 0) {
240
+ const detailedCores = coresUsage.filter(c => c.core !== 'cpu');
241
+ const targetSet = detailedCores.length > 0 ? detailedCores : coresUsage;
242
+
243
+ const totalSum = targetSet.reduce((sum, core) => sum + core.usage, 0);
244
+ cpuTotalUsage = totalSum / targetSet.length;
245
+ }
246
+ }
247
+
248
+ lastCpuInfo = currentCpuInfo;
249
+
250
+ const processMemory = process.memoryUsage();
251
+
252
+ // Populate active server-telemetry payload
253
+ currentTelemetry = {
254
+ cpu: {
255
+ totalUsage: parseFloat(cpuTotalUsage.toFixed(2)),
256
+ cores: coresUsage
257
+ },
258
+ memory: {
259
+ total: memory.totalMemory,
260
+ available: memory.availableMemory,
261
+ used: memory.usedMemory,
262
+ percentage: parseFloat(memory.usedMemoryPercentage.toFixed(2))
263
+ },
264
+ system: {
265
+ loadavg: os.loadavg(),
266
+ uptime: Math.round(os.uptime())
267
+ },
268
+ process: {
269
+ uptime: Math.round(process.uptime()),
270
+ heapTotal: Math.round(processMemory.heapTotal / 1024),
271
+ heapUsed: Math.round(processMemory.heapUsed / 1024),
272
+ rss: Math.round(processMemory.rss / 1024),
273
+ external: Math.round(processMemory.external / 1024)
274
+ },
275
+ // Symmetrically merge custom component blocks directly into the heartbeat payload
276
+ ...customTelemetryRegistry,
277
+ timestamp: new Date().toISOString()
278
+ };
279
+
280
+ dbBuffer.push(JSON.parse(JSON.stringify(currentTelemetry)));
281
+
282
+ // --- INSTANTANEOUS 1-SECOND OOM PROTECTION CHECK ---
283
+ // If memory spikes above the threshold on any individual 1-second sample,
284
+ // fire an immediate emergency signal to the Master process.
285
+ let targetMemoryThreshold = telemetryConfig
286
+ ? parseInt(getValueFromObject(telemetryConfig, 'server-telemetry.scaleUpMemoryThreshold'), 10)
287
+ : 85;
288
+
289
+ // Apply Dynamic Safety limits to 1s emergency checks if enabled (on by default)
290
+ const isSafetyLimitsEnabled = getValueFromObject(telemetryConfig, 'server-telemetry.safetyLimits') !== false && getValueFromObject(telemetryConfig, 'server-telemetry.safetyLimits') !== 'false';
291
+
292
+ if (isSafetyLimitsEnabled) {
293
+ const dynamicSafetyThreshold = calculateDynamicSafetyThreshold();
294
+ if (targetMemoryThreshold > dynamicSafetyThreshold) {
295
+ targetMemoryThreshold = dynamicSafetyThreshold;
296
+ }
297
+ }
298
+
299
+ if (currentTelemetry.memory.percentage >= targetMemoryThreshold) {
300
+ const emergencyCooldown = 300000; // Throttle to maximum 1 emergency signal every 5 minutes per worker node
301
+ if (Date.now() - lastEmergencyAlertTimestamp >= emergencyCooldown) {
302
+ lastEmergencyAlertTimestamp = Date.now();
303
+ console.warn(`[@cocreate/server-telemetry] [EMERGENCY] Memory threshold breached on 1s tick (${currentTelemetry.memory.percentage}% >= ${targetMemoryThreshold}%). Dispatching direct system-up scaling alarm!`);
304
+
305
+ server.send({
306
+ method: 'telemetry.emergency',
307
+ data: currentTelemetry
308
+ });
309
+ }
310
+ }
311
+
312
+ // Dynamic Time-Staggering Check:
313
+ // All workers write their own process stats, but their flush intervals are staggered
314
+ // using a round-robin calculation to prevent massive write collisions.
315
+ const customWriteInterval = telemetryConfig
316
+ ? parseInt(getValueFromObject(telemetryConfig, 'server-telemetry.writeInterval'), 10)
317
+ : defaults.writeInterval;
318
+
319
+ const writeInterval = isNaN(customWriteInterval) ? defaults.writeInterval : customWriteInterval;
320
+
321
+ if (Date.now() - lastDbFlushTimestamp >= writeInterval) {
322
+ flushTelemetryToDatabase().catch(err => {
323
+ console.error('[@cocreate/server-telemetry] Failed to flush staggered DB telemetry:', err);
324
+ });
325
+ }
326
+
327
+ return currentTelemetry;
328
+ }
329
+
330
+
331
+ /**
332
+ * Processes buffered raw tick records to calculate minimum, maximum, and average values.
333
+ * Writes aggregated results as a single document utilizing standard server.crud structures.
334
+ * Also emits the statistical telemetry directly to the IPC loop using fail-fast server.send().
335
+ * Cleans up outdated database telemetry if retentionDays is configured.
336
+ */
337
+ export async function flushTelemetryToDatabase() {
338
+ if (dbBuffer.length === 0) {
339
+ lastDbFlushTimestamp = Date.now();
340
+ return;
341
+ }
342
+
343
+ try {
344
+ const totalSamples = dbBuffer.length;
345
+
346
+ const cpuUsages = dbBuffer.map(sample => sample.cpu.totalUsage);
347
+ const memUsages = dbBuffer.map(sample => sample.memory.percentage);
348
+ const load1m = dbBuffer.map(sample => sample.system.loadavg[0]);
349
+ const heapUsed = dbBuffer.map(sample => sample.process.heapUsed);
350
+
351
+ // Compute statistical aggregates
352
+ const cpuMin = Math.min(...cpuUsages);
353
+ const cpuMax = Math.max(...cpuUsages);
354
+ const cpuAvg = cpuUsages.reduce((sum, val) => sum + val, 0) / totalSamples;
355
+
356
+ const memMin = Math.min(...memUsages);
357
+ const memMax = Math.max(...memUsages);
358
+ const memAvg = memUsages.reduce((sum, val) => sum + val, 0) / totalSamples;
359
+
360
+ const loadAvg1m = load1m.reduce((sum, val) => sum + val, 0) / totalSamples;
361
+ const maxHeap = Math.max(...heapUsed);
362
+
363
+ const lastSample = dbBuffer[totalSamples - 1];
364
+
365
+ // Gather any custom component keys injected during the current buffer window
366
+ const componentAggregates = {};
367
+ for (const key of Object.keys(customTelemetryRegistry)) {
368
+ componentAggregates[key] = lastSample[key] || null;
369
+ }
370
+
371
+ const workerId = server.workerId;
372
+ const totalWorkers = server.totalWorkers;
373
+
374
+ const aggregatedPayload = {
375
+ component: 'server-telemetry',
376
+ serverId: server.id,
377
+ workerId: workerId,
378
+ totalWorkers: totalWorkers,
379
+ timestamp: new Date().toISOString(),
380
+ interval_seconds: Math.round((Date.now() - lastDbFlushTimestamp) / 1000),
381
+ samples_collected: totalSamples,
382
+ cpu: {
383
+ min: parseFloat(cpuMin.toFixed(2)),
384
+ max: parseFloat(cpuMax.toFixed(2)),
385
+ avg: parseFloat(cpuAvg.toFixed(2))
386
+ },
387
+ memory: {
388
+ total_kb: lastSample.memory.total,
389
+ min_used_pct: parseFloat(memMin.toFixed(2)),
390
+ max_used_pct: parseFloat(memMax.toFixed(2)),
391
+ avg_used_pct: parseFloat(memAvg.toFixed(2))
392
+ },
393
+ system: {
394
+ loadavg_1m_avg: parseFloat(loadAvg1m.toFixed(2)),
395
+ os_uptime: lastSample.system.uptime
396
+ },
397
+ process: {
398
+ worker_uptime: lastSample.process.uptime,
399
+ max_heap_kb: maxHeap
400
+ },
401
+ // Save dynamic custom telemetry blocks
402
+ custom: componentAggregates
403
+ };
404
+
405
+ // Clear local buffer immediately to avoid execution loop overlap
406
+ dbBuffer = [];
407
+ lastDbFlushTimestamp = Date.now();
408
+
409
+ // 1. DISPATCH LIVE EVENT VIA SERVER SEND (FAIL-FAST)
410
+ // Directly fire the 'telemetry' method upstream.
411
+ // It should route cleanly or throw immediately.
412
+ server.send({
413
+ method: 'telemetry',
414
+ data: aggregatedPayload
415
+ });
416
+
417
+ // 2. WRITE HISTORICAL RECORD TO DATABASE
418
+ // Target standard CoCreate server storage configuration using server.crud directly
419
+ const dbResponse = await server.crud.send({
420
+ method: 'object.create',
421
+ array: 'system_telemetry',
422
+ object: aggregatedPayload,
423
+ organization_id: server.organization_id,
424
+ host: server.host
425
+ });
426
+
427
+ if (dbResponse && dbResponse.error) {
428
+ console.error('[@cocreate/server-telemetry] Database telemetry registration rejected:', dbResponse.error);
429
+ } else if (dbResponse) {
430
+ const broadcastConfig = telemetryConfig
431
+ ? getValueFromObject(telemetryConfig, 'server-telemetry.broadcast')
432
+ : defaults.broadcast;
433
+
434
+ const isBroadcastEnabled = broadcastConfig !== false && broadcastConfig !== 'false';
435
+
436
+ if (isBroadcastEnabled) {
437
+ // Inject broadcast parameters onto the validated response object
438
+ dbResponse.broadcast = broadcastConfig;
439
+ server.wsManager.send(dbResponse);
440
+ }
441
+ }
442
+
443
+ // Clean up outdated telemetry logs if configured
444
+ const configuredRetention = telemetryConfig
445
+ ? parseInt(getValueFromObject(telemetryConfig, 'server-telemetry.retentionDays'), 10)
446
+ : defaults.retentionDays;
447
+
448
+ const retentionDays = isNaN(configuredRetention) ? defaults.retentionDays : configuredRetention;
449
+
450
+ // Only Worker 1 handles retention pruning to prevent lock disputes
451
+ if (retentionDays > 0 && workerId === 1) {
452
+ const cutoffDate = new Date();
453
+ cutoffDate.setDate(cutoffDate.getDate() - retentionDays);
454
+
455
+ await server.crud.send({
456
+ method: 'object.delete',
457
+ array: 'system_telemetry',
458
+ object: {
459
+ timestamp: { $lt: cutoffDate.toISOString() }
460
+ },
461
+ organization_id: server.organization_id,
462
+ host: server.host
463
+ });
464
+ }
465
+
466
+ } catch (error) {
467
+ console.error('[@cocreate/server-telemetry] Error executing database flush:', error.message);
468
+ }
469
+ }
470
+
471
+
472
+ /**
473
+ * Returns the cached memory and system snapshot compiled during the last execution sweep.
474
+ *
475
+ * @returns {Object} Active system performance telemetry snapshot
476
+ */
477
+ export function get() {
478
+ return currentTelemetry;
479
+ }
480
+
481
+ /**
482
+ * Integrates server-telemetry capabilities, loads configuration, and schedules staggered execution threads.
483
+ * Returns the functional API contract directly instead of mutating the parent server context.
484
+ *
485
+ * @param {Object} serverCtx - The core instantiated server environment
486
+ * @returns {Object} Functional API contract for the server-telemetry subsystem
487
+ */
488
+ export async function init(serverCtx) {
489
+ server = serverCtx;
490
+
491
+ // Define functional API contract
492
+ const telemetryAPI = {
493
+ init,
494
+ stop,
495
+ send,
496
+ get,
497
+ read,
498
+ readCpuInfo,
499
+ readMemoryInfo,
500
+ calculateCpuUsage,
501
+ flushTelemetryToDatabase
502
+ };
503
+
504
+ // Load nested telemetry parameters matching standard configuration queries
505
+ telemetryConfig = await Config({
506
+ 'server-telemetry': ''
507
+ });
508
+
509
+ const telemetryEnabled = getValueFromObject(telemetryConfig, 'server-telemetry.enabled') !== false && getValueFromObject(telemetryConfig, 'server-telemetry.enabled') !== 'false';
510
+
511
+ if (!telemetryEnabled) {
512
+ console.log('[@cocreate/server-telemetry] Monitoring is disabled by configuration.');
513
+ return telemetryAPI;
514
+ }
515
+
516
+ const collectionInterval = parseInt(getValueFromObject(telemetryConfig, 'server-telemetry.collectionInterval'), 10) || defaults.collectionInterval;
517
+ const customWriteInterval = parseInt(getValueFromObject(telemetryConfig, 'server-telemetry.writeInterval'), 10) || defaults.writeInterval;
518
+
519
+ const workerId = server.workerId;
520
+ const totalWorkers = server.totalWorkers;
521
+
522
+ // Staggered launch delays to prevent resource spikes during multi-worker server boots
523
+ const staggeredDelay = (workerId * collectionInterval) + (totalWorkers * collectionInterval);
524
+
525
+ stop();
526
+
527
+ lastCpuInfo = readCpuInfo();
528
+
529
+ // Mathematically offset the first write target so that worker logging phases
530
+ // are perfectly staggered across the write window, avoiding write overlaps!
531
+ const phaseOffset = ((workerId - 1) / totalWorkers) * customWriteInterval;
532
+ lastDbFlushTimestamp = Date.now() - phaseOffset;
533
+
534
+ dbBuffer = [];
535
+
536
+ console.log(`[@cocreate/server-telemetry] Starting telemetry monitoring on Worker ${process.pid} (Worker ID: ${workerId}/${totalWorkers}, Write Phase Offset: ${phaseOffset}ms)`);
537
+ activeIntervalId = setInterval(read, collectionInterval);
538
+
539
+ return telemetryAPI;
540
+ }
541
+
542
+ export function stop() {
543
+ if (activeIntervalId) {
544
+ clearInterval(activeIntervalId);
545
+ activeIntervalId = null;
546
+ }
547
+ }
548
+
549
+ // Default export matches standard functional API entry contract
550
+ export default {
551
+ init,
552
+ stop,
553
+ send,
554
+ get,
555
+ read,
556
+ readCpuInfo,
557
+ readMemoryInfo,
558
+ calculateCpuUsage,
559
+ flushTelemetryToDatabase
560
+ };