UNPKG

newrelic

Version:
222 lines (199 loc) 7.63 kB
/* * Copyright 2021 New Relic Corporation. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ 'use strict' const cat = require('../util/cat') const recordExternal = require('../metrics/recorders/http_external') const logger = require('../logger').child({ component: 'undici' }) const NAMES = require('../metrics/names') const symbols = require('../symbols') // eslint-disable-next-line n/no-unsupported-features/node-builtins const diagnosticsChannel = require('diagnostics_channel') const synthetics = require('../synthetics') const urltils = require('../util/urltils') const channels = [ { channel: 'undici:request:create', hook: requestCreateHook }, { channel: 'undici:request:headers', hook: requestHeadersHook }, { channel: 'undici:request:trailers', hook: endAndRestoreSegment }, { channel: 'undici:request:error', hook: endAndRestoreSegment } ] /** * Subscribes to all relevant undici hook points * See: https://github.com/nodejs/undici/blob/main/docs/api/DiagnosticsChannel.md * * @param agent * @param _undici * @param _modName * @param shim */ module.exports = function addUndiciChannels(agent, _undici, _modName, shim) { channels.forEach(({ channel, hook }, index) => { const boundHook = hook.bind(null, shim) diagnosticsChannel.subscribe(channel, boundHook) // store the bound hook for unsubscription later channels[index].boundHook = boundHook }) } module.exports.unsubscribe = function unsubscribe() { channels.forEach(({ channel, boundHook }) => { diagnosticsChannel.unsubscribe(channel, boundHook) }) } /** * Injects relevant DT headers for the external request * * @param {object} params object to fn * @param {Shim} params.transaction current transaction * @param {object} params.request undici request object * @param {object} params.config agent config */ function addDTHeaders({ transaction, config, request }) { const outboundHeaders = Object.create(null) synthetics.assignHeadersToOutgoingRequest(config, transaction, outboundHeaders) if (config.distributed_tracing.enabled) { transaction.insertDistributedTraceHeaders(outboundHeaders) } else if (config.cross_application_tracer.enabled) { cat.addCatHeaders(config, transaction, outboundHeaders) } else { logger.trace('Both DT and CAT are disabled, not adding headers!') } for (const key in outboundHeaders) { request.addHeader(key, outboundHeaders[key]) } } /** * Creates the external segment with url, procedure and request.parameters attributes * * @param {object} params object to fn * @param {Shim} params.shim instance of shim * @param {object} params.request undici request object * @param {TraceSegment} params.segment current active, about to be parent of external segment */ function createExternalSegment({ shim, request, segment }) { const url = new URL(request.origin + request.path) const obfuscatedPath = urltils.obfuscatePath(shim.agent.config, url.pathname) const name = NAMES.EXTERNAL.PREFIX + url.host + obfuscatedPath // Metrics for `External/<host>` will have a suffix of undici // We will have to see if this matters for people only using fetch // It's undici under the hood so ¯\_(ツ)_/¯ const externalSegment = shim.createSegment(name, recordExternal(url.host, 'undici'), segment) // the captureExternalAttributes expects queryParams to be an object, do conversion // to object see: https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams const queryParams = Object.fromEntries(url.searchParams.entries()) if (externalSegment) { externalSegment.start() shim.setActiveSegment(externalSegment) externalSegment.captureExternalAttributes({ protocol: url.protocol, hostname: url.hostname, host: url.host, method: request.method, port: url.port, path: obfuscatedPath, queryParams }) request[symbols.segment] = externalSegment } } /** * This event occurs after the Undici Request is created. * We will check current segment for opaque before creating the * external segment with the standard url/procedure/request.parameters * attributes. We will also attach relevant DT headers to outgoing http request. * * @param {Shim} shim instance of shim * @param {object} params object from undici hook * @param {object} params.request undici request object */ function requestCreateHook(shim, { request }) { const { config } = shim.agent const { transaction, segment } = shim.tracer.getContext() request[symbols.parentSegment] = segment request[symbols.transaction] = transaction if (!(segment || transaction) || segment?.opaque) { logger.trace( 'Not capturing data for outbound request (%s) because parent segment opaque (%s)', request.path, segment?.name ) return } try { createExternalSegment({ shim, request, segment }) addDTHeaders({ transaction, config, request }) } catch (err) { logger.warn(err, 'Unable to create external segment') } } /** * This event occurs after the response headers have been received. * We will add the relevant http response attributes to active segment. * Also add CAT specific keys to active segment. * * @param {Shim} shim instance of shim * @param {object} params object from undici hook * @param {object} params.request undici request object * @param {object} params.response { statusCode, headers, statusText } */ function requestHeadersHook(shim, { request, response }) { const { config } = shim.agent const activeSegment = request[symbols.segment] const transaction = request[symbols.transaction] if (!activeSegment) { return } activeSegment.addSpanAttribute('http.statusCode', response.statusCode) activeSegment.addSpanAttribute('http.statusText', response.statusText) if (config.cross_application_tracer.enabled && !config.distributed_tracing.enabled) { try { const { appData } = cat.extractCatHeaders(response.headers) const decodedAppData = cat.parseAppData(config, appData) const attrs = activeSegment.getAttributes() const url = new URL(attrs.url) cat.assignCatToSegment({ appData: decodedAppData, segment: activeSegment, host: url.host, transaction }) } catch (err) { logger.warn(err, 'Cannot add CAT data to segment') } } } /** * Gets the active segment, parent segment and transaction from given ctx(request, client connector) * and ends segment and sets the previous parent segment as the active segment. If an error exists it will add the error to the transaction * * @param {Shim} shim instance of shim * @param {object} params object from undici hook * @param {object} params.request or client connector * @param {Error} params.error error from undici request */ function endAndRestoreSegment(shim, { request, error }) { const { config } = shim.agent const activeSegment = request[symbols.segment] const parentSegment = request[symbols.parentSegment] const tx = request[symbols.transaction] if (activeSegment) { activeSegment.end() } if (error && tx && config.feature_flag.undici_error_tracking === true) { handleError(shim, tx, error) } if (parentSegment) { shim.setActiveSegment(parentSegment) } } /** * Adds the error to the active transaction * * @param {Shim} shim instance of shim * @param {Transaction} tx active transaction * @param {Error} error error from undici request */ function handleError(shim, tx, error) { logger.trace(error, 'Captured outbound error on behalf of the user.') shim.agent.errors.add(tx, error) }