UNPKG

logops

Version:

Simple and performant nodejs JSON logger

277 lines (258 loc) 8.51 kB
/** * @license * Copyright 2015 Telefonica Investigación y Desarrollo, S.A.U * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ const formatters = require('./formatters'); const levels = ['DEBUG', 'INFO', 'WARN', 'ERROR', 'FATAL']; const DEFAULT_LEVEL = 'INFO'; module.exports = logops(); function noop() {}; function logops(options) { let opts = merge({ level: process.env.LOGOPS_LEVEL || DEFAULT_LEVEL, format: formatters[process.env.LOGOPS_FORMAT] || (process.env.NODE_ENV === 'development' ? formatters.dev : formatters.json ), getContext: noop, stream: process.stdout, }, options); let API = {}; /** * Internal private function that implements a decorator to all * the level functions. * * @param {String} level one of * ['DEBUG', 'INFO', 'WARN', 'ERROR', 'FATAL'] */ function logWrap(level) { return function log() { let context, message, args, trace, err; if (arguments[0] instanceof Error) { // log.<level>(err, ...) context = API.getContext(); args = Array.prototype.slice.call(arguments, 1); if (!args.length) { // log.<level>(err) err = arguments[0]; message = err.name + ': ' + err.message; } else { // log.<level>(err, "More %s", "things") // Use the err as context information err = arguments[0]; message = arguments[1]; args = Array.prototype.slice.call(args, 1); } } else if (arguments[0] == null || (typeof (arguments[0]) !== 'object' && arguments[0] !== null) || Array.isArray(arguments[0])) { // log.<level>(msg, ...) context = API.getContext(); message = arguments[0]; args = Array.prototype.slice.call(arguments, 1); } else { // log.<level>(fields, msg, ...) context = merge(API.getContext(), arguments[0]); message = arguments[1]; args = Array.prototype.slice.call(arguments, 2); } trace = API.format(level, context || {}, message, args, err); API.stream.write(trace + '\n'); }; } /** * Sets the enabled logging level. * All the disabled logging methods are replaced by a noop, * so there is not any performance penalty at production using an undesired level * * @param {String} level */ function setLevel(level) { opts.level = level; let logLevelIndex = levels.indexOf(opts.level.toUpperCase()); levels.forEach((logLevel) => { let fn; if (logLevelIndex <= levels.indexOf(logLevel)) { fn = logWrap(logLevel); } else { fn = noop; } API[logLevel.toLowerCase()] = fn; }); } /** * The exported API. * The following methods are added dynamically * API.debug * API.info * API.warn * API.error * API.fatal * * @type {Object} */ API = { /** * The stream where the logger will write string traces * Defaults to process.stdout */ stream: opts.stream, setStream: (stream) => { API.stream = stream; }, /** * Sets the enabled logging level. * All the disabled logging methods are replaced by a noop, * so there is not any performance penalty at production using an undesired level * * @param {String} level one of the following values * ['DEBUG', 'INFO', 'WARN', 'ERROR', 'FATAL'] */ setLevel: setLevel, /** * Gets the current log level. */ getLevel: () => opts.level, /** * Gets the context for a determinate trace. By default, this is a noop that * you can override if you are managing your execution context with node domains * * This function must return an object with the following fields * { * corr: {String}, * trans: {String}, * op: {String} * } * * If you are not using domain, you should pass the context information for * EVERY log call * * Both examples will produce the same trace * * Example usage not using getContext: * var logger = require('logops'); * req.context = { * corr: 'cbefb082-3429-4f5c-aafd-26b060d6a9fc', * trans: 'cbefb082-3429-4f5c-aafd-26b060d6a9fc', * op: 'SendEMail' * } * logger.info(req.context, 'This is an example'); * * Example using this feature: * var logger = require('logops'), * domain = require('domain'); * * logger.getContext = function domainContext() { * return domain.active.myCustomContext; * } * //... * * logger.info('This is an example'); * * @return {Object} The context object */ getContext: opts.getContext, setContextGetter: (fn) => { API.getContext = fn; }, /** * Creates a string representation for a trace. * * It checks the `LOGOPS_FORMAT` environment variable to use the built-in * format functions. It fallbacks to check the de-facto `NODE_ENV` env var * to use the `formatters.dev` when the value is `development`. Otherwise, it * will use the `formatters.pipe` (while in production, for example) * * Example * NODE_ENV=development node index.js * the logger will write traces for developers * * node index.js * the logger will write traces in a pipe format (assuming NODE_ENV nor * LOGOPS_FORMAT environment vars are defined with valid values) * * LOGOPS_FORMAT=json node index.js * the logger will write json traces * * You can override this func and manage by yourself the formatting. * * @param {String} level One of the following values * ['DEBUG', 'INFO', 'WARN', 'ERROR', 'FATAL'] * @param {Object} context Additional information to add to the trace * @param {String} message The main message to be added to the trace * @param {Array} args More arguments provided to the log function * * @return {String} The trace formatted */ format: opts.format, setFormat: (format) => { API.format = format; }, /** * Return an Object containing the available formatters ("dev", "pipe", "json"). * * Example using this feature to write JSON logs. * * var logger = require('logops'); * logger.format = logger.formatters.json; * logger.info('This is an example') * * @return {Object} The available formatters. */ formatters: formatters, /** * Creates a child logger with the this logger settings that will append the * passed context to the parent one (if any) * ``` * let child = logger.child({hostname: 'host.local'}); * child.info({app: 'server'}, 'Startup'); * // {"hostname":"host.local","app":"server","time":"2015-12-23T11:47:25.862Z","lvl":"INFO","msg":"Startup"} * ``` */ child: (localContext) => { // the current limitation for this approach is that child // loggers will overwrite the parent and localcontext when // calling againg `child.setContextGetter(fn)` or assign a function // to `child.getContext = fn` // But as the context is specified then creating the child, we can // considerer it a corner case and dont optimize for the use case return logops({ level: API.getLevel(), getContext: () => merge(API.getContext(), localContext), format: API.format, stream: API.stream, }); } }; setLevel(opts.level); return API; } /** * Merges accesible properties in two objects. * obj2 takes precedence when common properties are found * * @param {Object} obj1 * @param {Object} obj2 * @returns {{}} The merged, new, object */ function merge(obj1, obj2) { var res = {}, attrname; for (attrname in obj1) { res[attrname] = obj1[attrname]; } for (attrname in obj2) { res[attrname] = obj2[attrname]; } return res; }