Skip to content

Commit d48f10d

Browse files
committed
feat(monitoring): modulo shared/monitoring/ — metriche CPU/mem, heartbeat agenti, alerter con soglie runtime
2 parents f932538 + 37aaa19 commit d48f10d

4 files changed

Lines changed: 265 additions & 0 deletions

File tree

shared/monitoring/alerter.ts

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
/**
2+
* Monitoring — Alerter: soglie, valutazione, alert attivi
3+
*/
4+
import type { AlertThreshold, AlertEvent, SystemMetrics } from './types.js';
5+
6+
const thresholds = new Map<string, AlertThreshold>();
7+
const activeAlerts = new Map<string, AlertEvent>();
8+
9+
/** Registra una soglia di alert */
10+
export function defineThreshold(threshold: AlertThreshold): void {
11+
thresholds.set(threshold.id, threshold);
12+
}
13+
14+
/** Rimuove una soglia */
15+
export function removeThreshold(id: string): boolean {
16+
activeAlerts.delete(id);
17+
return thresholds.delete(id);
18+
}
19+
20+
/** Lista soglie registrate */
21+
export function getThresholds(): AlertThreshold[] {
22+
return [...thresholds.values()];
23+
}
24+
25+
/** Valuta le metriche contro le soglie e ritorna nuovi alert */
26+
export function checkThresholds(metrics: SystemMetrics): AlertEvent[] {
27+
const triggered: AlertEvent[] = [];
28+
29+
for (const t of thresholds.values()) {
30+
if (t.metric === 'heartbeat') continue;
31+
const current = metrics[t.metric] as number;
32+
const fired = evaluate(current, t.operator, t.value);
33+
34+
if (fired) {
35+
const event: AlertEvent = {
36+
thresholdId: t.id, metric: t.metric,
37+
currentValue: current, thresholdValue: t.value,
38+
operator: t.operator, description: t.description,
39+
triggeredAt: Date.now(),
40+
};
41+
activeAlerts.set(t.id, event);
42+
triggered.push(event);
43+
} else {
44+
activeAlerts.delete(t.id);
45+
}
46+
}
47+
48+
return triggered;
49+
}
50+
51+
/** Valuta alert heartbeat (chiamare con tempo dall'ultimo heartbeat in ms) */
52+
export function checkHeartbeatAlert(agentId: string, elapsedMs: number): AlertEvent | null {
53+
for (const t of thresholds.values()) {
54+
if (t.metric !== 'heartbeat') continue;
55+
if (evaluate(elapsedMs, t.operator, t.value)) {
56+
const event: AlertEvent = {
57+
thresholdId: t.id, metric: `heartbeat:${agentId}`,
58+
currentValue: elapsedMs, thresholdValue: t.value,
59+
operator: t.operator, description: `${t.description} (${agentId})`,
60+
triggeredAt: Date.now(),
61+
};
62+
activeAlerts.set(`${t.id}:${agentId}`, event);
63+
return event;
64+
}
65+
}
66+
return null;
67+
}
68+
69+
/** Ritorna tutti gli alert attivi */
70+
export function getActiveAlerts(): AlertEvent[] {
71+
return [...activeAlerts.values()];
72+
}
73+
74+
/** Rimuove un alert attivo */
75+
export function clearAlert(thresholdId: string): boolean {
76+
return activeAlerts.delete(thresholdId);
77+
}
78+
79+
/** Rimuove tutti gli alert */
80+
export function clearAllAlerts(): void {
81+
activeAlerts.clear();
82+
}
83+
84+
/** Reset completo (per test) */
85+
export function resetAlerter(): void {
86+
thresholds.clear();
87+
activeAlerts.clear();
88+
}
89+
90+
function evaluate(current: number, op: string, threshold: number): boolean {
91+
if (op === 'gt') return current > threshold;
92+
if (op === 'lt') return current < threshold;
93+
if (op === 'eq') return current === threshold;
94+
return false;
95+
}

shared/monitoring/index.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/**
2+
* Monitoring — Monitoraggio runtime, heartbeat, alerting
3+
*/
4+
5+
export type {
6+
SystemMetrics, AgentHeartbeat,
7+
AlertThreshold, AlertEvent, MonitorConfig,
8+
} from './types.js';
9+
export { DEFAULT_MONITOR_CONFIG } from './types.js';
10+
11+
export {
12+
configureMonitor, collectMetrics, getMetricsHistory,
13+
registerHeartbeat, getAgentStatus, getAllAgentStatuses,
14+
checkHeartbeats, removeAgent, resetMonitor,
15+
} from './monitor.js';
16+
17+
export {
18+
defineThreshold, removeThreshold, getThresholds,
19+
checkThresholds, checkHeartbeatAlert,
20+
getActiveAlerts, clearAlert, clearAllAlerts, resetAlerter,
21+
} from './alerter.js';

shared/monitoring/monitor.ts

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
/**
2+
* Monitoring — Raccolta metriche sistema e heartbeat agenti
3+
*/
4+
import os from 'node:os';
5+
import type { SystemMetrics, AgentHeartbeat, MonitorConfig } from './types.js';
6+
import { DEFAULT_MONITOR_CONFIG } from './types.js';
7+
8+
const heartbeats = new Map<string, { lastSeen: number; metadata?: Record<string, unknown> }>();
9+
const metricsHistory: SystemMetrics[] = [];
10+
let config: Required<MonitorConfig> = { ...DEFAULT_MONITOR_CONFIG };
11+
12+
/** Configura il monitor */
13+
export function configureMonitor(opts: MonitorConfig): void {
14+
config = { ...DEFAULT_MONITOR_CONFIG, ...opts };
15+
}
16+
17+
/** Raccoglie metriche di sistema correnti */
18+
export function collectMetrics(): SystemMetrics {
19+
const totalMem = os.totalmem();
20+
const freeMem = os.freemem();
21+
const usedMem = totalMem - freeMem;
22+
const cpus = os.cpus();
23+
let cpuUsage = 0;
24+
if (cpus.length > 0) {
25+
const totals = cpus.reduce((acc, c) => {
26+
const t = Object.values(c.times).reduce((s, v) => s + v, 0);
27+
return { idle: acc.idle + c.times.idle, total: acc.total + t };
28+
}, { idle: 0, total: 0 });
29+
cpuUsage = Math.round((1 - totals.idle / totals.total) * 10000) / 100;
30+
}
31+
32+
const metrics: SystemMetrics = {
33+
cpuUsage,
34+
memoryUsedMB: Math.round(usedMem / 1048576),
35+
memoryTotalMB: Math.round(totalMem / 1048576),
36+
memoryPercent: Math.round((usedMem / totalMem) * 10000) / 100,
37+
uptimeSeconds: Math.floor(os.uptime()),
38+
loadAvg: os.loadavg() as [number, number, number],
39+
timestamp: Date.now(),
40+
};
41+
42+
metricsHistory.push(metrics);
43+
while (metricsHistory.length > config.metricsHistorySize) metricsHistory.shift();
44+
45+
return metrics;
46+
}
47+
48+
/** Ritorna storico metriche */
49+
export function getMetricsHistory(): SystemMetrics[] {
50+
return [...metricsHistory];
51+
}
52+
53+
/** Registra heartbeat di un agente */
54+
export function registerHeartbeat(agentId: string, metadata?: Record<string, unknown>): void {
55+
heartbeats.set(agentId, { lastSeen: Date.now(), metadata });
56+
}
57+
58+
/** Stato di un singolo agente */
59+
export function getAgentStatus(agentId: string): AgentHeartbeat | null {
60+
const hb = heartbeats.get(agentId);
61+
if (!hb) return null;
62+
return { agentId, lastSeen: hb.lastSeen, status: resolveStatus(hb.lastSeen), metadata: hb.metadata };
63+
}
64+
65+
/** Stato di tutti gli agenti registrati */
66+
export function getAllAgentStatuses(): AgentHeartbeat[] {
67+
return [...heartbeats.entries()].map(([agentId, hb]) => ({
68+
agentId, lastSeen: hb.lastSeen, status: resolveStatus(hb.lastSeen), metadata: hb.metadata,
69+
}));
70+
}
71+
72+
/** Controlla heartbeat e ritorna agenti stale/dead */
73+
export function checkHeartbeats(): AgentHeartbeat[] {
74+
return getAllAgentStatuses().filter(a => a.status !== 'alive');
75+
}
76+
77+
/** Rimuove un agente dal monitoraggio */
78+
export function removeAgent(agentId: string): boolean {
79+
return heartbeats.delete(agentId);
80+
}
81+
82+
/** Reset completo (per test) */
83+
export function resetMonitor(): void {
84+
heartbeats.clear();
85+
metricsHistory.length = 0;
86+
config = { ...DEFAULT_MONITOR_CONFIG };
87+
}
88+
89+
function resolveStatus(lastSeen: number): 'alive' | 'stale' | 'dead' {
90+
const elapsed = Date.now() - lastSeen;
91+
if (elapsed > config.heartbeatDeadMs) return 'dead';
92+
if (elapsed > config.heartbeatStaleMs) return 'stale';
93+
return 'alive';
94+
}

shared/monitoring/types.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/**
2+
* Monitoring — Tipi per monitoraggio runtime
3+
*/
4+
5+
/** Metriche di sistema */
6+
export interface SystemMetrics {
7+
cpuUsage: number;
8+
memoryUsedMB: number;
9+
memoryTotalMB: number;
10+
memoryPercent: number;
11+
uptimeSeconds: number;
12+
loadAvg: [number, number, number];
13+
timestamp: number;
14+
}
15+
16+
/** Heartbeat di un agente */
17+
export interface AgentHeartbeat {
18+
agentId: string;
19+
lastSeen: number;
20+
status: 'alive' | 'stale' | 'dead';
21+
metadata?: Record<string, unknown>;
22+
}
23+
24+
/** Soglia di alert */
25+
export interface AlertThreshold {
26+
id: string;
27+
metric: 'cpuUsage' | 'memoryPercent' | 'heartbeat';
28+
operator: 'gt' | 'lt' | 'eq';
29+
value: number;
30+
description: string;
31+
}
32+
33+
/** Evento di alert attivo */
34+
export interface AlertEvent {
35+
thresholdId: string;
36+
metric: string;
37+
currentValue: number;
38+
thresholdValue: number;
39+
operator: string;
40+
description: string;
41+
triggeredAt: number;
42+
}
43+
44+
/** Configurazione monitor */
45+
export interface MonitorConfig {
46+
heartbeatStaleMs?: number;
47+
heartbeatDeadMs?: number;
48+
metricsHistorySize?: number;
49+
}
50+
51+
export const DEFAULT_MONITOR_CONFIG: Required<MonitorConfig> = {
52+
heartbeatStaleMs: 30_000,
53+
heartbeatDeadMs: 120_000,
54+
metricsHistorySize: 60,
55+
};

0 commit comments

Comments
 (0)