Open Ignigena opened 1 year ago
Hi @Ignigena have done some changes for my own project, hope it will help, "newrelic": "^12.3.0"
import { ExecutionResult, getOperationAST, GraphQLError } from 'graphql';
import newrelic from 'newrelic';
import { DefaultContext, isAsyncIterable, Path, Plugin } from '@envelop/core';
import { useOnResolve } from '@envelop/on-resolve';
import logger from 'logger'; // we can't access the New Relic logger must be checked ! I use pino
export enum AttributeName {
/* eslint-disable no-unused-vars */
COMPONENT_NAME = 'NewRelic_Plugin',
ANONYMOUS_OPERATION = '<anonymous>',
EXECUTION_RESULT = 'graphql.execute.result',
EXECUTION_OPERATION_NAME = 'graphql.execute.operationName',
EXECUTION_OPERATION_TYPE = 'graphql.execute.operationType',
EXECUTION_OPERATION_DOCUMENT = 'graphql.execute.document',
EXECUTION_VARIABLES = 'graphql.execute.variables',
RESOLVER_FIELD_PATH = 'graphql.resolver.fieldPath',
RESOLVER_TYPE_NAME = 'graphql.resolver.typeName',
RESOLVER_RESULT_TYPE = 'graphql.resolver.resultType',
RESOLVER_RESULT = 'graphql.resolver.result',
RESOLVER_ARGS = 'graphql.resolver.args',
/* eslint-enable no-unused-vars */
}
export type UseNewRelicOptions = {
includeOperationDocument?: boolean;
includeExecuteVariables?: boolean | RegExp;
includeRawResult?: boolean;
trackResolvers?: boolean;
includeResolverArgs?: boolean | RegExp;
rootFieldsNaming?: boolean;
/* eslint-disable-next-line no-unused-vars */
extractOperationName?: (context: DefaultContext) => string | undefined;
/* eslint-disable-next-line no-unused-vars */
skipError?: (error: GraphQLError) => boolean;
};
interface InternalOptions extends UseNewRelicOptions {
isExecuteVariablesRegex?: boolean;
isResolverArgsRegex?: boolean;
}
const DEFAULT_OPTIONS: UseNewRelicOptions = {
includeOperationDocument: false,
includeExecuteVariables: false,
includeRawResult: false,
trackResolvers: false,
includeResolverArgs: false,
rootFieldsNaming: false,
skipError: () => false,
};
export const useNewRelic = (rawOptions?: UseNewRelicOptions): Plugin => {
const options: InternalOptions = {
...DEFAULT_OPTIONS,
...rawOptions,
};
options.isExecuteVariablesRegex =
options.includeExecuteVariables instanceof RegExp;
options.isResolverArgsRegex = options.includeResolverArgs instanceof RegExp;
if (!newrelic.getTransaction()) {
// Check if New Relic Agent is available
logger.warn(
'New Relic transaction method is not available. Check your New Relic Agent configuration.',
);
return {};
}
logger.info(`${AttributeName.COMPONENT_NAME} registered`);
return {
onPluginInit({ addPlugin }) {
if (options.trackResolvers) {
addPlugin(
useOnResolve(({ args: resolversArgs, info }) => {
const transaction = newrelic.getTransaction();
if (!transaction) {
// Check if parent transaction is available
logger.warn(
'No transaction found. Not recording resolver.',
);
return () => {};
}
const { returnType, path, parentType } = info;
const formattedPath = flattenPath(path, '/');
newrelic.startSegment(
`resolver/${formattedPath}`,
false,
() => {
logger.debug(
`Resolver segment started: ${formattedPath}`,
);
newrelic.addCustomSpanAttribute(
AttributeName.RESOLVER_FIELD_PATH,
formattedPath,
);
newrelic.addCustomSpanAttribute(
AttributeName.RESOLVER_TYPE_NAME,
parentType.toString(),
);
newrelic.addCustomSpanAttribute(
AttributeName.RESOLVER_RESULT_TYPE,
returnType.toString(),
);
if (options.includeResolverArgs) {
const rawArgs = resolversArgs || {};
const resolverArgsToTrack =
options.isResolverArgsRegex
? filterPropertiesByRegex(
rawArgs,
options.includeResolverArgs as RegExp,
)
: rawArgs;
newrelic.addCustomSpanAttribute(
AttributeName.RESOLVER_ARGS,
JSON.stringify(resolverArgsToTrack),
);
}
},
);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return ({ result }: { result: any }) => {
if (options.includeRawResult) {
newrelic.addCustomSpanAttribute(
AttributeName.RESOLVER_RESULT,
JSON.stringify(result),
);
}
logger.debug(
`Resolver segment ended: ${formattedPath}`,
);
};
}),
);
}
},
onExecute({ args }) {
const rootOperation = getOperationAST(
args.document,
args.operationName,
);
if (!rootOperation) {
logger.warn(
'No root operation found. Not recording transaction.',
);
return;
}
const operationType = rootOperation.operation;
const operationName =
options.extractOperationName?.(args.contextValue) ||
args.operationName ||
rootOperation.name?.value ||
AttributeName.ANONYMOUS_OPERATION;
newrelic.startSegment('graphql-operation', false, () => {
logger.debug(
`Operation segment started: ${operationType}/${operationName}`,
);
newrelic.addCustomAttribute(
AttributeName.EXECUTION_OPERATION_NAME,
operationName,
);
newrelic.addCustomAttribute(
AttributeName.EXECUTION_OPERATION_TYPE,
operationType,
);
if (options.includeExecuteVariables) {
const rawVariables = args.variableValues || {};
const executeVariablesToTrack =
options.isExecuteVariablesRegex
? filterPropertiesByRegex(
rawVariables,
options.includeExecuteVariables as RegExp,
)
: rawVariables;
newrelic.addCustomAttribute(
AttributeName.EXECUTION_VARIABLES,
JSON.stringify(executeVariablesToTrack),
);
}
return {
onExecuteDone({ result }: { result: ExecutionResult }) {
const sendResult = (
singularResult: ExecutionResult,
) => {
if (
singularResult.data &&
options.includeRawResult
) {
newrelic.addCustomAttribute(
AttributeName.EXECUTION_RESULT,
JSON.stringify(singularResult),
);
}
if (
singularResult.errors &&
singularResult.errors.length > 0
) {
for (const error of singularResult.errors) {
if (options.skipError?.(error)) continue;
newrelic.noticeError(error);
}
}
};
if (isAsyncIterable(result)) {
return {
onNext: ({
result: singularResult,
}: {
result: ExecutionResult;
}) => {
sendResult(singularResult);
},
onEnd: () => {
logger.debug('Operation segment ended.');
},
};
}
sendResult(result);
logger.debug('Operation segment ended.');
return {};
},
};
});
},
};
};
function flattenPath(fieldPath: Path, delimiter = '/') {
const pathArray = [];
let thisPath: Path | undefined = fieldPath;
while (thisPath) {
if (typeof thisPath.key !== 'number') {
pathArray.push(thisPath.key);
}
thisPath = thisPath.prev;
}
return pathArray.reverse().join(delimiter);
}
function filterPropertiesByRegex(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
initialObject: { [key: string]: any },
pattern: RegExp,
) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const filteredObject: Record<string, any> = {};
for (const property of Object.keys(initialObject)) {
if (pattern.test(property))
filteredObject[property] = initialObject[property];
}
return filteredObject;
}
Would you create a PR? @jordanco
Yes @ardatan, its my first time contributing, will check contributing rules.
Is your feature request related to a problem? Please describe. The current version of New Relic used in
@envelop/newrelic
is very badly out of date. We've been experiencing issues with the instrumentation and missing spans and other anomalies that New Relic support claims may be related to the outdated agent version.Unfortunately we aren't able to upgrade the version of New Relic in our installation to test because
peerDependencies
tops out at major version 8.Oddly enough, the
devDependencies
is already using New Relic version 9.x -- but meanwhile New Relic is on version 10.5.0Describe the solution you'd like
Expand the peer dependencies up to the latest major semver.
Describe alternatives you've considered
Additional context
Our team uses New Relic for our production apps, though we are still very early in our GraphQL adoption. More than happy to help out if there's need for testing or dev assistance to more closely follow New Relic's releases.