Feat : Log recorder
This commit is contained in:
@@ -2,7 +2,7 @@ import { randomUUID } from 'node:crypto';
|
||||
import type { IncomingMessage } from 'node:http';
|
||||
import type { RawData, WebSocket } from 'ws';
|
||||
import type { AgentToServer, ServerToAgent } from '@stream-control/shared';
|
||||
import { PROTOCOL_VERSION, detectPlatform, safeJsonParse } from '@stream-control/shared';
|
||||
import { PROTOCOL_VERSION, detectPlatform, isAgentEvent, safeJsonParse } from '@stream-control/shared';
|
||||
import { config } from './config.ts';
|
||||
import { extractBearer, generateToken, hashToken, safeEqual } from './auth.ts';
|
||||
import { agentsRepo, type AgentRecord } from './db.ts';
|
||||
@@ -88,7 +88,13 @@ export function handleAgentConnection(
|
||||
platform,
|
||||
agentVersion: message.agentVersion,
|
||||
});
|
||||
hub.log(record.id, 'info', `Nouvel agent enrôlé depuis ${remoteAddress}`);
|
||||
hub.log(
|
||||
record.id,
|
||||
'info',
|
||||
`Nouvel agent enrôlé depuis ${remoteAddress}`,
|
||||
Date.now(),
|
||||
'agent.enrolled',
|
||||
);
|
||||
} else {
|
||||
record = auth.record;
|
||||
agentsRepo.updateIdentity(record.id, {
|
||||
@@ -101,7 +107,13 @@ export function handleAgentConnection(
|
||||
|
||||
agentId = record.id;
|
||||
hub.attachAgent(record.id, socket);
|
||||
hub.log(record.id, 'info', `Agent connecté (${platform}, ${remoteAddress})`);
|
||||
hub.log(
|
||||
record.id,
|
||||
'info',
|
||||
`Agent connecté (${platform}, ${remoteAddress})`,
|
||||
Date.now(),
|
||||
'agent.connected',
|
||||
);
|
||||
|
||||
send(socket, {
|
||||
type: 'welcome',
|
||||
@@ -132,7 +144,13 @@ export function handleAgentConnection(
|
||||
}
|
||||
|
||||
case 'log': {
|
||||
hub.log(agentId, message.level, message.message, message.ts || Date.now());
|
||||
hub.log(
|
||||
agentId,
|
||||
message.level,
|
||||
message.message,
|
||||
message.ts || Date.now(),
|
||||
isAgentEvent(message.event) ? message.event : null,
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -147,7 +165,7 @@ export function handleAgentConnection(
|
||||
clearTimeout(helloTimer);
|
||||
if (agentId) {
|
||||
hub.detachAgent(agentId, socket);
|
||||
hub.log(agentId, 'info', 'Agent déconnecté');
|
||||
hub.log(agentId, 'info', 'Agent déconnecté', Date.now(), 'agent.disconnected');
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -100,6 +100,7 @@ api.patch('/agents/:id', (req, res) => {
|
||||
if (updated) {
|
||||
hub.pushConfig(updated);
|
||||
hub.publishAgent(updated.id);
|
||||
hub.log(updated.id, 'info', 'Configuration modifiée depuis le dashboard', Date.now(), 'config.changed');
|
||||
res.json({ agent: hub.view(updated) });
|
||||
} else {
|
||||
res.status(500).json({ error: 'Mise à jour impossible' });
|
||||
@@ -155,7 +156,13 @@ api.post('/agents/:id/command', async (req, res) => {
|
||||
res.json({ ok: true, data });
|
||||
} catch (err) {
|
||||
const messageText = err instanceof Error ? err.message : String(err);
|
||||
hub.log(record.id, 'error', `Commande « ${action} » en échec : ${messageText}`);
|
||||
hub.log(
|
||||
record.id,
|
||||
'error',
|
||||
`Commande « ${action} » en échec : ${messageText}`,
|
||||
Date.now(),
|
||||
'command.failed',
|
||||
);
|
||||
res.status(502).json({ ok: false, error: messageText });
|
||||
}
|
||||
});
|
||||
@@ -369,6 +376,17 @@ api.post('/watchlist/:id/stop', async (req, res) => {
|
||||
|
||||
// --- Divers -----------------------------------------------------------------
|
||||
|
||||
/** Historique d'une VM : les mêmes entrées que le journal, filtrées et bornées. */
|
||||
api.get('/agents/:id/logs', (req, res) => {
|
||||
const record = agentsRepo.get(req.params.id);
|
||||
if (!record) {
|
||||
res.status(404).json({ error: 'Agent introuvable' });
|
||||
return;
|
||||
}
|
||||
const limit = Math.min(Number.parseInt(String(req.query.limit ?? '300'), 10) || 300, 1000);
|
||||
res.json({ logs: logsRepo.forAgent(record.id, limit) });
|
||||
});
|
||||
|
||||
api.get('/logs', (req, res) => {
|
||||
const limit = Math.min(Number.parseInt(String(req.query.limit ?? '200'), 10) || 200, 1000);
|
||||
res.json({ logs: logsRepo.recent(limit) });
|
||||
|
||||
@@ -2,6 +2,7 @@ import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { DatabaseSync } from 'node:sqlite';
|
||||
import type {
|
||||
AgentEvent,
|
||||
LogEntry,
|
||||
LogLevel,
|
||||
ObsSettings,
|
||||
@@ -17,6 +18,7 @@ import {
|
||||
DEFAULT_OBS_SETTINGS,
|
||||
DEFAULT_RECORDING_SETTINGS,
|
||||
DEFAULT_WATCH_SETTINGS,
|
||||
isAgentEvent,
|
||||
normalizeBrowserSettings,
|
||||
normalizeRecordingSettings,
|
||||
normalizeWatchSettings,
|
||||
@@ -96,6 +98,10 @@ addColumnIfMissing('agents', 'browser_json', 'TEXT');
|
||||
// Preset d'enregistrement : réglable VM par VM, absent = aucune intervention
|
||||
// de l'agent sur les réglages d'OBS.
|
||||
addColumnIfMissing('agents', 'recording_json', 'TEXT');
|
||||
// Nature de l'évènement journalisé : classe l'entrée dans l'historique d'une VM.
|
||||
// Nul sur les entrées écrites avant cette colonne, et sur celles des agents non
|
||||
// mis à jour — l'interface s'en accommode.
|
||||
addColumnIfMissing('logs', 'event', 'TEXT');
|
||||
|
||||
// Enrichissement des profils surveillés : photo et historique de diffusion.
|
||||
addColumnIfMissing('watch_targets', 'avatar_url', 'TEXT');
|
||||
@@ -194,12 +200,20 @@ const stmts = {
|
||||
rotateToken: db.prepare('UPDATE agents SET token_hash = ? WHERE id = ?'),
|
||||
deleteAgent: db.prepare('DELETE FROM agents WHERE id = ?'),
|
||||
|
||||
insertLog: db.prepare('INSERT INTO logs (agent_id, level, message, ts) VALUES (?, ?, ?, ?)'),
|
||||
insertLog: db.prepare(
|
||||
'INSERT INTO logs (agent_id, level, message, ts, event) VALUES (?, ?, ?, ?, ?)',
|
||||
),
|
||||
recentLogs: db.prepare(`
|
||||
SELECT l.id, l.agent_id, l.level, l.message, l.ts, a.name AS agent_name
|
||||
SELECT l.id, l.agent_id, l.level, l.message, l.ts, l.event, a.name AS agent_name
|
||||
FROM logs l LEFT JOIN agents a ON a.id = l.agent_id
|
||||
ORDER BY l.id DESC LIMIT ?
|
||||
`),
|
||||
agentLogs: db.prepare(`
|
||||
SELECT l.id, l.agent_id, l.level, l.message, l.ts, l.event, a.name AS agent_name
|
||||
FROM logs l LEFT JOIN agents a ON a.id = l.agent_id
|
||||
WHERE l.agent_id = ?
|
||||
ORDER BY l.id DESC LIMIT ?
|
||||
`),
|
||||
pruneLogs: db.prepare(`
|
||||
DELETE FROM logs WHERE id NOT IN (SELECT id FROM logs ORDER BY id DESC LIMIT ?)
|
||||
`),
|
||||
@@ -466,11 +480,30 @@ interface LogRow {
|
||||
level: string;
|
||||
message: string;
|
||||
ts: number;
|
||||
event: string | null;
|
||||
}
|
||||
|
||||
function toLogEntry(row: LogRow): LogEntry {
|
||||
return {
|
||||
id: Number(row.id),
|
||||
agentId: row.agent_id,
|
||||
agentName: row.agent_name,
|
||||
level: row.level as LogLevel,
|
||||
message: row.message,
|
||||
ts: Number(row.ts),
|
||||
event: isAgentEvent(row.event) ? row.event : null,
|
||||
};
|
||||
}
|
||||
|
||||
export const logsRepo = {
|
||||
append(agentId: string | null, level: LogLevel, message: string, ts = Date.now()): LogEntry {
|
||||
const info = stmts.insertLog.run(agentId, level, message, ts);
|
||||
append(
|
||||
agentId: string | null,
|
||||
level: LogLevel,
|
||||
message: string,
|
||||
ts = Date.now(),
|
||||
event: AgentEvent | null = null,
|
||||
): LogEntry {
|
||||
const info = stmts.insertLog.run(agentId, level, message, ts, event);
|
||||
if (Math.random() < 0.02) stmts.pruneLogs.run(config.logRetention);
|
||||
const agent = agentId ? agentsRepo.get(agentId) : null;
|
||||
return {
|
||||
@@ -480,20 +513,18 @@ export const logsRepo = {
|
||||
level,
|
||||
message,
|
||||
ts,
|
||||
event,
|
||||
};
|
||||
},
|
||||
|
||||
recent(limit = 200): LogEntry[] {
|
||||
const rows = stmts.recentLogs.all(limit) as unknown as LogRow[];
|
||||
return rows
|
||||
.map((row) => ({
|
||||
id: Number(row.id),
|
||||
agentId: row.agent_id,
|
||||
agentName: row.agent_name,
|
||||
level: row.level as LogLevel,
|
||||
message: row.message,
|
||||
ts: Number(row.ts),
|
||||
}))
|
||||
.reverse();
|
||||
return rows.map(toLogEntry).reverse();
|
||||
},
|
||||
|
||||
/** Historique d'une VM, du plus ancien au plus récent. */
|
||||
forAgent(agentId: string, limit = 300): LogEntry[] {
|
||||
const rows = stmts.agentLogs.all(agentId, limit) as unknown as LogRow[];
|
||||
return rows.map(toLogEntry).reverse();
|
||||
},
|
||||
};
|
||||
|
||||
@@ -2,6 +2,7 @@ import { randomUUID } from 'node:crypto';
|
||||
import type { WebSocket } from 'ws';
|
||||
import type {
|
||||
AgentAction,
|
||||
AgentEvent,
|
||||
AgentStatus,
|
||||
AgentView,
|
||||
LogEntry,
|
||||
@@ -234,8 +235,14 @@ class Hub {
|
||||
}
|
||||
|
||||
/** Journalise un évènement : persistance + diffusion temps réel. */
|
||||
log(agentId: string | null, level: LogLevel, message: string, ts = Date.now()): LogEntry {
|
||||
const entry = logsRepo.append(agentId, level, message, ts);
|
||||
log(
|
||||
agentId: string | null,
|
||||
level: LogLevel,
|
||||
message: string,
|
||||
ts = Date.now(),
|
||||
event: AgentEvent | null = null,
|
||||
): LogEntry {
|
||||
const entry = logsRepo.append(agentId, level, message, ts, event);
|
||||
this.broadcast({ type: 'log', entry });
|
||||
if (level === 'error' || level === 'warn') {
|
||||
console.warn(`[${level}] ${entry.agentName ?? 'serveur'} — ${message}`);
|
||||
|
||||
Reference in New Issue
Block a user