mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-01 04:42:10 +03:00
feat(logs): add Logs Dashboard with real-time Console Viewer
- Consolidated 4-tab Logs page: Request Logs, Proxy Logs, Audit Logs, Console - Terminal-style Console Log Viewer with level filter, search, auto-scroll - Console interceptor captures all console.log/warn/error to JSON log file - Integrated in Next.js instrumentation.ts for both dev and prod - Log rotation by size + retention-based cleanup - Fixed pino logger file transport (pino/file targets only) - Moved initAuditLog() to instrumentation.ts for proper initialization - Bumped version to 1.0.3 - Updated CHANGELOG, all 8 README translations, and .env.example
This commit is contained in:
@@ -9,25 +9,99 @@
|
||||
*
|
||||
* In development, output is pretty-printed via pino-pretty.
|
||||
* In production, output is structured JSON for log aggregation.
|
||||
*
|
||||
* When LOG_TO_FILE is enabled (default: true), logs are also written
|
||||
* as JSON lines to the file specified by LOG_FILE_PATH.
|
||||
*/
|
||||
import pino from "pino";
|
||||
import { resolve } from "path";
|
||||
import { getLogConfig, initLogRotation } from "@/lib/logRotation";
|
||||
|
||||
const isDev = process.env.NODE_ENV !== "production";
|
||||
|
||||
const baseConfig = {
|
||||
const baseConfig: pino.LoggerOptions = {
|
||||
level: process.env.LOG_LEVEL || (isDev ? "debug" : "info"),
|
||||
base: { service: "omniroute" },
|
||||
timestamp: pino.stdTimeFunctions.isoTime,
|
||||
formatters: {
|
||||
level(label) {
|
||||
level(label: string) {
|
||||
return { level: label };
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// In development, use pino-pretty for human-readable output
|
||||
const devTransport = isDev
|
||||
? {
|
||||
/**
|
||||
* Build the logger with optional file transport.
|
||||
* Uses pino transport targets for all destinations.
|
||||
*/
|
||||
function buildLogger(): pino.Logger {
|
||||
const logConfig = getLogConfig();
|
||||
const logLevel = (baseConfig.level as string) || "info";
|
||||
|
||||
// If file logging is enabled, set up dual transport (stdout + file)
|
||||
if (logConfig.logToFile) {
|
||||
try {
|
||||
// Initialize log directory and rotation
|
||||
initLogRotation();
|
||||
|
||||
// Resolve to absolute path for pino worker threads
|
||||
const absLogPath = resolve(logConfig.logFilePath);
|
||||
|
||||
if (isDev) {
|
||||
// Dev: pino-pretty → stdout, JSON → file
|
||||
return pino({
|
||||
...baseConfig,
|
||||
transport: {
|
||||
targets: [
|
||||
{
|
||||
target: "pino-pretty",
|
||||
options: {
|
||||
colorize: true,
|
||||
translateTime: "HH:MM:ss.l",
|
||||
ignore: "pid,hostname,service",
|
||||
messageFormat: "[{module}] {msg}",
|
||||
destination: 1,
|
||||
},
|
||||
level: logLevel,
|
||||
},
|
||||
{
|
||||
target: "pino/file",
|
||||
options: { destination: absLogPath, mkdir: true },
|
||||
level: logLevel,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Production: JSON → stdout + JSON → file
|
||||
return pino({
|
||||
...baseConfig,
|
||||
transport: {
|
||||
targets: [
|
||||
{
|
||||
target: "pino/file",
|
||||
options: { destination: 1 }, // stdout
|
||||
level: logLevel,
|
||||
},
|
||||
{
|
||||
target: "pino/file",
|
||||
options: { destination: absLogPath, mkdir: true },
|
||||
level: logLevel,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
} catch {
|
||||
// If file setup fails, fall back to console-only logging
|
||||
console.warn("[logger] Failed to set up file transport, falling back to console only");
|
||||
}
|
||||
}
|
||||
|
||||
// Console-only (no file logging)
|
||||
if (isDev) {
|
||||
return pino({
|
||||
...baseConfig,
|
||||
transport: {
|
||||
target: "pino-pretty",
|
||||
options: {
|
||||
@@ -37,17 +111,20 @@ const devTransport = isDev
|
||||
messageFormat: "[{module}] {msg}",
|
||||
},
|
||||
},
|
||||
}
|
||||
: {};
|
||||
});
|
||||
}
|
||||
|
||||
export const logger = pino({ ...baseConfig, ...devTransport });
|
||||
return pino(baseConfig);
|
||||
}
|
||||
|
||||
export const logger = buildLogger();
|
||||
|
||||
/**
|
||||
* Create a child logger with a module tag.
|
||||
* @param {string} module - Module name for log context (e.g., "proxy", "db", "sse")
|
||||
* @returns {pino.Logger}
|
||||
*/
|
||||
export function createLogger(module) {
|
||||
export function createLogger(module: string) {
|
||||
return logger.child({ module });
|
||||
}
|
||||
|
||||
|
||||
@@ -5,10 +5,15 @@
|
||||
* and human-readable output for development. Replaces scattered console.log
|
||||
* calls with consistent, parseable log entries.
|
||||
*
|
||||
* When LOG_TO_FILE is enabled, log entries are also appended as JSON lines
|
||||
* to the application log file for the Console Log Viewer.
|
||||
*
|
||||
* @module shared/utils/structuredLogger
|
||||
*/
|
||||
|
||||
import { getCorrelationId } from "../middleware/correlationId";
|
||||
import { appendFileSync, existsSync, mkdirSync } from "fs";
|
||||
import { dirname, resolve } from "path";
|
||||
|
||||
const LOG_LEVELS: Record<string, number> = {
|
||||
debug: 10,
|
||||
@@ -21,7 +26,40 @@ const LOG_LEVELS: Record<string, number> = {
|
||||
const currentLevel = LOG_LEVELS[process.env.LOG_LEVEL?.toLowerCase() || ""] || LOG_LEVELS.info;
|
||||
const isProduction = process.env.NODE_ENV === "production";
|
||||
|
||||
function formatEntry(level: string, component: string, message: string, meta?: Record<string, unknown>) {
|
||||
// File logging configuration
|
||||
const logToFile = process.env.LOG_TO_FILE !== "false";
|
||||
const logFilePath = resolve(process.env.LOG_FILE_PATH || "logs/application/app.log");
|
||||
|
||||
// Ensure log directory exists once at module load
|
||||
if (logToFile) {
|
||||
try {
|
||||
const dir = dirname(logFilePath);
|
||||
if (!existsSync(dir)) {
|
||||
mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
} catch {
|
||||
// silently ignore — will retry on each write
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Append a JSON log line to the log file (non-blocking best-effort).
|
||||
*/
|
||||
function writeToFile(entry: Record<string, unknown>) {
|
||||
if (!logToFile) return;
|
||||
try {
|
||||
appendFileSync(logFilePath, JSON.stringify(entry) + "\n");
|
||||
} catch {
|
||||
// Silently fail — file logging should never break the app
|
||||
}
|
||||
}
|
||||
|
||||
function formatEntry(
|
||||
level: string,
|
||||
component: string,
|
||||
message: string,
|
||||
meta?: Record<string, unknown>
|
||||
) {
|
||||
const entry: Record<string, unknown> = {
|
||||
timestamp: new Date().toISOString(),
|
||||
level,
|
||||
@@ -46,30 +84,60 @@ function formatEntry(level: string, component: string, message: string, meta?: R
|
||||
return `[${entry.timestamp}] ${level.toUpperCase().padEnd(5)} [${component}]${corrStr} ${message}${metaStr}`;
|
||||
}
|
||||
|
||||
function buildEntry(
|
||||
level: string,
|
||||
component: string,
|
||||
message: string,
|
||||
meta?: Record<string, unknown>
|
||||
) {
|
||||
const entry: Record<string, unknown> = {
|
||||
timestamp: new Date().toISOString(),
|
||||
level,
|
||||
component,
|
||||
message,
|
||||
...meta,
|
||||
};
|
||||
const correlationId = getCorrelationId() as string | undefined;
|
||||
if (correlationId) {
|
||||
entry.correlationId = correlationId;
|
||||
}
|
||||
return entry;
|
||||
}
|
||||
|
||||
export function createLogger(component: string) {
|
||||
return {
|
||||
debug(message: string, meta?: Record<string, unknown>) {
|
||||
if (currentLevel <= LOG_LEVELS.debug) {
|
||||
const entry = buildEntry("debug", component, message, meta);
|
||||
console.debug(formatEntry("debug", component, message, meta));
|
||||
writeToFile(entry);
|
||||
}
|
||||
},
|
||||
info(message: string, meta?: Record<string, unknown>) {
|
||||
if (currentLevel <= LOG_LEVELS.info) {
|
||||
const entry = buildEntry("info", component, message, meta);
|
||||
console.info(formatEntry("info", component, message, meta));
|
||||
writeToFile(entry);
|
||||
}
|
||||
},
|
||||
warn(message: string, meta?: Record<string, unknown>) {
|
||||
if (currentLevel <= LOG_LEVELS.warn) {
|
||||
const entry = buildEntry("warn", component, message, meta);
|
||||
console.warn(formatEntry("warn", component, message, meta));
|
||||
writeToFile(entry);
|
||||
}
|
||||
},
|
||||
error(message: string, meta?: Record<string, unknown>) {
|
||||
if (currentLevel <= LOG_LEVELS.error) {
|
||||
const entry = buildEntry("error", component, message, meta);
|
||||
console.error(formatEntry("error", component, message, meta));
|
||||
writeToFile(entry);
|
||||
}
|
||||
},
|
||||
fatal(message: string, meta?: Record<string, unknown>) {
|
||||
const entry = buildEntry("fatal", component, message, meta);
|
||||
console.error(formatEntry("fatal", component, message, meta));
|
||||
writeToFile(entry);
|
||||
},
|
||||
child(defaultMeta: Record<string, unknown>) {
|
||||
return createLogger(component);
|
||||
|
||||
Reference in New Issue
Block a user