UNPKG

@envelop/prometheus

Version:

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

384 lines (383 loc) • 19.6 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.usePrometheus = exports.execStartTimeMap = exports.fillLabelsFnParamsMap = exports.getSummaryFromConfig = exports.getHistogramFromConfig = exports.getCounterFromConfig = exports.createSummary = exports.createHistogram = exports.createCounter = void 0; /* eslint-disable @typescript-eslint/no-non-null-asserted-optional-chain */ const graphql_1 = require("graphql"); const prom_client_1 = require("prom-client"); const core_1 = require("@envelop/core"); const on_resolve_1 = require("@envelop/on-resolve"); const utils_js_1 = require("./utils.js"); Object.defineProperty(exports, "createCounter", { enumerable: true, get: function () { return utils_js_1.createCounter; } }); Object.defineProperty(exports, "createHistogram", { enumerable: true, get: function () { return utils_js_1.createHistogram; } }); Object.defineProperty(exports, "createSummary", { enumerable: true, get: function () { return utils_js_1.createSummary; } }); Object.defineProperty(exports, "getCounterFromConfig", { enumerable: true, get: function () { return utils_js_1.getCounterFromConfig; } }); Object.defineProperty(exports, "getHistogramFromConfig", { enumerable: true, get: function () { return utils_js_1.getHistogramFromConfig; } }); Object.defineProperty(exports, "getSummaryFromConfig", { enumerable: true, get: function () { return utils_js_1.getSummaryFromConfig; } }); exports.fillLabelsFnParamsMap = new WeakMap(); exports.execStartTimeMap = new WeakMap(); const usePrometheus = (config) => { let typeInfo = null; config.registry = (0, utils_js_1.instrumentRegistry)(config.registry || prom_client_1.register); const parseHistogram = (0, utils_js_1.getHistogramFromConfig)(config, 'graphql_envelop_phase_parse', ['parse'], { help: 'Time spent on running GraphQL "parse" function', }); const validateHistogram = (0, utils_js_1.getHistogramFromConfig)(config, 'graphql_envelop_phase_validate', ['validate'], { help: 'Time spent on running GraphQL "validate" function', }); const contextBuildingHistogram = (0, utils_js_1.getHistogramFromConfig)(config, 'graphql_envelop_phase_context', ['context'], { help: 'Time spent on building the GraphQL context', }); const executeHistogram = (0, utils_js_1.getHistogramFromConfig)(config, 'graphql_envelop_phase_execute', ['execute'], { help: 'Time spent on running the GraphQL "execute" function', }); const subscribeHistogram = (0, utils_js_1.getHistogramFromConfig)(config, 'graphql_envelop_phase_subscribe', ['subscribe'], { help: 'Time spent on running the GraphQL "subscribe" function', }); const resolversHistogram = (0, utils_js_1.getHistogramFromConfig)(config, 'graphql_envelop_execute_resolver', ['execute', 'subscribe'], { help: 'Time spent on running the GraphQL resolvers', labelNames: ['operationType', 'operationName', 'fieldName', 'typeName', 'returnType'], }, params => (0, utils_js_1.filterFillParamsFnParams)(config, { operationName: params.operationName, operationType: params.operationType, fieldName: params.info?.fieldName, typeName: params.info?.parentType.name, returnType: params.info?.returnType.toString(), })); const requestTotalHistogram = (0, utils_js_1.getHistogramFromConfig)(config, 'graphql_envelop_request_duration', ['execute', 'subscribe'], { help: 'Time spent on running the GraphQL operation from parse to execute', }); const requestSummary = (0, utils_js_1.getSummaryFromConfig)(config, 'graphql_envelop_request_time_summary', ['execute', 'subscribe'], { help: 'Summary to measure the time to complete GraphQL operations', }); const errorsCounter = (0, utils_js_1.getCounterFromConfig)(config, 'graphql_envelop_error_result', ['parse', 'validate', 'context', 'execute', 'subscribe'], { help: 'Counts the amount of errors reported from all phases', labelNames: ['operationType', 'operationName', 'path', 'phase'], }, params => { const labels = { operationName: params.operationName, operationType: params.operationType, phase: params.errorPhase, }; if (params.error?.path) { labels.path = params.error.path?.join('.'); } return (0, utils_js_1.filterFillParamsFnParams)(config, labels); }); const reqCounter = (0, utils_js_1.getCounterFromConfig)(config, 'graphql_envelop_request', ['execute', 'subscribe'], { help: 'Counts the amount of GraphQL requests executed through Envelop', }); const deprecationCounter = (0, utils_js_1.getCounterFromConfig)(config, 'graphql_envelop_deprecated_field', ['parse'], { help: 'Counts the amount of deprecated fields used in selection sets', labelNames: ['operationType', 'operationName', 'fieldName', 'typeName'], }, params => (0, utils_js_1.filterFillParamsFnParams)(config, { operationName: params.operationName, operationType: params.operationType, fieldName: params.deprecationInfo?.fieldName, typeName: params.deprecationInfo?.typeName, })); const schemaChangeCounter = (0, utils_js_1.getCounterFromConfig)(config, 'graphql_envelop_schema_change', ['schema'], { help: 'Counts the amount of schema changes', labelNames: [], }, () => ({})); // parse is mandatory, because it sets up label params for the whole request. const phasesToHook = new Set(['parse']); for (const metric of [ parseHistogram, validateHistogram, contextBuildingHistogram, executeHistogram, subscribeHistogram, resolversHistogram, requestTotalHistogram, requestSummary, errorsCounter, reqCounter, deprecationCounter, schemaChangeCounter, ]) { metric?.phases.forEach(phase => phasesToHook.add(phase)); } const onParse = ({ context, params }) => { if (config.skipIntrospection && (0, core_1.isIntrospectionOperationString)(params.source)) { return; } const startTime = Date.now(); return params => { const fillLabelsFnParams = (0, utils_js_1.createFillLabelFnParams)(params.result, context, params => (0, utils_js_1.filterFillParamsFnParams)(config, params)); exports.fillLabelsFnParamsMap.set(context, fillLabelsFnParams); if (!fillLabelsFnParams) { // means that we got a parse error if (errorsCounter?.shouldObserve({ error: params.result, errorPhase: 'parse' }, context)) { // TODO: use fillLabelsFn errorsCounter?.counter.labels({ phase: 'parse' }).inc(); } // TODO: We should probably always report parse timing, error or not. return; } const totalTime = (Date.now() - startTime) / 1000; if (parseHistogram?.shouldObserve(fillLabelsFnParams, context)) { parseHistogram?.histogram.observe(parseHistogram.fillLabelsFn(fillLabelsFnParams, context), totalTime); } if (deprecationCounter && typeInfo) { const deprecatedFields = (0, utils_js_1.extractDeprecatedFields)(fillLabelsFnParams.document, typeInfo); for (const depField of deprecatedFields) { const deprecationLabelParams = { ...fillLabelsFnParams, deprecationInfo: depField, }; if (deprecationCounter.shouldObserve(deprecationLabelParams, context)) { deprecationCounter.counter .labels(deprecationCounter.fillLabelsFn(deprecationLabelParams, context)) .inc(); } } } }; }; const onValidate = ({ context }) => { const fillLabelsFnParams = exports.fillLabelsFnParamsMap.get(context); if (!fillLabelsFnParams) { return undefined; } const startTime = Date.now(); return ({ valid }) => { const totalTime = (Date.now() - startTime) / 1000; let labels; if (validateHistogram?.shouldObserve(fillLabelsFnParams, context)) { labels = validateHistogram.fillLabelsFn(fillLabelsFnParams, context); validateHistogram.histogram.observe(labels, totalTime); } if (!valid && errorsCounter?.phases.includes('validate') && errorsCounter?.shouldObserve(fillLabelsFnParams, context)) { // TODO: we should probably iterate over validation errors to report each error. errorsCounter?.counter // TODO: Use fillLabelsFn .labels(errorsCounter.fillLabelsFn({ ...fillLabelsFnParams, errorPhase: 'validate' }, context)) .inc(); } }; }; const onContextBuilding = ({ context }) => { const fillLabelsFnParams = exports.fillLabelsFnParamsMap.get(context); if (!fillLabelsFnParams || !contextBuildingHistogram?.shouldObserve(fillLabelsFnParams, context)) { return; } const startTime = Date.now(); return () => { const totalTime = (Date.now() - startTime) / 1000; contextBuildingHistogram.histogram.observe(contextBuildingHistogram.fillLabelsFn(fillLabelsFnParams, context), totalTime); }; }; const onExecute = ({ args }) => { const fillLabelsFnParams = exports.fillLabelsFnParamsMap.get(args.contextValue); if (!fillLabelsFnParams) { return; } const shouldObserveRequsets = reqCounter?.shouldObserve(fillLabelsFnParams, args.contextValue); const shouldObserveExecute = executeHistogram?.shouldObserve(fillLabelsFnParams, args.contextValue); const shouldObserveRequestTotal = requestTotalHistogram?.shouldObserve(fillLabelsFnParams, args.contextValue); const shouldObserveSummary = requestSummary?.shouldObserve(fillLabelsFnParams, args.contextValue); const shouldHandleEnd = shouldObserveRequsets || shouldObserveExecute || shouldObserveRequestTotal || shouldObserveSummary; const shouldHandleResult = errorsCounter !== undefined; if (!shouldHandleEnd && !shouldHandleResult) { return; } const startTime = Date.now(); if (shouldObserveRequsets) { reqCounter?.counter .labels(reqCounter.fillLabelsFn(fillLabelsFnParams, args.contextValue)) .inc(); } function handleResult(result) { if (result.errors && result.errors.length > 0) { for (const error of result.errors) { const labelParams = { ...fillLabelsFnParams, errorPhase: 'execute', error, }; if (errorsCounter.shouldObserve(labelParams, args.contextValue)) { errorsCounter.counter .labels(errorsCounter.fillLabelsFn(labelParams, args.contextValue)) .inc(); } } } } const result = { onExecuteDone: ({ result }) => { const handleEnd = () => { const totalTime = (Date.now() - startTime) / 1000; if (shouldObserveExecute) { executeHistogram.histogram.observe(executeHistogram.fillLabelsFn(fillLabelsFnParams, args.contextValue), totalTime); } if (shouldObserveRequestTotal) { requestTotalHistogram.histogram.observe(requestTotalHistogram.fillLabelsFn(fillLabelsFnParams, args.contextValue), totalTime); } if (shouldObserveSummary) { const execStartTime = exports.execStartTimeMap.get(args.contextValue); if (execStartTime) { const summaryTime = (Date.now() - execStartTime) / 1000; requestSummary.summary.observe(requestSummary.fillLabelsFn(fillLabelsFnParams, args.contextValue), summaryTime); } } }; if (!(0, core_1.isAsyncIterable)(result)) { shouldHandleResult && handleResult(result); shouldHandleEnd && handleEnd(); return undefined; } else { return { onNext: shouldHandleResult ? ({ result }) => { handleResult(result); } : undefined, onEnd: shouldHandleEnd ? handleEnd : undefined, }; } }, }; return result; }; const onSubscribe = ({ args }) => { const fillLabelsFnParams = exports.fillLabelsFnParamsMap.get(args.contextValue); if (!fillLabelsFnParams) { return undefined; } const shouldObserveRequsets = reqCounter?.shouldObserve(fillLabelsFnParams, args.contextValue); const shouldObserveExecute = executeHistogram?.shouldObserve(fillLabelsFnParams, args.contextValue); const shouldObserveRequestTotal = requestTotalHistogram?.shouldObserve(fillLabelsFnParams, args.contextValue); const shouldObserveSummary = requestSummary?.shouldObserve(fillLabelsFnParams, args.contextValue); const shouldHandleEnd = shouldObserveRequsets || shouldObserveExecute || shouldObserveRequestTotal || shouldObserveSummary; const shouldHandleResult = errorsCounter !== undefined; const startTime = Date.now(); if (shouldObserveRequsets) { 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(); } } } if (!shouldHandleEnd && !shouldHandleResult) { return; } const result = { onSubscribeResult: ({ result }) => { const handleEnd = () => { const totalTime = (Date.now() - startTime) / 1000; if (shouldObserveExecute) { subscribeHistogram.histogram.observe(subscribeHistogram.fillLabelsFn(fillLabelsFnParams, args.contextValue), totalTime); } if (shouldObserveRequestTotal) { requestTotalHistogram?.histogram.observe(requestTotalHistogram.fillLabelsFn(fillLabelsFnParams, args.contextValue), totalTime); } if (shouldObserveSummary) { const execStartTime = exports.execStartTimeMap.get(args.contextValue); if (execStartTime) { const summaryTime = (Date.now() - execStartTime) / 1000; requestSummary.summary.observe(requestSummary.fillLabelsFn(fillLabelsFnParams, args.contextValue), summaryTime); } } }; if (!(0, core_1.isAsyncIterable)(result)) { shouldHandleResult && handleResult(result); shouldHandleEnd && handleEnd(); return undefined; } else { return { onNext: shouldHandleResult ? ({ result }) => { handleResult(result); } : undefined, onEnd: shouldHandleEnd ? handleEnd : undefined, }; } }, }; return result; }; const onPluginInit = ({ addPlugin, registerContextErrorHandler }) => { if (resolversHistogram) { addPlugin((0, on_resolve_1.useOnResolve)(({ info, context }) => { const shouldTrace = (0, utils_js_1.shouldTraceFieldResolver)(info, config.resolversWhitelist); if (!shouldTrace) { return undefined; } const startTime = Date.now(); return () => { const totalTime = (Date.now() - startTime) / 1000; const fillLabelsFnParams = exports.fillLabelsFnParamsMap.get(context); const paramsCtx = { ...fillLabelsFnParams, info, }; resolversHistogram.histogram.observe(resolversHistogram.fillLabelsFn(paramsCtx, context), totalTime); }; })); } if (errorsCounter) { registerContextErrorHandler(({ context, error }) => { const fillLabelsFnParams = exports.fillLabelsFnParamsMap.get(context); if (errorsCounter.shouldObserve({ error: error, errorPhase: 'context', ...exports.fillLabelsFnParamsMap }, context)) { errorsCounter.counter .labels(errorsCounter?.fillLabelsFn( // FIXME: unsafe cast here, but it's ok, fillabelfn is doing duck typing anyway { ...fillLabelsFnParams, errorPhase: 'context', error: error }, context)) .inc(); } }); } }; const onEnveloped = ({ context }) => { if (!exports.execStartTimeMap.has(context)) { exports.execStartTimeMap.set(context, Date.now()); } }; function hookIf(phase, hook) { if (phasesToHook.has(phase)) { return hook; } return undefined; } const countedSchemas = new WeakSet(); return { onSchemaChange({ schema }) { typeInfo = new graphql_1.TypeInfo(schema); if (schemaChangeCounter?.shouldObserve({}, null) && !countedSchemas.has(schema)) { schemaChangeCounter.counter.inc(); countedSchemas.add(schema); } }, onEnveloped: hookIf('execute', onEnveloped) ?? hookIf('subscribe', onEnveloped), onPluginInit: errorsCounter?.phases.includes('context') || resolversHistogram ? onPluginInit : undefined, onParse: hookIf('parse', onParse), onValidate: hookIf('validate', onValidate), onContextBuilding: hookIf('context', onContextBuilding), onExecute: hookIf('execute', onExecute), onSubscribe: hookIf('subscribe', onSubscribe), }; }; exports.usePrometheus = usePrometheus;