UNPKG

@graphql-mesh/plugin-newrelic

Version:
176 lines (175 loc) • 9.41 kB
import newRelic from 'newrelic'; import attributeFilter from 'newrelic/lib/config/attribute-filter.js'; import NAMES from 'newrelic/lib/metrics/names.js'; import recordExternal from 'newrelic/lib/metrics/recorders/http_external.js'; import cat from 'newrelic/lib/util/cat.js'; import { useNewRelic } from '@envelop/newrelic'; import { process } from '@graphql-mesh/cross-helpers'; import { stringInterpolator } from '@graphql-mesh/string-interpolation'; import { getHeadersObj, mapMaybePromise } from '@graphql-mesh/utils'; const DESTS = attributeFilter.DESTINATIONS; const EnvelopAttributeName = 'Envelop_NewRelic_Plugin'; function isPromise(value) { return value?.then != null; } export default function useMeshNewrelic(options, { instrumentationApi = newRelic?.shim, agentApi = newRelic } = {}) { if (!instrumentationApi?.agent) { options.logger.debug('Agent unavailable. Please check your New Relic Agent configuration and ensure New Relic is enabled.'); return {}; } instrumentationApi.agent.metrics .getOrCreateMetric(`Supportability/ExternalModules/${EnvelopAttributeName}`) .incrementCallCount(); const logger = instrumentationApi.logger.child({ component: EnvelopAttributeName }); const segmentByRequestContext = new WeakMap(); const headersToTrack = { 'user-agent': 'userAgent', host: 'host', 'content-type': 'contentType', 'content-length': 'contentLength', accept: 'accept', }; function sendResAttributes(res, currentSegment = instrumentationApi.getSegment(), currentTransaction = instrumentationApi.agent.tracer.getTransaction()) { currentSegment.addAttribute('http.statusCode', res.status); currentTransaction.trace.attributes.addAttribute(DESTS.TRANS_COMMON, 'http.statusCode', res.status); currentTransaction.statusCode = res.status; currentSegment.addAttribute('http.statusText', res.statusText); currentTransaction.trace.attributes.addAttribute(DESTS.TRANS_COMMON, 'http.statusText', res.statusText); } return { onPluginInit({ addPlugin }) { addPlugin(useNewRelic({ ...options, shim: instrumentationApi, extractOperationName: options.extractOperationName ? context => stringInterpolator.parse(options.extractOperationName, { context, env: process.env, }) : undefined, })); }, onRequest({ request, url, requestHandler, setRequestHandler }) { const currentTransaction = instrumentationApi.agent.tracer.getTransaction(); if (!currentTransaction) { setRequestHandler((...args) => agentApi.startWebTransaction(url.pathname, () => { const currentSegment = instrumentationApi.getSegment(); segmentByRequestContext.set(request, currentSegment); const currentTransaction = instrumentationApi.agent.tracer.getTransaction(); currentTransaction.trace.attributes.addAttribute(DESTS.TRANS_COMMON, 'request.uri', url.pathname); currentSegment.addAttribute('request.uri', url.pathname); currentTransaction.parsedUrl = url; currentTransaction.verb = request.method; currentTransaction.trace.attributes.addAttribute(DESTS.TRANS_COMMON, 'request.method', request.method); currentTransaction.nameState.setVerb(request.method); currentSegment.addAttribute('request.method', request.method); for (const headerName in headersToTrack) { const headerValue = request.headers.get(headerName); if (headerValue) { const key = `request.headers.${headersToTrack[headerName]}`; currentTransaction.trace.attributes.addAttribute(DESTS.TRANS_COMMON, key, headerValue); currentSegment.addAttribute(key, headerValue); } } const res$ = requestHandler(...args); return mapMaybePromise(res$, res => { sendResAttributes(res); return res; }); })); } }, onExecute({ args: { contextValue } }) { const operationSegment = instrumentationApi.getActiveSegment() || instrumentationApi.getSegment(); segmentByRequestContext.set(contextValue.request || contextValue, operationSegment); }, onDelegate({ sourceName, fieldName, args, context, key }) { const parentSegment = instrumentationApi.getActiveSegment() || instrumentationApi.getSegment() || segmentByRequestContext.get(context.request || context); const delimiter = parentSegment?.transaction?.nameState?.delimiter || '/'; const sourceSegment = instrumentationApi.createSegment(`source${delimiter}${sourceName || 'unknown'}${delimiter}${fieldName}`, null, parentSegment); if (!sourceSegment) { return undefined; } if (options.includeResolverArgs) { if (args) { sourceSegment.addAttribute('args', JSON.stringify(args)); } if (key) { sourceSegment.addAttribute('key', JSON.stringify(key)); } } sourceSegment.start(); return ({ result }) => { if (options.includeRawResult) { sourceSegment.addAttribute('result', JSON.stringify(result)); } sourceSegment.end(); }; }, onFetch({ url, options, context }) { const agent = instrumentationApi?.agent; const parentSegment = instrumentationApi.getActiveSegment() || instrumentationApi.getSegment() || (context ? segmentByRequestContext.get(context.request || context) : undefined); const parsedUrl = new URL(url); const name = NAMES.EXTERNAL.PREFIX + parsedUrl.host + parsedUrl.pathname; const httpDetailSegment = instrumentationApi.createSegment(name, recordExternal(parsedUrl.host, 'graphql-mesh'), parentSegment); if (!httpDetailSegment) { logger.error(`Unable to create segment for external request: ${name}`); return undefined; } httpDetailSegment.start(); httpDetailSegment.addAttribute('url', url); parsedUrl.searchParams.forEach((value, key) => { httpDetailSegment.addAttribute(`request.parameters.${key}`, value); }); httpDetailSegment.addAttribute('procedure', options.method || 'GET'); const transaction = parentSegment?.transaction; if (transaction) { const outboundHeaders = Object.create(null); if (agent.config.encoding_key && transaction.syntheticsHeader) { outboundHeaders['x-newrelic-synthetics'] = transaction.syntheticsHeader; } if (agent.config.distributed_tracing.enabled) { transaction.insertDistributedTraceHeaders(outboundHeaders); } else if (agent.config.cross_application_tracer.enabled) { cat.addCatHeaders(agent.config, transaction, outboundHeaders); } else { logger.trace('Both DT and CAT are disabled, not adding headers!'); } for (const key in outboundHeaders) { options.headers[key] = outboundHeaders[key]; } } for (const key in options.headers) { httpDetailSegment.addAttribute(`request.headers.${key}`, options.headers[key]); } return ({ response }) => { httpDetailSegment.addAttribute('http.statusCode', response.status); httpDetailSegment.addAttribute('http.statusText', response.statusText); const responseHeadersObj = getHeadersObj(response.headers); for (const key in responseHeadersObj) { httpDetailSegment.addAttribute(`response.headers.${key}`, responseHeadersObj[key]); } if (agent.config.cross_application_tracer.enabled && !agent.config.distributed_tracing.enabled) { try { const { appData } = cat.extractCatHeaders(responseHeadersObj); const decodedAppData = cat.parseAppData(agent.config, appData); const attrs = httpDetailSegment.getAttributes(); const url = new URL(attrs.url); cat.assignCatToSegment(decodedAppData, httpDetailSegment, url.host); } catch (err) { logger.warn(err, 'Cannot add CAT data to segment'); } } httpDetailSegment.end(); }; }, }; }