@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
JavaScript
;
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;