@instana/collector
Version:
The Instana Node.js metrics and trace data collector
249 lines (212 loc) • 8.28 kB
JavaScript
/*
* (c) Copyright IBM Corp. 2021
* (c) Copyright Instana Inc. and contributors 2016
*/
/* eslint-disable dot-notation */
;
/** @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'
);
}