mirror of
https://github.com/Dictionarry-Hub/profilarr.git
synced 2026-01-22 10:51:02 +01:00
refactor(logger): make Logger independently testable
- Add LoggerConfig interface for dependency injection - Logger class now accepts optional config in constructor - Default singleton still uses system config/database (no breaking changes) - Enables standalone usage for testing without full system init - Add auto-creation of logs directory if missing
This commit is contained in:
@@ -1,134 +1,215 @@
|
||||
/**
|
||||
* Logger singleton with console and file output
|
||||
* Logger with console and file output
|
||||
* Supports configurable settings and daily rotation
|
||||
* Can run independently with provided config or use system defaults
|
||||
*/
|
||||
|
||||
import { config } from '$config';
|
||||
import { colors } from './colors.ts';
|
||||
import { logSettings } from './settings.ts';
|
||||
import type { LogEntry, LogOptions } from './types.ts';
|
||||
import { config } from "$config";
|
||||
import { colors } from "./colors.ts";
|
||||
import { logSettings } from "./settings.ts";
|
||||
import type { LogEntry, LoggerConfig, LogLevel, LogOptions } from "./types.ts";
|
||||
|
||||
class Logger {
|
||||
private formatTimestamp(): string {
|
||||
const timestamp = new Date().toISOString();
|
||||
return `${colors.grey}${timestamp}${colors.reset}`;
|
||||
}
|
||||
private config: Required<LoggerConfig>;
|
||||
|
||||
private formatLevel(level: string, color: string): string {
|
||||
return `${color}${level.padEnd(5)}${colors.reset}`;
|
||||
}
|
||||
constructor(config?: LoggerConfig) {
|
||||
// Use provided config or sensible defaults
|
||||
this.config = {
|
||||
logsDir: config?.logsDir ?? "/tmp/logs",
|
||||
enabled: config?.enabled ?? true,
|
||||
fileLogging: config?.fileLogging ?? true,
|
||||
consoleLogging: config?.consoleLogging ?? true,
|
||||
minLevel: config?.minLevel ?? "INFO",
|
||||
};
|
||||
}
|
||||
|
||||
private formatSource(source?: string): string {
|
||||
if (!source) return '';
|
||||
return `${colors.grey}[${source}]${colors.reset}`;
|
||||
}
|
||||
private formatTimestamp(): string {
|
||||
const timestamp = new Date().toISOString();
|
||||
return `${colors.grey}${timestamp}${colors.reset}`;
|
||||
}
|
||||
|
||||
private formatMeta(meta?: unknown): string {
|
||||
if (!meta) return '';
|
||||
return `${colors.grey}${JSON.stringify(meta)}${colors.reset}`;
|
||||
}
|
||||
private formatLevel(level: string, color: string): string {
|
||||
return `${color}${level.padEnd(5)}${colors.reset}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get log file path with daily rotation (YYYY-MM-DD.log)
|
||||
*/
|
||||
private getLogFilePath(): string {
|
||||
const date = new Date().toISOString().split('T')[0]; // YYYY-MM-DD
|
||||
return `${config.paths.logs}/${date}.log`;
|
||||
}
|
||||
private formatSource(source?: string): string {
|
||||
if (!source) return "";
|
||||
return `${colors.grey}[${source}]${colors.reset}`;
|
||||
}
|
||||
|
||||
private async log(
|
||||
level: 'DEBUG' | 'INFO' | 'WARN' | 'ERROR',
|
||||
color: string,
|
||||
message: string,
|
||||
options?: LogOptions
|
||||
): Promise<void> {
|
||||
// Check if this log level should be logged
|
||||
if (!logSettings.shouldLog(level)) {
|
||||
return;
|
||||
}
|
||||
private formatMeta(meta?: unknown): string {
|
||||
if (!meta) return "";
|
||||
return `${colors.grey}${JSON.stringify(meta)}${colors.reset}`;
|
||||
}
|
||||
|
||||
const timestamp = new Date().toISOString();
|
||||
/**
|
||||
* Get log file path with daily rotation (YYYY-MM-DD.log)
|
||||
*/
|
||||
private getLogFilePath(): string {
|
||||
const date = new Date().toISOString().split("T")[0]; // YYYY-MM-DD
|
||||
return `${this.config.logsDir}/${date}.log`;
|
||||
}
|
||||
|
||||
// Console output (colored)
|
||||
if (logSettings.isConsoleLoggingEnabled()) {
|
||||
const consoleParts = [
|
||||
this.formatTimestamp(),
|
||||
this.formatLevel(level, color),
|
||||
message,
|
||||
options?.source ? this.formatSource(options.source) : '',
|
||||
options?.meta ? this.formatMeta(options.meta) : ''
|
||||
].filter(Boolean);
|
||||
/**
|
||||
* Check if logging is enabled
|
||||
*/
|
||||
private isEnabled(): boolean {
|
||||
return this.config.enabled;
|
||||
}
|
||||
|
||||
console.log(consoleParts.join(' | '));
|
||||
}
|
||||
/**
|
||||
* Check if file logging is enabled
|
||||
*/
|
||||
private isFileLoggingEnabled(): boolean {
|
||||
return this.config.fileLogging;
|
||||
}
|
||||
|
||||
// File output (JSON)
|
||||
if (logSettings.isFileLoggingEnabled()) {
|
||||
const logEntry: LogEntry = {
|
||||
timestamp,
|
||||
level,
|
||||
message,
|
||||
...(options?.source ? { source: options.source } : {}),
|
||||
...(options?.meta ? { meta: options.meta } : {})
|
||||
};
|
||||
/**
|
||||
* Check if console logging is enabled
|
||||
*/
|
||||
private isConsoleLoggingEnabled(): boolean {
|
||||
return this.config.consoleLogging;
|
||||
}
|
||||
|
||||
try {
|
||||
const filePath = this.getLogFilePath();
|
||||
/**
|
||||
* Check if a log level should be logged based on minimum level
|
||||
*/
|
||||
private shouldLog(level: LogLevel): boolean {
|
||||
if (!this.isEnabled()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Write to log file
|
||||
await Deno.writeTextFile(filePath, JSON.stringify(logEntry) + '\n', {
|
||||
append: true
|
||||
});
|
||||
} catch (error) {
|
||||
// If file write fails, at least we have console output
|
||||
console.error('Failed to write to log file:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
const levels: LogLevel[] = ["DEBUG", "INFO", "WARN", "ERROR"];
|
||||
const minIndex = levels.indexOf(this.config.minLevel);
|
||||
const levelIndex = levels.indexOf(level);
|
||||
|
||||
async debug(message: string, options?: LogOptions): Promise<void> {
|
||||
await this.log('DEBUG', colors.cyan, message, options);
|
||||
}
|
||||
return levelIndex >= minIndex;
|
||||
}
|
||||
|
||||
async info(message: string, options?: LogOptions): Promise<void> {
|
||||
await this.log('INFO', colors.green, message, options);
|
||||
}
|
||||
private async log(
|
||||
level: LogLevel,
|
||||
color: string,
|
||||
message: string,
|
||||
options?: LogOptions,
|
||||
): Promise<void> {
|
||||
// Check if this log level should be logged
|
||||
if (!this.shouldLog(level)) {
|
||||
return;
|
||||
}
|
||||
|
||||
async warn(message: string, options?: LogOptions): Promise<void> {
|
||||
await this.log('WARN', colors.yellow, message, options);
|
||||
}
|
||||
const timestamp = new Date().toISOString();
|
||||
|
||||
async error(message: string, options?: LogOptions): Promise<void> {
|
||||
await this.log('ERROR', colors.red, message, options);
|
||||
}
|
||||
// Console output (colored)
|
||||
if (this.isConsoleLoggingEnabled()) {
|
||||
const consoleParts = [
|
||||
this.formatTimestamp(),
|
||||
this.formatLevel(level, color),
|
||||
message,
|
||||
options?.source ? this.formatSource(options.source) : "",
|
||||
options?.meta ? this.formatMeta(options.meta) : "",
|
||||
].filter(Boolean);
|
||||
|
||||
async errorWithTrace(message: string, error?: Error, options?: LogOptions): Promise<void> {
|
||||
await this.log('ERROR', colors.red, message, options);
|
||||
console.log(consoleParts.join(" | "));
|
||||
}
|
||||
|
||||
// Print stack trace to console
|
||||
if (error?.stack && logSettings.isConsoleLoggingEnabled()) {
|
||||
console.log(`${colors.grey}${error.stack}${colors.reset}`);
|
||||
}
|
||||
// File output (JSON)
|
||||
if (this.isFileLoggingEnabled()) {
|
||||
const logEntry: LogEntry = {
|
||||
timestamp,
|
||||
level,
|
||||
message,
|
||||
...(options?.source ? { source: options.source } : {}),
|
||||
...(options?.meta ? { meta: options.meta } : {}),
|
||||
};
|
||||
|
||||
// Write stack trace to file
|
||||
if (error?.stack && logSettings.isFileLoggingEnabled()) {
|
||||
const traceEntry: LogEntry = {
|
||||
timestamp: new Date().toISOString(),
|
||||
level: 'ERROR',
|
||||
message: 'Stack trace',
|
||||
meta: { stack: error.stack }
|
||||
};
|
||||
try {
|
||||
const filePath = this.getLogFilePath();
|
||||
|
||||
try {
|
||||
const filePath = this.getLogFilePath();
|
||||
await Deno.writeTextFile(filePath, JSON.stringify(traceEntry) + '\n', {
|
||||
append: true
|
||||
});
|
||||
} catch (writeError) {
|
||||
console.error('Failed to write stack trace to log file:', writeError);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Ensure logs directory exists
|
||||
try {
|
||||
await Deno.mkdir(this.config.logsDir, { recursive: true });
|
||||
} catch {
|
||||
// Directory might already exist
|
||||
}
|
||||
|
||||
// Write to log file
|
||||
await Deno.writeTextFile(filePath, JSON.stringify(logEntry) + "\n", {
|
||||
append: true,
|
||||
});
|
||||
} catch (error) {
|
||||
// If file write fails, at least we have console output
|
||||
console.error("Failed to write to log file:", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async debug(message: string, options?: LogOptions): Promise<void> {
|
||||
await this.log("DEBUG", colors.cyan, message, options);
|
||||
}
|
||||
|
||||
async info(message: string, options?: LogOptions): Promise<void> {
|
||||
await this.log("INFO", colors.green, message, options);
|
||||
}
|
||||
|
||||
async warn(message: string, options?: LogOptions): Promise<void> {
|
||||
await this.log("WARN", colors.yellow, message, options);
|
||||
}
|
||||
|
||||
async error(message: string, options?: LogOptions): Promise<void> {
|
||||
await this.log("ERROR", colors.red, message, options);
|
||||
}
|
||||
|
||||
async errorWithTrace(
|
||||
message: string,
|
||||
error?: Error,
|
||||
options?: LogOptions,
|
||||
): Promise<void> {
|
||||
await this.log("ERROR", colors.red, message, options);
|
||||
|
||||
// Print stack trace to console
|
||||
if (error?.stack && this.isConsoleLoggingEnabled()) {
|
||||
console.log(`${colors.grey}${error.stack}${colors.reset}`);
|
||||
}
|
||||
|
||||
// Write stack trace to file
|
||||
if (error?.stack && this.isFileLoggingEnabled()) {
|
||||
const traceEntry: LogEntry = {
|
||||
timestamp: new Date().toISOString(),
|
||||
level: "ERROR",
|
||||
message: "Stack trace",
|
||||
meta: { stack: error.stack },
|
||||
};
|
||||
|
||||
try {
|
||||
const filePath = this.getLogFilePath();
|
||||
await Deno.writeTextFile(filePath, JSON.stringify(traceEntry) + "\n", {
|
||||
append: true,
|
||||
});
|
||||
} catch (writeError) {
|
||||
console.error("Failed to write stack trace to log file:", writeError);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const logger = new Logger();
|
||||
// Export Logger class for creating custom instances (for testing)
|
||||
export { Logger };
|
||||
|
||||
/**
|
||||
* Default logger singleton for production use
|
||||
* Uses system config and database settings
|
||||
*
|
||||
* For testing, create a standalone Logger instance:
|
||||
* @example
|
||||
* import { Logger } from './logger.ts';
|
||||
* const testLogger = new Logger({ logsDir: '/tmp/test-logs', minLevel: 'DEBUG' });
|
||||
* await testLogger.info('test message');
|
||||
*/
|
||||
const settings = logSettings.get();
|
||||
export const logger = new Logger({
|
||||
logsDir: config.paths.logs,
|
||||
enabled: settings.enabled === 1,
|
||||
fileLogging: settings.file_logging === 1,
|
||||
consoleLogging: settings.console_logging === 1,
|
||||
minLevel: settings.min_level,
|
||||
});
|
||||
|
||||
@@ -4,78 +4,80 @@
|
||||
* Supports reading from multiple files (for daily rotation)
|
||||
*/
|
||||
|
||||
import { config } from '$config';
|
||||
import type { LogEntry } from './types.ts';
|
||||
import { config } from "$config";
|
||||
import type { LogEntry } from "./types.ts";
|
||||
|
||||
/**
|
||||
* Get all log files sorted by modification time (newest first)
|
||||
*/
|
||||
async function getLogFiles(): Promise<string[]> {
|
||||
const logsDir = config.paths.logs;
|
||||
const logFiles: Array<{ path: string; mtime: Date }> = [];
|
||||
const logsDir = config.paths.logs;
|
||||
const logFiles: Array<{ path: string; mtime: Date }> = [];
|
||||
|
||||
try {
|
||||
for await (const entry of Deno.readDir(logsDir)) {
|
||||
if (entry.isFile && entry.name.endsWith('.log')) {
|
||||
const filePath = `${logsDir}/${entry.name}`;
|
||||
try {
|
||||
const stat = await Deno.stat(filePath);
|
||||
if (stat.mtime) {
|
||||
logFiles.push({ path: filePath, mtime: stat.mtime });
|
||||
}
|
||||
} catch {
|
||||
// Skip files we can't stat
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// If directory doesn't exist, return empty array
|
||||
return [];
|
||||
}
|
||||
try {
|
||||
for await (const entry of Deno.readDir(logsDir)) {
|
||||
if (entry.isFile && entry.name.endsWith(".log")) {
|
||||
const filePath = `${logsDir}/${entry.name}`;
|
||||
try {
|
||||
const stat = await Deno.stat(filePath);
|
||||
if (stat.mtime) {
|
||||
logFiles.push({ path: filePath, mtime: stat.mtime });
|
||||
}
|
||||
} catch {
|
||||
// Skip files we can't stat
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// If directory doesn't exist, return empty array
|
||||
return [];
|
||||
}
|
||||
|
||||
// Sort by modification time (newest first)
|
||||
logFiles.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
|
||||
// Sort by modification time (newest first)
|
||||
logFiles.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
|
||||
|
||||
return logFiles.map((f) => f.path);
|
||||
return logFiles.map((f) => f.path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all log files with metadata
|
||||
*/
|
||||
export async function getLogFilesList(): Promise<
|
||||
Array<{ filename: string; path: string; size: number; modified: Date }>
|
||||
Array<{ filename: string; path: string; size: number; modified: Date }>
|
||||
> {
|
||||
const logsDir = config.paths.logs;
|
||||
const logFiles: Array<{ filename: string; path: string; size: number; modified: Date }> = [];
|
||||
const logsDir = config.paths.logs;
|
||||
const logFiles: Array<
|
||||
{ filename: string; path: string; size: number; modified: Date }
|
||||
> = [];
|
||||
|
||||
try {
|
||||
for await (const entry of Deno.readDir(logsDir)) {
|
||||
if (entry.isFile && entry.name.endsWith('.log')) {
|
||||
const filePath = `${logsDir}/${entry.name}`;
|
||||
try {
|
||||
const stat = await Deno.stat(filePath);
|
||||
if (stat.mtime) {
|
||||
logFiles.push({
|
||||
filename: entry.name,
|
||||
path: filePath,
|
||||
size: stat.size,
|
||||
modified: stat.mtime
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
// Skip files we can't stat
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// If directory doesn't exist, return empty array
|
||||
return [];
|
||||
}
|
||||
try {
|
||||
for await (const entry of Deno.readDir(logsDir)) {
|
||||
if (entry.isFile && entry.name.endsWith(".log")) {
|
||||
const filePath = `${logsDir}/${entry.name}`;
|
||||
try {
|
||||
const stat = await Deno.stat(filePath);
|
||||
if (stat.mtime) {
|
||||
logFiles.push({
|
||||
filename: entry.name,
|
||||
path: filePath,
|
||||
size: stat.size,
|
||||
modified: stat.mtime,
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
// Skip files we can't stat
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// If directory doesn't exist, return empty array
|
||||
return [];
|
||||
}
|
||||
|
||||
// Sort by modification time (newest first)
|
||||
logFiles.sort((a, b) => b.modified.getTime() - a.modified.getTime());
|
||||
// Sort by modification time (newest first)
|
||||
logFiles.sort((a, b) => b.modified.getTime() - a.modified.getTime());
|
||||
|
||||
return logFiles;
|
||||
return logFiles;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -85,36 +87,38 @@ export async function getLogFilesList(): Promise<
|
||||
* @returns Array of log entries sorted by timestamp (oldest first)
|
||||
*/
|
||||
export async function readLogsFromFile(
|
||||
filename: string,
|
||||
count: number = 1000
|
||||
filename: string,
|
||||
count: number = 1000,
|
||||
): Promise<LogEntry[]> {
|
||||
try {
|
||||
const logsDir = config.paths.logs;
|
||||
const filePath = `${logsDir}/${filename}`;
|
||||
const logs: LogEntry[] = [];
|
||||
try {
|
||||
const logsDir = config.paths.logs;
|
||||
const filePath = `${logsDir}/${filename}`;
|
||||
const logs: LogEntry[] = [];
|
||||
|
||||
const content = await Deno.readTextFile(filePath);
|
||||
const lines = content.split('\n').filter((line) => line.trim());
|
||||
const content = await Deno.readTextFile(filePath);
|
||||
const lines = content.split("\n").filter((line) => line.trim());
|
||||
|
||||
// Parse each line as JSON
|
||||
for (const line of lines) {
|
||||
try {
|
||||
const entry = JSON.parse(line) as LogEntry;
|
||||
logs.push(entry);
|
||||
} catch {
|
||||
// Skip invalid JSON lines
|
||||
}
|
||||
}
|
||||
// Parse each line as JSON
|
||||
for (const line of lines) {
|
||||
try {
|
||||
const entry = JSON.parse(line) as LogEntry;
|
||||
logs.push(entry);
|
||||
} catch {
|
||||
// Skip invalid JSON lines
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by timestamp (oldest first)
|
||||
logs.sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
|
||||
// Sort by timestamp (oldest first)
|
||||
logs.sort((a, b) =>
|
||||
new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime()
|
||||
);
|
||||
|
||||
// Return last N entries
|
||||
return logs.slice(-count);
|
||||
} catch (error) {
|
||||
// If anything fails, return empty array
|
||||
return [];
|
||||
}
|
||||
// Return last N entries
|
||||
return logs.slice(-count);
|
||||
} catch (_error) {
|
||||
// If anything fails, return empty array
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -123,44 +127,46 @@ export async function readLogsFromFile(
|
||||
* @returns Array of log entries sorted by timestamp (oldest first)
|
||||
*/
|
||||
export async function readLastLogs(count: number = 1000): Promise<LogEntry[]> {
|
||||
try {
|
||||
const logFiles = await getLogFiles();
|
||||
const logs: LogEntry[] = [];
|
||||
try {
|
||||
const logFiles = await getLogFiles();
|
||||
const logs: LogEntry[] = [];
|
||||
|
||||
// Read from newest files first until we have enough logs
|
||||
for (const filePath of logFiles) {
|
||||
try {
|
||||
const content = await Deno.readTextFile(filePath);
|
||||
const lines = content.split('\n').filter((line) => line.trim());
|
||||
// Read from newest files first until we have enough logs
|
||||
for (const filePath of logFiles) {
|
||||
try {
|
||||
const content = await Deno.readTextFile(filePath);
|
||||
const lines = content.split("\n").filter((line) => line.trim());
|
||||
|
||||
// Parse each line as JSON
|
||||
for (const line of lines) {
|
||||
try {
|
||||
const entry = JSON.parse(line) as LogEntry;
|
||||
logs.push(entry);
|
||||
} catch {
|
||||
// Skip invalid JSON lines
|
||||
}
|
||||
}
|
||||
// Parse each line as JSON
|
||||
for (const line of lines) {
|
||||
try {
|
||||
const entry = JSON.parse(line) as LogEntry;
|
||||
logs.push(entry);
|
||||
} catch {
|
||||
// Skip invalid JSON lines
|
||||
}
|
||||
}
|
||||
|
||||
// Stop if we have enough logs
|
||||
if (logs.length >= count) {
|
||||
break;
|
||||
}
|
||||
} catch {
|
||||
// Skip files we can't read
|
||||
}
|
||||
}
|
||||
// Stop if we have enough logs
|
||||
if (logs.length >= count) {
|
||||
break;
|
||||
}
|
||||
} catch {
|
||||
// Skip files we can't read
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by timestamp (oldest first)
|
||||
logs.sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
|
||||
// Sort by timestamp (oldest first)
|
||||
logs.sort((a, b) =>
|
||||
new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime()
|
||||
);
|
||||
|
||||
// Return last N entries
|
||||
return logs.slice(-count);
|
||||
} catch (error) {
|
||||
// If anything fails, return empty array
|
||||
return [];
|
||||
}
|
||||
// Return last N entries
|
||||
return logs.slice(-count);
|
||||
} catch (_error) {
|
||||
// If anything fails, return empty array
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -169,11 +175,11 @@ export async function readLastLogs(count: number = 1000): Promise<LogEntry[]> {
|
||||
* @returns Parsed log entry or null if invalid
|
||||
*/
|
||||
export function parseLogLine(line: string): LogEntry | null {
|
||||
try {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed) return null;
|
||||
return JSON.parse(trimmed) as LogEntry;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed) return null;
|
||||
return JSON.parse(trimmed) as LogEntry;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,101 +1,101 @@
|
||||
import { logSettingsQueries } from '$db/queries/logSettings.ts';
|
||||
import type { LogSettings } from '$db/queries/logSettings.ts';
|
||||
import { logSettingsQueries } from "$db/queries/logSettings.ts";
|
||||
import type { LogSettings } from "$db/queries/logSettings.ts";
|
||||
|
||||
/**
|
||||
* Log settings manager
|
||||
* Loads and caches log settings from database
|
||||
*/
|
||||
class LogSettingsManager {
|
||||
private settings: LogSettings | null = null;
|
||||
private initialized = false;
|
||||
private settings: LogSettings | null = null;
|
||||
private initialized = false;
|
||||
|
||||
/**
|
||||
* Load settings from database
|
||||
*/
|
||||
load(): void {
|
||||
try {
|
||||
this.settings = logSettingsQueries.get() ?? null;
|
||||
this.initialized = true;
|
||||
} catch (error) {
|
||||
console.error('Failed to load log settings:', error);
|
||||
// Use defaults if database not available
|
||||
this.settings = null;
|
||||
this.initialized = false;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Load settings from database
|
||||
*/
|
||||
load(): void {
|
||||
try {
|
||||
this.settings = logSettingsQueries.get() ?? null;
|
||||
this.initialized = true;
|
||||
} catch (error) {
|
||||
console.error("Failed to load log settings:", error);
|
||||
// Use defaults if database not available
|
||||
this.settings = null;
|
||||
this.initialized = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reload settings from database
|
||||
* Call this after updating settings
|
||||
*/
|
||||
reload(): void {
|
||||
this.load();
|
||||
}
|
||||
/**
|
||||
* Reload settings from database
|
||||
* Call this after updating settings
|
||||
*/
|
||||
reload(): void {
|
||||
this.load();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current settings
|
||||
* Returns defaults if not loaded
|
||||
*/
|
||||
get(): LogSettings {
|
||||
if (!this.initialized || !this.settings) {
|
||||
// Return defaults if not initialized
|
||||
return {
|
||||
id: 1,
|
||||
retention_days: 30,
|
||||
min_level: 'INFO',
|
||||
enabled: 1,
|
||||
file_logging: 1,
|
||||
console_logging: 1,
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString()
|
||||
};
|
||||
}
|
||||
return this.settings;
|
||||
}
|
||||
/**
|
||||
* Get current settings
|
||||
* Returns defaults if not loaded
|
||||
*/
|
||||
get(): LogSettings {
|
||||
if (!this.initialized || !this.settings) {
|
||||
// Return defaults if not initialized
|
||||
return {
|
||||
id: 1,
|
||||
retention_days: 30,
|
||||
min_level: "INFO",
|
||||
enabled: 1,
|
||||
file_logging: 1,
|
||||
console_logging: 1,
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
return this.settings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if logging is enabled
|
||||
*/
|
||||
isEnabled(): boolean {
|
||||
return this.get().enabled === 1;
|
||||
}
|
||||
/**
|
||||
* Check if logging is enabled
|
||||
*/
|
||||
isEnabled(): boolean {
|
||||
return this.get().enabled === 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if file logging is enabled
|
||||
*/
|
||||
isFileLoggingEnabled(): boolean {
|
||||
return this.get().file_logging === 1;
|
||||
}
|
||||
/**
|
||||
* Check if file logging is enabled
|
||||
*/
|
||||
isFileLoggingEnabled(): boolean {
|
||||
return this.get().file_logging === 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if console logging is enabled
|
||||
*/
|
||||
isConsoleLoggingEnabled(): boolean {
|
||||
return this.get().console_logging === 1;
|
||||
}
|
||||
/**
|
||||
* Check if console logging is enabled
|
||||
*/
|
||||
isConsoleLoggingEnabled(): boolean {
|
||||
return this.get().console_logging === 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get minimum log level
|
||||
*/
|
||||
getMinLevel(): 'DEBUG' | 'INFO' | 'WARN' | 'ERROR' {
|
||||
return this.get().min_level;
|
||||
}
|
||||
/**
|
||||
* Get minimum log level
|
||||
*/
|
||||
getMinLevel(): "DEBUG" | "INFO" | "WARN" | "ERROR" {
|
||||
return this.get().min_level;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a log level should be logged
|
||||
*/
|
||||
shouldLog(level: 'DEBUG' | 'INFO' | 'WARN' | 'ERROR'): boolean {
|
||||
if (!this.isEnabled()) {
|
||||
return false;
|
||||
}
|
||||
/**
|
||||
* Check if a log level should be logged
|
||||
*/
|
||||
shouldLog(level: "DEBUG" | "INFO" | "WARN" | "ERROR"): boolean {
|
||||
if (!this.isEnabled()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const minLevel = this.getMinLevel();
|
||||
const levels = ['DEBUG', 'INFO', 'WARN', 'ERROR'];
|
||||
const minIndex = levels.indexOf(minLevel);
|
||||
const levelIndex = levels.indexOf(level);
|
||||
const minLevel = this.getMinLevel();
|
||||
const levels = ["DEBUG", "INFO", "WARN", "ERROR"];
|
||||
const minIndex = levels.indexOf(minLevel);
|
||||
const levelIndex = levels.indexOf(level);
|
||||
|
||||
return levelIndex >= minIndex;
|
||||
}
|
||||
return levelIndex >= minIndex;
|
||||
}
|
||||
}
|
||||
|
||||
// Export singleton instance
|
||||
|
||||
@@ -2,17 +2,36 @@
|
||||
* Logger types and interfaces
|
||||
*/
|
||||
|
||||
export type LogLevel = "DEBUG" | "INFO" | "WARN" | "ERROR";
|
||||
|
||||
export interface LogOptions {
|
||||
/** Optional metadata to include with the log */
|
||||
meta?: unknown;
|
||||
/** Optional source/context tag (e.g., "database", "api") */
|
||||
source?: string;
|
||||
/** Optional metadata to include with the log */
|
||||
meta?: unknown;
|
||||
/** Optional source/context tag (e.g., "database", "api") */
|
||||
source?: string;
|
||||
}
|
||||
|
||||
export interface LogEntry {
|
||||
timestamp: string;
|
||||
level: string;
|
||||
message: string;
|
||||
source?: string;
|
||||
meta?: unknown;
|
||||
timestamp: string;
|
||||
level: string;
|
||||
message: string;
|
||||
source?: string;
|
||||
meta?: unknown;
|
||||
}
|
||||
|
||||
/**
|
||||
* Logger configuration
|
||||
* Allows logger to run independently without config/database dependencies
|
||||
*/
|
||||
export interface LoggerConfig {
|
||||
/** Directory where log files will be written (e.g., "/app/logs") */
|
||||
logsDir: string;
|
||||
/** Master toggle for all logging */
|
||||
enabled?: boolean;
|
||||
/** Enable file logging */
|
||||
fileLogging?: boolean;
|
||||
/** Enable console logging */
|
||||
consoleLogging?: boolean;
|
||||
/** Minimum log level to output */
|
||||
minLevel?: LogLevel;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user