n1ru4l / envelop

Envelop is a lightweight library allowing developers to easily develop, share, collaborate and extend their GraphQL execution layer. Envelop is the missing GraphQL plugin system.
https://envelop.dev
MIT License
788 stars 127 forks source link

Allow latest version of New Relic to be used #1917

Open Ignigena opened 1 year ago

Ignigena commented 1 year ago

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.0

Describe 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.

jordanco commented 1 month 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;
}
ardatan commented 1 month ago

Would you create a PR? @jordanco

jordanco commented 1 month ago

Yes @ardatan, its my first time contributing, will check contributing rules.