Skip to content

Commit

Permalink
Prometheus Plugin (#4411)
Browse files Browse the repository at this point in the history
* Prometheus Plugin

* Better names

* Go

* Improve plugin DX
  • Loading branch information
ardatan authored Sep 1, 2022
1 parent fdc8d18 commit ca7994f
Show file tree
Hide file tree
Showing 13 changed files with 343 additions and 31 deletions.
6 changes: 6 additions & 0 deletions .changeset/curly-baboons-run.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@graphql-mesh/types': minor
'@graphql-mesh/plugin-prometheus': minor
---

New Prometheus Plugin
7 changes: 7 additions & 0 deletions .changeset/pretty-books-nail.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@graphql-mesh/config': minor
'@graphql-mesh/plugin-prometheus': minor
'@graphql-mesh/types': minor
---

Plugin factories now can return promises
2 changes: 2 additions & 0 deletions examples/openapi-javascript-wiki/.meshrc.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,5 @@ additionalResolvers:
documents:
- example-queries/*.graphql

plugins:
- newrelic: {}
37 changes: 37 additions & 0 deletions examples/openapi-javascript-wiki/newrelic_agent.log
Original file line number Diff line number Diff line change
Expand Up @@ -497,3 +497,40 @@
{"v":0,"level":30,"name":"newrelic","hostname":"ARDAL","pid":27674,"time":"2022-09-01T09:30:53.995Z","msg":"Valid event_harvest_config received. Updating harvest cycles. {\"report_period_ms\":5000,\"harvest_limits\":{\"error_event_data\":8,\"log_event_data\":833,\"analytic_event_data\":833,\"custom_event_data\":83}}"}
{"v":0,"level":30,"name":"newrelic","hostname":"ARDAL","pid":27674,"time":"2022-09-01T09:30:54.000Z","msg":"Agent state changed from connected to started."}
{"v":0,"level":30,"name":"newrelic","hostname":"ARDAL","pid":27674,"time":"2022-09-01T09:30:55.001Z","msg":"Starting initial 1000ms harvest."}
{"v":0,"level":30,"name":"newrelic","hostname":"ARDAL","pid":22729,"time":"2022-09-01T11:20:49.606Z","msg":"Using New Relic for Node.js. Agent version: 8.8.0; Node version: v18.8.0."}
{"v":0,"level":40,"name":"newrelic","hostname":"ARDAL","pid":22729,"time":"2022-09-01T11:20:49.609Z","msg":"New Relic for Node.js 8.8.0 has not been tested on Node.js v18.8.0. Please update the agent or downgrade your version of Node.js"}
{"v":0,"level":30,"name":"newrelic","hostname":"ARDAL","pid":22729,"time":"2022-09-01T11:20:49.661Z","msg":"Using LegacyContextManager"}
{"v":0,"level":40,"name":"newrelic","hostname":"ARDAL","pid":22729,"time":"2022-09-01T11:20:49.701Z","msg":"The newrelic module must be the first module required.\nThe following modules were required before newrelic and are not being instrumented:\n\tq: /home/ardat_000/Guild/graphql-mesh/node_modules/queue-microtask/index.js\n\tundici: /home/ardat_000/Guild/graphql-mesh/node_modules/undici/index.js\n\texpress: /home/ardat_000/Guild/graphql-mesh/node_modules/express/index.js"}
{"v":0,"level":30,"name":"newrelic","hostname":"ARDAL","pid":22729,"time":"2022-09-01T11:20:49.702Z","msg":"Agent state changed from stopped to starting."}
{"v":0,"level":30,"name":"newrelic","hostname":"ARDAL","pid":22729,"time":"2022-09-01T11:20:49.707Z","msg":"Starting New Relic for Node.js connection process."}
{"v":0,"level":30,"name":"newrelic","hostname":"ARDAL","pid":22729,"time":"2022-09-01T11:20:49.707Z","msg":"Agent state changed from starting to connecting."}
{"v":0,"level":40,"name":"newrelic","hostname":"ARDAL","pid":22729,"time":"2022-09-01T11:20:51.105Z","msg":"Unable to retrieve cached path for one or more modules with an already loaded parent. Forcing resolution. This should not occur during normal agent execution. Module resolution performance my be impacted. See trace-level logs for specific modules.","component":"shimmer"}
{"v":0,"level":30,"name":"newrelic","hostname":"ARDAL","pid":22729,"time":"2022-09-01T11:20:51.135Z","msg":"Envelop_NewRelic_Plugin registered","component":"Envelop_NewRelic_Plugin","module":"NewRelicAPI"}
{"v":0,"level":30,"name":"newrelic","hostname":"ARDAL","pid":22729,"time":"2022-09-01T11:20:51.135Z","msg":"Envelop_NewRelic_Plugin registered","component":"Envelop_NewRelic_Plugin","module":"NewRelicAPI"}
{"v":0,"level":30,"name":"newrelic","hostname":"ARDAL","pid":22729,"time":"2022-09-01T11:20:55.306Z","msg":"Agent state changed from connecting to connected."}
{"v":0,"level":30,"name":"newrelic","hostname":"ARDAL","pid":22729,"time":"2022-09-01T11:20:55.307Z","msg":"Connected to collector-001.eu01.nr-data.net:443 with agent run ID BZTaAvbT_KLqADb36BijQCtjEJXYAAgBAAAnIQEAAFjJAgQYlHdAAwAFOC44LjAABUFSREFMABVPcGVuQVBJSmF2YVNjcmlwdFdpa2k.","component":"collector_api"}
{"v":0,"level":30,"name":"newrelic","hostname":"ARDAL","pid":22729,"time":"2022-09-01T11:20:55.307Z","msg":"Reporting to: https://rpm.eu.newrelic.com/accounts/3602408/applications/412383040","component":"collector_api"}
{"v":0,"level":30,"name":"newrelic","hostname":"ARDAL","pid":22729,"time":"2022-09-01T11:20:55.308Z","msg":"Valid event_harvest_config received. Updating harvest cycles. {\"report_period_ms\":5000,\"harvest_limits\":{\"error_event_data\":8,\"log_event_data\":833,\"analytic_event_data\":833,\"custom_event_data\":83}}"}
{"v":0,"level":30,"name":"newrelic","hostname":"ARDAL","pid":22729,"time":"2022-09-01T11:20:55.313Z","msg":"Agent state changed from connected to started."}
{"v":0,"level":30,"name":"newrelic","hostname":"ARDAL","pid":22729,"time":"2022-09-01T11:20:56.314Z","msg":"Starting initial 1000ms harvest."}
{"v":0,"level":30,"name":"newrelic","hostname":"ARDAL","pid":23917,"time":"2022-09-01T11:24:55.621Z","msg":"Using New Relic for Node.js. Agent version: 8.8.0; Node version: v18.8.0."}
{"v":0,"level":40,"name":"newrelic","hostname":"ARDAL","pid":23917,"time":"2022-09-01T11:24:55.625Z","msg":"New Relic for Node.js 8.8.0 has not been tested on Node.js v18.8.0. Please update the agent or downgrade your version of Node.js"}
{"v":0,"level":30,"name":"newrelic","hostname":"ARDAL","pid":23917,"time":"2022-09-01T11:24:55.669Z","msg":"Using LegacyContextManager"}
{"v":0,"level":40,"name":"newrelic","hostname":"ARDAL","pid":23917,"time":"2022-09-01T11:24:55.706Z","msg":"The newrelic module must be the first module required.\nThe following modules were required before newrelic and are not being instrumented:\n\tq: /home/ardat_000/Guild/graphql-mesh/node_modules/queue-microtask/index.js\n\tundici: /home/ardat_000/Guild/graphql-mesh/node_modules/undici/index.js\n\texpress: /home/ardat_000/Guild/graphql-mesh/node_modules/express/index.js"}
{"v":0,"level":30,"name":"newrelic","hostname":"ARDAL","pid":23917,"time":"2022-09-01T11:24:55.707Z","msg":"Agent state changed from stopped to starting."}
{"v":0,"level":30,"name":"newrelic","hostname":"ARDAL","pid":23917,"time":"2022-09-01T11:24:55.712Z","msg":"Starting New Relic for Node.js connection process."}
{"v":0,"level":30,"name":"newrelic","hostname":"ARDAL","pid":23917,"time":"2022-09-01T11:24:55.712Z","msg":"Agent state changed from starting to connecting."}
{"v":0,"level":40,"name":"newrelic","hostname":"ARDAL","pid":23917,"time":"2022-09-01T11:24:56.254Z","msg":"Unable to retrieve cached path for one or more modules with an already loaded parent. Forcing resolution. This should not occur during normal agent execution. Module resolution performance my be impacted. See trace-level logs for specific modules.","component":"shimmer"}
{"v":0,"level":30,"name":"newrelic","hostname":"ARDAL","pid":23917,"time":"2022-09-01T11:24:56.281Z","msg":"Envelop_NewRelic_Plugin registered","component":"Envelop_NewRelic_Plugin","module":"NewRelicAPI"}
{"v":0,"level":30,"name":"newrelic","hostname":"ARDAL","pid":23917,"time":"2022-09-01T11:24:56.281Z","msg":"Envelop_NewRelic_Plugin registered","component":"Envelop_NewRelic_Plugin","module":"NewRelicAPI"}
{"v":0,"level":30,"name":"newrelic","hostname":"ARDAL","pid":23917,"time":"2022-09-01T11:25:00.803Z","msg":"Agent state changed from connecting to connected."}
{"v":0,"level":30,"name":"newrelic","hostname":"ARDAL","pid":23917,"time":"2022-09-01T11:25:00.803Z","msg":"Connected to collector-001.eu01.nr-data.net:443 with agent run ID BXB9o1SvJP9MADb36BijQCtjEJbIAAgBAAAnIQEAAF1tAgQYlHdAAwAFOC44LjAABUFSREFMABVPcGVuQVBJSmF2YVNjcmlwdFdpa2k.","component":"collector_api"}
{"v":0,"level":30,"name":"newrelic","hostname":"ARDAL","pid":23917,"time":"2022-09-01T11:25:00.803Z","msg":"Reporting to: https://rpm.eu.newrelic.com/accounts/3602408/applications/412383040","component":"collector_api"}
{"v":0,"level":30,"name":"newrelic","hostname":"ARDAL","pid":23917,"time":"2022-09-01T11:25:00.804Z","msg":"Valid event_harvest_config received. Updating harvest cycles. {\"report_period_ms\":5000,\"harvest_limits\":{\"error_event_data\":8,\"log_event_data\":833,\"analytic_event_data\":833,\"custom_event_data\":83}}"}
{"v":0,"level":30,"name":"newrelic","hostname":"ARDAL","pid":23917,"time":"2022-09-01T11:25:00.809Z","msg":"Agent state changed from connected to started."}
{"v":0,"level":30,"name":"newrelic","hostname":"ARDAL","pid":23917,"time":"2022-09-01T11:25:01.810Z","msg":"Starting initial 1000ms harvest."}
{"v":0,"level":30,"name":"newrelic","hostname":"ARDAL","pid":24103,"time":"2022-09-01T11:25:11.148Z","msg":"Using New Relic for Node.js. Agent version: 8.8.0; Node version: v18.8.0."}
{"v":0,"level":40,"name":"newrelic","hostname":"ARDAL","pid":24103,"time":"2022-09-01T11:25:11.152Z","msg":"New Relic for Node.js 8.8.0 has not been tested on Node.js v18.8.0. Please update the agent or downgrade your version of Node.js"}
{"v":0,"level":30,"name":"newrelic","hostname":"ARDAL","pid":24103,"time":"2022-09-01T11:25:11.197Z","msg":"Using LegacyContextManager"}
{"v":0,"level":50,"name":"newrelic","hostname":"ARDAL","pid":24103,"time":"2022-09-01T11:25:11.198Z","msg":"New Relic requires that you name this application!\nSet app_name in your newrelic.js file or set environment variable\nNEW_RELIC_APP_NAME. Not starting!"}
{"v":0,"level":50,"name":"newrelic","hostname":"ARDAL","pid":24103,"time":"2022-09-01T11:25:11.198Z","msg":"New Relic for Node.js was unable to bootstrap itself due to an error:","stack":"Error: New Relic requires that you name this application!\nSet app_name in your newrelic.js file or set environment variable\nNEW_RELIC_APP_NAME. Not starting!\n at createAgent (/home/ardat_000/Guild/graphql-mesh/node_modules/newrelic/index.js:140:11)\n at initialize (/home/ardat_000/Guild/graphql-mesh/node_modules/newrelic/index.js:81:15)\n at Object.<anonymous> (/home/ardat_000/Guild/graphql-mesh/node_modules/newrelic/index.js:36:3)\n at Module._compile (node:internal/modules/cjs/loader:1119:14)\n at Module._extensions..js (node:internal/modules/cjs/loader:1173:10)\n at Object.require.extensions.<computed> [as .js] (/home/ardat_000/Guild/graphql-mesh/node_modules/ts-node/src/index.ts:1608:43)\n at Module.load (node:internal/modules/cjs/loader:997:32)\n at Function.Module._load (node:internal/modules/cjs/loader:838:12)\n at Module.require (node:internal/modules/cjs/loader:1021:19)\n at require (node:internal/modules/cjs/helpers:103:18)","message":"New Relic requires that you name this application!\nSet app_name in your newrelic.js file or set environment variable\nNEW_RELIC_APP_NAME. Not starting!"}
14 changes: 11 additions & 3 deletions packages/config/src/process.ts
Original file line number Diff line number Diff line change
Expand Up @@ -300,7 +300,11 @@ export async function processConfig(
if (options.generateCode) {
importCodes.push(`import { ${importName} } from ${JSON.stringify(moduleName)};`);
codes.push(
`additionalEnvelopPlugins[${pluginIndex}] = ${importName}(${JSON.stringify(pluginConfig, null, 2)}))`
`additionalEnvelopPlugins[${pluginIndex}] = await ${importName}(${JSON.stringify(
pluginConfig,
null,
2
)}))`
);
}
return pluginFactory(pluginConfig);
Expand All @@ -319,7 +323,7 @@ export async function processConfig(
if (options.generateCode) {
importName = pascalCase('use_' + pluginName);
importCodes.push(`import ${importName} from ${JSON.stringify(moduleName)};`);
codes.push(`additionalEnvelopPlugins[${pluginIndex}] = ${importName}({
codes.push(`additionalEnvelopPlugins[${pluginIndex}] = await ${importName}({
...(${JSON.stringify(pluginConfig, null, 2)}),
logger: logger.child(${JSON.stringify(pluginName)}),
cache,
Expand All @@ -335,7 +339,11 @@ export async function processConfig(
if (options.generateCode) {
importCodes.push(`import { ${importName} } from ${JSON.stringify(moduleName)};`);
codes.push(
`additionalEnvelopPlugins[${pluginIndex}] = ${importName}(${JSON.stringify(pluginConfig, null, 2)}]`
`additionalEnvelopPlugins[${pluginIndex}] = await ${importName}(${JSON.stringify(
pluginConfig,
null,
2
)}]`
);
}
}
Expand Down
45 changes: 17 additions & 28 deletions packages/plugins/newrelic/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,30 +6,23 @@ 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';
import { shim as instrumentationApi } from 'newrelic';

enum AttributeName {
COMPONENT_NAME = 'Envelop_NewRelic_Plugin',
}
const EnvelopAttributeName = '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;
});
if (!instrumentationApi?.agent) {
options.logger.error(
'Agent unavailable. Please check your New Relic Agent configuration and ensure New Relic is enabled.'
);
return {};
}

instrumentationApi.agent.metrics
.getOrCreateMetric(`Supportability/ExternalModules/${EnvelopAttributeName}`)
.incrementCallCount();

const logger = instrumentationApi.logger.child({ component: EnvelopAttributeName });

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

Expand All @@ -48,13 +41,11 @@ export default function useMeshNewrelic(options: MeshPluginOptions<YamlConfig.Ne
})
);
},
async onExecute({ args: { contextValue } }) {
const instrumentationApi = await instrumentationApi$;
onExecute({ args: { contextValue } }) {
const operationSegment = instrumentationApi.getActiveSegment() || instrumentationApi.getSegment();
segmentByContext.set(contextValue, operationSegment);
},
async onDelegate({ sourceName, fieldName, args, context, key }) {
const instrumentationApi = await instrumentationApi$;
onDelegate({ sourceName, fieldName, args, context, key }) {
const parentSegment =
instrumentationApi.getActiveSegment() || instrumentationApi.getSegment() || segmentByContext.get(context);
const transaction = parentSegment?.transaction;
Expand All @@ -80,9 +71,7 @@ export default function useMeshNewrelic(options: MeshPluginOptions<YamlConfig.Ne
}
return undefined;
},
async onFetch({ url, options, context }) {
const instrumentationApi = await instrumentationApi$;
const logger = await logger$;
onFetch({ url, options, context }) {
const agent = instrumentationApi?.agent;
const parentSegment =
instrumentationApi.getActiveSegment() || instrumentationApi.getSegment() || segmentByContext.get(context);
Expand Down
44 changes: 44 additions & 0 deletions packages/plugins/prometheus/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
{
"name": "@graphql-mesh/plugin-prometheus",
"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/prometheus"
},
"peerDependencies": {
"graphql": "*",
"prom-client": "^13 || ^14.0.0"
},
"dependencies": {
"@envelop/prometheus": "6.6.0",
"@graphql-mesh/utils": "0.41.0",
"@graphql-mesh/types": "0.81.0",
"tslib": "^2.4.0"
},
"devDependencies": {
"prom-client": "14.0.1"
},
"publishConfig": {
"access": "public",
"directory": "dist"
}
}
79 changes: 79 additions & 0 deletions packages/plugins/prometheus/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { usePrometheus } from '@envelop/prometheus';
import { MeshPlugin, MeshPluginOptions, YamlConfig } from '@graphql-mesh/types';
import { getHeadersObj, loadFromModuleExportExpression } from '@graphql-mesh/utils';
import { Histogram, register as defaultRegistry, Registry } from 'prom-client';

export default async function useMeshPrometheus(
pluginOptions: MeshPluginOptions<YamlConfig.PrometheusConfig>
): Promise<MeshPlugin<any>> {
const registry = pluginOptions.registry
? await loadFromModuleExportExpression<Registry>(pluginOptions.registry, {
cwd: pluginOptions.baseDir,
importFn: pluginOptions.importFn,
defaultExportName: 'default',
})
: defaultRegistry;
const fetchHistogram = new Histogram({
name: 'graphql_mesh_fetch_duration',
help: 'Time spent on outgoing HTTP calls',
labelNames: ['url', 'method', 'requestHeaders', 'statusCode', 'statusText', 'responseHeaders'],
registers: [registry],
});
const delegateHistogram = new Histogram({
name: 'graphql_mesh_delegate_duration',
help: 'Time spent on delegate execution',
labelNames: ['sourceName', 'typeName', 'fieldName', 'args', 'key'],
registers: [registry],
});
return {
onPluginInit({ addPlugin }) {
addPlugin(
usePrometheus({
...pluginOptions,
registry,
})
);
},
onDelegate({ sourceName, typeName, fieldName, args, key }) {
if (pluginOptions.delegation !== false) {
const start = Date.now();
return () => {
const end = Date.now();
const duration = end - start;
delegateHistogram.observe(
{
sourceName,
typeName,
fieldName,
args: JSON.stringify(args),
key: JSON.stringify(key),
},
duration
);
};
}
return undefined;
},
onFetch({ url, options }) {
if (pluginOptions.fetch !== false) {
const start = Date.now();
return ({ response }) => {
const end = Date.now();
const duration = end - start;
fetchHistogram.observe(
{
url,
method: options.method,
requestHeaders: JSON.stringify(options.headers),
statusCode: response.status,
statusText: response.statusText,
responseHeaders: JSON.stringify(getHeadersObj(response.headers)),
},
duration
);
};
}
return undefined;
},
};
}
21 changes: 21 additions & 0 deletions packages/plugins/prometheus/yaml-config.graphql
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
extend type Plugin {
prometheus: PrometheusConfig
}

type PrometheusConfig @md {
requestCount: Boolean
requestTotalDuration: Boolean
requestSummary: Boolean
parse: Boolean
validate: Boolean
contextBuilding: Boolean
execute: Boolean
errors: Boolean
resolvers: Boolean
resolversWhiteList: [String]
deprecatedFields: Boolean
delegation: Boolean
fetch: Boolean
skipIntrospection: Boolean
registry: String
}
Loading

1 comment on commit ca7994f

@vercel
Copy link

@vercel vercel bot commented on ca7994f Sep 1, 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.