This commit is contained in:
IanKulin
2025-09-25 21:26:31 +08:00
parent b95d3ea50a
commit 01cc58aa7a
14 changed files with 924 additions and 824 deletions

View File

@@ -6,7 +6,8 @@
- **Multiple log levels**: silent, error, warn, info, debug
- **Flexible output formats**: JSON or simple text
- **Caller detection**: Automatically identifies source file and line number based on log level
- **Caller detection**: Automatically identifies source file and line number
based on log level
- **Color support**: Automatic TTY detection with colored output
## Install
@@ -27,8 +28,8 @@ $ deno run --allow-env --allow-sys your-script.js
import Logger from "@iankulin/logger";
const logger = new Logger();
logger.info('Hello from logger');
logger.error('Something went wrong');
logger.info("Hello from logger");
logger.error("Something went wrong");
```
## Usage Examples
@@ -37,12 +38,12 @@ logger.error('Something went wrong');
```typescript
import Logger from "@iankulin/logger";
const logger = new Logger({ level: 'info' });
const logger = new Logger({ level: "info" });
logger.error('Critical error occurred');
logger.warn('This is a warning');
logger.info('Informational message');
logger.debug('Debug info'); // Won't be shown (level is 'info')
logger.error("Critical error occurred");
logger.warn("This is a warning");
logger.info("Informational message");
logger.debug("Debug info"); // Won't be shown (level is 'info')
```
### Log Levels
@@ -56,17 +57,17 @@ The logger supports five log levels (from least to most verbose):
- `debug` - All messages
```typescript
const logger = new Logger({ level: 'debug' });
const logger = new Logger({ level: "debug" });
// All of these will be logged
logger.error('Error message');
logger.warn('Warning message');
logger.info('Info message');
logger.debug('Debug message');
logger.error("Error message");
logger.warn("Warning message");
logger.info("Info message");
logger.debug("Debug message");
// Change level dynamically
logger.level('error');
logger.info('This will not be logged');
logger.level("error");
logger.info("This will not be logged");
// Get current level
console.log(logger.level()); // 'error'
@@ -77,8 +78,8 @@ console.log(logger.level()); // 'error'
#### JSON Format (Default)
```typescript
const logger = new Logger({ format: 'json' });
logger.info('Hello world');
const logger = new Logger({ format: "json" });
logger.info("Hello world");
```
```json
@@ -97,8 +98,8 @@ logger.info('Hello world');
#### Simple Format
```typescript
const logger = new Logger({ format: 'simple' });
logger.error('Something failed');
const logger = new Logger({ format: "simple" });
logger.error("Something failed");
```
```
@@ -107,22 +108,23 @@ logger.error('Something failed');
### Message Formatting
The logger supports `util.format()` style message formatting with placeholders like `%s`, `%d`, `%j`.
The logger supports `util.format()` style message formatting with placeholders
like `%s`, `%d`, `%j`.
```typescript
const logger = new Logger({ format: 'json' });
const logger = new Logger({ format: "json" });
// String formatting
logger.info('User %s has %d points', 'john', 100);
logger.info("User %s has %d points", "john", 100);
// Output: {"level":"info","msg":"User john has 100 points",...}
// JSON formatting
logger.info('Config: %j', { debug: true, port: 3000 });
logger.info("Config: %j", { debug: true, port: 3000 });
// Output: {"level":"info","msg":"Config: {\"debug\":true,\"port\":3000}",...}
// Simple format example
const simpleLogger = new Logger({ format: 'simple' });
simpleLogger.warn('Processing file %s (%d bytes)', 'data.txt', 1024);
const simpleLogger = new Logger({ format: "simple" });
simpleLogger.warn("Processing file %s (%d bytes)", "data.txt", 1024);
// Output: [2025-07-05 10:30] [WARN ] [app.js:15] Processing file data.txt (1024 bytes)
```
@@ -131,26 +133,28 @@ simpleLogger.warn('Processing file %s (%d bytes)', 'data.txt', 1024);
```typescript
const logger = new Logger({
colours: {
error: '\x1b[31m', // Red
warn: '\x1b[93m', // Bright yellow
info: '\x1b[36m', // Cyan
debug: '\x1b[90m', // Dark gray
error: "\x1b[31m", // Red
warn: "\x1b[93m", // Bright yellow
info: "\x1b[36m", // Cyan
debug: "\x1b[90m", // Dark gray
},
});
```
### Caller Level Control
Control when caller information (file and line number) is included in log messages. This is useful for performance optimization since caller detection can be expensive.
Control when caller information (file and line number) is included in log
messages. This is useful for performance optimization since caller detection can
be expensive.
```typescript
// Default: only include caller info for warnings and errors
const logger = new Logger({ callerLevel: 'warn' });
const logger = new Logger({ callerLevel: "warn" });
logger.error('Critical error'); // Includes caller info
logger.warn('Warning message'); // Includes caller info
logger.info('Info message'); // No caller info
logger.debug('Debug message'); // No caller info
logger.error("Critical error"); // Includes caller info
logger.warn("Warning message"); // Includes caller info
logger.info("Info message"); // No caller info
logger.debug("Debug message"); // No caller info
```
**JSON Format Output:**
@@ -175,7 +179,9 @@ logger.debug('Debug message'); // No caller info
- `'info'` - Include caller info for info, warnings, and errors
- `'debug'` - Always include caller info
**Performance Tip:** For production applications that primarily log info/debug messages, setting `callerLevel: 'error'` can significantly improve performance by avoiding expensive stack trace analysis for routine logging.
**Performance Tip:** For production applications that primarily log info/debug
messages, setting `callerLevel: 'error'` can significantly improve performance
by avoiding expensive stack trace analysis for routine logging.
## Constructor Options
@@ -194,19 +200,19 @@ import Logger from "@iankulin/logger";
// Production: JSON format with environment-based level
const prodLogger = new Logger({
level: Deno.env.get('LOG_LEVEL') || 'info',
format: 'json',
callerLevel: 'error', // Performance optimization
level: Deno.env.get("LOG_LEVEL") || "info",
format: "json",
callerLevel: "error", // Performance optimization
});
// Development: Simple format with debug level
const devLogger = new Logger({
level: 'debug',
format: 'simple',
level: "debug",
format: "simple",
});
// Testing: Silent mode
const testLogger = new Logger({ level: 'silent' });
const testLogger = new Logger({ level: "silent" });
```
## Requirements
@@ -220,7 +226,8 @@ const testLogger = new Logger({ level: 'silent' });
## Versions
- **1.0.0** - JSR release with full Deno support
- Migrated from [npm version](https://www.npmjs.com/package/@iankulin/logger) to JSR (JavaScript Registry)
- Migrated from [npm version](https://www.npmjs.com/package/@iankulin/logger)
to JSR (JavaScript Registry)
- Full TypeScript support
- Native Deno compatibility

124
demo.js
View File

@@ -1,94 +1,102 @@
import Logger from './lib/logger.ts';
const logger = new Logger({ level: 'debug' });
import Logger from "./lib/logger.ts";
const logger = new Logger({ level: "debug" });
logger.error('Unable to fetch student');
logger.info('Hello from logger');
logger.warn('This is a warning');
logger.debug('This is a debug message'); // This won't be logged if level is set to 'info'
logger.level('error');
logger.debug('This is a debug message'); // This won't be logged if level is set to 'info' or higher
logger.error("Unable to fetch student");
logger.info("Hello from logger");
logger.warn("This is a warning");
logger.debug("This is a debug message"); // This won't be logged if level is set to 'info'
logger.level("error");
logger.debug("This is a debug message"); // This won't be logged if level is set to 'info' or higher
const simple_logger = new Logger({ level: 'debug', format: 'simple' });
const simple_logger = new Logger({ level: "debug", format: "simple" });
simple_logger.error('Unable to fetch student');
simple_logger.info('Hello from logger');
simple_logger.warn('This is a warning');
simple_logger.debug('This is a debug message'); // This won't be logged if level is set to 'info'
simple_logger.level('error');
simple_logger.debug('This is a debug message'); // This won't be logged if level is set to 'info' or higher
simple_logger.error("Unable to fetch student");
simple_logger.info("Hello from logger");
simple_logger.warn("This is a warning");
simple_logger.debug("This is a debug message"); // This won't be logged if level is set to 'info'
simple_logger.level("error");
simple_logger.debug("This is a debug message"); // This won't be logged if level is set to 'info' or higher
const longLogger = new Logger({ time: 'long', format: 'simple' });
const shortLogger = new Logger({ time: 'short', format: 'simple' });
const longLogger = new Logger({ time: "long", format: "simple" });
const shortLogger = new Logger({ time: "short", format: "simple" });
longLogger.info('This uses long time format');
shortLogger.info('This uses short time format');
longLogger.info("This uses long time format");
shortLogger.info("This uses short time format");
// Demonstrate callerLevel functionality
console.log('\n=== Caller Level Demo ===');
console.log("\n=== Caller Level Demo ===");
// Default callerLevel is 'warn' - only errors and warnings include caller info
const defaultCallerLogger = new Logger({ format: 'simple' });
const defaultCallerLogger = new Logger({ format: "simple" });
console.log(
'Default callerLevel (warn) - only errors and warnings show caller info:'
"Default callerLevel (warn) - only errors and warnings show caller info:",
);
defaultCallerLogger.error('Error with caller info');
defaultCallerLogger.warn('Warning with caller info');
defaultCallerLogger.info('Info without caller info');
defaultCallerLogger.debug('Debug without caller info');
defaultCallerLogger.error("Error with caller info");
defaultCallerLogger.warn("Warning with caller info");
defaultCallerLogger.info("Info without caller info");
defaultCallerLogger.debug("Debug without caller info");
// Set callerLevel to 'error' - only errors include caller info
const errorOnlyLogger = new Logger({ format: 'simple', callerLevel: 'error' });
console.log('\nCallerLevel set to error - only errors show caller info:');
errorOnlyLogger.error('Error with caller info');
errorOnlyLogger.warn('Warning without caller info');
errorOnlyLogger.info('Info without caller info');
const errorOnlyLogger = new Logger({ format: "simple", callerLevel: "error" });
console.log("\nCallerLevel set to error - only errors show caller info:");
errorOnlyLogger.error("Error with caller info");
errorOnlyLogger.warn("Warning without caller info");
errorOnlyLogger.info("Info without caller info");
// Set callerLevel to 'debug' - all levels include caller info
const allLevelsLogger = new Logger({ format: 'simple', callerLevel: 'debug' });
console.log('\nCallerLevel set to debug - all levels show caller info:');
allLevelsLogger.error('Error with caller info');
allLevelsLogger.warn('Warning with caller info');
allLevelsLogger.info('Info with caller info');
allLevelsLogger.debug('Debug with caller info');
const allLevelsLogger = new Logger({ format: "simple", callerLevel: "debug" });
console.log("\nCallerLevel set to debug - all levels show caller info:");
allLevelsLogger.error("Error with caller info");
allLevelsLogger.warn("Warning with caller info");
allLevelsLogger.info("Info with caller info");
allLevelsLogger.debug("Debug with caller info");
// Set callerLevel to 'silent' - no levels include caller info
const noneLogger = new Logger({ format: 'simple', callerLevel: 'silent' });
console.log('\nCallerLevel set to silent - no levels show caller info:');
noneLogger.error('Error without caller info');
noneLogger.warn('Warning without caller info');
noneLogger.info('Info without caller info');
const noneLogger = new Logger({ format: "simple", callerLevel: "silent" });
console.log("\nCallerLevel set to silent - no levels show caller info:");
noneLogger.error("Error without caller info");
noneLogger.warn("Warning without caller info");
noneLogger.info("Info without caller info");
// Demonstrate format string functionality (util.format style)
console.log('\n=== Format String Demo ===');
console.log("\n=== Format String Demo ===");
const formatLogger = new Logger({ format: 'simple', level: 'debug' });
console.log('Format strings with various specifiers:');
const formatLogger = new Logger({ format: "simple", level: "debug" });
console.log("Format strings with various specifiers:");
// String formatting (%s)
formatLogger.info('User %s logged in successfully', 'john_doe');
formatLogger.info("User %s logged in successfully", "john_doe");
// Number formatting (%d, %i, %f)
formatLogger.warn('Database has %d connections, CPU usage: %f%%', 25, 84.3);
formatLogger.debug('Processing item %i of %d', 42, 100);
formatLogger.warn("Database has %d connections, CPU usage: %f%%", 25, 84.3);
formatLogger.debug("Processing item %i of %d", 42, 100);
// JSON formatting (%j)
const user = { name: 'Alice', role: 'admin', active: true };
const user = { name: "Alice", role: "admin", active: true };
const config = { timeout: 5000, retries: 3 };
formatLogger.info('User data: %j, Config: %j', user, config);
formatLogger.info("User data: %j, Config: %j", user, config);
// Mixed formatting
formatLogger.error('API call failed for user %s (ID: %d) with config %j', 'bob', 1234, config);
formatLogger.error(
"API call failed for user %s (ID: %d) with config %j",
"bob",
1234,
config,
);
// Multiple arguments without format specifiers
formatLogger.warn('System alert:', 'High memory usage detected', { usage: '89%', threshold: '80%' });
formatLogger.warn("System alert:", "High memory usage detected", {
usage: "89%",
threshold: "80%",
});
// Literal percentage with %%
formatLogger.info('Upload progress: 50%% complete');
formatLogger.info("Upload progress: 50%% complete");
// Edge cases
formatLogger.debug('Values: %s, %s, %d', null, undefined, null);
formatLogger.debug("Values: %s, %s, %d", null, undefined, null);
console.log('\nJSON format with same messages:');
const jsonFormatLogger = new Logger({ format: 'json' });
jsonFormatLogger.info('User %s logged in with %d failed attempts', 'alice', 2);
jsonFormatLogger.warn('Config loaded: %j', { env: 'production', debug: false });
console.log("\nJSON format with same messages:");
const jsonFormatLogger = new Logger({ format: "json" });
jsonFormatLogger.info("User %s logged in with %d failed attempts", "alice", 2);
jsonFormatLogger.warn("Config loaded: %j", { env: "production", debug: false });

View File

@@ -1,29 +1,29 @@
// Native implementation of util.format functionality
function format(f: unknown, ...args: unknown[]): string {
if (typeof f !== 'string') {
if (typeof f !== "string") {
// If first argument is not a string, just join all args with spaces
return [f, ...args].map(arg => {
if (arg === null) return 'null';
if (arg === undefined) return 'undefined';
if (typeof arg === 'object') {
return [f, ...args].map((arg) => {
if (arg === null) return "null";
if (arg === undefined) return "undefined";
if (typeof arg === "object") {
try {
return JSON.stringify(arg, null, 0);
} catch {
// Handle circular references and other JSON errors
return '[object Object]';
return "[object Object]";
}
}
return String(arg);
}).join(' ');
}).join(" ");
}
let i = 0;
// First, handle %% replacement - if there are no args, %% stays as %%
// If there are args, %% becomes %
const handlePercentPercent = args.length === 0 ? '%%' : '%';
const handlePercentPercent = args.length === 0 ? "%%" : "%";
const str = f.replace(/%[sdifj%]/g, (match: string) => {
if (match === '%%') {
if (match === "%%") {
return handlePercentPercent;
}
@@ -32,48 +32,50 @@ function format(f: unknown, ...args: unknown[]): string {
const arg = args[i++];
switch (match) {
case '%s':
if (arg === null) return 'null';
if (arg === undefined) return 'undefined';
if (typeof arg === 'object') {
case "%s":
if (arg === null) return "null";
if (arg === undefined) return "undefined";
if (typeof arg === "object") {
try {
// For objects without %j, use a simplified string representation
if (Array.isArray(arg)) {
return `[ ${arg.join(', ')} ]`;
return `[ ${arg.join(", ")} ]`;
}
// For plain objects, show key-value pairs
const entries = Object.entries(arg).map(([k, v]) => `${k}: ${typeof v === 'string' ? `'${v}'` : v}`);
return `{ ${entries.join(', ')} }`;
const entries = Object.entries(arg).map(([k, v]) =>
`${k}: ${typeof v === "string" ? `'${v}'` : v}`
);
return `{ ${entries.join(", ")} }`;
} catch {
return '[object Object]';
return "[object Object]";
}
}
try {
return String(arg);
} catch {
return '[object Object]';
return "[object Object]";
}
case '%d':
if (arg === null) return '0';
if (arg === undefined) return 'NaN';
case "%d":
if (arg === null) return "0";
if (arg === undefined) return "NaN";
return String(Number(arg));
case '%i':
if (arg === null) return '0';
if (arg === undefined) return 'NaN';
case "%i":
if (arg === null) return "0";
if (arg === undefined) return "NaN";
return String(parseInt(String(Number(arg)), 10));
case '%f':
if (arg === null) return '0';
if (arg === undefined) return 'NaN';
case "%f":
if (arg === null) return "0";
if (arg === undefined) return "NaN";
return String(parseFloat(String(Number(arg))));
case '%j':
case "%j":
try {
return JSON.stringify(arg);
} catch {
return '[Circular]';
return "[Circular]";
}
default:
@@ -84,28 +86,30 @@ function format(f: unknown, ...args: unknown[]): string {
// Append any remaining arguments
const remainingArgs = args.slice(i);
if (remainingArgs.length > 0) {
return str + ' ' + remainingArgs.map(arg => {
if (arg === null) return 'null';
if (arg === undefined) return 'undefined';
if (typeof arg === 'object') {
return str + " " + remainingArgs.map((arg) => {
if (arg === null) return "null";
if (arg === undefined) return "undefined";
if (typeof arg === "object") {
try {
if (Array.isArray(arg)) {
return `[ ${arg.join(', ')} ]`;
return `[ ${arg.join(", ")} ]`;
}
const entries = Object.entries(arg).map(([k, v]) => `${k}: ${typeof v === 'string' ? `'${v}'` : v}`);
return `{ ${entries.join(', ')} }`;
const entries = Object.entries(arg).map(([k, v]) =>
`${k}: ${typeof v === "string" ? `'${v}'` : v}`
);
return `{ ${entries.join(", ")} }`;
} catch {
return '[object Object]';
return "[object Object]";
}
}
return String(arg);
}).join(' ');
}).join(" ");
}
return str;
}
export type LogLevel = 'silent' | 'error' | 'warn' | 'info' | 'debug';
export type LogLevel = "silent" | "error" | "warn" | "info" | "debug";
export interface LogLevels {
[level: string]: number;
@@ -142,8 +146,8 @@ export type Formatter = (logEntry: LogEntry) => string;
export interface LoggerOptions {
level?: LogLevel;
levels?: Partial<LogLevels>;
format?: 'json' | 'simple';
time?: 'long' | 'short';
format?: "json" | "simple";
time?: "long" | "short";
callerLevel?: LogLevel;
colours?: Partial<Colours>;
}
@@ -152,8 +156,8 @@ class Logger {
options: {
level: LogLevel;
levels: LogLevels;
format: 'json' | 'simple';
time: 'long' | 'short';
format: "json" | "simple";
time: "long" | "short";
callerLevel: LogLevel;
colours: Colours;
};
@@ -174,19 +178,19 @@ class Logger {
};
const defaultColours: Colours = {
error: '\x1b[91m',
warn: '\x1b[33m',
info: '\x1b[94m',
debug: '\x1b[37m',
reset: '\x1b[0m',
error: "\x1b[91m",
warn: "\x1b[33m",
info: "\x1b[94m",
debug: "\x1b[37m",
reset: "\x1b[0m",
};
this.options = {
level: options.level || 'info',
level: options.level || "info",
levels: Object.assign({}, defaultLevels, options.levels),
format: options.format || 'json',
time: options.time || 'short',
callerLevel: options.callerLevel || 'warn',
format: options.format || "json",
time: options.time || "short",
callerLevel: options.callerLevel || "warn",
colours: Object.assign({}, defaultColours, options.colours),
};
@@ -207,71 +211,83 @@ class Logger {
validateOptions(options: LoggerOptions): void {
// Validate level if provided
if (options.level !== undefined) {
const validLevels: LogLevel[] = ['silent', 'error', 'warn', 'info', 'debug'];
const validLevels: LogLevel[] = [
"silent",
"error",
"warn",
"info",
"debug",
];
if (!validLevels.includes(options.level)) {
throw new Error(
`Invalid log level: ${
options.level
}. Valid levels are: ${validLevels.join(', ')}`
`Invalid log level: ${options.level}. Valid levels are: ${
validLevels.join(", ")
}`,
);
}
}
// Validate format if provided
if (options.format !== undefined) {
const validFormats = ['json', 'simple'];
const validFormats = ["json", "simple"];
if (!validFormats.includes(options.format)) {
throw new Error(
`Invalid format: ${
options.format
}. Valid formats are: ${validFormats.join(', ')}`
`Invalid format: ${options.format}. Valid formats are: ${
validFormats.join(", ")
}`,
);
}
}
// Validate time if provided
if (options.time !== undefined) {
const validTimes = ['long', 'short'];
const validTimes = ["long", "short"];
if (!validTimes.includes(options.time)) {
throw new Error(
`Invalid time: ${
options.time
}. Valid times are: ${validTimes.join(', ')}`
`Invalid time: ${options.time}. Valid times are: ${
validTimes.join(", ")
}`,
);
}
}
// Validate callerLevel if provided
if (options.callerLevel !== undefined) {
const validLevels: LogLevel[] = ['silent', 'error', 'warn', 'info', 'debug'];
const validLevels: LogLevel[] = [
"silent",
"error",
"warn",
"info",
"debug",
];
if (!validLevels.includes(options.callerLevel)) {
throw new Error(
`Invalid callerLevel: ${
options.callerLevel
}. Valid levels are: ${validLevels.join(', ')}`
`Invalid callerLevel: ${options.callerLevel}. Valid levels are: ${
validLevels.join(", ")
}`,
);
}
}
// Validate colours if provided (should be an object)
if (options.colours !== undefined && typeof options.colours !== 'object') {
throw new Error('colours option must be an object');
if (options.colours !== undefined && typeof options.colours !== "object") {
throw new Error("colours option must be an object");
}
// Validate levels if provided (should be an object with numeric values)
if (options.levels !== undefined) {
if (typeof options.levels !== 'object') {
throw new Error('levels option must be an object');
if (typeof options.levels !== "object") {
throw new Error("levels option must be an object");
}
for (const [level, value] of Object.entries(options.levels)) {
if (
typeof value !== 'number' ||
typeof value !== "number" ||
value < 0 ||
!Number.isInteger(value)
) {
throw new Error(
`Level value for '${level}' must be a non-negative integer`
`Level value for '${level}' must be a non-negative integer`,
);
}
}
@@ -295,15 +311,19 @@ class Logger {
msg: String(logEntry.msg), // Convert to string safely
callerFile: logEntry.callerFile,
callerLine: logEntry.callerLine,
jsonError: `JSON stringify failed: ${error instanceof Error ? error.message : 'Unknown error'}`,
jsonError: `JSON stringify failed: ${
error instanceof Error ? error.message : "Unknown error"
}`,
};
return JSON.stringify(safeEntry);
} catch {
// Last resort - return a plain string
return `{"level":"${logEntry.level}","msg":"${String(
logEntry.msg
return `{"level":"${logEntry.level}","msg":"${
String(
logEntry.msg,
// eslint-disable-next-line no-useless-escape
).replace(/"/g, '\"')}","jsonError":"Multiple JSON errors occurred"}`;
).replace(/"/g, '"')
}","jsonError":"Multiple JSON errors occurred"}`;
}
}
}
@@ -312,7 +332,7 @@ class Logger {
simpleFormatter(logEntry: LogEntry): string {
const levelPadded = logEntry.level.toUpperCase().padEnd(5);
const caller = logEntry.callerFile
? `${logEntry.callerFile.split('/').pop()}:${logEntry.callerLine}`
? `${logEntry.callerFile.split("/").pop()}:${logEntry.callerLine}`
: null;
return caller
@@ -321,17 +341,22 @@ class Logger {
}
getCallerInfo(): { callerFile: string; callerLine: number } {
const originalFunc = (Error as unknown as { prepareStackTrace?: unknown }).prepareStackTrace;
let callerFile = 'unknown';
const originalFunc =
(Error as unknown as { prepareStackTrace?: unknown }).prepareStackTrace;
let callerFile = "unknown";
let callerLine = 0;
try {
const err = new Error();
// deno-lint-ignore prefer-const
let currentFile: string | undefined;
(Error as unknown as { prepareStackTrace?: unknown }).prepareStackTrace = function (_err: unknown, stack: unknown) {
(Error as unknown as { prepareStackTrace?: unknown }).prepareStackTrace =
function (_err: unknown, stack: unknown) {
return stack;
};
const stack = err.stack as unknown as { shift: () => { getFileName: () => string; getLineNumber: () => number }; length: number };
const stack = err.stack as unknown as {
shift: () => { getFileName: () => string; getLineNumber: () => number };
length: number;
};
currentFile = stack.shift().getFileName();
while (stack.length) {
const stackFrame = stack.shift();
@@ -345,17 +370,18 @@ class Logger {
} catch (e) {
this.callerErrorCount++;
if (this.callerErrorCount <= this.maxCallerErrors) {
console.error('Error retrieving caller info:', e);
console.error("Error retrieving caller info:", e);
if (this.callerErrorCount === this.maxCallerErrors) {
// loop detected
console.error(
`Caller detection failed ${this.maxCallerErrors} times. Suppressing further caller error messages.`
`Caller detection failed ${this.maxCallerErrors} times. Suppressing further caller error messages.`,
);
}
}
// callerFile and callerLine already set to defaults above
} finally {
(Error as unknown as { prepareStackTrace?: unknown }).prepareStackTrace = originalFunc;
(Error as unknown as { prepareStackTrace?: unknown }).prepareStackTrace =
originalFunc;
}
return { callerFile, callerLine };
}
@@ -366,18 +392,16 @@ class Logger {
}
// Only get caller info if current level is at or above callerLevel threshold
const shouldIncludeCaller =
this.options.levels[level] <=
const shouldIncludeCaller = this.options.levels[level] <=
this.options.levels[this.options.callerLevel];
const { callerFile, callerLine } = shouldIncludeCaller
? this.getCallerInfo()
: { callerFile: undefined, callerLine: undefined };
const now = new Date();
const time =
this.options.time === 'long'
const time = this.options.time === "long"
? now.toISOString()
: now.toISOString().slice(0, 16).replace('T', ' ');
: now.toISOString().slice(0, 16).replace("T", " ");
const logEntry: LogEntry = {
level,
@@ -389,7 +413,10 @@ class Logger {
};
// Only include caller info if it was requested
if (shouldIncludeCaller && callerFile !== undefined && callerLine !== undefined) {
if (
shouldIncludeCaller && callerFile !== undefined &&
callerLine !== undefined
) {
logEntry.callerFile = callerFile;
logEntry.callerLine = callerLine;
}
@@ -398,17 +425,17 @@ class Logger {
const resetColour = this.options.colours.reset;
// Select the appropriate formatter
const formatter =
this.formatters[this.options.format] || this.formatters.json;
const formatter = this.formatters[this.options.format] ||
this.formatters.json;
let formattedLog: string;
try {
formattedLog = formatter(logEntry);
// Ensure formatter returned a string
if (typeof formattedLog !== 'string') {
if (typeof formattedLog !== "string") {
throw new Error(
`Formatter returned ${typeof formattedLog} instead of string`
`Formatter returned ${typeof formattedLog} instead of string`,
);
}
} catch (error) {
@@ -416,18 +443,24 @@ class Logger {
try {
const safeEntry = {
...logEntry,
formatterError: `Formatter failed: ${error instanceof Error ? error.message : 'Unknown error'}`,
formatterError: `Formatter failed: ${
error instanceof Error ? error.message : "Unknown error"
}`,
};
formattedLog = this.formatters.json(safeEntry);
} catch {
// Even JSON formatter failed, create minimal safe output
formattedLog = `{"level":"${logEntry.level}","msg":"${String(
logEntry.msg
formattedLog = `{"level":"${logEntry.level}","msg":"${
String(
logEntry.msg,
).replace(
/"/g,
// eslint-disable-next-line no-useless-escape
'\"'
)}","formatterError":"Formatter failed: ${error instanceof Error ? error.message : 'Unknown error'}"}`;
'"',
)
}","formatterError":"Formatter failed: ${
error instanceof Error ? error.message : "Unknown error"
}"}`;
}
}
@@ -440,19 +473,19 @@ class Logger {
}
error(message: unknown, ...args: unknown[]): void {
this.log('error', message, ...args);
this.log("error", message, ...args);
}
warn(message: unknown, ...args: unknown[]): void {
this.log('warn', message, ...args);
this.log("warn", message, ...args);
}
info(message: unknown, ...args: unknown[]): void {
this.log('info', message, ...args);
this.log("info", message, ...args);
}
debug(message: unknown, ...args: unknown[]): void {
this.log('debug', message, ...args);
this.log("debug", message, ...args);
}
level(): LogLevel;

View File

@@ -9,7 +9,7 @@ const originalIsTTY = Deno.stdout.isTerminal();
export function mockConsole() {
console.log = (...args) => {
capturedLogs.push(args.join(' '));
capturedLogs.push(args.join(" "));
};
}
@@ -74,7 +74,7 @@ export function restoreMocks() {
// Helper to get parsed JSON from first captured log
export function getFirstLogAsJSON() {
if (capturedLogs.length === 0) {
throw new Error('No logs captured');
throw new Error("No logs captured");
}
return JSON.parse(capturedLogs[0]);
}

View File

@@ -1,28 +1,28 @@
import { assertEquals, assert } from "@std/assert";
import Logger from '../lib/logger.ts';
import { assert, assertEquals } from "@std/assert";
import Logger from "../lib/logger.ts";
Deno.test("Logger callerLevel - Caller Information Filtering - should include caller info for error when callerLevel is warn", () => {
const logger = new Logger({ callerLevel: 'warn' });
const logger = new Logger({ callerLevel: "warn" });
// Mock console.log to capture output
const originalLog = console.log;
let capturedOutput = '';
let capturedOutput = "";
console.log = (message) => {
capturedOutput = message;
};
try {
logger.error('test message');
logger.error("test message");
// Parse JSON output and check for caller info
const logEntry = JSON.parse(capturedOutput);
assert(
logEntry.callerFile,
'Should include callerFile for error level'
"Should include callerFile for error level",
);
assert(
typeof logEntry.callerLine === 'number',
'Should include callerLine for error level'
typeof logEntry.callerLine === "number",
"Should include callerLine for error level",
);
} finally {
console.log = originalLog;
@@ -30,27 +30,27 @@ Deno.test("Logger callerLevel - Caller Information Filtering - should include ca
});
Deno.test("Logger callerLevel - Caller Information Filtering - should include caller info for warn when callerLevel is warn", () => {
const logger = new Logger({ callerLevel: 'warn' });
const logger = new Logger({ callerLevel: "warn" });
// Mock console.log to capture output
const originalLog = console.log;
let capturedOutput = '';
let capturedOutput = "";
console.log = (message) => {
capturedOutput = message;
};
try {
logger.warn('test message');
logger.warn("test message");
// Parse JSON output and check for caller info
const logEntry = JSON.parse(capturedOutput);
assert(
logEntry.callerFile,
'Should include callerFile for warn level'
"Should include callerFile for warn level",
);
assert(
typeof logEntry.callerLine === 'number',
'Should include callerLine for warn level'
typeof logEntry.callerLine === "number",
"Should include callerLine for warn level",
);
} finally {
console.log = originalLog;
@@ -58,29 +58,29 @@ Deno.test("Logger callerLevel - Caller Information Filtering - should include ca
});
Deno.test("Logger callerLevel - Caller Information Filtering - should NOT include caller info for info when callerLevel is warn", () => {
const logger = new Logger({ callerLevel: 'warn' });
const logger = new Logger({ callerLevel: "warn" });
// Mock console.log to capture output
const originalLog = console.log;
let capturedOutput = '';
let capturedOutput = "";
console.log = (message) => {
capturedOutput = message;
};
try {
logger.info('test message');
logger.info("test message");
// Parse JSON output and check for absence of caller info
const logEntry = JSON.parse(capturedOutput);
assertEquals(
logEntry.callerFile,
undefined,
'Should NOT include callerFile for info level'
"Should NOT include callerFile for info level",
);
assertEquals(
logEntry.callerLine,
undefined,
'Should NOT include callerLine for info level'
"Should NOT include callerLine for info level",
);
} finally {
console.log = originalLog;
@@ -88,29 +88,29 @@ Deno.test("Logger callerLevel - Caller Information Filtering - should NOT includ
});
Deno.test("Logger callerLevel - Caller Information Filtering - should NOT include caller info for debug when callerLevel is warn", () => {
const logger = new Logger({ callerLevel: 'warn', level: 'debug' }); // Set level to debug to ensure debug messages are logged
const logger = new Logger({ callerLevel: "warn", level: "debug" }); // Set level to debug to ensure debug messages are logged
// Mock console.log to capture output
const originalLog = console.log;
let capturedOutput = '';
let capturedOutput = "";
console.log = (message) => {
capturedOutput = message;
};
try {
logger.debug('test message');
logger.debug("test message");
// Parse JSON output and check for absence of caller info
const logEntry = JSON.parse(capturedOutput);
assertEquals(
logEntry.callerFile,
undefined,
'Should NOT include callerFile for debug level'
"Should NOT include callerFile for debug level",
);
assertEquals(
logEntry.callerLine,
undefined,
'Should NOT include callerLine for debug level'
"Should NOT include callerLine for debug level",
);
} finally {
console.log = originalLog;
@@ -118,29 +118,29 @@ Deno.test("Logger callerLevel - Caller Information Filtering - should NOT includ
});
Deno.test("Logger callerLevel - Caller Information Filtering - should include caller info for all levels when callerLevel is debug", () => {
const logger = new Logger({ callerLevel: 'debug', level: 'debug' });
const logger = new Logger({ callerLevel: "debug", level: "debug" });
// Mock console.log to capture output
const originalLog = console.log;
let capturedOutput = '';
let capturedOutput = "";
console.log = (message) => {
capturedOutput = message;
};
try {
// Test each level
const levels = ['error', 'warn', 'info', 'debug'];
const levels = ["error", "warn", "info", "debug"];
for (const level of levels) {
logger[level]('test message');
logger[level]("test message");
const logEntry = JSON.parse(capturedOutput);
assert(
logEntry.callerFile,
`Should include callerFile for ${level} level`
`Should include callerFile for ${level} level`,
);
assert(
typeof logEntry.callerLine === 'number',
`Should include callerLine for ${level} level`
typeof logEntry.callerLine === "number",
`Should include callerLine for ${level} level`,
);
}
} finally {
@@ -149,31 +149,31 @@ Deno.test("Logger callerLevel - Caller Information Filtering - should include ca
});
Deno.test("Logger callerLevel - Caller Information Filtering - should NOT include caller info for any level when callerLevel is silent", () => {
const logger = new Logger({ callerLevel: 'silent', level: 'debug' });
const logger = new Logger({ callerLevel: "silent", level: "debug" });
// Mock console.log to capture output
const originalLog = console.log;
let capturedOutput = '';
let capturedOutput = "";
console.log = (message) => {
capturedOutput = message;
};
try {
// Test each level
const levels = ['error', 'warn', 'info', 'debug'];
const levels = ["error", "warn", "info", "debug"];
for (const level of levels) {
logger[level]('test message');
logger[level]("test message");
const logEntry = JSON.parse(capturedOutput);
assertEquals(
logEntry.callerFile,
undefined,
`Should NOT include callerFile for ${level} level`
`Should NOT include callerFile for ${level} level`,
);
assertEquals(
logEntry.callerLine,
undefined,
`Should NOT include callerLine for ${level} level`
`Should NOT include callerLine for ${level} level`,
);
}
} finally {
@@ -182,33 +182,33 @@ Deno.test("Logger callerLevel - Caller Information Filtering - should NOT includ
});
Deno.test("Logger callerLevel - Simple Formatter with callerLevel - should format correctly without caller info when excluded", () => {
const logger = new Logger({ format: 'simple', callerLevel: 'error' });
const logger = new Logger({ format: "simple", callerLevel: "error" });
// Mock console.log to capture output
const originalLog = console.log;
let capturedOutput = '';
let capturedOutput = "";
console.log = (message) => {
capturedOutput = message;
};
try {
logger.info('test message');
logger.info("test message");
// Should not contain caller info pattern
assert(
!capturedOutput.includes('unknown'),
'Should not include caller placeholder'
!capturedOutput.includes("unknown"),
"Should not include caller placeholder",
);
assert(
!capturedOutput.includes('.js:'),
'Should not include file:line pattern'
!capturedOutput.includes(".js:"),
"Should not include file:line pattern",
);
// Should still contain other parts
assert(capturedOutput.includes('INFO'), 'Should include log level');
assert(capturedOutput.includes("INFO"), "Should include log level");
assert(
capturedOutput.includes('test message'),
'Should include message'
capturedOutput.includes("test message"),
"Should include message",
);
} finally {
console.log = originalLog;
@@ -216,27 +216,27 @@ Deno.test("Logger callerLevel - Simple Formatter with callerLevel - should forma
});
Deno.test("Logger callerLevel - Simple Formatter with callerLevel - should format correctly with caller info when included", () => {
const logger = new Logger({ format: 'simple', callerLevel: 'info' });
const logger = new Logger({ format: "simple", callerLevel: "info" });
// Mock console.log to capture output
const originalLog = console.log;
let capturedOutput = '';
let capturedOutput = "";
console.log = (message) => {
capturedOutput = message;
};
try {
logger.info('test message');
logger.info("test message");
// Should contain caller info pattern
assert(
capturedOutput.includes('.js:'),
'Should include file:line pattern'
capturedOutput.includes(".js:"),
"Should include file:line pattern",
);
assert(capturedOutput.includes('INFO'), 'Should include log level');
assert(capturedOutput.includes("INFO"), "Should include log level");
assert(
capturedOutput.includes('test message'),
'Should include message'
capturedOutput.includes("test message"),
"Should include message",
);
} finally {
console.log = originalLog;
@@ -244,7 +244,7 @@ Deno.test("Logger callerLevel - Simple Formatter with callerLevel - should forma
});
Deno.test("Logger callerLevel - Performance Considerations - should not call getCallerInfo when caller info is not needed", () => {
const logger = new Logger({ callerLevel: 'error' });
const logger = new Logger({ callerLevel: "error" });
// Spy on getCallerInfo method
let getCallerInfoCalled = false;
@@ -259,11 +259,11 @@ Deno.test("Logger callerLevel - Performance Considerations - should not call get
console.log = () => {};
try {
logger.info('test message');
logger.info("test message");
assertEquals(
getCallerInfoCalled,
false,
'getCallerInfo should not be called for info level when callerLevel is error'
"getCallerInfo should not be called for info level when callerLevel is error",
);
} finally {
console.log = originalLog;
@@ -271,7 +271,7 @@ Deno.test("Logger callerLevel - Performance Considerations - should not call get
});
Deno.test("Logger callerLevel - Performance Considerations - should call getCallerInfo when caller info is needed", () => {
const logger = new Logger({ callerLevel: 'warn' });
const logger = new Logger({ callerLevel: "warn" });
// Spy on getCallerInfo method
let getCallerInfoCalled = false;
@@ -286,11 +286,11 @@ Deno.test("Logger callerLevel - Performance Considerations - should call getCall
console.log = () => {};
try {
logger.error('test message');
logger.error("test message");
assertEquals(
getCallerInfoCalled,
true,
'getCallerInfo should be called for error level when callerLevel is warn'
"getCallerInfo should be called for error level when callerLevel is warn",
);
} finally {
console.log = originalLog;

View File

@@ -1,74 +1,110 @@
import { assertEquals, assertThrows } from "@std/assert";
import Logger from '../lib/logger.ts';
import Logger from "../lib/logger.ts";
Deno.test("Logger Constructor - should throw error for invalid log level", () => {
assertThrows(() => {
new Logger({ level: 'invalid' });
}, Error, "Invalid log level: invalid. Valid levels are: silent, error, warn, info, debug");
assertThrows(
() => {
new Logger({ level: "invalid" });
},
Error,
"Invalid log level: invalid. Valid levels are: silent, error, warn, info, debug",
);
});
Deno.test("Logger Constructor - should throw error for invalid format", () => {
assertThrows(() => {
new Logger({ format: 'invalid' });
}, Error, "Invalid format: invalid. Valid formats are: json, simple");
assertThrows(
() => {
new Logger({ format: "invalid" });
},
Error,
"Invalid format: invalid. Valid formats are: json, simple",
);
});
Deno.test("Logger Constructor - should throw error for invalid time option", () => {
assertThrows(() => {
new Logger({ time: 'invalid' });
}, Error, "Invalid time: invalid. Valid times are: long, short");
assertThrows(
() => {
new Logger({ time: "invalid" });
},
Error,
"Invalid time: invalid. Valid times are: long, short",
);
});
Deno.test("Logger Constructor - should throw error for invalid callerLevel", () => {
assertThrows(() => {
new Logger({ callerLevel: 'invalid' });
}, Error, "Invalid callerLevel: invalid. Valid levels are: silent, error, warn, info, debug");
assertThrows(
() => {
new Logger({ callerLevel: "invalid" });
},
Error,
"Invalid callerLevel: invalid. Valid levels are: silent, error, warn, info, debug",
);
});
Deno.test("Logger Constructor - should throw error for non-object colours", () => {
assertThrows(() => {
new Logger({ colours: 'not an object' });
}, Error, "colours option must be an object");
assertThrows(
() => {
new Logger({ colours: "not an object" });
},
Error,
"colours option must be an object",
);
});
Deno.test("Logger Constructor - should throw error for non-object levels", () => {
assertThrows(() => {
new Logger({ levels: 'not an object' });
}, Error, "levels option must be an object");
assertThrows(
() => {
new Logger({ levels: "not an object" });
},
Error,
"levels option must be an object",
);
});
Deno.test("Logger Constructor - should throw error for invalid level values", () => {
assertThrows(() => {
assertThrows(
() => {
new Logger({ levels: { error: -1 } });
}, Error, "Level value for 'error' must be a non-negative integer");
},
Error,
"Level value for 'error' must be a non-negative integer",
);
assertThrows(() => {
new Logger({ levels: { error: 'not a number' } });
}, Error, "Level value for 'error' must be a non-negative integer");
assertThrows(
() => {
new Logger({ levels: { error: "not a number" } });
},
Error,
"Level value for 'error' must be a non-negative integer",
);
assertThrows(() => {
assertThrows(
() => {
new Logger({ levels: { error: 1.5 } });
}, Error, "Level value for 'error' must be a non-negative integer");
},
Error,
"Level value for 'error' must be a non-negative integer",
);
});
Deno.test("Logger Constructor - should accept valid options without throwing", () => {
// This should not throw
new Logger({
level: 'debug',
format: 'simple',
time: 'long',
callerLevel: 'error',
colours: { error: '\x1b[31m' },
level: "debug",
format: "simple",
time: "long",
callerLevel: "error",
colours: { error: "\x1b[31m" },
levels: { custom: 4 },
});
});
Deno.test("Logger Constructor - should instantiate with default options", () => {
const logger = new Logger();
assertEquals(logger.options.level, 'info');
assertEquals(logger.options.format, 'json');
assertEquals(logger.options.time, 'short');
assertEquals(logger.options.callerLevel, 'warn');
assertEquals(logger.options.level, "info");
assertEquals(logger.options.format, "json");
assertEquals(logger.options.time, "short");
assertEquals(logger.options.callerLevel, "warn");
assertEquals(logger.options.levels, {
silent: -1,
error: 0,
@@ -80,49 +116,49 @@ Deno.test("Logger Constructor - should instantiate with default options", () =>
Deno.test("Logger Constructor - should instantiate with custom options", () => {
const logger = new Logger({
level: 'debug',
format: 'simple',
time: 'long',
callerLevel: 'error',
level: "debug",
format: "simple",
time: "long",
callerLevel: "error",
});
assertEquals(logger.options.level, 'debug');
assertEquals(logger.options.format, 'simple');
assertEquals(logger.options.time, 'long');
assertEquals(logger.options.callerLevel, 'error');
assertEquals(logger.options.level, "debug");
assertEquals(logger.options.format, "simple");
assertEquals(logger.options.time, "long");
assertEquals(logger.options.callerLevel, "error");
});
Deno.test("Logger Constructor - should merge options correctly", () => {
const customOptions = {
level: 'debug',
format: 'simple',
time: 'long',
level: "debug",
format: "simple",
time: "long",
colours: {
error: '\x1b[31m', // different red
error: "\x1b[31m", // different red
},
};
const logger = new Logger(customOptions);
assertEquals(logger.options.level, 'debug');
assertEquals(logger.options.format, 'simple');
assertEquals(logger.options.time, 'long');
assertEquals(logger.options.colours.error, '\x1b[31m');
assertEquals(logger.options.level, "debug");
assertEquals(logger.options.format, "simple");
assertEquals(logger.options.time, "long");
assertEquals(logger.options.colours.error, "\x1b[31m");
// Should still have other default colors
assertEquals(logger.options.colours.warn, '\x1b[33m');
assertEquals(logger.options.colours.warn, "\x1b[33m");
});
Deno.test("Logger Constructor - should have all log level methods", () => {
const logger = new Logger();
assertEquals(typeof logger.error, 'function');
assertEquals(typeof logger.warn, 'function');
assertEquals(typeof logger.info, 'function');
assertEquals(typeof logger.debug, 'function');
assertEquals(typeof logger.error, "function");
assertEquals(typeof logger.warn, "function");
assertEquals(typeof logger.info, "function");
assertEquals(typeof logger.debug, "function");
});
Deno.test("Logger Constructor - should have level management methods", () => {
const logger = new Logger();
assertEquals(typeof logger.level, 'function');
assertEquals(typeof logger.setLevel, 'function');
assertEquals(typeof logger.level, "function");
assertEquals(typeof logger.setLevel, "function");
});
Deno.test("Logger Constructor - should detect TTY correctly", () => {
@@ -146,13 +182,13 @@ Deno.test("Logger Constructor - should work with all existing constructor patter
new Logger();
// Partial options - should not throw
new Logger({ level: 'debug' });
new Logger({ level: "debug" });
// Full options (without time) - should not throw
new Logger({
level: 'warn',
format: 'simple',
colours: { error: '\x1b[31m' },
level: "warn",
format: "simple",
colours: { error: "\x1b[31m" },
levels: { custom: 5 },
});
});

View File

@@ -1,24 +1,24 @@
import { assertEquals, assert } from "@std/assert";
import Logger from '../lib/logger.ts';
import { assert, assertEquals } from "@std/assert";
import Logger from "../lib/logger.ts";
import {
setupMocks,
getCapturedLogs,
clearCapturedLogs,
} from './helpers/logger-test-helpers.js';
getCapturedLogs,
setupMocks,
} from "./helpers/logger-test-helpers.js";
// Setup and teardown for all tests
setupMocks();
Deno.test("Logger Internal Error Handling - Formatter Error Handling - should fall back to JSON formatter when custom formatter throws", () => {
clearCapturedLogs();
const logger = new Logger({ format: 'simple' });
const logger = new Logger({ format: "simple" });
// Replace the simple formatter with one that throws
logger.formatters.simple = function () {
throw new Error('Custom formatter error');
throw new Error("Custom formatter error");
};
logger.info('test message');
logger.info("test message");
// Should still produce output using JSON formatter fallback
assertEquals(getCapturedLogs().length, 1);
@@ -26,24 +26,24 @@ Deno.test("Logger Internal Error Handling - Formatter Error Handling - should fa
// Should be valid JSON (fallback to JSON formatter)
const logOutput = getCapturedLogs()[0];
const parsed = JSON.parse(logOutput);
assertEquals(parsed.msg, 'test message');
assertEquals(parsed.msg, "test message");
assert(
parsed.formatterError.includes(
'Formatter failed: Custom formatter error'
)
"Formatter failed: Custom formatter error",
),
);
});
Deno.test("Logger Internal Error Handling - Formatter Error Handling - should not crash when formatter returns non-string", () => {
clearCapturedLogs();
const logger = new Logger({ format: 'simple' });
const logger = new Logger({ format: "simple" });
// Replace formatter with one that returns an object instead of string
logger.formatters.simple = function () {
return { notAString: true };
};
logger.info('test message');
logger.info("test message");
// Should still produce output (fallback should handle this)
assertEquals(getCapturedLogs().length, 1);
@@ -51,25 +51,25 @@ Deno.test("Logger Internal Error Handling - Formatter Error Handling - should no
// Should be valid JSON from fallback
const logOutput = getCapturedLogs()[0];
const parsed = JSON.parse(logOutput);
assertEquals(parsed.msg, 'test message');
assertEquals(parsed.msg, "test message");
});
Deno.test("Logger Internal Error Handling - Formatter Error Handling - should preserve original formatters after error", () => {
clearCapturedLogs();
const logger = new Logger({ format: 'simple' });
const logger = new Logger({ format: "simple" });
// Temporarily break the formatter
const originalSimple = logger.formatters.simple;
logger.formatters.simple = function () {
throw new Error('Temporary error');
throw new Error("Temporary error");
};
logger.info('first message');
logger.info("first message");
// Restore the formatter
logger.formatters.simple = originalSimple;
logger.info('second message');
logger.info("second message");
// First message should have used fallback, second should work normally
assertEquals(getCapturedLogs().length, 2);
@@ -78,17 +78,17 @@ Deno.test("Logger Internal Error Handling - Formatter Error Handling - should pr
JSON.parse(getCapturedLogs()[0]); // This should not throw
// Second log should be simple format
assert(getCapturedLogs()[1].includes('[INFO ]'));
assert(getCapturedLogs()[1].includes('second message'));
assert(getCapturedLogs()[1].includes("[INFO ]"));
assert(getCapturedLogs()[1].includes("second message"));
});
Deno.test("Logger Internal Error Handling - Formatter Error Handling - should handle repeated formatter failures without memory leaks", () => {
clearCapturedLogs();
const logger = new Logger({ format: 'simple' });
const logger = new Logger({ format: "simple" });
// Break the formatter
logger.formatters.simple = function () {
throw new Error('Always fails');
throw new Error("Always fails");
};
// Log many times

View File

@@ -1,89 +1,89 @@
import { assertEquals, assertThrows, assert } from "@std/assert";
import Logger from '../lib/logger.ts';
import { assert, assertEquals, assertThrows } from "@std/assert";
import Logger from "../lib/logger.ts";
import {
setupMocks,
getCapturedLogs,
clearCapturedErrors,
clearCapturedLogs,
getCapturedErrors,
clearCapturedErrors,
} from './helpers/logger-test-helpers.js';
getCapturedLogs,
setupMocks,
} from "./helpers/logger-test-helpers.js";
// Setup and teardown for all tests
setupMocks();
Deno.test("Logger Additional Fallback Tests - JSON Formatter Error Handling - should handle circular references in log data", () => {
clearCapturedLogs();
const logger = new Logger({ format: 'json' });
const logger = new Logger({ format: "json" });
// Create circular reference
const obj = { name: 'test' };
const obj = { name: "test" };
obj.self = obj;
logger.info('Message with circular ref: %j', obj);
logger.info("Message with circular ref: %j", obj);
assertEquals(getCapturedLogs().length, 1);
const logOutput = getCapturedLogs()[0];
// Should be valid JSON despite circular reference
const parsed = JSON.parse(logOutput);
assertEquals(parsed.level, 'info');
assertEquals(parsed.level, "info");
// Check for either jsonError or that the message was logged successfully
assert(
parsed.jsonError?.includes('JSON stringify failed') ||
parsed.msg.includes('Message with circular ref')
parsed.jsonError?.includes("JSON stringify failed") ||
parsed.msg.includes("Message with circular ref"),
);
});
Deno.test("Logger Additional Fallback Tests - JSON Formatter Error Handling - should handle objects with non-serializable properties", () => {
clearCapturedLogs();
const logger = new Logger({ format: 'json' });
const logger = new Logger({ format: "json" });
// Create object with function (non-serializable)
const objWithFunction = {
name: 'test',
name: "test",
func: function () {
return 'hello';
return "hello";
},
symbol: Symbol('test'),
symbol: Symbol("test"),
undefined: undefined,
};
logger.info('Object: %j', objWithFunction);
logger.info("Object: %j", objWithFunction);
assertEquals(getCapturedLogs().length, 1);
const logOutput = getCapturedLogs()[0];
// Should produce valid JSON
const parsed = JSON.parse(logOutput);
assertEquals(parsed.level, 'info');
assertEquals(parsed.level, "info");
// Should have the message in some form
assert(parsed.msg.includes('Object:'));
assert(parsed.msg.includes("Object:"));
});
Deno.test("Logger Additional Fallback Tests - JSON Formatter Error Handling - should handle when JSON formatter itself is broken", () => {
clearCapturedLogs();
const logger = new Logger({ format: 'json' });
const logger = new Logger({ format: "json" });
// Break the JSON formatter
logger.formatters.json = function () {
throw new Error('JSON formatter is broken');
throw new Error("JSON formatter is broken");
};
logger.info('test message');
logger.info("test message");
// Should still produce some output (last resort fallback)
assertEquals(getCapturedLogs().length, 1);
const logOutput = getCapturedLogs()[0];
// Should contain the message even if not perfectly formatted
assert(logOutput.includes('test message'));
assert(logOutput.includes("test message"));
});
Deno.test("Logger Additional Fallback Tests - Caller Detection Error Handling - should handle stack manipulation errors", () => {
clearCapturedLogs();
clearCapturedErrors();
const logger = new Logger({ format: 'json', callerLevel: 'info' });
const logger = new Logger({ format: "json", callerLevel: "info" });
// Override getCallerInfo to simulate an error
const originalGetCallerInfo = logger.getCallerInfo;
@@ -91,26 +91,26 @@ Deno.test("Logger Additional Fallback Tests - Caller Detection Error Handling -
this.callerErrorCount++;
if (this.callerErrorCount <= this.maxCallerErrors) {
console.error(
'Error retrieving caller info:',
new Error('Simulated caller error')
"Error retrieving caller info:",
new Error("Simulated caller error"),
);
if (this.callerErrorCount === this.maxCallerErrors) {
console.error(
`Caller detection failed ${this.maxCallerErrors} times. Suppressing further caller error messages.`
`Caller detection failed ${this.maxCallerErrors} times. Suppressing further caller error messages.`,
);
}
}
return { callerFile: 'unknown', callerLine: 0 };
return { callerFile: "unknown", callerLine: 0 };
};
try {
logger.info('test with simulated caller error');
logger.info("test with simulated caller error");
// Should still log the message
assertEquals(getCapturedLogs().length, 1);
const parsed = JSON.parse(getCapturedLogs()[0]);
assertEquals(parsed.msg, 'test with simulated caller error');
assertEquals(parsed.callerFile, 'unknown');
assertEquals(parsed.msg, "test with simulated caller error");
assertEquals(parsed.callerFile, "unknown");
assertEquals(parsed.callerLine, 0);
// Should have logged an error about caller detection
@@ -125,7 +125,7 @@ Deno.test("Logger Additional Fallback Tests - Caller Detection Error Handling -
clearCapturedLogs();
clearCapturedErrors();
const logger = new Logger({ format: 'json', callerLevel: 'info' });
const logger = new Logger({ format: "json", callerLevel: "info" });
// Override getCallerInfo to always simulate errors
const originalGetCallerInfo = logger.getCallerInfo;
@@ -133,16 +133,16 @@ Deno.test("Logger Additional Fallback Tests - Caller Detection Error Handling -
this.callerErrorCount++;
if (this.callerErrorCount <= this.maxCallerErrors) {
console.error(
'Error retrieving caller info:',
new Error('Always fails')
"Error retrieving caller info:",
new Error("Always fails"),
);
if (this.callerErrorCount === this.maxCallerErrors) {
console.error(
`Caller detection failed ${this.maxCallerErrors} times. Suppressing further caller error messages.`
`Caller detection failed ${this.maxCallerErrors} times. Suppressing further caller error messages.`,
);
}
}
return { callerFile: 'unknown', callerLine: 0 };
return { callerFile: "unknown", callerLine: 0 };
};
try {
@@ -163,8 +163,8 @@ Deno.test("Logger Additional Fallback Tests - Caller Detection Error Handling -
const suppressionFound = errorLogs.some((errorArgs) =>
errorArgs.some(
(arg) =>
typeof arg === 'string' &&
arg.includes('Suppressing further caller error messages')
typeof arg === "string" &&
arg.includes("Suppressing further caller error messages"),
)
);
assert(suppressionFound);
@@ -178,50 +178,50 @@ Deno.test("Logger Additional Fallback Tests - Caller Detection Error Handling -
clearCapturedLogs();
clearCapturedErrors();
const logger = new Logger({ format: 'json', callerLevel: 'info' });
const logger = new Logger({ format: "json", callerLevel: "info" });
// Override getCallerInfo to simulate different phases
const originalGetCallerInfo = logger.getCallerInfo;
let phase = 'error1';
let phase = "error1";
logger.getCallerInfo = function () {
if (phase === 'error1') {
if (phase === "error1") {
this.callerErrorCount++;
if (this.callerErrorCount <= this.maxCallerErrors) {
console.error(
'Error retrieving caller info:',
new Error('Phase 1 error')
"Error retrieving caller info:",
new Error("Phase 1 error"),
);
}
return { callerFile: 'unknown', callerLine: 0 };
} else if (phase === 'working') {
return { callerFile: "unknown", callerLine: 0 };
} else if (phase === "working") {
// Reset error count on successful call
this.callerErrorCount = 0;
return originalGetCallerInfo.call(this);
} else if (phase === 'error2') {
} else if (phase === "error2") {
this.callerErrorCount++;
if (this.callerErrorCount <= this.maxCallerErrors) {
console.error(
'Error retrieving caller info:',
new Error('Phase 2 error')
"Error retrieving caller info:",
new Error("Phase 2 error"),
);
}
return { callerFile: 'unknown', callerLine: 0 };
return { callerFile: "unknown", callerLine: 0 };
}
};
try {
// Cause some errors
logger.info('test 1');
logger.info('test 2');
logger.info("test 1");
logger.info("test 2");
// Switch to working mode
phase = 'working';
logger.info('test 3');
phase = "working";
logger.info("test 3");
// Break it again
phase = 'error2';
logger.info('test 4');
phase = "error2";
logger.info("test 4");
const errorLogs = getCapturedErrors();
// Should have errors from both phases
@@ -234,20 +234,24 @@ Deno.test("Logger Additional Fallback Tests - Caller Detection Error Handling -
Deno.test("Logger Additional Fallback Tests - Extreme Error Conditions - should handle when console.log itself throws", () => {
clearCapturedLogs();
const logger = new Logger({ format: 'json' });
const logger = new Logger({ format: "json" });
// Break console.log
const originalLog = console.log;
console.log = function () {
throw new Error('Console is broken');
throw new Error("Console is broken");
};
try {
// This should not crash the process, but the error will bubble up
// since there's no try-catch around console.log in the logger
assertThrows(() => {
logger.info('test message');
}, Error, "Console is broken");
assertThrows(
() => {
logger.info("test message");
},
Error,
"Console is broken",
);
} finally {
console.log = originalLog;
}
@@ -255,20 +259,20 @@ Deno.test("Logger Additional Fallback Tests - Extreme Error Conditions - should
Deno.test("Logger Additional Fallback Tests - Extreme Error Conditions - should handle when util.format has issues with complex objects", () => {
setupMocks();
const logger = new Logger({ format: 'json' });
const logger = new Logger({ format: "json" });
// Create an object that will cause issues with string conversion
const problematicObject = {
toString: function () {
throw new Error('toString failed');
throw new Error("toString failed");
},
valueOf: function () {
throw new Error('valueOf failed');
throw new Error("valueOf failed");
},
};
// The logger should handle this gracefully and not throw
logger.info('Message: %s', problematicObject);
logger.info("Message: %s", problematicObject);
const logs = getCapturedLogs();
assertEquals(logs.length, 1);
@@ -276,15 +280,14 @@ Deno.test("Logger Additional Fallback Tests - Extreme Error Conditions - should
const output = JSON.parse(logs[0]);
// The logger should handle the problematic object gracefully
// Either by showing [object Object] or the actual object structure
assertEquals(typeof output.msg, 'string');
assertEquals(output.msg.startsWith('Message: '), true);
assertEquals(typeof output.msg, "string");
assertEquals(output.msg.startsWith("Message: "), true);
});
Deno.test("Logger Additional Fallback Tests - Extreme Error Conditions - should handle when hostname fails", () => {
setupMocks();
clearCapturedLogs();
const logger = new Logger({ format: 'json' });
const logger = new Logger({ format: "json" });
// Mock hostname retrieval to throw (this would need to be mocked at the source)
// Since we can't directly mock os.hostname in Deno, this test shows the concept
@@ -292,9 +295,8 @@ Deno.test("Logger Additional Fallback Tests - Extreme Error Conditions - should
// For Deno, we could mock the hostname call if it were extracted to a mockable function
// For now, this is more of a documentation test
logger.info('test message');
logger.info("test message");
assertEquals(getCapturedLogs().length, 1);
});
Deno.test("Logger Additional Fallback Tests - Fallback Chain Testing - should handle formatter failures gracefully", () => {
@@ -302,11 +304,11 @@ Deno.test("Logger Additional Fallback Tests - Fallback Chain Testing - should ha
clearCapturedLogs();
clearCapturedErrors();
const logger = new Logger({ format: 'simple', callerLevel: 'info' });
const logger = new Logger({ format: "simple", callerLevel: "info" });
// Break the simple formatter
logger.formatters.simple = function () {
throw new Error('Simple formatter broken');
throw new Error("Simple formatter broken");
};
// Also simulate caller detection failure
@@ -315,16 +317,16 @@ Deno.test("Logger Additional Fallback Tests - Fallback Chain Testing - should ha
this.callerErrorCount++;
if (this.callerErrorCount <= this.maxCallerErrors) {
console.error(
'Error retrieving caller info:',
new Error('Caller detection failed')
"Error retrieving caller info:",
new Error("Caller detection failed"),
);
}
return { callerFile: 'unknown', callerLine: 0 };
return { callerFile: "unknown", callerLine: 0 };
};
try {
// Should still produce some output despite multiple failures
logger.info('test message');
logger.info("test message");
// Should produce some kind of output (fallback to JSON formatter)
assertEquals(getCapturedLogs().length, 1);
@@ -332,9 +334,9 @@ Deno.test("Logger Additional Fallback Tests - Fallback Chain Testing - should ha
// Should be valid JSON (fallback formatter)
const parsed = JSON.parse(output);
assertEquals(parsed.msg, 'test message');
assert(parsed.formatterError.includes('Simple formatter broken'));
assertEquals(parsed.callerFile, 'unknown');
assertEquals(parsed.msg, "test message");
assert(parsed.formatterError.includes("Simple formatter broken"));
assertEquals(parsed.callerFile, "unknown");
} finally {
logger.getCallerInfo = originalGetCallerInfo;
logger.callerErrorCount = 0;
@@ -346,11 +348,11 @@ Deno.test("Logger Additional Fallback Tests - Resource Cleanup - should not leak
clearCapturedLogs();
clearCapturedErrors();
const logger = new Logger({ format: 'simple' });
const logger = new Logger({ format: "simple" });
// Break the formatter
logger.formatters.simple = function () {
throw new Error('Always fails');
throw new Error("Always fails");
};
// Log many times to check for memory leaks
@@ -364,5 +366,4 @@ Deno.test("Logger Additional Fallback Tests - Resource Cleanup - should not leak
// Check that we're not accumulating error state
// (This is more of a smoke test - real memory leak detection would need different tools)
assert(true); // If we get here without crashing, that's good
});

View File

@@ -1,19 +1,19 @@
import { assertEquals, assert } from "@std/assert";
import Logger from '../lib/logger.ts';
import { assert, assertEquals } from "@std/assert";
import Logger from "../lib/logger.ts";
import {
setupMocks,
getCapturedLogs,
clearCapturedLogs,
getCapturedLogs,
getFirstLogAsJSON,
} from './helpers/logger-test-helpers.js';
setupMocks,
} from "./helpers/logger-test-helpers.js";
// Setup and teardown for all tests
setupMocks();
Deno.test("Logger JSON Formatter - Basic JSON Output - should produce valid JSON output", () => {
clearCapturedLogs();
const logger = new Logger({ format: 'json' });
logger.info('test message');
const logger = new Logger({ format: "json" });
logger.info("test message");
assertEquals(getCapturedLogs().length, 1);
const logOutput = getCapturedLogs()[0];
@@ -24,36 +24,36 @@ Deno.test("Logger JSON Formatter - Basic JSON Output - should produce valid JSON
Deno.test("Logger JSON Formatter - Basic JSON Output - should include all required fields in JSON output", () => {
clearCapturedLogs();
const logger = new Logger({ format: 'json', callerLevel: 'info' });
logger.info('test message');
const logger = new Logger({ format: "json", callerLevel: "info" });
logger.info("test message");
const parsed = getFirstLogAsJSON();
assertEquals(parsed.level, 'info');
assertEquals(parsed.level, "info");
assertEquals(parsed.levelNumber, 2);
assertEquals(parsed.msg, 'test message');
assertEquals(typeof parsed.time, 'string');
assertEquals(typeof parsed.pid, 'number');
assertEquals(typeof parsed.hostname, 'string');
assertEquals(parsed.msg, "test message");
assertEquals(typeof parsed.time, "string");
assertEquals(typeof parsed.pid, "number");
assertEquals(typeof parsed.hostname, "string");
assert(parsed.callerFile);
assertEquals(typeof parsed.callerLine, 'number');
assertEquals(typeof parsed.callerLine, "number");
});
Deno.test("Logger JSON Formatter - Basic JSON Output - should format timestamp correctly based on time option", () => {
clearCapturedLogs();
const logger = new Logger({ format: 'json', time: 'short' });
logger.info('test message');
const logger = new Logger({ format: "json", time: "short" });
logger.info("test message");
const parsed = getFirstLogAsJSON();
// Should be short format, not ISO
assert(parsed.time.match(/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}$/));
assert(!parsed.time.includes('T'));
assert(!parsed.time.includes('Z'));
assert(!parsed.time.includes("T"));
assert(!parsed.time.includes("Z"));
});
Deno.test("Logger JSON Formatter - JSON Error Handling - should handle circular references in log entry", () => {
clearCapturedLogs();
const logger = new Logger({ format: 'json' });
const logger = new Logger({ format: "json" });
// Create a circular reference by modifying the logger's formatters
const originalJsonFormatter = logger.formatters.json;
@@ -67,33 +67,33 @@ Deno.test("Logger JSON Formatter - JSON Error Handling - should handle circular
return originalJsonFormatter.call(this, logEntry);
};
logger.info('test with circular reference');
logger.info("test with circular reference");
const logOutput = getCapturedLogs()[0];
// Should be valid JSON despite circular reference
const parsed = JSON.parse(logOutput);
// Should contain error information
assert(parsed.jsonError.includes('JSON stringify failed'));
assertEquals(parsed.msg, 'test with circular reference');
assert(parsed.jsonError.includes("JSON stringify failed"));
assertEquals(parsed.msg, "test with circular reference");
});
Deno.test("Logger JSON Formatter - JSON Error Handling - should handle JSON stringify errors with fallback", () => {
clearCapturedLogs();
const logger = new Logger({ format: 'json' });
const logger = new Logger({ format: "json" });
// Create a problematic object that will cause JSON.stringify to fail
const problematic = {};
Object.defineProperty(problematic, 'badProp', {
Object.defineProperty(problematic, "badProp", {
get() {
throw new Error('Property access error');
throw new Error("Property access error");
},
enumerable: true,
});
// Test the formatter directly with a problematic object
const problematicLogEntry = {
level: 'info',
msg: 'test message',
level: "info",
msg: "test message",
problematic: problematic,
};
@@ -101,12 +101,12 @@ Deno.test("Logger JSON Formatter - JSON Error Handling - should handle JSON stri
// Should produce valid JSON with error info
const parsed = JSON.parse(result);
assert(parsed.jsonError.includes('JSON stringify failed'));
assert(parsed.jsonError.includes("JSON stringify failed"));
});
Deno.test("Logger JSON Formatter - JSON Error Handling - should handle extreme JSON stringify failures", () => {
clearCapturedLogs();
const logger = new Logger({ format: 'json' });
const logger = new Logger({ format: "json" });
// Create an object that will fail even the safe fallback
// by mocking JSON.stringify to always throw
@@ -116,22 +116,22 @@ Deno.test("Logger JSON Formatter - JSON Error Handling - should handle extreme J
JSON.stringify = function (...args) {
callCount++;
if (callCount <= 2) {
throw new Error('Mock JSON error');
throw new Error("Mock JSON error");
}
return originalStringify.apply(this, args);
};
try {
const result = logger.formatters.json({
level: 'error',
msg: 'test message',
level: "error",
msg: "test message",
});
// Should still produce valid JSON string even after multiple failures
const parsed = JSON.parse(result);
assertEquals(parsed.level, 'error');
assertEquals(parsed.msg, 'test message');
assert(parsed.jsonError.includes('Multiple JSON errors occurred'));
assertEquals(parsed.level, "error");
assertEquals(parsed.msg, "test message");
assert(parsed.jsonError.includes("Multiple JSON errors occurred"));
} finally {
JSON.stringify = originalStringify;
}
@@ -139,7 +139,7 @@ Deno.test("Logger JSON Formatter - JSON Error Handling - should handle extreme J
Deno.test("Logger JSON Formatter - Special Characters and Edge Cases - should handle special characters", () => {
clearCapturedLogs();
const logger = new Logger({ format: 'json' });
const logger = new Logger({ format: "json" });
logger.info('Special chars: "quotes", \\backslash, \nnewline');
// Should produce valid JSON despite special characters
@@ -148,26 +148,26 @@ Deno.test("Logger JSON Formatter - Special Characters and Edge Cases - should ha
Deno.test("Logger JSON Formatter - Special Characters and Edge Cases - should handle empty messages", () => {
clearCapturedLogs();
const logger = new Logger({ format: 'json' });
logger.info('');
const logger = new Logger({ format: "json" });
logger.info("");
const parsed = getFirstLogAsJSON();
assertEquals(parsed.msg, '');
assertEquals(parsed.msg, "");
});
Deno.test("Logger JSON Formatter - Special Characters and Edge Cases - should handle null and undefined arguments", () => {
clearCapturedLogs();
const logger = new Logger({ format: 'json' });
logger.info('Value: %s', null);
const logger = new Logger({ format: "json" });
logger.info("Value: %s", null);
const parsed = getFirstLogAsJSON();
assertEquals(parsed.msg, 'Value: null');
assertEquals(parsed.msg, "Value: null");
});
Deno.test("Logger JSON Formatter - Special Characters and Edge Cases - should handle very long messages", () => {
clearCapturedLogs();
const longMessage = 'x'.repeat(10000);
const logger = new Logger({ format: 'json' });
const longMessage = "x".repeat(10000);
const logger = new Logger({ format: "json" });
logger.info(longMessage);
const parsed = getFirstLogAsJSON();
@@ -176,9 +176,9 @@ Deno.test("Logger JSON Formatter - Special Characters and Edge Cases - should ha
Deno.test("Logger JSON Formatter - Special Characters and Edge Cases - should handle objects in messages", () => {
clearCapturedLogs();
const logger = new Logger({ format: 'json' });
const obj = { key: 'value', nested: { prop: 123 } };
logger.info('Object: %j', obj);
const logger = new Logger({ format: "json" });
const obj = { key: "value", nested: { prop: 123 } };
logger.info("Object: %j", obj);
const parsed = getFirstLogAsJSON();
assert(parsed.msg.includes('{"key":"value","nested":{"prop":123}}'));
@@ -186,40 +186,40 @@ Deno.test("Logger JSON Formatter - Special Characters and Edge Cases - should ha
Deno.test("Logger JSON Formatter - All Log Levels in JSON - should log error messages with correct level", () => {
clearCapturedLogs();
const logger = new Logger({ format: 'json' });
logger.error('error message');
const logger = new Logger({ format: "json" });
logger.error("error message");
const parsed = getFirstLogAsJSON();
assertEquals(parsed.level, 'error');
assertEquals(parsed.level, "error");
assertEquals(parsed.levelNumber, 0);
});
Deno.test("Logger JSON Formatter - All Log Levels in JSON - should log warn messages with correct level", () => {
clearCapturedLogs();
const logger = new Logger({ format: 'json' });
logger.warn('warn message');
const logger = new Logger({ format: "json" });
logger.warn("warn message");
const parsed = getFirstLogAsJSON();
assertEquals(parsed.level, 'warn');
assertEquals(parsed.level, "warn");
assertEquals(parsed.levelNumber, 1);
});
Deno.test("Logger JSON Formatter - All Log Levels in JSON - should log info messages with correct level", () => {
clearCapturedLogs();
const logger = new Logger({ format: 'json' });
logger.info('info message');
const logger = new Logger({ format: "json" });
logger.info("info message");
const parsed = getFirstLogAsJSON();
assertEquals(parsed.level, 'info');
assertEquals(parsed.level, "info");
assertEquals(parsed.levelNumber, 2);
});
Deno.test("Logger JSON Formatter - All Log Levels in JSON - should log debug messages with correct level", () => {
clearCapturedLogs();
const logger = new Logger({ level: 'debug', format: 'json' });
logger.debug('debug message');
const logger = new Logger({ level: "debug", format: "json" });
logger.debug("debug message");
const parsed = getFirstLogAsJSON();
assertEquals(parsed.level, 'debug');
assertEquals(parsed.level, "debug");
assertEquals(parsed.levelNumber, 3);
});

View File

@@ -1,151 +1,159 @@
import { assertEquals, assertThrows } from "@std/assert";
import Logger from '../lib/logger.ts';
import Logger from "../lib/logger.ts";
import {
setupMocks,
getCapturedLogs,
clearCapturedLogs,
} from './helpers/logger-test-helpers.js';
getCapturedLogs,
setupMocks,
} from "./helpers/logger-test-helpers.js";
// Setup and teardown for all tests
setupMocks();
Deno.test("Logger Level Management - Level Setting and Getting - should change log level with level() method", () => {
const logger = new Logger();
logger.level('debug');
assertEquals(logger.options.level, 'debug');
logger.level("debug");
assertEquals(logger.options.level, "debug");
});
Deno.test("Logger Level Management - Level Setting and Getting - should return current level when called without arguments", () => {
const logger = new Logger({ level: 'debug' });
assertEquals(logger.level(), 'debug');
const logger = new Logger({ level: "debug" });
assertEquals(logger.level(), "debug");
});
Deno.test("Logger Level Management - Level Setting and Getting - should return new level when setting level", () => {
const logger = new Logger();
const result = logger.level('error');
assertEquals(result, 'error');
assertEquals(logger.options.level, 'error');
const result = logger.level("error");
assertEquals(result, "error");
assertEquals(logger.options.level, "error");
});
Deno.test("Logger Level Management - Level Setting and Getting - should throw error for invalid log level", () => {
const logger = new Logger();
assertThrows(() => {
logger.level('invalid');
}, Error, "Invalid log level: invalid");
assertThrows(
() => {
logger.level("invalid");
},
Error,
"Invalid log level: invalid",
);
});
Deno.test("Logger Level Management - Level Setting and Getting - should allow method chaining after setting level", () => {
const logger = new Logger();
// This should not throw and should return a level
const result = logger.level('warn');
assertEquals(result, 'warn');
assertEquals(typeof result, 'string');
const result = logger.level("warn");
assertEquals(result, "warn");
assertEquals(typeof result, "string");
});
Deno.test("Logger Level Management - setLevel Method - should have setLevel method as alias", () => {
const logger = new Logger();
assertEquals(typeof logger.setLevel, 'function');
assertEquals(typeof logger.setLevel, "function");
});
Deno.test("Logger Level Management - setLevel Method - should set level correctly with setLevel method", () => {
const logger = new Logger();
const result = logger.setLevel('debug');
assertEquals(result, 'debug');
assertEquals(logger.options.level, 'debug');
const result = logger.setLevel("debug");
assertEquals(result, "debug");
assertEquals(logger.options.level, "debug");
});
Deno.test("Logger Level Management - setLevel Method - should return current level with setLevel when no args", () => {
const logger = new Logger({ level: 'warn' });
const logger = new Logger({ level: "warn" });
const result = logger.setLevel();
assertEquals(result, 'warn');
assertEquals(result, "warn");
});
Deno.test("Logger Level Management - setLevel Method - should throw error for invalid level in setLevel", () => {
const logger = new Logger();
assertThrows(() => {
logger.setLevel('invalid');
}, Error, "Invalid log level: invalid");
assertThrows(
() => {
logger.setLevel("invalid");
},
Error,
"Invalid log level: invalid",
);
});
Deno.test("Logger Level Management - setLevel Method - should maintain consistency between level() and setLevel()", () => {
const logger = new Logger();
logger.level('error');
assertEquals(logger.setLevel(), 'error');
logger.level("error");
assertEquals(logger.setLevel(), "error");
logger.setLevel('debug');
assertEquals(logger.level(), 'debug');
logger.setLevel("debug");
assertEquals(logger.level(), "debug");
});
Deno.test("Logger Level Management - setLevel Method - should support fluent interface pattern", () => {
const logger = new Logger();
// This demonstrates the fluent interface working
const currentLevel = logger.level('warn');
assertEquals(currentLevel, 'warn');
const currentLevel = logger.level("warn");
assertEquals(currentLevel, "warn");
// Both methods should return the current level for chaining
assertEquals(logger.level('info'), 'info');
assertEquals(logger.setLevel('debug'), 'debug');
assertEquals(logger.level("info"), "info");
assertEquals(logger.setLevel("debug"), "debug");
});
Deno.test("Logger Level Management - Log Level Filtering - should filter debug messages when level is info", () => {
clearCapturedLogs();
const logger = new Logger({ level: 'info' });
logger.debug('debug message');
const logger = new Logger({ level: "info" });
logger.debug("debug message");
assertEquals(getCapturedLogs().length, 0);
});
Deno.test("Logger Level Management - Log Level Filtering - should show info messages when level is info", () => {
clearCapturedLogs();
const logger = new Logger({ level: 'info' });
logger.info('info message');
const logger = new Logger({ level: "info" });
logger.info("info message");
assertEquals(getCapturedLogs().length, 1);
});
Deno.test("Logger Level Management - Log Level Filtering - should show error messages at any level", () => {
clearCapturedLogs();
const logger = new Logger({ level: 'error' });
logger.error('error message');
const logger = new Logger({ level: "error" });
logger.error("error message");
assertEquals(getCapturedLogs().length, 1);
});
Deno.test("Logger Level Management - Log Level Filtering - should filter warn and info when level is error", () => {
clearCapturedLogs();
const logger = new Logger({ level: 'error' });
logger.warn('warn message');
logger.info('info message');
const logger = new Logger({ level: "error" });
logger.warn("warn message");
logger.info("info message");
assertEquals(getCapturedLogs().length, 0);
});
Deno.test("Logger Level Management - Log Level Filtering - should show all messages when level is debug", () => {
clearCapturedLogs();
const logger = new Logger({ level: 'debug' });
logger.error('error message');
logger.warn('warn message');
logger.info('info message');
logger.debug('debug message');
const logger = new Logger({ level: "debug" });
logger.error("error message");
logger.warn("warn message");
logger.info("info message");
logger.debug("debug message");
assertEquals(getCapturedLogs().length, 4);
});
Deno.test("Logger Level Management - Log Level Filtering - should show warn and above when level is warn", () => {
clearCapturedLogs();
const logger = new Logger({ level: 'warn' });
logger.error('error message');
logger.warn('warn message');
logger.info('info message');
logger.debug('debug message');
const logger = new Logger({ level: "warn" });
logger.error("error message");
logger.warn("warn message");
logger.info("info message");
logger.debug("debug message");
assertEquals(getCapturedLogs().length, 2);
});
Deno.test("Logger Level Management - Silent Level - should suppress all output when level is silent", () => {
clearCapturedLogs();
const logger = new Logger({ level: 'silent' });
const logger = new Logger({ level: "silent" });
logger.error('error message');
logger.warn('warn message');
logger.info('info message');
logger.debug('debug message');
logger.error("error message");
logger.warn("warn message");
logger.info("info message");
logger.debug("debug message");
// No messages should be logged
assertEquals(getCapturedLogs().length, 0);
@@ -153,21 +161,21 @@ Deno.test("Logger Level Management - Silent Level - should suppress all output w
Deno.test("Logger Level Management - Silent Level - should allow setting level to silent", () => {
const logger = new Logger();
const result = logger.level('silent');
assertEquals(result, 'silent');
assertEquals(logger.options.level, 'silent');
const result = logger.level("silent");
assertEquals(result, "silent");
assertEquals(logger.options.level, "silent");
});
Deno.test("Logger Level Management - Silent Level - should work with setLevel for silent level", () => {
const logger = new Logger();
const result = logger.setLevel('silent');
assertEquals(result, 'silent');
assertEquals(logger.options.level, 'silent');
const result = logger.setLevel("silent");
assertEquals(result, "silent");
assertEquals(logger.options.level, "silent");
});
Deno.test("Logger Level Management - Silent Level - should remain silent after multiple log attempts", () => {
clearCapturedLogs();
const logger = new Logger({ level: 'silent' });
const logger = new Logger({ level: "silent" });
// Try logging multiple times
for (let i = 0; i < 5; i++) {
@@ -183,23 +191,23 @@ Deno.test("Logger Level Management - Silent Level - should remain silent after m
Deno.test("Logger Level Management - Dynamic Level Changes - should respect level changes during runtime", () => {
clearCapturedLogs();
const logger = new Logger({ level: 'error' });
const logger = new Logger({ level: "error" });
// Should not log at info level
logger.info('info message 1');
logger.info("info message 1");
assertEquals(getCapturedLogs().length, 0);
// Change to info level
logger.level('info');
logger.level("info");
// Should now log info messages
logger.info('info message 2');
logger.info("info message 2");
assertEquals(getCapturedLogs().length, 1);
// Change to silent
logger.level('silent');
logger.level("silent");
// Should not log anything
logger.error('error message');
logger.error("error message");
assertEquals(getCapturedLogs().length, 1); // Still just the previous info message
});

View File

@@ -1,10 +1,10 @@
import { assertEquals, assert } from "@std/assert";
import Logger from '../lib/logger.ts';
import { assert, assertEquals } from "@std/assert";
import Logger from "../lib/logger.ts";
import {
setupMocks,
getCapturedLogs,
clearCapturedLogs,
} from './helpers/logger-test-helpers.js';
getCapturedLogs,
setupMocks,
} from "./helpers/logger-test-helpers.js";
// Setup and teardown for all tests
setupMocks();
@@ -14,12 +14,12 @@ Deno.test("Logger Robustness - Edge Cases and Data Handling - should not crash o
const logger = new Logger();
// This should not throw
logger.info('test message');
logger.info("test message");
});
Deno.test("Logger Robustness - Edge Cases and Data Handling - should handle undefined and null messages gracefully", () => {
clearCapturedLogs();
const logger = new Logger({ format: 'json' });
const logger = new Logger({ format: "json" });
// These should not crash
logger.info(undefined);
@@ -31,14 +31,14 @@ Deno.test("Logger Robustness - Edge Cases and Data Handling - should handle unde
const parsed1 = JSON.parse(logs[0]);
const parsed2 = JSON.parse(logs[1]);
assertEquals(parsed1.msg, 'undefined');
assertEquals(parsed2.msg, 'null');
assertEquals(parsed1.msg, "undefined");
assertEquals(parsed2.msg, "null");
});
Deno.test("Logger Robustness - Edge Cases and Data Handling - should handle extremely large messages", () => {
clearCapturedLogs();
const logger = new Logger({ format: 'json' });
const hugeMessage = 'x'.repeat(100000);
const logger = new Logger({ format: "json" });
const hugeMessage = "x".repeat(100000);
logger.info(hugeMessage);
@@ -48,12 +48,12 @@ Deno.test("Logger Robustness - Edge Cases and Data Handling - should handle extr
Deno.test("Logger Robustness - Edge Cases and Data Handling - should handle circular objects in message formatting", () => {
clearCapturedLogs();
const logger = new Logger({ format: 'json' });
const logger = new Logger({ format: "json" });
const circular = { name: 'test' };
const circular = { name: "test" };
circular.self = circular;
logger.info('Circular: %j', circular);
logger.info("Circular: %j", circular);
// Should still log something
assertEquals(getCapturedLogs().length, 1);
@@ -61,7 +61,7 @@ Deno.test("Logger Robustness - Edge Cases and Data Handling - should handle circ
Deno.test("Logger Robustness - Performance and Memory - should handle rapid consecutive logging without issues", () => {
clearCapturedLogs();
const logger = new Logger({ format: 'json' });
const logger = new Logger({ format: "json" });
for (let i = 0; i < 1000; i++) {
logger.info(`rapid message ${i}`);
@@ -72,7 +72,7 @@ Deno.test("Logger Robustness - Performance and Memory - should handle rapid cons
Deno.test("Logger Robustness - Performance and Memory - should handle repeated logging operations efficiently", () => {
clearCapturedLogs();
const logger = new Logger({ format: 'simple' });
const logger = new Logger({ format: "simple" });
const startTime = Date.now();
@@ -91,8 +91,8 @@ Deno.test("Logger Robustness - Performance and Memory - should handle repeated l
Deno.test("Logger Robustness - Performance and Memory - should handle mixed format types in rapid succession", () => {
clearCapturedLogs();
const jsonLogger = new Logger({ format: 'json' });
const simpleLogger = new Logger({ format: 'simple' });
const jsonLogger = new Logger({ format: "json" });
const simpleLogger = new Logger({ format: "simple" });
for (let i = 0; i < 50; i++) {
jsonLogger.info(`json message ${i}`);
@@ -104,40 +104,40 @@ Deno.test("Logger Robustness - Performance and Memory - should handle mixed form
Deno.test("Logger Robustness - Complex Data Structures - should handle deeply nested objects", () => {
clearCapturedLogs();
const logger = new Logger({ format: 'json' });
const logger = new Logger({ format: "json" });
const deepObject = {
level1: { level2: { level3: { level4: { value: 'deep' } } } },
level1: { level2: { level3: { level4: { value: "deep" } } } },
};
logger.info('Deep object: %j', deepObject);
logger.info("Deep object: %j", deepObject);
assertEquals(getCapturedLogs().length, 1);
});
Deno.test("Logger Robustness - Complex Data Structures - should handle arrays with mixed data types", () => {
clearCapturedLogs();
const logger = new Logger({ format: 'json' });
const logger = new Logger({ format: "json" });
const mixedArray = [
1,
'string',
"string",
{ obj: true },
[1, 2, 3],
null,
undefined,
];
logger.info('Mixed array: %j', mixedArray);
logger.info("Mixed array: %j", mixedArray);
assertEquals(getCapturedLogs().length, 1);
});
Deno.test("Logger Robustness - Complex Data Structures - should handle special characters and unicode", () => {
clearCapturedLogs();
const logger = new Logger({ format: 'json' });
const logger = new Logger({ format: "json" });
const specialMessage = 'Special chars: \n\t\r\\"\'🚀 Unicode: こんにちは';
const specialMessage = "Special chars: \n\t\r\\\"'🚀 Unicode: こんにちは";
logger.info(specialMessage);

View File

@@ -1,182 +1,182 @@
import { assertEquals, assert } from "@std/assert";
import Logger from '../lib/logger.ts';
import { assert, assertEquals } from "@std/assert";
import Logger from "../lib/logger.ts";
import {
setupMocks,
getCapturedLogs,
clearCapturedLogs,
setTTYMode,
getCapturedLogs,
restoreTTY,
} from './helpers/logger-test-helpers.js';
setTTYMode,
setupMocks,
} from "./helpers/logger-test-helpers.js";
// Setup and teardown for all tests
setupMocks();
Deno.test("Logger Simple Formatter - Basic Simple Format - should produce simple text format", () => {
clearCapturedLogs();
const logger = new Logger({ format: 'simple' });
logger.info('test message');
const logger = new Logger({ format: "simple" });
logger.info("test message");
assertEquals(getCapturedLogs().length, 1);
const logOutput = getCapturedLogs()[0];
// Should contain timestamp, level, caller, and message
assert(logOutput.includes('[INFO ]'));
assert(logOutput.includes('test message'));
assert(logOutput.includes("[INFO ]"));
assert(logOutput.includes("test message"));
// Should contain short timestamp by default
assert(logOutput.match(/\[\d{4}-\d{2}-\d{2} \d{2}:\d{2}\]/));
});
Deno.test("Logger Simple Formatter - Basic Simple Format - should pad log levels correctly", () => {
clearCapturedLogs();
const logger = new Logger({ format: 'simple', level: 'debug' });
logger.error('error msg');
logger.debug('debug msg');
const logger = new Logger({ format: "simple", level: "debug" });
logger.error("error msg");
logger.debug("debug msg");
const logs = getCapturedLogs();
assert(logs[0].includes('[ERROR]'));
assert(logs[1].includes('[DEBUG]'));
assert(logs[0].includes("[ERROR]"));
assert(logs[1].includes("[DEBUG]"));
});
Deno.test("Logger Simple Formatter - Basic Simple Format - should include caller information", () => {
clearCapturedLogs();
const logger = new Logger({ format: 'simple', callerLevel: 'info' });
logger.info('test message');
const logger = new Logger({ format: "simple", callerLevel: "info" });
logger.info("test message");
const logOutput = getCapturedLogs()[0];
// Should contain filename and line number
assert(logOutput.includes('.js:'));
assert(logOutput.includes(".js:"));
});
Deno.test("Logger Simple Formatter - Basic Simple Format - should format with long timestamp when specified", () => {
clearCapturedLogs();
const logger = new Logger({ format: 'simple', time: 'long' });
logger.info('test message');
const logger = new Logger({ format: "simple", time: "long" });
logger.info("test message");
const logOutput = getCapturedLogs()[0];
// Should contain long time format in brackets
assert(
logOutput.match(/\[\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z\]/)
logOutput.match(/\[\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z\]/),
);
assert(logOutput.includes('T'));
assert(logOutput.includes('Z'));
assert(logOutput.includes("T"));
assert(logOutput.includes("Z"));
});
Deno.test("Logger Simple Formatter - All Log Levels in Simple Format - should format error level correctly", () => {
clearCapturedLogs();
const logger = new Logger({ format: 'simple' });
logger.error('error message');
const logger = new Logger({ format: "simple" });
logger.error("error message");
const logOutput = getCapturedLogs()[0];
assert(logOutput.includes('[ERROR]'));
assert(logOutput.includes('error message'));
assert(logOutput.includes("[ERROR]"));
assert(logOutput.includes("error message"));
});
Deno.test("Logger Simple Formatter - All Log Levels in Simple Format - should format warn level correctly", () => {
clearCapturedLogs();
const logger = new Logger({ format: 'simple' });
logger.warn('warn message');
const logger = new Logger({ format: "simple" });
logger.warn("warn message");
const logOutput = getCapturedLogs()[0];
assert(logOutput.includes('[WARN ]'));
assert(logOutput.includes('warn message'));
assert(logOutput.includes("[WARN ]"));
assert(logOutput.includes("warn message"));
});
Deno.test("Logger Simple Formatter - All Log Levels in Simple Format - should format info level correctly", () => {
clearCapturedLogs();
const logger = new Logger({ format: 'simple' });
logger.info('info message');
const logger = new Logger({ format: "simple" });
logger.info("info message");
const logOutput = getCapturedLogs()[0];
assert(logOutput.includes('[INFO ]'));
assert(logOutput.includes('info message'));
assert(logOutput.includes("[INFO ]"));
assert(logOutput.includes("info message"));
});
Deno.test("Logger Simple Formatter - All Log Levels in Simple Format - should format debug level correctly", () => {
clearCapturedLogs();
const logger = new Logger({ format: 'simple', level: 'debug' });
logger.debug('debug message');
const logger = new Logger({ format: "simple", level: "debug" });
logger.debug("debug message");
const logOutput = getCapturedLogs()[0];
assert(logOutput.includes('[DEBUG]'));
assert(logOutput.includes('debug message'));
assert(logOutput.includes("[DEBUG]"));
assert(logOutput.includes("debug message"));
});
Deno.test("Logger Simple Formatter - Color Handling - should include color codes when output is TTY", () => {
clearCapturedLogs();
setTTYMode(true);
const logger = new Logger({ format: 'simple' });
logger.error('error message');
const logger = new Logger({ format: "simple" });
logger.error("error message");
const logOutput = getCapturedLogs()[0];
// Should contain ANSI color codes
assert(logOutput.includes('\x1b[91m')); // red for error
assert(logOutput.includes('\x1b[0m')); // reset
assert(logOutput.includes("\x1b[91m")); // red for error
assert(logOutput.includes("\x1b[0m")); // reset
});
Deno.test("Logger Simple Formatter - Color Handling - should not include color codes when output is redirected", () => {
clearCapturedLogs();
setTTYMode(false);
const logger = new Logger({ format: 'simple' });
logger.error('error message');
const logger = new Logger({ format: "simple" });
logger.error("error message");
const logOutput = getCapturedLogs()[0];
// Should not contain ANSI color codes
assert(!logOutput.includes('\x1b['));
assert(!logOutput.includes("\x1b["));
});
Deno.test("Logger Simple Formatter - Color Handling - should use appropriate colors for different levels", () => {
clearCapturedLogs();
setTTYMode(true);
const logger = new Logger({ format: 'simple', level: 'debug' });
const logger = new Logger({ format: "simple", level: "debug" });
logger.error('error');
logger.warn('warn');
logger.info('info');
logger.debug('debug');
logger.error("error");
logger.warn("warn");
logger.info("info");
logger.debug("debug");
const logs = getCapturedLogs();
// Error should be red
assert(logs[0].includes('\x1b[91m'));
assert(logs[0].includes("\x1b[91m"));
// Warn should be yellow
assert(logs[1].includes('\x1b[33m'));
assert(logs[1].includes("\x1b[33m"));
// Info and debug might have different or no colors, but should have reset codes
assert(logs[2].includes('\x1b[0m'));
assert(logs[3].includes('\x1b[0m'));
assert(logs[2].includes("\x1b[0m"));
assert(logs[3].includes("\x1b[0m"));
});
Deno.test("Logger Simple Formatter - Color Handling - should respect custom color configuration", () => {
clearCapturedLogs();
setTTYMode(true);
const logger = new Logger({
format: 'simple',
format: "simple",
colours: {
error: '\x1b[31m', // different red
warn: '\x1b[35m', // magenta instead of yellow
error: "\x1b[31m", // different red
warn: "\x1b[35m", // magenta instead of yellow
},
});
logger.error('error message');
logger.warn('warn message');
logger.error("error message");
logger.warn("warn message");
const logs = getCapturedLogs();
assert(logs[0].includes('\x1b[31m'));
assert(logs[1].includes('\x1b[35m'));
assert(logs[0].includes("\x1b[31m"));
assert(logs[1].includes("\x1b[35m"));
});
Deno.test("Logger Simple Formatter - Message Formatting in Simple Mode - should handle multiple arguments", () => {
clearCapturedLogs();
const logger = new Logger({ format: 'simple' });
logger.info('Hello %s, you are %d years old', 'John', 25);
const logger = new Logger({ format: "simple" });
logger.info("Hello %s, you are %d years old", "John", 25);
const logOutput = getCapturedLogs()[0];
assert(logOutput.includes('Hello John, you are 25 years old'));
assert(logOutput.includes("Hello John, you are 25 years old"));
});
Deno.test("Logger Simple Formatter - Message Formatting in Simple Mode - should handle special characters", () => {
clearCapturedLogs();
const logger = new Logger({ format: 'simple' });
const logger = new Logger({ format: "simple" });
logger.info('Special chars: "quotes", \\backslash, \nnewline');
const logOutput = getCapturedLogs()[0];
@@ -185,12 +185,12 @@ Deno.test("Logger Simple Formatter - Message Formatting in Simple Mode - should
Deno.test("Logger Simple Formatter - Message Formatting in Simple Mode - should handle empty messages", () => {
clearCapturedLogs();
const logger = new Logger({ format: 'simple' });
logger.info('');
const logger = new Logger({ format: "simple" });
logger.info("");
const logOutput = getCapturedLogs()[0];
// Should still have the level and timestamp parts
assert(logOutput.includes('[INFO ]'));
assert(logOutput.includes("[INFO ]"));
});
Deno.test("Logger Simple Formatter - TTY Detection Integration - should detect TTY mode changes correctly", () => {
@@ -198,19 +198,19 @@ Deno.test("Logger Simple Formatter - TTY Detection Integration - should detect T
// Test with TTY
setTTYMode(true);
const ttyLogger = new Logger({ format: 'simple' });
ttyLogger.error('tty error');
const ttyLogger = new Logger({ format: "simple" });
ttyLogger.error("tty error");
// Test without TTY
setTTYMode(false);
const noTtyLogger = new Logger({ format: 'simple' });
noTtyLogger.error('no tty error');
const noTtyLogger = new Logger({ format: "simple" });
noTtyLogger.error("no tty error");
const logs = getCapturedLogs();
// First should have colors, second should not
assert(logs[0].includes('\x1b['));
assert(!logs[1].includes('\x1b['));
assert(logs[0].includes("\x1b["));
assert(!logs[1].includes("\x1b["));
});
// Cleanup

View File

@@ -1,43 +1,43 @@
import { assertEquals, assertThrows, assert } from "@std/assert";
import Logger from '../lib/logger.ts';
import { assert, assertEquals, assertThrows } from "@std/assert";
import Logger from "../lib/logger.ts";
import {
setupMocks,
getCapturedLogs,
clearCapturedLogs,
getCapturedLogs,
getFirstLogAsJSON,
} from './helpers/logger-test-helpers.js';
setupMocks,
} from "./helpers/logger-test-helpers.js";
// Setup and teardown for all tests
setupMocks();
Deno.test("Logger Time Formatting - Default Time Format - should default to short time format", () => {
clearCapturedLogs();
const logger = new Logger({ format: 'json' });
logger.info('test message');
const logger = new Logger({ format: "json" });
logger.info("test message");
const parsed = getFirstLogAsJSON();
// Short format should be YYYY-MM-DD HH:MM (without seconds)
assert(parsed.time.match(/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}$/));
assert(!parsed.time.includes('T'));
assert(!parsed.time.includes('Z'));
assert(!parsed.time.includes('.'));
assert(!parsed.time.includes("T"));
assert(!parsed.time.includes("Z"));
assert(!parsed.time.includes("."));
});
Deno.test("Logger Time Formatting - Default Time Format - should include time option in logger options", () => {
const shortLogger = new Logger({ time: 'short' });
const longLogger = new Logger({ time: 'long' });
const shortLogger = new Logger({ time: "short" });
const longLogger = new Logger({ time: "long" });
const defaultLogger = new Logger();
assertEquals(shortLogger.options.time, 'short');
assertEquals(longLogger.options.time, 'long');
assertEquals(defaultLogger.options.time, 'short'); // default
assertEquals(shortLogger.options.time, "short");
assertEquals(longLogger.options.time, "long");
assertEquals(defaultLogger.options.time, "short"); // default
});
Deno.test("Logger Time Formatting - Short Time Format - should format time as short when time option is 'short'", () => {
clearCapturedLogs();
const logger = new Logger({ time: 'short', format: 'json' });
logger.info('test message');
const logger = new Logger({ time: "short", format: "json" });
logger.info("test message");
const parsed = getFirstLogAsJSON();
@@ -48,30 +48,30 @@ Deno.test("Logger Time Formatting - Short Time Format - should format time as sh
Deno.test("Logger Time Formatting - Short Time Format - should work with simple formatter and short time", () => {
clearCapturedLogs();
const logger = new Logger({ time: 'short', format: 'simple' });
logger.info('test message');
const logger = new Logger({ time: "short", format: "simple" });
logger.info("test message");
const logOutput = getCapturedLogs()[0];
// Should contain short time format in brackets
assert(logOutput.match(/\[\d{4}-\d{2}-\d{2} \d{2}:\d{2}\]/));
assert(!logOutput.includes('T'));
assert(!logOutput.includes('Z'));
assert(!logOutput.includes("T"));
assert(!logOutput.includes("Z"));
});
Deno.test("Logger Time Formatting - Short Time Format - should truncate time correctly in short format", () => {
clearCapturedLogs();
const logger = new Logger({ time: 'short', format: 'json' });
const logger = new Logger({ time: "short", format: "json" });
logger.info('test message');
logger.info("test message");
const parsed = getFirstLogAsJSON();
// Short format should not have seconds or milliseconds
assert(
!parsed.time.includes(':') || parsed.time.split(':').length === 2
!parsed.time.includes(":") || parsed.time.split(":").length === 2,
);
assert(!parsed.time.includes('.'));
assert(!parsed.time.includes("."));
// Should be exactly 16 characters: YYYY-MM-DD HH:MM
assertEquals(parsed.time.length, 16);
@@ -79,46 +79,46 @@ Deno.test("Logger Time Formatting - Short Time Format - should truncate time cor
Deno.test("Logger Time Formatting - Long Time Format - should format time as long ISO string when time is 'long'", () => {
clearCapturedLogs();
const logger = new Logger({ time: 'long', format: 'json' });
logger.info('test message');
const logger = new Logger({ time: "long", format: "json" });
logger.info("test message");
const parsed = getFirstLogAsJSON();
// Long format should be full ISO string
assert(parsed.time.includes('T'));
assert(parsed.time.includes('Z'));
assert(parsed.time.includes('.'));
assert(parsed.time.includes("T"));
assert(parsed.time.includes("Z"));
assert(parsed.time.includes("."));
// Should be valid ISO string
new Date(parsed.time); // This should not throw
// Should match ISO format pattern
assert(
parsed.time.match(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/)
parsed.time.match(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/),
);
});
Deno.test("Logger Time Formatting - Long Time Format - should work with simple formatter and long time", () => {
clearCapturedLogs();
const logger = new Logger({ time: 'long', format: 'simple' });
logger.info('test message');
const logger = new Logger({ time: "long", format: "simple" });
logger.info("test message");
const logOutput = getCapturedLogs()[0];
// Should contain long time format in brackets
assert(
logOutput.match(/\[\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z\]/)
logOutput.match(/\[\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z\]/),
);
assert(logOutput.includes('T'));
assert(logOutput.includes('Z'));
assert(logOutput.includes("T"));
assert(logOutput.includes("Z"));
});
Deno.test("Logger Time Formatting - Long Time Format - should preserve time precision in long format", () => {
clearCapturedLogs();
const logger = new Logger({ time: 'long', format: 'json' });
const logger = new Logger({ time: "long", format: "json" });
const startTime = Date.now();
logger.info('test message');
logger.info("test message");
const endTime = Date.now();
const parsed = getFirstLogAsJSON();
@@ -129,15 +129,15 @@ Deno.test("Logger Time Formatting - Long Time Format - should preserve time prec
assert(logTime <= endTime);
// Should have millisecond precision
assert(parsed.time.includes('.'));
assert(parsed.time.includes("."));
});
Deno.test("Logger Time Formatting - Time Format Consistency - should use consistent time format across multiple log calls", () => {
clearCapturedLogs();
const logger = new Logger({ time: 'short', format: 'json' });
const logger = new Logger({ time: "short", format: "json" });
logger.info('first message');
logger.warn('second message');
logger.info("first message");
logger.warn("second message");
const logs = getCapturedLogs();
const parsed1 = JSON.parse(logs[0]);
@@ -150,34 +150,42 @@ Deno.test("Logger Time Formatting - Time Format Consistency - should use consist
Deno.test("Logger Time Formatting - Time Option Validation - should validate time option in constructor", () => {
// Valid options should not throw
new Logger({ time: 'long' });
new Logger({ time: 'short' });
new Logger({ time: "long" });
new Logger({ time: "short" });
// Invalid option should throw
assertThrows(() => {
new Logger({ time: 'medium' });
}, Error, "Invalid time: medium. Valid times are: long, short");
assertThrows(
() => {
new Logger({ time: "medium" });
},
Error,
"Invalid time: medium. Valid times are: long, short",
);
assertThrows(() => {
new Logger({ time: 'invalid' });
}, Error, "Invalid time: invalid. Valid times are: long, short");
assertThrows(
() => {
new Logger({ time: "invalid" });
},
Error,
"Invalid time: invalid. Valid times are: long, short",
);
});
Deno.test("Logger Time Formatting - Backward Compatibility - should maintain existing behavior for existing code", () => {
clearCapturedLogs();
// Code that doesn't specify time option should work as before
const logger = new Logger({ format: 'json', level: 'info' });
logger.info('test message');
const logger = new Logger({ format: "json", level: "info" });
logger.info("test message");
const parsed = getFirstLogAsJSON();
// Should still have all expected fields
assertEquals(parsed.level, 'info');
assertEquals(parsed.msg, 'test message');
assertEquals(typeof parsed.time, 'string');
assertEquals(typeof parsed.pid, 'number');
assertEquals(typeof parsed.hostname, 'string');
assertEquals(parsed.level, "info");
assertEquals(parsed.msg, "test message");
assertEquals(typeof parsed.time, "string");
assertEquals(typeof parsed.pid, "number");
assertEquals(typeof parsed.hostname, "string");
// Time should be in short format (new default)
assert(parsed.time.match(/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}$/));
@@ -185,15 +193,15 @@ Deno.test("Logger Time Formatting - Backward Compatibility - should maintain exi
Deno.test("Logger Time Formatting - Backward Compatibility - should not break existing simple formatter tests", () => {
clearCapturedLogs();
const logger = new Logger({ format: 'simple' });
logger.warn('warning message');
const logger = new Logger({ format: "simple" });
logger.warn("warning message");
const logOutput = getCapturedLogs()[0];
// Should still contain expected elements
assert(logOutput.includes('[WARN ]'));
assert(logOutput.includes('warning message'));
assert(logOutput.includes('.js:'));
assert(logOutput.includes("[WARN ]"));
assert(logOutput.includes("warning message"));
assert(logOutput.includes(".js:"));
// Should use short time format (new default)
assert(logOutput.match(/\[\d{4}-\d{2}-\d{2} \d{2}:\d{2}\]/));

View File

@@ -1,89 +1,89 @@
import { assertEquals, assertMatch } from "@std/assert";
import Logger from '../lib/logger.ts';
import Logger from "../lib/logger.ts";
Deno.test("Logger util.format functionality - Format specifiers - should handle %s string formatting", () => {
let capturedOutput = '';
let capturedOutput = "";
const originalLog = console.log;
console.log = (message) => {
capturedOutput = message;
};
try {
const logger = new Logger({ format: 'json' });
logger.info('User %s logged in', 'john');
const logger = new Logger({ format: "json" });
logger.info("User %s logged in", "john");
const output = JSON.parse(capturedOutput);
assertEquals(output.msg, 'User john logged in');
assertEquals(output.msg, "User john logged in");
} finally {
console.log = originalLog;
}
});
Deno.test("Logger util.format functionality - Format specifiers - should handle %d number formatting", () => {
let capturedOutput = '';
let capturedOutput = "";
const originalLog = console.log;
console.log = (message) => {
capturedOutput = message;
};
try {
const logger = new Logger({ format: 'json' });
logger.info('User has %d points', 100);
const logger = new Logger({ format: "json" });
logger.info("User has %d points", 100);
const output = JSON.parse(capturedOutput);
assertEquals(output.msg, 'User has 100 points');
assertEquals(output.msg, "User has 100 points");
} finally {
console.log = originalLog;
}
});
Deno.test("Logger util.format functionality - Format specifiers - should handle %i integer formatting", () => {
let capturedOutput = '';
let capturedOutput = "";
const originalLog = console.log;
console.log = (message) => {
capturedOutput = message;
};
try {
const logger = new Logger({ format: 'json' });
logger.info('Value: %i', 42.7);
const logger = new Logger({ format: "json" });
logger.info("Value: %i", 42.7);
const output = JSON.parse(capturedOutput);
assertEquals(output.msg, 'Value: 42');
assertEquals(output.msg, "Value: 42");
} finally {
console.log = originalLog;
}
});
Deno.test("Logger util.format functionality - Format specifiers - should handle %f float formatting", () => {
let capturedOutput = '';
let capturedOutput = "";
const originalLog = console.log;
console.log = (message) => {
capturedOutput = message;
};
try {
const logger = new Logger({ format: 'json' });
logger.info('Price: %f', 19.99);
const logger = new Logger({ format: "json" });
logger.info("Price: %f", 19.99);
const output = JSON.parse(capturedOutput);
assertEquals(output.msg, 'Price: 19.99');
assertEquals(output.msg, "Price: 19.99");
} finally {
console.log = originalLog;
}
});
Deno.test("Logger util.format functionality - Format specifiers - should handle %j JSON formatting", () => {
let capturedOutput = '';
let capturedOutput = "";
const originalLog = console.log;
console.log = (message) => {
capturedOutput = message;
};
try {
const logger = new Logger({ format: 'json' });
const obj = { name: 'test', value: 42 };
logger.info('Config: %j', obj);
const logger = new Logger({ format: "json" });
const obj = { name: "test", value: 42 };
logger.info("Config: %j", obj);
const output = JSON.parse(capturedOutput);
assertEquals(output.msg, 'Config: {"name":"test","value":42}');
@@ -93,43 +93,43 @@ Deno.test("Logger util.format functionality - Format specifiers - should handle
});
Deno.test("Logger util.format functionality - Format specifiers - should handle %% literal percentage", () => {
let capturedOutput = '';
let capturedOutput = "";
const originalLog = console.log;
console.log = (message) => {
capturedOutput = message;
};
try {
const logger = new Logger({ format: 'json' });
logger.info('Progress: 50%% complete');
const logger = new Logger({ format: "json" });
logger.info("Progress: 50%% complete");
const output = JSON.parse(capturedOutput);
assertEquals(output.msg, 'Progress: 50%% complete');
assertEquals(output.msg, "Progress: 50%% complete");
} finally {
console.log = originalLog;
}
});
Deno.test("Logger util.format functionality - Multiple format specifiers - should handle multiple format specifiers", () => {
let capturedOutput = '';
let capturedOutput = "";
const originalLog = console.log;
console.log = (message) => {
capturedOutput = message;
};
try {
const logger = new Logger({ format: 'json' });
const logger = new Logger({ format: "json" });
logger.info(
'User %s has %d points and %f%% completion',
'alice',
"User %s has %d points and %f%% completion",
"alice",
150,
75.5
75.5,
);
const output = JSON.parse(capturedOutput);
assertEquals(
output.msg,
'User alice has 150 points and 75.5% completion'
"User alice has 150 points and 75.5% completion",
);
} finally {
console.log = originalLog;
@@ -137,26 +137,26 @@ Deno.test("Logger util.format functionality - Multiple format specifiers - shoul
});
Deno.test("Logger util.format functionality - Multiple format specifiers - should handle mixed format specifiers with JSON", () => {
let capturedOutput = '';
let capturedOutput = "";
const originalLog = console.log;
console.log = (message) => {
capturedOutput = message;
};
try {
const logger = new Logger({ format: 'json' });
const logger = new Logger({ format: "json" });
const config = { debug: true, port: 3000 };
logger.info(
'Server %s running on port %d with config %j',
'api',
"Server %s running on port %d with config %j",
"api",
8080,
config
config,
);
const output = JSON.parse(capturedOutput);
assertEquals(
output.msg,
'Server api running on port 8080 with config {"debug":true,"port":3000}'
'Server api running on port 8080 with config {"debug":true,"port":3000}',
);
} finally {
console.log = originalLog;
@@ -164,34 +164,34 @@ Deno.test("Logger util.format functionality - Multiple format specifiers - shoul
});
Deno.test("Logger util.format functionality - Multiple arguments without format specifiers - should handle multiple arguments without format specifiers", () => {
let capturedOutput = '';
let capturedOutput = "";
const originalLog = console.log;
console.log = (message) => {
capturedOutput = message;
};
try {
const logger = new Logger({ format: 'json' });
logger.info('Message', 'arg1', 'arg2', 123);
const logger = new Logger({ format: "json" });
logger.info("Message", "arg1", "arg2", 123);
const output = JSON.parse(capturedOutput);
assertEquals(output.msg, 'Message arg1 arg2 123');
assertEquals(output.msg, "Message arg1 arg2 123");
} finally {
console.log = originalLog;
}
});
Deno.test("Logger util.format functionality - Multiple arguments without format specifiers - should handle mixed objects and primitives", () => {
let capturedOutput = '';
let capturedOutput = "";
const originalLog = console.log;
console.log = (message) => {
capturedOutput = message;
};
try {
const logger = new Logger({ format: 'json' });
const obj = { key: 'value' };
logger.info('Data:', obj, 42, true);
const logger = new Logger({ format: "json" });
const obj = { key: "value" };
logger.info("Data:", obj, 42, true);
const output = JSON.parse(capturedOutput);
assertEquals(output.msg, "Data: { key: 'value' } 42 true");
@@ -201,89 +201,89 @@ Deno.test("Logger util.format functionality - Multiple arguments without format
});
Deno.test("Logger util.format functionality - Edge cases - should handle more format specifiers than arguments", () => {
let capturedOutput = '';
let capturedOutput = "";
const originalLog = console.log;
console.log = (message) => {
capturedOutput = message;
};
try {
const logger = new Logger({ format: 'json' });
logger.info('Hello %s, you are %d years old', 'John');
const logger = new Logger({ format: "json" });
logger.info("Hello %s, you are %d years old", "John");
const output = JSON.parse(capturedOutput);
assertEquals(output.msg, 'Hello John, you are %d years old');
assertEquals(output.msg, "Hello John, you are %d years old");
} finally {
console.log = originalLog;
}
});
Deno.test("Logger util.format functionality - Edge cases - should handle more arguments than format specifiers", () => {
let capturedOutput = '';
let capturedOutput = "";
const originalLog = console.log;
console.log = (message) => {
capturedOutput = message;
};
try {
const logger = new Logger({ format: 'json' });
logger.info('Hello %s', 'John', 'extra', 'args', 123);
const logger = new Logger({ format: "json" });
logger.info("Hello %s", "John", "extra", "args", 123);
const output = JSON.parse(capturedOutput);
assertEquals(output.msg, 'Hello John extra args 123');
assertEquals(output.msg, "Hello John extra args 123");
} finally {
console.log = originalLog;
}
});
Deno.test("Logger util.format functionality - Edge cases - should handle null and undefined values", () => {
let capturedOutput = '';
let capturedOutput = "";
const originalLog = console.log;
console.log = (message) => {
capturedOutput = message;
};
try {
const logger = new Logger({ format: 'json' });
logger.info('Values: %s %s %d', null, undefined, null);
const logger = new Logger({ format: "json" });
logger.info("Values: %s %s %d", null, undefined, null);
const output = JSON.parse(capturedOutput);
assertEquals(output.msg, 'Values: null undefined 0');
assertEquals(output.msg, "Values: null undefined 0");
} finally {
console.log = originalLog;
}
});
Deno.test("Logger util.format functionality - Edge cases - should handle arrays and objects without %j", () => {
let capturedOutput = '';
let capturedOutput = "";
const originalLog = console.log;
console.log = (message) => {
capturedOutput = message;
};
try {
const logger = new Logger({ format: 'json' });
const logger = new Logger({ format: "json" });
const arr = [1, 2, 3];
const obj = { a: 1 };
logger.info('Data %s and %s', arr, obj);
logger.info("Data %s and %s", arr, obj);
const output = JSON.parse(capturedOutput);
assertEquals(output.msg, 'Data [ 1, 2, 3 ] and { a: 1 }');
assertEquals(output.msg, "Data [ 1, 2, 3 ] and { a: 1 }");
} finally {
console.log = originalLog;
}
});
Deno.test("Logger util.format functionality - Simple format output - should format messages correctly in simple format", () => {
let capturedOutput = '';
let capturedOutput = "";
const originalLog = console.log;
console.log = (message) => {
capturedOutput = message;
};
try {
const logger = new Logger({ format: 'simple' });
logger.info('User %s has %d points', 'bob', 200);
const logger = new Logger({ format: "simple" });
logger.info("User %s has %d points", "bob", 200);
assertMatch(capturedOutput, /User bob has 200 points/);
} finally {
@@ -292,16 +292,16 @@ Deno.test("Logger util.format functionality - Simple format output - should form
});
Deno.test("Logger util.format functionality - Simple format output - should handle JSON formatting in simple format", () => {
let capturedOutput = '';
let capturedOutput = "";
const originalLog = console.log;
console.log = (message) => {
capturedOutput = message;
};
try {
const logger = new Logger({ format: 'simple' });
const data = { status: 'active', count: 5 };
logger.warn('Status: %j', data);
const logger = new Logger({ format: "simple" });
const data = { status: "active", count: 5 };
logger.warn("Status: %j", data);
assertMatch(capturedOutput, /Status: {"status":"active","count":5}/);
} finally {
@@ -310,44 +310,44 @@ Deno.test("Logger util.format functionality - Simple format output - should hand
});
Deno.test("Logger util.format functionality - Error handling in util.format - should handle objects that throw during toString", () => {
let capturedOutput = '';
let capturedOutput = "";
const originalLog = console.log;
console.log = (message) => {
capturedOutput = message;
};
try {
const logger = new Logger({ format: 'json' });
const logger = new Logger({ format: "json" });
const problematicObj = {
toString() {
throw new Error('toString failed');
throw new Error("toString failed");
},
};
// The logger might handle the error gracefully, so let's test the actual output
logger.info('Object: %s', problematicObj);
logger.info("Object: %s", problematicObj);
// Check that something was logged (the logger should handle the error)
const output = JSON.parse(capturedOutput);
assertEquals(typeof output.msg, 'string');
assertEquals(typeof output.msg, "string");
} finally {
console.log = originalLog;
}
});
Deno.test("Logger util.format functionality - Error handling in util.format - should handle circular references with %j", () => {
let capturedOutput = '';
let capturedOutput = "";
const originalLog = console.log;
console.log = (message) => {
capturedOutput = message;
};
try {
const logger = new Logger({ format: 'json' });
const circular = { name: 'test' };
const logger = new Logger({ format: "json" });
const circular = { name: "test" };
circular.self = circular;
logger.info('Circular: %j', circular);
logger.info("Circular: %j", circular);
const output = JSON.parse(capturedOutput);
assertMatch(output.msg, /Circular: \[Circular\]/);
@@ -355,4 +355,3 @@ Deno.test("Logger util.format functionality - Error handling in util.format - sh
console.log = originalLog;
}
});