UNPKG

@instana/collector

Version:

The Instana Node.js metrics and trace data collector

249 lines (212 loc) 8.28 kB
/* * (c) Copyright IBM Corp. 2021 * (c) Copyright Instana Inc. and contributors 2016 */ /* eslint-disable dot-notation */ 'use strict'; /** @type number */ let threadId = 0; try { threadId = require('worker_threads').threadId; } catch (ignored) { // We are apparently running in a Node.js version that does not have worker threads yet, thus we are on the main // thread (0). } const uninstrumentedLogger = require('./uninstrumentedLogger'); const loggerToAgentStream = require('./agent/loggerToAgentStream'); /** @type {import('@instana/core/src/core').GenericLogger} */ let instanaLogger; class InstanaLogger { /** * @param {import('@instana/core/src/core').GenericLogger} logger */ constructor(logger) { this.logger = logger; } /** * @param {import('@instana/core/src/core').GenericLogger} _logger */ setLogger(_logger) { this.logger = _logger; } warn = (/** @type {any[]} */ ...args) => this.logger?.warn?.(...args); error = (/** @type {any[]} */ ...args) => this.logger?.error?.(...args); info = (/** @type {any[]} */ ...args) => this.logger?.info?.(...args); debug = (/** @type {any[]} */ ...args) => this.logger?.debug?.(...args); trace = (/** @type {any[]} */ ...args) => this.logger?.trace?.(...args); } /** * @param {import('./types/collector').CollectorConfig} userConfig */ exports.init = function init(userConfig = {}) { /** @type {import('@instana/core/src/core').GenericLogger} */ let parentLogger; if (userConfig.logger && typeof userConfig.logger.child === 'function') { // A bunyan or pino logger has been provided via config. In either case we create a child logger directly under the // given logger which serves as the parent for all loggers we create later on. // BUG: Winston does not support child logger levels! // Neither in `.child(...)` nor with `level(...)`. // Setting INSTANA_DEBUG=true has no affect in the child winston logger. // It takes the parent logger level. // We cannot fix this in our side. // https://github.com/winstonjs/winston/issues/1854#issuecomment-710195110 parentLogger = userConfig.logger.child({ // TODO: Rename to "instana-nodejs-logger" in next major version. module: 'instana-nodejs-logger-parent', threadId }); // CASE: Attach custom instana meta attribute to filter out internal instana logs. // This will prevent these logs from being traced. parentLogger.__instana = true; } else if (userConfig.logger && hasLoggingFunctions(userConfig.logger)) { // CASE: Built-in console logger or log4js. We use it as is. // The __instana attribute identifies the Instana logger, distinguishing it from the client application logger, // and also prevents these logs from being traced // Extends config.logger with __instana while preserving its prototype and methods. parentLogger = Object.setPrototypeOf( { ...userConfig.logger, __instana: 1 }, Object.getPrototypeOf(userConfig.logger) ); } else { const os = require('os'); // No custom logger has been provided via config, we create a new pino logger as our internal // Instana logger. // If we override `base`, we have to provide the default pino values too. // Pino does not merge them together. // See https://github.com/pinojs/pino/blob/main/docs/api.md#base-object parentLogger = uninstrumentedLogger({ name: '@instana/collector', level: 'info', base: { threadId, pid: process.pid, hostname: os.hostname() }, // Set ISO timestamp format for the default logger timestamp: () => `,"time":"${new Date().toISOString()}"` }); } // Passing the log stream to agentStream for Debugging purposes // TODO: consider adding winston and other major loggers also for this if (isPinoLogger(parentLogger)) { // This consoleStream creates a destination stream for the logger that writes log data to the standard output. // Since we are using multistream here, this needs to be specified explicitly try { const consoleStream = uninstrumentedLogger.destination(parentLogger.destination); const multiStream = { /** * Custom write method to send logs to multiple destinations * @param {string} chunk */ write(chunk) { consoleStream.write(chunk); loggerToAgentStream.write(chunk); } }; const pinoOpts = { ...parentLogger.levels, level: parentLogger.level || 'info', base: parentLogger.bindings(), timestamp: () => `,"time":"${new Date().toISOString()}"` }; // CASE: Either its the customers pino instance (and probably a different pino installation/version) // or its our own pino instance. const instance = userConfig.logger || parentLogger; // NOTE: Pino uses symbols, because they don't use proper classes. const symbols = Object.getOwnPropertySymbols(instance); // CASE: We copy any settings from the pino instance such as custom formats. if (symbols && Array.isArray(symbols)) { const timeSym = symbols.find(sym => String(sym) === 'Symbol(pino.time)'); // @ts-ignore const timestampFn = instance[timeSym]; if (timestampFn) { pinoOpts.timestamp = timestampFn; } const formattersSym = symbols.find(sym => String(sym) === 'Symbol(pino.formatters)'); // @ts-ignore const formatters = instance[formattersSym]; if (formatters) { pinoOpts.formatters = formatters; } } parentLogger = uninstrumentedLogger(pinoOpts, multiStream); } catch (error) { // TODO: If any error occurs, we don't forward any logs to the agent. Why? // https://jsw.ibm.com/browse/INSTA-59515 parentLogger.debug(`An issue occurred while modifying the current logger: ${error.message}`); } } else if (parentLogger && parentLogger.addStream) { // in case we are using a bunyan logger we also forward logs to the agent parentLogger.addStream({ type: 'raw', stream: loggerToAgentStream, level: parentLogger._level || 'info' }); } if (process.env['INSTANA_DEBUG']) { setLoggerLevel(parentLogger, 'debug'); } else if (userConfig.level) { setLoggerLevel(parentLogger, userConfig.level); } else if (process.env['INSTANA_LOG_LEVEL']) { setLoggerLevel(parentLogger, process.env['INSTANA_LOG_LEVEL'].toLowerCase()); } if (!instanaLogger) { instanaLogger = new InstanaLogger(parentLogger); } else { instanaLogger.setLogger(parentLogger); } return instanaLogger; }; exports.getLogger = () => instanaLogger; /** * * @param {import('./agentConnection')} downstreamConnection */ exports.setDownstreamConnection = downstreamConnection => { loggerToAgentStream.setDownstreamConnection(downstreamConnection); }; /** * @param {import('@instana/core/src/core').GenericLogger | *} _logger * @returns {boolean} */ function hasLoggingFunctions(_logger) { return ( typeof _logger.debug === 'function' && typeof _logger.info === 'function' && typeof _logger.warn === 'function' && typeof _logger.error === 'function' ); } /** * @param {import("@instana/core/src/core").GenericLogger} _logger * @param {string|number} level * * Bunyan uses `level()` function to set the log level. * https://www.npmjs.com/package/bunyan#levels * * Pino uses `level = LEVEL`. * https://github.com/pinojs/pino/blob/main/docs/api.md#logger-level * * As far as I could figure out, Winston uses `level = LEVEL` or * `transports.console.level = 'info';` * * I could not figure out who uses `setLevel`. */ function setLoggerLevel(_logger, level) { if (typeof _logger.setLevel === 'function') { _logger.setLevel(level); } else if (typeof _logger.level === 'function') { _logger.level(level); } else { _logger.level = level; } } /** * @param {*} _logger * @returns {boolean} */ function isPinoLogger(_logger) { return ( _logger && typeof _logger === 'object' && typeof _logger.child === 'function' && typeof _logger.level === 'string' && typeof _logger.bindings === 'function' ); }