@diagramers/api
Version:
Diagramers API - A comprehensive Node.js API template with TypeScript, Firebase Functions, and Socket.io
396 lines (341 loc) • 10.3 kB
text/typescript
export interface LoggerConfig {
level: string;
format: 'json' | 'simple' | 'colored';
transports: string[];
file?: {
enabled: boolean;
path: string;
maxSize: string;
maxFiles: number;
};
console?: {
enabled: boolean;
colors: boolean;
};
database?: {
enabled: boolean;
collection: string;
connection?: any;
};
external?: {
enabled: boolean;
type: 'serilog' | 'sentry' | 'loggly' | 'papertrail';
url?: string;
apiKey?: string;
appName?: string;
};
}
export interface LogEntry {
timestamp: string;
level: string;
message: string;
meta?: any;
trace?: string;
userId?: string;
requestId?: string;
sessionId?: string;
}
export interface LogTransport {
name: string;
log(entry: LogEntry): Promise<void>;
isEnabled(): boolean;
}
class ConsoleTransport implements LogTransport {
name = 'console';
private colors: boolean;
private format: string;
constructor(config: LoggerConfig) {
this.colors = config.console?.colors ?? true;
this.format = config.format;
}
isEnabled(): boolean {
return true; // Console is always enabled
}
async log(entry: LogEntry): Promise<void> {
const formattedMessage = this.formatMessage(entry);
switch (entry.level) {
case 'error':
console.error(formattedMessage);
break;
case 'warn':
console.warn(formattedMessage);
break;
case 'info':
console.log(formattedMessage);
break;
case 'debug':
console.debug(formattedMessage);
break;
default:
console.log(formattedMessage);
}
}
private formatMessage(entry: LogEntry): string {
if (this.format === 'json') {
return JSON.stringify(entry);
}
if (this.format === 'colored' && this.colors) {
const colors = {
error: '\x1b[31m', // Red
warn: '\x1b[33m', // Yellow
info: '\x1b[36m', // Cyan
debug: '\x1b[35m', // Magenta
verbose: '\x1b[90m' // Gray
};
const reset = '\x1b[0m';
return `${colors[entry.level as keyof typeof colors]}[${entry.level.toUpperCase()}]${reset} ${entry.timestamp} - ${entry.message}`;
}
return `[${entry.level.toUpperCase()}] ${entry.timestamp} - ${entry.message}`;
}
}
class FileTransport implements LogTransport {
name = 'file';
private enabled: boolean;
private path: string;
private fs: any;
constructor(config: LoggerConfig) {
this.enabled = config.file?.enabled ?? false;
this.path = config.file?.path ?? './logs/app.log';
if (this.enabled) {
// Dynamically import fs-extra to avoid issues
this.fs = require('fs-extra');
this.ensureLogDirectory();
}
}
isEnabled(): boolean {
return this.enabled;
}
async log(entry: LogEntry): Promise<void> {
if (!this.enabled || !this.fs) return;
try {
const logLine = JSON.stringify(entry) + '\n';
await this.fs.appendFile(this.path, logLine);
} catch (error) {
console.error('Failed to write to log file:', error);
}
}
private async ensureLogDirectory(): Promise<void> {
if (!this.fs) return;
try {
const lastSlashIndex = this.path.lastIndexOf('/');
if (lastSlashIndex > 0) {
const dir = this.path.substring(0, lastSlashIndex);
await this.fs.ensureDir(dir);
}
} catch (error) {
console.error('Failed to create log directory:', error);
}
}
}
class DatabaseTransport implements LogTransport {
name = 'database';
private enabled: boolean;
private collection: string;
private connection: any;
constructor(config: LoggerConfig) {
this.enabled = config.database?.enabled ?? false;
this.collection = config.database?.collection ?? 'logs';
this.connection = config.database?.connection;
}
isEnabled(): boolean {
return this.enabled && this.connection;
}
async log(entry: LogEntry): Promise<void> {
if (!this.enabled || !this.connection) return;
try {
const logDocument = {
...entry,
createdAt: new Date(entry.timestamp)
};
await this.connection.collection(this.collection).insertOne(logDocument);
} catch (error) {
console.error('Failed to write to database:', error);
}
}
}
class ExternalTransport implements LogTransport {
name = 'external';
private enabled: boolean;
private type: string;
private url?: string;
private apiKey?: string;
private appName?: string;
constructor(config: LoggerConfig) {
this.enabled = config.external?.enabled ?? false;
this.type = config.external?.type ?? 'serilog';
this.url = config.external?.url;
this.apiKey = config.external?.apiKey;
this.appName = config.external?.appName;
}
isEnabled(): boolean {
return this.enabled === true && this.url !== undefined;
}
async log(entry: LogEntry): Promise<void> {
if (!this.enabled || !this.url) return;
try {
const payload = this.formatPayload(entry);
await fetch(this.url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...(this.apiKey && { 'Authorization': `Bearer ${this.apiKey}` })
},
body: JSON.stringify(payload)
});
} catch (error) {
console.error('Failed to send to external logging service:', error);
}
}
private formatPayload(entry: LogEntry): any {
switch (this.type) {
case 'serilog':
return {
timestamp: entry.timestamp,
level: entry.level,
message: entry.message,
properties: entry.meta,
...(this.appName && { application: this.appName })
};
case 'sentry':
return {
level: entry.level,
message: entry.message,
timestamp: entry.timestamp,
extra: entry.meta,
tags: {
...(this.appName && { application: this.appName })
}
};
case 'loggly':
return {
level: entry.level,
message: entry.message,
timestamp: entry.timestamp,
meta: entry.meta,
...(this.appName && { application: this.appName })
};
default:
return entry;
}
}
}
class Logger {
private level: string;
private transports: LogTransport[] = [];
private requestId?: string;
private userId?: string;
private sessionId?: string;
constructor(config: LoggerConfig) {
this.level = config.level || 'info';
// Initialize transports
this.transports.push(new ConsoleTransport(config));
if (config.file?.enabled) {
this.transports.push(new FileTransport(config));
}
if (config.database?.enabled) {
this.transports.push(new DatabaseTransport(config));
}
if (config.external?.enabled) {
this.transports.push(new ExternalTransport(config));
}
}
private shouldLog(level: string): boolean {
const levels = ['error', 'warn', 'info', 'debug', 'verbose'];
return levels.indexOf(level) <= levels.indexOf(this.level);
}
private createLogEntry(level: string, message: string, meta?: any): LogEntry {
return {
timestamp: new Date().toISOString(),
level: level.toUpperCase(),
message,
meta,
...(this.requestId && { requestId: this.requestId }),
...(this.userId && { userId: this.userId }),
...(this.sessionId && { sessionId: this.sessionId })
};
}
private async writeToTransports(entry: LogEntry): Promise<void> {
const enabledTransports = this.transports.filter(t => t.isEnabled());
await Promise.allSettled(
enabledTransports.map(transport => transport.log(entry))
);
}
info(message: string, meta?: any): void {
if (this.shouldLog('info')) {
const entry = this.createLogEntry('info', message, meta);
this.writeToTransports(entry);
}
}
warn(message: string, meta?: any): void {
if (this.shouldLog('warn')) {
const entry = this.createLogEntry('warn', message, meta);
this.writeToTransports(entry);
}
}
error(message: string, meta?: any): void {
if (this.shouldLog('error')) {
const entry = this.createLogEntry('error', message, meta);
this.writeToTransports(entry);
}
}
debug(message: string, meta?: any): void {
if (this.shouldLog('debug')) {
const entry = this.createLogEntry('debug', message, meta);
this.writeToTransports(entry);
}
}
verbose(message: string, meta?: any): void {
if (this.shouldLog('verbose')) {
const entry = this.createLogEntry('verbose', message, meta);
this.writeToTransports(entry);
}
}
// Method to change log level at runtime
setLevel(level: string): void {
this.level = level;
}
// Method to get current log level
getLevel(): string {
return this.level;
}
// Method to set context for request tracking
setContext(requestId?: string, userId?: string, sessionId?: string): void {
this.requestId = requestId;
this.userId = userId;
this.sessionId = sessionId;
}
// Method to add custom transport
addTransport(transport: LogTransport): void {
this.transports.push(transport);
}
// Method to get all transports
getTransports(): LogTransport[] {
return this.transports;
}
}
export const logger = new Logger({
level: process.env.LOG_LEVEL || 'info',
format: (process.env.LOG_FORMAT as 'json' | 'simple' | 'colored') || 'colored',
transports: ['console'],
console: {
enabled: true,
colors: process.env.LOG_COLORS !== 'false'
},
file: {
enabled: process.env.LOG_FILE_ENABLED === 'true',
path: process.env.LOG_FILE_PATH || './logs/app.log',
maxSize: process.env.LOG_FILE_MAX_SIZE || '10m',
maxFiles: parseInt(process.env.LOG_FILE_MAX_FILES || '5')
},
database: {
enabled: process.env.LOG_DATABASE_ENABLED === 'true',
collection: process.env.LOG_DATABASE_COLLECTION || 'logs'
},
external: {
enabled: process.env.LOG_EXTERNAL_ENABLED === 'true',
type: (process.env.LOG_EXTERNAL_TYPE as 'serilog' | 'sentry' | 'loggly' | 'papertrail') || 'serilog',
url: process.env.LOG_EXTERNAL_URL,
apiKey: process.env.LOG_EXTERNAL_API_KEY,
appName: process.env.LOG_EXTERNAL_APP_NAME
}
});