packages/opentelemetry-node/lib/luggite.js (278 lines of code) (raw):

/** * This file is from https://github.com/trentm/node-luggite * Copyright Trent Mick. Licensed under the MIT license. * * The luggite logging library for node.js. A logging lib that a technical * curmudgeon might even use. */ var {format, inspect} = require('util'); var assert = require('assert'); var stream = require('stream'); var safeStableStringify = require('safe-stable-stringify'); var EOL = '\n'; //---- Internal support stuff /** * Warn about an internal processing error. * * @param {string} msg Message with which to warn. * @param {string} [dedupKey] A short string key for this warning to * have its warning only printed once. */ function _warn(msg, dedupKey) { assert.ok(msg); if (dedupKey) { if (_warned[dedupKey]) { return; } _warned[dedupKey] = true; } process.stderr.write(msg + '\n'); } var _warned = {}; //---- Levels var TRACE = 10; var DEBUG = 20; var INFO = 30; var WARN = 40; var ERROR = 50; var FATAL = 60; /** @type {Record<string, number>} */ var levelFromName = { trace: TRACE, debug: DEBUG, info: INFO, warn: WARN, error: ERROR, fatal: FATAL, }; /** @type {Record<number, string>} */ var nameFromLevel = {}; Object.keys(levelFromName).forEach(function (name) { nameFromLevel[levelFromName[name]] = name; }); /** * Resolve a level number, name (upper or lowercase) to a level number value. * * @param {string|number} nameOrNum A level name (case-insensitive) or positive * integer level. */ function resolveLevel(nameOrNum) { var level; if (typeof nameOrNum === 'string') { level = levelFromName[nameOrNum.toLowerCase()]; if (!level) { throw new Error(format('unknown level name: "%s"', nameOrNum)); } } else if (typeof nameOrNum !== 'number') { throw new TypeError( format( 'cannot resolve level: invalid arg (%s):', typeof nameOrNum, nameOrNum ) ); } else if (nameOrNum < 0 || Math.floor(nameOrNum) !== nameOrNum) { throw new TypeError( format('level is not a positive integer: %s', nameOrNum) ); } else { level = nameOrNum; } return level; } /** * @param {any} obj * @returns {boolean} */ function isWritable(obj) { if (obj instanceof stream.Writable) { return true; } return typeof obj.write === 'function'; } //---- Logger class class Logger { /** * @param {Object} opts * @param {string} [opts.name] Name for the logger * @param {number|string} [opts.level] Log level to apply to this logger * @param {Record<string, any>} [opts.fields] */ constructor(opts) { opts = opts || {}; this._level = Infinity; this._stringify = safeStableStringify.configure({deterministic: false}); this._serializers = {err: errSerializer}; this._haveNonRawStreams = false; this._streams = []; this._addStream({ type: 'stream', stream: process.stdout, level: opts.level, }); // To allow storing raw log records (unrendered), `this._fields` must never // be mutated. Create a copy for any changes. this._fields = Object.assign({}, opts.fields); if (opts.name) { this._fields.name = opts.name; } } /** * @param {Object} s * @param {string} [s.type] * @param {number|string} [s.level] * @param {stream.Writable} [s.stream] * @param {number|string} [defaultLevel] */ _addStream(s, defaultLevel) { if (defaultLevel === null || defaultLevel === undefined) { defaultLevel = INFO; } s = Object.assign({}, s); // Implicit 'type' from other args. if (!s.type) { if (s.stream) { s.type = 'stream'; } } // @ts-expect-error -- adding a property `raw` which is not part of the param s.raw = s.type === 'raw'; // PERF: Allow for faster check in `_emit`. if (s.level !== undefined) { s.level = resolveLevel(s.level); } else { s.level = resolveLevel(defaultLevel); } if (s.level < this._level) { this._level = s.level; } switch (s.type) { case 'stream': assert.ok( isWritable(s.stream), '"stream" stream is not writable: ' + inspect(s.stream) ); break; case 'raw': break; default: throw new TypeError('unknown stream type "' + s.type + '"'); } this._streams.push(s); // @ts-expect-error -- inspecting the previously added `raw` property if (!this._haveNonRawStreams && !s.raw) { this._haveNonRawStreams = true; } } /** * Get/set the level of all streams on this logger. * * Get Usage: * // Returns the current log level (lowest level of all its streams). * log.level() -> INFO * * Set Usage: * log.level(INFO) // set all streams to level INFO * log.level('info') // can use 'info' et al aliases * * @param {number|string} [value] * @returns {number|undefined} */ level(value) { if (value === undefined) { return this._level; } var newLevel = resolveLevel(value); var len = this._streams.length; for (var i = 0; i < len; i++) { this._streams[i].level = newLevel; } this._level = newLevel; } /** * Apply registered serializers to the appropriate keys in the given fields. * * Pre-condition: This is only called if there is at least one serializer. * * @param {Record<string, any>} fields The log record fields. * @param {Record<string, boolean>} excludeFields Optional mapping of keys to `true` for * keys to NOT apply a serializer. */ _applySerializers(fields, excludeFields) { var self = this; // Check each serializer against these (presuming number of serializers // is typically less than number of fields). Object.keys(this._serializers).forEach(function (name) { if ( fields[name] === undefined || (excludeFields && excludeFields[name]) ) { return; } try { fields[name] = self._serializers[name](fields[name]); } catch (err) { _warn( format( 'luggite: ERROR: Exception thrown from the "%s" ' + 'serializer. This should never happen. This is a bug ' + 'in that serializer function.\n%s', name, err.stack || err ) ); fields[name] = format( '(Error in log "%s" serializer ' + 'broke field. See stderr for details.)', name ); } }); } /** * Emit a log record. * * @param {object} rec The log record */ _emit(rec) { var i; var str; if (this._haveNonRawStreams) { str = this._stringify(rec) + EOL; } var level = rec.level; for (i = 0; i < this._streams.length; i++) { var s = this._streams[i]; if (s.level <= level) { s.stream.write(s.raw ? rec : str); } } } } /** * Build a record object suitable for emitting from the arguments * provided to the a log emitter. * * @param {Logger} log * @param {number} minLevel * @param {Array<any>} args * @returns {object} */ function mkRecord(log, minLevel, args) { var excludeFields, fields, msgArgs; if (args[0] instanceof Error) { // `log.<level>(err, ...)` fields = { // Use this Logger's err serializer, if defined. err: log._serializers && log._serializers.err ? log._serializers.err(args[0]) : errSerializer(args[0]), }; excludeFields = {err: true}; if (args.length === 1) { msgArgs = [fields.err.message]; } else { msgArgs = args.slice(1); } } else if (typeof args[0] !== 'object' || Array.isArray(args[0])) { // `log.<level>(msg, ...)` fields = null; msgArgs = args.slice(); } else if (Buffer.isBuffer(args[0])) { // `log.<level>(buf, ...)` // Almost certainly an error, show `inspect(buf)`. See bunyan // issue #35. fields = null; msgArgs = args.slice(); msgArgs[0] = inspect(msgArgs[0]); } else { // `log.<level>(fields, msg, ...)` fields = args[0]; if ( fields && args.length === 1 && fields.err && fields.err instanceof Error ) { msgArgs = [fields.err.message]; } else { msgArgs = args.slice(1); } } // Build up the record object. var rec = Object.assign({}, log._fields); rec.level = minLevel; if (fields) { // TODO(perf): Possible to avoid this Object.assign by tweaking serializer API? var recFields = Object.assign({}, fields); if (log._serializers) { log._applySerializers(recFields, excludeFields); } Object.assign(rec, recFields); } rec.msg = format.apply(log, msgArgs); if (!rec.time) { rec.time = new Date(); } return rec; } /** * Build a log emitter function for level minLevel. I.e. this is the * creator of `log.info`, `log.error`, etc. * * @param {number} minLevel * @returns {function(Record<string, any> | string, ...any): void} */ function mkLogEmitter(minLevel) { return function LOG(...args) { var log = this; var rec = null; if (!this._emit) { // See <https://github.com/trentm/node-bunyan/issues/100> for // an example of how this can happen. var loc = new Error(''); loc.name = ''; _warn( 'usage error: attempt to log with an unbound log method' + loc.stack, 'unbound' ); return; } else if (args.length === 0) { // `log.<level>()` return this._level <= minLevel; } if (this._level <= minLevel) { rec = mkRecord(log, minLevel, args); this._emit(rec); } }; } /** * The functions below log a record at a specific level. * * Usages: * log.<level>() -> boolean is-trace-enabled * log.<level>(<Error> err, [<string> msg, ...]) * log.<level>(<string> msg, ...) * log.<level>(<object> fields, <string> msg, ...) * * where <level> is the lowercase version of the log level. E.g.: * * log.info() * * @param {Object} [fields] Record of additional fields to log. * @param {string} msg Log message. This can be followed by additional * arguments that are handled like * [util.format](http://nodejs.org/docs/latest/api/all.html#util.format). */ Logger.prototype.trace = mkLogEmitter(TRACE); Logger.prototype.debug = mkLogEmitter(DEBUG); Logger.prototype.info = mkLogEmitter(INFO); Logger.prototype.warn = mkLogEmitter(WARN); Logger.prototype.error = mkLogEmitter(ERROR); Logger.prototype.fatal = mkLogEmitter(FATAL); // ---- Serializers /** * A serializer is a function that serializes a JavaScript object to a * JSON representation for logging. There is a standard set of presumed * interesting objects in node.js-land. * * Serialize an Error object * (Core error properties are enumerable in node 0.4, not in 0.6). * @param {Error | Object} err * @returns {Object} */ function errSerializer(err) { if (!err || !err.stack) return err; var obj = { message: err.message, name: err.name, stack: err.stack, code: err.code, signal: err.signal, }; return obj; } //---- Exports /** * @param {Object} options * @param {string} [options.name] Name for the logger * @param {number|string} [options.level] Log level to apply to this logger * @param {Record<string, any>} [options.fields] * @returns {Logger} */ function createLogger(options) { return new Logger(options); } module.exports = { TRACE: TRACE, DEBUG: DEBUG, INFO: INFO, WARN: WARN, ERROR: ERROR, FATAL: FATAL, levelFromName: levelFromName, nameFromLevel: nameFromLevel, resolveLevel: resolveLevel, createLogger: createLogger, // Logger class is exported only for types, should not be used directly, use `createLogger` Logger: Logger, };