@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/.github/FUNDING.yml +3 -0
- package/.github/workflows/automated.yml +56 -0
- package/CHANGELOG.md +796 -0
- package/CONTRIBUTING.md +96 -0
- package/CoCreate.config.js +23 -0
- package/LICENSE +683 -0
- package/README.md +65 -0
- package/docs/index.html +217 -0
- package/package.json +34 -0
- package/prettier.config.js +16 -0
- package/release.config.js +30 -0
- package/src/index.js +560 -0
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
|
+
};
|