UNPKG

@envelop/newrelic

Version:

Instrument your GraphQL application with New Relic reporting. Take advantage of Distributed tracing to monitor performance and errors whilst ultimately getting to the root cause of issues.

202 lines (201 loc) • 10.5 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.useNewRelic = void 0; const core_1 = require("@envelop/core"); const on_resolve_1 = require("@envelop/on-resolve"); const graphql_1 = require("graphql"); var AttributeName; (function (AttributeName) { AttributeName["COMPONENT_NAME"] = "Envelop_NewRelic_Plugin"; AttributeName["ANONYMOUS_OPERATION"] = "<anonymous>"; AttributeName["EXECUTION_RESULT"] = "graphql.execute.result"; AttributeName["EXECUTION_OPERATION_NAME"] = "graphql.execute.operationName"; AttributeName["EXECUTION_OPERATION_TYPE"] = "graphql.execute.operationType"; AttributeName["EXECUTION_OPERATION_DOCUMENT"] = "graphql.execute.document"; AttributeName["EXECUTION_VARIABLES"] = "graphql.execute.variables"; AttributeName["RESOLVER_FIELD_PATH"] = "graphql.resolver.fieldPath"; AttributeName["RESOLVER_TYPE_NAME"] = "graphql.resolver.typeName"; AttributeName["RESOLVER_RESULT_TYPE"] = "graphql.resolver.resultType"; AttributeName["RESOLVER_RESULT"] = "graphql.resolver.result"; AttributeName["RESOLVER_ARGS"] = "graphql.resolver.args"; })(AttributeName || (AttributeName = {})); const DEFAULT_OPTIONS = { includeOperationDocument: false, includeExecuteVariables: false, includeRawResult: false, trackResolvers: false, includeResolverArgs: false, rootFieldsNaming: false, skipError: () => false, }; const useNewRelic = (rawOptions) => { const options = { ...DEFAULT_OPTIONS, ...rawOptions, }; options.isExecuteVariablesRegex = options.includeExecuteVariables instanceof RegExp; options.isResolverArgsRegex = options.includeResolverArgs instanceof RegExp; const instrumentationApi$ = Promise.resolve().then(() => __importStar(require('newrelic'))).then(m => m.default || m) .then(({ shim }) => { if (!shim?.agent) { throw new Error('Agent unavailable. Please check your New Relic Agent configuration and ensure New Relic is enabled.'); } shim.agent.metrics .getOrCreateMetric(`Supportability/ExternalModules/${AttributeName.COMPONENT_NAME}`) .incrementCallCount(); return shim; }); const logger$ = instrumentationApi$.then(({ logger }) => { const childLogger = logger.child({ component: AttributeName.COMPONENT_NAME }); childLogger.info(`${AttributeName.COMPONENT_NAME} registered`); return childLogger; }); return { onPluginInit({ addPlugin }) { if (options.trackResolvers) { addPlugin((0, on_resolve_1.useOnResolve)(async ({ args: resolversArgs, info }) => { const instrumentationApi = await instrumentationApi$; const transactionNameState = instrumentationApi.agent.tracer.getTransaction().nameState; const delimiter = transactionNameState.delimiter; const logger = await logger$; const { returnType, path, parentType } = info; const formattedPath = flattenPath(path, delimiter); const currentSegment = instrumentationApi.getActiveSegment(); if (!currentSegment) { logger.trace('No active segment found at resolver call. Not recording resolver (%s).', formattedPath); return () => { }; } const resolverSegment = instrumentationApi.createSegment(`resolver${delimiter}${formattedPath}`, null, currentSegment); if (!resolverSegment) { logger.trace('Resolver segment was not created (%s).', formattedPath); return () => { }; } resolverSegment.start(); resolverSegment.addAttribute(AttributeName.RESOLVER_FIELD_PATH, formattedPath); resolverSegment.addAttribute(AttributeName.RESOLVER_TYPE_NAME, parentType.toString()); resolverSegment.addAttribute(AttributeName.RESOLVER_RESULT_TYPE, returnType.toString()); if (options.includeResolverArgs) { const rawArgs = resolversArgs || {}; const resolverArgsToTrack = options.isResolverArgsRegex ? filterPropertiesByRegex(rawArgs, options.includeResolverArgs) : rawArgs; resolverSegment.addAttribute(AttributeName.RESOLVER_ARGS, JSON.stringify(resolverArgsToTrack)); } return ({ result }) => { if (options.includeRawResult) { resolverSegment.addAttribute(AttributeName.RESOLVER_RESULT, JSON.stringify(result)); } resolverSegment.end(); }; })); } }, async onExecute({ args }) { const instrumentationApi = await instrumentationApi$; const transactionNameState = instrumentationApi.agent.tracer.getTransaction().nameState; const spanContext = instrumentationApi.agent.tracer.getSpanContext(); const delimiter = transactionNameState.delimiter; const rootOperation = args.document.definitions.find( // @ts-expect-error TODO: not sure how we will make it dev friendly definitionNode => definitionNode.kind === graphql_1.Kind.OPERATION_DEFINITION); const operationType = rootOperation.operation; const document = (0, graphql_1.print)(args.document); const operationName = options.extractOperationName?.(args.contextValue) || args.operationName || rootOperation.name?.value || AttributeName.ANONYMOUS_OPERATION; let rootFields = null; if (options.rootFieldsNaming) { const fieldNodes = rootOperation.selectionSet.selections.filter(selectionNode => selectionNode.kind === graphql_1.Kind.FIELD); rootFields = fieldNodes.map(fieldNode => fieldNode.name.value); } transactionNameState.setName(transactionNameState.prefix, transactionNameState.verb, delimiter, operationType + delimiter + operationName + (rootFields ? delimiter + rootFields.join('&') : '')); spanContext.addCustomAttribute(AttributeName.EXECUTION_OPERATION_NAME, operationName); spanContext.addCustomAttribute(AttributeName.EXECUTION_OPERATION_TYPE, operationType); options.includeOperationDocument && spanContext.addCustomAttribute(AttributeName.EXECUTION_OPERATION_DOCUMENT, document); if (options.includeExecuteVariables) { const rawVariables = args.variableValues || {}; const executeVariablesToTrack = options.isExecuteVariablesRegex ? filterPropertiesByRegex(rawVariables, options.includeExecuteVariables) : rawVariables; spanContext.addCustomAttribute(AttributeName.EXECUTION_VARIABLES, JSON.stringify(executeVariablesToTrack)); } const operationSegment = instrumentationApi.getActiveSegment(); return { onExecuteDone({ result }) { const sendResult = (singularResult) => { if (singularResult.data && options.includeRawResult) { spanContext.addCustomAttribute(AttributeName.EXECUTION_RESULT, JSON.stringify(singularResult)); } if (singularResult.errors && singularResult.errors.length > 0) { const agent = instrumentationApi.agent; const transaction = instrumentationApi.tracer.getTransaction(); for (const error of singularResult.errors) { if (options.skipError?.(error)) continue; agent.errors.add(transaction, JSON.stringify(error)); } } }; if ((0, core_1.isAsyncIterable)(result)) { return { onNext: ({ result: singularResult }) => { sendResult(singularResult); }, onEnd: () => { operationSegment.end(); }, }; } sendResult(result); operationSegment.end(); return {}; }, }; }, }; }; exports.useNewRelic = useNewRelic; function flattenPath(fieldPath, delimiter = '/') { const pathArray = []; let thisPath = fieldPath; while (thisPath) { if (typeof thisPath.key !== 'number') { pathArray.push(thisPath.key); } thisPath = thisPath.prev; } return pathArray.reverse().join(delimiter); } function filterPropertiesByRegex(initialObject, pattern) { const filteredObject = {}; for (const property of Object.keys(initialObject)) { if (pattern.test(property)) filteredObject[property] = initialObject[property]; } return filteredObject; }