@ceon-oy/monitor-sdk 1.2.1 → 1.4.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/README.md +92 -1
- package/dist/index.d.mts +64 -1
- package/dist/index.d.ts +64 -1
- package/dist/index.js +265 -17
- package/dist/index.mjs +265 -17
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Ceon Monitor SDK
|
|
2
2
|
|
|
3
|
-
Lightweight client SDK for integrating with the Ceon Monitor service. Provides error reporting, technology tracking, vulnerability auditing, and security event monitoring.
|
|
3
|
+
Lightweight client SDK for integrating with the Ceon Monitor service. Provides error reporting, technology tracking, vulnerability auditing, system metrics collection (CPU, RAM, disk), and security event monitoring.
|
|
4
4
|
|
|
5
5
|
## Table of Contents
|
|
6
6
|
|
|
@@ -12,6 +12,7 @@ Lightweight client SDK for integrating with the Ceon Monitor service. Provides e
|
|
|
12
12
|
- [Technology Tracking](#technology-tracking)
|
|
13
13
|
- [Vulnerability Auditing](#vulnerability-auditing)
|
|
14
14
|
- [SDK-Based Health Checks](#sdk-based-health-checks)
|
|
15
|
+
- [System Metrics](#system-metrics)
|
|
15
16
|
- [Security Events](#security-events)
|
|
16
17
|
- [Framework Examples](#framework-examples)
|
|
17
18
|
- [Express.js](#expressjs)
|
|
@@ -490,6 +491,75 @@ const results = [
|
|
|
490
491
|
await monitor.submitHealthResults(results);
|
|
491
492
|
```
|
|
492
493
|
|
|
494
|
+
### System Metrics
|
|
495
|
+
|
|
496
|
+
The SDK automatically collects and reports server resource usage — CPU, RAM, and disk — to Ceon Monitor. No SDK configuration flag is needed; the feature activates based on the project settings configured in the Ceon Monitor dashboard.
|
|
497
|
+
|
|
498
|
+
#### How It Works
|
|
499
|
+
|
|
500
|
+
When the `MonitorClient` is created, it fetches your project settings from the server. If the project has **Enable Metric Collection** turned on, the SDK:
|
|
501
|
+
|
|
502
|
+
1. Reads `metricsIntervalSeconds` from project settings (configured in the dashboard)
|
|
503
|
+
2. Reads `metricsDiskPaths` from project settings (e.g. `["/", "/data"]`)
|
|
504
|
+
3. Starts collecting metrics at that interval automatically
|
|
505
|
+
4. POSTs metrics to the server — no manual calls needed
|
|
506
|
+
|
|
507
|
+
#### What Is Collected
|
|
508
|
+
|
|
509
|
+
| Metric | Method | Notes |
|
|
510
|
+
|--------|--------|-------|
|
|
511
|
+
| CPU usage % | Dual `os.cpus()` sample with 500 ms gap | Accurate active-cycle average |
|
|
512
|
+
| RAM total | `os.totalmem()` | Bytes |
|
|
513
|
+
| RAM used | `os.totalmem() - os.freemem()` | Bytes |
|
|
514
|
+
| RAM % | `usedMemory / totalMemory * 100` | |
|
|
515
|
+
| Disk total / used / free per path | `fs.statfs(path)` | Node.js 18.15+ built-in, zero external deps |
|
|
516
|
+
|
|
517
|
+
Hostname is automatically set via `os.hostname()` and included with every report.
|
|
518
|
+
|
|
519
|
+
#### Enable in Dashboard
|
|
520
|
+
|
|
521
|
+
1. Open the **Ceon Monitor** dashboard
|
|
522
|
+
2. Go to **Projects** and click **Edit** on your project
|
|
523
|
+
3. Enable the **System Metrics** widget
|
|
524
|
+
4. Click **Enable Metric Collection**
|
|
525
|
+
5. Set the collection interval (e.g. every 60 seconds)
|
|
526
|
+
6. Add disk paths to monitor (e.g. `/` for root, `/data` for a data volume)
|
|
527
|
+
7. Configure CPU / RAM / disk alert thresholds
|
|
528
|
+
8. Save
|
|
529
|
+
|
|
530
|
+
The SDK will pick up the new settings on its next startup.
|
|
531
|
+
|
|
532
|
+
#### Notifications
|
|
533
|
+
|
|
534
|
+
When a metric exceeds a configured threshold, Ceon Monitor sends a `SYSTEM_METRIC_CRITICAL` notification to any user who has subscribed to **System Metric Alerts** for that project. Subscribe in **Settings → Subscriptions**.
|
|
535
|
+
|
|
536
|
+
#### Node.js Version Requirement
|
|
537
|
+
|
|
538
|
+
`fs.statfs()` (used for disk monitoring) requires **Node.js 18.15 or later**. No external packages are needed.
|
|
539
|
+
|
|
540
|
+
#### Manual Metric Submission
|
|
541
|
+
|
|
542
|
+
You can also submit a metric reading manually:
|
|
543
|
+
|
|
544
|
+
```typescript
|
|
545
|
+
await monitor.submitSystemMetric({
|
|
546
|
+
hostname: os.hostname(),
|
|
547
|
+
cpuPercent: 72.5,
|
|
548
|
+
memoryTotal: 8 * 1024 ** 3, // 8 GB in bytes
|
|
549
|
+
memoryUsed: 5 * 1024 ** 3, // 5 GB in bytes
|
|
550
|
+
memoryPercent: 62.5,
|
|
551
|
+
disks: [
|
|
552
|
+
{
|
|
553
|
+
path: '/',
|
|
554
|
+
total: 100 * 1024 ** 3, // 100 GB
|
|
555
|
+
used: 60 * 1024 ** 3, // 60 GB
|
|
556
|
+
free: 40 * 1024 ** 3, // 40 GB
|
|
557
|
+
usedPercent: 60,
|
|
558
|
+
},
|
|
559
|
+
],
|
|
560
|
+
});
|
|
561
|
+
```
|
|
562
|
+
|
|
493
563
|
### Security Events
|
|
494
564
|
|
|
495
565
|
#### Report Security Event
|
|
@@ -1092,6 +1162,10 @@ Runs npm audit and sends results to the server.
|
|
|
1092
1162
|
|
|
1093
1163
|
Runs npm audit on all directories configured in `auditPaths` and returns a combined summary.
|
|
1094
1164
|
|
|
1165
|
+
#### `submitSystemMetric(metric: SystemMetric): Promise<void>`
|
|
1166
|
+
|
|
1167
|
+
Manually submits a system metric reading. Under normal usage this is called automatically by the SDK's internal collection loop — use this only if you need custom collection logic.
|
|
1168
|
+
|
|
1095
1169
|
#### `flush(): Promise<void>`
|
|
1096
1170
|
|
|
1097
1171
|
Immediately sends all queued errors.
|
|
@@ -1160,6 +1234,23 @@ interface AuditPath {
|
|
|
1160
1234
|
environment: string; // Environment label (e.g., 'server', 'client')
|
|
1161
1235
|
}
|
|
1162
1236
|
|
|
1237
|
+
interface DiskMetric {
|
|
1238
|
+
path: string; // Mount path (e.g. '/', '/data')
|
|
1239
|
+
total: number; // Total bytes
|
|
1240
|
+
used: number; // Used bytes
|
|
1241
|
+
free: number; // Free bytes
|
|
1242
|
+
usedPercent: number; // 0–100
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
interface SystemMetric {
|
|
1246
|
+
hostname: string;
|
|
1247
|
+
cpuPercent: number;
|
|
1248
|
+
memoryTotal: number;
|
|
1249
|
+
memoryUsed: number;
|
|
1250
|
+
memoryPercent: number;
|
|
1251
|
+
disks: DiskMetric[];
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1163
1254
|
interface MultiAuditSummary {
|
|
1164
1255
|
results: Array<{
|
|
1165
1256
|
environment: string;
|
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;
|
|
@@ -166,6 +166,7 @@ interface SdkHealthEndpoint {
|
|
|
166
166
|
intervalMs: number;
|
|
167
167
|
timeoutMs: number;
|
|
168
168
|
expectedStatus: number;
|
|
169
|
+
allowInsecureTls?: boolean;
|
|
169
170
|
}
|
|
170
171
|
interface SdkHealthResult {
|
|
171
172
|
endpointId: string;
|
|
@@ -226,6 +227,12 @@ declare class MonitorClient {
|
|
|
226
227
|
private healthCheckTimers;
|
|
227
228
|
private healthCheckResultsQueue;
|
|
228
229
|
private healthCheckFlushTimer;
|
|
230
|
+
private sdkErrorQueue;
|
|
231
|
+
private sdkErrorFlushTimer;
|
|
232
|
+
private sdkErrorsInCurrentWindow;
|
|
233
|
+
private sdkErrorWindowResetTimer;
|
|
234
|
+
private metricsCollectionTimer;
|
|
235
|
+
private metricsSettingsCheckTimer;
|
|
229
236
|
constructor(config: MonitorClientConfig);
|
|
230
237
|
/**
|
|
231
238
|
* Security: Validate and sanitize metadata to prevent oversized payloads
|
|
@@ -253,6 +260,15 @@ declare class MonitorClient {
|
|
|
253
260
|
flush(): Promise<void>;
|
|
254
261
|
private getErrorKey;
|
|
255
262
|
close(): Promise<void>;
|
|
263
|
+
/**
|
|
264
|
+
* Queue an SDK error to be reported to the server's system errors page.
|
|
265
|
+
* Fire-and-forget — never throws. If reporting itself fails, logs to console only.
|
|
266
|
+
* Throttled to max 20 errors per minute to prevent flooding.
|
|
267
|
+
*/
|
|
268
|
+
private reportError;
|
|
269
|
+
private startSdkErrorFlushTimer;
|
|
270
|
+
private stopSdkErrorFlushTimer;
|
|
271
|
+
private flushSdkErrors;
|
|
256
272
|
private stopAuditIntervalTimer;
|
|
257
273
|
/**
|
|
258
274
|
* Fetch project settings from the monitoring server.
|
|
@@ -263,6 +279,12 @@ declare class MonitorClient {
|
|
|
263
279
|
vulnerabilityScanIntervalHours: number;
|
|
264
280
|
scanRequestedAt: string | null;
|
|
265
281
|
techScanRequestedAt: string | null;
|
|
282
|
+
metricsEnabled?: boolean;
|
|
283
|
+
metricsIntervalSeconds?: number;
|
|
284
|
+
metricsDiskPaths?: string[];
|
|
285
|
+
metricsCpuThreshold?: number;
|
|
286
|
+
metricsRamThreshold?: number;
|
|
287
|
+
metricsDiskThreshold?: number;
|
|
266
288
|
} | null>;
|
|
267
289
|
/**
|
|
268
290
|
* Setup automatic vulnerability scanning based on server-configured interval.
|
|
@@ -465,6 +487,47 @@ declare class MonitorClient {
|
|
|
465
487
|
* Stop all health check timers
|
|
466
488
|
*/
|
|
467
489
|
private stopHealthCheckTimers;
|
|
490
|
+
/**
|
|
491
|
+
* Start system metrics collection based on server-configured settings.
|
|
492
|
+
* Only runs if metricsEnabled is true in project settings.
|
|
493
|
+
* Collects CPU, RAM, and disk usage using built-in Node.js modules.
|
|
494
|
+
*/
|
|
495
|
+
setupSystemMetricsCollection(): Promise<void>;
|
|
496
|
+
private startMetricsCollection;
|
|
497
|
+
private stopMetricsCollection;
|
|
498
|
+
/**
|
|
499
|
+
* Collect system metrics (CPU, RAM, disk) using built-in Node.js modules
|
|
500
|
+
* and submit to the monitoring server.
|
|
501
|
+
*
|
|
502
|
+
* Uses: os.cpus(), os.totalmem(), os.freemem(), os.hostname(), fs.statfs()
|
|
503
|
+
* Zero dependencies — all built-in Node.js 18.15+
|
|
504
|
+
*/
|
|
505
|
+
private collectAndSubmitMetrics;
|
|
506
|
+
/**
|
|
507
|
+
* Measure CPU utilization by sampling cpus() twice with a 500ms gap.
|
|
508
|
+
* Returns a percentage (0–100).
|
|
509
|
+
*/
|
|
510
|
+
private measureCpuPercent;
|
|
511
|
+
/**
|
|
512
|
+
* Collect disk usage for each configured path using fs.statfs() (Node 18.15+).
|
|
513
|
+
*/
|
|
514
|
+
private collectDiskMetrics;
|
|
515
|
+
/**
|
|
516
|
+
* Submit a system metric snapshot to the monitoring server.
|
|
517
|
+
*/
|
|
518
|
+
submitSystemMetric(metric: {
|
|
519
|
+
hostname: string;
|
|
520
|
+
cpuPercent: number;
|
|
521
|
+
memoryTotal: number;
|
|
522
|
+
memoryUsed: number;
|
|
523
|
+
memoryPercent: number;
|
|
524
|
+
disks: Array<{
|
|
525
|
+
path: string;
|
|
526
|
+
total: number;
|
|
527
|
+
used: number;
|
|
528
|
+
percent: number;
|
|
529
|
+
}>;
|
|
530
|
+
}): Promise<void>;
|
|
468
531
|
}
|
|
469
532
|
|
|
470
533
|
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;
|
|
@@ -166,6 +166,7 @@ interface SdkHealthEndpoint {
|
|
|
166
166
|
intervalMs: number;
|
|
167
167
|
timeoutMs: number;
|
|
168
168
|
expectedStatus: number;
|
|
169
|
+
allowInsecureTls?: boolean;
|
|
169
170
|
}
|
|
170
171
|
interface SdkHealthResult {
|
|
171
172
|
endpointId: string;
|
|
@@ -226,6 +227,12 @@ declare class MonitorClient {
|
|
|
226
227
|
private healthCheckTimers;
|
|
227
228
|
private healthCheckResultsQueue;
|
|
228
229
|
private healthCheckFlushTimer;
|
|
230
|
+
private sdkErrorQueue;
|
|
231
|
+
private sdkErrorFlushTimer;
|
|
232
|
+
private sdkErrorsInCurrentWindow;
|
|
233
|
+
private sdkErrorWindowResetTimer;
|
|
234
|
+
private metricsCollectionTimer;
|
|
235
|
+
private metricsSettingsCheckTimer;
|
|
229
236
|
constructor(config: MonitorClientConfig);
|
|
230
237
|
/**
|
|
231
238
|
* Security: Validate and sanitize metadata to prevent oversized payloads
|
|
@@ -253,6 +260,15 @@ declare class MonitorClient {
|
|
|
253
260
|
flush(): Promise<void>;
|
|
254
261
|
private getErrorKey;
|
|
255
262
|
close(): Promise<void>;
|
|
263
|
+
/**
|
|
264
|
+
* Queue an SDK error to be reported to the server's system errors page.
|
|
265
|
+
* Fire-and-forget — never throws. If reporting itself fails, logs to console only.
|
|
266
|
+
* Throttled to max 20 errors per minute to prevent flooding.
|
|
267
|
+
*/
|
|
268
|
+
private reportError;
|
|
269
|
+
private startSdkErrorFlushTimer;
|
|
270
|
+
private stopSdkErrorFlushTimer;
|
|
271
|
+
private flushSdkErrors;
|
|
256
272
|
private stopAuditIntervalTimer;
|
|
257
273
|
/**
|
|
258
274
|
* Fetch project settings from the monitoring server.
|
|
@@ -263,6 +279,12 @@ declare class MonitorClient {
|
|
|
263
279
|
vulnerabilityScanIntervalHours: number;
|
|
264
280
|
scanRequestedAt: string | null;
|
|
265
281
|
techScanRequestedAt: string | null;
|
|
282
|
+
metricsEnabled?: boolean;
|
|
283
|
+
metricsIntervalSeconds?: number;
|
|
284
|
+
metricsDiskPaths?: string[];
|
|
285
|
+
metricsCpuThreshold?: number;
|
|
286
|
+
metricsRamThreshold?: number;
|
|
287
|
+
metricsDiskThreshold?: number;
|
|
266
288
|
} | null>;
|
|
267
289
|
/**
|
|
268
290
|
* Setup automatic vulnerability scanning based on server-configured interval.
|
|
@@ -465,6 +487,47 @@ declare class MonitorClient {
|
|
|
465
487
|
* Stop all health check timers
|
|
466
488
|
*/
|
|
467
489
|
private stopHealthCheckTimers;
|
|
490
|
+
/**
|
|
491
|
+
* Start system metrics collection based on server-configured settings.
|
|
492
|
+
* Only runs if metricsEnabled is true in project settings.
|
|
493
|
+
* Collects CPU, RAM, and disk usage using built-in Node.js modules.
|
|
494
|
+
*/
|
|
495
|
+
setupSystemMetricsCollection(): Promise<void>;
|
|
496
|
+
private startMetricsCollection;
|
|
497
|
+
private stopMetricsCollection;
|
|
498
|
+
/**
|
|
499
|
+
* Collect system metrics (CPU, RAM, disk) using built-in Node.js modules
|
|
500
|
+
* and submit to the monitoring server.
|
|
501
|
+
*
|
|
502
|
+
* Uses: os.cpus(), os.totalmem(), os.freemem(), os.hostname(), fs.statfs()
|
|
503
|
+
* Zero dependencies — all built-in Node.js 18.15+
|
|
504
|
+
*/
|
|
505
|
+
private collectAndSubmitMetrics;
|
|
506
|
+
/**
|
|
507
|
+
* Measure CPU utilization by sampling cpus() twice with a 500ms gap.
|
|
508
|
+
* Returns a percentage (0–100).
|
|
509
|
+
*/
|
|
510
|
+
private measureCpuPercent;
|
|
511
|
+
/**
|
|
512
|
+
* Collect disk usage for each configured path using fs.statfs() (Node 18.15+).
|
|
513
|
+
*/
|
|
514
|
+
private collectDiskMetrics;
|
|
515
|
+
/**
|
|
516
|
+
* Submit a system metric snapshot to the monitoring server.
|
|
517
|
+
*/
|
|
518
|
+
submitSystemMetric(metric: {
|
|
519
|
+
hostname: string;
|
|
520
|
+
cpuPercent: number;
|
|
521
|
+
memoryTotal: number;
|
|
522
|
+
memoryUsed: number;
|
|
523
|
+
memoryPercent: number;
|
|
524
|
+
disks: Array<{
|
|
525
|
+
path: string;
|
|
526
|
+
total: number;
|
|
527
|
+
used: number;
|
|
528
|
+
percent: number;
|
|
529
|
+
}>;
|
|
530
|
+
}): Promise<void>;
|
|
468
531
|
}
|
|
469
532
|
|
|
470
533
|
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,14 @@ 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;
|
|
104
|
+
this.metricsSettingsCheckTimer = null;
|
|
97
105
|
if (!config.apiKey || config.apiKey.trim().length === 0) {
|
|
98
106
|
throw new Error("[MonitorClient] API key is required");
|
|
99
107
|
}
|
|
@@ -153,24 +161,28 @@ var MonitorClient = class {
|
|
|
153
161
|
this.auditTimeoutMs = Math.min(CONFIG_LIMITS.MAX_AUDIT_TIMEOUT_MS, Math.max(1e3, config.auditTimeoutMs || CONFIG_LIMITS.AUDIT_TIMEOUT_MS));
|
|
154
162
|
this.registryTimeoutMs = Math.min(CONFIG_LIMITS.MAX_REGISTRY_TIMEOUT_MS, Math.max(1e3, config.registryTimeoutMs || CONFIG_LIMITS.REGISTRY_TIMEOUT_MS));
|
|
155
163
|
this.npmRegistryUrl = (config.npmRegistryUrl || "https://registry.npmjs.org").replace(/\/$/, "");
|
|
156
|
-
this.healthCheckEnabled = config.healthCheckEnabled
|
|
164
|
+
this.healthCheckEnabled = config.healthCheckEnabled ?? true;
|
|
157
165
|
this.healthCheckFetchIntervalMs = config.healthCheckFetchIntervalMs || CONFIG_LIMITS.HEALTH_CHECK_FETCH_INTERVAL_MS;
|
|
158
166
|
this.startFlushTimer();
|
|
159
167
|
if (this.trackDependencies) {
|
|
160
168
|
this.syncDependencies().catch((err) => {
|
|
161
|
-
|
|
169
|
+
this.reportError("DEPENDENCY_SYNC", "Failed to sync dependencies on startup", err);
|
|
162
170
|
});
|
|
163
171
|
}
|
|
164
172
|
if (this.autoAudit) {
|
|
165
173
|
this.setupAutoAudit().catch((err) => {
|
|
166
|
-
|
|
174
|
+
this.reportError("AUDIT_SETUP", "Failed to setup auto audit", err);
|
|
167
175
|
});
|
|
168
176
|
}
|
|
169
177
|
if (this.healthCheckEnabled) {
|
|
170
178
|
this.setupHealthCheckPolling().catch((err) => {
|
|
171
|
-
|
|
179
|
+
this.reportError("HEALTH_POLLING_SETUP", "Failed to setup health check polling", err);
|
|
172
180
|
});
|
|
173
181
|
}
|
|
182
|
+
this.startSdkErrorFlushTimer();
|
|
183
|
+
this.setupSystemMetricsCollection().catch((err) => {
|
|
184
|
+
this.reportError("SYSTEM_METRICS_SETUP", "Failed to setup system metrics collection", err);
|
|
185
|
+
});
|
|
174
186
|
}
|
|
175
187
|
/**
|
|
176
188
|
* Security: Validate and sanitize metadata to prevent oversized payloads
|
|
@@ -320,8 +332,66 @@ var MonitorClient = class {
|
|
|
320
332
|
this.stopFlushTimer();
|
|
321
333
|
this.stopAuditIntervalTimer();
|
|
322
334
|
this.stopHealthCheckTimers();
|
|
335
|
+
this.stopSdkErrorFlushTimer();
|
|
336
|
+
this.stopMetricsCollection();
|
|
323
337
|
await this.flush();
|
|
324
338
|
await this.flushHealthResults();
|
|
339
|
+
await this.flushSdkErrors();
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* Queue an SDK error to be reported to the server's system errors page.
|
|
343
|
+
* Fire-and-forget — never throws. If reporting itself fails, logs to console only.
|
|
344
|
+
* Throttled to max 20 errors per minute to prevent flooding.
|
|
345
|
+
*/
|
|
346
|
+
reportError(category, message, err) {
|
|
347
|
+
if (this.sdkErrorsInCurrentWindow >= 20) return;
|
|
348
|
+
this.sdkErrorsInCurrentWindow++;
|
|
349
|
+
const errorMessage = err instanceof Error ? `${message}: ${err.message}` : message;
|
|
350
|
+
const stack = err instanceof Error ? err.stack : void 0;
|
|
351
|
+
this.sdkErrorQueue.push({ category, message: errorMessage, stack });
|
|
352
|
+
if (this.sdkErrorQueue.length >= 20) {
|
|
353
|
+
this.flushSdkErrors().catch(() => {
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
startSdkErrorFlushTimer() {
|
|
358
|
+
this.sdkErrorFlushTimer = setInterval(() => {
|
|
359
|
+
this.flushSdkErrors().catch(() => {
|
|
360
|
+
});
|
|
361
|
+
}, 3e4);
|
|
362
|
+
this.sdkErrorWindowResetTimer = setInterval(() => {
|
|
363
|
+
this.sdkErrorsInCurrentWindow = 0;
|
|
364
|
+
}, 6e4);
|
|
365
|
+
}
|
|
366
|
+
stopSdkErrorFlushTimer() {
|
|
367
|
+
if (this.sdkErrorFlushTimer) {
|
|
368
|
+
clearInterval(this.sdkErrorFlushTimer);
|
|
369
|
+
this.sdkErrorFlushTimer = null;
|
|
370
|
+
}
|
|
371
|
+
if (this.sdkErrorWindowResetTimer) {
|
|
372
|
+
clearInterval(this.sdkErrorWindowResetTimer);
|
|
373
|
+
this.sdkErrorWindowResetTimer = null;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
async flushSdkErrors() {
|
|
377
|
+
if (this.sdkErrorQueue.length === 0) return;
|
|
378
|
+
const errors = [...this.sdkErrorQueue];
|
|
379
|
+
this.sdkErrorQueue = [];
|
|
380
|
+
try {
|
|
381
|
+
const response = await this.fetchWithTimeout(`${this.endpoint}/api/v1/sdk-errors`, {
|
|
382
|
+
method: "POST",
|
|
383
|
+
headers: {
|
|
384
|
+
"Content-Type": "application/json",
|
|
385
|
+
"Authorization": `Bearer ${this.apiKey}`
|
|
386
|
+
},
|
|
387
|
+
body: JSON.stringify({ errors })
|
|
388
|
+
});
|
|
389
|
+
if (!response.ok) {
|
|
390
|
+
console.warn(`[MonitorClient] Failed to report SDK errors: HTTP ${response.status}`);
|
|
391
|
+
}
|
|
392
|
+
} catch (err) {
|
|
393
|
+
console.warn("[MonitorClient] Failed to flush SDK errors to server:", err instanceof Error ? err.message : String(err));
|
|
394
|
+
}
|
|
325
395
|
}
|
|
326
396
|
stopAuditIntervalTimer() {
|
|
327
397
|
if (this.auditIntervalTimer) {
|
|
@@ -383,14 +453,14 @@ var MonitorClient = class {
|
|
|
383
453
|
const intervalMs = intervalHours * 60 * 60 * 1e3;
|
|
384
454
|
this.auditIntervalTimer = setInterval(() => {
|
|
385
455
|
this.runScanAndTrackTime().catch((err) => {
|
|
386
|
-
|
|
456
|
+
this.reportError("AUDIT_SCAN", "Auto audit scan failed", err);
|
|
387
457
|
});
|
|
388
458
|
}, intervalMs);
|
|
389
459
|
}
|
|
390
460
|
console.log("[MonitorClient] Polling for scan requests enabled (every 5 minutes)");
|
|
391
461
|
this.settingsPollingTimer = setInterval(() => {
|
|
392
462
|
this.checkForScanRequest().catch((err) => {
|
|
393
|
-
|
|
463
|
+
this.reportError("SETTINGS_POLL", "Scan request check failed", err);
|
|
394
464
|
});
|
|
395
465
|
}, CONFIG_LIMITS.SETTINGS_POLL_INTERVAL_MS);
|
|
396
466
|
}
|
|
@@ -411,7 +481,7 @@ var MonitorClient = class {
|
|
|
411
481
|
const duration = Date.now() - startTime;
|
|
412
482
|
console.log(`[MonitorClient] Vulnerability scan completed in ${duration}ms`);
|
|
413
483
|
} catch (err) {
|
|
414
|
-
|
|
484
|
+
this.reportError("VULNERABILITY_SCAN", "Vulnerability scan failed", err);
|
|
415
485
|
}
|
|
416
486
|
}
|
|
417
487
|
/**
|
|
@@ -438,7 +508,7 @@ var MonitorClient = class {
|
|
|
438
508
|
}
|
|
439
509
|
}
|
|
440
510
|
} catch (err) {
|
|
441
|
-
|
|
511
|
+
this.reportError("SETTINGS_POLL", "Failed to check for scan request", err);
|
|
442
512
|
}
|
|
443
513
|
}
|
|
444
514
|
enqueue(payload) {
|
|
@@ -525,7 +595,7 @@ var MonitorClient = class {
|
|
|
525
595
|
);
|
|
526
596
|
console.log("[MonitorClient] Technology sync completed successfully");
|
|
527
597
|
} catch (err) {
|
|
528
|
-
|
|
598
|
+
this.reportError("DEPENDENCY_SYNC", "Technology sync failed", err);
|
|
529
599
|
}
|
|
530
600
|
}
|
|
531
601
|
async performDependencySync(signal) {
|
|
@@ -1029,7 +1099,7 @@ var MonitorClient = class {
|
|
|
1029
1099
|
const result = await response.json();
|
|
1030
1100
|
return result.data;
|
|
1031
1101
|
} catch (err) {
|
|
1032
|
-
|
|
1102
|
+
this.reportError("VULNERABILITY_SCAN", "Failed to audit dependencies", err);
|
|
1033
1103
|
return null;
|
|
1034
1104
|
}
|
|
1035
1105
|
}
|
|
@@ -1059,7 +1129,7 @@ var MonitorClient = class {
|
|
|
1059
1129
|
}
|
|
1060
1130
|
return result;
|
|
1061
1131
|
} catch (err) {
|
|
1062
|
-
|
|
1132
|
+
this.reportError("VULNERABILITY_SCAN", "Multi-path audit failed", err);
|
|
1063
1133
|
return null;
|
|
1064
1134
|
}
|
|
1065
1135
|
}
|
|
@@ -1278,6 +1348,19 @@ var MonitorClient = class {
|
|
|
1278
1348
|
"Content-Type": "application/json"
|
|
1279
1349
|
};
|
|
1280
1350
|
}
|
|
1351
|
+
if (endpoint.allowInsecureTls) {
|
|
1352
|
+
try {
|
|
1353
|
+
const undici = await import(
|
|
1354
|
+
/* webpackIgnore: true */
|
|
1355
|
+
"undici"
|
|
1356
|
+
);
|
|
1357
|
+
const Agent = undici.Agent;
|
|
1358
|
+
requestOptions.dispatcher = new Agent({
|
|
1359
|
+
connect: { rejectUnauthorized: false }
|
|
1360
|
+
});
|
|
1361
|
+
} catch {
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1281
1364
|
const response = await this.fetchWithTimeout(
|
|
1282
1365
|
endpoint.url,
|
|
1283
1366
|
requestOptions,
|
|
@@ -1321,12 +1404,12 @@ var MonitorClient = class {
|
|
|
1321
1404
|
await this.fetchAndScheduleHealthChecks();
|
|
1322
1405
|
this.healthCheckFetchTimer = setInterval(() => {
|
|
1323
1406
|
this.fetchAndScheduleHealthChecks().catch((err) => {
|
|
1324
|
-
|
|
1407
|
+
this.reportError("HEALTH_ENDPOINT_FETCH", "Failed to fetch and schedule health checks", err);
|
|
1325
1408
|
});
|
|
1326
1409
|
}, this.healthCheckFetchIntervalMs);
|
|
1327
1410
|
this.healthCheckFlushTimer = setInterval(() => {
|
|
1328
1411
|
this.flushHealthResults().catch((err) => {
|
|
1329
|
-
|
|
1412
|
+
this.reportError("HEALTH_RESULTS_FLUSH", "Failed to flush health results", err);
|
|
1330
1413
|
});
|
|
1331
1414
|
}, CONFIG_LIMITS.HEALTH_CHECK_FLUSH_INTERVAL_MS);
|
|
1332
1415
|
}
|
|
@@ -1347,7 +1430,7 @@ var MonitorClient = class {
|
|
|
1347
1430
|
this.scheduleHealthCheck(endpoint);
|
|
1348
1431
|
}
|
|
1349
1432
|
} catch (err) {
|
|
1350
|
-
|
|
1433
|
+
this.reportError("HEALTH_ENDPOINT_FETCH", "Failed to fetch health endpoints from server", err);
|
|
1351
1434
|
}
|
|
1352
1435
|
}
|
|
1353
1436
|
/**
|
|
@@ -1356,11 +1439,11 @@ var MonitorClient = class {
|
|
|
1356
1439
|
scheduleHealthCheck(endpoint) {
|
|
1357
1440
|
const intervalMs = Math.max(CONFIG_LIMITS.HEALTH_CHECK_MIN_INTERVAL_MS, endpoint.intervalMs);
|
|
1358
1441
|
this.runHealthCheck(endpoint).catch((err) => {
|
|
1359
|
-
|
|
1442
|
+
this.reportError("HEALTH_CHECK", `Health check failed for ${endpoint.name}`, err);
|
|
1360
1443
|
});
|
|
1361
1444
|
const timer = setInterval(() => {
|
|
1362
1445
|
this.runHealthCheck(endpoint).catch((err) => {
|
|
1363
|
-
|
|
1446
|
+
this.reportError("HEALTH_CHECK", `Health check failed for ${endpoint.name}`, err);
|
|
1364
1447
|
});
|
|
1365
1448
|
}, intervalMs);
|
|
1366
1449
|
this.healthCheckTimers.set(endpoint.id, timer);
|
|
@@ -1386,7 +1469,7 @@ var MonitorClient = class {
|
|
|
1386
1469
|
const response = await this.submitHealthResults(results);
|
|
1387
1470
|
console.log(`[MonitorClient] Submitted ${response.processed}/${response.total} health check results`);
|
|
1388
1471
|
} catch (err) {
|
|
1389
|
-
|
|
1472
|
+
this.reportError("HEALTH_RESULTS_FLUSH", "Failed to submit health check results to server", err);
|
|
1390
1473
|
if (this.healthCheckResultsQueue.length + results.length <= CONFIG_LIMITS.HEALTH_CHECK_MAX_BATCH_SIZE) {
|
|
1391
1474
|
this.healthCheckResultsQueue.unshift(...results);
|
|
1392
1475
|
}
|
|
@@ -1409,6 +1492,171 @@ var MonitorClient = class {
|
|
|
1409
1492
|
this.healthCheckTimers.delete(endpointId);
|
|
1410
1493
|
}
|
|
1411
1494
|
}
|
|
1495
|
+
// ---- System Metrics (on-premise only) ----
|
|
1496
|
+
/**
|
|
1497
|
+
* Start system metrics collection based on server-configured settings.
|
|
1498
|
+
* Only runs if metricsEnabled is true in project settings.
|
|
1499
|
+
* Collects CPU, RAM, and disk usage using built-in Node.js modules.
|
|
1500
|
+
*/
|
|
1501
|
+
async setupSystemMetricsCollection() {
|
|
1502
|
+
const settings = await this.fetchProjectSettings();
|
|
1503
|
+
if (!settings?.metricsEnabled) {
|
|
1504
|
+
this.metricsSettingsCheckTimer = setInterval(() => {
|
|
1505
|
+
this.fetchProjectSettings().then((latestSettings) => {
|
|
1506
|
+
if (latestSettings?.metricsEnabled) {
|
|
1507
|
+
clearInterval(this.metricsSettingsCheckTimer);
|
|
1508
|
+
this.metricsSettingsCheckTimer = null;
|
|
1509
|
+
this.startMetricsCollection(latestSettings);
|
|
1510
|
+
}
|
|
1511
|
+
}).catch(() => {
|
|
1512
|
+
});
|
|
1513
|
+
}, 3e5);
|
|
1514
|
+
return;
|
|
1515
|
+
}
|
|
1516
|
+
this.startMetricsCollection(settings);
|
|
1517
|
+
}
|
|
1518
|
+
startMetricsCollection(settings) {
|
|
1519
|
+
const intervalMs = Math.max(3e4, (settings.metricsIntervalSeconds ?? 3600) * 1e3);
|
|
1520
|
+
console.log(`[MonitorClient] System metrics collection enabled (every ${intervalMs / 1e3}s)`);
|
|
1521
|
+
const initialDiskPaths = settings.metricsDiskPaths ?? ["/"];
|
|
1522
|
+
this.collectAndSubmitMetrics(initialDiskPaths).catch((err) => {
|
|
1523
|
+
this.reportError("SYSTEM_METRICS", "Initial system metrics collection failed", err);
|
|
1524
|
+
});
|
|
1525
|
+
this.metricsCollectionTimer = setInterval(() => {
|
|
1526
|
+
this.fetchProjectSettings().then((latestSettings) => {
|
|
1527
|
+
if (!latestSettings?.metricsEnabled) return;
|
|
1528
|
+
const diskPaths = latestSettings.metricsDiskPaths ?? ["/"];
|
|
1529
|
+
return this.collectAndSubmitMetrics(diskPaths);
|
|
1530
|
+
}).catch((err) => {
|
|
1531
|
+
this.reportError("SYSTEM_METRICS", "Scheduled system metrics collection failed", err);
|
|
1532
|
+
});
|
|
1533
|
+
}, intervalMs);
|
|
1534
|
+
}
|
|
1535
|
+
stopMetricsCollection() {
|
|
1536
|
+
if (this.metricsCollectionTimer) {
|
|
1537
|
+
clearInterval(this.metricsCollectionTimer);
|
|
1538
|
+
this.metricsCollectionTimer = null;
|
|
1539
|
+
}
|
|
1540
|
+
if (this.metricsSettingsCheckTimer) {
|
|
1541
|
+
clearInterval(this.metricsSettingsCheckTimer);
|
|
1542
|
+
this.metricsSettingsCheckTimer = null;
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1545
|
+
/**
|
|
1546
|
+
* Collect system metrics (CPU, RAM, disk) using built-in Node.js modules
|
|
1547
|
+
* and submit to the monitoring server.
|
|
1548
|
+
*
|
|
1549
|
+
* Uses: os.cpus(), os.totalmem(), os.freemem(), os.hostname(), fs.statfs()
|
|
1550
|
+
* Zero dependencies — all built-in Node.js 18.15+
|
|
1551
|
+
*/
|
|
1552
|
+
async collectAndSubmitMetrics(diskPaths) {
|
|
1553
|
+
if (typeof window !== "undefined" || typeof document !== "undefined") {
|
|
1554
|
+
return;
|
|
1555
|
+
}
|
|
1556
|
+
let os;
|
|
1557
|
+
let fs;
|
|
1558
|
+
try {
|
|
1559
|
+
const osModule = await import(
|
|
1560
|
+
/* webpackIgnore: true */
|
|
1561
|
+
"os"
|
|
1562
|
+
);
|
|
1563
|
+
const fsModule = await import(
|
|
1564
|
+
/* webpackIgnore: true */
|
|
1565
|
+
"fs/promises"
|
|
1566
|
+
);
|
|
1567
|
+
os = osModule;
|
|
1568
|
+
fs = fsModule;
|
|
1569
|
+
} catch {
|
|
1570
|
+
this.reportError("SYSTEM_METRICS", "System metrics require Node.js (not available in bundled/browser environments)");
|
|
1571
|
+
return;
|
|
1572
|
+
}
|
|
1573
|
+
try {
|
|
1574
|
+
const hostname = os.hostname();
|
|
1575
|
+
const cpuPercent = await this.measureCpuPercent(os);
|
|
1576
|
+
const memoryTotal = os.totalmem();
|
|
1577
|
+
const memoryFree = os.freemem();
|
|
1578
|
+
const memoryUsed = memoryTotal - memoryFree;
|
|
1579
|
+
const memoryPercent = memoryTotal > 0 ? memoryUsed / memoryTotal * 100 : 0;
|
|
1580
|
+
const disks = await this.collectDiskMetrics(diskPaths, fs);
|
|
1581
|
+
await this.submitSystemMetric({
|
|
1582
|
+
hostname,
|
|
1583
|
+
cpuPercent,
|
|
1584
|
+
memoryTotal,
|
|
1585
|
+
memoryUsed,
|
|
1586
|
+
memoryPercent,
|
|
1587
|
+
disks
|
|
1588
|
+
});
|
|
1589
|
+
} catch (err) {
|
|
1590
|
+
this.reportError("SYSTEM_METRICS", "Failed to collect system metrics", err);
|
|
1591
|
+
}
|
|
1592
|
+
}
|
|
1593
|
+
/**
|
|
1594
|
+
* Measure CPU utilization by sampling cpus() twice with a 500ms gap.
|
|
1595
|
+
* Returns a percentage (0–100).
|
|
1596
|
+
*/
|
|
1597
|
+
async measureCpuPercent(os) {
|
|
1598
|
+
const sample1 = os.cpus();
|
|
1599
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1600
|
+
const sample2 = os.cpus();
|
|
1601
|
+
let totalIdle = 0;
|
|
1602
|
+
let totalTick = 0;
|
|
1603
|
+
for (let i = 0; i < sample2.length; i++) {
|
|
1604
|
+
const prev = sample1[i];
|
|
1605
|
+
const curr = sample2[i];
|
|
1606
|
+
const prevTotal = Object.values(prev.times).reduce((a, b) => a + b, 0);
|
|
1607
|
+
const currTotal = Object.values(curr.times).reduce((a, b) => a + b, 0);
|
|
1608
|
+
totalTick += currTotal - prevTotal;
|
|
1609
|
+
totalIdle += curr.times.idle - prev.times.idle;
|
|
1610
|
+
}
|
|
1611
|
+
const idlePercent = totalTick > 0 ? totalIdle / totalTick * 100 : 0;
|
|
1612
|
+
return Math.max(0, Math.min(100, 100 - idlePercent));
|
|
1613
|
+
}
|
|
1614
|
+
/**
|
|
1615
|
+
* Collect disk usage for each configured path using fs.statfs() (Node 18.15+).
|
|
1616
|
+
*/
|
|
1617
|
+
async collectDiskMetrics(paths, fs) {
|
|
1618
|
+
const results = [];
|
|
1619
|
+
for (const diskPath of paths) {
|
|
1620
|
+
try {
|
|
1621
|
+
const stat = await fs.statfs(diskPath);
|
|
1622
|
+
const total = stat.bsize * stat.blocks;
|
|
1623
|
+
const free = stat.bsize * stat.bavail;
|
|
1624
|
+
const used = total - free;
|
|
1625
|
+
const percent = total > 0 ? used / total * 100 : 0;
|
|
1626
|
+
results.push({
|
|
1627
|
+
path: diskPath,
|
|
1628
|
+
total: Math.round(total),
|
|
1629
|
+
used: Math.round(used),
|
|
1630
|
+
percent: Math.round(percent * 10) / 10
|
|
1631
|
+
// 1 decimal place
|
|
1632
|
+
});
|
|
1633
|
+
} catch {
|
|
1634
|
+
this.reportError("SYSTEM_METRICS", `Failed to get disk stats for path: ${diskPath}`);
|
|
1635
|
+
}
|
|
1636
|
+
}
|
|
1637
|
+
return results;
|
|
1638
|
+
}
|
|
1639
|
+
/**
|
|
1640
|
+
* Submit a system metric snapshot to the monitoring server.
|
|
1641
|
+
*/
|
|
1642
|
+
async submitSystemMetric(metric) {
|
|
1643
|
+
try {
|
|
1644
|
+
const response = await this.fetchWithTimeout(`${this.endpoint}/api/v1/system-metrics`, {
|
|
1645
|
+
method: "POST",
|
|
1646
|
+
headers: {
|
|
1647
|
+
"Content-Type": "application/json",
|
|
1648
|
+
"Authorization": `Bearer ${this.apiKey}`
|
|
1649
|
+
},
|
|
1650
|
+
body: JSON.stringify(metric)
|
|
1651
|
+
});
|
|
1652
|
+
if (!response.ok) {
|
|
1653
|
+
const errorText = await response.text().catch(() => "");
|
|
1654
|
+
throw new Error(`HTTP ${response.status}${errorText ? `: ${this.sanitizeErrorResponse(errorText)}` : ""}`);
|
|
1655
|
+
}
|
|
1656
|
+
} catch (err) {
|
|
1657
|
+
this.reportError("SYSTEM_METRICS", "Failed to submit system metric to server", err);
|
|
1658
|
+
}
|
|
1659
|
+
}
|
|
1412
1660
|
};
|
|
1413
1661
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1414
1662
|
0 && (module.exports = {
|
package/dist/index.mjs
CHANGED
|
@@ -58,6 +58,14 @@ 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;
|
|
68
|
+
this.metricsSettingsCheckTimer = null;
|
|
61
69
|
if (!config.apiKey || config.apiKey.trim().length === 0) {
|
|
62
70
|
throw new Error("[MonitorClient] API key is required");
|
|
63
71
|
}
|
|
@@ -117,24 +125,28 @@ var MonitorClient = class {
|
|
|
117
125
|
this.auditTimeoutMs = Math.min(CONFIG_LIMITS.MAX_AUDIT_TIMEOUT_MS, Math.max(1e3, config.auditTimeoutMs || CONFIG_LIMITS.AUDIT_TIMEOUT_MS));
|
|
118
126
|
this.registryTimeoutMs = Math.min(CONFIG_LIMITS.MAX_REGISTRY_TIMEOUT_MS, Math.max(1e3, config.registryTimeoutMs || CONFIG_LIMITS.REGISTRY_TIMEOUT_MS));
|
|
119
127
|
this.npmRegistryUrl = (config.npmRegistryUrl || "https://registry.npmjs.org").replace(/\/$/, "");
|
|
120
|
-
this.healthCheckEnabled = config.healthCheckEnabled
|
|
128
|
+
this.healthCheckEnabled = config.healthCheckEnabled ?? true;
|
|
121
129
|
this.healthCheckFetchIntervalMs = config.healthCheckFetchIntervalMs || CONFIG_LIMITS.HEALTH_CHECK_FETCH_INTERVAL_MS;
|
|
122
130
|
this.startFlushTimer();
|
|
123
131
|
if (this.trackDependencies) {
|
|
124
132
|
this.syncDependencies().catch((err) => {
|
|
125
|
-
|
|
133
|
+
this.reportError("DEPENDENCY_SYNC", "Failed to sync dependencies on startup", err);
|
|
126
134
|
});
|
|
127
135
|
}
|
|
128
136
|
if (this.autoAudit) {
|
|
129
137
|
this.setupAutoAudit().catch((err) => {
|
|
130
|
-
|
|
138
|
+
this.reportError("AUDIT_SETUP", "Failed to setup auto audit", err);
|
|
131
139
|
});
|
|
132
140
|
}
|
|
133
141
|
if (this.healthCheckEnabled) {
|
|
134
142
|
this.setupHealthCheckPolling().catch((err) => {
|
|
135
|
-
|
|
143
|
+
this.reportError("HEALTH_POLLING_SETUP", "Failed to setup health check polling", err);
|
|
136
144
|
});
|
|
137
145
|
}
|
|
146
|
+
this.startSdkErrorFlushTimer();
|
|
147
|
+
this.setupSystemMetricsCollection().catch((err) => {
|
|
148
|
+
this.reportError("SYSTEM_METRICS_SETUP", "Failed to setup system metrics collection", err);
|
|
149
|
+
});
|
|
138
150
|
}
|
|
139
151
|
/**
|
|
140
152
|
* Security: Validate and sanitize metadata to prevent oversized payloads
|
|
@@ -284,8 +296,66 @@ var MonitorClient = class {
|
|
|
284
296
|
this.stopFlushTimer();
|
|
285
297
|
this.stopAuditIntervalTimer();
|
|
286
298
|
this.stopHealthCheckTimers();
|
|
299
|
+
this.stopSdkErrorFlushTimer();
|
|
300
|
+
this.stopMetricsCollection();
|
|
287
301
|
await this.flush();
|
|
288
302
|
await this.flushHealthResults();
|
|
303
|
+
await this.flushSdkErrors();
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* Queue an SDK error to be reported to the server's system errors page.
|
|
307
|
+
* Fire-and-forget — never throws. If reporting itself fails, logs to console only.
|
|
308
|
+
* Throttled to max 20 errors per minute to prevent flooding.
|
|
309
|
+
*/
|
|
310
|
+
reportError(category, message, err) {
|
|
311
|
+
if (this.sdkErrorsInCurrentWindow >= 20) return;
|
|
312
|
+
this.sdkErrorsInCurrentWindow++;
|
|
313
|
+
const errorMessage = err instanceof Error ? `${message}: ${err.message}` : message;
|
|
314
|
+
const stack = err instanceof Error ? err.stack : void 0;
|
|
315
|
+
this.sdkErrorQueue.push({ category, message: errorMessage, stack });
|
|
316
|
+
if (this.sdkErrorQueue.length >= 20) {
|
|
317
|
+
this.flushSdkErrors().catch(() => {
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
startSdkErrorFlushTimer() {
|
|
322
|
+
this.sdkErrorFlushTimer = setInterval(() => {
|
|
323
|
+
this.flushSdkErrors().catch(() => {
|
|
324
|
+
});
|
|
325
|
+
}, 3e4);
|
|
326
|
+
this.sdkErrorWindowResetTimer = setInterval(() => {
|
|
327
|
+
this.sdkErrorsInCurrentWindow = 0;
|
|
328
|
+
}, 6e4);
|
|
329
|
+
}
|
|
330
|
+
stopSdkErrorFlushTimer() {
|
|
331
|
+
if (this.sdkErrorFlushTimer) {
|
|
332
|
+
clearInterval(this.sdkErrorFlushTimer);
|
|
333
|
+
this.sdkErrorFlushTimer = null;
|
|
334
|
+
}
|
|
335
|
+
if (this.sdkErrorWindowResetTimer) {
|
|
336
|
+
clearInterval(this.sdkErrorWindowResetTimer);
|
|
337
|
+
this.sdkErrorWindowResetTimer = null;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
async flushSdkErrors() {
|
|
341
|
+
if (this.sdkErrorQueue.length === 0) return;
|
|
342
|
+
const errors = [...this.sdkErrorQueue];
|
|
343
|
+
this.sdkErrorQueue = [];
|
|
344
|
+
try {
|
|
345
|
+
const response = await this.fetchWithTimeout(`${this.endpoint}/api/v1/sdk-errors`, {
|
|
346
|
+
method: "POST",
|
|
347
|
+
headers: {
|
|
348
|
+
"Content-Type": "application/json",
|
|
349
|
+
"Authorization": `Bearer ${this.apiKey}`
|
|
350
|
+
},
|
|
351
|
+
body: JSON.stringify({ errors })
|
|
352
|
+
});
|
|
353
|
+
if (!response.ok) {
|
|
354
|
+
console.warn(`[MonitorClient] Failed to report SDK errors: HTTP ${response.status}`);
|
|
355
|
+
}
|
|
356
|
+
} catch (err) {
|
|
357
|
+
console.warn("[MonitorClient] Failed to flush SDK errors to server:", err instanceof Error ? err.message : String(err));
|
|
358
|
+
}
|
|
289
359
|
}
|
|
290
360
|
stopAuditIntervalTimer() {
|
|
291
361
|
if (this.auditIntervalTimer) {
|
|
@@ -347,14 +417,14 @@ var MonitorClient = class {
|
|
|
347
417
|
const intervalMs = intervalHours * 60 * 60 * 1e3;
|
|
348
418
|
this.auditIntervalTimer = setInterval(() => {
|
|
349
419
|
this.runScanAndTrackTime().catch((err) => {
|
|
350
|
-
|
|
420
|
+
this.reportError("AUDIT_SCAN", "Auto audit scan failed", err);
|
|
351
421
|
});
|
|
352
422
|
}, intervalMs);
|
|
353
423
|
}
|
|
354
424
|
console.log("[MonitorClient] Polling for scan requests enabled (every 5 minutes)");
|
|
355
425
|
this.settingsPollingTimer = setInterval(() => {
|
|
356
426
|
this.checkForScanRequest().catch((err) => {
|
|
357
|
-
|
|
427
|
+
this.reportError("SETTINGS_POLL", "Scan request check failed", err);
|
|
358
428
|
});
|
|
359
429
|
}, CONFIG_LIMITS.SETTINGS_POLL_INTERVAL_MS);
|
|
360
430
|
}
|
|
@@ -375,7 +445,7 @@ var MonitorClient = class {
|
|
|
375
445
|
const duration = Date.now() - startTime;
|
|
376
446
|
console.log(`[MonitorClient] Vulnerability scan completed in ${duration}ms`);
|
|
377
447
|
} catch (err) {
|
|
378
|
-
|
|
448
|
+
this.reportError("VULNERABILITY_SCAN", "Vulnerability scan failed", err);
|
|
379
449
|
}
|
|
380
450
|
}
|
|
381
451
|
/**
|
|
@@ -402,7 +472,7 @@ var MonitorClient = class {
|
|
|
402
472
|
}
|
|
403
473
|
}
|
|
404
474
|
} catch (err) {
|
|
405
|
-
|
|
475
|
+
this.reportError("SETTINGS_POLL", "Failed to check for scan request", err);
|
|
406
476
|
}
|
|
407
477
|
}
|
|
408
478
|
enqueue(payload) {
|
|
@@ -489,7 +559,7 @@ var MonitorClient = class {
|
|
|
489
559
|
);
|
|
490
560
|
console.log("[MonitorClient] Technology sync completed successfully");
|
|
491
561
|
} catch (err) {
|
|
492
|
-
|
|
562
|
+
this.reportError("DEPENDENCY_SYNC", "Technology sync failed", err);
|
|
493
563
|
}
|
|
494
564
|
}
|
|
495
565
|
async performDependencySync(signal) {
|
|
@@ -993,7 +1063,7 @@ var MonitorClient = class {
|
|
|
993
1063
|
const result = await response.json();
|
|
994
1064
|
return result.data;
|
|
995
1065
|
} catch (err) {
|
|
996
|
-
|
|
1066
|
+
this.reportError("VULNERABILITY_SCAN", "Failed to audit dependencies", err);
|
|
997
1067
|
return null;
|
|
998
1068
|
}
|
|
999
1069
|
}
|
|
@@ -1023,7 +1093,7 @@ var MonitorClient = class {
|
|
|
1023
1093
|
}
|
|
1024
1094
|
return result;
|
|
1025
1095
|
} catch (err) {
|
|
1026
|
-
|
|
1096
|
+
this.reportError("VULNERABILITY_SCAN", "Multi-path audit failed", err);
|
|
1027
1097
|
return null;
|
|
1028
1098
|
}
|
|
1029
1099
|
}
|
|
@@ -1242,6 +1312,19 @@ var MonitorClient = class {
|
|
|
1242
1312
|
"Content-Type": "application/json"
|
|
1243
1313
|
};
|
|
1244
1314
|
}
|
|
1315
|
+
if (endpoint.allowInsecureTls) {
|
|
1316
|
+
try {
|
|
1317
|
+
const undici = await import(
|
|
1318
|
+
/* webpackIgnore: true */
|
|
1319
|
+
"undici"
|
|
1320
|
+
);
|
|
1321
|
+
const Agent = undici.Agent;
|
|
1322
|
+
requestOptions.dispatcher = new Agent({
|
|
1323
|
+
connect: { rejectUnauthorized: false }
|
|
1324
|
+
});
|
|
1325
|
+
} catch {
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1245
1328
|
const response = await this.fetchWithTimeout(
|
|
1246
1329
|
endpoint.url,
|
|
1247
1330
|
requestOptions,
|
|
@@ -1285,12 +1368,12 @@ var MonitorClient = class {
|
|
|
1285
1368
|
await this.fetchAndScheduleHealthChecks();
|
|
1286
1369
|
this.healthCheckFetchTimer = setInterval(() => {
|
|
1287
1370
|
this.fetchAndScheduleHealthChecks().catch((err) => {
|
|
1288
|
-
|
|
1371
|
+
this.reportError("HEALTH_ENDPOINT_FETCH", "Failed to fetch and schedule health checks", err);
|
|
1289
1372
|
});
|
|
1290
1373
|
}, this.healthCheckFetchIntervalMs);
|
|
1291
1374
|
this.healthCheckFlushTimer = setInterval(() => {
|
|
1292
1375
|
this.flushHealthResults().catch((err) => {
|
|
1293
|
-
|
|
1376
|
+
this.reportError("HEALTH_RESULTS_FLUSH", "Failed to flush health results", err);
|
|
1294
1377
|
});
|
|
1295
1378
|
}, CONFIG_LIMITS.HEALTH_CHECK_FLUSH_INTERVAL_MS);
|
|
1296
1379
|
}
|
|
@@ -1311,7 +1394,7 @@ var MonitorClient = class {
|
|
|
1311
1394
|
this.scheduleHealthCheck(endpoint);
|
|
1312
1395
|
}
|
|
1313
1396
|
} catch (err) {
|
|
1314
|
-
|
|
1397
|
+
this.reportError("HEALTH_ENDPOINT_FETCH", "Failed to fetch health endpoints from server", err);
|
|
1315
1398
|
}
|
|
1316
1399
|
}
|
|
1317
1400
|
/**
|
|
@@ -1320,11 +1403,11 @@ var MonitorClient = class {
|
|
|
1320
1403
|
scheduleHealthCheck(endpoint) {
|
|
1321
1404
|
const intervalMs = Math.max(CONFIG_LIMITS.HEALTH_CHECK_MIN_INTERVAL_MS, endpoint.intervalMs);
|
|
1322
1405
|
this.runHealthCheck(endpoint).catch((err) => {
|
|
1323
|
-
|
|
1406
|
+
this.reportError("HEALTH_CHECK", `Health check failed for ${endpoint.name}`, err);
|
|
1324
1407
|
});
|
|
1325
1408
|
const timer = setInterval(() => {
|
|
1326
1409
|
this.runHealthCheck(endpoint).catch((err) => {
|
|
1327
|
-
|
|
1410
|
+
this.reportError("HEALTH_CHECK", `Health check failed for ${endpoint.name}`, err);
|
|
1328
1411
|
});
|
|
1329
1412
|
}, intervalMs);
|
|
1330
1413
|
this.healthCheckTimers.set(endpoint.id, timer);
|
|
@@ -1350,7 +1433,7 @@ var MonitorClient = class {
|
|
|
1350
1433
|
const response = await this.submitHealthResults(results);
|
|
1351
1434
|
console.log(`[MonitorClient] Submitted ${response.processed}/${response.total} health check results`);
|
|
1352
1435
|
} catch (err) {
|
|
1353
|
-
|
|
1436
|
+
this.reportError("HEALTH_RESULTS_FLUSH", "Failed to submit health check results to server", err);
|
|
1354
1437
|
if (this.healthCheckResultsQueue.length + results.length <= CONFIG_LIMITS.HEALTH_CHECK_MAX_BATCH_SIZE) {
|
|
1355
1438
|
this.healthCheckResultsQueue.unshift(...results);
|
|
1356
1439
|
}
|
|
@@ -1373,6 +1456,171 @@ var MonitorClient = class {
|
|
|
1373
1456
|
this.healthCheckTimers.delete(endpointId);
|
|
1374
1457
|
}
|
|
1375
1458
|
}
|
|
1459
|
+
// ---- System Metrics (on-premise only) ----
|
|
1460
|
+
/**
|
|
1461
|
+
* Start system metrics collection based on server-configured settings.
|
|
1462
|
+
* Only runs if metricsEnabled is true in project settings.
|
|
1463
|
+
* Collects CPU, RAM, and disk usage using built-in Node.js modules.
|
|
1464
|
+
*/
|
|
1465
|
+
async setupSystemMetricsCollection() {
|
|
1466
|
+
const settings = await this.fetchProjectSettings();
|
|
1467
|
+
if (!settings?.metricsEnabled) {
|
|
1468
|
+
this.metricsSettingsCheckTimer = setInterval(() => {
|
|
1469
|
+
this.fetchProjectSettings().then((latestSettings) => {
|
|
1470
|
+
if (latestSettings?.metricsEnabled) {
|
|
1471
|
+
clearInterval(this.metricsSettingsCheckTimer);
|
|
1472
|
+
this.metricsSettingsCheckTimer = null;
|
|
1473
|
+
this.startMetricsCollection(latestSettings);
|
|
1474
|
+
}
|
|
1475
|
+
}).catch(() => {
|
|
1476
|
+
});
|
|
1477
|
+
}, 3e5);
|
|
1478
|
+
return;
|
|
1479
|
+
}
|
|
1480
|
+
this.startMetricsCollection(settings);
|
|
1481
|
+
}
|
|
1482
|
+
startMetricsCollection(settings) {
|
|
1483
|
+
const intervalMs = Math.max(3e4, (settings.metricsIntervalSeconds ?? 3600) * 1e3);
|
|
1484
|
+
console.log(`[MonitorClient] System metrics collection enabled (every ${intervalMs / 1e3}s)`);
|
|
1485
|
+
const initialDiskPaths = settings.metricsDiskPaths ?? ["/"];
|
|
1486
|
+
this.collectAndSubmitMetrics(initialDiskPaths).catch((err) => {
|
|
1487
|
+
this.reportError("SYSTEM_METRICS", "Initial system metrics collection failed", err);
|
|
1488
|
+
});
|
|
1489
|
+
this.metricsCollectionTimer = setInterval(() => {
|
|
1490
|
+
this.fetchProjectSettings().then((latestSettings) => {
|
|
1491
|
+
if (!latestSettings?.metricsEnabled) return;
|
|
1492
|
+
const diskPaths = latestSettings.metricsDiskPaths ?? ["/"];
|
|
1493
|
+
return this.collectAndSubmitMetrics(diskPaths);
|
|
1494
|
+
}).catch((err) => {
|
|
1495
|
+
this.reportError("SYSTEM_METRICS", "Scheduled system metrics collection failed", err);
|
|
1496
|
+
});
|
|
1497
|
+
}, intervalMs);
|
|
1498
|
+
}
|
|
1499
|
+
stopMetricsCollection() {
|
|
1500
|
+
if (this.metricsCollectionTimer) {
|
|
1501
|
+
clearInterval(this.metricsCollectionTimer);
|
|
1502
|
+
this.metricsCollectionTimer = null;
|
|
1503
|
+
}
|
|
1504
|
+
if (this.metricsSettingsCheckTimer) {
|
|
1505
|
+
clearInterval(this.metricsSettingsCheckTimer);
|
|
1506
|
+
this.metricsSettingsCheckTimer = null;
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
/**
|
|
1510
|
+
* Collect system metrics (CPU, RAM, disk) using built-in Node.js modules
|
|
1511
|
+
* and submit to the monitoring server.
|
|
1512
|
+
*
|
|
1513
|
+
* Uses: os.cpus(), os.totalmem(), os.freemem(), os.hostname(), fs.statfs()
|
|
1514
|
+
* Zero dependencies — all built-in Node.js 18.15+
|
|
1515
|
+
*/
|
|
1516
|
+
async collectAndSubmitMetrics(diskPaths) {
|
|
1517
|
+
if (typeof window !== "undefined" || typeof document !== "undefined") {
|
|
1518
|
+
return;
|
|
1519
|
+
}
|
|
1520
|
+
let os;
|
|
1521
|
+
let fs;
|
|
1522
|
+
try {
|
|
1523
|
+
const osModule = await import(
|
|
1524
|
+
/* webpackIgnore: true */
|
|
1525
|
+
"os"
|
|
1526
|
+
);
|
|
1527
|
+
const fsModule = await import(
|
|
1528
|
+
/* webpackIgnore: true */
|
|
1529
|
+
"fs/promises"
|
|
1530
|
+
);
|
|
1531
|
+
os = osModule;
|
|
1532
|
+
fs = fsModule;
|
|
1533
|
+
} catch {
|
|
1534
|
+
this.reportError("SYSTEM_METRICS", "System metrics require Node.js (not available in bundled/browser environments)");
|
|
1535
|
+
return;
|
|
1536
|
+
}
|
|
1537
|
+
try {
|
|
1538
|
+
const hostname = os.hostname();
|
|
1539
|
+
const cpuPercent = await this.measureCpuPercent(os);
|
|
1540
|
+
const memoryTotal = os.totalmem();
|
|
1541
|
+
const memoryFree = os.freemem();
|
|
1542
|
+
const memoryUsed = memoryTotal - memoryFree;
|
|
1543
|
+
const memoryPercent = memoryTotal > 0 ? memoryUsed / memoryTotal * 100 : 0;
|
|
1544
|
+
const disks = await this.collectDiskMetrics(diskPaths, fs);
|
|
1545
|
+
await this.submitSystemMetric({
|
|
1546
|
+
hostname,
|
|
1547
|
+
cpuPercent,
|
|
1548
|
+
memoryTotal,
|
|
1549
|
+
memoryUsed,
|
|
1550
|
+
memoryPercent,
|
|
1551
|
+
disks
|
|
1552
|
+
});
|
|
1553
|
+
} catch (err) {
|
|
1554
|
+
this.reportError("SYSTEM_METRICS", "Failed to collect system metrics", err);
|
|
1555
|
+
}
|
|
1556
|
+
}
|
|
1557
|
+
/**
|
|
1558
|
+
* Measure CPU utilization by sampling cpus() twice with a 500ms gap.
|
|
1559
|
+
* Returns a percentage (0–100).
|
|
1560
|
+
*/
|
|
1561
|
+
async measureCpuPercent(os) {
|
|
1562
|
+
const sample1 = os.cpus();
|
|
1563
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1564
|
+
const sample2 = os.cpus();
|
|
1565
|
+
let totalIdle = 0;
|
|
1566
|
+
let totalTick = 0;
|
|
1567
|
+
for (let i = 0; i < sample2.length; i++) {
|
|
1568
|
+
const prev = sample1[i];
|
|
1569
|
+
const curr = sample2[i];
|
|
1570
|
+
const prevTotal = Object.values(prev.times).reduce((a, b) => a + b, 0);
|
|
1571
|
+
const currTotal = Object.values(curr.times).reduce((a, b) => a + b, 0);
|
|
1572
|
+
totalTick += currTotal - prevTotal;
|
|
1573
|
+
totalIdle += curr.times.idle - prev.times.idle;
|
|
1574
|
+
}
|
|
1575
|
+
const idlePercent = totalTick > 0 ? totalIdle / totalTick * 100 : 0;
|
|
1576
|
+
return Math.max(0, Math.min(100, 100 - idlePercent));
|
|
1577
|
+
}
|
|
1578
|
+
/**
|
|
1579
|
+
* Collect disk usage for each configured path using fs.statfs() (Node 18.15+).
|
|
1580
|
+
*/
|
|
1581
|
+
async collectDiskMetrics(paths, fs) {
|
|
1582
|
+
const results = [];
|
|
1583
|
+
for (const diskPath of paths) {
|
|
1584
|
+
try {
|
|
1585
|
+
const stat = await fs.statfs(diskPath);
|
|
1586
|
+
const total = stat.bsize * stat.blocks;
|
|
1587
|
+
const free = stat.bsize * stat.bavail;
|
|
1588
|
+
const used = total - free;
|
|
1589
|
+
const percent = total > 0 ? used / total * 100 : 0;
|
|
1590
|
+
results.push({
|
|
1591
|
+
path: diskPath,
|
|
1592
|
+
total: Math.round(total),
|
|
1593
|
+
used: Math.round(used),
|
|
1594
|
+
percent: Math.round(percent * 10) / 10
|
|
1595
|
+
// 1 decimal place
|
|
1596
|
+
});
|
|
1597
|
+
} catch {
|
|
1598
|
+
this.reportError("SYSTEM_METRICS", `Failed to get disk stats for path: ${diskPath}`);
|
|
1599
|
+
}
|
|
1600
|
+
}
|
|
1601
|
+
return results;
|
|
1602
|
+
}
|
|
1603
|
+
/**
|
|
1604
|
+
* Submit a system metric snapshot to the monitoring server.
|
|
1605
|
+
*/
|
|
1606
|
+
async submitSystemMetric(metric) {
|
|
1607
|
+
try {
|
|
1608
|
+
const response = await this.fetchWithTimeout(`${this.endpoint}/api/v1/system-metrics`, {
|
|
1609
|
+
method: "POST",
|
|
1610
|
+
headers: {
|
|
1611
|
+
"Content-Type": "application/json",
|
|
1612
|
+
"Authorization": `Bearer ${this.apiKey}`
|
|
1613
|
+
},
|
|
1614
|
+
body: JSON.stringify(metric)
|
|
1615
|
+
});
|
|
1616
|
+
if (!response.ok) {
|
|
1617
|
+
const errorText = await response.text().catch(() => "");
|
|
1618
|
+
throw new Error(`HTTP ${response.status}${errorText ? `: ${this.sanitizeErrorResponse(errorText)}` : ""}`);
|
|
1619
|
+
}
|
|
1620
|
+
} catch (err) {
|
|
1621
|
+
this.reportError("SYSTEM_METRICS", "Failed to submit system metric to server", err);
|
|
1622
|
+
}
|
|
1623
|
+
}
|
|
1376
1624
|
};
|
|
1377
1625
|
export {
|
|
1378
1626
|
MonitorClient
|
package/package.json
CHANGED