UNPKG

logpm

Version:
185 lines (184 loc) 6.08 kB
/** * 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); } }