newrelic
Version:
New Relic agent
196 lines (174 loc) • 6.54 kB
JavaScript
/*
* Copyright 2022 New Relic Corporation. All rights reserved.
* SPDX-License-Identifier: Apache-2.0
*/
const Stream = require('stream')
const {
isApplicationLoggingEnabled,
isLogForwardingEnabled,
isLocalDecoratingEnabled,
isMetricsEnabled,
createModuleUsageMetric,
incrementLoggingLinesMetrics
} = require('../util/application-logging')
const logger = require('../logger').child({ component: 'winston' })
const NrTransport = require('./nr-winston-transport')
module.exports = function instrument(agent, winston, _, shim) {
const config = agent.config
if (!isApplicationLoggingEnabled(config)) {
logger.debug('Application logging not enabled. Not instrumenting winston.')
return
}
shim.wrap(winston, 'createLogger', function wrapCreate(shim, createLogger) {
return function wrappedCreateLogger(...args) {
// re-assign the first arg to an object if does not exist so we can assign the default formatter
const opts = (args[0] = args[0] || {})
if (isStream(opts)) {
return createLogger.apply(this, args)
}
return performInstrumentation({
obj: this,
args,
opts,
agent,
winston,
shim,
registerLogger: createLogger
})
}
})
shim.wrap(winston.loggers, 'add', function wrapAdd(shim, add) {
return function wrappedAdd(...args) {
const id = args[0]
const opts = args[1] || {}
// add does nothing if the logger has already been added, so we
// have to do the same nothingness here.
const alreadyAdded = this.loggers.has(id)
if (alreadyAdded || isStream(opts)) {
return add.apply(this, args)
}
return performInstrumentation({
obj: this,
args,
opts,
agent,
winston,
shim,
registerLogger: add
})
}
})
}
/**
* Does the necessary instrumentation depending on application_logging configuration.
* It will register a formatter to track metrics or decorate message in formatter(if local log decoration)
* and register a transport do to the log fowarding
*
* @param {object} params object passed to function
* @param {object} params.obj instance of winston logger
* @param {Array} params.args arguments passed to the logger creation method(createLogger or logger.add)
* @param {object} params.opts the logger options argument
* @param {Agent} params.agent NR instance
* @param {object} params.winston the winston export
* @param {Shim} params.shim shim instance
* @param {Function} params.registerLogger the function to create winston logger
* @returns {object} the winston logger instance with relevant instrumentation
*/
function performInstrumentation({ obj, args, opts, agent, winston, shim, registerLogger }) {
const config = agent.config
createModuleUsageMetric('winston', agent.metrics)
if (isLocalDecoratingEnabled(config) || isMetricsEnabled(config)) {
registerFormatter({ opts, agent, winston })
}
const winstonLogger = registerLogger.apply(obj, args)
if (isLogForwardingEnabled(config, agent)) {
winstonLogger.add(new NrTransport({ agent }))
wrapConfigure({ shim, winstonLogger, agent })
}
return winstonLogger
}
/**
* Wraps logger.configure and checks the transports key in the arguments and adds the NrTransport as
* it will get cleared in configure.
*
* @param {object} params object passed to function
* @param {object} params.shim shim instance
* @param {object} params.winstonLogger instance of logger
* @param {object} params.agent NR agent
*/
function wrapConfigure({ shim, winstonLogger, agent }) {
shim.wrap(winstonLogger, 'configure', function nrConfigure(shim, configure) {
return function wrappedConfigure(...args) {
const transportsArg = args?.[0]?.transports
if (this.transports.length) {
const nrTransport = new NrTransport({ agent })
args[0].transports = Array.isArray(transportsArg)
? [...transportsArg, nrTransport]
: nrTransport
}
return configure.apply(this, args)
}
})
}
/**
* Apply a formatter to keep track of logging metrics, and in the case of local decorating appending
* the NR-LINKING metadata to message. We want to do this first so any formatter that is transforming
* data will have the changes.
*
* @param {object} params object passed to function
* @param {object} params.opts options from winston.createLogger
* @param {object} params.agent NR agent
* @param {object} params.winston exported winston package
*/
function registerFormatter({ opts, agent, winston }) {
const instrumentedFormatter = nrWinstonFormatter(agent, winston)
if (opts.format) {
opts.format = winston.format.combine(instrumentedFormatter(), opts.format)
} else {
// The default formatter for Winston is the JSON formatter. If the user
// has not provided a formatter through opts.format, we must emulate the
// default. Otherwise, the message symbol will not get attached to log
// messages and transports, e.g. the "Console" transport, will not be able
// to output logs correctly.
opts.format = winston.format.combine(instrumentedFormatter(), winston.format.json())
}
}
/**
* This formatter is being used to facilitate
* the two application logging use cases: metrics and local log decorating.
*
* Local log decorating appends `NR-LINKING` piped metadata to
* the message key in log line. You must configure a log forwarder to get
* this data to NR1.
*
* @param {object} agent NR agent
* @param {object} winston exported winston package
* @returns {object} log line NR-LINKING metadata on message when local log decorating is enabled
*/
function nrWinstonFormatter(agent, winston) {
const config = agent.config
const metrics = agent.metrics
return winston.format((logLine) => {
if (isMetricsEnabled(config)) {
incrementLoggingLinesMetrics(logLine.level, metrics)
}
if (isLocalDecoratingEnabled(config)) {
logLine.message += agent.getNRLinkingMetadata()
}
return logLine
})
}
/**
* winston allows you to compose a logger
* from an instantiated logger. Through a series
* of inherited classes, this logger instance is a Stream.
* Check what gets passed into `winston.createLogger` to avoid
* instrumenting an already instrumented instance of a logger.
*
* @param {*} obj obj to check if a stream
* @returns {boolean} is object is a stream or not
*/
function isStream(obj) {
return obj instanceof Stream
}