// Request logger — writes all requests + full conversations to disk // Usage: import { requestLogger, getRecentLogs, getLogStats, readHistoricalLogs, listLogDates } from './logger.js'; // app.use(requestLogger('deepseek-2api')); import { appendFileSync, mkdirSync, readFileSync, readdirSync } from 'fs'; import { join } from 'path'; import { recordRequest } from './metrics.js'; const MEMORY_LIMIT = 1000; const recentLogs = []; let totalLogged = 0; let logDir = process.env.LOG_DIR || '/srv/threadripper-backups/newapi/logs'; let serviceName = 'default'; function getLogPath(date) { const dir = join(logDir, serviceName); mkdirSync(dir, { recursive: true }); return join(dir, `${date || new Date().toISOString().slice(0, 10)}.jsonl`); } function getChatLogPath(date) { const dir = join(logDir, serviceName, 'chats'); mkdirSync(dir, { recursive: true }); return join(dir, `${date || new Date().toISOString().slice(0, 10)}.jsonl`); } function writeLog(entry) { try { appendFileSync(getLogPath(), JSON.stringify(entry) + '\n'); } catch (e) { console.error('Log write failed:', e.message); } } function writeChatLog(entry) { try { appendFileSync(getChatLogPath(), JSON.stringify(entry) + '\n'); } catch (e) { console.error('Chat log write failed:', e.message); } } function parseSSEChunks(chunks) { const messages = []; for (const chunk of chunks) { const lines = chunk.split('\n'); for (const line of lines) { if (line.startsWith('data: ') && line !== 'data: [DONE]') { try { const data = JSON.parse(line.slice(6)); const delta = data.choices?.[0]?.delta; if (delta?.content) messages.push(delta.content); } catch {} } } } return messages.join(''); } export function requestLogger(name) { serviceName = name || serviceName; if (process.env.LOG_DIR) logDir = process.env.LOG_DIR; return (req, res, next) => { const start = Date.now(); const isChat = req.path === '/v1/chat/completions' || req.path === '/api/v0/chat/completion'; const messages = req.body?.messages || null; const model = req.body?.model || '-'; const stream = req.body?.stream || false; // Capture response body const chunks = []; const originalWrite = res.write.bind(res); const originalEnd = res.end.bind(res); res.write = function (chunk, ...args) { chunks.push(typeof chunk === 'string' ? chunk : chunk.toString()); return originalWrite(chunk, ...args); }; res.end = function (chunk, ...args) { if (chunk) chunks.push(typeof chunk === 'string' ? chunk : chunk.toString()); return originalEnd(chunk, ...args); }; res.on('finish', () => { const duration = Date.now() - start; const entry = { time: new Date().toISOString(), method: req.method, path: req.path, model, status: res.statusCode, duration, }; console.log(`[${entry.time}] ${entry.method} ${entry.path} model=${entry.model} ${entry.status} ${entry.duration}ms`); // Record to metrics collector recordRequest(model, duration, res.statusCode); // Write request log (metadata only) writeLog(entry); totalLogged++; // Keep recent in memory if (recentLogs.length >= MEMORY_LIMIT) recentLogs.shift(); recentLogs.push(entry); // Write full chat log for chat endpoints if (isChat && messages && res.statusCode === 200) { let assistantContent = ''; if (stream) { assistantContent = parseSSEChunks(chunks); } else { try { const body = chunks.join(''); const json = JSON.parse(body); assistantContent = json.choices?.[0]?.message?.content || ''; } catch {} } writeChatLog({ time: entry.time, model, stream, duration, messages, response: assistantContent, }); } }); next(); }; } export function getRecentLogs(count = 50) { return recentLogs.slice(-count); } export function getLogStats() { const now = Date.now(); const last5min = recentLogs.filter(e => now - new Date(e.time).getTime() < 300000); const errors = last5min.filter(e => e.status >= 400); const avgDuration = last5min.length ? Math.round(last5min.reduce((s, e) => s + e.duration, 0) / last5min.length) : 0; return { totalLogged, memoryBuffer: recentLogs.length, last5min: last5min.length, errors5min: errors.length, avgDuration5min: avgDuration, }; } export function readHistoricalLogs(date, count = 100) { try { const data = readFileSync(getLogPath(date), 'utf-8'); const lines = data.trim().split('\n').filter(Boolean); return lines.map(l => JSON.parse(l)).slice(-count); } catch { return []; } } export function readChatLogs(date, count = 50) { try { const data = readFileSync(getChatLogPath(date), 'utf-8'); const lines = data.trim().split('\n').filter(Boolean); return lines.map(l => JSON.parse(l)).slice(-count); } catch { return []; } } export function listLogDates() { try { const dir = join(logDir, serviceName); return readdirSync(dir) .filter(f => f.endsWith('.jsonl')) .map(f => f.replace('.jsonl', '')) .sort(); } catch { return []; } }