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:
@@ -19,7 +19,9 @@ const PATH_LABELS = {
|
||||
providers: "Providers",
|
||||
combos: "Combos",
|
||||
settings: "Settings",
|
||||
usage: "Usage",
|
||||
logs: "Logs",
|
||||
"audit-log": "Audit Log",
|
||||
console: "Console",
|
||||
logger: "Logger",
|
||||
translator: "Translator",
|
||||
playground: "Playground",
|
||||
@@ -27,7 +29,6 @@ const PATH_LABELS = {
|
||||
edit: "Edit",
|
||||
keys: "API Keys",
|
||||
models: "Models",
|
||||
logs: "Logs",
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -85,8 +86,12 @@ export default function Breadcrumbs() {
|
||||
textDecoration: "none",
|
||||
transition: "color 0.15s",
|
||||
}}
|
||||
onMouseEnter={(e) => ((e.currentTarget as HTMLElement).style.color = "var(--accent, #818cf8)")}
|
||||
onMouseLeave={(e) => ((e.currentTarget as HTMLElement).style.color = "var(--text-secondary, #888)")}
|
||||
onMouseEnter={(e) =>
|
||||
((e.currentTarget as HTMLElement).style.color = "var(--accent, #818cf8)")
|
||||
}
|
||||
onMouseLeave={(e) =>
|
||||
((e.currentTarget as HTMLElement).style.color = "var(--text-secondary, #888)")
|
||||
}
|
||||
>
|
||||
{crumb.label}
|
||||
</Link>
|
||||
|
||||
302
src/shared/components/ConsoleLogViewer.tsx
Normal file
302
src/shared/components/ConsoleLogViewer.tsx
Normal file
@@ -0,0 +1,302 @@
|
||||
"use client";
|
||||
|
||||
/**
|
||||
* Console Log Viewer — Real-time application log viewer.
|
||||
*
|
||||
* Displays structured application logs from the server with a terminal-like UI.
|
||||
* Polls the backend API every 5 seconds. Shows logs from the last 1 hour.
|
||||
* Supports level filtering, text search, auto-scroll, and copy-to-clipboard.
|
||||
*/
|
||||
|
||||
import { useState, useEffect, useRef, useCallback } from "react";
|
||||
|
||||
interface LogEntry {
|
||||
timestamp: string;
|
||||
level: string;
|
||||
component?: string;
|
||||
module?: string;
|
||||
message?: string;
|
||||
msg?: string;
|
||||
correlationId?: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
const LEVEL_COLORS: Record<string, string> = {
|
||||
debug: "text-gray-400",
|
||||
trace: "text-gray-500",
|
||||
info: "text-cyan-400",
|
||||
warn: "text-yellow-400",
|
||||
error: "text-red-400",
|
||||
fatal: "text-fuchsia-400",
|
||||
};
|
||||
|
||||
const LEVEL_BG: Record<string, string> = {
|
||||
debug: "bg-gray-500/10 border-gray-500/20",
|
||||
trace: "bg-gray-500/10 border-gray-500/20",
|
||||
info: "bg-cyan-500/10 border-cyan-500/20",
|
||||
warn: "bg-yellow-500/10 border-yellow-500/20",
|
||||
error: "bg-red-500/10 border-red-500/20",
|
||||
fatal: "bg-fuchsia-500/10 border-fuchsia-500/20",
|
||||
};
|
||||
|
||||
const POLL_INTERVAL = 5000; // 5 seconds
|
||||
|
||||
export default function ConsoleLogViewer() {
|
||||
const [logs, setLogs] = useState<LogEntry[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [levelFilter, setLevelFilter] = useState("all");
|
||||
const [searchText, setSearchText] = useState("");
|
||||
const [autoScroll, setAutoScroll] = useState(true);
|
||||
const [lastUpdated, setLastUpdated] = useState<Date | null>(null);
|
||||
const [copiedIdx, setCopiedIdx] = useState<number | null>(null);
|
||||
const scrollRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const fetchLogs = useCallback(async () => {
|
||||
try {
|
||||
const params = new URLSearchParams();
|
||||
if (levelFilter !== "all") params.set("level", levelFilter);
|
||||
params.set("limit", "500");
|
||||
|
||||
const res = await fetch(`/api/logs/console?${params.toString()}`);
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
const data: LogEntry[] = await res.json();
|
||||
|
||||
setLogs(data);
|
||||
setLastUpdated(new Date());
|
||||
setError(null);
|
||||
} catch (err: any) {
|
||||
setError(err.message || "Failed to fetch logs");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [levelFilter]);
|
||||
|
||||
// Initial fetch + polling
|
||||
useEffect(() => {
|
||||
fetchLogs();
|
||||
const interval = setInterval(fetchLogs, POLL_INTERVAL);
|
||||
return () => clearInterval(interval);
|
||||
}, [fetchLogs]);
|
||||
|
||||
// Auto-scroll to bottom on new logs
|
||||
useEffect(() => {
|
||||
if (autoScroll && scrollRef.current) {
|
||||
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
|
||||
}
|
||||
}, [logs, autoScroll]);
|
||||
|
||||
const handleCopy = (entry: LogEntry, idx: number) => {
|
||||
const text = JSON.stringify(entry, null, 2);
|
||||
navigator.clipboard.writeText(text).then(() => {
|
||||
setCopiedIdx(idx);
|
||||
setTimeout(() => setCopiedIdx(null), 2000);
|
||||
});
|
||||
};
|
||||
|
||||
const formatTime = (ts: string) => {
|
||||
try {
|
||||
const d = new Date(ts);
|
||||
return d.toLocaleTimeString("en-US", {
|
||||
hour12: false,
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
fractionalSecondDigits: 3,
|
||||
});
|
||||
} catch {
|
||||
return ts;
|
||||
}
|
||||
};
|
||||
|
||||
const getText = (entry: LogEntry) => entry.msg || entry.message || "";
|
||||
const getComponent = (entry: LogEntry) => entry.component || entry.module || "";
|
||||
|
||||
// Apply text search filter
|
||||
const filteredLogs = searchText
|
||||
? logs.filter((entry) => {
|
||||
const full = JSON.stringify(entry).toLowerCase();
|
||||
return full.includes(searchText.toLowerCase());
|
||||
})
|
||||
: logs;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
{/* Toolbar */}
|
||||
<div className="flex flex-wrap items-center gap-3 p-4 rounded-xl bg-[var(--color-surface)] border border-[var(--color-border)]">
|
||||
{/* Level filter */}
|
||||
<select
|
||||
value={levelFilter}
|
||||
onChange={(e) => setLevelFilter(e.target.value)}
|
||||
aria-label="Filter by log level"
|
||||
className="px-3 py-2 rounded-lg text-sm bg-[var(--color-bg)] border border-[var(--color-border)] text-[var(--color-text-main)] focus:outline-2 focus:outline-[var(--color-accent)]"
|
||||
>
|
||||
<option value="all">All Levels</option>
|
||||
<option value="debug">Debug+</option>
|
||||
<option value="info">Info+</option>
|
||||
<option value="warn">Warn+</option>
|
||||
<option value="error">Error+</option>
|
||||
</select>
|
||||
|
||||
{/* Search */}
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search logs..."
|
||||
value={searchText}
|
||||
onChange={(e) => setSearchText(e.target.value)}
|
||||
aria-label="Search log entries"
|
||||
className="flex-1 min-w-[200px] px-3 py-2 rounded-lg text-sm bg-[var(--color-bg)] border border-[var(--color-border)] text-[var(--color-text-main)] placeholder:text-[var(--color-text-muted)] focus:outline-2 focus:outline-[var(--color-accent)]"
|
||||
/>
|
||||
|
||||
{/* Auto-scroll toggle */}
|
||||
<button
|
||||
onClick={() => setAutoScroll(!autoScroll)}
|
||||
title={autoScroll ? "Disable auto-scroll" : "Enable auto-scroll"}
|
||||
className={`px-3 py-2 rounded-lg text-sm font-medium border transition-colors ${
|
||||
autoScroll
|
||||
? "bg-cyan-500/15 text-cyan-400 border-cyan-500/30"
|
||||
: "bg-[var(--color-bg)] text-[var(--color-text-muted)] border-[var(--color-border)]"
|
||||
}`}
|
||||
>
|
||||
<span className="material-symbols-outlined text-[16px] align-middle mr-1">
|
||||
{autoScroll ? "vertical_align_bottom" : "lock"}
|
||||
</span>
|
||||
Auto-scroll
|
||||
</button>
|
||||
|
||||
{/* Refresh */}
|
||||
<button
|
||||
onClick={fetchLogs}
|
||||
disabled={loading}
|
||||
className="px-3 py-2 rounded-lg text-sm font-medium bg-[var(--color-bg)] border border-[var(--color-border)] text-[var(--color-text-main)] hover:bg-[var(--color-bg-alt)] disabled:opacity-50 transition-colors"
|
||||
>
|
||||
<span className="material-symbols-outlined text-[16px] align-middle">refresh</span>
|
||||
</button>
|
||||
|
||||
{/* Status */}
|
||||
<div className="flex items-center gap-2 ml-auto text-xs text-[var(--color-text-muted)]">
|
||||
<span className="inline-block w-2 h-2 rounded-full bg-green-500 animate-pulse" />
|
||||
<span>{filteredLogs.length} entries</span>
|
||||
<span className="text-[var(--color-text-muted)]/50">•</span>
|
||||
<span>Last 1h</span>
|
||||
{lastUpdated && (
|
||||
<>
|
||||
<span className="text-[var(--color-text-muted)]/50">•</span>
|
||||
<span>Updated {lastUpdated.toLocaleTimeString()}</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Error */}
|
||||
{error && (
|
||||
<div
|
||||
className="p-4 rounded-lg bg-red-500/10 border border-red-500/30 text-red-400 text-sm"
|
||||
role="alert"
|
||||
>
|
||||
<span className="material-symbols-outlined text-[16px] align-middle mr-2">error</span>
|
||||
{error}
|
||||
<span className="text-xs ml-2 opacity-70">
|
||||
— Make sure the application is writing logs to file (LOG_TO_FILE=true)
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Console output */}
|
||||
<div
|
||||
ref={scrollRef}
|
||||
className="rounded-xl border border-[var(--color-border)] bg-[#0d1117] overflow-auto font-mono text-xs leading-relaxed"
|
||||
style={{ maxHeight: "calc(100vh - 340px)", minHeight: "400px" }}
|
||||
role="log"
|
||||
aria-label="Application console logs"
|
||||
aria-live="polite"
|
||||
>
|
||||
{/* Header bar */}
|
||||
<div className="sticky top-0 z-10 px-4 py-2 bg-[#161b22] border-b border-[#30363d] flex items-center gap-2">
|
||||
<div className="w-3 h-3 rounded-full bg-[#FF5F56]" />
|
||||
<div className="w-3 h-3 rounded-full bg-[#FFBD2E]" />
|
||||
<div className="w-3 h-3 rounded-full bg-[#27C93F]" />
|
||||
<span className="ml-3 text-[#8b949e] text-[11px]">OmniRoute — Application Console</span>
|
||||
</div>
|
||||
|
||||
{/* Log entries */}
|
||||
<div className="p-3 space-y-px">
|
||||
{filteredLogs.length === 0 && !loading ? (
|
||||
<div className="text-[#8b949e] text-center py-12">
|
||||
<span className="material-symbols-outlined text-[40px] block mb-2 opacity-30">
|
||||
terminal
|
||||
</span>
|
||||
<p>No log entries found</p>
|
||||
<p className="text-[10px] mt-1 opacity-60">
|
||||
Ensure LOG_TO_FILE=true is set in your .env file
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
filteredLogs.map((entry, idx) => {
|
||||
const level = (entry.level || "info").toLowerCase();
|
||||
const colorClass = LEVEL_COLORS[level] || LEVEL_COLORS.info;
|
||||
const bgClass = LEVEL_BG[level] || "";
|
||||
const comp = getComponent(entry);
|
||||
const msg = getText(entry);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={idx}
|
||||
className={`group flex items-start gap-2 px-2 py-1 rounded hover:bg-white/5 transition-colors ${
|
||||
level === "error" || level === "fatal" ? "bg-red-500/5" : ""
|
||||
}`}
|
||||
>
|
||||
{/* Timestamp */}
|
||||
<span className="text-[#484f58] whitespace-nowrap shrink-0 select-none">
|
||||
{formatTime(entry.timestamp)}
|
||||
</span>
|
||||
|
||||
{/* Level badge */}
|
||||
<span
|
||||
className={`inline-block px-1.5 py-0 rounded text-[10px] font-semibold uppercase border shrink-0 ${colorClass} ${bgClass}`}
|
||||
>
|
||||
{level.padEnd(5)}
|
||||
</span>
|
||||
|
||||
{/* Component */}
|
||||
{comp && <span className="text-purple-400/80 shrink-0">[{comp}]</span>}
|
||||
|
||||
{/* Message */}
|
||||
<span className="text-[#c9d1d9] flex-1 break-all">
|
||||
{msg}
|
||||
{/* Extra meta */}
|
||||
{entry.correlationId && (
|
||||
<span className="text-[#484f58] ml-2">
|
||||
cid:{entry.correlationId.slice(0, 8)}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
|
||||
{/* Copy button */}
|
||||
<button
|
||||
onClick={() => handleCopy(entry, idx)}
|
||||
title="Copy log entry"
|
||||
className="opacity-0 group-hover:opacity-100 transition-opacity shrink-0 text-[#8b949e] hover:text-white"
|
||||
>
|
||||
<span className="material-symbols-outlined text-[14px]">
|
||||
{copiedIdx === idx ? "check" : "content_copy"}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
)}
|
||||
|
||||
{loading && filteredLogs.length === 0 && (
|
||||
<div className="text-[#8b949e] text-center py-12">
|
||||
<span className="material-symbols-outlined text-[24px] animate-spin block mb-2">
|
||||
progress_activity
|
||||
</span>
|
||||
Loading logs...
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -16,7 +16,7 @@ const navItems = [
|
||||
{ href: "/dashboard/endpoint", label: "Endpoint", icon: "api" },
|
||||
{ href: "/dashboard/providers", label: "Providers", icon: "dns" },
|
||||
{ href: "/dashboard/combos", label: "Combos", icon: "layers" },
|
||||
{ href: "/dashboard/usage", label: "Request Logs", icon: "receipt_long" },
|
||||
{ href: "/dashboard/logs", label: "Logs", icon: "description" },
|
||||
{ href: "/dashboard/costs", label: "Costs", icon: "account_balance_wallet" },
|
||||
{ href: "/dashboard/analytics", label: "Analytics", icon: "analytics" },
|
||||
{ href: "/dashboard/limits", label: "Limits & Quotas", icon: "tune" },
|
||||
|
||||
@@ -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