UNPKG

@envelop/prometheus

Version:

This plugin tracks the complete execution flow, and reports metrics using Prometheus tracing (based on `prom-client`).

390 lines (389 loc) • 19 kB
/* eslint-disable @typescript-eslint/no-non-null-asserted-optional-chain */ import { TypeInfo } from 'graphql'; import { register as defaultRegistry } from 'prom-client'; import { isAsyncIterable, isIntrospectionOperationString, } from '@envelop/core'; import { useOnResolve } from '@envelop/on-resolve'; import { createCounter, createFillLabelFnParams, createHistogram, createSummary, extractDeprecatedFields, filterFillParamsFnParams, getHistogramFromConfig, instrumentRegistry, labelExists, shouldTraceFieldResolver, } from './utils.js'; export { createCounter, createHistogram, createSummary, }; export const fillLabelsFnParamsMap = new WeakMap(); export const execStartTimeMap = new WeakMap(); export const usePrometheus = (config = {}) => { let typeInfo = null; const registry = instrumentRegistry(config.registry || defaultRegistry); const parseHistogram = getHistogramFromConfig(config, 'parse', typeof config.parse === 'string' ? config.parse : 'graphql_envelop_phase_parse', 'Time spent on running GraphQL "parse" function'); const validateHistogram = getHistogramFromConfig(config, 'validate', typeof config.validate === 'string' ? config.validate : 'graphql_envelop_phase_validate', 'Time spent on running GraphQL "validate" function'); const contextBuildingHistogram = getHistogramFromConfig(config, 'contextBuilding', typeof config.contextBuilding === 'string' ? config.contextBuilding : 'graphql_envelop_phase_context', 'Time spent on building the GraphQL context'); const executeHistogram = getHistogramFromConfig(config, 'execute', typeof config.execute === 'string' ? config.execute : 'graphql_envelop_phase_execute', 'Time spent on running the GraphQL "execute" function'); const subscribeHistogram = getHistogramFromConfig(config, 'subscribe', typeof config.subscribe === 'string' ? config.subscribe : 'graphql_envelop_phase_subscribe', 'Time spent on running the GraphQL "subscribe" function'); const resolversHistogram = typeof config.resolvers === 'object' ? config.resolvers : config.resolvers === true || typeof config.resolvers === 'string' ? createHistogram({ registry, histogram: { name: typeof config.resolvers === 'string' ? config.resolvers : 'graphql_envelop_execute_resolver', help: 'Time spent on running the GraphQL resolvers', labelNames: [ 'operationType', 'operationName', 'fieldName', 'typeName', 'returnType', ].filter(label => labelExists(config, label)), }, fillLabelsFn: params => filterFillParamsFnParams(config, { operationName: params.operationName, operationType: params.operationType, fieldName: params.info?.fieldName, typeName: params.info?.parentType.name, returnType: params.info?.returnType.toString(), }), }) : undefined; const requestTotalHistogram = typeof config.requestTotalDuration === 'object' ? config.requestTotalDuration : config.requestTotalDuration === true || typeof config.requestTotalDuration === 'string' ? createHistogram({ registry, histogram: { name: typeof config.requestTotalDuration === 'string' ? config.requestTotalDuration : 'graphql_envelop_request_duration', help: 'Time spent on running the GraphQL operation from parse to execute', labelNames: ['operationType', 'operationName'].filter(label => labelExists(config, label)), }, fillLabelsFn: params => filterFillParamsFnParams(config, { operationName: params.operationName, operationType: params.operationType, }), }) : undefined; const requestSummary = typeof config.requestSummary === 'object' ? config.requestSummary : config.requestSummary === true || typeof config.requestSummary === 'string' ? createSummary({ registry, summary: { name: typeof config.requestSummary === 'string' ? config.requestSummary : 'graphql_envelop_request_time_summary', help: 'Summary to measure the time to complete GraphQL operations', labelNames: ['operationType', 'operationName'].filter(label => labelExists(config, label)), }, fillLabelsFn: params => filterFillParamsFnParams(config, { operationName: params.operationName, operationType: params.operationType, }), }) : undefined; const errorsCounter = typeof config.errors === 'object' ? config.errors : config.errors === true || typeof config.errors === 'string' ? createCounter({ registry, counter: { name: typeof config.errors === 'string' ? config.errors : 'graphql_envelop_error_result', help: 'Counts the amount of errors reported from all phases', labelNames: ['operationType', 'operationName', 'path', 'phase'].filter(label => labelExists(config, label)), }, fillLabelsFn: params => filterFillParamsFnParams(config, { operationName: params.operationName, operationType: params.operationType, path: params.error?.path?.join('.'), phase: params.errorPhase, }), }) : undefined; const reqCounter = typeof config.requestCount === 'object' ? config.requestCount : config.requestCount === true || typeof config.requestCount === 'string' ? createCounter({ registry, counter: { name: typeof config.requestCount === 'string' ? config.requestCount : 'graphql_envelop_request', help: 'Counts the amount of GraphQL requests executed through Envelop', labelNames: ['operationType', 'operationName'].filter(label => labelExists(config, label)), }, fillLabelsFn: params => filterFillParamsFnParams(config, { operationName: params.operationName, operationType: params.operationType, }), }) : undefined; const deprecationCounter = typeof config.deprecatedFields === 'object' ? config.deprecatedFields : config.deprecatedFields === true || typeof config.deprecatedFields === 'string' ? createCounter({ registry, counter: { name: typeof config.deprecatedFields === 'string' ? config.deprecatedFields : 'graphql_envelop_deprecated_field', help: 'Counts the amount of deprecated fields used in selection sets', labelNames: ['operationType', 'operationName', 'fieldName', 'typeName'].filter(label => labelExists(config, label)), }, fillLabelsFn: params => filterFillParamsFnParams(config, { operationName: params.operationName, operationType: params.operationType, fieldName: params.deprecationInfo?.fieldName, typeName: params.deprecationInfo?.typeName, }), }) : undefined; const schemaChangeCounter = typeof config.schemaChangeCount === 'object' ? config.schemaChangeCount : config.schemaChangeCount === true || typeof config.schemaChangeCount === 'string' ? createCounter({ registry, counter: { name: typeof config.schemaChangeCount === 'string' ? config.schemaChangeCount : 'graphql_envelop_schema_change', help: 'Counts the amount of schema changes', }, fillLabelsFn: () => ({}), }) : undefined; const onParse = ({ context, params }) => { if (config.skipIntrospection && isIntrospectionOperationString(params.source)) { return; } const startTime = Date.now(); return params => { const totalTime = (Date.now() - startTime) / 1000; let fillLabelsFnParams = fillLabelsFnParamsMap.get(params.result); if (!fillLabelsFnParams) { fillLabelsFnParams = createFillLabelFnParams(params.result, context, params => filterFillParamsFnParams(config, params)); fillLabelsFnParamsMap.set(context, fillLabelsFnParams); } if (fillLabelsFnParams) { parseHistogram?.histogram.observe(parseHistogram.fillLabelsFn(fillLabelsFnParams, context), totalTime); if (deprecationCounter && typeInfo) { const deprecatedFields = extractDeprecatedFields(fillLabelsFnParams.document, typeInfo); if (deprecatedFields.length > 0) { for (const depField of deprecatedFields) { deprecationCounter.counter .labels(deprecationCounter.fillLabelsFn({ ...fillLabelsFnParams, deprecationInfo: depField, }, context)) .inc(); } } } } else { // means that we got a parse error, report it errorsCounter?.counter .labels({ phase: 'parse', }) .inc(); } }; }; const onValidate = validateHistogram ? ({ context }) => { const fillLabelsFnParams = fillLabelsFnParamsMap.get(context); if (!fillLabelsFnParams) { return undefined; } const startTime = Date.now(); return ({ valid }) => { const totalTime = (Date.now() - startTime) / 1000; const labels = validateHistogram.fillLabelsFn(fillLabelsFnParams, context); validateHistogram.histogram.observe(labels, totalTime); if (!valid) { errorsCounter?.counter .labels({ ...labels, phase: 'validate', }) .inc(); } }; } : undefined; const onContextBuilding = contextBuildingHistogram ? ({ context }) => { const fillLabelsFnParams = fillLabelsFnParamsMap.get(context); if (!fillLabelsFnParams) { return undefined; } const startTime = Date.now(); return () => { const totalTime = (Date.now() - startTime) / 1000; contextBuildingHistogram.histogram.observe(contextBuildingHistogram.fillLabelsFn(fillLabelsFnParams, context), totalTime); }; } : undefined; const onExecute = executeHistogram ? ({ args }) => { const fillLabelsFnParams = fillLabelsFnParamsMap.get(args.contextValue); if (!fillLabelsFnParams) { return undefined; } const startTime = Date.now(); reqCounter?.counter .labels(reqCounter.fillLabelsFn(fillLabelsFnParams, args.contextValue)) .inc(); function handleResult(result) { if (errorsCounter && result.errors && result.errors.length > 0) { for (const error of result.errors) { errorsCounter.counter .labels(errorsCounter.fillLabelsFn({ ...fillLabelsFnParams, errorPhase: 'execute', error, }, args.contextValue)) .inc(); } } } const result = { onExecuteDone: ({ result }) => { const execStartTime = execStartTimeMap.get(args.contextValue); const handleEnd = () => { const totalTime = (Date.now() - startTime) / 1000; executeHistogram.histogram.observe(executeHistogram.fillLabelsFn(fillLabelsFnParams, args.contextValue), totalTime); requestTotalHistogram?.histogram.observe(requestTotalHistogram.fillLabelsFn(fillLabelsFnParams, args.contextValue), totalTime); if (requestSummary && execStartTime) { const summaryTime = (Date.now() - execStartTime) / 1000; requestSummary.summary.observe(requestSummary.fillLabelsFn(fillLabelsFnParams, args.contextValue), summaryTime); } }; if (!isAsyncIterable(result)) { handleResult(result); handleEnd(); return undefined; } else { return { onNext({ result }) { handleResult(result); }, onEnd() { handleEnd(); }, }; } }, }; return result; } : undefined; const onSubscribe = subscribeHistogram ? ({ args }) => { const fillLabelsFnParams = fillLabelsFnParamsMap.get(args.contextValue); if (!fillLabelsFnParams) { return undefined; } const startTime = Date.now(); reqCounter?.counter .labels(reqCounter.fillLabelsFn(fillLabelsFnParams, args.contextValue)) .inc(); function handleResult(result) { if (errorsCounter && result.errors && result.errors.length > 0) { for (const error of result.errors) { errorsCounter.counter .labels(errorsCounter.fillLabelsFn({ ...fillLabelsFnParams, errorPhase: 'execute', error, }, args.contextValue)) .inc(); } } } const result = { onSubscribeResult: ({ result }) => { const execStartTime = execStartTimeMap.get(args.contextValue); const handleEnd = () => { const totalTime = (Date.now() - startTime) / 1000; subscribeHistogram.histogram.observe(subscribeHistogram.fillLabelsFn(fillLabelsFnParams, args.contextValue), totalTime); requestTotalHistogram?.histogram.observe(requestTotalHistogram.fillLabelsFn(fillLabelsFnParams, args.contextValue), totalTime); if (requestSummary && execStartTime) { const summaryTime = (Date.now() - execStartTime) / 1000; requestSummary.summary.observe(requestSummary.fillLabelsFn(fillLabelsFnParams, args.contextValue), summaryTime); } }; if (!isAsyncIterable(result)) { handleResult(result); handleEnd(); return undefined; } else { return { onNext({ result }) { handleResult(result); }, onEnd() { handleEnd(); }, }; } }, }; return result; } : undefined; const countedSchemas = new WeakSet(); return { onEnveloped({ context }) { if (!execStartTimeMap.has(context)) { execStartTimeMap.set(context, Date.now()); } }, onPluginInit({ addPlugin, registerContextErrorHandler }) { if (resolversHistogram) { addPlugin(useOnResolve(({ info, context }) => { const shouldTrace = shouldTraceFieldResolver(info, config.resolversWhitelist); if (!shouldTrace) { return undefined; } const startTime = Date.now(); return () => { const totalTime = (Date.now() - startTime) / 1000; const fillLabelsFnParams = fillLabelsFnParamsMap.get(context); const paramsCtx = { ...fillLabelsFnParams, info, }; resolversHistogram.histogram.observe(resolversHistogram.fillLabelsFn(paramsCtx, context), totalTime); }; })); } registerContextErrorHandler(({ context }) => { const fillLabelsFnParams = fillLabelsFnParamsMap.get(context); let extraLabels; if (fillLabelsFnParams) { extraLabels = contextBuildingHistogram?.fillLabelsFn(fillLabelsFnParams, context); } errorsCounter?.counter .labels({ ...extraLabels, phase: 'context', }) .inc(); }); }, onSchemaChange({ schema }) { typeInfo = new TypeInfo(schema); if (schemaChangeCounter && !countedSchemas.has(schema)) { schemaChangeCounter.counter.inc(); countedSchemas.add(schema); } }, onParse, onValidate, onContextBuilding, onExecute, onSubscribe, }; };