@ceon-oy/monitor-sdk 1.2.1 → 1.3.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/dist/index.d.mts +61 -1
- package/dist/index.d.ts +61 -1
- package/dist/index.js +230 -17
- package/dist/index.mjs +230 -17
- package/package.json +1 -1
package/dist/index.d.mts
CHANGED
|
@@ -53,7 +53,7 @@ interface MonitorClientConfig {
|
|
|
53
53
|
allowInsecureHttp?: boolean;
|
|
54
54
|
/** Custom npm registry URL for fetching latest versions (default: https://registry.npmjs.org) */
|
|
55
55
|
npmRegistryUrl?: string;
|
|
56
|
-
/** Enable SDK-based health check polling (default:
|
|
56
|
+
/** Enable SDK-based health check polling (default: true) */
|
|
57
57
|
healthCheckEnabled?: boolean;
|
|
58
58
|
/** Interval to fetch health endpoints from server in ms (default: 60000) */
|
|
59
59
|
healthCheckFetchIntervalMs?: number;
|
|
@@ -226,6 +226,11 @@ declare class MonitorClient {
|
|
|
226
226
|
private healthCheckTimers;
|
|
227
227
|
private healthCheckResultsQueue;
|
|
228
228
|
private healthCheckFlushTimer;
|
|
229
|
+
private sdkErrorQueue;
|
|
230
|
+
private sdkErrorFlushTimer;
|
|
231
|
+
private sdkErrorsInCurrentWindow;
|
|
232
|
+
private sdkErrorWindowResetTimer;
|
|
233
|
+
private metricsCollectionTimer;
|
|
229
234
|
constructor(config: MonitorClientConfig);
|
|
230
235
|
/**
|
|
231
236
|
* Security: Validate and sanitize metadata to prevent oversized payloads
|
|
@@ -253,6 +258,15 @@ declare class MonitorClient {
|
|
|
253
258
|
flush(): Promise<void>;
|
|
254
259
|
private getErrorKey;
|
|
255
260
|
close(): Promise<void>;
|
|
261
|
+
/**
|
|
262
|
+
* Queue an SDK error to be reported to the server's system errors page.
|
|
263
|
+
* Fire-and-forget — never throws. If reporting itself fails, logs to console only.
|
|
264
|
+
* Throttled to max 20 errors per minute to prevent flooding.
|
|
265
|
+
*/
|
|
266
|
+
private reportError;
|
|
267
|
+
private startSdkErrorFlushTimer;
|
|
268
|
+
private stopSdkErrorFlushTimer;
|
|
269
|
+
private flushSdkErrors;
|
|
256
270
|
private stopAuditIntervalTimer;
|
|
257
271
|
/**
|
|
258
272
|
* Fetch project settings from the monitoring server.
|
|
@@ -263,6 +277,12 @@ declare class MonitorClient {
|
|
|
263
277
|
vulnerabilityScanIntervalHours: number;
|
|
264
278
|
scanRequestedAt: string | null;
|
|
265
279
|
techScanRequestedAt: string | null;
|
|
280
|
+
metricsEnabled?: boolean;
|
|
281
|
+
metricsIntervalSeconds?: number;
|
|
282
|
+
metricsDiskPaths?: string[];
|
|
283
|
+
metricsCpuThreshold?: number;
|
|
284
|
+
metricsRamThreshold?: number;
|
|
285
|
+
metricsDiskThreshold?: number;
|
|
266
286
|
} | null>;
|
|
267
287
|
/**
|
|
268
288
|
* Setup automatic vulnerability scanning based on server-configured interval.
|
|
@@ -465,6 +485,46 @@ declare class MonitorClient {
|
|
|
465
485
|
* Stop all health check timers
|
|
466
486
|
*/
|
|
467
487
|
private stopHealthCheckTimers;
|
|
488
|
+
/**
|
|
489
|
+
* Start system metrics collection based on server-configured settings.
|
|
490
|
+
* Only runs if metricsEnabled is true in project settings.
|
|
491
|
+
* Collects CPU, RAM, and disk usage using built-in Node.js modules.
|
|
492
|
+
*/
|
|
493
|
+
setupSystemMetricsCollection(): Promise<void>;
|
|
494
|
+
private stopMetricsCollection;
|
|
495
|
+
/**
|
|
496
|
+
* Collect system metrics (CPU, RAM, disk) using built-in Node.js modules
|
|
497
|
+
* and submit to the monitoring server.
|
|
498
|
+
*
|
|
499
|
+
* Uses: os.cpus(), os.totalmem(), os.freemem(), os.hostname(), fs.statfs()
|
|
500
|
+
* Zero dependencies — all built-in Node.js 18.15+
|
|
501
|
+
*/
|
|
502
|
+
private collectAndSubmitMetrics;
|
|
503
|
+
/**
|
|
504
|
+
* Measure CPU utilization by sampling cpus() twice with a 500ms gap.
|
|
505
|
+
* Returns a percentage (0–100).
|
|
506
|
+
*/
|
|
507
|
+
private measureCpuPercent;
|
|
508
|
+
/**
|
|
509
|
+
* Collect disk usage for each configured path using fs.statfs() (Node 18.15+).
|
|
510
|
+
*/
|
|
511
|
+
private collectDiskMetrics;
|
|
512
|
+
/**
|
|
513
|
+
* Submit a system metric snapshot to the monitoring server.
|
|
514
|
+
*/
|
|
515
|
+
submitSystemMetric(metric: {
|
|
516
|
+
hostname: string;
|
|
517
|
+
cpuPercent: number;
|
|
518
|
+
memoryTotal: number;
|
|
519
|
+
memoryUsed: number;
|
|
520
|
+
memoryPercent: number;
|
|
521
|
+
disks: Array<{
|
|
522
|
+
path: string;
|
|
523
|
+
total: number;
|
|
524
|
+
used: number;
|
|
525
|
+
percent: number;
|
|
526
|
+
}>;
|
|
527
|
+
}): Promise<void>;
|
|
468
528
|
}
|
|
469
529
|
|
|
470
530
|
export { type AuditPath, type AuditResult, type AuditSummary, type BruteForceDetectionResult, type DependencySource, type ErrorContext, type ErrorPayload, type HealthStatus, MonitorClient, type MonitorClientConfig, type MultiAuditSummary, type SdkHealthEndpoint, type SdkHealthEndpointsResponse, type SdkHealthResult, type SdkHealthResultsResponse, type SecurityCategory, type SecurityEventInput, type SecurityEventPayload, type SecuritySeverity, type Severity, type TechnologyItem, type TechnologyType, type VulnerabilityItem, type VulnerabilitySeverity };
|
package/dist/index.d.ts
CHANGED
|
@@ -53,7 +53,7 @@ interface MonitorClientConfig {
|
|
|
53
53
|
allowInsecureHttp?: boolean;
|
|
54
54
|
/** Custom npm registry URL for fetching latest versions (default: https://registry.npmjs.org) */
|
|
55
55
|
npmRegistryUrl?: string;
|
|
56
|
-
/** Enable SDK-based health check polling (default:
|
|
56
|
+
/** Enable SDK-based health check polling (default: true) */
|
|
57
57
|
healthCheckEnabled?: boolean;
|
|
58
58
|
/** Interval to fetch health endpoints from server in ms (default: 60000) */
|
|
59
59
|
healthCheckFetchIntervalMs?: number;
|
|
@@ -226,6 +226,11 @@ declare class MonitorClient {
|
|
|
226
226
|
private healthCheckTimers;
|
|
227
227
|
private healthCheckResultsQueue;
|
|
228
228
|
private healthCheckFlushTimer;
|
|
229
|
+
private sdkErrorQueue;
|
|
230
|
+
private sdkErrorFlushTimer;
|
|
231
|
+
private sdkErrorsInCurrentWindow;
|
|
232
|
+
private sdkErrorWindowResetTimer;
|
|
233
|
+
private metricsCollectionTimer;
|
|
229
234
|
constructor(config: MonitorClientConfig);
|
|
230
235
|
/**
|
|
231
236
|
* Security: Validate and sanitize metadata to prevent oversized payloads
|
|
@@ -253,6 +258,15 @@ declare class MonitorClient {
|
|
|
253
258
|
flush(): Promise<void>;
|
|
254
259
|
private getErrorKey;
|
|
255
260
|
close(): Promise<void>;
|
|
261
|
+
/**
|
|
262
|
+
* Queue an SDK error to be reported to the server's system errors page.
|
|
263
|
+
* Fire-and-forget — never throws. If reporting itself fails, logs to console only.
|
|
264
|
+
* Throttled to max 20 errors per minute to prevent flooding.
|
|
265
|
+
*/
|
|
266
|
+
private reportError;
|
|
267
|
+
private startSdkErrorFlushTimer;
|
|
268
|
+
private stopSdkErrorFlushTimer;
|
|
269
|
+
private flushSdkErrors;
|
|
256
270
|
private stopAuditIntervalTimer;
|
|
257
271
|
/**
|
|
258
272
|
* Fetch project settings from the monitoring server.
|
|
@@ -263,6 +277,12 @@ declare class MonitorClient {
|
|
|
263
277
|
vulnerabilityScanIntervalHours: number;
|
|
264
278
|
scanRequestedAt: string | null;
|
|
265
279
|
techScanRequestedAt: string | null;
|
|
280
|
+
metricsEnabled?: boolean;
|
|
281
|
+
metricsIntervalSeconds?: number;
|
|
282
|
+
metricsDiskPaths?: string[];
|
|
283
|
+
metricsCpuThreshold?: number;
|
|
284
|
+
metricsRamThreshold?: number;
|
|
285
|
+
metricsDiskThreshold?: number;
|
|
266
286
|
} | null>;
|
|
267
287
|
/**
|
|
268
288
|
* Setup automatic vulnerability scanning based on server-configured interval.
|
|
@@ -465,6 +485,46 @@ declare class MonitorClient {
|
|
|
465
485
|
* Stop all health check timers
|
|
466
486
|
*/
|
|
467
487
|
private stopHealthCheckTimers;
|
|
488
|
+
/**
|
|
489
|
+
* Start system metrics collection based on server-configured settings.
|
|
490
|
+
* Only runs if metricsEnabled is true in project settings.
|
|
491
|
+
* Collects CPU, RAM, and disk usage using built-in Node.js modules.
|
|
492
|
+
*/
|
|
493
|
+
setupSystemMetricsCollection(): Promise<void>;
|
|
494
|
+
private stopMetricsCollection;
|
|
495
|
+
/**
|
|
496
|
+
* Collect system metrics (CPU, RAM, disk) using built-in Node.js modules
|
|
497
|
+
* and submit to the monitoring server.
|
|
498
|
+
*
|
|
499
|
+
* Uses: os.cpus(), os.totalmem(), os.freemem(), os.hostname(), fs.statfs()
|
|
500
|
+
* Zero dependencies — all built-in Node.js 18.15+
|
|
501
|
+
*/
|
|
502
|
+
private collectAndSubmitMetrics;
|
|
503
|
+
/**
|
|
504
|
+
* Measure CPU utilization by sampling cpus() twice with a 500ms gap.
|
|
505
|
+
* Returns a percentage (0–100).
|
|
506
|
+
*/
|
|
507
|
+
private measureCpuPercent;
|
|
508
|
+
/**
|
|
509
|
+
* Collect disk usage for each configured path using fs.statfs() (Node 18.15+).
|
|
510
|
+
*/
|
|
511
|
+
private collectDiskMetrics;
|
|
512
|
+
/**
|
|
513
|
+
* Submit a system metric snapshot to the monitoring server.
|
|
514
|
+
*/
|
|
515
|
+
submitSystemMetric(metric: {
|
|
516
|
+
hostname: string;
|
|
517
|
+
cpuPercent: number;
|
|
518
|
+
memoryTotal: number;
|
|
519
|
+
memoryUsed: number;
|
|
520
|
+
memoryPercent: number;
|
|
521
|
+
disks: Array<{
|
|
522
|
+
path: string;
|
|
523
|
+
total: number;
|
|
524
|
+
used: number;
|
|
525
|
+
percent: number;
|
|
526
|
+
}>;
|
|
527
|
+
}): Promise<void>;
|
|
468
528
|
}
|
|
469
529
|
|
|
470
530
|
export { type AuditPath, type AuditResult, type AuditSummary, type BruteForceDetectionResult, type DependencySource, type ErrorContext, type ErrorPayload, type HealthStatus, MonitorClient, type MonitorClientConfig, type MultiAuditSummary, type SdkHealthEndpoint, type SdkHealthEndpointsResponse, type SdkHealthResult, type SdkHealthResultsResponse, type SecurityCategory, type SecurityEventInput, type SecurityEventPayload, type SecuritySeverity, type Severity, type TechnologyItem, type TechnologyType, type VulnerabilityItem, type VulnerabilitySeverity };
|
package/dist/index.js
CHANGED
|
@@ -94,6 +94,13 @@ var MonitorClient = class {
|
|
|
94
94
|
this.healthCheckTimers = /* @__PURE__ */ new Map();
|
|
95
95
|
this.healthCheckResultsQueue = [];
|
|
96
96
|
this.healthCheckFlushTimer = null;
|
|
97
|
+
// SDK error reporting queue (errors visible on admin system errors page)
|
|
98
|
+
this.sdkErrorQueue = [];
|
|
99
|
+
this.sdkErrorFlushTimer = null;
|
|
100
|
+
this.sdkErrorsInCurrentWindow = 0;
|
|
101
|
+
this.sdkErrorWindowResetTimer = null;
|
|
102
|
+
// System metrics collection (on-premise only, opt-in)
|
|
103
|
+
this.metricsCollectionTimer = null;
|
|
97
104
|
if (!config.apiKey || config.apiKey.trim().length === 0) {
|
|
98
105
|
throw new Error("[MonitorClient] API key is required");
|
|
99
106
|
}
|
|
@@ -153,24 +160,28 @@ var MonitorClient = class {
|
|
|
153
160
|
this.auditTimeoutMs = Math.min(CONFIG_LIMITS.MAX_AUDIT_TIMEOUT_MS, Math.max(1e3, config.auditTimeoutMs || CONFIG_LIMITS.AUDIT_TIMEOUT_MS));
|
|
154
161
|
this.registryTimeoutMs = Math.min(CONFIG_LIMITS.MAX_REGISTRY_TIMEOUT_MS, Math.max(1e3, config.registryTimeoutMs || CONFIG_LIMITS.REGISTRY_TIMEOUT_MS));
|
|
155
162
|
this.npmRegistryUrl = (config.npmRegistryUrl || "https://registry.npmjs.org").replace(/\/$/, "");
|
|
156
|
-
this.healthCheckEnabled = config.healthCheckEnabled
|
|
163
|
+
this.healthCheckEnabled = config.healthCheckEnabled ?? true;
|
|
157
164
|
this.healthCheckFetchIntervalMs = config.healthCheckFetchIntervalMs || CONFIG_LIMITS.HEALTH_CHECK_FETCH_INTERVAL_MS;
|
|
158
165
|
this.startFlushTimer();
|
|
159
166
|
if (this.trackDependencies) {
|
|
160
167
|
this.syncDependencies().catch((err) => {
|
|
161
|
-
|
|
168
|
+
this.reportError("DEPENDENCY_SYNC", "Failed to sync dependencies on startup", err);
|
|
162
169
|
});
|
|
163
170
|
}
|
|
164
171
|
if (this.autoAudit) {
|
|
165
172
|
this.setupAutoAudit().catch((err) => {
|
|
166
|
-
|
|
173
|
+
this.reportError("AUDIT_SETUP", "Failed to setup auto audit", err);
|
|
167
174
|
});
|
|
168
175
|
}
|
|
169
176
|
if (this.healthCheckEnabled) {
|
|
170
177
|
this.setupHealthCheckPolling().catch((err) => {
|
|
171
|
-
|
|
178
|
+
this.reportError("HEALTH_POLLING_SETUP", "Failed to setup health check polling", err);
|
|
172
179
|
});
|
|
173
180
|
}
|
|
181
|
+
this.startSdkErrorFlushTimer();
|
|
182
|
+
this.setupSystemMetricsCollection().catch((err) => {
|
|
183
|
+
this.reportError("SYSTEM_METRICS_SETUP", "Failed to setup system metrics collection", err);
|
|
184
|
+
});
|
|
174
185
|
}
|
|
175
186
|
/**
|
|
176
187
|
* Security: Validate and sanitize metadata to prevent oversized payloads
|
|
@@ -320,8 +331,66 @@ var MonitorClient = class {
|
|
|
320
331
|
this.stopFlushTimer();
|
|
321
332
|
this.stopAuditIntervalTimer();
|
|
322
333
|
this.stopHealthCheckTimers();
|
|
334
|
+
this.stopSdkErrorFlushTimer();
|
|
335
|
+
this.stopMetricsCollection();
|
|
323
336
|
await this.flush();
|
|
324
337
|
await this.flushHealthResults();
|
|
338
|
+
await this.flushSdkErrors();
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* Queue an SDK error to be reported to the server's system errors page.
|
|
342
|
+
* Fire-and-forget — never throws. If reporting itself fails, logs to console only.
|
|
343
|
+
* Throttled to max 20 errors per minute to prevent flooding.
|
|
344
|
+
*/
|
|
345
|
+
reportError(category, message, err) {
|
|
346
|
+
if (this.sdkErrorsInCurrentWindow >= 20) return;
|
|
347
|
+
this.sdkErrorsInCurrentWindow++;
|
|
348
|
+
const errorMessage = err instanceof Error ? `${message}: ${err.message}` : message;
|
|
349
|
+
const stack = err instanceof Error ? err.stack : void 0;
|
|
350
|
+
this.sdkErrorQueue.push({ category, message: errorMessage, stack });
|
|
351
|
+
if (this.sdkErrorQueue.length >= 20) {
|
|
352
|
+
this.flushSdkErrors().catch(() => {
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
startSdkErrorFlushTimer() {
|
|
357
|
+
this.sdkErrorFlushTimer = setInterval(() => {
|
|
358
|
+
this.flushSdkErrors().catch(() => {
|
|
359
|
+
});
|
|
360
|
+
}, 3e4);
|
|
361
|
+
this.sdkErrorWindowResetTimer = setInterval(() => {
|
|
362
|
+
this.sdkErrorsInCurrentWindow = 0;
|
|
363
|
+
}, 6e4);
|
|
364
|
+
}
|
|
365
|
+
stopSdkErrorFlushTimer() {
|
|
366
|
+
if (this.sdkErrorFlushTimer) {
|
|
367
|
+
clearInterval(this.sdkErrorFlushTimer);
|
|
368
|
+
this.sdkErrorFlushTimer = null;
|
|
369
|
+
}
|
|
370
|
+
if (this.sdkErrorWindowResetTimer) {
|
|
371
|
+
clearInterval(this.sdkErrorWindowResetTimer);
|
|
372
|
+
this.sdkErrorWindowResetTimer = null;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
async flushSdkErrors() {
|
|
376
|
+
if (this.sdkErrorQueue.length === 0) return;
|
|
377
|
+
const errors = [...this.sdkErrorQueue];
|
|
378
|
+
this.sdkErrorQueue = [];
|
|
379
|
+
try {
|
|
380
|
+
const response = await this.fetchWithTimeout(`${this.endpoint}/api/v1/sdk-errors`, {
|
|
381
|
+
method: "POST",
|
|
382
|
+
headers: {
|
|
383
|
+
"Content-Type": "application/json",
|
|
384
|
+
"Authorization": `Bearer ${this.apiKey}`
|
|
385
|
+
},
|
|
386
|
+
body: JSON.stringify({ errors })
|
|
387
|
+
});
|
|
388
|
+
if (!response.ok) {
|
|
389
|
+
console.warn(`[MonitorClient] Failed to report SDK errors: HTTP ${response.status}`);
|
|
390
|
+
}
|
|
391
|
+
} catch (err) {
|
|
392
|
+
console.warn("[MonitorClient] Failed to flush SDK errors to server:", err instanceof Error ? err.message : String(err));
|
|
393
|
+
}
|
|
325
394
|
}
|
|
326
395
|
stopAuditIntervalTimer() {
|
|
327
396
|
if (this.auditIntervalTimer) {
|
|
@@ -383,14 +452,14 @@ var MonitorClient = class {
|
|
|
383
452
|
const intervalMs = intervalHours * 60 * 60 * 1e3;
|
|
384
453
|
this.auditIntervalTimer = setInterval(() => {
|
|
385
454
|
this.runScanAndTrackTime().catch((err) => {
|
|
386
|
-
|
|
455
|
+
this.reportError("AUDIT_SCAN", "Auto audit scan failed", err);
|
|
387
456
|
});
|
|
388
457
|
}, intervalMs);
|
|
389
458
|
}
|
|
390
459
|
console.log("[MonitorClient] Polling for scan requests enabled (every 5 minutes)");
|
|
391
460
|
this.settingsPollingTimer = setInterval(() => {
|
|
392
461
|
this.checkForScanRequest().catch((err) => {
|
|
393
|
-
|
|
462
|
+
this.reportError("SETTINGS_POLL", "Scan request check failed", err);
|
|
394
463
|
});
|
|
395
464
|
}, CONFIG_LIMITS.SETTINGS_POLL_INTERVAL_MS);
|
|
396
465
|
}
|
|
@@ -411,7 +480,7 @@ var MonitorClient = class {
|
|
|
411
480
|
const duration = Date.now() - startTime;
|
|
412
481
|
console.log(`[MonitorClient] Vulnerability scan completed in ${duration}ms`);
|
|
413
482
|
} catch (err) {
|
|
414
|
-
|
|
483
|
+
this.reportError("VULNERABILITY_SCAN", "Vulnerability scan failed", err);
|
|
415
484
|
}
|
|
416
485
|
}
|
|
417
486
|
/**
|
|
@@ -438,7 +507,7 @@ var MonitorClient = class {
|
|
|
438
507
|
}
|
|
439
508
|
}
|
|
440
509
|
} catch (err) {
|
|
441
|
-
|
|
510
|
+
this.reportError("SETTINGS_POLL", "Failed to check for scan request", err);
|
|
442
511
|
}
|
|
443
512
|
}
|
|
444
513
|
enqueue(payload) {
|
|
@@ -525,7 +594,7 @@ var MonitorClient = class {
|
|
|
525
594
|
);
|
|
526
595
|
console.log("[MonitorClient] Technology sync completed successfully");
|
|
527
596
|
} catch (err) {
|
|
528
|
-
|
|
597
|
+
this.reportError("DEPENDENCY_SYNC", "Technology sync failed", err);
|
|
529
598
|
}
|
|
530
599
|
}
|
|
531
600
|
async performDependencySync(signal) {
|
|
@@ -1029,7 +1098,7 @@ var MonitorClient = class {
|
|
|
1029
1098
|
const result = await response.json();
|
|
1030
1099
|
return result.data;
|
|
1031
1100
|
} catch (err) {
|
|
1032
|
-
|
|
1101
|
+
this.reportError("VULNERABILITY_SCAN", "Failed to audit dependencies", err);
|
|
1033
1102
|
return null;
|
|
1034
1103
|
}
|
|
1035
1104
|
}
|
|
@@ -1059,7 +1128,7 @@ var MonitorClient = class {
|
|
|
1059
1128
|
}
|
|
1060
1129
|
return result;
|
|
1061
1130
|
} catch (err) {
|
|
1062
|
-
|
|
1131
|
+
this.reportError("VULNERABILITY_SCAN", "Multi-path audit failed", err);
|
|
1063
1132
|
return null;
|
|
1064
1133
|
}
|
|
1065
1134
|
}
|
|
@@ -1321,12 +1390,12 @@ var MonitorClient = class {
|
|
|
1321
1390
|
await this.fetchAndScheduleHealthChecks();
|
|
1322
1391
|
this.healthCheckFetchTimer = setInterval(() => {
|
|
1323
1392
|
this.fetchAndScheduleHealthChecks().catch((err) => {
|
|
1324
|
-
|
|
1393
|
+
this.reportError("HEALTH_ENDPOINT_FETCH", "Failed to fetch and schedule health checks", err);
|
|
1325
1394
|
});
|
|
1326
1395
|
}, this.healthCheckFetchIntervalMs);
|
|
1327
1396
|
this.healthCheckFlushTimer = setInterval(() => {
|
|
1328
1397
|
this.flushHealthResults().catch((err) => {
|
|
1329
|
-
|
|
1398
|
+
this.reportError("HEALTH_RESULTS_FLUSH", "Failed to flush health results", err);
|
|
1330
1399
|
});
|
|
1331
1400
|
}, CONFIG_LIMITS.HEALTH_CHECK_FLUSH_INTERVAL_MS);
|
|
1332
1401
|
}
|
|
@@ -1347,7 +1416,7 @@ var MonitorClient = class {
|
|
|
1347
1416
|
this.scheduleHealthCheck(endpoint);
|
|
1348
1417
|
}
|
|
1349
1418
|
} catch (err) {
|
|
1350
|
-
|
|
1419
|
+
this.reportError("HEALTH_ENDPOINT_FETCH", "Failed to fetch health endpoints from server", err);
|
|
1351
1420
|
}
|
|
1352
1421
|
}
|
|
1353
1422
|
/**
|
|
@@ -1356,11 +1425,11 @@ var MonitorClient = class {
|
|
|
1356
1425
|
scheduleHealthCheck(endpoint) {
|
|
1357
1426
|
const intervalMs = Math.max(CONFIG_LIMITS.HEALTH_CHECK_MIN_INTERVAL_MS, endpoint.intervalMs);
|
|
1358
1427
|
this.runHealthCheck(endpoint).catch((err) => {
|
|
1359
|
-
|
|
1428
|
+
this.reportError("HEALTH_CHECK", `Health check failed for ${endpoint.name}`, err);
|
|
1360
1429
|
});
|
|
1361
1430
|
const timer = setInterval(() => {
|
|
1362
1431
|
this.runHealthCheck(endpoint).catch((err) => {
|
|
1363
|
-
|
|
1432
|
+
this.reportError("HEALTH_CHECK", `Health check failed for ${endpoint.name}`, err);
|
|
1364
1433
|
});
|
|
1365
1434
|
}, intervalMs);
|
|
1366
1435
|
this.healthCheckTimers.set(endpoint.id, timer);
|
|
@@ -1386,7 +1455,7 @@ var MonitorClient = class {
|
|
|
1386
1455
|
const response = await this.submitHealthResults(results);
|
|
1387
1456
|
console.log(`[MonitorClient] Submitted ${response.processed}/${response.total} health check results`);
|
|
1388
1457
|
} catch (err) {
|
|
1389
|
-
|
|
1458
|
+
this.reportError("HEALTH_RESULTS_FLUSH", "Failed to submit health check results to server", err);
|
|
1390
1459
|
if (this.healthCheckResultsQueue.length + results.length <= CONFIG_LIMITS.HEALTH_CHECK_MAX_BATCH_SIZE) {
|
|
1391
1460
|
this.healthCheckResultsQueue.unshift(...results);
|
|
1392
1461
|
}
|
|
@@ -1409,6 +1478,150 @@ var MonitorClient = class {
|
|
|
1409
1478
|
this.healthCheckTimers.delete(endpointId);
|
|
1410
1479
|
}
|
|
1411
1480
|
}
|
|
1481
|
+
// ---- System Metrics (on-premise only) ----
|
|
1482
|
+
/**
|
|
1483
|
+
* Start system metrics collection based on server-configured settings.
|
|
1484
|
+
* Only runs if metricsEnabled is true in project settings.
|
|
1485
|
+
* Collects CPU, RAM, and disk usage using built-in Node.js modules.
|
|
1486
|
+
*/
|
|
1487
|
+
async setupSystemMetricsCollection() {
|
|
1488
|
+
const settings = await this.fetchProjectSettings();
|
|
1489
|
+
if (!settings?.metricsEnabled) {
|
|
1490
|
+
return;
|
|
1491
|
+
}
|
|
1492
|
+
const intervalMs = Math.max(3e4, (settings.metricsIntervalSeconds ?? 60) * 1e3);
|
|
1493
|
+
const diskPaths = settings.metricsDiskPaths ?? ["/"];
|
|
1494
|
+
console.log(`[MonitorClient] System metrics collection enabled (every ${intervalMs / 1e3}s, paths: ${diskPaths.join(", ")})`);
|
|
1495
|
+
this.collectAndSubmitMetrics(diskPaths).catch((err) => {
|
|
1496
|
+
this.reportError("SYSTEM_METRICS", "Initial system metrics collection failed", err);
|
|
1497
|
+
});
|
|
1498
|
+
this.metricsCollectionTimer = setInterval(() => {
|
|
1499
|
+
this.collectAndSubmitMetrics(diskPaths).catch((err) => {
|
|
1500
|
+
this.reportError("SYSTEM_METRICS", "Scheduled system metrics collection failed", err);
|
|
1501
|
+
});
|
|
1502
|
+
}, intervalMs);
|
|
1503
|
+
}
|
|
1504
|
+
stopMetricsCollection() {
|
|
1505
|
+
if (this.metricsCollectionTimer) {
|
|
1506
|
+
clearInterval(this.metricsCollectionTimer);
|
|
1507
|
+
this.metricsCollectionTimer = null;
|
|
1508
|
+
}
|
|
1509
|
+
}
|
|
1510
|
+
/**
|
|
1511
|
+
* Collect system metrics (CPU, RAM, disk) using built-in Node.js modules
|
|
1512
|
+
* and submit to the monitoring server.
|
|
1513
|
+
*
|
|
1514
|
+
* Uses: os.cpus(), os.totalmem(), os.freemem(), os.hostname(), fs.statfs()
|
|
1515
|
+
* Zero dependencies — all built-in Node.js 18.15+
|
|
1516
|
+
*/
|
|
1517
|
+
async collectAndSubmitMetrics(diskPaths) {
|
|
1518
|
+
if (typeof window !== "undefined" || typeof document !== "undefined") {
|
|
1519
|
+
return;
|
|
1520
|
+
}
|
|
1521
|
+
let os;
|
|
1522
|
+
let fs;
|
|
1523
|
+
try {
|
|
1524
|
+
const osModule = await import(
|
|
1525
|
+
/* webpackIgnore: true */
|
|
1526
|
+
"os"
|
|
1527
|
+
);
|
|
1528
|
+
const fsModule = await import(
|
|
1529
|
+
/* webpackIgnore: true */
|
|
1530
|
+
"fs/promises"
|
|
1531
|
+
);
|
|
1532
|
+
os = osModule;
|
|
1533
|
+
fs = fsModule;
|
|
1534
|
+
} catch {
|
|
1535
|
+
this.reportError("SYSTEM_METRICS", "System metrics require Node.js (not available in bundled/browser environments)");
|
|
1536
|
+
return;
|
|
1537
|
+
}
|
|
1538
|
+
try {
|
|
1539
|
+
const hostname = os.hostname();
|
|
1540
|
+
const cpuPercent = await this.measureCpuPercent(os);
|
|
1541
|
+
const memoryTotal = os.totalmem();
|
|
1542
|
+
const memoryFree = os.freemem();
|
|
1543
|
+
const memoryUsed = memoryTotal - memoryFree;
|
|
1544
|
+
const memoryPercent = memoryTotal > 0 ? memoryUsed / memoryTotal * 100 : 0;
|
|
1545
|
+
const disks = await this.collectDiskMetrics(diskPaths, fs);
|
|
1546
|
+
await this.submitSystemMetric({
|
|
1547
|
+
hostname,
|
|
1548
|
+
cpuPercent,
|
|
1549
|
+
memoryTotal,
|
|
1550
|
+
memoryUsed,
|
|
1551
|
+
memoryPercent,
|
|
1552
|
+
disks
|
|
1553
|
+
});
|
|
1554
|
+
} catch (err) {
|
|
1555
|
+
this.reportError("SYSTEM_METRICS", "Failed to collect system metrics", err);
|
|
1556
|
+
}
|
|
1557
|
+
}
|
|
1558
|
+
/**
|
|
1559
|
+
* Measure CPU utilization by sampling cpus() twice with a 500ms gap.
|
|
1560
|
+
* Returns a percentage (0–100).
|
|
1561
|
+
*/
|
|
1562
|
+
async measureCpuPercent(os) {
|
|
1563
|
+
const sample1 = os.cpus();
|
|
1564
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1565
|
+
const sample2 = os.cpus();
|
|
1566
|
+
let totalIdle = 0;
|
|
1567
|
+
let totalTick = 0;
|
|
1568
|
+
for (let i = 0; i < sample2.length; i++) {
|
|
1569
|
+
const prev = sample1[i];
|
|
1570
|
+
const curr = sample2[i];
|
|
1571
|
+
const prevTotal = Object.values(prev.times).reduce((a, b) => a + b, 0);
|
|
1572
|
+
const currTotal = Object.values(curr.times).reduce((a, b) => a + b, 0);
|
|
1573
|
+
totalTick += currTotal - prevTotal;
|
|
1574
|
+
totalIdle += curr.times.idle - prev.times.idle;
|
|
1575
|
+
}
|
|
1576
|
+
const idlePercent = totalTick > 0 ? totalIdle / totalTick * 100 : 0;
|
|
1577
|
+
return Math.max(0, Math.min(100, 100 - idlePercent));
|
|
1578
|
+
}
|
|
1579
|
+
/**
|
|
1580
|
+
* Collect disk usage for each configured path using fs.statfs() (Node 18.15+).
|
|
1581
|
+
*/
|
|
1582
|
+
async collectDiskMetrics(paths, fs) {
|
|
1583
|
+
const results = [];
|
|
1584
|
+
for (const diskPath of paths) {
|
|
1585
|
+
try {
|
|
1586
|
+
const stat = await fs.statfs(diskPath);
|
|
1587
|
+
const total = stat.bsize * stat.blocks;
|
|
1588
|
+
const free = stat.bsize * stat.bavail;
|
|
1589
|
+
const used = total - free;
|
|
1590
|
+
const percent = total > 0 ? used / total * 100 : 0;
|
|
1591
|
+
results.push({
|
|
1592
|
+
path: diskPath,
|
|
1593
|
+
total: Math.round(total),
|
|
1594
|
+
used: Math.round(used),
|
|
1595
|
+
percent: Math.round(percent * 10) / 10
|
|
1596
|
+
// 1 decimal place
|
|
1597
|
+
});
|
|
1598
|
+
} catch {
|
|
1599
|
+
this.reportError("SYSTEM_METRICS", `Failed to get disk stats for path: ${diskPath}`);
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1602
|
+
return results;
|
|
1603
|
+
}
|
|
1604
|
+
/**
|
|
1605
|
+
* Submit a system metric snapshot to the monitoring server.
|
|
1606
|
+
*/
|
|
1607
|
+
async submitSystemMetric(metric) {
|
|
1608
|
+
try {
|
|
1609
|
+
const response = await this.fetchWithTimeout(`${this.endpoint}/api/v1/system-metrics`, {
|
|
1610
|
+
method: "POST",
|
|
1611
|
+
headers: {
|
|
1612
|
+
"Content-Type": "application/json",
|
|
1613
|
+
"Authorization": `Bearer ${this.apiKey}`
|
|
1614
|
+
},
|
|
1615
|
+
body: JSON.stringify(metric)
|
|
1616
|
+
});
|
|
1617
|
+
if (!response.ok) {
|
|
1618
|
+
const errorText = await response.text().catch(() => "");
|
|
1619
|
+
throw new Error(`HTTP ${response.status}${errorText ? `: ${this.sanitizeErrorResponse(errorText)}` : ""}`);
|
|
1620
|
+
}
|
|
1621
|
+
} catch (err) {
|
|
1622
|
+
this.reportError("SYSTEM_METRICS", "Failed to submit system metric to server", err);
|
|
1623
|
+
}
|
|
1624
|
+
}
|
|
1412
1625
|
};
|
|
1413
1626
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1414
1627
|
0 && (module.exports = {
|
package/dist/index.mjs
CHANGED
|
@@ -58,6 +58,13 @@ var MonitorClient = class {
|
|
|
58
58
|
this.healthCheckTimers = /* @__PURE__ */ new Map();
|
|
59
59
|
this.healthCheckResultsQueue = [];
|
|
60
60
|
this.healthCheckFlushTimer = null;
|
|
61
|
+
// SDK error reporting queue (errors visible on admin system errors page)
|
|
62
|
+
this.sdkErrorQueue = [];
|
|
63
|
+
this.sdkErrorFlushTimer = null;
|
|
64
|
+
this.sdkErrorsInCurrentWindow = 0;
|
|
65
|
+
this.sdkErrorWindowResetTimer = null;
|
|
66
|
+
// System metrics collection (on-premise only, opt-in)
|
|
67
|
+
this.metricsCollectionTimer = null;
|
|
61
68
|
if (!config.apiKey || config.apiKey.trim().length === 0) {
|
|
62
69
|
throw new Error("[MonitorClient] API key is required");
|
|
63
70
|
}
|
|
@@ -117,24 +124,28 @@ var MonitorClient = class {
|
|
|
117
124
|
this.auditTimeoutMs = Math.min(CONFIG_LIMITS.MAX_AUDIT_TIMEOUT_MS, Math.max(1e3, config.auditTimeoutMs || CONFIG_LIMITS.AUDIT_TIMEOUT_MS));
|
|
118
125
|
this.registryTimeoutMs = Math.min(CONFIG_LIMITS.MAX_REGISTRY_TIMEOUT_MS, Math.max(1e3, config.registryTimeoutMs || CONFIG_LIMITS.REGISTRY_TIMEOUT_MS));
|
|
119
126
|
this.npmRegistryUrl = (config.npmRegistryUrl || "https://registry.npmjs.org").replace(/\/$/, "");
|
|
120
|
-
this.healthCheckEnabled = config.healthCheckEnabled
|
|
127
|
+
this.healthCheckEnabled = config.healthCheckEnabled ?? true;
|
|
121
128
|
this.healthCheckFetchIntervalMs = config.healthCheckFetchIntervalMs || CONFIG_LIMITS.HEALTH_CHECK_FETCH_INTERVAL_MS;
|
|
122
129
|
this.startFlushTimer();
|
|
123
130
|
if (this.trackDependencies) {
|
|
124
131
|
this.syncDependencies().catch((err) => {
|
|
125
|
-
|
|
132
|
+
this.reportError("DEPENDENCY_SYNC", "Failed to sync dependencies on startup", err);
|
|
126
133
|
});
|
|
127
134
|
}
|
|
128
135
|
if (this.autoAudit) {
|
|
129
136
|
this.setupAutoAudit().catch((err) => {
|
|
130
|
-
|
|
137
|
+
this.reportError("AUDIT_SETUP", "Failed to setup auto audit", err);
|
|
131
138
|
});
|
|
132
139
|
}
|
|
133
140
|
if (this.healthCheckEnabled) {
|
|
134
141
|
this.setupHealthCheckPolling().catch((err) => {
|
|
135
|
-
|
|
142
|
+
this.reportError("HEALTH_POLLING_SETUP", "Failed to setup health check polling", err);
|
|
136
143
|
});
|
|
137
144
|
}
|
|
145
|
+
this.startSdkErrorFlushTimer();
|
|
146
|
+
this.setupSystemMetricsCollection().catch((err) => {
|
|
147
|
+
this.reportError("SYSTEM_METRICS_SETUP", "Failed to setup system metrics collection", err);
|
|
148
|
+
});
|
|
138
149
|
}
|
|
139
150
|
/**
|
|
140
151
|
* Security: Validate and sanitize metadata to prevent oversized payloads
|
|
@@ -284,8 +295,66 @@ var MonitorClient = class {
|
|
|
284
295
|
this.stopFlushTimer();
|
|
285
296
|
this.stopAuditIntervalTimer();
|
|
286
297
|
this.stopHealthCheckTimers();
|
|
298
|
+
this.stopSdkErrorFlushTimer();
|
|
299
|
+
this.stopMetricsCollection();
|
|
287
300
|
await this.flush();
|
|
288
301
|
await this.flushHealthResults();
|
|
302
|
+
await this.flushSdkErrors();
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* Queue an SDK error to be reported to the server's system errors page.
|
|
306
|
+
* Fire-and-forget — never throws. If reporting itself fails, logs to console only.
|
|
307
|
+
* Throttled to max 20 errors per minute to prevent flooding.
|
|
308
|
+
*/
|
|
309
|
+
reportError(category, message, err) {
|
|
310
|
+
if (this.sdkErrorsInCurrentWindow >= 20) return;
|
|
311
|
+
this.sdkErrorsInCurrentWindow++;
|
|
312
|
+
const errorMessage = err instanceof Error ? `${message}: ${err.message}` : message;
|
|
313
|
+
const stack = err instanceof Error ? err.stack : void 0;
|
|
314
|
+
this.sdkErrorQueue.push({ category, message: errorMessage, stack });
|
|
315
|
+
if (this.sdkErrorQueue.length >= 20) {
|
|
316
|
+
this.flushSdkErrors().catch(() => {
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
startSdkErrorFlushTimer() {
|
|
321
|
+
this.sdkErrorFlushTimer = setInterval(() => {
|
|
322
|
+
this.flushSdkErrors().catch(() => {
|
|
323
|
+
});
|
|
324
|
+
}, 3e4);
|
|
325
|
+
this.sdkErrorWindowResetTimer = setInterval(() => {
|
|
326
|
+
this.sdkErrorsInCurrentWindow = 0;
|
|
327
|
+
}, 6e4);
|
|
328
|
+
}
|
|
329
|
+
stopSdkErrorFlushTimer() {
|
|
330
|
+
if (this.sdkErrorFlushTimer) {
|
|
331
|
+
clearInterval(this.sdkErrorFlushTimer);
|
|
332
|
+
this.sdkErrorFlushTimer = null;
|
|
333
|
+
}
|
|
334
|
+
if (this.sdkErrorWindowResetTimer) {
|
|
335
|
+
clearInterval(this.sdkErrorWindowResetTimer);
|
|
336
|
+
this.sdkErrorWindowResetTimer = null;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
async flushSdkErrors() {
|
|
340
|
+
if (this.sdkErrorQueue.length === 0) return;
|
|
341
|
+
const errors = [...this.sdkErrorQueue];
|
|
342
|
+
this.sdkErrorQueue = [];
|
|
343
|
+
try {
|
|
344
|
+
const response = await this.fetchWithTimeout(`${this.endpoint}/api/v1/sdk-errors`, {
|
|
345
|
+
method: "POST",
|
|
346
|
+
headers: {
|
|
347
|
+
"Content-Type": "application/json",
|
|
348
|
+
"Authorization": `Bearer ${this.apiKey}`
|
|
349
|
+
},
|
|
350
|
+
body: JSON.stringify({ errors })
|
|
351
|
+
});
|
|
352
|
+
if (!response.ok) {
|
|
353
|
+
console.warn(`[MonitorClient] Failed to report SDK errors: HTTP ${response.status}`);
|
|
354
|
+
}
|
|
355
|
+
} catch (err) {
|
|
356
|
+
console.warn("[MonitorClient] Failed to flush SDK errors to server:", err instanceof Error ? err.message : String(err));
|
|
357
|
+
}
|
|
289
358
|
}
|
|
290
359
|
stopAuditIntervalTimer() {
|
|
291
360
|
if (this.auditIntervalTimer) {
|
|
@@ -347,14 +416,14 @@ var MonitorClient = class {
|
|
|
347
416
|
const intervalMs = intervalHours * 60 * 60 * 1e3;
|
|
348
417
|
this.auditIntervalTimer = setInterval(() => {
|
|
349
418
|
this.runScanAndTrackTime().catch((err) => {
|
|
350
|
-
|
|
419
|
+
this.reportError("AUDIT_SCAN", "Auto audit scan failed", err);
|
|
351
420
|
});
|
|
352
421
|
}, intervalMs);
|
|
353
422
|
}
|
|
354
423
|
console.log("[MonitorClient] Polling for scan requests enabled (every 5 minutes)");
|
|
355
424
|
this.settingsPollingTimer = setInterval(() => {
|
|
356
425
|
this.checkForScanRequest().catch((err) => {
|
|
357
|
-
|
|
426
|
+
this.reportError("SETTINGS_POLL", "Scan request check failed", err);
|
|
358
427
|
});
|
|
359
428
|
}, CONFIG_LIMITS.SETTINGS_POLL_INTERVAL_MS);
|
|
360
429
|
}
|
|
@@ -375,7 +444,7 @@ var MonitorClient = class {
|
|
|
375
444
|
const duration = Date.now() - startTime;
|
|
376
445
|
console.log(`[MonitorClient] Vulnerability scan completed in ${duration}ms`);
|
|
377
446
|
} catch (err) {
|
|
378
|
-
|
|
447
|
+
this.reportError("VULNERABILITY_SCAN", "Vulnerability scan failed", err);
|
|
379
448
|
}
|
|
380
449
|
}
|
|
381
450
|
/**
|
|
@@ -402,7 +471,7 @@ var MonitorClient = class {
|
|
|
402
471
|
}
|
|
403
472
|
}
|
|
404
473
|
} catch (err) {
|
|
405
|
-
|
|
474
|
+
this.reportError("SETTINGS_POLL", "Failed to check for scan request", err);
|
|
406
475
|
}
|
|
407
476
|
}
|
|
408
477
|
enqueue(payload) {
|
|
@@ -489,7 +558,7 @@ var MonitorClient = class {
|
|
|
489
558
|
);
|
|
490
559
|
console.log("[MonitorClient] Technology sync completed successfully");
|
|
491
560
|
} catch (err) {
|
|
492
|
-
|
|
561
|
+
this.reportError("DEPENDENCY_SYNC", "Technology sync failed", err);
|
|
493
562
|
}
|
|
494
563
|
}
|
|
495
564
|
async performDependencySync(signal) {
|
|
@@ -993,7 +1062,7 @@ var MonitorClient = class {
|
|
|
993
1062
|
const result = await response.json();
|
|
994
1063
|
return result.data;
|
|
995
1064
|
} catch (err) {
|
|
996
|
-
|
|
1065
|
+
this.reportError("VULNERABILITY_SCAN", "Failed to audit dependencies", err);
|
|
997
1066
|
return null;
|
|
998
1067
|
}
|
|
999
1068
|
}
|
|
@@ -1023,7 +1092,7 @@ var MonitorClient = class {
|
|
|
1023
1092
|
}
|
|
1024
1093
|
return result;
|
|
1025
1094
|
} catch (err) {
|
|
1026
|
-
|
|
1095
|
+
this.reportError("VULNERABILITY_SCAN", "Multi-path audit failed", err);
|
|
1027
1096
|
return null;
|
|
1028
1097
|
}
|
|
1029
1098
|
}
|
|
@@ -1285,12 +1354,12 @@ var MonitorClient = class {
|
|
|
1285
1354
|
await this.fetchAndScheduleHealthChecks();
|
|
1286
1355
|
this.healthCheckFetchTimer = setInterval(() => {
|
|
1287
1356
|
this.fetchAndScheduleHealthChecks().catch((err) => {
|
|
1288
|
-
|
|
1357
|
+
this.reportError("HEALTH_ENDPOINT_FETCH", "Failed to fetch and schedule health checks", err);
|
|
1289
1358
|
});
|
|
1290
1359
|
}, this.healthCheckFetchIntervalMs);
|
|
1291
1360
|
this.healthCheckFlushTimer = setInterval(() => {
|
|
1292
1361
|
this.flushHealthResults().catch((err) => {
|
|
1293
|
-
|
|
1362
|
+
this.reportError("HEALTH_RESULTS_FLUSH", "Failed to flush health results", err);
|
|
1294
1363
|
});
|
|
1295
1364
|
}, CONFIG_LIMITS.HEALTH_CHECK_FLUSH_INTERVAL_MS);
|
|
1296
1365
|
}
|
|
@@ -1311,7 +1380,7 @@ var MonitorClient = class {
|
|
|
1311
1380
|
this.scheduleHealthCheck(endpoint);
|
|
1312
1381
|
}
|
|
1313
1382
|
} catch (err) {
|
|
1314
|
-
|
|
1383
|
+
this.reportError("HEALTH_ENDPOINT_FETCH", "Failed to fetch health endpoints from server", err);
|
|
1315
1384
|
}
|
|
1316
1385
|
}
|
|
1317
1386
|
/**
|
|
@@ -1320,11 +1389,11 @@ var MonitorClient = class {
|
|
|
1320
1389
|
scheduleHealthCheck(endpoint) {
|
|
1321
1390
|
const intervalMs = Math.max(CONFIG_LIMITS.HEALTH_CHECK_MIN_INTERVAL_MS, endpoint.intervalMs);
|
|
1322
1391
|
this.runHealthCheck(endpoint).catch((err) => {
|
|
1323
|
-
|
|
1392
|
+
this.reportError("HEALTH_CHECK", `Health check failed for ${endpoint.name}`, err);
|
|
1324
1393
|
});
|
|
1325
1394
|
const timer = setInterval(() => {
|
|
1326
1395
|
this.runHealthCheck(endpoint).catch((err) => {
|
|
1327
|
-
|
|
1396
|
+
this.reportError("HEALTH_CHECK", `Health check failed for ${endpoint.name}`, err);
|
|
1328
1397
|
});
|
|
1329
1398
|
}, intervalMs);
|
|
1330
1399
|
this.healthCheckTimers.set(endpoint.id, timer);
|
|
@@ -1350,7 +1419,7 @@ var MonitorClient = class {
|
|
|
1350
1419
|
const response = await this.submitHealthResults(results);
|
|
1351
1420
|
console.log(`[MonitorClient] Submitted ${response.processed}/${response.total} health check results`);
|
|
1352
1421
|
} catch (err) {
|
|
1353
|
-
|
|
1422
|
+
this.reportError("HEALTH_RESULTS_FLUSH", "Failed to submit health check results to server", err);
|
|
1354
1423
|
if (this.healthCheckResultsQueue.length + results.length <= CONFIG_LIMITS.HEALTH_CHECK_MAX_BATCH_SIZE) {
|
|
1355
1424
|
this.healthCheckResultsQueue.unshift(...results);
|
|
1356
1425
|
}
|
|
@@ -1373,6 +1442,150 @@ var MonitorClient = class {
|
|
|
1373
1442
|
this.healthCheckTimers.delete(endpointId);
|
|
1374
1443
|
}
|
|
1375
1444
|
}
|
|
1445
|
+
// ---- System Metrics (on-premise only) ----
|
|
1446
|
+
/**
|
|
1447
|
+
* Start system metrics collection based on server-configured settings.
|
|
1448
|
+
* Only runs if metricsEnabled is true in project settings.
|
|
1449
|
+
* Collects CPU, RAM, and disk usage using built-in Node.js modules.
|
|
1450
|
+
*/
|
|
1451
|
+
async setupSystemMetricsCollection() {
|
|
1452
|
+
const settings = await this.fetchProjectSettings();
|
|
1453
|
+
if (!settings?.metricsEnabled) {
|
|
1454
|
+
return;
|
|
1455
|
+
}
|
|
1456
|
+
const intervalMs = Math.max(3e4, (settings.metricsIntervalSeconds ?? 60) * 1e3);
|
|
1457
|
+
const diskPaths = settings.metricsDiskPaths ?? ["/"];
|
|
1458
|
+
console.log(`[MonitorClient] System metrics collection enabled (every ${intervalMs / 1e3}s, paths: ${diskPaths.join(", ")})`);
|
|
1459
|
+
this.collectAndSubmitMetrics(diskPaths).catch((err) => {
|
|
1460
|
+
this.reportError("SYSTEM_METRICS", "Initial system metrics collection failed", err);
|
|
1461
|
+
});
|
|
1462
|
+
this.metricsCollectionTimer = setInterval(() => {
|
|
1463
|
+
this.collectAndSubmitMetrics(diskPaths).catch((err) => {
|
|
1464
|
+
this.reportError("SYSTEM_METRICS", "Scheduled system metrics collection failed", err);
|
|
1465
|
+
});
|
|
1466
|
+
}, intervalMs);
|
|
1467
|
+
}
|
|
1468
|
+
stopMetricsCollection() {
|
|
1469
|
+
if (this.metricsCollectionTimer) {
|
|
1470
|
+
clearInterval(this.metricsCollectionTimer);
|
|
1471
|
+
this.metricsCollectionTimer = null;
|
|
1472
|
+
}
|
|
1473
|
+
}
|
|
1474
|
+
/**
|
|
1475
|
+
* Collect system metrics (CPU, RAM, disk) using built-in Node.js modules
|
|
1476
|
+
* and submit to the monitoring server.
|
|
1477
|
+
*
|
|
1478
|
+
* Uses: os.cpus(), os.totalmem(), os.freemem(), os.hostname(), fs.statfs()
|
|
1479
|
+
* Zero dependencies — all built-in Node.js 18.15+
|
|
1480
|
+
*/
|
|
1481
|
+
async collectAndSubmitMetrics(diskPaths) {
|
|
1482
|
+
if (typeof window !== "undefined" || typeof document !== "undefined") {
|
|
1483
|
+
return;
|
|
1484
|
+
}
|
|
1485
|
+
let os;
|
|
1486
|
+
let fs;
|
|
1487
|
+
try {
|
|
1488
|
+
const osModule = await import(
|
|
1489
|
+
/* webpackIgnore: true */
|
|
1490
|
+
"os"
|
|
1491
|
+
);
|
|
1492
|
+
const fsModule = await import(
|
|
1493
|
+
/* webpackIgnore: true */
|
|
1494
|
+
"fs/promises"
|
|
1495
|
+
);
|
|
1496
|
+
os = osModule;
|
|
1497
|
+
fs = fsModule;
|
|
1498
|
+
} catch {
|
|
1499
|
+
this.reportError("SYSTEM_METRICS", "System metrics require Node.js (not available in bundled/browser environments)");
|
|
1500
|
+
return;
|
|
1501
|
+
}
|
|
1502
|
+
try {
|
|
1503
|
+
const hostname = os.hostname();
|
|
1504
|
+
const cpuPercent = await this.measureCpuPercent(os);
|
|
1505
|
+
const memoryTotal = os.totalmem();
|
|
1506
|
+
const memoryFree = os.freemem();
|
|
1507
|
+
const memoryUsed = memoryTotal - memoryFree;
|
|
1508
|
+
const memoryPercent = memoryTotal > 0 ? memoryUsed / memoryTotal * 100 : 0;
|
|
1509
|
+
const disks = await this.collectDiskMetrics(diskPaths, fs);
|
|
1510
|
+
await this.submitSystemMetric({
|
|
1511
|
+
hostname,
|
|
1512
|
+
cpuPercent,
|
|
1513
|
+
memoryTotal,
|
|
1514
|
+
memoryUsed,
|
|
1515
|
+
memoryPercent,
|
|
1516
|
+
disks
|
|
1517
|
+
});
|
|
1518
|
+
} catch (err) {
|
|
1519
|
+
this.reportError("SYSTEM_METRICS", "Failed to collect system metrics", err);
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1522
|
+
/**
|
|
1523
|
+
* Measure CPU utilization by sampling cpus() twice with a 500ms gap.
|
|
1524
|
+
* Returns a percentage (0–100).
|
|
1525
|
+
*/
|
|
1526
|
+
async measureCpuPercent(os) {
|
|
1527
|
+
const sample1 = os.cpus();
|
|
1528
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1529
|
+
const sample2 = os.cpus();
|
|
1530
|
+
let totalIdle = 0;
|
|
1531
|
+
let totalTick = 0;
|
|
1532
|
+
for (let i = 0; i < sample2.length; i++) {
|
|
1533
|
+
const prev = sample1[i];
|
|
1534
|
+
const curr = sample2[i];
|
|
1535
|
+
const prevTotal = Object.values(prev.times).reduce((a, b) => a + b, 0);
|
|
1536
|
+
const currTotal = Object.values(curr.times).reduce((a, b) => a + b, 0);
|
|
1537
|
+
totalTick += currTotal - prevTotal;
|
|
1538
|
+
totalIdle += curr.times.idle - prev.times.idle;
|
|
1539
|
+
}
|
|
1540
|
+
const idlePercent = totalTick > 0 ? totalIdle / totalTick * 100 : 0;
|
|
1541
|
+
return Math.max(0, Math.min(100, 100 - idlePercent));
|
|
1542
|
+
}
|
|
1543
|
+
/**
|
|
1544
|
+
* Collect disk usage for each configured path using fs.statfs() (Node 18.15+).
|
|
1545
|
+
*/
|
|
1546
|
+
async collectDiskMetrics(paths, fs) {
|
|
1547
|
+
const results = [];
|
|
1548
|
+
for (const diskPath of paths) {
|
|
1549
|
+
try {
|
|
1550
|
+
const stat = await fs.statfs(diskPath);
|
|
1551
|
+
const total = stat.bsize * stat.blocks;
|
|
1552
|
+
const free = stat.bsize * stat.bavail;
|
|
1553
|
+
const used = total - free;
|
|
1554
|
+
const percent = total > 0 ? used / total * 100 : 0;
|
|
1555
|
+
results.push({
|
|
1556
|
+
path: diskPath,
|
|
1557
|
+
total: Math.round(total),
|
|
1558
|
+
used: Math.round(used),
|
|
1559
|
+
percent: Math.round(percent * 10) / 10
|
|
1560
|
+
// 1 decimal place
|
|
1561
|
+
});
|
|
1562
|
+
} catch {
|
|
1563
|
+
this.reportError("SYSTEM_METRICS", `Failed to get disk stats for path: ${diskPath}`);
|
|
1564
|
+
}
|
|
1565
|
+
}
|
|
1566
|
+
return results;
|
|
1567
|
+
}
|
|
1568
|
+
/**
|
|
1569
|
+
* Submit a system metric snapshot to the monitoring server.
|
|
1570
|
+
*/
|
|
1571
|
+
async submitSystemMetric(metric) {
|
|
1572
|
+
try {
|
|
1573
|
+
const response = await this.fetchWithTimeout(`${this.endpoint}/api/v1/system-metrics`, {
|
|
1574
|
+
method: "POST",
|
|
1575
|
+
headers: {
|
|
1576
|
+
"Content-Type": "application/json",
|
|
1577
|
+
"Authorization": `Bearer ${this.apiKey}`
|
|
1578
|
+
},
|
|
1579
|
+
body: JSON.stringify(metric)
|
|
1580
|
+
});
|
|
1581
|
+
if (!response.ok) {
|
|
1582
|
+
const errorText = await response.text().catch(() => "");
|
|
1583
|
+
throw new Error(`HTTP ${response.status}${errorText ? `: ${this.sanitizeErrorResponse(errorText)}` : ""}`);
|
|
1584
|
+
}
|
|
1585
|
+
} catch (err) {
|
|
1586
|
+
this.reportError("SYSTEM_METRICS", "Failed to submit system metric to server", err);
|
|
1587
|
+
}
|
|
1588
|
+
}
|
|
1376
1589
|
};
|
|
1377
1590
|
export {
|
|
1378
1591
|
MonitorClient
|
package/package.json
CHANGED