@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.
176 lines (175 loc) • 9.27 kB
JavaScript
import { isAsyncIterable } from '@envelop/core';
import { useOnResolve } from '@envelop/on-resolve';
import { print, Kind } from '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,
};
export const useNewRelic = (rawOptions) => {
const options = {
...DEFAULT_OPTIONS,
...rawOptions,
};
options.isExecuteVariablesRegex = options.includeExecuteVariables instanceof RegExp;
options.isResolverArgsRegex = options.includeResolverArgs instanceof RegExp;
const instrumentationApi$ = import('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(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 === Kind.OPERATION_DEFINITION);
const operationType = rootOperation.operation;
const document = 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 === 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 (isAsyncIterable(result)) {
return {
onNext: ({ result: singularResult }) => {
sendResult(singularResult);
},
onEnd: () => {
operationSegment.end();
},
};
}
sendResult(result);
operationSegment.end();
return {};
},
};
},
};
};
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;
}