@clipwhisperer/common
Version:
ClipWhisperer Common - Shared library providing core utilities, database schemas, authentication, bucket management, and common functionality across all ClipWhisperer microservices
376 lines (375 loc) • 12.8 kB
JavaScript
;
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;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.Logger = exports.FileTransport = exports.ConsoleTransport = void 0;
const crypto_1 = require("crypto");
const fs_1 = require("fs");
const promises_1 = require("fs/promises");
const path_1 = require("path");
/**
* Console transport for logging to stdout/stderr
*/
class ConsoleTransport {
constructor(colors = true) {
this.name = 'console';
this.colors = colors;
}
async write(entry) {
const formatted = this.formatEntry(entry);
if (entry.level === 'error' || entry.level === 'warn') {
console.error(formatted);
}
else {
console.log(formatted);
}
}
formatEntry(entry) {
const timestamp = entry.timestamp;
const level = this.colors ? this.colorizeLevel(entry.level) : entry.level.toUpperCase();
const service = entry.service ? `[${entry.service}]` : '';
const component = entry.component ? `[${entry.component}]` : '';
const correlationId = entry.correlationId ? `(${entry.correlationId.slice(0, 8)})` : '';
let message = `${timestamp} ${level} ${service}${component}${correlationId} ${entry.message}`;
if (entry.context && Object.keys(entry.context).length > 0) {
message += ` | Context: ${JSON.stringify(entry.context)}`;
}
if (entry.error) {
message += `\n Error: ${entry.error.name}: ${entry.error.message}`;
if (entry.error.stack) {
message += `\n Stack: ${entry.error.stack}`;
}
}
return message;
}
colorizeLevel(level) {
if (!this.colors)
return level.toUpperCase();
const colors = {
error: '\x1b[31m', // Red
warn: '\x1b[33m', // Yellow
info: '\x1b[36m', // Cyan
debug: '\x1b[35m', // Magenta
trace: '\x1b[37m', // White
};
const reset = '\x1b[0m';
return `${colors[level]}${level.toUpperCase()}${reset}`;
}
}
exports.ConsoleTransport = ConsoleTransport;
/**
* File transport for logging to files with rotation
*/
class FileTransport {
constructor(logPath, maxFileSize = 10 * 1024 * 1024, // 10MB
maxFiles = 5) {
this.name = 'file';
this.writeStream = null;
this.currentFileSize = 0;
this.logPath = logPath;
this.maxFileSize = maxFileSize;
this.maxFiles = maxFiles;
}
async write(entry) {
if (!this.writeStream) {
await this.createWriteStream();
}
const formatted = JSON.stringify(entry) + '\n';
// Check if we need to rotate the log file
if (this.currentFileSize + formatted.length > this.maxFileSize) {
await this.rotateLogFile();
}
return new Promise((resolve, reject) => {
this.writeStream.write(formatted, (error) => {
if (error) {
reject(error);
}
else {
this.currentFileSize += formatted.length;
resolve();
}
});
});
}
async flush() {
if (this.writeStream) {
return new Promise((resolve) => {
this.writeStream.end(resolve);
});
}
}
async close() {
if (this.writeStream) {
await this.flush();
this.writeStream = null;
}
}
async createWriteStream() {
// Ensure log directory exists
await (0, promises_1.mkdir)((0, path_1.join)(this.logPath, '..'), { recursive: true });
this.writeStream = (0, fs_1.createWriteStream)(this.logPath, { flags: 'a' });
this.currentFileSize = 0; // Reset size counter
}
async rotateLogFile() {
if (this.writeStream) {
await this.close();
}
// Rotate existing files
for (let i = this.maxFiles - 1; i > 0; i--) {
const oldFile = `${this.logPath}.${i}`;
const newFile = `${this.logPath}.${i + 1}`;
try {
const fs = await Promise.resolve().then(() => __importStar(require('fs/promises')));
await fs.rename(oldFile, newFile);
}
catch {
// File doesn't exist, continue
}
}
// Move current log to .1
try {
const fs = await Promise.resolve().then(() => __importStar(require('fs/promises')));
await fs.rename(this.logPath, `${this.logPath}.1`);
}
catch {
// File doesn't exist, continue
}
await this.createWriteStream();
}
}
exports.FileTransport = FileTransport;
/**
* Enterprise-grade structured logger with multiple transports and correlation tracking
*/
class Logger {
constructor() {
this.transports = new Map();
this.correlationId = null;
this.service = null;
this.component = null;
this.logLevels = {
error: 0,
warn: 1,
info: 2,
debug: 3,
trace: 4,
};
this.logLevel = process.env.LOG_LEVEL || 'info';
this.initializeTransports();
}
static getInstance() {
if (!Logger.instance) {
Logger.instance = new Logger();
}
return Logger.instance;
}
async initializeTransports() {
// Add console transport
this.addTransport(new ConsoleTransport(process.env.LOG_COLORS !== 'false'));
// Add file transport if log directory is specified
const logDir = process.env.LOG_DIRECTORY || './logs';
const logFile = (0, path_1.join)(logDir, 'application.log');
this.addTransport(new FileTransport(logFile));
}
setCorrelationId(correlationId) {
this.correlationId = correlationId;
}
setService(service) {
this.service = service;
}
setComponent(component) {
this.component = component;
}
clearContext() {
this.correlationId = null;
this.service = null;
this.component = null;
}
child(context) {
const childLogger = Object.create(this);
childLogger.service = context.service || this.service;
childLogger.component = context.component || this.component;
childLogger.correlationId = context.correlationId || this.correlationId;
return childLogger;
}
error(message, context, error) {
this.log('error', message, context, error);
}
warn(message, context) {
this.log('warn', message, context);
}
info(message, context) {
this.log('info', message, context);
}
debug(message, context) {
this.log('debug', message, context);
}
trace(message, context) {
this.log('trace', message, context);
}
log(level, message, context, error) {
if (!this.shouldLog(level))
return;
const entry = {
timestamp: new Date().toISOString(),
level,
message,
context,
correlationId: this.correlationId || undefined,
service: this.service || undefined,
component: this.component || undefined,
error: error ? {
name: error.name,
message: error.message,
stack: error.stack,
} : undefined,
metadata: {
pid: process.pid,
hostname: require('os').hostname(),
version: process.version,
},
};
// Write to all transports (fire and forget for performance)
this.writeToTransports(entry).catch(console.error);
}
shouldLog(level) {
return this.logLevels[level] <= this.logLevels[this.logLevel];
}
async writeToTransports(entry) {
const writePromises = Array.from(this.transports.values()).map(transport => transport.write(entry).catch(error => {
console.error(`Failed to write to transport ${transport.name}:`, error);
}));
await Promise.allSettled(writePromises);
}
addTransport(transport) {
this.transports.set(transport.name, transport);
}
removeTransport(name) {
return this.transports.delete(name);
}
getTransports() {
return Array.from(this.transports.values());
}
async flush() {
const flushPromises = Array.from(this.transports.values())
.filter(transport => transport.flush)
.map(transport => transport.flush());
await Promise.allSettled(flushPromises);
}
async close() {
const closePromises = Array.from(this.transports.values())
.filter(transport => transport.close)
.map(transport => transport.close());
await Promise.allSettled(closePromises);
this.transports.clear();
}
async performance(operation, fn) {
return this.performanceSync(operation, fn);
}
async performanceSync(operation, fn) {
const startTime = Date.now();
const correlationId = this.generateCorrelationId();
this.info(`Starting operation: ${operation}`, {
operation,
correlationId,
startTime: new Date(startTime).toISOString()
});
try {
const result = await fn();
const duration = Date.now() - startTime;
this.info(`Operation completed: ${operation}`, {
operation,
correlationId,
duration,
status: 'success'
});
return result;
}
catch (error) {
const duration = Date.now() - startTime;
this.error(`Operation failed: ${operation}`, {
operation,
correlationId,
duration,
status: 'error'
}, error instanceof Error ? error : new Error(String(error)));
throw error;
}
}
metric(name, value, tags) {
this.debug(`Metric: ${name}`, { metric: name, value, tags });
}
counter(name, increment = 1, tags) {
this.metric(name, increment, { ...tags, type: 'counter' });
}
gauge(name, value, tags) {
this.metric(name, value, { ...tags, type: 'gauge' });
}
histogram(name, value, tags) {
this.metric(name, value, { ...tags, type: 'histogram' });
}
audit(action, resource, context) {
this.info(`Audit: ${action}`, {
audit: true,
action,
resource,
...context
});
}
security(event, details) {
this.warn(`Security event: ${event}`, {
security: true,
event,
...details
});
}
health(service, status, details) {
const level = status === 'healthy' ? 'info' : 'warn';
this.log(level, `Health check: ${service} is ${status}`, {
health: true,
service,
status,
...details
});
}
static generateCorrelationId() {
return (0, crypto_1.randomUUID)();
}
generateCorrelationId() {
return Logger.generateCorrelationId();
}
}
exports.Logger = Logger;
// Export singleton instance for convenience
exports.default = Logger.getInstance();