diff --git a/.changeset/sharp-deers-leave.md b/.changeset/sharp-deers-leave.md new file mode 100644 index 0000000000000..2d66a217334b3 --- /dev/null +++ b/.changeset/sharp-deers-leave.md @@ -0,0 +1,6 @@ +--- +'@graphql-mesh/plugin-prometheus': minor +'@graphql-mesh/types': minor +--- + +Now you can customize the names of the metrics diff --git a/packages/plugins/prometheus/src/createHistogramForEnvelop.ts b/packages/plugins/prometheus/src/createHistogramForEnvelop.ts new file mode 100644 index 0000000000000..5beba7fd4e68a --- /dev/null +++ b/packages/plugins/prometheus/src/createHistogramForEnvelop.ts @@ -0,0 +1,37 @@ +import { Histogram, Registry } from 'prom-client'; + +export const commonLabelsForEnvelop = ['operationType', 'operationName'] as const; + +export function commonFillLabelsFnForEnvelop(params: { + operationName?: string; + operationType?: string; +}) { + return { + operationName: params.operationName!, + operationType: params.operationType!, + }; +} + +interface CreateHistogramContainerForEnvelop { + defaultName: string; + help: string; + valueFromConfig: string | boolean; + registry: Registry; +} + +export function createHistogramForEnvelop({ + defaultName, + help, + valueFromConfig, + registry, +}: CreateHistogramContainerForEnvelop) { + return { + histogram: new Histogram({ + name: typeof valueFromConfig === 'string' ? valueFromConfig : defaultName, + help, + labelNames: commonLabelsForEnvelop, + registers: [registry], + }), + fillLabelsFn: commonFillLabelsFnForEnvelop, + }; +} diff --git a/packages/plugins/prometheus/src/index.ts b/packages/plugins/prometheus/src/index.ts index e0595460f3870..cd8c15c490d48 100644 --- a/packages/plugins/prometheus/src/index.ts +++ b/packages/plugins/prometheus/src/index.ts @@ -1,8 +1,20 @@ -import { usePrometheus } from '@graphql-yoga/plugin-prometheus'; +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 { Histogram, register as defaultRegistry, Registry } from 'prom-client'; -import type { Plugin as YogaPlugin } from 'graphql-yoga'; +import { + usePrometheus, + PrometheusTracingPluginConfig as YogaPromPluginConfig, +} from '@graphql-yoga/plugin-prometheus'; +import { + commonFillLabelsFnForEnvelop, + commonLabelsForEnvelop, + createHistogramForEnvelop, +} from './createHistogramForEnvelop'; + +type HistogramContainer = Exclude; +type CounterContainer = Exclude; +type SummaryContainer = Exclude; export default async function useMeshPrometheus( pluginOptions: MeshPluginOptions, @@ -14,29 +26,200 @@ export default async function useMeshPrometheus( 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], - }); + + let fetchHistogram: Histogram | undefined; + + if (pluginOptions.fetch) { + const name = + typeof pluginOptions.fetch === 'string' ? pluginOptions.fetch : 'graphql_mesh_fetch_duration'; + fetchHistogram = new Histogram({ + name, + help: 'Time spent on outgoing HTTP calls', + labelNames: [ + 'url', + 'method', + 'requestHeaders', + 'statusCode', + 'statusText', + 'responseHeaders', + ], + registers: [registry], + }); + } + + let delegateHistogram: Histogram | undefined; + + if (pluginOptions.delegation) { + const name = + typeof pluginOptions.delegation === 'string' + ? pluginOptions.delegation + : 'graphql_mesh_delegate_duration'; + delegateHistogram = new Histogram({ + name, + help: 'Time spent on delegate execution', + labelNames: ['sourceName', 'typeName', 'fieldName', 'args', 'key'], + registers: [registry], + }); + } + + let httpHistogram: HistogramContainer | undefined; + + if (pluginOptions.http) { + const labelNames = ['url', 'method', 'statusCode', 'statusText', 'responseHeaders']; + if (pluginOptions.httpRequestHeaders) { + labelNames.push('requestHeaders'); + } + 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 = { + url: request.url, + method: request.method, + statusCode: response.status, + statusText: response.statusText, + responseHeaders: JSON.stringify(getHeadersObj(response.headers)), + }; + if (pluginOptions.httpRequestHeaders) { + labels.requestHeaders = JSON.stringify(getHeadersObj(request.headers)); + } + return labels; + }, + }; + } + + let requestCounter: CounterContainer | undefined; + + if (pluginOptions.requestCount) { + 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, + }), + }; + } + 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, }), ); }, onDelegate({ sourceName, typeName, fieldName, args, key }) { - if (pluginOptions.delegation !== false) { + if (delegateHistogram) { const start = Date.now(); return () => { const end = Date.now(); @@ -56,7 +239,7 @@ export default async function useMeshPrometheus( return undefined; }, onFetch({ url, options }) { - if (pluginOptions.fetch !== false) { + if (fetchHistogram) { const start = Date.now(); return ({ response }) => { const end = Date.now(); diff --git a/packages/plugins/prometheus/yaml-config.graphql b/packages/plugins/prometheus/yaml-config.graphql index 8855e99b58f04..8a0242ad71727 100644 --- a/packages/plugins/prometheus/yaml-config.graphql +++ b/packages/plugins/prometheus/yaml-config.graphql @@ -3,24 +3,32 @@ extend type Plugin { } 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 + requestCount: BooleanOrString + requestTotalDuration: BooleanOrString + requestSummary: BooleanOrString + parse: BooleanOrString + validate: BooleanOrString + contextBuilding: BooleanOrString + execute: BooleanOrString + errors: BooleanOrString + deprecatedFields: BooleanOrString + skipIntrospection: Boolean registry: String + + # Mesh specific flags + delegation: BooleanOrString + fetch: BooleanOrString + fetchRequestHeaders: Boolean + + # Yoga specific flags + http: BooleanOrString + httpRequestHeaders: Boolean """ The path to the metrics endpoint default: `/metrics` """ endpoint: String } + +union BooleanOrString = Boolean | String diff --git a/packages/types/src/config-schema.json b/packages/types/src/config-schema.json index c88a04977b199..2d8af0079e266 100644 --- a/packages/types/src/config-schema.json +++ b/packages/types/src/config-schema.json @@ -2317,53 +2317,148 @@ "title": "PrometheusConfig", "properties": { "requestCount": { - "type": "boolean" + "description": "Any of: Boolean, String", + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string" + } + ] }, "requestTotalDuration": { - "type": "boolean" + "description": "Any of: Boolean, String", + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string" + } + ] }, "requestSummary": { - "type": "boolean" + "description": "Any of: Boolean, String", + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string" + } + ] }, "parse": { - "type": "boolean" + "description": "Any of: Boolean, String", + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string" + } + ] }, "validate": { - "type": "boolean" + "description": "Any of: Boolean, String", + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string" + } + ] }, "contextBuilding": { - "type": "boolean" + "description": "Any of: Boolean, String", + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string" + } + ] }, "execute": { - "type": "boolean" + "description": "Any of: Boolean, String", + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string" + } + ] }, "errors": { - "type": "boolean" - }, - "resolvers": { - "type": "boolean" - }, - "resolversWhiteList": { - "type": "array", - "items": { - "type": "string" - }, - "additionalItems": false + "description": "Any of: Boolean, String", + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string" + } + ] }, "deprecatedFields": { + "description": "Any of: Boolean, String", + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string" + } + ] + }, + "skipIntrospection": { "type": "boolean" }, + "registry": { + "type": "string" + }, "delegation": { - "type": "boolean" + "description": "Any of: Boolean, String", + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string" + } + ] }, "fetch": { - "type": "boolean" + "description": "Any of: Boolean, String", + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string" + } + ] }, - "skipIntrospection": { + "fetchRequestHeaders": { "type": "boolean" }, - "registry": { - "type": "string" + "http": { + "description": "Any of: Boolean, String", + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string" + } + ] + }, + "httpRequestHeaders": { + "type": "boolean" }, "endpoint": { "type": "string", diff --git a/packages/types/src/config.ts b/packages/types/src/config.ts index 3f14edd4a49d4..9b3eedec847ec 100644 --- a/packages/types/src/config.ts +++ b/packages/types/src/config.ts @@ -1892,21 +1892,58 @@ export interface OperationFieldPermission { allow?: string[]; } export interface PrometheusConfig { - 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; + /** + * Any of: Boolean, String + */ + requestCount?: boolean | string; + /** + * Any of: Boolean, String + */ + requestTotalDuration?: boolean | string; + /** + * Any of: Boolean, String + */ + requestSummary?: boolean | string; + /** + * Any of: Boolean, String + */ + parse?: boolean | string; + /** + * Any of: Boolean, String + */ + validate?: boolean | string; + /** + * Any of: Boolean, String + */ + contextBuilding?: boolean | string; + /** + * Any of: Boolean, String + */ + execute?: boolean | string; + /** + * Any of: Boolean, String + */ + errors?: boolean | string; + /** + * Any of: Boolean, String + */ + deprecatedFields?: boolean | string; skipIntrospection?: boolean; registry?: string; + /** + * Any of: Boolean, String + */ + delegation?: boolean | string; + /** + * Any of: Boolean, String + */ + fetch?: boolean | string; + fetchRequestHeaders?: boolean; + /** + * Any of: Boolean, String + */ + http?: boolean | string; + httpRequestHeaders?: boolean; /** * The path to the metrics endpoint * default: `/metrics` diff --git a/website/src/generated-markdown/PrometheusConfig.generated.md b/website/src/generated-markdown/PrometheusConfig.generated.md index 494e2afe1f228..f7f3c08d23522 100644 --- a/website/src/generated-markdown/PrometheusConfig.generated.md +++ b/website/src/generated-markdown/PrometheusConfig.generated.md @@ -1,18 +1,43 @@ -* `requestCount` (type: `Boolean`) -* `requestTotalDuration` (type: `Boolean`) -* `requestSummary` (type: `Boolean`) -* `parse` (type: `Boolean`) -* `validate` (type: `Boolean`) -* `contextBuilding` (type: `Boolean`) -* `execute` (type: `Boolean`) -* `errors` (type: `Boolean`) -* `resolvers` (type: `Boolean`) -* `resolversWhiteList` (type: `Array of String`) -* `deprecatedFields` (type: `Boolean`) -* `delegation` (type: `Boolean`) -* `fetch` (type: `Boolean`) +* `requestCount` - One of: + * `Boolean` + * `String` +* `requestTotalDuration` - One of: + * `Boolean` + * `String` +* `requestSummary` - One of: + * `Boolean` + * `String` +* `parse` - One of: + * `Boolean` + * `String` +* `validate` - One of: + * `Boolean` + * `String` +* `contextBuilding` - One of: + * `Boolean` + * `String` +* `execute` - One of: + * `Boolean` + * `String` +* `errors` - One of: + * `Boolean` + * `String` +* `deprecatedFields` - One of: + * `Boolean` + * `String` * `skipIntrospection` (type: `Boolean`) * `registry` (type: `String`) +* `delegation` - One of: + * `Boolean` + * `String` +* `fetch` - One of: + * `Boolean` + * `String` +* `fetchRequestHeaders` (type: `Boolean`) +* `http` - One of: + * `Boolean` + * `String` +* `httpRequestHeaders` (type: `Boolean`) * `endpoint` (type: `String`) - The path to the metrics endpoint default: `/metrics` \ No newline at end of file diff --git a/website/src/pages/docs/plugins/prometheus.mdx b/website/src/pages/docs/plugins/prometheus.mdx index f556fc0b4993f..93122ec418676 100644 --- a/website/src/pages/docs/plugins/prometheus.mdx +++ b/website/src/pages/docs/plugins/prometheus.mdx @@ -43,6 +43,25 @@ plugins: execute: true errors: true resolvers: true + + # reports metrics for the delegation to the individual sources + delegation: true + + # reports metrics for the outgoing HTTP requests + fetch: true + # Adds the request headers to the metrics + fetchRequestHeaders: true + # Adds the response headers to the metrics + fetchResponseHeaders: true + + # reports metrics for the incoming HTTP requests (this sets a custom name for http) + # If you pass a string instead of boolean, it will be used as the name of the metric + http: my-http-duration-metric + # Adds the request headers to the metrics + httpRequestHeaders: true + # Adds the response headers to the metrics + httpResponseHeaders: true + # reports metrics also for these resolvers, by default all fields are reported resolversWhiteList: - Mutation.*