diff --git a/.changeset/spotty-icons-allow.md b/.changeset/spotty-icons-allow.md new file mode 100644 index 0000000000000..fd96d25098ffb --- /dev/null +++ b/.changeset/spotty-icons-allow.md @@ -0,0 +1,7 @@ +--- +"javascript-wiki": minor +"@graphql-mesh/plugin-newrelic": minor +"@graphql-mesh/types": minor +--- + +Newrelic Plugin diff --git a/declarations.d.ts b/declarations.d.ts index f9c66470190e4..adc86c6621d6a 100644 --- a/declarations.d.ts +++ b/declarations.d.ts @@ -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; +} diff --git a/examples/openapi-javascript-wiki/.gitignore b/examples/openapi-javascript-wiki/.gitignore index d444803ca796b..49ac5ea8eca08 100644 --- a/examples/openapi-javascript-wiki/.gitignore +++ b/examples/openapi-javascript-wiki/.gitignore @@ -1 +1,3 @@ .mesh +.env +newrelic_agent.log diff --git a/packages/plugins/newrelic/package.json b/packages/plugins/newrelic/package.json new file mode 100644 index 0000000000000..a2acc21438442 --- /dev/null +++ b/packages/plugins/newrelic/package.json @@ -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" + } +} diff --git a/packages/plugins/newrelic/src/index.ts b/packages/plugins/newrelic/src/index.ts new file mode 100644 index 0000000000000..82413a53d0371 --- /dev/null +++ b/packages/plugins/newrelic/src/index.ts @@ -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): MeshPlugin { + 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(); + + 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); +} diff --git a/packages/plugins/newrelic/yaml-config.graphql b/packages/plugins/newrelic/yaml-config.graphql new file mode 100644 index 0000000000000..52f6cba991e6a --- /dev/null +++ b/packages/plugins/newrelic/yaml-config.graphql @@ -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 +} diff --git a/packages/types/src/config-schema.json b/packages/types/src/config-schema.json index f12506e74cf84..1dd38bba82616 100644 --- a/packages/types/src/config-schema.json +++ b/packages/types/src/config-schema.json @@ -510,6 +510,9 @@ "$ref": "#/definitions/MockingConfig", "description": "Mock configuration for your source" }, + "newrelic": { + "$ref": "#/definitions/NewrelicConfig" + }, "rateLimit": { "$ref": "#/definitions/RateLimitPluginConfig", "description": "RateLimit plugin" @@ -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", diff --git a/packages/types/src/config.ts b/packages/types/src/config.ts index cb1d7a04b00f8..64768dd9c0990 100644 --- a/packages/types/src/config.ts +++ b/packages/types/src/config.ts @@ -1732,6 +1732,7 @@ export interface Plugin { immediateIntrospection?: any; liveQuery?: LiveQueryConfig; mock?: MockingConfig; + newrelic?: NewrelicConfig; rateLimit?: RateLimitPluginConfig; responseCache?: ResponseCacheConfig; [k: string]: any; @@ -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 */ diff --git a/website/src/generated-markdown/NewrelicConfig.generated.md b/website/src/generated-markdown/NewrelicConfig.generated.md new file mode 100644 index 0000000000000..fe8d3a1dcb6e6 --- /dev/null +++ b/website/src/generated-markdown/NewrelicConfig.generated.md @@ -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`) \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 861b824b952e2..86f74f8dcd85c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2585,6 +2585,11 @@ "@n1ru4l/graphql-live-query-patch" "^0.7.0" "@n1ru4l/in-memory-live-query-store" "^0.10.0" +"@envelop/newrelic@4.2.0": + version "4.2.0" + resolved "https://registry.yarnpkg.com/@envelop/newrelic/-/newrelic-4.2.0.tgz#6e702022d7d0a9d0a7c33146d86815529e4a9f41" + integrity sha512-WhvoR/2W63JyIMAG0MCKY15N2ta69S9blYjH2qIbgnxgDQaRphuD2h9/6TlnuEavgyar1ZyYs6A0ZtMW/FLzCg== + "@envelop/parser-cache@^4.6.0": version "4.6.0" resolved "https://registry.yarnpkg.com/@envelop/parser-cache/-/parser-cache-4.6.0.tgz#3ff71acdfbc51097d0dd8051cb961f856cddc400" @@ -3159,6 +3164,14 @@ "@grpc/proto-loader" "^0.7.0" "@types/node" ">=12.12.47" +"@grpc/grpc-js@^1.5.5": + version "1.6.11" + resolved "https://registry.yarnpkg.com/@grpc/grpc-js/-/grpc-js-1.6.11.tgz#74c04cde0cde4e8a88ffc514bd9cd1bb307815b9" + integrity sha512-e/adiPjUxf5cKYiAlV4m+0jJS4k6g2w78X7WTZB3ISOBzcCwm+cwjB2dSRfBHbu46inGGzQMmWAmsgYLg8yT5g== + dependencies: + "@grpc/proto-loader" "^0.7.0" + "@types/node" ">=12.12.47" + "@grpc/proto-loader@0.7.2", "@grpc/proto-loader@^0.7.0": version "0.7.2" resolved "https://registry.yarnpkg.com/@grpc/proto-loader/-/proto-loader-0.7.2.tgz#fa63178853afe1473c50cff89fe572f7c8b20154" @@ -3170,7 +3183,7 @@ protobufjs "^7.0.0" yargs "^16.2.0" -"@grpc/proto-loader@^0.6.0-pre17": +"@grpc/proto-loader@^0.6.0-pre17", "@grpc/proto-loader@^0.6.9": version "0.6.13" resolved "https://registry.yarnpkg.com/@grpc/proto-loader/-/proto-loader-0.6.13.tgz#008f989b72a40c60c96cd4088522f09b05ac66bc" integrity sha512-FjxPYDRTn6Ec3V0arm1FtSpmP6V50wuph2yILpyvTKzjc76oDdoihXqM1DzOW5ubvCC8GivfCnNtfaRE8myJ7g== @@ -4052,6 +4065,32 @@ debug "^4.3.2" pluralize "^8.0.0" +"@newrelic/aws-sdk@^4.1.1": + version "4.1.2" + resolved "https://registry.yarnpkg.com/@newrelic/aws-sdk/-/aws-sdk-4.1.2.tgz#6548bbe408542cd9ee2999319dc14c11e37e4a95" + integrity sha512-B83gZDS6eseNAMd41s8FTyd+JSxKHl4cN8kQnh1k5aYe0XB/Mi3hxn0/mxGzui84L4kL0GJCCg/UOg+A2ciSQg== + dependencies: + semver "^7.3.5" + +"@newrelic/koa@^6.1.0": + version "6.1.2" + resolved "https://registry.yarnpkg.com/@newrelic/koa/-/koa-6.1.2.tgz#98e8e24ea0bee97197f02030e3ad09912dadacf2" + integrity sha512-nmjr5hv+nRDC2NaRF4+iex41K6iJ/UCujgnj8oyht1grazJXQHq0dJZdMxUVNMMO+m4ukTeisSlI4d/H/W9JUw== + +"@newrelic/native-metrics@^7.1.1": + version "7.1.2" + resolved "https://registry.yarnpkg.com/@newrelic/native-metrics/-/native-metrics-7.1.2.tgz#e18af9447894d3f637c85bca19474cb81dc71d99" + integrity sha512-Ay0iLiwb/TIlbxxuWqxhrW1FxOSokKS09NKcRi1VXsMCMmvJiVhq6wvJcFvpoGLzvkTLLMFrJAHP0eJBKUpZfQ== + dependencies: + https-proxy-agent "^5.0.0" + nan "^2.15.0" + semver "^5.5.1" + +"@newrelic/superagent@^5.1.0": + version "5.1.1" + resolved "https://registry.yarnpkg.com/@newrelic/superagent/-/superagent-5.1.1.tgz#57e61878903718e1ba4310a365f65fae14a2b1a6" + integrity sha512-Bp2QtknriKHLKSfrBRyg4PjGJ8CCSkxYfZEDppOWmrGukJAP/9Vvr+ya0Mmj7SU8eIMMhaTvAnjvb2mVmX8wBw== + "@next/bundle-analyzer@^12.2.3": version "12.2.3" resolved "https://registry.yarnpkg.com/@next/bundle-analyzer/-/bundle-analyzer-12.2.3.tgz#8b4b934d28c09b9c11c4a074fbcc444402c8e017" @@ -5316,6 +5355,11 @@ dependencies: "@types/node" "*" +"@types/newrelic@7.0.3": + version "7.0.3" + resolved "https://registry.yarnpkg.com/@types/newrelic/-/newrelic-7.0.3.tgz#07ae3d0175f712e0fcaef69564dbe06b6039febb" + integrity sha512-MAaZYpJ9HEumg8MR4OFNDu8Cy4LnNc89aZ4pqXcBiQyo8SaQVv0sPj2JzzR1rQ/Rk86WL9vLjrDjuEg2vDMimg== + "@types/node-fetch@2.6.1": version "2.6.1" resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.6.1.tgz#8f127c50481db65886800ef496f20bbf15518975" @@ -5726,6 +5770,11 @@ "@typescript-eslint/types" "5.35.1" eslint-visitor-keys "^3.3.0" +"@tyriar/fibonacci-heap@^2.0.7": + version "2.0.9" + resolved "https://registry.yarnpkg.com/@tyriar/fibonacci-heap/-/fibonacci-heap-2.0.9.tgz#df3dcbdb1b9182168601f6318366157ee16666e9" + integrity sha512-bYuSNomfn4hu2tPiDN+JZtnzCpSpbJ/PNeulmocDy3xN2X5OkJL65zo6rPZp65cPPhLF9vfT/dgE+RtFRCSxOA== + "@udacity/types-service-worker-mock@1.2.0": version "1.2.0" resolved "https://registry.yarnpkg.com/@udacity/types-service-worker-mock/-/types-service-worker-mock-1.2.0.tgz#f047a26d036469f11b8f0f50d4c0901ae999bb93" @@ -7812,6 +7861,16 @@ concat-stream@^1.5.2: readable-stream "^2.2.2" typedarray "^0.0.6" +concat-stream@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-2.0.0.tgz#414cf5af790a48c60ab9be4527d56d5e41133cb1" + integrity sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A== + dependencies: + buffer-from "^1.0.0" + inherits "^2.0.3" + readable-stream "^3.0.2" + typedarray "^0.0.6" + concurrently@5.3.0: version "5.3.0" resolved "https://registry.yarnpkg.com/concurrently/-/concurrently-5.3.0.tgz#7500de6410d043c912b2da27de3202cb489b1e7b" @@ -13296,7 +13355,7 @@ json-stable-stringify@1.0.1: dependencies: jsonify "~0.0.0" -json-stringify-safe@^5.0.1: +json-stringify-safe@^5.0.0, json-stringify-safe@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus= @@ -15030,6 +15089,11 @@ nan@^2.12.1, nan@^2.14.0: resolved "https://registry.yarnpkg.com/nan/-/nan-2.15.0.tgz#3f34a473ff18e15c1b5626b62903b5ad6e665fee" integrity sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ== +nan@^2.15.0: + version "2.16.0" + resolved "https://registry.yarnpkg.com/nan/-/nan-2.16.0.tgz#664f43e45460fb98faf00edca0bb0d7b8dce7916" + integrity sha512-UdAqHyFngu7TfQKsCBgAA6pWDkT8MAO7d0jyOecVhN5354xbLqdn8mV9Tat9gepAupm0bt2DbeaSC8vS52MuFA== + nano-css@^5.3.1: version "5.3.4" resolved "https://registry.yarnpkg.com/nano-css/-/nano-css-5.3.4.tgz#40af6a83a76f84204f346e8ccaa9169cdae9167b" @@ -15102,6 +15166,26 @@ neo4j-driver@4.4.7: neo4j-driver-core "^4.4.7" rxjs "^6.6.3" +newrelic@8.8.0: + version "8.8.0" + resolved "https://registry.yarnpkg.com/newrelic/-/newrelic-8.8.0.tgz#bb8aa8915ec402709be300d1ba4a19a23f0176fa" + integrity sha512-1aaI7WigG2drbNWWfPITdwKWfVCbjdlHfsNa3AWk4mt9KahU/O0iS0R/OvWNInYIQNQ1hwMTGue5N2PfPKGxBw== + dependencies: + "@grpc/grpc-js" "^1.5.5" + "@grpc/proto-loader" "^0.6.9" + "@newrelic/aws-sdk" "^4.1.1" + "@newrelic/koa" "^6.1.0" + "@newrelic/superagent" "^5.1.0" + "@tyriar/fibonacci-heap" "^2.0.7" + async "^3.2.3" + concat-stream "^2.0.0" + https-proxy-agent "^5.0.0" + json-stringify-safe "^5.0.0" + readable-stream "^3.6.0" + semver "^5.3.0" + optionalDependencies: + "@newrelic/native-metrics" "^7.1.1" + next-seo@5.5.0: version "5.5.0" resolved "https://registry.yarnpkg.com/next-seo/-/next-seo-5.5.0.tgz#12bdfce60a6ae098f49617357a166c2d44dbc29e" @@ -17413,7 +17497,7 @@ readable-stream@2.3.7, readable-stream@^2.0.1, readable-stream@^2.0.6, readable- string_decoder "~1.1.1" util-deprecate "~1.0.1" -readable-stream@^3.0.0, readable-stream@^3.0.6, readable-stream@^3.1.1, readable-stream@^3.4.0: +readable-stream@^3.0.0, readable-stream@^3.0.2, readable-stream@^3.0.6, readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.6.0: version "3.6.0" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== @@ -18019,7 +18103,7 @@ semiver@^1.1.0: resolved "https://registry.yarnpkg.com/semiver/-/semiver-1.1.0.tgz#9c97fb02c21c7ce4fcf1b73e2c7a24324bdddd5f" integrity sha512-QNI2ChmuioGC1/xjyYwyZYADILWyW6AmS1UH6gDj/SFUUUS4MBAWs/7mxnkRPc/F4iHezDP+O8t0dO8WHiEOdg== -"semver@2 || 3 || 4 || 5", semver@^5.3.0, semver@^5.4.1, semver@^5.5.0, semver@^5.6.0, semver@^5.7.1: +"semver@2 || 3 || 4 || 5", semver@^5.3.0, semver@^5.4.1, semver@^5.5.0, semver@^5.5.1, semver@^5.6.0, semver@^5.7.1: version "5.7.1" resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==