initial
This commit is contained in:
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Ian Bailey
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
225
README.md
Normal file
225
README.md
Normal file
@@ -0,0 +1,225 @@
|
||||
# logger [](https://www.npmjs.com/package/@iankulin/logger) [](https://npmjs.org/package/@iankulin/logger)
|
||||
|
||||
> Flexible console logging utility with colors, and multiple output formats
|
||||
|
||||
## Features
|
||||
|
||||
- **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
|
||||
- **Color support**: Automatic TTY detection with colored output
|
||||
- **ESM only**: Modern ES module support
|
||||
|
||||
## Install
|
||||
|
||||
Install with [npm](https://npmjs.org/package/@iankulin/logger):
|
||||
|
||||
```sh
|
||||
$ npm install @iankulin/logger
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
```js
|
||||
import Logger from '@iankulin/logger';
|
||||
|
||||
const logger = new Logger();
|
||||
logger.info('Hello from logger');
|
||||
logger.error('Something went wrong');
|
||||
```
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Basic Logging
|
||||
|
||||
```js
|
||||
import Logger from '@iankulin/logger';
|
||||
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')
|
||||
```
|
||||
|
||||
### Log Levels
|
||||
|
||||
The logger supports five log levels (from least to most verbose):
|
||||
|
||||
- `silent` - Suppresses all output
|
||||
- `error` - Only error messages
|
||||
- `warn` - Error and warning messages
|
||||
- `info` - Error, warning, and info messages (default)
|
||||
- `debug` - All messages
|
||||
|
||||
```js
|
||||
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');
|
||||
|
||||
// Change level dynamically
|
||||
logger.level('error');
|
||||
logger.info('This will not be logged');
|
||||
|
||||
// Get current level
|
||||
console.log(logger.level()); // 'error'
|
||||
```
|
||||
|
||||
### Output Formats
|
||||
|
||||
#### JSON Format (Default)
|
||||
|
||||
```js
|
||||
const logger = new Logger({ format: 'json' });
|
||||
logger.info('Hello world');
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"level": "info",
|
||||
"levelNumber": 2,
|
||||
"time": "2025-06-02T12:00:00.000Z",
|
||||
"pid": 12345,
|
||||
"hostname": "my-computer",
|
||||
"msg": "Hello world",
|
||||
"callerFile": "file:///path/to/file.js",
|
||||
"callerLine": 3
|
||||
}
|
||||
```
|
||||
|
||||
#### Simple Format
|
||||
|
||||
```js
|
||||
const logger = new Logger({ format: 'simple' });
|
||||
logger.error('Something failed');
|
||||
```
|
||||
|
||||
```
|
||||
[2025-07-04 22:50] [ERROR] [basic.js:3] Something failed
|
||||
```
|
||||
|
||||
### Message Formatting
|
||||
|
||||
The logger uses Node.js `util.format()` for message formatting with placeholders like `%s`, `%d`, `%j`.
|
||||
|
||||
```js
|
||||
const logger = new Logger({ format: 'json' });
|
||||
|
||||
// String formatting
|
||||
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 });
|
||||
// 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);
|
||||
// Output: [2025-07-05 10:30] [WARN ] [app.js:15] Processing file data.txt (1024 bytes)
|
||||
```
|
||||
|
||||
### Custom Colors
|
||||
|
||||
```js
|
||||
const logger = new Logger({
|
||||
colours: {
|
||||
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.
|
||||
|
||||
```js
|
||||
// Default: only include caller info for warnings and errors
|
||||
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
|
||||
```
|
||||
|
||||
**JSON Format Output:**
|
||||
|
||||
```json
|
||||
{"level":"error","msg":"Critical error","callerFile":"/path/to/file.js","callerLine":42}
|
||||
{"level":"info","msg":"Info message"}
|
||||
```
|
||||
|
||||
**Simple Format Output:**
|
||||
|
||||
```
|
||||
[2025-07-04 13:13] [ERROR] [app.js:42] Critical error
|
||||
[2025-07-04 13:13] [INFO ] Info message
|
||||
```
|
||||
|
||||
**Available callerLevel Options:**
|
||||
|
||||
- `'silent'` - Never include caller info (best performance)
|
||||
- `'error'` - Only include caller info for errors
|
||||
- `'warn'` - Include caller info for warnings and errors (default)
|
||||
- `'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.
|
||||
|
||||
## Constructor Options
|
||||
|
||||
| Option | Type | Default | Description |
|
||||
| ------------- | ------ | --------- | ----------------------------------------------------------------------------------------------- |
|
||||
| `level` | string | `'info'` | Minimum log level to output (`'silent'`, `'error'`, `'warn'`, `'info'`, `'debug'`) |
|
||||
| `format` | string | `'json'` | Output format (`'json'` or `'simple'`) |
|
||||
| `callerLevel` | string | `'warn'` | Minimum log level to include caller info (`'silent'`, `'error'`, `'warn'`, `'info'`, `'debug'`) |
|
||||
| `colours` | object | See below | Color codes for each log level |
|
||||
| `levels` | object | See below | Custom level names and numeric values |
|
||||
|
||||
## Common Usage Patterns
|
||||
|
||||
```js
|
||||
import Logger from '@iankulin/logger';
|
||||
|
||||
// Production: JSON format with environment-based level
|
||||
const prodLogger = new Logger({
|
||||
level: process.env.LOG_LEVEL || 'info',
|
||||
format: 'json',
|
||||
callerLevel: 'error', // Performance optimization
|
||||
});
|
||||
|
||||
// Development: Simple format with debug level
|
||||
const devLogger = new Logger({
|
||||
level: 'debug',
|
||||
format: 'simple',
|
||||
});
|
||||
|
||||
// Testing: Silent mode
|
||||
const testLogger = new Logger({ level: 'silent' });
|
||||
```
|
||||
|
||||
## Requirements
|
||||
|
||||
- Node.js 18.0.0 or higher
|
||||
- ES modules support
|
||||
|
||||
## License
|
||||
|
||||
[MIT](LICENSE)
|
||||
|
||||
## Versions
|
||||
|
||||
- **0.1.5** - Initial release
|
||||
- **0.1.6** - Added tests, improved error handling, caller detection loop prevention, `silent` logging level
|
||||
- **1.0.0** - Production release
|
||||
- **1.0.2** - Added types for intellisense
|
||||
- **1.1.0** - added { time: 'short' } option, refactor tests, added { callerLevel: 'warn' } option
|
||||
- **1.1.2** - dependencies update following [chalk supply chain attack](https://www.bleepingcomputer.com/news/security/self-propagating-supply-chain-attack-hits-187-npm-packages/) although not affected.
|
||||
94
demo.js
Normal file
94
demo.js
Normal file
@@ -0,0 +1,94 @@
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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');
|
||||
|
||||
// Demonstrate callerLevel functionality
|
||||
console.log('\n=== Caller Level Demo ===');
|
||||
|
||||
// Default callerLevel is 'warn' - only errors and warnings include caller info
|
||||
const defaultCallerLogger = new Logger({ format: 'simple' });
|
||||
console.log(
|
||||
'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');
|
||||
|
||||
// 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');
|
||||
|
||||
// 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');
|
||||
|
||||
// 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');
|
||||
|
||||
// Demonstrate format string functionality (util.format style)
|
||||
console.log('\n=== Format String Demo ===');
|
||||
|
||||
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');
|
||||
|
||||
// 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);
|
||||
|
||||
// JSON formatting (%j)
|
||||
const user = { name: 'Alice', role: 'admin', active: true };
|
||||
const config = { timeout: 5000, retries: 3 };
|
||||
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);
|
||||
|
||||
// Multiple arguments without format specifiers
|
||||
formatLogger.warn('System alert:', 'High memory usage detected', { usage: '89%', threshold: '80%' });
|
||||
|
||||
// Literal percentage with %%
|
||||
formatLogger.info('Upload progress: 50%% complete');
|
||||
|
||||
// Edge cases
|
||||
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 });
|
||||
12
deno.json
Normal file
12
deno.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"imports": {
|
||||
"@std/assert": "https://deno.land/std@0.224.0/assert/mod.ts",
|
||||
"@std/testing": "https://deno.land/std@0.224.0/testing/mock.ts"
|
||||
},
|
||||
"tasks": {
|
||||
"dev": "deno run --allow-env --allow-sys demo.js",
|
||||
"test": "deno test --allow-env --allow-sys",
|
||||
"lint": "deno lint",
|
||||
"check": "deno check lib/logger.ts"
|
||||
}
|
||||
}
|
||||
52
deno.lock
generated
Normal file
52
deno.lock
generated
Normal file
@@ -0,0 +1,52 @@
|
||||
{
|
||||
"version": "5",
|
||||
"specifiers": {
|
||||
"npm:@types/node@*": "24.2.0"
|
||||
},
|
||||
"npm": {
|
||||
"@types/node@24.2.0": {
|
||||
"integrity": "sha512-3xyG3pMCq3oYCNg7/ZP+E1ooTaGB4cG8JWRsqqOYQdbWNY4zbaV0Ennrd7stjiJEFZCaybcIgpTjJWHRfBSIDw==",
|
||||
"dependencies": [
|
||||
"undici-types"
|
||||
]
|
||||
},
|
||||
"undici-types@7.10.0": {
|
||||
"integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="
|
||||
}
|
||||
},
|
||||
"remote": {
|
||||
"https://deno.land/std@0.224.0/assert/_constants.ts": "a271e8ef5a573f1df8e822a6eb9d09df064ad66a4390f21b3e31f820a38e0975",
|
||||
"https://deno.land/std@0.224.0/assert/assert.ts": "09d30564c09de846855b7b071e62b5974b001bb72a4b797958fe0660e7849834",
|
||||
"https://deno.land/std@0.224.0/assert/assert_almost_equals.ts": "9e416114322012c9a21fa68e187637ce2d7df25bcbdbfd957cd639e65d3cf293",
|
||||
"https://deno.land/std@0.224.0/assert/assert_array_includes.ts": "14c5094471bc8e4a7895fc6aa5a184300d8a1879606574cb1cd715ef36a4a3c7",
|
||||
"https://deno.land/std@0.224.0/assert/assert_equals.ts": "3bbca947d85b9d374a108687b1a8ba3785a7850436b5a8930d81f34a32cb8c74",
|
||||
"https://deno.land/std@0.224.0/assert/assert_exists.ts": "43420cf7f956748ae6ed1230646567b3593cb7a36c5a5327269279c870c5ddfd",
|
||||
"https://deno.land/std@0.224.0/assert/assert_false.ts": "3e9be8e33275db00d952e9acb0cd29481a44fa0a4af6d37239ff58d79e8edeff",
|
||||
"https://deno.land/std@0.224.0/assert/assert_greater.ts": "5e57b201fd51b64ced36c828e3dfd773412c1a6120c1a5a99066c9b261974e46",
|
||||
"https://deno.land/std@0.224.0/assert/assert_greater_or_equal.ts": "9870030f997a08361b6f63400273c2fb1856f5db86c0c3852aab2a002e425c5b",
|
||||
"https://deno.land/std@0.224.0/assert/assert_instance_of.ts": "e22343c1fdcacfaea8f37784ad782683ec1cf599ae9b1b618954e9c22f376f2c",
|
||||
"https://deno.land/std@0.224.0/assert/assert_is_error.ts": "f856b3bc978a7aa6a601f3fec6603491ab6255118afa6baa84b04426dd3cc491",
|
||||
"https://deno.land/std@0.224.0/assert/assert_less.ts": "60b61e13a1982865a72726a5fa86c24fad7eb27c3c08b13883fb68882b307f68",
|
||||
"https://deno.land/std@0.224.0/assert/assert_less_or_equal.ts": "d2c84e17faba4afe085e6c9123a63395accf4f9e00150db899c46e67420e0ec3",
|
||||
"https://deno.land/std@0.224.0/assert/assert_match.ts": "ace1710dd3b2811c391946954234b5da910c5665aed817943d086d4d4871a8b7",
|
||||
"https://deno.land/std@0.224.0/assert/assert_not_equals.ts": "78d45dd46133d76ce624b2c6c09392f6110f0df9b73f911d20208a68dee2ef29",
|
||||
"https://deno.land/std@0.224.0/assert/assert_not_instance_of.ts": "3434a669b4d20cdcc5359779301a0588f941ffdc2ad68803c31eabdb4890cf7a",
|
||||
"https://deno.land/std@0.224.0/assert/assert_not_match.ts": "df30417240aa2d35b1ea44df7e541991348a063d9ee823430e0b58079a72242a",
|
||||
"https://deno.land/std@0.224.0/assert/assert_not_strict_equals.ts": "37f73880bd672709373d6dc2c5f148691119bed161f3020fff3548a0496f71b8",
|
||||
"https://deno.land/std@0.224.0/assert/assert_object_match.ts": "411450fd194fdaabc0089ae68f916b545a49d7b7e6d0026e84a54c9e7eed2693",
|
||||
"https://deno.land/std@0.224.0/assert/assert_rejects.ts": "4bee1d6d565a5b623146a14668da8f9eb1f026a4f338bbf92b37e43e0aa53c31",
|
||||
"https://deno.land/std@0.224.0/assert/assert_strict_equals.ts": "b4f45f0fd2e54d9029171876bd0b42dd9ed0efd8f853ab92a3f50127acfa54f5",
|
||||
"https://deno.land/std@0.224.0/assert/assert_string_includes.ts": "496b9ecad84deab72c8718735373feb6cdaa071eb91a98206f6f3cb4285e71b8",
|
||||
"https://deno.land/std@0.224.0/assert/assert_throws.ts": "c6508b2879d465898dab2798009299867e67c570d7d34c90a2d235e4553906eb",
|
||||
"https://deno.land/std@0.224.0/assert/assertion_error.ts": "ba8752bd27ebc51f723702fac2f54d3e94447598f54264a6653d6413738a8917",
|
||||
"https://deno.land/std@0.224.0/assert/equal.ts": "bddf07bb5fc718e10bb72d5dc2c36c1ce5a8bdd3b647069b6319e07af181ac47",
|
||||
"https://deno.land/std@0.224.0/assert/fail.ts": "0eba674ffb47dff083f02ced76d5130460bff1a9a68c6514ebe0cdea4abadb68",
|
||||
"https://deno.land/std@0.224.0/assert/mod.ts": "48b8cb8a619ea0b7958ad7ee9376500fe902284bb36f0e32c598c3dc34cbd6f3",
|
||||
"https://deno.land/std@0.224.0/assert/unimplemented.ts": "8c55a5793e9147b4f1ef68cd66496b7d5ba7a9e7ca30c6da070c1a58da723d73",
|
||||
"https://deno.land/std@0.224.0/assert/unreachable.ts": "5ae3dbf63ef988615b93eb08d395dda771c96546565f9e521ed86f6510c29e19",
|
||||
"https://deno.land/std@0.224.0/fmt/colors.ts": "508563c0659dd7198ba4bbf87e97f654af3c34eb56ba790260f252ad8012e1c5",
|
||||
"https://deno.land/std@0.224.0/internal/diff.ts": "6234a4b493ebe65dc67a18a0eb97ef683626a1166a1906232ce186ae9f65f4e6",
|
||||
"https://deno.land/std@0.224.0/internal/format.ts": "0a98ee226fd3d43450245b1844b47003419d34d210fa989900861c79820d21c2",
|
||||
"https://deno.land/std@0.224.0/internal/mod.ts": "534125398c8e7426183e12dc255bb635d94e06d0f93c60a297723abe69d3b22e"
|
||||
}
|
||||
}
|
||||
483
lib/logger.ts
Normal file
483
lib/logger.ts
Normal file
@@ -0,0 +1,483 @@
|
||||
// Native implementation of util.format functionality
|
||||
function format(f: unknown, ...args: unknown[]): 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') {
|
||||
try {
|
||||
return JSON.stringify(arg, null, 0);
|
||||
} catch {
|
||||
// Handle circular references and other JSON errors
|
||||
return '[object Object]';
|
||||
}
|
||||
}
|
||||
return String(arg);
|
||||
}).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 str = f.replace(/%[sdifj%]/g, (match: string) => {
|
||||
if (match === '%%') {
|
||||
return handlePercentPercent;
|
||||
}
|
||||
|
||||
if (i >= args.length) return match;
|
||||
|
||||
const arg = args[i++];
|
||||
|
||||
switch (match) {
|
||||
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(', ')} ]`;
|
||||
}
|
||||
// For plain objects, show key-value pairs
|
||||
const entries = Object.entries(arg).map(([k, v]) => `${k}: ${typeof v === 'string' ? `'${v}'` : v}`);
|
||||
return `{ ${entries.join(', ')} }`;
|
||||
} catch {
|
||||
return '[object Object]';
|
||||
}
|
||||
}
|
||||
try {
|
||||
return String(arg);
|
||||
} catch {
|
||||
return '[object Object]';
|
||||
}
|
||||
|
||||
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';
|
||||
return String(parseInt(String(Number(arg)), 10));
|
||||
|
||||
case '%f':
|
||||
if (arg === null) return '0';
|
||||
if (arg === undefined) return 'NaN';
|
||||
return String(parseFloat(String(Number(arg))));
|
||||
|
||||
case '%j':
|
||||
try {
|
||||
return JSON.stringify(arg);
|
||||
} catch {
|
||||
return '[Circular]';
|
||||
}
|
||||
|
||||
default:
|
||||
return match;
|
||||
}
|
||||
});
|
||||
|
||||
// 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') {
|
||||
try {
|
||||
if (Array.isArray(arg)) {
|
||||
return `[ ${arg.join(', ')} ]`;
|
||||
}
|
||||
const entries = Object.entries(arg).map(([k, v]) => `${k}: ${typeof v === 'string' ? `'${v}'` : v}`);
|
||||
return `{ ${entries.join(', ')} }`;
|
||||
} catch {
|
||||
return '[object Object]';
|
||||
}
|
||||
}
|
||||
return String(arg);
|
||||
}).join(' ');
|
||||
}
|
||||
|
||||
return str;
|
||||
}
|
||||
|
||||
export type LogLevel = 'silent' | 'error' | 'warn' | 'info' | 'debug';
|
||||
|
||||
export interface LogLevels {
|
||||
[level: string]: number;
|
||||
silent: -1;
|
||||
error: 0;
|
||||
warn: 1;
|
||||
info: 2;
|
||||
debug: 3;
|
||||
}
|
||||
|
||||
export interface Colours {
|
||||
[level: string]: string;
|
||||
error: string;
|
||||
warn: string;
|
||||
info: string;
|
||||
debug: string;
|
||||
reset: string;
|
||||
}
|
||||
|
||||
export interface LogEntry {
|
||||
level: string;
|
||||
levelNumber: number;
|
||||
time: string;
|
||||
pid: number;
|
||||
hostname: string;
|
||||
msg: string;
|
||||
callerFile?: string;
|
||||
callerLine?: number;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export type Formatter = (logEntry: LogEntry) => string;
|
||||
|
||||
export interface LoggerOptions {
|
||||
level?: LogLevel;
|
||||
levels?: Partial<LogLevels>;
|
||||
format?: 'json' | 'simple';
|
||||
time?: 'long' | 'short';
|
||||
callerLevel?: LogLevel;
|
||||
colours?: Partial<Colours>;
|
||||
}
|
||||
|
||||
class Logger {
|
||||
options: {
|
||||
level: LogLevel;
|
||||
levels: LogLevels;
|
||||
format: 'json' | 'simple';
|
||||
time: 'long' | 'short';
|
||||
callerLevel: LogLevel;
|
||||
colours: Colours;
|
||||
};
|
||||
isRedirected: boolean;
|
||||
formatters: { [key: string]: Formatter };
|
||||
callerErrorCount: number;
|
||||
maxCallerErrors: number;
|
||||
|
||||
constructor(options: LoggerOptions = {}) {
|
||||
this.validateOptions(options);
|
||||
|
||||
const defaultLevels: LogLevels = {
|
||||
silent: -1,
|
||||
error: 0,
|
||||
warn: 1,
|
||||
info: 2,
|
||||
debug: 3,
|
||||
};
|
||||
|
||||
const defaultColours: Colours = {
|
||||
error: '\x1b[91m',
|
||||
warn: '\x1b[33m',
|
||||
info: '\x1b[94m',
|
||||
debug: '\x1b[37m',
|
||||
reset: '\x1b[0m',
|
||||
};
|
||||
|
||||
this.options = {
|
||||
level: options.level || 'info',
|
||||
levels: Object.assign({}, defaultLevels, options.levels),
|
||||
format: options.format || 'json',
|
||||
time: options.time || 'short',
|
||||
callerLevel: options.callerLevel || 'warn',
|
||||
colours: Object.assign({}, defaultColours, options.colours),
|
||||
};
|
||||
|
||||
// Detect if output is redirected to a file
|
||||
this.isRedirected = !Deno.stdout.isTerminal();
|
||||
|
||||
// Initialize formatters registry
|
||||
this.formatters = {
|
||||
json: this.jsonFormatter.bind(this),
|
||||
simple: this.simpleFormatter.bind(this),
|
||||
};
|
||||
|
||||
// prevent infinite loop when reporting internal errors in getCallerInfo()
|
||||
this.callerErrorCount = 0;
|
||||
this.maxCallerErrors = 5;
|
||||
}
|
||||
|
||||
validateOptions(options: LoggerOptions): void {
|
||||
// Validate level if provided
|
||||
if (options.level !== undefined) {
|
||||
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(', ')}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Validate format if provided
|
||||
if (options.format !== undefined) {
|
||||
const validFormats = ['json', 'simple'];
|
||||
if (!validFormats.includes(options.format)) {
|
||||
throw new Error(
|
||||
`Invalid format: ${
|
||||
options.format
|
||||
}. Valid formats are: ${validFormats.join(', ')}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Validate time if provided
|
||||
if (options.time !== undefined) {
|
||||
const validTimes = ['long', 'short'];
|
||||
if (!validTimes.includes(options.time)) {
|
||||
throw new Error(
|
||||
`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'];
|
||||
if (!validLevels.includes(options.callerLevel)) {
|
||||
throw new Error(
|
||||
`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');
|
||||
}
|
||||
|
||||
// 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');
|
||||
}
|
||||
|
||||
for (const [level, value] of Object.entries(options.levels)) {
|
||||
if (
|
||||
typeof value !== 'number' ||
|
||||
value < 0 ||
|
||||
!Number.isInteger(value)
|
||||
) {
|
||||
throw new Error(
|
||||
`Level value for '${level}' must be a non-negative integer`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// JSON log formatter
|
||||
jsonFormatter(logEntry: LogEntry): string {
|
||||
try {
|
||||
return JSON.stringify(logEntry);
|
||||
} catch (error) {
|
||||
// Fallback for circular references or other JSON.stringify errors
|
||||
try {
|
||||
// Try to create a safe version by stringifying individual fields
|
||||
const safeEntry = {
|
||||
level: logEntry.level,
|
||||
levelNumber: logEntry.levelNumber,
|
||||
time: logEntry.time,
|
||||
pid: logEntry.pid,
|
||||
hostname: logEntry.hostname,
|
||||
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'}`,
|
||||
};
|
||||
return JSON.stringify(safeEntry);
|
||||
} catch {
|
||||
// Last resort - return a plain string
|
||||
return `{"level":"${logEntry.level}","msg":"${String(
|
||||
logEntry.msg
|
||||
// eslint-disable-next-line no-useless-escape
|
||||
).replace(/"/g, '\"')}","jsonError":"Multiple JSON errors occurred"}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Simple text log formatter
|
||||
simpleFormatter(logEntry: LogEntry): string {
|
||||
const levelPadded = logEntry.level.toUpperCase().padEnd(5);
|
||||
const caller = logEntry.callerFile
|
||||
? `${logEntry.callerFile.split('/').pop()}:${logEntry.callerLine}`
|
||||
: null;
|
||||
|
||||
return caller
|
||||
? `[${logEntry.time}] [${levelPadded}] [${caller}] ${logEntry.msg}`
|
||||
: `[${logEntry.time}] [${levelPadded}] ${logEntry.msg}`;
|
||||
}
|
||||
|
||||
getCallerInfo(): { callerFile: string; callerLine: number } {
|
||||
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) {
|
||||
return stack;
|
||||
};
|
||||
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();
|
||||
callerFile = stackFrame.getFileName();
|
||||
if (currentFile !== callerFile) {
|
||||
callerLine = stackFrame.getLineNumber();
|
||||
break;
|
||||
}
|
||||
}
|
||||
this.callerErrorCount = 0;
|
||||
} catch (e) {
|
||||
this.callerErrorCount++;
|
||||
if (this.callerErrorCount <= this.maxCallerErrors) {
|
||||
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.`
|
||||
);
|
||||
}
|
||||
}
|
||||
// callerFile and callerLine already set to defaults above
|
||||
} finally {
|
||||
(Error as unknown as { prepareStackTrace?: unknown }).prepareStackTrace = originalFunc;
|
||||
}
|
||||
return { callerFile, callerLine };
|
||||
}
|
||||
|
||||
log(level: LogLevel, message: unknown, ...args: unknown[]): void {
|
||||
if (this.options.levels[level] > this.options.levels[this.options.level]) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only get caller info if current level is at or above callerLevel threshold
|
||||
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'
|
||||
? now.toISOString()
|
||||
: now.toISOString().slice(0, 16).replace('T', ' ');
|
||||
|
||||
const logEntry: LogEntry = {
|
||||
level,
|
||||
levelNumber: this.options.levels[level],
|
||||
time: time,
|
||||
pid: Deno.pid,
|
||||
hostname: Deno.hostname(),
|
||||
msg: format(message, ...args),
|
||||
};
|
||||
|
||||
// Only include caller info if it was requested
|
||||
if (shouldIncludeCaller && callerFile !== undefined && callerLine !== undefined) {
|
||||
logEntry.callerFile = callerFile;
|
||||
logEntry.callerLine = callerLine;
|
||||
}
|
||||
|
||||
const colour = this.options.colours[level];
|
||||
const resetColour = this.options.colours.reset;
|
||||
|
||||
// Select the appropriate formatter
|
||||
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') {
|
||||
throw new Error(
|
||||
`Formatter returned ${typeof formattedLog} instead of string`
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
// Formatter failed, fall back to JSON formatter
|
||||
try {
|
||||
const safeEntry = {
|
||||
...logEntry,
|
||||
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
|
||||
).replace(
|
||||
/"/g,
|
||||
// eslint-disable-next-line no-useless-escape
|
||||
'\"'
|
||||
)}","formatterError":"Formatter failed: ${error instanceof Error ? error.message : 'Unknown error'}"}`;
|
||||
}
|
||||
}
|
||||
|
||||
// only show colours if logging to console
|
||||
if (this.isRedirected) {
|
||||
console.log(formattedLog);
|
||||
} else {
|
||||
console.log(`${colour}${formattedLog}${resetColour}`);
|
||||
}
|
||||
}
|
||||
|
||||
error(message: unknown, ...args: unknown[]): void {
|
||||
this.log('error', message, ...args);
|
||||
}
|
||||
|
||||
warn(message: unknown, ...args: unknown[]): void {
|
||||
this.log('warn', message, ...args);
|
||||
}
|
||||
|
||||
info(message: unknown, ...args: unknown[]): void {
|
||||
this.log('info', message, ...args);
|
||||
}
|
||||
|
||||
debug(message: unknown, ...args: unknown[]): void {
|
||||
this.log('debug', message, ...args);
|
||||
}
|
||||
|
||||
level(): LogLevel;
|
||||
level(newLevel: LogLevel): LogLevel;
|
||||
level(newLevel?: LogLevel): LogLevel {
|
||||
// If argument provided, set the new level
|
||||
if (arguments.length > 0 && newLevel !== undefined) {
|
||||
if (Object.hasOwn(this.options.levels, newLevel)) {
|
||||
this.options.level = newLevel;
|
||||
} else {
|
||||
throw new Error(`Invalid log level: ${newLevel}`);
|
||||
}
|
||||
}
|
||||
return this.options.level;
|
||||
}
|
||||
|
||||
setLevel(): LogLevel;
|
||||
setLevel(newLevel: LogLevel): LogLevel;
|
||||
setLevel(newLevel?: LogLevel): LogLevel {
|
||||
if (arguments.length === 0) {
|
||||
// Why are you using this as a getter?
|
||||
return this.level();
|
||||
}
|
||||
return this.level(newLevel!);
|
||||
}
|
||||
}
|
||||
|
||||
export default Logger;
|
||||
80
test/helpers/logger-test-helpers.js
Normal file
80
test/helpers/logger-test-helpers.js
Normal file
@@ -0,0 +1,80 @@
|
||||
// Shared test utilities for logger tests
|
||||
|
||||
// Mock console.log to capture output
|
||||
let capturedLogs = [];
|
||||
let capturedErrors = [];
|
||||
const originalConsoleLog = console.log;
|
||||
const originalConsoleError = console.error;
|
||||
const originalIsTTY = Deno.stdout.isTerminal();
|
||||
|
||||
export function mockConsole() {
|
||||
console.log = (...args) => {
|
||||
capturedLogs.push(args.join(' '));
|
||||
};
|
||||
}
|
||||
|
||||
export function mockConsoleError() {
|
||||
console.error = (...args) => {
|
||||
capturedErrors.push(args);
|
||||
};
|
||||
}
|
||||
|
||||
export function restoreConsole() {
|
||||
console.log = originalConsoleLog;
|
||||
capturedLogs = [];
|
||||
}
|
||||
|
||||
export function restoreConsoleError() {
|
||||
console.error = originalConsoleError;
|
||||
capturedErrors = [];
|
||||
}
|
||||
|
||||
export function getCapturedLogs() {
|
||||
return capturedLogs;
|
||||
}
|
||||
|
||||
export function getCapturedErrors() {
|
||||
return capturedErrors;
|
||||
}
|
||||
|
||||
export function clearCapturedLogs() {
|
||||
capturedLogs = [];
|
||||
}
|
||||
|
||||
export function clearCapturedErrors() {
|
||||
capturedErrors = [];
|
||||
}
|
||||
|
||||
let mockIsTTY = originalIsTTY;
|
||||
|
||||
export function setTTYMode(isTTY) {
|
||||
mockIsTTY = isTTY;
|
||||
// Mock Deno.stdout.isTerminal for testing
|
||||
Deno.stdout.isTerminal = () => mockIsTTY;
|
||||
}
|
||||
|
||||
export function restoreTTY() {
|
||||
mockIsTTY = originalIsTTY;
|
||||
// Restore original Deno.stdout.isTerminal
|
||||
Deno.stdout.isTerminal = () => originalIsTTY;
|
||||
}
|
||||
|
||||
// Helper to setup both console mocks
|
||||
export function setupMocks() {
|
||||
mockConsole();
|
||||
mockConsoleError();
|
||||
}
|
||||
|
||||
// Helper to restore both console mocks
|
||||
export function restoreMocks() {
|
||||
restoreConsole();
|
||||
restoreConsoleError();
|
||||
}
|
||||
|
||||
// Helper to get parsed JSON from first captured log
|
||||
export function getFirstLogAsJSON() {
|
||||
if (capturedLogs.length === 0) {
|
||||
throw new Error('No logs captured');
|
||||
}
|
||||
return JSON.parse(capturedLogs[0]);
|
||||
}
|
||||
298
test/logger.caller-level.test.js
Normal file
298
test/logger.caller-level.test.js
Normal file
@@ -0,0 +1,298 @@
|
||||
import { assertEquals, assert } 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' });
|
||||
|
||||
// Mock console.log to capture output
|
||||
const originalLog = console.log;
|
||||
let capturedOutput = '';
|
||||
console.log = (message) => {
|
||||
capturedOutput = message;
|
||||
};
|
||||
|
||||
try {
|
||||
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'
|
||||
);
|
||||
assert(
|
||||
typeof logEntry.callerLine === 'number',
|
||||
'Should include callerLine for error level'
|
||||
);
|
||||
} finally {
|
||||
console.log = originalLog;
|
||||
}
|
||||
});
|
||||
|
||||
Deno.test("Logger callerLevel - Caller Information Filtering - should include caller info for warn when callerLevel is warn", () => {
|
||||
const logger = new Logger({ callerLevel: 'warn' });
|
||||
|
||||
// Mock console.log to capture output
|
||||
const originalLog = console.log;
|
||||
let capturedOutput = '';
|
||||
console.log = (message) => {
|
||||
capturedOutput = message;
|
||||
};
|
||||
|
||||
try {
|
||||
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'
|
||||
);
|
||||
assert(
|
||||
typeof logEntry.callerLine === 'number',
|
||||
'Should include callerLine for warn level'
|
||||
);
|
||||
} finally {
|
||||
console.log = originalLog;
|
||||
}
|
||||
});
|
||||
|
||||
Deno.test("Logger callerLevel - Caller Information Filtering - should NOT include caller info for info when callerLevel is warn", () => {
|
||||
const logger = new Logger({ callerLevel: 'warn' });
|
||||
|
||||
// Mock console.log to capture output
|
||||
const originalLog = console.log;
|
||||
let capturedOutput = '';
|
||||
console.log = (message) => {
|
||||
capturedOutput = message;
|
||||
};
|
||||
|
||||
try {
|
||||
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'
|
||||
);
|
||||
assertEquals(
|
||||
logEntry.callerLine,
|
||||
undefined,
|
||||
'Should NOT include callerLine for info level'
|
||||
);
|
||||
} finally {
|
||||
console.log = originalLog;
|
||||
}
|
||||
});
|
||||
|
||||
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
|
||||
|
||||
// Mock console.log to capture output
|
||||
const originalLog = console.log;
|
||||
let capturedOutput = '';
|
||||
console.log = (message) => {
|
||||
capturedOutput = message;
|
||||
};
|
||||
|
||||
try {
|
||||
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'
|
||||
);
|
||||
assertEquals(
|
||||
logEntry.callerLine,
|
||||
undefined,
|
||||
'Should NOT include callerLine for debug level'
|
||||
);
|
||||
} finally {
|
||||
console.log = originalLog;
|
||||
}
|
||||
});
|
||||
|
||||
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' });
|
||||
|
||||
// Mock console.log to capture output
|
||||
const originalLog = console.log;
|
||||
let capturedOutput = '';
|
||||
console.log = (message) => {
|
||||
capturedOutput = message;
|
||||
};
|
||||
|
||||
try {
|
||||
// Test each level
|
||||
const levels = ['error', 'warn', 'info', 'debug'];
|
||||
for (const level of levels) {
|
||||
logger[level]('test message');
|
||||
|
||||
const logEntry = JSON.parse(capturedOutput);
|
||||
assert(
|
||||
logEntry.callerFile,
|
||||
`Should include callerFile for ${level} level`
|
||||
);
|
||||
assert(
|
||||
typeof logEntry.callerLine === 'number',
|
||||
`Should include callerLine for ${level} level`
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
console.log = originalLog;
|
||||
}
|
||||
});
|
||||
|
||||
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' });
|
||||
|
||||
// Mock console.log to capture output
|
||||
const originalLog = console.log;
|
||||
let capturedOutput = '';
|
||||
console.log = (message) => {
|
||||
capturedOutput = message;
|
||||
};
|
||||
|
||||
try {
|
||||
// Test each level
|
||||
const levels = ['error', 'warn', 'info', 'debug'];
|
||||
for (const level of levels) {
|
||||
logger[level]('test message');
|
||||
|
||||
const logEntry = JSON.parse(capturedOutput);
|
||||
assertEquals(
|
||||
logEntry.callerFile,
|
||||
undefined,
|
||||
`Should NOT include callerFile for ${level} level`
|
||||
);
|
||||
assertEquals(
|
||||
logEntry.callerLine,
|
||||
undefined,
|
||||
`Should NOT include callerLine for ${level} level`
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
console.log = originalLog;
|
||||
}
|
||||
});
|
||||
|
||||
Deno.test("Logger callerLevel - Simple Formatter with callerLevel - should format correctly without caller info when excluded", () => {
|
||||
const logger = new Logger({ format: 'simple', callerLevel: 'error' });
|
||||
|
||||
// Mock console.log to capture output
|
||||
const originalLog = console.log;
|
||||
let capturedOutput = '';
|
||||
console.log = (message) => {
|
||||
capturedOutput = message;
|
||||
};
|
||||
|
||||
try {
|
||||
logger.info('test message');
|
||||
|
||||
// Should not contain caller info pattern
|
||||
assert(
|
||||
!capturedOutput.includes('unknown'),
|
||||
'Should not include caller placeholder'
|
||||
);
|
||||
assert(
|
||||
!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('test message'),
|
||||
'Should include message'
|
||||
);
|
||||
} finally {
|
||||
console.log = originalLog;
|
||||
}
|
||||
});
|
||||
|
||||
Deno.test("Logger callerLevel - Simple Formatter with callerLevel - should format correctly with caller info when included", () => {
|
||||
const logger = new Logger({ format: 'simple', callerLevel: 'info' });
|
||||
|
||||
// Mock console.log to capture output
|
||||
const originalLog = console.log;
|
||||
let capturedOutput = '';
|
||||
console.log = (message) => {
|
||||
capturedOutput = message;
|
||||
};
|
||||
|
||||
try {
|
||||
logger.info('test message');
|
||||
|
||||
// Should contain caller info pattern
|
||||
assert(
|
||||
capturedOutput.includes('.js:'),
|
||||
'Should include file:line pattern'
|
||||
);
|
||||
assert(capturedOutput.includes('INFO'), 'Should include log level');
|
||||
assert(
|
||||
capturedOutput.includes('test message'),
|
||||
'Should include message'
|
||||
);
|
||||
} finally {
|
||||
console.log = originalLog;
|
||||
}
|
||||
});
|
||||
|
||||
Deno.test("Logger callerLevel - Performance Considerations - should not call getCallerInfo when caller info is not needed", () => {
|
||||
const logger = new Logger({ callerLevel: 'error' });
|
||||
|
||||
// Spy on getCallerInfo method
|
||||
let getCallerInfoCalled = false;
|
||||
const originalGetCallerInfo = logger.getCallerInfo;
|
||||
logger.getCallerInfo = function () {
|
||||
getCallerInfoCalled = true;
|
||||
return originalGetCallerInfo.call(this);
|
||||
};
|
||||
|
||||
// Mock console.log to prevent actual output
|
||||
const originalLog = console.log;
|
||||
console.log = () => {};
|
||||
|
||||
try {
|
||||
logger.info('test message');
|
||||
assertEquals(
|
||||
getCallerInfoCalled,
|
||||
false,
|
||||
'getCallerInfo should not be called for info level when callerLevel is error'
|
||||
);
|
||||
} finally {
|
||||
console.log = originalLog;
|
||||
}
|
||||
});
|
||||
|
||||
Deno.test("Logger callerLevel - Performance Considerations - should call getCallerInfo when caller info is needed", () => {
|
||||
const logger = new Logger({ callerLevel: 'warn' });
|
||||
|
||||
// Spy on getCallerInfo method
|
||||
let getCallerInfoCalled = false;
|
||||
const originalGetCallerInfo = logger.getCallerInfo;
|
||||
logger.getCallerInfo = function () {
|
||||
getCallerInfoCalled = true;
|
||||
return originalGetCallerInfo.call(this);
|
||||
};
|
||||
|
||||
// Mock console.log to prevent actual output
|
||||
const originalLog = console.log;
|
||||
console.log = () => {};
|
||||
|
||||
try {
|
||||
logger.error('test message');
|
||||
assertEquals(
|
||||
getCallerInfoCalled,
|
||||
true,
|
||||
'getCallerInfo should be called for error level when callerLevel is warn'
|
||||
);
|
||||
} finally {
|
||||
console.log = originalLog;
|
||||
}
|
||||
});
|
||||
158
test/logger.constructor.test.js
Normal file
158
test/logger.constructor.test.js
Normal file
@@ -0,0 +1,158 @@
|
||||
import { assertEquals, assertThrows } from "@std/assert";
|
||||
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");
|
||||
});
|
||||
|
||||
Deno.test("Logger Constructor - should throw error for invalid format", () => {
|
||||
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");
|
||||
});
|
||||
|
||||
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");
|
||||
});
|
||||
|
||||
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");
|
||||
});
|
||||
|
||||
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");
|
||||
});
|
||||
|
||||
Deno.test("Logger Constructor - should throw error for invalid level values", () => {
|
||||
assertThrows(() => {
|
||||
new Logger({ levels: { error: -1 } });
|
||||
}, 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: 1.5 } });
|
||||
}, 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' },
|
||||
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.levels, {
|
||||
silent: -1,
|
||||
error: 0,
|
||||
warn: 1,
|
||||
info: 2,
|
||||
debug: 3,
|
||||
});
|
||||
});
|
||||
|
||||
Deno.test("Logger Constructor - should instantiate with custom options", () => {
|
||||
const logger = new Logger({
|
||||
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');
|
||||
});
|
||||
|
||||
Deno.test("Logger Constructor - should merge options correctly", () => {
|
||||
const customOptions = {
|
||||
level: 'debug',
|
||||
format: 'simple',
|
||||
time: 'long',
|
||||
colours: {
|
||||
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');
|
||||
// Should still have other default colors
|
||||
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');
|
||||
});
|
||||
|
||||
Deno.test("Logger Constructor - should have level management methods", () => {
|
||||
const logger = new Logger();
|
||||
assertEquals(typeof logger.level, 'function');
|
||||
assertEquals(typeof logger.setLevel, 'function');
|
||||
});
|
||||
|
||||
Deno.test("Logger Constructor - should detect TTY correctly", () => {
|
||||
const originalIsTTY = Deno.stdout.isTerminal();
|
||||
|
||||
// Mock TTY mode
|
||||
Deno.stdout.isTerminal = () => true;
|
||||
const logger1 = new Logger();
|
||||
assertEquals(logger1.isRedirected, false);
|
||||
|
||||
Deno.stdout.isTerminal = () => false;
|
||||
const logger2 = new Logger();
|
||||
assertEquals(logger2.isRedirected, true);
|
||||
|
||||
// Restore original
|
||||
Deno.stdout.isTerminal = () => originalIsTTY;
|
||||
});
|
||||
|
||||
Deno.test("Logger Constructor - should work with all existing constructor patterns", () => {
|
||||
// No options - should not throw
|
||||
new Logger();
|
||||
|
||||
// Partial options - should not throw
|
||||
new Logger({ level: 'debug' });
|
||||
|
||||
// Full options (without time) - should not throw
|
||||
new Logger({
|
||||
level: 'warn',
|
||||
format: 'simple',
|
||||
colours: { error: '\x1b[31m' },
|
||||
levels: { custom: 5 },
|
||||
});
|
||||
});
|
||||
106
test/logger.internal-errors.test.js
Normal file
106
test/logger.internal-errors.test.js
Normal file
@@ -0,0 +1,106 @@
|
||||
import { assertEquals, assert } from "@std/assert";
|
||||
import Logger from '../lib/logger.ts';
|
||||
import {
|
||||
setupMocks,
|
||||
getCapturedLogs,
|
||||
clearCapturedLogs,
|
||||
} 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' });
|
||||
|
||||
// Replace the simple formatter with one that throws
|
||||
logger.formatters.simple = function () {
|
||||
throw new Error('Custom formatter error');
|
||||
};
|
||||
|
||||
logger.info('test message');
|
||||
|
||||
// Should still produce output using JSON formatter fallback
|
||||
assertEquals(getCapturedLogs().length, 1);
|
||||
|
||||
// Should be valid JSON (fallback to JSON formatter)
|
||||
const logOutput = getCapturedLogs()[0];
|
||||
const parsed = JSON.parse(logOutput);
|
||||
assertEquals(parsed.msg, 'test message');
|
||||
assert(
|
||||
parsed.formatterError.includes(
|
||||
'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' });
|
||||
|
||||
// Replace formatter with one that returns an object instead of string
|
||||
logger.formatters.simple = function () {
|
||||
return { notAString: true };
|
||||
};
|
||||
|
||||
logger.info('test message');
|
||||
|
||||
// Should still produce output (fallback should handle this)
|
||||
assertEquals(getCapturedLogs().length, 1);
|
||||
|
||||
// Should be valid JSON from fallback
|
||||
const logOutput = getCapturedLogs()[0];
|
||||
const parsed = JSON.parse(logOutput);
|
||||
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' });
|
||||
|
||||
// Temporarily break the formatter
|
||||
const originalSimple = logger.formatters.simple;
|
||||
logger.formatters.simple = function () {
|
||||
throw new Error('Temporary error');
|
||||
};
|
||||
|
||||
logger.info('first message');
|
||||
|
||||
// Restore the formatter
|
||||
logger.formatters.simple = originalSimple;
|
||||
|
||||
logger.info('second message');
|
||||
|
||||
// First message should have used fallback, second should work normally
|
||||
assertEquals(getCapturedLogs().length, 2);
|
||||
|
||||
// First log should be JSON (fallback)
|
||||
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'));
|
||||
});
|
||||
|
||||
Deno.test("Logger Internal Error Handling - Formatter Error Handling - should handle repeated formatter failures without memory leaks", () => {
|
||||
clearCapturedLogs();
|
||||
const logger = new Logger({ format: 'simple' });
|
||||
|
||||
// Break the formatter
|
||||
logger.formatters.simple = function () {
|
||||
throw new Error('Always fails');
|
||||
};
|
||||
|
||||
// Log many times
|
||||
for (let i = 0; i < 100; i++) {
|
||||
logger.info(`message ${i}`);
|
||||
}
|
||||
|
||||
// Should have produced 100 fallback logs
|
||||
assertEquals(getCapturedLogs().length, 100);
|
||||
|
||||
// All should be valid JSON (fallback format)
|
||||
getCapturedLogs().forEach((log) => {
|
||||
JSON.parse(log); // This should not throw
|
||||
});
|
||||
});
|
||||
368
test/logger.internal-fallback.test.js
Normal file
368
test/logger.internal-fallback.test.js
Normal file
@@ -0,0 +1,368 @@
|
||||
import { assertEquals, assertThrows, assert } from "@std/assert";
|
||||
import Logger from '../lib/logger.ts';
|
||||
import {
|
||||
setupMocks,
|
||||
getCapturedLogs,
|
||||
clearCapturedLogs,
|
||||
getCapturedErrors,
|
||||
clearCapturedErrors,
|
||||
} 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' });
|
||||
|
||||
// Create circular reference
|
||||
const obj = { name: 'test' };
|
||||
obj.self = 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');
|
||||
// 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')
|
||||
);
|
||||
});
|
||||
|
||||
Deno.test("Logger Additional Fallback Tests - JSON Formatter Error Handling - should handle objects with non-serializable properties", () => {
|
||||
clearCapturedLogs();
|
||||
const logger = new Logger({ format: 'json' });
|
||||
|
||||
// Create object with function (non-serializable)
|
||||
const objWithFunction = {
|
||||
name: 'test',
|
||||
func: function () {
|
||||
return 'hello';
|
||||
},
|
||||
symbol: Symbol('test'),
|
||||
undefined: undefined,
|
||||
};
|
||||
|
||||
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');
|
||||
// Should have the message in some form
|
||||
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' });
|
||||
|
||||
// Break the JSON formatter
|
||||
logger.formatters.json = function () {
|
||||
throw new Error('JSON formatter is broken');
|
||||
};
|
||||
|
||||
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'));
|
||||
});
|
||||
|
||||
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' });
|
||||
|
||||
// Override getCallerInfo to simulate an error
|
||||
const originalGetCallerInfo = logger.getCallerInfo;
|
||||
logger.getCallerInfo = function () {
|
||||
this.callerErrorCount++;
|
||||
if (this.callerErrorCount <= this.maxCallerErrors) {
|
||||
console.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.`
|
||||
);
|
||||
}
|
||||
}
|
||||
return { callerFile: 'unknown', callerLine: 0 };
|
||||
};
|
||||
|
||||
try {
|
||||
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.callerLine, 0);
|
||||
|
||||
// Should have logged an error about caller detection
|
||||
assert(getCapturedErrors().length > 0);
|
||||
} finally {
|
||||
logger.getCallerInfo = originalGetCallerInfo;
|
||||
logger.callerErrorCount = 0;
|
||||
}
|
||||
});
|
||||
|
||||
Deno.test("Logger Additional Fallback Tests - Caller Detection Error Handling - should suppress caller errors after max threshold", () => {
|
||||
clearCapturedLogs();
|
||||
clearCapturedErrors();
|
||||
|
||||
const logger = new Logger({ format: 'json', callerLevel: 'info' });
|
||||
|
||||
// Override getCallerInfo to always simulate errors
|
||||
const originalGetCallerInfo = logger.getCallerInfo;
|
||||
logger.getCallerInfo = function () {
|
||||
this.callerErrorCount++;
|
||||
if (this.callerErrorCount <= this.maxCallerErrors) {
|
||||
console.error(
|
||||
'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.`
|
||||
);
|
||||
}
|
||||
}
|
||||
return { callerFile: 'unknown', callerLine: 0 };
|
||||
};
|
||||
|
||||
try {
|
||||
// Log more than maxCallerErrors (5) times
|
||||
for (let i = 0; i < 10; i++) {
|
||||
logger.info(`test message ${i}`);
|
||||
}
|
||||
|
||||
// Should have logged all 10 messages
|
||||
assertEquals(getCapturedLogs().length, 10);
|
||||
|
||||
// Should have logged errors for first 5 attempts, then suppression message
|
||||
const errorLogs = getCapturedErrors();
|
||||
assert(errorLogs.length >= 5); // At least 5 error calls
|
||||
assert(errorLogs.length <= 6); // But not more than 6 (5 + suppression message)
|
||||
|
||||
// Check that suppression message is included
|
||||
const suppressionFound = errorLogs.some((errorArgs) =>
|
||||
errorArgs.some(
|
||||
(arg) =>
|
||||
typeof arg === 'string' &&
|
||||
arg.includes('Suppressing further caller error messages')
|
||||
)
|
||||
);
|
||||
assert(suppressionFound);
|
||||
} finally {
|
||||
logger.getCallerInfo = originalGetCallerInfo;
|
||||
logger.callerErrorCount = 0;
|
||||
}
|
||||
});
|
||||
|
||||
Deno.test("Logger Additional Fallback Tests - Caller Detection Error Handling - should reset caller error count after successful detection", () => {
|
||||
clearCapturedLogs();
|
||||
clearCapturedErrors();
|
||||
|
||||
const logger = new Logger({ format: 'json', callerLevel: 'info' });
|
||||
|
||||
// Override getCallerInfo to simulate different phases
|
||||
const originalGetCallerInfo = logger.getCallerInfo;
|
||||
let phase = 'error1';
|
||||
|
||||
logger.getCallerInfo = function () {
|
||||
if (phase === 'error1') {
|
||||
this.callerErrorCount++;
|
||||
if (this.callerErrorCount <= this.maxCallerErrors) {
|
||||
console.error(
|
||||
'Error retrieving caller info:',
|
||||
new Error('Phase 1 error')
|
||||
);
|
||||
}
|
||||
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') {
|
||||
this.callerErrorCount++;
|
||||
if (this.callerErrorCount <= this.maxCallerErrors) {
|
||||
console.error(
|
||||
'Error retrieving caller info:',
|
||||
new Error('Phase 2 error')
|
||||
);
|
||||
}
|
||||
return { callerFile: 'unknown', callerLine: 0 };
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
// Cause some errors
|
||||
logger.info('test 1');
|
||||
logger.info('test 2');
|
||||
|
||||
// Switch to working mode
|
||||
phase = 'working';
|
||||
logger.info('test 3');
|
||||
|
||||
// Break it again
|
||||
phase = 'error2';
|
||||
logger.info('test 4');
|
||||
|
||||
const errorLogs = getCapturedErrors();
|
||||
// Should have errors from both phases
|
||||
assert(errorLogs.length >= 3);
|
||||
} finally {
|
||||
logger.getCallerInfo = originalGetCallerInfo;
|
||||
logger.callerErrorCount = 0;
|
||||
}
|
||||
});
|
||||
|
||||
Deno.test("Logger Additional Fallback Tests - Extreme Error Conditions - should handle when console.log itself throws", () => {
|
||||
clearCapturedLogs();
|
||||
const logger = new Logger({ format: 'json' });
|
||||
|
||||
// Break console.log
|
||||
const originalLog = console.log;
|
||||
console.log = function () {
|
||||
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");
|
||||
} finally {
|
||||
console.log = originalLog;
|
||||
}
|
||||
});
|
||||
|
||||
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' });
|
||||
|
||||
// Create an object that will cause issues with string conversion
|
||||
const problematicObject = {
|
||||
toString: function () {
|
||||
throw new Error('toString failed');
|
||||
},
|
||||
valueOf: function () {
|
||||
throw new Error('valueOf failed');
|
||||
},
|
||||
};
|
||||
|
||||
// The logger should handle this gracefully and not throw
|
||||
logger.info('Message: %s', problematicObject);
|
||||
|
||||
const logs = getCapturedLogs();
|
||||
assertEquals(logs.length, 1);
|
||||
|
||||
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);
|
||||
|
||||
});
|
||||
|
||||
Deno.test("Logger Additional Fallback Tests - Extreme Error Conditions - should handle when hostname fails", () => {
|
||||
setupMocks();
|
||||
clearCapturedLogs();
|
||||
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
|
||||
// In practice, hostname failures are rare but could happen
|
||||
|
||||
// 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');
|
||||
assertEquals(getCapturedLogs().length, 1);
|
||||
|
||||
});
|
||||
|
||||
Deno.test("Logger Additional Fallback Tests - Fallback Chain Testing - should handle formatter failures gracefully", () => {
|
||||
setupMocks();
|
||||
clearCapturedLogs();
|
||||
clearCapturedErrors();
|
||||
|
||||
const logger = new Logger({ format: 'simple', callerLevel: 'info' });
|
||||
|
||||
// Break the simple formatter
|
||||
logger.formatters.simple = function () {
|
||||
throw new Error('Simple formatter broken');
|
||||
};
|
||||
|
||||
// Also simulate caller detection failure
|
||||
const originalGetCallerInfo = logger.getCallerInfo;
|
||||
logger.getCallerInfo = function () {
|
||||
this.callerErrorCount++;
|
||||
if (this.callerErrorCount <= this.maxCallerErrors) {
|
||||
console.error(
|
||||
'Error retrieving caller info:',
|
||||
new Error('Caller detection failed')
|
||||
);
|
||||
}
|
||||
return { callerFile: 'unknown', callerLine: 0 };
|
||||
};
|
||||
|
||||
try {
|
||||
// Should still produce some output despite multiple failures
|
||||
logger.info('test message');
|
||||
|
||||
// Should produce some kind of output (fallback to JSON formatter)
|
||||
assertEquals(getCapturedLogs().length, 1);
|
||||
const output = getCapturedLogs()[0];
|
||||
|
||||
// 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');
|
||||
} finally {
|
||||
logger.getCallerInfo = originalGetCallerInfo;
|
||||
logger.callerErrorCount = 0;
|
||||
}
|
||||
});
|
||||
|
||||
Deno.test("Logger Additional Fallback Tests - Resource Cleanup - should not leak memory during repeated errors", () => {
|
||||
setupMocks();
|
||||
clearCapturedLogs();
|
||||
clearCapturedErrors();
|
||||
|
||||
const logger = new Logger({ format: 'simple' });
|
||||
|
||||
// Break the formatter
|
||||
logger.formatters.simple = function () {
|
||||
throw new Error('Always fails');
|
||||
};
|
||||
|
||||
// Log many times to check for memory leaks
|
||||
for (let i = 0; i < 1000; i++) {
|
||||
logger.info(`message ${i}`);
|
||||
}
|
||||
|
||||
// Should have produced all logs
|
||||
assertEquals(getCapturedLogs().length, 1000);
|
||||
|
||||
// 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
|
||||
|
||||
});
|
||||
225
test/logger.json-formatter.test.js
Normal file
225
test/logger.json-formatter.test.js
Normal file
@@ -0,0 +1,225 @@
|
||||
import { assertEquals, assert } from "@std/assert";
|
||||
import Logger from '../lib/logger.ts';
|
||||
import {
|
||||
setupMocks,
|
||||
getCapturedLogs,
|
||||
clearCapturedLogs,
|
||||
getFirstLogAsJSON,
|
||||
} 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');
|
||||
|
||||
assertEquals(getCapturedLogs().length, 1);
|
||||
const logOutput = getCapturedLogs()[0];
|
||||
|
||||
// Should be valid JSON
|
||||
JSON.parse(logOutput);
|
||||
});
|
||||
|
||||
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 parsed = getFirstLogAsJSON();
|
||||
|
||||
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');
|
||||
assert(parsed.callerFile);
|
||||
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 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'));
|
||||
});
|
||||
|
||||
Deno.test("Logger JSON Formatter - JSON Error Handling - should handle circular references in log entry", () => {
|
||||
clearCapturedLogs();
|
||||
const logger = new Logger({ format: 'json' });
|
||||
|
||||
// Create a circular reference by modifying the logger's formatters
|
||||
const originalJsonFormatter = logger.formatters.json;
|
||||
logger.formatters.json = function (logEntry) {
|
||||
// Add a circular reference to the logEntry
|
||||
const circular = { self: null };
|
||||
circular.self = circular;
|
||||
logEntry.circular = circular;
|
||||
|
||||
// Call the original formatter which should handle the error
|
||||
return originalJsonFormatter.call(this, logEntry);
|
||||
};
|
||||
|
||||
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');
|
||||
});
|
||||
|
||||
Deno.test("Logger JSON Formatter - JSON Error Handling - should handle JSON stringify errors with fallback", () => {
|
||||
clearCapturedLogs();
|
||||
const logger = new Logger({ format: 'json' });
|
||||
|
||||
// Create a problematic object that will cause JSON.stringify to fail
|
||||
const problematic = {};
|
||||
Object.defineProperty(problematic, 'badProp', {
|
||||
get() {
|
||||
throw new Error('Property access error');
|
||||
},
|
||||
enumerable: true,
|
||||
});
|
||||
|
||||
// Test the formatter directly with a problematic object
|
||||
const problematicLogEntry = {
|
||||
level: 'info',
|
||||
msg: 'test message',
|
||||
problematic: problematic,
|
||||
};
|
||||
|
||||
const result = logger.formatters.json(problematicLogEntry);
|
||||
|
||||
// Should produce valid JSON with error info
|
||||
const parsed = JSON.parse(result);
|
||||
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' });
|
||||
|
||||
// Create an object that will fail even the safe fallback
|
||||
// by mocking JSON.stringify to always throw
|
||||
const originalStringify = JSON.stringify;
|
||||
let callCount = 0;
|
||||
|
||||
JSON.stringify = function (...args) {
|
||||
callCount++;
|
||||
if (callCount <= 2) {
|
||||
throw new Error('Mock JSON error');
|
||||
}
|
||||
return originalStringify.apply(this, args);
|
||||
};
|
||||
|
||||
try {
|
||||
const result = logger.formatters.json({
|
||||
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'));
|
||||
} finally {
|
||||
JSON.stringify = originalStringify;
|
||||
}
|
||||
});
|
||||
|
||||
Deno.test("Logger JSON Formatter - Special Characters and Edge Cases - should handle special characters", () => {
|
||||
clearCapturedLogs();
|
||||
const logger = new Logger({ format: 'json' });
|
||||
logger.info('Special chars: "quotes", \\backslash, \nnewline');
|
||||
|
||||
// Should produce valid JSON despite special characters
|
||||
JSON.parse(getCapturedLogs()[0]);
|
||||
});
|
||||
|
||||
Deno.test("Logger JSON Formatter - Special Characters and Edge Cases - should handle empty messages", () => {
|
||||
clearCapturedLogs();
|
||||
const logger = new Logger({ format: 'json' });
|
||||
logger.info('');
|
||||
|
||||
const parsed = getFirstLogAsJSON();
|
||||
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 parsed = getFirstLogAsJSON();
|
||||
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' });
|
||||
logger.info(longMessage);
|
||||
|
||||
const parsed = getFirstLogAsJSON();
|
||||
assertEquals(parsed.msg, longMessage);
|
||||
});
|
||||
|
||||
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 parsed = getFirstLogAsJSON();
|
||||
assert(parsed.msg.includes('{"key":"value","nested":{"prop":123}}'));
|
||||
});
|
||||
|
||||
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 parsed = getFirstLogAsJSON();
|
||||
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 parsed = getFirstLogAsJSON();
|
||||
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 parsed = getFirstLogAsJSON();
|
||||
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 parsed = getFirstLogAsJSON();
|
||||
assertEquals(parsed.level, 'debug');
|
||||
assertEquals(parsed.levelNumber, 3);
|
||||
});
|
||||
205
test/logger.level-management.test.js
Normal file
205
test/logger.level-management.test.js
Normal file
@@ -0,0 +1,205 @@
|
||||
import { assertEquals, assertThrows } from "@std/assert";
|
||||
import Logger from '../lib/logger.ts';
|
||||
import {
|
||||
setupMocks,
|
||||
getCapturedLogs,
|
||||
clearCapturedLogs,
|
||||
} 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');
|
||||
});
|
||||
|
||||
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');
|
||||
});
|
||||
|
||||
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');
|
||||
});
|
||||
|
||||
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");
|
||||
});
|
||||
|
||||
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');
|
||||
});
|
||||
|
||||
Deno.test("Logger Level Management - setLevel Method - should have setLevel method as alias", () => {
|
||||
const logger = new Logger();
|
||||
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');
|
||||
});
|
||||
|
||||
Deno.test("Logger Level Management - setLevel Method - should return current level with setLevel when no args", () => {
|
||||
const logger = new Logger({ level: 'warn' });
|
||||
const result = logger.setLevel();
|
||||
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");
|
||||
});
|
||||
|
||||
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.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');
|
||||
|
||||
// Both methods should return the current level for chaining
|
||||
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');
|
||||
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');
|
||||
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');
|
||||
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');
|
||||
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');
|
||||
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');
|
||||
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' });
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
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');
|
||||
});
|
||||
|
||||
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');
|
||||
});
|
||||
|
||||
Deno.test("Logger Level Management - Silent Level - should remain silent after multiple log attempts", () => {
|
||||
clearCapturedLogs();
|
||||
const logger = new Logger({ level: 'silent' });
|
||||
|
||||
// Try logging multiple times
|
||||
for (let i = 0; i < 5; i++) {
|
||||
logger.error(`error ${i}`);
|
||||
logger.warn(`warn ${i}`);
|
||||
logger.info(`info ${i}`);
|
||||
logger.debug(`debug ${i}`);
|
||||
}
|
||||
|
||||
// Still no output
|
||||
assertEquals(getCapturedLogs().length, 0);
|
||||
});
|
||||
|
||||
Deno.test("Logger Level Management - Dynamic Level Changes - should respect level changes during runtime", () => {
|
||||
clearCapturedLogs();
|
||||
const logger = new Logger({ level: 'error' });
|
||||
|
||||
// Should not log at info level
|
||||
logger.info('info message 1');
|
||||
assertEquals(getCapturedLogs().length, 0);
|
||||
|
||||
// Change to info level
|
||||
logger.level('info');
|
||||
|
||||
// Should now log info messages
|
||||
logger.info('info message 2');
|
||||
assertEquals(getCapturedLogs().length, 1);
|
||||
|
||||
// Change to silent
|
||||
logger.level('silent');
|
||||
|
||||
// Should not log anything
|
||||
logger.error('error message');
|
||||
assertEquals(getCapturedLogs().length, 1); // Still just the previous info message
|
||||
});
|
||||
146
test/logger.robustness.test.js
Normal file
146
test/logger.robustness.test.js
Normal file
@@ -0,0 +1,146 @@
|
||||
import { assertEquals, assert } from "@std/assert";
|
||||
import Logger from '../lib/logger.ts';
|
||||
import {
|
||||
setupMocks,
|
||||
getCapturedLogs,
|
||||
clearCapturedLogs,
|
||||
} from './helpers/logger-test-helpers.js';
|
||||
|
||||
// Setup and teardown for all tests
|
||||
setupMocks();
|
||||
|
||||
Deno.test("Logger Robustness - Edge Cases and Data Handling - should not crash on logging errors", () => {
|
||||
clearCapturedLogs();
|
||||
const logger = new Logger();
|
||||
|
||||
// This should not throw
|
||||
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' });
|
||||
|
||||
// These should not crash
|
||||
logger.info(undefined);
|
||||
logger.info(null);
|
||||
|
||||
const logs = getCapturedLogs();
|
||||
assertEquals(logs.length, 2);
|
||||
|
||||
const parsed1 = JSON.parse(logs[0]);
|
||||
const parsed2 = JSON.parse(logs[1]);
|
||||
|
||||
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);
|
||||
|
||||
logger.info(hugeMessage);
|
||||
|
||||
const parsed = JSON.parse(getCapturedLogs()[0]);
|
||||
assertEquals(parsed.msg, hugeMessage);
|
||||
});
|
||||
|
||||
Deno.test("Logger Robustness - Edge Cases and Data Handling - should handle circular objects in message formatting", () => {
|
||||
clearCapturedLogs();
|
||||
const logger = new Logger({ format: 'json' });
|
||||
|
||||
const circular = { name: 'test' };
|
||||
circular.self = circular;
|
||||
|
||||
logger.info('Circular: %j', circular);
|
||||
|
||||
// Should still log something
|
||||
assertEquals(getCapturedLogs().length, 1);
|
||||
});
|
||||
|
||||
Deno.test("Logger Robustness - Performance and Memory - should handle rapid consecutive logging without issues", () => {
|
||||
clearCapturedLogs();
|
||||
const logger = new Logger({ format: 'json' });
|
||||
|
||||
for (let i = 0; i < 1000; i++) {
|
||||
logger.info(`rapid message ${i}`);
|
||||
}
|
||||
|
||||
assertEquals(getCapturedLogs().length, 1000);
|
||||
});
|
||||
|
||||
Deno.test("Logger Robustness - Performance and Memory - should handle repeated logging operations efficiently", () => {
|
||||
clearCapturedLogs();
|
||||
const logger = new Logger({ format: 'simple' });
|
||||
|
||||
const startTime = Date.now();
|
||||
|
||||
// Log a reasonable number of messages
|
||||
for (let i = 0; i < 500; i++) {
|
||||
logger.info(`performance test message ${i}`);
|
||||
}
|
||||
|
||||
const endTime = Date.now();
|
||||
const duration = endTime - startTime;
|
||||
|
||||
// Should complete within reasonable time (adjust threshold as needed)
|
||||
assert(duration < 5000, `Logging took too long: ${duration}ms`);
|
||||
assertEquals(getCapturedLogs().length, 500);
|
||||
});
|
||||
|
||||
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' });
|
||||
|
||||
for (let i = 0; i < 50; i++) {
|
||||
jsonLogger.info(`json message ${i}`);
|
||||
simpleLogger.info(`simple message ${i}`);
|
||||
}
|
||||
|
||||
assertEquals(getCapturedLogs().length, 100);
|
||||
});
|
||||
|
||||
Deno.test("Logger Robustness - Complex Data Structures - should handle deeply nested objects", () => {
|
||||
clearCapturedLogs();
|
||||
const logger = new Logger({ format: 'json' });
|
||||
|
||||
const deepObject = {
|
||||
level1: { level2: { level3: { level4: { value: 'deep' } } } },
|
||||
};
|
||||
|
||||
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 mixedArray = [
|
||||
1,
|
||||
'string',
|
||||
{ obj: true },
|
||||
[1, 2, 3],
|
||||
null,
|
||||
undefined,
|
||||
];
|
||||
|
||||
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 specialMessage = 'Special chars: \n\t\r\\"\'🚀 Unicode: こんにちは';
|
||||
|
||||
logger.info(specialMessage);
|
||||
|
||||
const parsed = JSON.parse(getCapturedLogs()[0]);
|
||||
assertEquals(parsed.msg, specialMessage);
|
||||
});
|
||||
217
test/logger.simple-formatter.test.js
Normal file
217
test/logger.simple-formatter.test.js
Normal file
@@ -0,0 +1,217 @@
|
||||
import { assertEquals, assert } from "@std/assert";
|
||||
import Logger from '../lib/logger.ts';
|
||||
import {
|
||||
setupMocks,
|
||||
getCapturedLogs,
|
||||
clearCapturedLogs,
|
||||
setTTYMode,
|
||||
restoreTTY,
|
||||
} 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');
|
||||
|
||||
assertEquals(getCapturedLogs().length, 1);
|
||||
const logOutput = getCapturedLogs()[0];
|
||||
|
||||
// Should contain timestamp, level, caller, and 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 logs = getCapturedLogs();
|
||||
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 logOutput = getCapturedLogs()[0];
|
||||
// Should contain filename and line number
|
||||
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 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\]/)
|
||||
);
|
||||
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 logOutput = getCapturedLogs()[0];
|
||||
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 logOutput = getCapturedLogs()[0];
|
||||
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 logOutput = getCapturedLogs()[0];
|
||||
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 logOutput = getCapturedLogs()[0];
|
||||
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 logOutput = getCapturedLogs()[0];
|
||||
// Should contain ANSI color codes
|
||||
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 logOutput = getCapturedLogs()[0];
|
||||
// Should not contain ANSI color codes
|
||||
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' });
|
||||
|
||||
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'));
|
||||
// Warn should be yellow
|
||||
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'));
|
||||
});
|
||||
|
||||
Deno.test("Logger Simple Formatter - Color Handling - should respect custom color configuration", () => {
|
||||
clearCapturedLogs();
|
||||
setTTYMode(true);
|
||||
const logger = new Logger({
|
||||
format: 'simple',
|
||||
colours: {
|
||||
error: '\x1b[31m', // different red
|
||||
warn: '\x1b[35m', // magenta instead of yellow
|
||||
},
|
||||
});
|
||||
|
||||
logger.error('error message');
|
||||
logger.warn('warn message');
|
||||
|
||||
const logs = getCapturedLogs();
|
||||
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 logOutput = getCapturedLogs()[0];
|
||||
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' });
|
||||
logger.info('Special chars: "quotes", \\backslash, \nnewline');
|
||||
|
||||
const logOutput = getCapturedLogs()[0];
|
||||
assert(logOutput.includes('Special chars: "quotes"'));
|
||||
});
|
||||
|
||||
Deno.test("Logger Simple Formatter - Message Formatting in Simple Mode - should handle empty messages", () => {
|
||||
clearCapturedLogs();
|
||||
const logger = new Logger({ format: 'simple' });
|
||||
logger.info('');
|
||||
|
||||
const logOutput = getCapturedLogs()[0];
|
||||
// Should still have the level and timestamp parts
|
||||
assert(logOutput.includes('[INFO ]'));
|
||||
});
|
||||
|
||||
Deno.test("Logger Simple Formatter - TTY Detection Integration - should detect TTY mode changes correctly", () => {
|
||||
clearCapturedLogs();
|
||||
|
||||
// Test with TTY
|
||||
setTTYMode(true);
|
||||
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 logs = getCapturedLogs();
|
||||
|
||||
// First should have colors, second should not
|
||||
assert(logs[0].includes('\x1b['));
|
||||
assert(!logs[1].includes('\x1b['));
|
||||
});
|
||||
|
||||
// Cleanup
|
||||
restoreTTY();
|
||||
200
test/logger.time-formatting.test.js
Normal file
200
test/logger.time-formatting.test.js
Normal file
@@ -0,0 +1,200 @@
|
||||
import { assertEquals, assertThrows, assert } from "@std/assert";
|
||||
import Logger from '../lib/logger.ts';
|
||||
import {
|
||||
setupMocks,
|
||||
getCapturedLogs,
|
||||
clearCapturedLogs,
|
||||
getFirstLogAsJSON,
|
||||
} 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 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('.'));
|
||||
});
|
||||
|
||||
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 defaultLogger = new Logger();
|
||||
|
||||
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 parsed = getFirstLogAsJSON();
|
||||
|
||||
// Short format: YYYY-MM-DD HH:MM
|
||||
assert(parsed.time.match(/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}$/));
|
||||
assertEquals(parsed.time.length, 16);
|
||||
});
|
||||
|
||||
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 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'));
|
||||
});
|
||||
|
||||
Deno.test("Logger Time Formatting - Short Time Format - should truncate time correctly in short format", () => {
|
||||
clearCapturedLogs();
|
||||
const logger = new Logger({ time: 'short', format: 'json' });
|
||||
|
||||
logger.info('test message');
|
||||
|
||||
const parsed = getFirstLogAsJSON();
|
||||
|
||||
// Short format should not have seconds or milliseconds
|
||||
assert(
|
||||
!parsed.time.includes(':') || parsed.time.split(':').length === 2
|
||||
);
|
||||
assert(!parsed.time.includes('.'));
|
||||
|
||||
// Should be exactly 16 characters: YYYY-MM-DD HH:MM
|
||||
assertEquals(parsed.time.length, 16);
|
||||
});
|
||||
|
||||
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 parsed = getFirstLogAsJSON();
|
||||
|
||||
// Long format should be full ISO string
|
||||
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$/)
|
||||
);
|
||||
});
|
||||
|
||||
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 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\]/)
|
||||
);
|
||||
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 startTime = Date.now();
|
||||
logger.info('test message');
|
||||
const endTime = Date.now();
|
||||
|
||||
const parsed = getFirstLogAsJSON();
|
||||
const logTime = new Date(parsed.time).getTime();
|
||||
|
||||
// Log time should be within the test execution window
|
||||
assert(logTime >= startTime);
|
||||
assert(logTime <= endTime);
|
||||
|
||||
// Should have millisecond precision
|
||||
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' });
|
||||
|
||||
logger.info('first message');
|
||||
logger.warn('second message');
|
||||
|
||||
const logs = getCapturedLogs();
|
||||
const parsed1 = JSON.parse(logs[0]);
|
||||
const parsed2 = JSON.parse(logs[1]);
|
||||
|
||||
// Both should use short format
|
||||
assert(parsed1.time.match(/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}$/));
|
||||
assert(parsed2.time.match(/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}$/));
|
||||
});
|
||||
|
||||
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' });
|
||||
|
||||
// Invalid option should throw
|
||||
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");
|
||||
});
|
||||
|
||||
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 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');
|
||||
|
||||
// Time should be in short format (new default)
|
||||
assert(parsed.time.match(/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}$/));
|
||||
});
|
||||
|
||||
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 logOutput = getCapturedLogs()[0];
|
||||
|
||||
// Should still contain expected elements
|
||||
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}\]/));
|
||||
});
|
||||
358
test/logger.util-format.test.js
Normal file
358
test/logger.util-format.test.js
Normal file
@@ -0,0 +1,358 @@
|
||||
import { assertEquals, assertMatch } from "@std/assert";
|
||||
import Logger from '../lib/logger.ts';
|
||||
|
||||
Deno.test("Logger util.format functionality - Format specifiers - should handle %s string formatting", () => {
|
||||
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 output = JSON.parse(capturedOutput);
|
||||
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 = '';
|
||||
const originalLog = console.log;
|
||||
console.log = (message) => {
|
||||
capturedOutput = message;
|
||||
};
|
||||
|
||||
try {
|
||||
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');
|
||||
} finally {
|
||||
console.log = originalLog;
|
||||
}
|
||||
});
|
||||
|
||||
Deno.test("Logger util.format functionality - Format specifiers - should handle %i integer formatting", () => {
|
||||
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 output = JSON.parse(capturedOutput);
|
||||
assertEquals(output.msg, 'Value: 42');
|
||||
} finally {
|
||||
console.log = originalLog;
|
||||
}
|
||||
});
|
||||
|
||||
Deno.test("Logger util.format functionality - Format specifiers - should handle %f float formatting", () => {
|
||||
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 output = JSON.parse(capturedOutput);
|
||||
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 = '';
|
||||
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 output = JSON.parse(capturedOutput);
|
||||
assertEquals(output.msg, 'Config: {"name":"test","value":42}');
|
||||
} finally {
|
||||
console.log = originalLog;
|
||||
}
|
||||
});
|
||||
|
||||
Deno.test("Logger util.format functionality - Format specifiers - should handle %% literal percentage", () => {
|
||||
let capturedOutput = '';
|
||||
const originalLog = console.log;
|
||||
console.log = (message) => {
|
||||
capturedOutput = message;
|
||||
};
|
||||
|
||||
try {
|
||||
const logger = new Logger({ format: 'json' });
|
||||
logger.info('Progress: 50%% complete');
|
||||
|
||||
const output = JSON.parse(capturedOutput);
|
||||
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 = '';
|
||||
const originalLog = console.log;
|
||||
console.log = (message) => {
|
||||
capturedOutput = message;
|
||||
};
|
||||
|
||||
try {
|
||||
const logger = new Logger({ format: 'json' });
|
||||
logger.info(
|
||||
'User %s has %d points and %f%% completion',
|
||||
'alice',
|
||||
150,
|
||||
75.5
|
||||
);
|
||||
|
||||
const output = JSON.parse(capturedOutput);
|
||||
assertEquals(
|
||||
output.msg,
|
||||
'User alice has 150 points and 75.5% completion'
|
||||
);
|
||||
} finally {
|
||||
console.log = originalLog;
|
||||
}
|
||||
});
|
||||
|
||||
Deno.test("Logger util.format functionality - Multiple format specifiers - should handle mixed format specifiers with JSON", () => {
|
||||
let capturedOutput = '';
|
||||
const originalLog = console.log;
|
||||
console.log = (message) => {
|
||||
capturedOutput = message;
|
||||
};
|
||||
|
||||
try {
|
||||
const logger = new Logger({ format: 'json' });
|
||||
const config = { debug: true, port: 3000 };
|
||||
logger.info(
|
||||
'Server %s running on port %d with config %j',
|
||||
'api',
|
||||
8080,
|
||||
config
|
||||
);
|
||||
|
||||
const output = JSON.parse(capturedOutput);
|
||||
assertEquals(
|
||||
output.msg,
|
||||
'Server api running on port 8080 with config {"debug":true,"port":3000}'
|
||||
);
|
||||
} finally {
|
||||
console.log = originalLog;
|
||||
}
|
||||
});
|
||||
|
||||
Deno.test("Logger util.format functionality - Multiple arguments without format specifiers - should handle multiple arguments without format specifiers", () => {
|
||||
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 output = JSON.parse(capturedOutput);
|
||||
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 = '';
|
||||
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 output = JSON.parse(capturedOutput);
|
||||
assertEquals(output.msg, "Data: { key: 'value' } 42 true");
|
||||
} finally {
|
||||
console.log = originalLog;
|
||||
}
|
||||
});
|
||||
|
||||
Deno.test("Logger util.format functionality - Edge cases - should handle more format specifiers than arguments", () => {
|
||||
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 output = JSON.parse(capturedOutput);
|
||||
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 = '';
|
||||
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 output = JSON.parse(capturedOutput);
|
||||
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 = '';
|
||||
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 output = JSON.parse(capturedOutput);
|
||||
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 = '';
|
||||
const originalLog = console.log;
|
||||
console.log = (message) => {
|
||||
capturedOutput = message;
|
||||
};
|
||||
|
||||
try {
|
||||
const logger = new Logger({ format: 'json' });
|
||||
const arr = [1, 2, 3];
|
||||
const obj = { a: 1 };
|
||||
logger.info('Data %s and %s', arr, obj);
|
||||
|
||||
const output = JSON.parse(capturedOutput);
|
||||
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 = '';
|
||||
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);
|
||||
|
||||
assertMatch(capturedOutput, /User bob has 200 points/);
|
||||
} finally {
|
||||
console.log = originalLog;
|
||||
}
|
||||
});
|
||||
|
||||
Deno.test("Logger util.format functionality - Simple format output - should handle JSON formatting in simple format", () => {
|
||||
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);
|
||||
|
||||
assertMatch(capturedOutput, /Status: {"status":"active","count":5}/);
|
||||
} finally {
|
||||
console.log = originalLog;
|
||||
}
|
||||
});
|
||||
|
||||
Deno.test("Logger util.format functionality - Error handling in util.format - should handle objects that throw during toString", () => {
|
||||
let capturedOutput = '';
|
||||
const originalLog = console.log;
|
||||
console.log = (message) => {
|
||||
capturedOutput = message;
|
||||
};
|
||||
|
||||
try {
|
||||
const logger = new Logger({ format: 'json' });
|
||||
const problematicObj = {
|
||||
toString() {
|
||||
throw new Error('toString failed');
|
||||
},
|
||||
};
|
||||
|
||||
// The logger might handle the error gracefully, so let's test the actual output
|
||||
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');
|
||||
} finally {
|
||||
console.log = originalLog;
|
||||
}
|
||||
});
|
||||
|
||||
Deno.test("Logger util.format functionality - Error handling in util.format - should handle circular references with %j", () => {
|
||||
let capturedOutput = '';
|
||||
const originalLog = console.log;
|
||||
console.log = (message) => {
|
||||
capturedOutput = message;
|
||||
};
|
||||
|
||||
try {
|
||||
const logger = new Logger({ format: 'json' });
|
||||
const circular = { name: 'test' };
|
||||
circular.self = circular;
|
||||
|
||||
logger.info('Circular: %j', circular);
|
||||
|
||||
const output = JSON.parse(capturedOutput);
|
||||
assertMatch(output.msg, /Circular: \[Circular\]/);
|
||||
} finally {
|
||||
console.log = originalLog;
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user