-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: support sprintf style logging when the a string contains %s, %i…
… or %d tokens
- Loading branch information
Showing
5 changed files
with
278 additions
and
36 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,94 @@ | ||
import 'jest'; | ||
import { LoggerEntry } from '../logging/logEntry'; | ||
|
||
describe('LoggerEntry', () => { | ||
describe('message (property)', () => { | ||
it('should format message with %s placeholder as string', () => { | ||
const entry = new LoggerEntry(0, 'test', ['Hello %s', 'World']); | ||
expect(entry.message).toBe('Hello World'); | ||
}); | ||
it('should format parts of the message with a %s placeholder as string', () => { | ||
const entry = new LoggerEntry(0, 'test', [ | ||
'Hello %s', | ||
'%s World %s', 'this', | ||
'is %s', 'Great' | ||
]); | ||
expect(entry.message).toBe('Hello this World is Great'); | ||
}); | ||
it('should run functions when passed as arg', () => { | ||
const entry = new LoggerEntry(0, 'test', [ | ||
'Hello %s', | ||
() => 'World' | ||
]); | ||
expect(entry.message).toBe('Hello World'); | ||
}); | ||
it('should support all args as functions and format them', () => { | ||
const entry = new LoggerEntry(0, 'test', [ | ||
() => 'Hello %s', | ||
() => 'amazing %s', | ||
() => 'World' | ||
]); | ||
expect(entry.message).toBe('Hello amazing World'); | ||
}); | ||
it('should format numbers with specified number of decimals', () => { | ||
const entry = new LoggerEntry(0, 'test', [ | ||
'PI %d:4', | ||
3.14159265359 | ||
]); | ||
expect(entry.message).toBe('PI 3.1416'); | ||
}); | ||
it('should format numbers with 2 decimals when no number of decimals are specified', () => { | ||
const entry = new LoggerEntry(0, 'test', [ | ||
'PI %d', | ||
3.14159265359 | ||
]); | ||
expect(entry.message).toBe('PI 3.14'); | ||
}); | ||
it('should format number without decimals when specified as integer', () => { | ||
const entry = new LoggerEntry(0, 'test', [ | ||
'PI %i', | ||
3.14159265359 | ||
]); | ||
expect(entry.message).toBe('PI 3'); | ||
}); | ||
it('should format strings as upper case when specified as %S', () => { | ||
const entry = new LoggerEntry(0, 'test', [ | ||
'Hello %S', | ||
'world' | ||
]); | ||
expect(entry.message).toBe('Hello WORLD'); | ||
}); | ||
it('should only format message once', () => { | ||
let called = 0; | ||
const entry = new LoggerEntry(0, 'test', [ | ||
() => { | ||
called ++; | ||
return 'Hello %s'; | ||
}, | ||
'world' | ||
]); | ||
|
||
expect(entry.message).toBe('Hello world'); | ||
expect(entry.message).toBe('Hello world'); | ||
expect(entry.message).toBe('Hello world'); | ||
|
||
expect(called).toBe(1); | ||
}); | ||
it('should only format message once', () => { | ||
const entry = new LoggerEntry(0, 'test', [ | ||
'Hello', 'world' | ||
]); | ||
expect(entry.message).toBe('Hello world'); | ||
}); | ||
it('should not replace placeholder when more placeholder count exceeds replacements', () => { | ||
const entry = new LoggerEntry(0, 'test', [ | ||
'Hello %s %s', 'world %s %s' | ||
]); | ||
expect(entry.message).toBe('Hello world %s %s %s'); | ||
}); | ||
it('should not replace placeholder when there are no replacements', () => { | ||
const entry = new LoggerEntry(0, 'test', [ 'Hello %s %s' ]); | ||
expect(entry.message).toBe('Hello %s %s'); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,114 @@ | ||
import { getErrorMessage } from "@vlocode/util"; | ||
import { LogLevel } from "./logLevels"; | ||
|
||
const FORMAT_REGEX = /%([sdiS])(?::([0-9]+))?/g; | ||
|
||
/** | ||
* Describes an entry inb the log | ||
*/ | ||
export interface LogEntry { | ||
readonly level: LogLevel; | ||
readonly time: Date; | ||
readonly category: string; | ||
readonly message: string; | ||
} | ||
|
||
/** | ||
* Log entry implementation that formats the message on demand | ||
* @internal | ||
*/ | ||
export class LoggerEntry implements LogEntry { | ||
|
||
public readonly time = new Date(); | ||
private formattedMessage?: string; | ||
|
||
/** | ||
* Formatted message of the log entry (lazy) | ||
*/ | ||
public get message() { | ||
if (!this.formattedMessage) { | ||
this.formattedMessage = this.formatMessage(this.args); | ||
} | ||
return this.formattedMessage; | ||
} | ||
|
||
constructor( | ||
public readonly level: LogLevel, | ||
public readonly category: string, | ||
public readonly args: unknown[]) { | ||
} | ||
|
||
/** | ||
* Format the message from the arguments array | ||
* @returns Formatted message | ||
*/ | ||
private formatMessage(args: any[]) : string { | ||
if (args.length === 0) { | ||
return ''; | ||
} else if (args.length === 1) { | ||
return this.formatArg(args[0], 0); | ||
} | ||
const stringArgs = args.map((arg, index) => this.formatArg(arg, index)); | ||
const parts: string[] = []; | ||
for (let index = 0; index < stringArgs.length; index++) { | ||
parts.push(this.formatArgs(stringArgs, index)); | ||
} | ||
return parts.join(' '); | ||
} | ||
|
||
/** | ||
* Format a single argument for the log message | ||
* @param arg Argument to format | ||
* @param index Index of the argument | ||
* @returns Formatted argument | ||
*/ | ||
private formatArg(arg: unknown, index: number) : string { | ||
if (arg instanceof Error) { | ||
const error = getErrorMessage(arg); | ||
return !index ? error : `\n${error}`; | ||
} else if (arg !== null && typeof arg === 'object') { | ||
try { | ||
return JSON.stringify(arg, undefined, 2); | ||
} catch(err) { | ||
return '[Object]'; | ||
} | ||
} else if (typeof arg === 'function') { | ||
try { | ||
return this.formatArg(arg(), index); | ||
} catch(err) { | ||
return `(<function error> ${this.formatArg(err, index)})`; | ||
} | ||
} else if (typeof arg !== 'string') { | ||
return String(arg); | ||
} | ||
|
||
return arg; | ||
} | ||
|
||
private formatArgs(args: string[], index: number) : string { | ||
if (index + 1 >= args.length) { | ||
return args[index]; | ||
} | ||
|
||
const offset = index; | ||
const formattedArg = args[index].replace(FORMAT_REGEX, (fullMatch, type, options) => { | ||
if (index + 1 >= args.length) { | ||
return fullMatch; | ||
} | ||
const formatted = this.formatArgs(args, ++index); | ||
if (type === 'S') { | ||
return formatted.toUpperCase(); | ||
} else if (type === 'd') { | ||
return Number(formatted).toFixed(options ? Number(options) : 2); | ||
} else if (type === 'i') { | ||
return Number(formatted).toFixed(0); | ||
} | ||
return formatted; | ||
}); | ||
|
||
if (offset !== index) { | ||
args.splice(offset + 1, index - offset); | ||
} | ||
return formattedArg; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters