refactor: simplify Docker config and add db optimizations

- Consolidate docker-compose files: dev.yml builds locally, main uses
  pre-built images; remove redundant prod.yml and images.yml
- Add docker-entrypoint.sh for database copy from network storage
- Add SQLite performance tuning (PRAGMA optimizations, mmap, cache)
- Add cache pre-warming for instant first page load
- Enhance logging with OTEL-compatible JSON format and trace context
- Simplify .env.example with clearer sections and fewer variables
- Remove EXTERNAL_IMAGE_URLS env var (auto-read from FP preferences)
- Hardcode container ports (use docker port mapping to customize)
- Add /api/errors/stats endpoint for admin dashboard
This commit is contained in:
Darkraise
2026-02-02 15:43:31 +07:00
parent 3321557229
commit b5feb04604
25 changed files with 1388 additions and 668 deletions
+175 -20
View File
@@ -3,6 +3,76 @@ import path from 'path';
import fs from 'fs';
import { config } from '../config';
// Try to import OpenTelemetry API for trace context
let trace: any = null;
let context: any = null;
try {
const otelApi = require('@opentelemetry/api');
trace = otelApi.trace;
context = otelApi.context;
} catch {
// OpenTelemetry not available - trace context will not be included
}
/**
* Get current trace context from OpenTelemetry
* Returns trace_id and span_id if available
*/
function getTraceContext(): { trace_id?: string; span_id?: string } {
if (!trace || !context) {
return {};
}
try {
const activeSpan = trace.getSpan(context.active());
if (activeSpan) {
const spanContext = activeSpan.spanContext();
return {
trace_id: spanContext.traceId,
span_id: spanContext.spanId
};
}
} catch {
// Ignore errors getting trace context
}
return {};
}
/**
* Map Winston log levels to OTEL severity numbers
* https://opentelemetry.io/docs/specs/otel/logs/data-model/#field-severitynumber
*/
function getSeverityNumber(level: string): number {
const severityMap: Record<string, number> = {
error: 17, // ERROR
warn: 13, // WARN
info: 9, // INFO
http: 9, // INFO
verbose: 5, // DEBUG
debug: 5, // DEBUG
silly: 1 // TRACE
};
return severityMap[level] || 9;
}
/**
* Custom format that adds OTEL-compatible fields
*/
const otelFormat = winston.format((info) => {
// Add trace context if available
const traceContext = getTraceContext();
if (traceContext.trace_id) {
info.trace_id = traceContext.trace_id;
info.span_id = traceContext.span_id;
}
// Add OTEL severity fields
info.severity = info.level.toUpperCase();
info.severityNumber = getSeverityNumber(info.level);
return info;
});
// Build transports array
const transports: winston.transport[] = [
// Console transport (always enabled)
@@ -11,8 +81,20 @@ const transports: winston.transport[] = [
winston.format.colorize(),
winston.format.printf(({ level, message, timestamp, ...meta }) => {
let msg = `${timestamp} [${level}]: ${message}`;
if (Object.keys(meta).length > 0 && meta.service !== 'flashpoint-web-api') {
msg += ` ${JSON.stringify(meta)}`;
// Include trace_id in console output if present
const traceId = meta.trace_id as string | undefined;
if (traceId) {
msg = `${timestamp} [${level}] [trace:${traceId.substring(0, 8)}]: ${message}`;
}
// Filter out internal fields for console display
const displayMeta = { ...meta };
delete displayMeta.service;
delete displayMeta.trace_id;
delete displayMeta.span_id;
delete displayMeta.severity;
delete displayMeta.severityNumber;
if (Object.keys(displayMeta).length > 0) {
msg += ` ${JSON.stringify(displayMeta)}`;
}
return msg;
})
@@ -20,44 +102,117 @@ const transports: winston.transport[] = [
})
];
// Track file transport for diagnostics
let fileTransport: winston.transports.FileTransportInstance | null = null;
let fileLoggingError: Error | null = null;
// Add file transport if LOG_FILE is configured
if (config.logFile) {
try {
// Ensure log directory exists
const logDir = path.dirname(config.logFile);
console.log(`[Logger] Attempting to setup file logging...`);
console.log(`[Logger] LOG_FILE: ${config.logFile}`);
console.log(`[Logger] Log directory: ${logDir}`);
if (!fs.existsSync(logDir)) {
console.log(`[Logger] Creating log directory...`);
fs.mkdirSync(logDir, { recursive: true });
}
// Add file transport with JSON format
transports.push(
new winston.transports.File({
filename: config.logFile,
format: winston.format.combine(
winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
winston.format.errors({ stack: true }),
winston.format.json()
),
maxsize: 10485760, // 10MB
maxFiles: 5,
tailable: true
})
);
// Check if directory is writable
try {
const testFile = path.join(logDir, '.write-test');
fs.writeFileSync(testFile, 'test');
fs.unlinkSync(testFile);
console.log(`[Logger] Directory is writable`);
} catch (writeError) {
throw new Error(`Log directory is not writable: ${writeError instanceof Error ? writeError.message : writeError}`);
}
console.log(`[Logger] File logging enabled: ${config.logFile}`);
// Create file transport with OTEL-compatible JSON format
fileTransport = new winston.transports.File({
filename: config.logFile,
format: winston.format.combine(
winston.format.timestamp({ format: 'YYYY-MM-DDTHH:mm:ss.SSSZ' }), // ISO 8601
winston.format.errors({ stack: true }),
otelFormat(),
winston.format.json()
),
maxsize: 10485760, // 10MB
maxFiles: 5,
tailable: true
});
// Add error handler to catch write failures
fileTransport.on('error', (error) => {
console.error(`[Logger] File transport error: ${error.message}`);
fileLoggingError = error;
});
transports.push(fileTransport);
console.log(`[Logger] ✅ File logging enabled: ${config.logFile}`);
} catch (error) {
console.error(`[Logger] Failed to setup file logging: ${error instanceof Error ? error.message : 'Unknown error'}`);
const errorMsg = error instanceof Error ? error.message : 'Unknown error';
console.error(`[Logger] ❌ Failed to setup file logging: ${errorMsg}`);
fileLoggingError = error instanceof Error ? error : new Error(errorMsg);
}
} else {
console.log(`[Logger] ℹ️ File logging disabled (LOG_FILE not set)`);
}
export const logger = winston.createLogger({
level: config.logLevel,
format: winston.format.combine(
winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
winston.format.timestamp({ format: 'YYYY-MM-DDTHH:mm:ss.SSSZ' }), // ISO 8601
winston.format.errors({ stack: true }),
winston.format.splat(),
otelFormat(),
winston.format.json()
),
defaultMeta: { service: 'flashpoint-web-api' },
defaultMeta: { service: 'flashpoint-web-backend' },
transports
});
/**
* Get logging status for diagnostics
*/
export function getLoggingStatus(): {
consoleEnabled: boolean;
fileEnabled: boolean;
filePath: string | null;
fileError: string | null;
logLevel: string;
otelIntegration: boolean;
} {
return {
consoleEnabled: true,
fileEnabled: fileTransport !== null && fileLoggingError === null,
filePath: config.logFile || null,
fileError: fileLoggingError?.message || null,
logLevel: config.logLevel,
otelIntegration: trace !== null
};
}
/**
* Verify file logging is working by writing a test entry
*/
export function verifyFileLogging(): boolean {
if (!config.logFile || !fileTransport) {
return false;
}
try {
// Write a verification entry
logger.info('Log file verification - logging system initialized');
// Give winston a moment to flush, then check if file exists
// Note: This is a sync check, actual write is async
return fs.existsSync(config.logFile);
} catch (error) {
console.error(`[Logger] Verification failed: ${error}`);
return false;
}
}