logpm
Version:
JavaScript semantic logging
185 lines (184 loc) • 6.08 kB
JavaScript
/**
* Available logging levels
*/
export const LogLevel = Object.freeze({
Error: 1,
Warn: 2,
Info: 3,
Debug: 4,
Trace: 5,
});
const LogLevelTextMap = Object.freeze({
1: 'error',
2: 'warn',
3: 'info',
4: 'debug',
5: 'trace',
});
/**
* Special class with internals. Not accessible to outside world despite declaration.
* Warning: "export" keyword gets removed after tests
*/
class Internals {
#reTokens = /\{(?<token>[^{}]+?)\}/gim;
/**
*
* @param message Message to log with argument placeholders to be filled
* @param args Arguments to be inserted in message template
* @returns Evaluated message and tokens.
*/
tokenize(message, ...args) {
// filled in message with tokens
let msg = message || '';
// map of tokens inside message with matching values
const tokens = {};
// argument number in arguments array
let argPos = 0;
// current match of the regexp
let match = null;
// extract tokens into an object
while (null !== (match = this.#reTokens.exec(message))) {
/**
* Argument value from arguments array
*/
const argVal = args?.[argPos++] || null;
const token = match.groups?.token;
if (token && !tokens[token]) {
tokens[token] = argVal;
}
}
// fill message with evaluated token values
for (const [key, val] of Object.entries(tokens)) {
msg = msg.replaceAll(`{${key}}`, val);
}
return {
message: msg,
tokens,
};
}
}
const _internals = new Internals();
/**
* Provides current time in UTC format
*/
export class DefaultTimeProvider {
get now() {
return new Date().toISOString();
}
}
/**
* Writes logs as JSONs to STDOUT
*/
export class ConsoleLogStream {
write(obj) {
const text = JSON.stringify(obj);
console.log(text);
}
}
/**
* Semantic logging class
*/
export class Logger {
#context;
#scope;
#time;
#stream;
/**
* Create Logger instance
* @constructor
* @param context (reqired) Name the context where operations are logged. Usually name of the class
* @param scope (optional) Common scope object for all logged messages
* @param timeProvider (optional/advanced) Leave null for default behavior or provide custom way to assign timestamps
* @param stream (optional/advanced) Leave null for default behavior or pass custom nonblocking stream. Async operations are not supported and not desired
*/
constructor(context, scope, timeProvider, stream) {
this.#context = context || '';
if (typeof this.#context !== 'string') {
throw new Error('Context must be a string');
}
this.#scope = Object.freeze({ ...scope } || null);
this.#time = timeProvider || new DefaultTimeProvider();
this.#stream = stream || new ConsoleLogStream();
}
/**
* Creates new sub scope from existing logger
* @param context New scope context name
* @param scope Optional scope data
* @returns {Logger}
*/
scopeTo(context, scope) {
// merge current scope with provided scope
const innerScope = this.#scope ? { ...this.#scope, ...scope } : scope;
return new Logger(context, innerScope, this.#time, this.#stream);
}
/**
* Error log
* @param message Message to log. May contain placeholders in format: {name}
* @param args Arguments to fill within placeholders. Order of placeholders matches order of arguments
*/
e(message, ...args) {
this.ll(LogLevel.Error, message, ...args);
}
/**
* Warning log
* @param message Message to log. May contain placeholders in format: {name}
* @param args Arguments to fill within placeholders. Order of placeholders matches order of arguments
*/
w(message, ...args) {
this.ll(LogLevel.Warn, message, ...args);
}
/**
* Information log
* @param message Message to log. May contain placeholders in format: {name}
* @param args Arguments to fill within placeholders. Order of placeholders matches order of arguments
*/
i(message, ...args) {
this.ll(LogLevel.Info, message, ...args);
}
/**
* Debug log
* @param message Message to log. May contain placeholders in format: {name}
* @param args Arguments to fill within placeholders. Order of placeholders matches order of arguments
*/
d(message, ...args) {
this.ll(LogLevel.Debug, message, ...args);
}
/**
* Trace (verbose) log
* @param message Message to log. May contain placeholders in format: {name}
* @param args Arguments to fill within placeholders. Order of placeholders matches order of arguments
*/
t(message, ...args) {
this.ll(LogLevel.Trace, message, ...args);
}
/**
* Log with level
* @param level Logging level
* @param message Message to log. May contain placeholders in format: {name}
* @param args Arguments to fill within placeholders. Order of placeholders matches order of arguments
*/
ll(level, message, ...args) {
// prepare minimum field set object
let obj = {
...this.#scope,
'@timestamp': '',
context: '',
level: '',
message,
};
const tokenized = _internals.tokenize(message, ...args);
for (let key of Object.getOwnPropertyNames(tokenized.tokens)) {
// yes, property overwrites is allowed here
obj[key] = tokenized.tokens[key];
}
// overwite log operation's key fields
obj = {
...obj,
'@timestamp': this.#time.now,
context: this.#context,
level: LogLevelTextMap[level] || LogLevelTextMap[LogLevel.Info],
message: tokenized.message,
};
this.#stream.write(obj);
}
}