feat: add comprehensive logging system with file output

- Add backend logging to logs/backend.log with file rotation support
- Add frontend logging to logs/frontend.log via /api/logs endpoint
- Add frontend logger utility with batching and user context
- Update .gitignore to exclude log files but preserve logs directory
- Update CLAUDE.md with logging documentation and usage examples

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-20 11:04:31 +01:00
parent ac0c8a39a9
commit 6b195d3014
4 changed files with 289 additions and 9 deletions

View File

@@ -1,12 +1,18 @@
import Database from 'bun:sqlite';
import { drizzle } from 'drizzle-orm/bun-sqlite';
import * as schema from './db/schema/index.js';
import { join } from 'path';
import { join, dirname } from 'path';
import { existsSync, mkdirSync } from 'fs';
import { fileURLToPath } from 'url';
// ===================================================================
// Configuration
// ===================================================================
// ES module equivalent of __dirname
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const isDevelopment = process.env.NODE_ENV !== 'production';
export const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key-change-in-production';
@@ -19,16 +25,46 @@ export const LOG_LEVEL = process.env.LOG_LEVEL || (isDevelopment ? 'debug' : 'in
const logLevels = { debug: 0, info: 1, warn: 2, error: 3 };
const currentLogLevel = logLevels[LOG_LEVEL] ?? 1;
// Log file paths
const logsDir = join(__dirname, '../../logs');
const backendLogFile = join(logsDir, 'backend.log');
// Ensure log directory exists
if (!existsSync(logsDir)) {
mkdirSync(logsDir, { recursive: true });
}
function formatLogMessage(level, message, data) {
const timestamp = new Date().toISOString();
let logMessage = `[${timestamp}] ${level.toUpperCase()}: ${message}`;
if (data && Object.keys(data).length > 0) {
logMessage += ' ' + JSON.stringify(data, null, 2);
}
return logMessage + '\n';
}
function log(level, message, data) {
if (logLevels[level] < currentLogLevel) return;
const timestamp = new Date().toISOString();
const logMessage = `[${timestamp}] ${level.toUpperCase()}: ${message}`;
const logMessage = formatLogMessage(level, message, data);
if (data && Object.keys(data).length > 0) {
console.log(logMessage, JSON.stringify(data, null, 2));
} else {
console.log(logMessage);
// Write to file asynchronously (fire and forget for performance)
Bun.write(backendLogFile, logMessage, { createPath: true }).catch(err => {
console.error('Failed to write to log file:', err);
});
// Also log to console in development
if (isDevelopment) {
const timestamp = new Date().toISOString();
const consoleMessage = `[${timestamp}] ${level.toUpperCase()}: ${message}`;
if (data && Object.keys(data).length > 0) {
console.log(consoleMessage, data);
} else {
console.log(consoleMessage);
}
}
}
@@ -39,6 +75,27 @@ export const logger = {
error: (message, data) => log('error', message, data),
};
// Frontend logger - writes to separate log file
const frontendLogFile = join(logsDir, 'frontend.log');
export function logToFrontend(level, message, data = null, context = {}) {
if (logLevels[level] < currentLogLevel) return;
const timestamp = new Date().toISOString();
let logMessage = `[${timestamp}] [${context.userAgent || 'unknown'}] [${context.userId || 'anonymous'}] ${level.toUpperCase()}: ${message}`;
if (data && Object.keys(data).length > 0) {
logMessage += ' ' + JSON.stringify(data, null, 2);
}
logMessage += '\n';
// Write to frontend log file
Bun.write(frontendLogFile, logMessage, { createPath: true }).catch(err => {
console.error('Failed to write to frontend log file:', err);
});
}
export default logger;
// ===================================================================
@@ -46,7 +103,6 @@ export default logger;
// ===================================================================
// Get the directory containing this config file, then go to parent for db location
const __dirname = new URL('.', import.meta.url).pathname;
const dbPath = join(__dirname, 'award.db');
const sqlite = new Database(dbPath);