Skip to content

Commit

Permalink
feat(prometheus): track Fusion subgraph execution
Browse files Browse the repository at this point in the history
  • Loading branch information
ardatan committed Mar 13, 2024
1 parent cea87fc commit 7a712ab
Show file tree
Hide file tree
Showing 8 changed files with 142 additions and 190 deletions.
6 changes: 6 additions & 0 deletions .changeset/sour-deers-love.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@graphql-mesh/plugin-prometheus": minor
"@graphql-mesh/types": patch
---

Track Fusion subgraphs
17 changes: 17 additions & 0 deletions packages/legacy/types/src/config-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -2626,6 +2626,23 @@
}
]
},
"delegationArgs": {
"type": "boolean"
},
"delegationKey": {
"type": "boolean"
},
"subgraphExecute": {
"description": "Any of: Boolean, String",
"anyOf": [
{
"type": "boolean"
},
{
"type": "string"
}
]
},
"fetch": {
"description": "Any of: Boolean, String",
"anyOf": [
Expand Down
6 changes: 6 additions & 0 deletions packages/legacy/types/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2134,6 +2134,12 @@ export interface PrometheusConfig {
* Any of: Boolean, String
*/
delegation?: boolean | string;
delegationArgs?: boolean;
delegationKey?: boolean;
/**
* Any of: Boolean, String
*/
subgraphExecute?: boolean | string;
/**
* Any of: Boolean, String
*/
Expand Down
3 changes: 2 additions & 1 deletion packages/plugins/prometheus/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@
"tslib": "^2.4.0"
},
"dependencies": {
"@graphql-yoga/plugin-prometheus": "^4.0.0"
"@graphql-mesh/serve-runtime": "^0.2.2",
"@graphql-yoga/plugin-prometheus": "^4.1.0"
},
"devDependencies": {
"prom-client": "15.1.0"
Expand Down
268 changes: 90 additions & 178 deletions packages/plugins/prometheus/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,41 @@
import type { Plugin as YogaPlugin } from 'graphql-yoga';
import { Counter, register as defaultRegistry, Histogram, Registry, Summary } from 'prom-client';
import { MeshPlugin, MeshPluginOptions, YamlConfig } from '@graphql-mesh/types';
import { getHeadersObj, loadFromModuleExportExpression } from '@graphql-mesh/utils';
import { isAsyncIterable, type Plugin as YogaPlugin } from 'graphql-yoga';
import { register as defaultRegistry, Histogram, Registry } from 'prom-client';
import { MeshServePlugin } from '@graphql-mesh/serve-runtime';
import { ImportFn, Logger, MeshPlugin, YamlConfig } from '@graphql-mesh/types';
import {
usePrometheus,
PrometheusTracingPluginConfig as YogaPromPluginConfig,
} from '@graphql-yoga/plugin-prometheus';
import {
commonFillLabelsFnForEnvelop,
commonLabelsForEnvelop,
createHistogramForEnvelop,
} from './createHistogramForEnvelop.js';

type HistogramContainer = Exclude<YogaPromPluginConfig['http'], boolean>;
type CounterContainer = Exclude<YogaPromPluginConfig['requestCount'], boolean>;
type SummaryContainer = Exclude<YogaPromPluginConfig['requestSummary'], boolean>;

export default async function useMeshPrometheus(
pluginOptions: MeshPluginOptions<YamlConfig.PrometheusConfig>,
): Promise<MeshPlugin<any> & YogaPlugin> {
const registry = pluginOptions.registry
? await loadFromModuleExportExpression<Registry>(pluginOptions.registry, {
cwd: pluginOptions.baseDir,
importFn: pluginOptions.importFn,
defaultExportName: 'default',
})
: defaultRegistry;
defaultImportFn,
getHeadersObj,
loadFromModuleExportExpression,
} from '@graphql-mesh/utils';
import { usePrometheus } from '@graphql-yoga/plugin-prometheus';

export default function useMeshPrometheus(
pluginOptions: YamlConfig.PrometheusConfig & {
logger?: Logger;
baseDir?: string;
importFn?: ImportFn;
},
): MeshPlugin<any> & YogaPlugin & MeshServePlugin {
let registry: Registry;
if (pluginOptions.registry) {
const registry$ = loadFromModuleExportExpression<Registry>(pluginOptions.registry, {
cwd: pluginOptions.baseDir || globalThis.process?.cwd(),
importFn: pluginOptions.importFn || defaultImportFn,
defaultExportName: 'default',
});
const registryProxy = Proxy.revocable(defaultRegistry, {
get(target, prop, receiver) {
if (typeof (target as any)[prop] === 'function') {
return function (...args: any[]) {
return registry$.then(registry => (registry as any)[prop](...args));
};
}
return Reflect.get(target, prop, receiver);
},
});
registry = registryProxy.proxy;
registry$.then(() => registryProxy.revoke()).catch(e => pluginOptions.logger.error(e));
}

let fetchHistogram: Histogram | undefined;

Expand Down Expand Up @@ -54,171 +64,42 @@ export default async function useMeshPrometheus(
typeof pluginOptions.delegation === 'string'
? pluginOptions.delegation
: 'graphql_mesh_delegate_duration';
const delegationLabelNames = ['sourceName', 'typeName', 'fieldName'];
if (pluginOptions.delegationArgs) {
delegationLabelNames.push('args');
}
if (pluginOptions.delegationKey) {
delegationLabelNames.push('key');
}
delegateHistogram = new Histogram({
name,
help: 'Time spent on delegate execution',
labelNames: ['sourceName', 'typeName', 'fieldName', 'args', 'key'],
labelNames: delegationLabelNames,
registers: [registry],
});
}

let httpHistogram: HistogramContainer | undefined;

if (pluginOptions.http) {
const labelNames = ['url', 'method', 'statusCode', 'statusText'];
if (pluginOptions.httpRequestHeaders) {
labelNames.push('requestHeaders');
}
if (pluginOptions.httpResponseHeaders) {
labelNames.push('responseHeaders');
}
const name =
typeof pluginOptions.http === 'string' ? pluginOptions.http : 'graphql_mesh_http_duration';
httpHistogram = {
histogram: new Histogram({
name,
help: 'Time spent on incoming HTTP requests',
labelNames,
registers: [registry],
}),
fillLabelsFn(_, { request, response }) {
const labels: Record<string, string> = {
url: request.url,
method: request.method,
statusCode: response.status,
statusText: response.statusText,
};
if (pluginOptions.httpRequestHeaders) {
labels.requestHeaders = JSON.stringify(getHeadersObj(request.headers));
}
if (pluginOptions.httpResponseHeaders) {
labels.responseHeaders = JSON.stringify(getHeadersObj(response.headers));
}
return labels;
},
};
}

let requestCounter: CounterContainer | undefined;
let subgraphExecuteHistogram: Histogram | undefined;

if (pluginOptions.requestCount) {
if (pluginOptions.subgraphExecute) {
const name =
typeof pluginOptions.requestCount === 'string'
? pluginOptions.requestCount
: 'graphql_mesh_request_count';
requestCounter = {
counter: new Counter({
name,
help: 'Counts the amount of GraphQL requests executed',
labelNames: commonLabelsForEnvelop,
registers: [registry],
}),
fillLabelsFn: commonFillLabelsFnForEnvelop,
};
}

let requestSummary: SummaryContainer | undefined;

if (pluginOptions.requestSummary) {
const name =
typeof pluginOptions.requestSummary === 'string'
? pluginOptions.requestSummary
: 'graphql_mesh_request_time_summary';
requestSummary = {
summary: new Summary({
name,
help: 'Summary to measure the time to complete GraphQL operations',
labelNames: commonLabelsForEnvelop,
registers: [registry],
}),
fillLabelsFn: commonFillLabelsFnForEnvelop,
};
}

let errorsCounter: CounterContainer | undefined;

if (pluginOptions.errors) {
const name =
typeof pluginOptions.errors === 'string' ? pluginOptions.errors : 'graphql_mesh_error_result';
errorsCounter = {
counter: new Counter({
name,
help: 'Counts the amount of errors reported from all phases',
labelNames: ['operationType', 'operationName', 'path', 'phase'] as const,
registers: [registry],
}),
fillLabelsFn: params => ({
operationName: params.operationName!,
operationType: params.operationType!,
path: params.error?.path?.join('.'),
phase: params.errorPhase!,
}),
};
}

let deprecatedCounter: CounterContainer | undefined;

if (pluginOptions.deprecatedFields) {
const name =
typeof pluginOptions.deprecatedFields === 'string'
? pluginOptions.deprecatedFields
: 'graphql_mesh_deprecated_fields';
deprecatedCounter = {
counter: new Counter({
name,
help: 'Counts the amount of deprecated fields used in selection sets',
labelNames: ['operationType', 'operationName', 'fieldName', 'typeName'] as const,
registers: [registry],
}),
fillLabelsFn: params => ({
operationName: params.operationName!,
operationType: params.operationType!,
fieldName: params.deprecationInfo?.fieldName,
typeName: params.deprecationInfo?.typeName,
}),
};
typeof pluginOptions.subgraphExecute === 'string'
? pluginOptions.subgraphExecute
: 'graphql_mesh_subgraph_execute_duration';
const subgraphExecuteLabelNames = ['subgraphName'];
subgraphExecuteHistogram = new Histogram({
name,
help: 'Time spent on subgraph execution',
labelNames: subgraphExecuteLabelNames,
registers: [registry],
});
}

return {
onPluginInit({ addPlugin }) {
addPlugin(
usePrometheus({
...pluginOptions,
http: httpHistogram,
requestCount: requestCounter,
requestTotalDuration: createHistogramForEnvelop({
defaultName: 'graphql_mesh_request_duration',
valueFromConfig: pluginOptions.requestTotalDuration,
help: 'Time spent on running the GraphQL operation from parse to execute',
registry,
}),
requestSummary,
parse: createHistogramForEnvelop({
defaultName: 'graphql_mesh_parse_duration',
valueFromConfig: pluginOptions.parse,
help: 'Time spent on parsing the GraphQL operation',
registry,
}),
validate: createHistogramForEnvelop({
defaultName: 'graphql_mesh_validate_duration',
valueFromConfig: pluginOptions.validate,
help: 'Time spent on validating the GraphQL operation',
registry,
}),
contextBuilding: createHistogramForEnvelop({
defaultName: 'graphql_mesh_context_building_duration',
valueFromConfig: pluginOptions.contextBuilding,
help: 'Time spent on building the GraphQL context',
registry,
}),
execute: createHistogramForEnvelop({
defaultName: 'graphql_mesh_execute_duration',
valueFromConfig: pluginOptions.execute,
help: 'Time spent on executing the GraphQL operation',
registry,
}),
errors: errorsCounter,
deprecatedFields: deprecatedCounter,
registry,
}),
);
Expand All @@ -234,11 +115,42 @@ export default async function useMeshPrometheus(
sourceName,
typeName,
fieldName,
args: JSON.stringify(args),
key: JSON.stringify(key),
args: pluginOptions.delegationArgs ? JSON.stringify(args) : undefined,
key: pluginOptions.delegationKey ? JSON.stringify(key) : undefined,
},
duration,
);
};
}
return undefined;
},
onSubgraphExecute({ subgraphName }) {
if (subgraphExecuteHistogram) {
const start = Date.now();
return ({ result }) => {
if (isAsyncIterable(result)) {
return {
onEnd: () => {
const end = Date.now();
const duration = end - start;
subgraphExecuteHistogram.observe(
{
subgraphName,
},
duration,
);
},
};
}
const end = Date.now();
const duration = end - start;
subgraphExecuteHistogram.observe(
{
subgraphName,
},
duration,
);
return undefined;
};
}
return undefined;
Expand Down
5 changes: 5 additions & 0 deletions packages/plugins/prometheus/yaml-config.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ type PrometheusConfig @md {
# Mesh specific flags

delegation: BooleanOrString
delegationArgs: Boolean
delegationKey: Boolean

subgraphExecute: BooleanOrString

fetch: BooleanOrString
fetchRequestHeaders: Boolean
fetchResponseHeaders: Boolean
Expand Down
5 changes: 5 additions & 0 deletions website/src/generated-markdown/PrometheusConfig.generated.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@
* `delegation` - One of:
* `Boolean`
* `String`
* `delegationArgs` (type: `Boolean`)
* `delegationKey` (type: `Boolean`)
* `subgraphExecute` - One of:
* `Boolean`
* `String`
* `fetch` - One of:
* `Boolean`
* `String`
Expand Down
Loading

0 comments on commit 7a712ab

Please sign in to comment.