UNPKG

@envelop/prometheus

Version:

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

263 lines (262 loc) • 13.1 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.usePrometheus = exports.createSummary = exports.createHistogram = exports.createCounter = void 0; /* eslint-disable @typescript-eslint/no-non-null-asserted-optional-chain */ const core_1 = require("@envelop/core"); const on_resolve_1 = require("@envelop/on-resolve"); const graphql_1 = require("graphql"); const prom_client_1 = require("prom-client"); const utils_js_1 = require("./utils.js"); Object.defineProperty(exports, "createHistogram", { enumerable: true, get: function () { return utils_js_1.createHistogram; } }); Object.defineProperty(exports, "createCounter", { enumerable: true, get: function () { return utils_js_1.createCounter; } }); Object.defineProperty(exports, "createSummary", { enumerable: true, get: function () { return utils_js_1.createSummary; } }); const promPluginContext = Symbol('promPluginContext'); const promPluginExecutionStartTimeSymbol = Symbol('promPluginExecutionStartTimeSymbol'); const usePrometheus = (config = {}) => { let typeInfo = null; const parseHistogram = (0, utils_js_1.getHistogramFromConfig)(config, 'parse', 'graphql_envelop_phase_parse', 'Time spent on running GraphQL "parse" function'); const validateHistogram = (0, utils_js_1.getHistogramFromConfig)(config, 'validate', 'graphql_envelop_phase_validate', 'Time spent on running GraphQL "validate" function'); const contextBuildingHistogram = (0, utils_js_1.getHistogramFromConfig)(config, 'contextBuilding', 'graphql_envelop_phase_context', 'Time spent on building the GraphQL context'); const executeHistogram = (0, utils_js_1.getHistogramFromConfig)(config, 'execute', 'graphql_envelop_phase_execute', 'Time spent on running the GraphQL "execute" function'); const resolversHistogram = typeof config.resolvers === 'object' ? config.resolvers : config.resolvers === true ? (0, utils_js_1.createHistogram)({ histogram: new prom_client_1.Histogram({ name: 'graphql_envelop_execute_resolver', help: 'Time spent on running the GraphQL resolvers', labelNames: ['operationType', 'operationName', 'fieldName', 'typeName', 'returnType'], registers: [config.registry || prom_client_1.register], }), fillLabelsFn: params => ({ 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 ? (0, utils_js_1.createHistogram)({ histogram: new prom_client_1.Histogram({ name: 'graphql_envelop_request_duration', help: 'Time spent on running the GraphQL operation from parse to execute', labelNames: ['operationType', 'operationName'], registers: [config.registry || prom_client_1.register], }), fillLabelsFn: params => ({ operationName: params.operationName, operationType: params.operationType, }), }) : undefined; const requestSummary = typeof config.requestSummary === 'object' ? config.requestSummary : config.requestSummary === true ? (0, utils_js_1.createSummary)({ summary: new prom_client_1.Summary({ name: 'graphql_envelop_request_time_summary', help: 'Summary to measure the time to complete GraphQL operations', labelNames: ['operationType', 'operationName'], registers: [config.registry || prom_client_1.register], }), fillLabelsFn: params => ({ operationName: params.operationName, operationType: params.operationType, }), }) : undefined; const errorsCounter = typeof config.errors === 'object' ? config.errors : config.errors === true ? (0, utils_js_1.createCounter)({ counter: new prom_client_1.Counter({ name: 'graphql_envelop_error_result', help: 'Counts the amount of errors reported from all phases', labelNames: ['operationType', 'operationName', 'path', 'phase'], registers: [config.registry || prom_client_1.register], }), fillLabelsFn: params => ({ 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 ? (0, utils_js_1.createCounter)({ counter: new prom_client_1.Counter({ name: 'graphql_envelop_request', help: 'Counts the amount of GraphQL requests executed through Envelop', labelNames: ['operationType', 'operationName'], registers: [config.registry || prom_client_1.register], }), fillLabelsFn: params => ({ operationName: params.operationName, operationType: params.operationType, }), }) : undefined; const deprecationCounter = typeof config.deprecatedFields === 'object' ? config.deprecatedFields : config.deprecatedFields === true ? (0, utils_js_1.createCounter)({ counter: new prom_client_1.Counter({ name: 'graphql_envelop_deprecated_field', help: 'Counts the amount of deprecated fields used in selection sets', labelNames: ['operationType', 'operationName', 'fieldName', 'typeName'], registers: [config.registry || prom_client_1.register], }), fillLabelsFn: params => ({ operationName: params.operationName, operationType: params.operationType, fieldName: params.deprecationInfo?.fieldName, typeName: params.deprecationInfo?.typeName, }), }) : undefined; const onParse = ({ context, extendContext, params }) => { if (config.skipIntrospection && (0, core_1.isIntrospectionOperationString)(params.source)) { return; } const startTime = Date.now(); return params => { const totalTime = (Date.now() - startTime) / 1000; const internalContext = (0, utils_js_1.createInternalContext)(params.result); if (internalContext) { extendContext({ [promPluginContext]: internalContext, }); parseHistogram?.histogram.observe(parseHistogram.fillLabelsFn(internalContext, context), totalTime); if (deprecationCounter && typeInfo) { const deprecatedFields = (0, utils_js_1.extractDeprecatedFields)(internalContext.document, typeInfo); if (deprecatedFields.length > 0) { for (const depField of deprecatedFields) { deprecationCounter.counter .labels(deprecationCounter.fillLabelsFn({ ...internalContext, deprecationInfo: depField, }, context)) .inc(); } } } } else { // means that we got a parse error, report it errorsCounter?.counter .labels({ phase: 'parse', }) .inc(); } }; }; const onValidate = validateHistogram ? ({ context }) => { if (!context[promPluginContext]) { return undefined; } const startTime = Date.now(); return ({ valid }) => { const totalTime = (Date.now() - startTime) / 1000; const labels = validateHistogram.fillLabelsFn(context[promPluginContext], context); validateHistogram.histogram.observe(labels, totalTime); if (!valid) { errorsCounter?.counter .labels({ ...labels, phase: 'validate', }) .inc(); } }; } : undefined; const onContextBuilding = contextBuildingHistogram ? ({ context }) => { if (!context[promPluginContext]) { return undefined; } const startTime = Date.now(); return () => { const totalTime = (Date.now() - startTime) / 1000; contextBuildingHistogram.histogram.observe(contextBuildingHistogram.fillLabelsFn(context[promPluginContext], context), totalTime); }; } : undefined; const onExecute = executeHistogram ? ({ args }) => { if (!args.contextValue[promPluginContext]) { return undefined; } const startTime = Date.now(); reqCounter?.counter .labels(reqCounter.fillLabelsFn(args.contextValue[promPluginContext], args.contextValue)) .inc(); const result = { onExecuteDone: ({ result }) => { const totalTime = (Date.now() - startTime) / 1000; executeHistogram.histogram.observe(executeHistogram.fillLabelsFn(args.contextValue[promPluginContext], args.contextValue), totalTime); requestTotalHistogram?.histogram.observe(requestTotalHistogram.fillLabelsFn(args.contextValue[promPluginContext], args.contextValue), totalTime); if (requestSummary && args.contextValue[promPluginExecutionStartTimeSymbol]) { const summaryTime = (Date.now() - args.contextValue[promPluginExecutionStartTimeSymbol]) / 1000; requestSummary.summary.observe(requestSummary.fillLabelsFn(args.contextValue[promPluginContext], args.contextValue), summaryTime); } if (errorsCounter && !(0, core_1.isAsyncIterable)(result) && result.errors && result.errors.length > 0) { for (const error of result.errors) { errorsCounter.counter .labels(errorsCounter.fillLabelsFn({ ...args.contextValue[promPluginContext], errorPhase: 'execute', error, }, args.contextValue)) .inc(); } } }, }; return result; } : undefined; return { onEnveloped({ extendContext }) { extendContext({ [promPluginExecutionStartTimeSymbol]: Date.now(), }); }, onPluginInit({ addPlugin }) { 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 paramsCtx = { ...context[promPluginContext], info, }; resolversHistogram.histogram.observe(resolversHistogram.fillLabelsFn(paramsCtx, context), totalTime); }; })); } }, onSchemaChange({ schema }) { typeInfo = new graphql_1.TypeInfo(schema); }, onParse, onValidate, onContextBuilding, onExecute, }; }; exports.usePrometheus = usePrometheus;