Skip to content

Commit

Permalink
New Relic plugin (#4398)
Browse files Browse the repository at this point in the history
* New Relic plugin

* Use new hook

* Go go

* Support batching
  • Loading branch information
ardatan authored Aug 31, 2022
1 parent 30bac02 commit 7a4023a
Show file tree
Hide file tree
Showing 10 changed files with 355 additions and 4 deletions.
7 changes: 7 additions & 0 deletions .changeset/spotty-icons-allow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"javascript-wiki": minor
"@graphql-mesh/plugin-newrelic": minor
"@graphql-mesh/types": minor
---

Newrelic Plugin
8 changes: 8 additions & 0 deletions declarations.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,11 @@ declare module 'ioredis-mock' {
import Redis from 'ioredis';
export default Redis;
}

declare module 'newrelic' {
const shim: any;
}

declare module 'newrelic/*' {
export = shim;
}
2 changes: 2 additions & 0 deletions examples/openapi-javascript-wiki/.gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
.mesh
.env
newrelic_agent.log
48 changes: 48 additions & 0 deletions packages/plugins/newrelic/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
{
"name": "@graphql-mesh/plugin-newrelic",
"version": "0.0.0",
"sideEffects": false,
"main": "dist/index.js",
"module": "dist/index.mjs",
"typings": "dist/index.d.ts",
"typescript": {
"definition": "dist/index.d.ts"
},
"exports": {
".": {
"require": "./dist/index.js",
"import": "./dist/index.mjs"
},
"./*": {
"require": "./dist/*.js",
"import": "./dist/*.mjs"
}
},
"license": "MIT",
"repository": {
"type": "git",
"url": "Urigo/graphql-mesh",
"directory": "packages/plugins/newrelic"
},
"peerDependencies": {
"graphql": "*",
"newrelic": "^7 || ^8.0.0"
},
"dependencies": {
"@envelop/newrelic": "4.2.0",
"@graphql-mesh/types": "0.80.2",
"@graphql-mesh/utils": "0.40.0",
"@graphql-mesh/cross-helpers": "0.2.2",
"@graphql-mesh/string-interpolation": "0.3.2",
"@envelop/core": "^2.3.2",
"tslib": "^2.4.0"
},
"devDependencies": {
"@types/newrelic": "7.0.3",
"newrelic": "8.8.0"
},
"publishConfig": {
"access": "public",
"directory": "dist"
}
}
140 changes: 140 additions & 0 deletions packages/plugins/newrelic/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import { Path } from '@envelop/core';
import { MeshPlugin, MeshPluginOptions, YamlConfig } from '@graphql-mesh/types';
import { useNewRelic } from '@envelop/newrelic';
import { stringInterpolator } from '@graphql-mesh/string-interpolation';
import { process } from '@graphql-mesh/cross-helpers';
import recordExternal from 'newrelic/lib/metrics/recorders/http_external';
import NAMES from 'newrelic/lib/metrics/names';
import cat from 'newrelic/lib/util/cat';
import { getHeadersObj } from '@graphql-mesh/utils';

enum AttributeName {
COMPONENT_NAME = 'Envelop_NewRelic_Plugin',
}

export default function useMeshNewrelic(options: MeshPluginOptions<YamlConfig.NewrelicConfig>): MeshPlugin<any> {
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;
});

const segmentByContext = new WeakMap<any, any>();

return {
onPluginInit({ addPlugin }) {
addPlugin(
useNewRelic({
...options,
extractOperationName: options.extractOperationName
? context =>
stringInterpolator.parse(options.extractOperationName, {
context,
env: process.env,
})
: undefined,
})
);
},
async onExecute({ args: { contextValue } }) {
const instrumentationApi = await instrumentationApi$;
const operationSegment = instrumentationApi.getActiveSegment() || instrumentationApi.getSegment();
segmentByContext.set(contextValue, operationSegment);
},
async onFetch({ url, options, context, info }) {
const instrumentationApi = await instrumentationApi$;
const logger = await logger$;
const agent = instrumentationApi?.agent;
const operationSegment = segmentByContext.get(context);
const transaction = operationSegment?.transaction;
if (transaction != null) {
const transactionNameState = transaction.nameState;
const delimiter = transactionNameState?.delimiter || '/';
const formattedPath = flattenPath(info.path, delimiter);
const sourceSegment = instrumentationApi.createSegment(
`source${delimiter}${(info as any).sourceName || 'unknown'}${delimiter}${formattedPath}`,
null,
operationSegment
);
if (!sourceSegment) {
logger.trace('Source segment was not created (%s).', formattedPath);
return 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'),
sourceSegment
);
if (httpDetailSegment) {
httpDetailSegment.start();
httpDetailSegment.addAttribute('url', url);
parsedUrl.searchParams.forEach((value, key) => {
httpDetailSegment.addAttribute(`request.parameters.${key}`, value);
});
httpDetailSegment.addAttribute('procedure', options.method || 'GET');
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];
}
}
return ({ response }) => {
httpDetailSegment.addAttribute('http.statusCode', response.status);
httpDetailSegment.addAttribute('http.statusText', response.statusText);
if (agent.config.cross_application_tracer.enabled && !agent.config.distributed_tracing.enabled) {
try {
const { appData } = cat.extractCatHeaders(getHeadersObj(response.headers));
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');
}
}
sourceSegment.end();
httpDetailSegment.end();
};
}
return undefined;
},
};
}

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);
}
13 changes: 13 additions & 0 deletions packages/plugins/newrelic/yaml-config.graphql
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
extend type Plugin {
newrelic: NewrelicConfig
}

type NewrelicConfig @md {
includeOperationDocument: Boolean
includeExecuteVariables: Boolean
includeRawResult: Boolean
trackResolvers: Boolean
includeResolverArgs: Boolean
rootFieldsNaming: Boolean
extractOperationName: String
}
31 changes: 31 additions & 0 deletions packages/types/src/config-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -510,6 +510,9 @@
"$ref": "#/definitions/MockingConfig",
"description": "Mock configuration for your source"
},
"newrelic": {
"$ref": "#/definitions/NewrelicConfig"
},
"rateLimit": {
"$ref": "#/definitions/RateLimitPluginConfig",
"description": "RateLimit plugin"
Expand Down Expand Up @@ -2104,6 +2107,34 @@
}
}
},
"NewrelicConfig": {
"additionalProperties": false,
"type": "object",
"title": "NewrelicConfig",
"properties": {
"includeOperationDocument": {
"type": "boolean"
},
"includeExecuteVariables": {
"type": "boolean"
},
"includeRawResult": {
"type": "boolean"
},
"trackResolvers": {
"type": "boolean"
},
"includeResolverArgs": {
"type": "boolean"
},
"rootFieldsNaming": {
"type": "boolean"
},
"extractOperationName": {
"type": "string"
}
}
},
"RateLimitPluginConfig": {
"additionalProperties": false,
"type": "object",
Expand Down
10 changes: 10 additions & 0 deletions packages/types/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1732,6 +1732,7 @@ export interface Plugin {
immediateIntrospection?: any;
liveQuery?: LiveQueryConfig;
mock?: MockingConfig;
newrelic?: NewrelicConfig;
rateLimit?: RateLimitPluginConfig;
responseCache?: ResponseCacheConfig;
[k: string]: any;
Expand Down Expand Up @@ -1834,6 +1835,15 @@ export interface UpdateMockStoreConfig {
fieldName?: string;
value?: string;
}
export interface NewrelicConfig {
includeOperationDocument?: boolean;
includeExecuteVariables?: boolean;
includeRawResult?: boolean;
trackResolvers?: boolean;
includeResolverArgs?: boolean;
rootFieldsNaming?: boolean;
extractOperationName?: string;
}
/**
* RateLimit plugin
*/
Expand Down
8 changes: 8 additions & 0 deletions website/src/generated-markdown/NewrelicConfig.generated.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@

* `includeOperationDocument` (type: `Boolean`)
* `includeExecuteVariables` (type: `Boolean`)
* `includeRawResult` (type: `Boolean`)
* `trackResolvers` (type: `Boolean`)
* `includeResolverArgs` (type: `Boolean`)
* `rootFieldsNaming` (type: `Boolean`)
* `extractOperationName` (type: `String`)
Loading

1 comment on commit 7a4023a

@vercel
Copy link

@vercel vercel bot commented on 7a4023a Aug 31, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.