363 lines
14 KiB
JavaScript
363 lines
14 KiB
JavaScript
"use strict";
|
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
if (k2 === undefined) k2 = k;
|
|
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
}
|
|
Object.defineProperty(o, k2, desc);
|
|
}) : (function(o, m, k, k2) {
|
|
if (k2 === undefined) k2 = k;
|
|
o[k2] = m[k];
|
|
}));
|
|
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
}) : function(o, v) {
|
|
o["default"] = v;
|
|
});
|
|
var __importStar = (this && this.__importStar) || (function () {
|
|
var ownKeys = function(o) {
|
|
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
var ar = [];
|
|
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
return ar;
|
|
};
|
|
return ownKeys(o);
|
|
};
|
|
return function (mod) {
|
|
if (mod && mod.__esModule) return mod;
|
|
var result = {};
|
|
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
__setModuleDefault(result, mod);
|
|
return result;
|
|
};
|
|
})();
|
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
};
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
exports.LoggingService = exports.LogLevel = void 0;
|
|
const fs_1 = __importDefault(require("fs"));
|
|
const path_1 = __importDefault(require("path"));
|
|
const Minio = __importStar(require("minio"));
|
|
var LogLevel;
|
|
(function (LogLevel) {
|
|
LogLevel["REQUEST"] = "REQUEST";
|
|
LogLevel["ERROR"] = "ERROR";
|
|
LogLevel["WARNING"] = "WARNING";
|
|
LogLevel["AUTH"] = "AUTH";
|
|
LogLevel["DATABASE"] = "DATABASE";
|
|
LogLevel["STARTUP"] = "STARTUP";
|
|
LogLevel["CONNECTION"] = "CONNECTION";
|
|
LogLevel["OTHER"] = "OTHER";
|
|
})(LogLevel || (exports.LogLevel = LogLevel = {}));
|
|
class LoggingService {
|
|
constructor() {
|
|
this.minioClient = null;
|
|
this.logBuffer = [];
|
|
this.currentLogFile = null;
|
|
this.logCount = 0;
|
|
this.maxLogsPerFile = parseInt(process.env.MAX_LOGS_PER_FILE || '10000');
|
|
this.logsDir = path_1.default.join(process.cwd(), 'logs');
|
|
this.bucketName = process.env.MINIO_BUCKET_NAME || 'serpentrace-logs';
|
|
this.uploadInterval = null;
|
|
this.initializeLogsDirectory();
|
|
this.initializeMinioClient();
|
|
this.createNewLogFile();
|
|
if (process.env.NODE_ENV !== 'test') {
|
|
this.startPeriodicUpload();
|
|
}
|
|
process.on('SIGTERM', () => this.shutdown());
|
|
process.on('SIGINT', () => this.shutdown());
|
|
process.on('beforeExit', () => this.shutdown());
|
|
}
|
|
static getInstance() {
|
|
if (!LoggingService.instance) {
|
|
LoggingService.instance = new LoggingService();
|
|
}
|
|
return LoggingService.instance;
|
|
}
|
|
initializeLogsDirectory() {
|
|
try {
|
|
if (!fs_1.default.existsSync(this.logsDir)) {
|
|
fs_1.default.mkdirSync(this.logsDir, { recursive: true });
|
|
}
|
|
// Create monthly subdirectory
|
|
const monthlyDir = this.getMonthlyDirectory();
|
|
if (!fs_1.default.existsSync(monthlyDir)) {
|
|
fs_1.default.mkdirSync(monthlyDir, { recursive: true });
|
|
}
|
|
}
|
|
catch (error) {
|
|
console.error('Failed to initialize logs directory:', error);
|
|
}
|
|
}
|
|
initializeMinioClient() {
|
|
try {
|
|
// Check if in production or development
|
|
if (process.env.NODE_ENV === 'production') {
|
|
if (process.env.MINIO_ENDPOINT && process.env.MINIO_ACCESS_KEY && process.env.MINIO_SECRET_KEY) {
|
|
this.minioClient = new Minio.Client({
|
|
endPoint: process.env.MINIO_ENDPOINT,
|
|
port: parseInt(process.env.MINIO_PORT || '9000'),
|
|
useSSL: process.env.MINIO_USE_SSL === 'true',
|
|
accessKey: process.env.MINIO_ACCESS_KEY,
|
|
secretKey: process.env.MINIO_SECRET_KEY
|
|
});
|
|
this.ensureBucketExists();
|
|
}
|
|
else {
|
|
console.warn('Minio configuration not found. Logs will only be stored locally and in console.');
|
|
}
|
|
}
|
|
else {
|
|
// Development-specific Minio configuration
|
|
this.minioClient = new Minio.Client({
|
|
endPoint: 'localhost',
|
|
port: 9000,
|
|
useSSL: false,
|
|
accessKey: 'serpentrace',
|
|
secretKey: 'serpentrace123!'
|
|
});
|
|
this.ensureBucketExists();
|
|
}
|
|
}
|
|
catch (error) {
|
|
console.error('Failed to initialize Minio client:', error);
|
|
this.minioClient = null;
|
|
}
|
|
}
|
|
async ensureBucketExists() {
|
|
if (!this.minioClient)
|
|
return;
|
|
try {
|
|
const exists = await this.minioClient.bucketExists(this.bucketName);
|
|
if (!exists) {
|
|
await this.minioClient.makeBucket(this.bucketName);
|
|
this.log(LogLevel.STARTUP, `Created Minio bucket: ${this.bucketName}`);
|
|
}
|
|
}
|
|
catch (error) {
|
|
console.error('Failed to ensure bucket exists:', error);
|
|
}
|
|
}
|
|
startPeriodicUpload() {
|
|
// Upload current log file to Minio every 2 minutes
|
|
this.uploadInterval = setInterval(async () => {
|
|
if (this.currentLogFile && this.minioClient) {
|
|
await this.uploadToMinio(this.currentLogFile);
|
|
}
|
|
}, 2 * 60 * 1000); // 2 minutes
|
|
}
|
|
getMonthlyDirectory() {
|
|
const now = new Date();
|
|
const year = now.getFullYear();
|
|
const month = String(now.getMonth() + 1).padStart(2, '0');
|
|
return path_1.default.join(this.logsDir, `${year}-${month}`);
|
|
}
|
|
getMonthlyMinioPrefix() {
|
|
const now = new Date();
|
|
const year = now.getFullYear();
|
|
const month = String(now.getMonth() + 1).padStart(2, '0');
|
|
return `${year}-${month}/`;
|
|
}
|
|
createNewLogFile() {
|
|
const now = new Date();
|
|
const timestamp = now.toISOString().replace(/[:.]/g, '-');
|
|
const fileName = `serpentrace-${timestamp}.log`;
|
|
this.currentLogFile = path_1.default.join(this.getMonthlyDirectory(), fileName);
|
|
this.logCount = 0;
|
|
// Write log file header
|
|
const header = `# SerpentRace Backend Logs\n# Started: ${now.toISOString()}\n# Max entries per file: ${this.maxLogsPerFile}\n\n`;
|
|
try {
|
|
fs_1.default.writeFileSync(this.currentLogFile, header);
|
|
}
|
|
catch (error) {
|
|
console.error('Failed to create log file:', error);
|
|
}
|
|
}
|
|
formatLogEntry(entry) {
|
|
const parts = [
|
|
entry.timestamp,
|
|
`[${entry.level}]`,
|
|
entry.message
|
|
];
|
|
if (entry.requestId)
|
|
parts.push(`ReqId:${entry.requestId}`);
|
|
if (entry.userId)
|
|
parts.push(`UserId:${entry.userId}`);
|
|
if (entry.ip)
|
|
parts.push(`IP:${entry.ip}`);
|
|
if (entry.method && entry.url)
|
|
parts.push(`${entry.method} ${entry.url}`);
|
|
if (entry.statusCode)
|
|
parts.push(`Status:${entry.statusCode}`);
|
|
if (entry.responseTime)
|
|
parts.push(`Time:${entry.responseTime}ms`);
|
|
if (entry.userAgent)
|
|
parts.push(`UA:${entry.userAgent.substring(0, 50)}`);
|
|
if (entry.metadata)
|
|
parts.push(`Meta:${JSON.stringify(entry.metadata)}`);
|
|
return parts.join(' | ');
|
|
}
|
|
async writeToLocalFile(entry) {
|
|
if (!this.currentLogFile)
|
|
return;
|
|
try {
|
|
const logLine = this.formatLogEntry(entry) + '\n';
|
|
fs_1.default.appendFileSync(this.currentLogFile, logLine);
|
|
this.logCount++;
|
|
// Check if we need to rotate the log file
|
|
if (this.logCount >= this.maxLogsPerFile) {
|
|
await this.rotateLogFile();
|
|
}
|
|
}
|
|
catch (error) {
|
|
console.error('Failed to write to log file:', error);
|
|
}
|
|
}
|
|
async rotateLogFile() {
|
|
if (!this.currentLogFile)
|
|
return;
|
|
try {
|
|
// Upload current file to Minio before rotating
|
|
await this.uploadToMinio(this.currentLogFile);
|
|
// Create new log file
|
|
this.createNewLogFile();
|
|
this.log(LogLevel.OTHER, 'Log file rotated due to size limit');
|
|
}
|
|
catch (error) {
|
|
console.error('Failed to rotate log file:', error);
|
|
}
|
|
}
|
|
async uploadToMinio(filePath) {
|
|
if (!this.minioClient) {
|
|
console.warn('Minio client not initialized, skipping upload');
|
|
return;
|
|
}
|
|
if (!fs_1.default.existsSync(filePath)) {
|
|
console.warn(`Log file does not exist: ${filePath}`);
|
|
return;
|
|
}
|
|
try {
|
|
const fileName = path_1.default.basename(filePath);
|
|
const objectName = this.getMonthlyMinioPrefix() + fileName;
|
|
console.log(`Attempting to upload log file to Minio: ${objectName}`);
|
|
await this.minioClient.fPutObject(this.bucketName, objectName, filePath);
|
|
console.log(`Successfully uploaded log file to Minio: ${objectName}`);
|
|
}
|
|
catch (error) {
|
|
console.error('Failed to upload to Minio:', error);
|
|
console.error('Minio config:', {
|
|
endpoint: this.minioClient ? 'configured' : 'not configured',
|
|
bucket: this.bucketName
|
|
});
|
|
}
|
|
}
|
|
logToConsole(entry) {
|
|
const formattedEntry = this.formatLogEntry(entry);
|
|
switch (entry.level) {
|
|
case LogLevel.ERROR:
|
|
console.error(formattedEntry);
|
|
break;
|
|
case LogLevel.WARNING:
|
|
console.warn(formattedEntry);
|
|
break;
|
|
case LogLevel.REQUEST:
|
|
case LogLevel.AUTH:
|
|
case LogLevel.DATABASE:
|
|
case LogLevel.CONNECTION:
|
|
console.info(formattedEntry);
|
|
break;
|
|
case LogLevel.STARTUP:
|
|
console.log(formattedEntry);
|
|
break;
|
|
default:
|
|
console.log(formattedEntry);
|
|
}
|
|
}
|
|
log(level, message, metadata, req, res, responseTime) {
|
|
const entry = {
|
|
timestamp: new Date().toISOString(),
|
|
level,
|
|
message,
|
|
metadata
|
|
};
|
|
// Add request context if available
|
|
if (req) {
|
|
entry.requestId = req.requestId || this.generateRequestId();
|
|
entry.userId = req.user?.userId;
|
|
entry.ip = req.ip || req.socket?.remoteAddress || 'unknown';
|
|
entry.userAgent = req.get ? req.get('User-Agent') : 'unknown';
|
|
entry.method = req.method;
|
|
entry.url = req.originalUrl || req.url;
|
|
}
|
|
if (res) {
|
|
entry.statusCode = res.statusCode;
|
|
}
|
|
if (responseTime !== undefined) {
|
|
entry.responseTime = responseTime;
|
|
}
|
|
// Log to all three destinations
|
|
this.logToConsole(entry);
|
|
this.writeToLocalFile(entry);
|
|
// Add to buffer for potential batch processing
|
|
this.logBuffer.push(entry);
|
|
// Limit buffer size
|
|
if (this.logBuffer.length > 1000) {
|
|
this.logBuffer = this.logBuffer.slice(-500);
|
|
}
|
|
}
|
|
generateRequestId() {
|
|
return Math.random().toString(36).substr(2, 9);
|
|
}
|
|
async shutdown() {
|
|
try {
|
|
// Clear the upload interval
|
|
if (this.uploadInterval) {
|
|
clearInterval(this.uploadInterval);
|
|
this.uploadInterval = null;
|
|
}
|
|
// Upload current log file to Minio
|
|
if (this.currentLogFile) {
|
|
await this.uploadToMinio(this.currentLogFile);
|
|
}
|
|
this.log(LogLevel.STARTUP, 'Logging service shutting down gracefully');
|
|
// Give time for final logs to be written
|
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
}
|
|
catch (error) {
|
|
console.error('Error during logging service shutdown:', error);
|
|
}
|
|
}
|
|
// Middleware factory methods
|
|
requestLoggingMiddleware() {
|
|
return (req, res, next) => {
|
|
const startTime = Date.now();
|
|
// Generate request ID
|
|
req.requestId = this.generateRequestId();
|
|
// Log request start
|
|
this.log(LogLevel.REQUEST, `Incoming request`, undefined, req);
|
|
// Override res.end to log response
|
|
const originalEnd = res.end.bind(res);
|
|
res.end = (...args) => {
|
|
const responseTime = Date.now() - startTime;
|
|
LoggingService.getInstance().log(LogLevel.REQUEST, `Request completed`, undefined, req, res, responseTime);
|
|
return originalEnd(...args);
|
|
};
|
|
next();
|
|
};
|
|
}
|
|
errorLoggingMiddleware() {
|
|
return (error, req, res, next) => {
|
|
this.log(LogLevel.ERROR, `Unhandled error: ${error.message}`, {
|
|
stack: error.stack,
|
|
name: error.name
|
|
}, req, res);
|
|
next(error);
|
|
};
|
|
}
|
|
}
|
|
exports.LoggingService = LoggingService;
|
|
exports.default = LoggingService;
|
|
//# sourceMappingURL=LoggingService.js.map
|