/** * Logger locale + invio realtime. * * Scrive una riga ogni secondo in un CSV locale (1 sessione = 1 file) e * contemporaneamente invia i field via WebSocket al realtime per InfluxDB. * * Path-list dinamica: i path letti ad ogni tick vengono presi dal ruleset * 'logs' (cores/rulesets). Quando il ruleset cambia (push WS o cambio remoto): * - la sessione corrente viene chiusa (CSV finalizzato) * - si manda `session_reset` al server (nuovo session_id su Influx) * - si apre un nuovo CSV con i nuovi path * - lo storico Influx resta coerente: tag `session` cambia, tag * `ruleset_version` riflette la nuova versione applicata. */ const fs = require('fs').promises; const fsSync = require('fs'); const pth = require('path'); const skFlow = require('../config/skFlow'); const realtime = require('./realtime/core'); const rulesets = require('./rulesets'); const logsDirectory = pth.join(__dirname, '../../data/logs/'); const WRITE_INTERVAL = 1000; let session = null; let writeInterval = null; let logPaths = []; const SPECIAL_RESOLVERS = { 'navigation.position.latitude': () => skFlow.get('navigation.position')?.latitude ?? null, 'navigation.position.longitude': () => skFlow.get('navigation.position')?.longitude ?? null, 'system.uptime': () => Math.floor(process.uptime()), }; function resolveValue(p) { if (SPECIAL_RESOLVERS[p]) return SPECIAL_RESOLVERS[p](); return skFlow.get(p); } /** * Init: prende i path iniziali dal ruleset attivo e si sottoscrive ai cambi. * @param {Array} [fallback] - opzionale, lista path se non c'e' ruleset */ function init(fallback) { logPaths = rulesets.getEnabledPaths('logs'); if ((!logPaths || logPaths.length === 0) && Array.isArray(fallback)) { logPaths = fallback; } console.log(`[LOGS] init: ${logPaths.length} path da ruleset v=${rulesets.versionStr('logs') || '?'}`); rulesets.onUpdateOf('logs', async (next) => { const newPaths = (next.content || []).filter(it => it.enabled !== false).map(it => it.path); console.log(`[LOGS] ruleset update: ${newPaths.length} path → restart recording`); const wasRecording = !!session; try { if (wasRecording) await stopRecording(); logPaths = newPaths; if (realtime.isConnected()) realtime.sendRaw({ _t: 'session_reset' }); if (wasRecording) await startRecording(); } catch (err) { console.error('[LOGS] restart on ruleset update failed:', err.message); } }); } async function ensureDir() { try { await fs.mkdir(logsDirectory, { recursive: true }); } catch (e) {} } async function startRecording(name) { if (session) await stopRecording(); if (!name) name = new Date().toISOString().replace(/[:.]/g, '-'); if (logPaths.length === 0) console.warn('[LOGS] nessun path configurato'); await ensureDir(); const header = ['timestamp', ...logPaths].join(',') + '\n'; const filePath = pth.join(logsDirectory, `${name}.csv`); await fs.writeFile(filePath, header); session = { name, paths: logPaths.slice(), startTime: new Date(), elements: 0, filePath }; writeInterval = setInterval(() => { writeLog(); }, WRITE_INTERVAL); console.log(`[LOGS] started: ${name} (${logPaths.length} cols)`); return session; } async function writeLog() { if (!session) return; try { const timestamp = new Date().toISOString(); const fields = {}; const csvValues = []; for (const p of session.paths) { const val = resolveValue(p); fields[p] = val; if (val === null || val === undefined) csvValues.push(''); else if (typeof val === 'object') csvValues.push(JSON.stringify(val).replace(/,/g, ';')); else csvValues.push(val); } const row = [timestamp, ...csvValues].join(',') + '\n'; await fs.appendFile(session.filePath, row); session.elements++; if (realtime.isConnected()) realtime.send([Date.now(), 'logs', fields]); } catch (err) { console.error('[LOGS] write error:', err.message); } } async function stopRecording() { if (writeInterval) { clearInterval(writeInterval); writeInterval = null; } if (session) { console.log(`[LOGS] stopped ${session.name} (${session.elements} righe)`); session = null; } } async function getLog(name) { try { return await fs.readFile(pth.join(logsDirectory, `${name}.csv`), 'utf-8'); } catch (err) { console.error('[LOGS] read err:', err.message); return null; } } function getLogFile(name) { const p = pth.join(logsDirectory, `${name}.csv`); return fsSync.existsSync(p) ? p : null; } async function listLogs() { await ensureDir(); try { const files = await fs.readdir(logsDirectory); const csv = files.filter(f => f.endsWith('.csv')); const out = []; for (const file of csv) { const p = pth.join(logsDirectory, file); const stat = await fs.stat(p); out.push({ name: file.replace('.csv', ''), filename: file, size: (stat.size / (1024 * 1024)).toFixed(2), created: stat.birthtime, modified: stat.mtime, }); } return out.sort((a, b) => b.modified - a.modified); } catch (err) { console.error('[LOGS] list err:', err.message); return []; } } function getSession() { if (!session) return null; return { name: session.name, paths: session.paths, startTime: session.startTime, elements: session.elements, delay: WRITE_INTERVAL, }; } module.exports = { init, startRecording, stopRecording, getLog, getLogFile, getSession, listLogs };