2026-01-20 22:56:14 +00:00
|
|
|
import { createLogger, format, transports, Logger as WinstonLogger } from "winston";
|
|
|
|
|
import type { Logform } from "winston";
|
2025-05-05 12:48:03 -07:00
|
|
|
import dotenv from "dotenv";
|
|
|
|
|
dotenv.config();
|
2025-04-20 11:29:53 -07:00
|
|
|
|
2025-07-25 10:16:29 -07:00
|
|
|
const SERVICE_NAME = "Logger";
|
|
|
|
|
|
2026-01-20 22:56:14 +00:00
|
|
|
interface LogConfig {
|
|
|
|
|
message: string;
|
|
|
|
|
service?: string;
|
|
|
|
|
method?: string;
|
|
|
|
|
details?: Record<string, unknown>;
|
|
|
|
|
stack?: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface LogEntry extends LogConfig {
|
|
|
|
|
level: string;
|
|
|
|
|
timestamp: string;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-21 11:18:13 -08:00
|
|
|
export interface ILogger {
|
|
|
|
|
readonly serviceName: string;
|
|
|
|
|
info(config: LogConfig): void;
|
|
|
|
|
warn(config: LogConfig): void;
|
|
|
|
|
error(config: LogConfig): void;
|
|
|
|
|
debug(config: LogConfig): void;
|
|
|
|
|
cacheLog(entry: LogEntry): void;
|
|
|
|
|
getLogs(): LogEntry[];
|
|
|
|
|
buildLogEntry(level: string, config: LogConfig): LogEntry;
|
2026-06-24 10:53:03 -07:00
|
|
|
setLogLevel(level: string): void;
|
2026-01-20 22:56:14 +00:00
|
|
|
}
|
|
|
|
|
|
2026-01-21 11:18:13 -08:00
|
|
|
class Logger implements ILogger {
|
2025-07-25 10:16:29 -07:00
|
|
|
static SERVICE_NAME = SERVICE_NAME;
|
2026-01-20 22:56:14 +00:00
|
|
|
|
|
|
|
|
private logger: WinstonLogger;
|
|
|
|
|
private logCache: LogEntry[];
|
|
|
|
|
private maxCacheSize: number;
|
|
|
|
|
|
2026-06-24 10:53:03 -07:00
|
|
|
constructor() {
|
2025-06-23 11:25:42 +08:00
|
|
|
this.logCache = [];
|
2025-06-23 12:19:52 +08:00
|
|
|
this.maxCacheSize = 1000;
|
2026-01-20 22:56:14 +00:00
|
|
|
const consoleFormat = format.printf((info: Logform.TransformableInfo) => {
|
|
|
|
|
const { level, service, method, details, timestamp, stack } = info;
|
2026-03-03 18:26:42 +00:00
|
|
|
const message = info.message as string;
|
2026-01-20 22:56:14 +00:00
|
|
|
let formattedMessage: string = message;
|
|
|
|
|
let formattedDetails: string | undefined;
|
|
|
|
|
|
|
|
|
|
if (typeof message === "object" && message !== null) {
|
|
|
|
|
formattedMessage = JSON.stringify(message, null, 2);
|
2025-07-21 10:34:16 -07:00
|
|
|
}
|
2025-04-20 11:29:53 -07:00
|
|
|
|
2026-01-20 22:56:14 +00:00
|
|
|
if (typeof details === "object" && details !== null) {
|
|
|
|
|
formattedDetails = JSON.stringify(details, null, 2);
|
2025-07-21 10:34:16 -07:00
|
|
|
}
|
2026-01-20 22:56:14 +00:00
|
|
|
|
2025-07-21 10:34:16 -07:00
|
|
|
let msg = `${timestamp} ${level}:`;
|
2026-03-03 18:26:42 +00:00
|
|
|
if (service) msg += ` [${service}]`;
|
|
|
|
|
if (method) msg += `(${method})`;
|
|
|
|
|
if (formattedMessage) msg += ` ${formattedMessage}`;
|
|
|
|
|
if (formattedDetails) msg += ` (details: ${formattedDetails})`;
|
2025-04-20 11:29:53 -07:00
|
|
|
|
2026-01-20 22:56:14 +00:00
|
|
|
if (typeof stack === "string") {
|
2025-07-21 10:34:16 -07:00
|
|
|
const stackTrace = stack
|
2026-01-20 22:56:14 +00:00
|
|
|
.split("\n")
|
2025-07-21 10:34:16 -07:00
|
|
|
.slice(1) // Remove first line (error message)
|
2026-01-20 22:56:14 +00:00
|
|
|
.map((line: string) => {
|
2025-07-21 10:34:16 -07:00
|
|
|
const match = line.match(/at\s+(.+?)\s+\((.+?):(\d+):(\d+)\)/);
|
|
|
|
|
if (match) {
|
|
|
|
|
return {
|
|
|
|
|
function: match[1],
|
|
|
|
|
file: match[2],
|
2026-01-20 22:56:14 +00:00
|
|
|
line: parseInt(match[3] ?? "0", 10),
|
|
|
|
|
column: parseInt(match[4] ?? "0", 10),
|
2025-07-21 10:34:16 -07:00
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
return line.trim();
|
|
|
|
|
});
|
2026-01-20 22:56:14 +00:00
|
|
|
msg += ` (stack: ${JSON.stringify(stackTrace, null, 2)})`;
|
2025-04-20 11:29:53 -07:00
|
|
|
}
|
2025-07-21 10:34:16 -07:00
|
|
|
|
|
|
|
|
return msg;
|
|
|
|
|
});
|
2025-04-20 11:29:53 -07:00
|
|
|
|
|
|
|
|
this.logger = createLogger({
|
2026-06-24 10:53:03 -07:00
|
|
|
level: "info",
|
2025-04-20 11:29:53 -07:00
|
|
|
format: format.combine(format.timestamp()),
|
|
|
|
|
transports: [
|
|
|
|
|
new transports.Console({
|
2025-07-21 10:34:16 -07:00
|
|
|
format: format.combine(format.colorize(), format.prettyPrint(), format.json(), consoleFormat),
|
2025-04-20 11:29:53 -07:00
|
|
|
}),
|
|
|
|
|
new transports.File({
|
|
|
|
|
format: format.combine(format.json()),
|
|
|
|
|
filename: "app.log",
|
|
|
|
|
}),
|
|
|
|
|
],
|
|
|
|
|
});
|
|
|
|
|
}
|
2025-07-25 15:15:14 -07:00
|
|
|
|
|
|
|
|
get serviceName() {
|
|
|
|
|
return Logger.SERVICE_NAME;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-20 22:56:14 +00:00
|
|
|
info(config: LogConfig) {
|
2025-06-23 13:19:18 +08:00
|
|
|
const logEntry = this.buildLogEntry("info", config);
|
2025-06-23 11:25:42 +08:00
|
|
|
this.cacheLog(logEntry);
|
2025-06-25 09:35:41 +08:00
|
|
|
this.logger.info(logEntry);
|
2025-04-20 11:29:53 -07:00
|
|
|
}
|
|
|
|
|
|
2026-01-20 22:56:14 +00:00
|
|
|
warn(config: LogConfig) {
|
2025-06-23 13:19:18 +08:00
|
|
|
const logEntry = this.buildLogEntry("warn", config);
|
2025-06-23 11:25:42 +08:00
|
|
|
this.cacheLog(logEntry);
|
2025-06-25 09:35:41 +08:00
|
|
|
this.logger.warn(logEntry);
|
2025-04-20 11:29:53 -07:00
|
|
|
}
|
|
|
|
|
|
2026-01-20 22:56:14 +00:00
|
|
|
error(config: LogConfig) {
|
2025-06-23 13:19:18 +08:00
|
|
|
const logEntry = this.buildLogEntry("error", config);
|
2025-06-23 11:25:42 +08:00
|
|
|
this.cacheLog(logEntry);
|
2025-06-25 09:35:41 +08:00
|
|
|
this.logger.error(logEntry);
|
2025-04-20 11:29:53 -07:00
|
|
|
}
|
2026-01-20 22:56:14 +00:00
|
|
|
|
|
|
|
|
debug(config: LogConfig) {
|
2025-06-23 13:19:18 +08:00
|
|
|
const logEntry = this.buildLogEntry("debug", config);
|
2025-06-23 11:25:42 +08:00
|
|
|
this.cacheLog(logEntry);
|
2025-06-25 09:35:41 +08:00
|
|
|
this.logger.debug(logEntry);
|
2025-06-23 11:25:42 +08:00
|
|
|
}
|
|
|
|
|
|
2026-01-20 22:56:14 +00:00
|
|
|
cacheLog(entry: LogEntry) {
|
2025-06-23 11:25:42 +08:00
|
|
|
this.logCache.push(entry);
|
|
|
|
|
if (this.logCache.length > this.maxCacheSize) {
|
|
|
|
|
this.logCache.shift();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
getLogs() {
|
|
|
|
|
return this.logCache;
|
2025-05-05 12:48:03 -07:00
|
|
|
}
|
2025-06-23 13:19:18 +08:00
|
|
|
|
2026-01-20 22:56:14 +00:00
|
|
|
buildLogEntry(level: string, config: LogConfig): LogEntry {
|
2025-06-23 13:19:18 +08:00
|
|
|
return {
|
|
|
|
|
level,
|
|
|
|
|
message: config.message,
|
|
|
|
|
service: config.service,
|
|
|
|
|
method: config.method,
|
|
|
|
|
details: config.details,
|
2025-06-23 13:22:53 +08:00
|
|
|
stack: config.stack,
|
2025-06-23 13:19:18 +08:00
|
|
|
timestamp: new Date().toISOString(),
|
|
|
|
|
};
|
|
|
|
|
}
|
2026-06-24 10:53:03 -07:00
|
|
|
|
|
|
|
|
setLogLevel(level: string): void {
|
|
|
|
|
this.logger.level = level || "info";
|
|
|
|
|
}
|
2025-04-20 11:29:53 -07:00
|
|
|
}
|
|
|
|
|
|
2025-07-28 15:03:28 -07:00
|
|
|
export default Logger;
|