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