import { registerPlugin, rootDir, loadConfig } from '../index.js'; import { Level } from 'level'; import ttl from 'level-ttl'; import fs from 'fs/promises'; import path from 'path'; import { fileURLToPath } from 'url'; import { Readable } from 'stream'; import cookie from 'cookie'; import { getRealIP } from '../utils/network.js'; import { parseDuration } from '../utils/time.js'; // Load stats configuration const statsConfig = {}; await loadConfig('stats', statsConfig); // Map configuration to internal structure const enabled = statsConfig.Core.Enabled; const statsTTL = parseDuration(statsConfig.Storage.StatsTTL); const statsUIPath = statsConfig.WebUI.StatsUIPath; const statsAPIPath = statsConfig.WebUI.StatsAPIPath; // Determine __dirname for ES modules const __dirname = path.dirname(fileURLToPath(import.meta.url)); /** * Adds createReadStream support to LevelDB instances using async iterator. */ function addReadStreamSupport(dbInstance) { if (!dbInstance.createReadStream) { dbInstance.createReadStream = (opts) => Readable.from( (async function* () { for await (const [key, value] of dbInstance.iterator(opts)) { yield { key, value }; } })(), ); } return dbInstance; } // Initialize LevelDB for stats under db/stats with TTL and stream support const statsDBPath = path.join(rootDir, 'db', 'stats'); await fs.mkdir(statsDBPath, { recursive: true }); let rawStatsDB = new Level(statsDBPath, { valueEncoding: 'json' }); rawStatsDB = addReadStreamSupport(rawStatsDB); const statsDB = ttl(rawStatsDB, { defaultTTL: statsTTL }); addReadStreamSupport(statsDB); /** * Record a stat event with a metric name and optional data. * @param {string} metric * @param {object} data */ function recordEvent(metric, data = {}) { // Skip if statsDB is not initialized if (typeof statsDB === 'undefined' || !statsDB || typeof statsDB.put !== 'function') { console.warn(`stats: cannot record "${metric}", statsDB not available`); return; } const timestamp = Date.now(); // key includes metric and timestamp and a random suffix to avoid collisions const key = `${metric}:${timestamp}:${Math.random().toString(36).slice(2, 8)}`; try { // Use callback form to avoid promise chaining statsDB.put(key, { timestamp, metric, ...data }, (err) => { if (err) console.error('stats: failed to record event', err); }); } catch (err) { console.error('stats: failed to record event', err); } } // Handler for serving the stats HTML UI async function handleStatsPage(request) { const url = new URL(request.url); if (url.pathname !== statsUIPath) return undefined; try { const html = await fs.readFile(path.join(__dirname, 'stats.html'), 'utf8'); return new Response(html, { status: 200, headers: { 'Content-Type': 'text/html; charset=utf-8' }, }); } catch (e) { return new Response('Stats UI not found', { status: 404 }); } } // Handler for stats API async function handleStatsAPI(request) { const url = new URL(request.url); if (url.pathname !== statsAPIPath) return undefined; const metric = url.searchParams.get('metric'); const start = parseInt(url.searchParams.get('start') || '0', 10); const end = parseInt(url.searchParams.get('end') || `${Date.now()}`, 10); const result = []; // Iterate over keys for this metric in the time range for await (const [key, value] of statsDB.iterator({ gte: `${metric}:${start}`, lte: `${metric}:${end}\uffff`, })) { result.push(value); } return new Response(JSON.stringify(result), { status: 200, headers: { 'Content-Type': 'application/json' }, }); } // Middleware for stats plugin function StatsMiddleware() { return async (request) => { // Always serve stats UI and API first, bypassing auth const pageResp = await handleStatsPage(request); if (pageResp) return pageResp; const apiResp = await handleStatsAPI(request); if (apiResp) return apiResp; // For any other routes, do not handle return undefined; }; } // Register the stats plugin if (enabled) { registerPlugin('stats', StatsMiddleware()); } else { console.log('Stats plugin disabled via config'); } // Export recordEvent for other plugins to use export { recordEvent };