@envelop/prometheus
Advanced tools
Comparing version 11.0.0 to 11.1.0-alpha-20241122091727-adade563355e3d213f27427a9a1d86adf9431d41
619
cjs/index.js
@@ -19,20 +19,80 @@ "use strict"; | ||
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', { | ||
const phasesToHook = { | ||
parse: [], | ||
validate: [], | ||
context: [], | ||
execute: { | ||
end: [], | ||
result: [], | ||
}, | ||
subscribe: { | ||
end: [], | ||
result: [], | ||
error: [], | ||
}, | ||
pluginInit: [], | ||
enveloped: [], | ||
schema: [], | ||
}; | ||
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', { | ||
if (parseHistogram) { | ||
phasesToHook.parse.push({ | ||
shouldHandle: parseHistogram.shouldObserve, | ||
handler: ({ params, context, totalTime }) => { | ||
parseHistogram.histogram.observe(parseHistogram.fillLabelsFn(params, context), totalTime); | ||
}, | ||
}); | ||
} | ||
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', { | ||
if (validateHistogram) { | ||
phasesToHook.validate.push({ | ||
shouldHandle: validateHistogram.shouldObserve, | ||
handler: ({ params, context, totalTime }) => { | ||
const labels = validateHistogram.fillLabelsFn(params, context); | ||
validateHistogram.histogram.observe(labels, totalTime); | ||
}, | ||
}); | ||
} | ||
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', { | ||
if (contextBuildingHistogram) { | ||
phasesToHook.context.push({ | ||
shouldHandle: contextBuildingHistogram.shouldObserve, | ||
handler: ({ params, context, totalTime }) => { | ||
const labels = contextBuildingHistogram.fillLabelsFn(params, context); | ||
contextBuildingHistogram.histogram.observe(labels, totalTime); | ||
}, | ||
}); | ||
} | ||
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', { | ||
if (executeHistogram) { | ||
phasesToHook.execute.end.push({ | ||
shouldHandle: executeHistogram.shouldObserve, | ||
handler: ({ params, context, totalTime }) => { | ||
const labels = executeHistogram.fillLabelsFn(params, context); | ||
executeHistogram.histogram.observe(labels, totalTime); | ||
}, | ||
}); | ||
} | ||
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', { | ||
if (subscribeHistogram) { | ||
phasesToHook.subscribe.end.push({ | ||
shouldHandle: subscribeHistogram.shouldObserve, | ||
handler: ({ params, context, totalTime }) => { | ||
const labels = subscribeHistogram.fillLabelsFn(params, context); | ||
subscribeHistogram.histogram.observe(labels, totalTime); | ||
}, | ||
}); | ||
} | ||
const resolversHistogram = (0, utils_js_1.getHistogramFromConfig)(config, 'graphql_envelop_execute_resolver', ['execute', 'subscribe'], { | ||
help: 'Time spent on running the GraphQL resolvers', | ||
@@ -47,21 +107,147 @@ labelNames: ['operationType', 'operationName', 'fieldName', 'typeName', 'returnType'], | ||
})); | ||
const requestTotalHistogram = (0, utils_js_1.getHistogramFromConfig)(config, 'graphql_envelop_request_duration', { | ||
if (resolversHistogram) { | ||
phasesToHook.pluginInit.push(({ addPlugin }) => { | ||
addPlugin((0, on_resolve_1.useOnResolve)(({ info, context }) => { | ||
const phase = info.operation.operation === 'subscription' ? 'subscribe' : 'execute'; | ||
if (!resolversHistogram.phases?.includes(phase) || | ||
!(0, utils_js_1.shouldTraceFieldResolver)(info, config.resolversWhitelist)) { | ||
return undefined; | ||
} | ||
const fillLabelsFnParams = exports.fillLabelsFnParamsMap.get(context); | ||
const paramsCtx = { ...fillLabelsFnParams, info }; | ||
if (!resolversHistogram.shouldObserve(paramsCtx, context)) { | ||
return undefined; | ||
} | ||
const startTime = Date.now(); | ||
return () => { | ||
const totalTime = (Date.now() - startTime) / 1000; | ||
resolversHistogram.histogram.observe(resolversHistogram.fillLabelsFn(paramsCtx, context), totalTime); | ||
}; | ||
})); | ||
}); | ||
} | ||
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', { | ||
if (requestTotalHistogram) { | ||
const handler = { | ||
shouldHandle: requestTotalHistogram.shouldObserve, | ||
handler: ({ params, context, totalTime }) => { | ||
const labels = requestTotalHistogram.fillLabelsFn(params, context); | ||
requestTotalHistogram.histogram.observe(labels, totalTime); | ||
}, | ||
}; | ||
for (const phase of requestTotalHistogram.phases) { | ||
phasesToHook[phase].end.push(handler); | ||
} | ||
} | ||
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', { | ||
if (requestSummary) { | ||
phasesToHook.enveloped.push(({ context }) => { | ||
if (!exports.execStartTimeMap.has(context)) { | ||
exports.execStartTimeMap.set(context, Date.now()); | ||
} | ||
}); | ||
const handler = { | ||
shouldHandle: (params, context) => requestSummary.shouldObserve(params, context) && exports.execStartTimeMap.has(context), | ||
handler: ({ params, context }) => { | ||
const execStartTime = exports.execStartTimeMap.get(context); | ||
const summaryTime = (Date.now() - execStartTime) / 1000; | ||
const labels = requestSummary.fillLabelsFn(params, context); | ||
requestSummary.summary.observe(labels, summaryTime); | ||
}, | ||
}; | ||
for (const phase of requestSummary.phases) { | ||
phasesToHook[phase].end.push(handler); | ||
} | ||
} | ||
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 => (0, utils_js_1.filterFillParamsFnParams)(config, { | ||
operationName: params.operationName, | ||
operationType: params.operationType, | ||
path: params.error?.path?.join('.'), | ||
phase: params.errorPhase, | ||
})); | ||
const reqCounter = (0, utils_js_1.getCounterFromConfig)(config, 'graphql_envelop_request', { | ||
}, 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); | ||
}); | ||
if (errorsCounter) { | ||
['parse', 'validate'] | ||
.filter(phase => errorsCounter.phases.includes(phase)) | ||
.forEach(phase => { | ||
phasesToHook[phase].push({ | ||
shouldHandle: (params, context) => !!params.errorPhase && errorsCounter.shouldObserve(params, context), | ||
handler: ({ params, context }) => { | ||
const labels = errorsCounter.fillLabelsFn(params, context); | ||
errorsCounter?.counter.labels(labels).inc(); | ||
}, | ||
}); | ||
}); | ||
['execute', 'subscribe'] | ||
.filter(phase => errorsCounter.phases.includes(phase)) | ||
.forEach(phase => { | ||
phasesToHook[phase].result.push({ | ||
shouldHandle: errorsCounter.shouldObserve, | ||
handler: ({ result, params, context }) => { | ||
if (!result.errors?.length) { | ||
return; | ||
} | ||
for (const error of result.errors) { | ||
const labelParams = { ...params, errorPhase: 'execute', error }; | ||
if (errorsCounter.shouldObserve(labelParams, context)) { | ||
errorsCounter.counter | ||
.labels(errorsCounter.fillLabelsFn(labelParams, context)) | ||
.inc(); | ||
} | ||
} | ||
}, | ||
}); | ||
}); | ||
if (errorsCounter.phases.includes('subscribe')) { | ||
phasesToHook.subscribe.error.push({ | ||
shouldHandle: errorsCounter.shouldObserve, | ||
handler: ({ params, context, error }) => { | ||
const labels = errorsCounter.fillLabelsFn(params, context); | ||
errorsCounter.counter.labels(labels).inc(); | ||
}, | ||
}); | ||
} | ||
if (errorsCounter.phases.includes('context')) { | ||
phasesToHook.pluginInit.push(({ registerContextErrorHandler }) => { | ||
registerContextErrorHandler(({ context, error }) => { | ||
const fillLabelsFnParams = exports.fillLabelsFnParamsMap.get(context); | ||
// FIXME: unsafe cast here, but it's ok, fillabelfn is doing duck typing anyway | ||
const params = { | ||
error: error, | ||
errorPhase: 'context', | ||
...fillLabelsFnParams, | ||
}; | ||
if (errorsCounter.shouldObserve(params, context)) { | ||
errorsCounter.counter.labels(errorsCounter?.fillLabelsFn(params, context)).inc(); | ||
} | ||
}); | ||
}); | ||
} | ||
} | ||
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', { | ||
if (reqCounter) { | ||
const handler = { | ||
shouldHandle: reqCounter.shouldObserve, | ||
handler: ({ params, context }) => { | ||
reqCounter.counter.labels(reqCounter.fillLabelsFn(params, context)).inc(); | ||
}, | ||
}; | ||
for (const phase of reqCounter.phases) { | ||
phasesToHook[phase].end.push(handler); | ||
} | ||
} | ||
const deprecationCounter = (0, utils_js_1.getCounterFromConfig)(config, 'graphql_envelop_deprecated_field', ['parse'], { | ||
help: 'Counts the amount of deprecated fields used in selection sets', | ||
@@ -75,6 +261,40 @@ labelNames: ['operationType', 'operationName', 'fieldName', 'typeName'], | ||
})); | ||
const schemaChangeCounter = (0, utils_js_1.getCounterFromConfig)(config, 'graphql_envelop_schema_change', { | ||
if (deprecationCounter) { | ||
let typeInfo = null; | ||
phasesToHook.schema.push(({ schema }) => { | ||
typeInfo = new graphql_1.TypeInfo(schema); | ||
}); | ||
phasesToHook.parse.push({ | ||
shouldHandle: (params, context) => | ||
// If parse error happens, we can't explore the query document | ||
!!typeInfo && !params.errorPhase && deprecationCounter.shouldObserve(params, context), | ||
handler: ({ params, context }) => { | ||
const deprecatedFields = (0, utils_js_1.extractDeprecatedFields)(params.document, typeInfo); | ||
for (const depField of deprecatedFields) { | ||
const deprecationLabelParams = { | ||
...params, | ||
deprecationInfo: depField, | ||
}; | ||
if (deprecationCounter.shouldObserve(deprecationLabelParams, context)) { | ||
deprecationCounter.counter | ||
.labels(deprecationCounter.fillLabelsFn(deprecationLabelParams, context)) | ||
.inc(); | ||
} | ||
} | ||
}, | ||
}); | ||
} | ||
const schemaChangeCounter = (0, utils_js_1.getCounterFromConfig)(config, 'graphql_envelop_schema_change', ['schema'], { | ||
help: 'Counts the amount of schema changes', | ||
labelNames: [], | ||
}, () => ({})); | ||
if (schemaChangeCounter) { | ||
const countedSchemas = new WeakSet(); | ||
phasesToHook.schema.push(({ schema }) => { | ||
if (schemaChangeCounter?.shouldObserve({}, null) && !countedSchemas.has(schema)) { | ||
schemaChangeCounter.counter.inc(); | ||
countedSchemas.add(schema); | ||
} | ||
}); | ||
} | ||
const onParse = ({ context, params }) => { | ||
@@ -86,233 +306,158 @@ if (config.skipIntrospection && (0, core_1.isIntrospectionOperationString)(params.source)) { | ||
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); | ||
const args = { | ||
context, | ||
totalTime: (Date.now() - startTime) / 1000, | ||
params: fillLabelsFnParams ?? { error: params.result, errorPhase: 'parse' }, | ||
}; | ||
phasesToHook.parse | ||
.filter(({ shouldHandle }) => shouldHandle(args.params, context)) | ||
.forEach(({ handler }) => handler(args)); | ||
}; | ||
}; | ||
const onValidate = ({ context }) => { | ||
const fillLabelsFnParams = exports.fillLabelsFnParamsMap.get(context); | ||
if (!fillLabelsFnParams) { | ||
return undefined; | ||
} | ||
const startTime = Date.now(); | ||
return ({ valid }) => { | ||
const args = { | ||
params: valid ? fillLabelsFnParams : { ...fillLabelsFnParams, errorPhase: 'validate' }, | ||
context, | ||
totalTime: (Date.now() - startTime) / 1000, | ||
}; | ||
phasesToHook.validate | ||
.filter(({ shouldHandle }) => shouldHandle(args.params, context)) | ||
.forEach(({ handler }) => handler(args)); | ||
// TODO: we should probably iterate over validation errors to report each error. | ||
}; | ||
}; | ||
const onContextBuilding = ({ context }) => { | ||
const fillLabelsFnParams = exports.fillLabelsFnParamsMap.get(context); | ||
if (!fillLabelsFnParams) { | ||
return; | ||
} | ||
const startTime = Date.now(); | ||
const args = { | ||
context, | ||
params: fillLabelsFnParams, | ||
totalTime: (Date.now() - startTime) / 1000, | ||
}; | ||
phasesToHook.context | ||
.filter(({ shouldHandle }) => shouldHandle(fillLabelsFnParams, context)) | ||
.forEach(({ handler }) => handler(args)); | ||
}; | ||
const onExecute = ({ args: { contextValue: context } }) => { | ||
const fillLabelsFnParams = exports.fillLabelsFnParamsMap.get(context); | ||
if (!fillLabelsFnParams) { | ||
return; | ||
} | ||
const endHandlers = phasesToHook.execute.end.filter(({ shouldHandle }) => shouldHandle(fillLabelsFnParams, context)); | ||
const resultHandlers = phasesToHook.execute.result.filter(({ shouldHandle }) => shouldHandle(fillLabelsFnParams, context)); | ||
if (endHandlers.length + resultHandlers.length === 0) { | ||
return undefined; | ||
} | ||
const startTime = Date.now(); | ||
function handleResult({ result }) { | ||
const totalTime = (Date.now() - startTime) / 1000; | ||
let fillLabelsFnParams = exports.fillLabelsFnParamsMap.get(params.result); | ||
if (!fillLabelsFnParams) { | ||
fillLabelsFnParams = (0, utils_js_1.createFillLabelFnParams)(params.result, context, params => (0, utils_js_1.filterFillParamsFnParams)(config, params)); | ||
exports.fillLabelsFnParamsMap.set(context, fillLabelsFnParams); | ||
} | ||
if (fillLabelsFnParams) { | ||
parseHistogram?.histogram.observe(parseHistogram.fillLabelsFn(fillLabelsFnParams, context), totalTime); | ||
if (deprecationCounter && typeInfo) { | ||
const deprecatedFields = (0, utils_js_1.extractDeprecatedFields)(fillLabelsFnParams.document, typeInfo); | ||
if (deprecatedFields.length > 0) { | ||
for (const depField of deprecatedFields) { | ||
deprecationCounter.counter | ||
.labels(deprecationCounter.fillLabelsFn({ | ||
...fillLabelsFnParams, | ||
deprecationInfo: depField, | ||
}, context)) | ||
.inc(); | ||
} | ||
} | ||
const args = { params: fillLabelsFnParams, context, totalTime, result }; | ||
resultHandlers.forEach(({ handler }) => handler(args)); | ||
} | ||
const handleEnd = () => { | ||
const totalTime = (Date.now() - startTime) / 1000; | ||
const args = { params: fillLabelsFnParams, context, totalTime }; | ||
endHandlers.forEach(({ handler }) => handler(args)); | ||
}; | ||
return { | ||
onExecuteDone: ({ result }) => { | ||
if ((0, core_1.isAsyncIterable)(result)) { | ||
return { | ||
onNext: resultHandlers.length ? handleResult : undefined, | ||
onEnd: endHandlers.length ? handleEnd : undefined, | ||
}; | ||
} | ||
} | ||
else { | ||
// means that we got a parse error, report it | ||
errorsCounter?.counter | ||
.labels({ | ||
phase: 'parse', | ||
}) | ||
.inc(); | ||
} | ||
else { | ||
handleResult({ result }); | ||
handleEnd(); | ||
return undefined; | ||
} | ||
}, | ||
}; | ||
}; | ||
const onValidate = validateHistogram | ||
? ({ context }) => { | ||
const fillLabelsFnParams = exports.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(); | ||
} | ||
}; | ||
const onSubscribe = ({ args: { contextValue: context } }) => { | ||
const fillLabelsFnParams = exports.fillLabelsFnParamsMap.get(context); | ||
if (!fillLabelsFnParams) { | ||
return; | ||
} | ||
: undefined; | ||
const onContextBuilding = contextBuildingHistogram | ||
? ({ context }) => { | ||
const fillLabelsFnParams = exports.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); | ||
}; | ||
const endHandlers = phasesToHook.subscribe.end.filter(({ shouldHandle }) => shouldHandle(fillLabelsFnParams, context)); | ||
const resultHandlers = phasesToHook.subscribe.result.filter(({ shouldHandle }) => shouldHandle(fillLabelsFnParams, context)); | ||
const errorHandlers = phasesToHook.subscribe.error.filter(({ shouldHandle }) => shouldHandle(fillLabelsFnParams, context)); | ||
if (endHandlers.length + resultHandlers.length + errorHandlers.length === 0) { | ||
return undefined; | ||
} | ||
: undefined; | ||
const onExecute = executeHistogram | ||
? ({ args }) => { | ||
const fillLabelsFnParams = exports.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 = exports.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); | ||
} | ||
const startTime = Date.now(); | ||
function handleResult({ result }) { | ||
const totalTime = (Date.now() - startTime) / 1000; | ||
const args = { params: fillLabelsFnParams, context, totalTime, result }; | ||
resultHandlers.forEach(({ handler }) => handler(args)); | ||
} | ||
const handleEnd = () => { | ||
const totalTime = (Date.now() - startTime) / 1000; | ||
const args = { params: fillLabelsFnParams, context, totalTime }; | ||
endHandlers.forEach(({ handler }) => handler(args)); | ||
}; | ||
const handleError = ({ error }) => { | ||
const totalTime = (Date.now() - startTime) / 1000; | ||
const args = { params: fillLabelsFnParams, context, totalTime, error }; | ||
errorHandlers.forEach(({ handler }) => handler(args)); | ||
}; | ||
return { | ||
onSubscribeResult: ({ result }) => { | ||
if ((0, core_1.isAsyncIterable)(result)) { | ||
return { | ||
onNext: resultHandlers.length ? handleResult : undefined, | ||
onEnd: endHandlers.length ? handleEnd : undefined, | ||
}; | ||
if (!(0, core_1.isAsyncIterable)(result)) { | ||
handleResult(result); | ||
handleEnd(); | ||
return undefined; | ||
} | ||
else { | ||
return { | ||
onNext({ result }) { | ||
handleResult(result); | ||
}, | ||
onEnd() { | ||
handleEnd(); | ||
}, | ||
}; | ||
} | ||
}, | ||
}; | ||
return result; | ||
} | ||
: undefined; | ||
const onSubscribe = subscribeHistogram | ||
? ({ args }) => { | ||
const fillLabelsFnParams = exports.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 = exports.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 (!(0, core_1.isAsyncIterable)(result)) { | ||
handleResult(result); | ||
handleEnd(); | ||
return undefined; | ||
} | ||
else { | ||
return { | ||
onNext({ result }) { | ||
handleResult(result); | ||
}, | ||
onEnd() { | ||
handleEnd(); | ||
}, | ||
}; | ||
} | ||
}, | ||
}; | ||
return result; | ||
else { | ||
handleResult({ result }); | ||
handleEnd(); | ||
return undefined; | ||
} | ||
}, | ||
onSubscribeError: errorHandlers.length ? handleError : undefined, | ||
}; | ||
}; | ||
const onPluginInit = payload => { | ||
for (const handler of phasesToHook.pluginInit) { | ||
handler(payload); | ||
} | ||
: undefined; | ||
const countedSchemas = new WeakSet(); | ||
}; | ||
const onEnveloped = payload => { | ||
for (const handler of phasesToHook.enveloped) { | ||
handler(payload); | ||
} | ||
}; | ||
const onSchemaChange = payload => { | ||
for (const handler of phasesToHook.schema) { | ||
handler(payload); | ||
} | ||
}; | ||
return { | ||
onEnveloped({ context }) { | ||
if (!exports.execStartTimeMap.has(context)) { | ||
exports.execStartTimeMap.set(context, Date.now()); | ||
} | ||
}, | ||
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); | ||
}; | ||
})); | ||
} | ||
registerContextErrorHandler(({ context }) => { | ||
const fillLabelsFnParams = exports.fillLabelsFnParamsMap.get(context); | ||
let extraLabels; | ||
if (fillLabelsFnParams) { | ||
extraLabels = contextBuildingHistogram?.fillLabelsFn(fillLabelsFnParams, context); | ||
} | ||
errorsCounter?.counter | ||
.labels({ | ||
...extraLabels, | ||
phase: 'context', | ||
}) | ||
.inc(); | ||
}); | ||
}, | ||
onSchemaChange({ schema }) { | ||
typeInfo = new graphql_1.TypeInfo(schema); | ||
if (schemaChangeCounter && !countedSchemas.has(schema)) { | ||
schemaChangeCounter.counter.inc(); | ||
countedSchemas.add(schema); | ||
} | ||
}, | ||
onParse, | ||
onValidate, | ||
onContextBuilding, | ||
onExecute, | ||
onSubscribe, | ||
onSchemaChange: phasesToHook.schema.length ? onSchemaChange : undefined, | ||
onPluginInit: phasesToHook.pluginInit.length ? onPluginInit : undefined, | ||
onEnveloped: phasesToHook.enveloped.length ? onEnveloped : undefined, | ||
onValidate: phasesToHook.validate.length ? onValidate : undefined, | ||
onContextBuilding: phasesToHook.context.length ? onContextBuilding : undefined, | ||
onExecute: phasesToHook.execute.end.length + phasesToHook.execute.result.length ? onExecute : undefined, | ||
onSubscribe: phasesToHook.subscribe.end.length + | ||
phasesToHook.subscribe.result.length + | ||
phasesToHook.subscribe.error.length | ||
? onSubscribe | ||
: undefined, | ||
}; | ||
}; | ||
exports.usePrometheus = usePrometheus; |
190
cjs/utils.js
@@ -49,7 +49,13 @@ "use strict"; | ||
exports.registerHistogram = registerHistogram; | ||
/** | ||
* Histogram metric factory allowing to define custom metrics with advanced configuration. | ||
* @param options | ||
* @returns | ||
*/ | ||
function createHistogram(options) { | ||
return { | ||
histogram: registerHistogram(options.registry, options.histogram), | ||
// histogram: new Histogram(options.histogram), | ||
fillLabelsFn: options.fillLabelsFn, | ||
phases: options.phases, | ||
shouldObserve: options.shouldObserve ?? (() => true), | ||
}; | ||
@@ -70,2 +76,7 @@ } | ||
exports.registerSummary = registerSummary; | ||
/** | ||
* Summary metric factory allowing to define custom metrics with advanced configuration. | ||
* @param options | ||
* @returns | ||
*/ | ||
function createSummary(options) { | ||
@@ -75,5 +86,12 @@ return { | ||
fillLabelsFn: options.fillLabelsFn, | ||
phases: options.phases, | ||
shouldObserve: options.shouldObserve ?? (() => true), | ||
}; | ||
} | ||
exports.createSummary = createSummary; | ||
/** | ||
* Counter metric factory allowing to define custom metrics with advanced configuration. | ||
* @param options | ||
* @returns | ||
*/ | ||
function registerCounter(registry, conf) { | ||
@@ -95,6 +113,8 @@ if (!counters.has(registry)) { | ||
fillLabelsFn: options.fillLabelsFn, | ||
phases: options.phases, | ||
shouldObserve: options.shouldObserve, | ||
}; | ||
} | ||
exports.createCounter = createCounter; | ||
function getHistogramFromConfig(config, phase, histogram, fillLabelsFn = params => ({ | ||
function getHistogramFromConfig(config, phase, availablePhases, histogram, fillLabelsFn = params => ({ | ||
operationName: params.operationName, | ||
@@ -104,21 +124,44 @@ operationType: params.operationType, | ||
const metric = config.metrics[phase]; | ||
if (Array.isArray(metric) && metric.length === 0) { | ||
histogram.buckets = metric; | ||
if (!metric) { | ||
return undefined; | ||
} | ||
return typeof metric === 'object' | ||
? metric | ||
: metric === true | ||
? createHistogram({ | ||
registry: config.registry || prom_client_1.register, | ||
histogram: { | ||
name: typeof metric === 'string' ? metric : phase, | ||
...histogram, | ||
labelNames: (histogram.labelNames ?? ['operationType', 'operationName']).filter(label => labelExists(config, label)), | ||
}, | ||
fillLabelsFn: (...args) => filterFillParamsFnParams(config, fillLabelsFn(...args)), | ||
}) | ||
: undefined; | ||
let phases = availablePhases; | ||
if (Array.isArray(metric)) { | ||
if (metric.length === 0) { | ||
throw TypeError(`Bad value provided for 'metrics.${phase.toString()}': the array must contain at least one element`); | ||
} | ||
else if (isBucketsList(metric)) { | ||
histogram.buckets = metric; | ||
} | ||
else if (isPhasesList(metric)) { | ||
phases = filterAvailablePhases(metric, availablePhases); | ||
} | ||
else { | ||
throw TypeError(`Bad value provided for 'metrics.${phase.toString()}': the array must contain only numbers (buckets) or string (phases)`); | ||
} | ||
} | ||
else if (typeof metric === 'object') { | ||
const customMetric = metric; | ||
if (!customMetric.phases) { | ||
customMetric.phases = availablePhases; | ||
} | ||
if (!customMetric.shouldObserve) { | ||
customMetric.shouldObserve = () => true; | ||
} | ||
return customMetric; | ||
} | ||
return createHistogram({ | ||
registry: config.registry || prom_client_1.register, | ||
histogram: { | ||
name: typeof metric === 'string' ? metric : phase, | ||
...histogram, | ||
labelNames: (histogram.labelNames ?? ['operationType', 'operationName']).filter(label => labelExists(config, label)), | ||
}, | ||
fillLabelsFn: (...args) => filterFillParamsFnParams(config, fillLabelsFn(...args)), | ||
phases, | ||
shouldObserve: () => true, | ||
}); | ||
} | ||
exports.getHistogramFromConfig = getHistogramFromConfig; | ||
function getSummaryFromConfig(config, phase, summary, fillLabelsFn = params => filterFillParamsFnParams(config, { | ||
function getSummaryFromConfig(config, phase, availablePhases, summary, fillLabelsFn = params => filterFillParamsFnParams(config, { | ||
operationName: params.operationName, | ||
@@ -128,18 +171,41 @@ operationType: params.operationType, | ||
const metric = config.metrics[phase]; | ||
return typeof metric === 'object' | ||
? metric | ||
: metric === true | ||
? createSummary({ | ||
registry: config.registry || prom_client_1.register, | ||
summary: { | ||
name: typeof metric === 'string' ? metric : phase, | ||
labelNames: ['operationType', 'operationName'].filter(label => labelExists(config, label)), | ||
...summary, | ||
}, | ||
fillLabelsFn, | ||
}) | ||
: undefined; | ||
if (!metric) { | ||
return undefined; | ||
} | ||
let phases = availablePhases; | ||
if (Array.isArray(metric)) { | ||
if (metric.length === 0) { | ||
throw TypeError(`Bad value provided for 'metrics.${phase.toString()}': the array must contain at least one element`); | ||
} | ||
else if (isPhasesList(metric)) { | ||
phases = filterAvailablePhases(metric, availablePhases); | ||
} | ||
else { | ||
throw new TypeError(`Bad value provided for 'metrics.${phase.toString()}': the array must contain only strings (phases)`); | ||
} | ||
} | ||
else if (typeof metric === 'object') { | ||
const customMetric = metric; | ||
if (!customMetric.phases) { | ||
customMetric.phases = availablePhases; | ||
} | ||
if (!customMetric.shouldObserve) { | ||
customMetric.shouldObserve = () => true; | ||
} | ||
return customMetric; | ||
} | ||
return createSummary({ | ||
registry: config.registry || prom_client_1.register, | ||
summary: { | ||
name: typeof metric === 'string' ? metric : phase, | ||
labelNames: ['operationType', 'operationName'].filter(label => labelExists(config, label)), | ||
...summary, | ||
}, | ||
fillLabelsFn, | ||
phases, | ||
shouldObserve: () => true, | ||
}); | ||
} | ||
exports.getSummaryFromConfig = getSummaryFromConfig; | ||
function getCounterFromConfig(config, phase, counter, fillLabelsFn = params => filterFillParamsFnParams(config, { | ||
function getCounterFromConfig(config, phase, availablePhases, counter, fillLabelsFn = params => filterFillParamsFnParams(config, { | ||
operationName: params.operationName, | ||
@@ -149,15 +215,38 @@ operationType: params.operationType, | ||
const metric = config.metrics[phase]; | ||
return typeof metric === 'object' | ||
? metric | ||
: metric === true | ||
? createCounter({ | ||
registry: config.registry || prom_client_1.register, | ||
counter: { | ||
name: typeof metric === 'string' ? metric : phase, | ||
labelNames: ['operationType', 'operationName'].filter(label => labelExists(config, label)), | ||
...counter, | ||
}, | ||
fillLabelsFn, | ||
}) | ||
: undefined; | ||
let phases = availablePhases; | ||
if (!metric) { | ||
return undefined; | ||
} | ||
if (Array.isArray(metric)) { | ||
if (metric.length === 0) { | ||
throw TypeError(`Bad value provided for 'metrics.${phase.toString()}': the array must contain at least one element`); | ||
} | ||
else if (isPhasesList(metric)) { | ||
phases = filterAvailablePhases(metric, availablePhases); | ||
} | ||
else { | ||
throw new TypeError(`Bad value provided for 'metrics.${phase.toString()}': the array must contain only strings (phases)`); | ||
} | ||
} | ||
else if (typeof metric === 'object') { | ||
const customMetric = metric; | ||
if (!customMetric.phases) { | ||
customMetric.phases = availablePhases; | ||
} | ||
if (!customMetric.shouldObserve) { | ||
customMetric.shouldObserve = () => true; | ||
} | ||
return customMetric; | ||
} | ||
return createCounter({ | ||
registry: config.registry || prom_client_1.register, | ||
counter: { | ||
name: typeof metric === 'string' ? metric : phase, | ||
labelNames: ['operationType', 'operationName'].filter(label => labelExists(config, label)), | ||
...counter, | ||
}, | ||
fillLabelsFn, | ||
phases, | ||
shouldObserve: () => true, | ||
}); | ||
} | ||
@@ -200,3 +289,3 @@ exports.getCounterFromConfig = getCounterFromConfig; | ||
} | ||
return labelFlag; | ||
return !!labelFlag; | ||
} | ||
@@ -222,1 +311,10 @@ exports.labelExists = labelExists; | ||
exports.instrumentRegistry = instrumentRegistry; | ||
function isBucketsList(list) { | ||
return list.every(item => typeof item === 'number'); | ||
} | ||
function isPhasesList(list) { | ||
return list.every(item => typeof item === 'string'); | ||
} | ||
function filterAvailablePhases(phases, availablePhases) { | ||
return availablePhases.filter(phase => phases.includes(phase)); | ||
} |
619
esm/index.js
@@ -11,20 +11,80 @@ /* eslint-disable @typescript-eslint/no-non-null-asserted-optional-chain */ | ||
export const usePrometheus = (config) => { | ||
let typeInfo = null; | ||
config.registry = instrumentRegistry(config.registry || defaultRegistry); | ||
const parseHistogram = getHistogramFromConfig(config, 'graphql_envelop_phase_parse', { | ||
const phasesToHook = { | ||
parse: [], | ||
validate: [], | ||
context: [], | ||
execute: { | ||
end: [], | ||
result: [], | ||
}, | ||
subscribe: { | ||
end: [], | ||
result: [], | ||
error: [], | ||
}, | ||
pluginInit: [], | ||
enveloped: [], | ||
schema: [], | ||
}; | ||
const parseHistogram = getHistogramFromConfig(config, 'graphql_envelop_phase_parse', ['parse'], { | ||
help: 'Time spent on running GraphQL "parse" function', | ||
}); | ||
const validateHistogram = getHistogramFromConfig(config, 'graphql_envelop_phase_validate', { | ||
if (parseHistogram) { | ||
phasesToHook.parse.push({ | ||
shouldHandle: parseHistogram.shouldObserve, | ||
handler: ({ params, context, totalTime }) => { | ||
parseHistogram.histogram.observe(parseHistogram.fillLabelsFn(params, context), totalTime); | ||
}, | ||
}); | ||
} | ||
const validateHistogram = getHistogramFromConfig(config, 'graphql_envelop_phase_validate', ['validate'], { | ||
help: 'Time spent on running GraphQL "validate" function', | ||
}); | ||
const contextBuildingHistogram = getHistogramFromConfig(config, 'graphql_envelop_phase_context', { | ||
if (validateHistogram) { | ||
phasesToHook.validate.push({ | ||
shouldHandle: validateHistogram.shouldObserve, | ||
handler: ({ params, context, totalTime }) => { | ||
const labels = validateHistogram.fillLabelsFn(params, context); | ||
validateHistogram.histogram.observe(labels, totalTime); | ||
}, | ||
}); | ||
} | ||
const contextBuildingHistogram = getHistogramFromConfig(config, 'graphql_envelop_phase_context', ['context'], { | ||
help: 'Time spent on building the GraphQL context', | ||
}); | ||
const executeHistogram = getHistogramFromConfig(config, 'graphql_envelop_phase_execute', { | ||
if (contextBuildingHistogram) { | ||
phasesToHook.context.push({ | ||
shouldHandle: contextBuildingHistogram.shouldObserve, | ||
handler: ({ params, context, totalTime }) => { | ||
const labels = contextBuildingHistogram.fillLabelsFn(params, context); | ||
contextBuildingHistogram.histogram.observe(labels, totalTime); | ||
}, | ||
}); | ||
} | ||
const executeHistogram = getHistogramFromConfig(config, 'graphql_envelop_phase_execute', ['execute'], { | ||
help: 'Time spent on running the GraphQL "execute" function', | ||
}); | ||
const subscribeHistogram = getHistogramFromConfig(config, 'graphql_envelop_phase_subscribe', { | ||
if (executeHistogram) { | ||
phasesToHook.execute.end.push({ | ||
shouldHandle: executeHistogram.shouldObserve, | ||
handler: ({ params, context, totalTime }) => { | ||
const labels = executeHistogram.fillLabelsFn(params, context); | ||
executeHistogram.histogram.observe(labels, totalTime); | ||
}, | ||
}); | ||
} | ||
const subscribeHistogram = getHistogramFromConfig(config, 'graphql_envelop_phase_subscribe', ['subscribe'], { | ||
help: 'Time spent on running the GraphQL "subscribe" function', | ||
}); | ||
const resolversHistogram = getHistogramFromConfig(config, 'graphql_envelop_execute_resolver', { | ||
if (subscribeHistogram) { | ||
phasesToHook.subscribe.end.push({ | ||
shouldHandle: subscribeHistogram.shouldObserve, | ||
handler: ({ params, context, totalTime }) => { | ||
const labels = subscribeHistogram.fillLabelsFn(params, context); | ||
subscribeHistogram.histogram.observe(labels, totalTime); | ||
}, | ||
}); | ||
} | ||
const resolversHistogram = getHistogramFromConfig(config, 'graphql_envelop_execute_resolver', ['execute', 'subscribe'], { | ||
help: 'Time spent on running the GraphQL resolvers', | ||
@@ -39,21 +99,147 @@ labelNames: ['operationType', 'operationName', 'fieldName', 'typeName', 'returnType'], | ||
})); | ||
const requestTotalHistogram = getHistogramFromConfig(config, 'graphql_envelop_request_duration', { | ||
if (resolversHistogram) { | ||
phasesToHook.pluginInit.push(({ addPlugin }) => { | ||
addPlugin(useOnResolve(({ info, context }) => { | ||
const phase = info.operation.operation === 'subscription' ? 'subscribe' : 'execute'; | ||
if (!resolversHistogram.phases?.includes(phase) || | ||
!shouldTraceFieldResolver(info, config.resolversWhitelist)) { | ||
return undefined; | ||
} | ||
const fillLabelsFnParams = fillLabelsFnParamsMap.get(context); | ||
const paramsCtx = { ...fillLabelsFnParams, info }; | ||
if (!resolversHistogram.shouldObserve(paramsCtx, context)) { | ||
return undefined; | ||
} | ||
const startTime = Date.now(); | ||
return () => { | ||
const totalTime = (Date.now() - startTime) / 1000; | ||
resolversHistogram.histogram.observe(resolversHistogram.fillLabelsFn(paramsCtx, context), totalTime); | ||
}; | ||
})); | ||
}); | ||
} | ||
const requestTotalHistogram = getHistogramFromConfig(config, 'graphql_envelop_request_duration', ['execute', 'subscribe'], { | ||
help: 'Time spent on running the GraphQL operation from parse to execute', | ||
}); | ||
const requestSummary = getSummaryFromConfig(config, 'graphql_envelop_request_time_summary', { | ||
if (requestTotalHistogram) { | ||
const handler = { | ||
shouldHandle: requestTotalHistogram.shouldObserve, | ||
handler: ({ params, context, totalTime }) => { | ||
const labels = requestTotalHistogram.fillLabelsFn(params, context); | ||
requestTotalHistogram.histogram.observe(labels, totalTime); | ||
}, | ||
}; | ||
for (const phase of requestTotalHistogram.phases) { | ||
phasesToHook[phase].end.push(handler); | ||
} | ||
} | ||
const requestSummary = getSummaryFromConfig(config, 'graphql_envelop_request_time_summary', ['execute', 'subscribe'], { | ||
help: 'Summary to measure the time to complete GraphQL operations', | ||
}); | ||
const errorsCounter = getCounterFromConfig(config, 'graphql_envelop_error_result', { | ||
if (requestSummary) { | ||
phasesToHook.enveloped.push(({ context }) => { | ||
if (!execStartTimeMap.has(context)) { | ||
execStartTimeMap.set(context, Date.now()); | ||
} | ||
}); | ||
const handler = { | ||
shouldHandle: (params, context) => requestSummary.shouldObserve(params, context) && execStartTimeMap.has(context), | ||
handler: ({ params, context }) => { | ||
const execStartTime = execStartTimeMap.get(context); | ||
const summaryTime = (Date.now() - execStartTime) / 1000; | ||
const labels = requestSummary.fillLabelsFn(params, context); | ||
requestSummary.summary.observe(labels, summaryTime); | ||
}, | ||
}; | ||
for (const phase of requestSummary.phases) { | ||
phasesToHook[phase].end.push(handler); | ||
} | ||
} | ||
const errorsCounter = 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 => filterFillParamsFnParams(config, { | ||
operationName: params.operationName, | ||
operationType: params.operationType, | ||
path: params.error?.path?.join('.'), | ||
phase: params.errorPhase, | ||
})); | ||
const reqCounter = getCounterFromConfig(config, 'graphql_envelop_request', { | ||
}, params => { | ||
const labels = { | ||
operationName: params.operationName, | ||
operationType: params.operationType, | ||
phase: params.errorPhase, | ||
}; | ||
if (params.error?.path) { | ||
labels.path = params.error.path?.join('.'); | ||
} | ||
return filterFillParamsFnParams(config, labels); | ||
}); | ||
if (errorsCounter) { | ||
['parse', 'validate'] | ||
.filter(phase => errorsCounter.phases.includes(phase)) | ||
.forEach(phase => { | ||
phasesToHook[phase].push({ | ||
shouldHandle: (params, context) => !!params.errorPhase && errorsCounter.shouldObserve(params, context), | ||
handler: ({ params, context }) => { | ||
const labels = errorsCounter.fillLabelsFn(params, context); | ||
errorsCounter?.counter.labels(labels).inc(); | ||
}, | ||
}); | ||
}); | ||
['execute', 'subscribe'] | ||
.filter(phase => errorsCounter.phases.includes(phase)) | ||
.forEach(phase => { | ||
phasesToHook[phase].result.push({ | ||
shouldHandle: errorsCounter.shouldObserve, | ||
handler: ({ result, params, context }) => { | ||
if (!result.errors?.length) { | ||
return; | ||
} | ||
for (const error of result.errors) { | ||
const labelParams = { ...params, errorPhase: 'execute', error }; | ||
if (errorsCounter.shouldObserve(labelParams, context)) { | ||
errorsCounter.counter | ||
.labels(errorsCounter.fillLabelsFn(labelParams, context)) | ||
.inc(); | ||
} | ||
} | ||
}, | ||
}); | ||
}); | ||
if (errorsCounter.phases.includes('subscribe')) { | ||
phasesToHook.subscribe.error.push({ | ||
shouldHandle: errorsCounter.shouldObserve, | ||
handler: ({ params, context, error }) => { | ||
const labels = errorsCounter.fillLabelsFn(params, context); | ||
errorsCounter.counter.labels(labels).inc(); | ||
}, | ||
}); | ||
} | ||
if (errorsCounter.phases.includes('context')) { | ||
phasesToHook.pluginInit.push(({ registerContextErrorHandler }) => { | ||
registerContextErrorHandler(({ context, error }) => { | ||
const fillLabelsFnParams = fillLabelsFnParamsMap.get(context); | ||
// FIXME: unsafe cast here, but it's ok, fillabelfn is doing duck typing anyway | ||
const params = { | ||
error: error, | ||
errorPhase: 'context', | ||
...fillLabelsFnParams, | ||
}; | ||
if (errorsCounter.shouldObserve(params, context)) { | ||
errorsCounter.counter.labels(errorsCounter?.fillLabelsFn(params, context)).inc(); | ||
} | ||
}); | ||
}); | ||
} | ||
} | ||
const reqCounter = getCounterFromConfig(config, 'graphql_envelop_request', ['execute', 'subscribe'], { | ||
help: 'Counts the amount of GraphQL requests executed through Envelop', | ||
}); | ||
const deprecationCounter = getCounterFromConfig(config, 'graphql_envelop_deprecated_field', { | ||
if (reqCounter) { | ||
const handler = { | ||
shouldHandle: reqCounter.shouldObserve, | ||
handler: ({ params, context }) => { | ||
reqCounter.counter.labels(reqCounter.fillLabelsFn(params, context)).inc(); | ||
}, | ||
}; | ||
for (const phase of reqCounter.phases) { | ||
phasesToHook[phase].end.push(handler); | ||
} | ||
} | ||
const deprecationCounter = getCounterFromConfig(config, 'graphql_envelop_deprecated_field', ['parse'], { | ||
help: 'Counts the amount of deprecated fields used in selection sets', | ||
@@ -67,6 +253,40 @@ labelNames: ['operationType', 'operationName', 'fieldName', 'typeName'], | ||
})); | ||
const schemaChangeCounter = getCounterFromConfig(config, 'graphql_envelop_schema_change', { | ||
if (deprecationCounter) { | ||
let typeInfo = null; | ||
phasesToHook.schema.push(({ schema }) => { | ||
typeInfo = new TypeInfo(schema); | ||
}); | ||
phasesToHook.parse.push({ | ||
shouldHandle: (params, context) => | ||
// If parse error happens, we can't explore the query document | ||
!!typeInfo && !params.errorPhase && deprecationCounter.shouldObserve(params, context), | ||
handler: ({ params, context }) => { | ||
const deprecatedFields = extractDeprecatedFields(params.document, typeInfo); | ||
for (const depField of deprecatedFields) { | ||
const deprecationLabelParams = { | ||
...params, | ||
deprecationInfo: depField, | ||
}; | ||
if (deprecationCounter.shouldObserve(deprecationLabelParams, context)) { | ||
deprecationCounter.counter | ||
.labels(deprecationCounter.fillLabelsFn(deprecationLabelParams, context)) | ||
.inc(); | ||
} | ||
} | ||
}, | ||
}); | ||
} | ||
const schemaChangeCounter = getCounterFromConfig(config, 'graphql_envelop_schema_change', ['schema'], { | ||
help: 'Counts the amount of schema changes', | ||
labelNames: [], | ||
}, () => ({})); | ||
if (schemaChangeCounter) { | ||
const countedSchemas = new WeakSet(); | ||
phasesToHook.schema.push(({ schema }) => { | ||
if (schemaChangeCounter?.shouldObserve({}, null) && !countedSchemas.has(schema)) { | ||
schemaChangeCounter.counter.inc(); | ||
countedSchemas.add(schema); | ||
} | ||
}); | ||
} | ||
const onParse = ({ context, params }) => { | ||
@@ -78,232 +298,157 @@ if (config.skipIntrospection && isIntrospectionOperationString(params.source)) { | ||
return params => { | ||
const fillLabelsFnParams = createFillLabelFnParams(params.result, context, params => filterFillParamsFnParams(config, params)); | ||
fillLabelsFnParamsMap.set(context, fillLabelsFnParams); | ||
const args = { | ||
context, | ||
totalTime: (Date.now() - startTime) / 1000, | ||
params: fillLabelsFnParams ?? { error: params.result, errorPhase: 'parse' }, | ||
}; | ||
phasesToHook.parse | ||
.filter(({ shouldHandle }) => shouldHandle(args.params, context)) | ||
.forEach(({ handler }) => handler(args)); | ||
}; | ||
}; | ||
const onValidate = ({ context }) => { | ||
const fillLabelsFnParams = fillLabelsFnParamsMap.get(context); | ||
if (!fillLabelsFnParams) { | ||
return undefined; | ||
} | ||
const startTime = Date.now(); | ||
return ({ valid }) => { | ||
const args = { | ||
params: valid ? fillLabelsFnParams : { ...fillLabelsFnParams, errorPhase: 'validate' }, | ||
context, | ||
totalTime: (Date.now() - startTime) / 1000, | ||
}; | ||
phasesToHook.validate | ||
.filter(({ shouldHandle }) => shouldHandle(args.params, context)) | ||
.forEach(({ handler }) => handler(args)); | ||
// TODO: we should probably iterate over validation errors to report each error. | ||
}; | ||
}; | ||
const onContextBuilding = ({ context }) => { | ||
const fillLabelsFnParams = fillLabelsFnParamsMap.get(context); | ||
if (!fillLabelsFnParams) { | ||
return; | ||
} | ||
const startTime = Date.now(); | ||
const args = { | ||
context, | ||
params: fillLabelsFnParams, | ||
totalTime: (Date.now() - startTime) / 1000, | ||
}; | ||
phasesToHook.context | ||
.filter(({ shouldHandle }) => shouldHandle(fillLabelsFnParams, context)) | ||
.forEach(({ handler }) => handler(args)); | ||
}; | ||
const onExecute = ({ args: { contextValue: context } }) => { | ||
const fillLabelsFnParams = fillLabelsFnParamsMap.get(context); | ||
if (!fillLabelsFnParams) { | ||
return; | ||
} | ||
const endHandlers = phasesToHook.execute.end.filter(({ shouldHandle }) => shouldHandle(fillLabelsFnParams, context)); | ||
const resultHandlers = phasesToHook.execute.result.filter(({ shouldHandle }) => shouldHandle(fillLabelsFnParams, context)); | ||
if (endHandlers.length + resultHandlers.length === 0) { | ||
return undefined; | ||
} | ||
const startTime = Date.now(); | ||
function handleResult({ result }) { | ||
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(); | ||
} | ||
} | ||
const args = { params: fillLabelsFnParams, context, totalTime, result }; | ||
resultHandlers.forEach(({ handler }) => handler(args)); | ||
} | ||
const handleEnd = () => { | ||
const totalTime = (Date.now() - startTime) / 1000; | ||
const args = { params: fillLabelsFnParams, context, totalTime }; | ||
endHandlers.forEach(({ handler }) => handler(args)); | ||
}; | ||
return { | ||
onExecuteDone: ({ result }) => { | ||
if (isAsyncIterable(result)) { | ||
return { | ||
onNext: resultHandlers.length ? handleResult : undefined, | ||
onEnd: endHandlers.length ? handleEnd : undefined, | ||
}; | ||
} | ||
} | ||
else { | ||
// means that we got a parse error, report it | ||
errorsCounter?.counter | ||
.labels({ | ||
phase: 'parse', | ||
}) | ||
.inc(); | ||
} | ||
else { | ||
handleResult({ result }); | ||
handleEnd(); | ||
return undefined; | ||
} | ||
}, | ||
}; | ||
}; | ||
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(); | ||
} | ||
}; | ||
const onSubscribe = ({ args: { contextValue: context } }) => { | ||
const fillLabelsFnParams = fillLabelsFnParamsMap.get(context); | ||
if (!fillLabelsFnParams) { | ||
return; | ||
} | ||
: 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); | ||
}; | ||
const endHandlers = phasesToHook.subscribe.end.filter(({ shouldHandle }) => shouldHandle(fillLabelsFnParams, context)); | ||
const resultHandlers = phasesToHook.subscribe.result.filter(({ shouldHandle }) => shouldHandle(fillLabelsFnParams, context)); | ||
const errorHandlers = phasesToHook.subscribe.error.filter(({ shouldHandle }) => shouldHandle(fillLabelsFnParams, context)); | ||
if (endHandlers.length + resultHandlers.length + errorHandlers.length === 0) { | ||
return undefined; | ||
} | ||
: 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); | ||
} | ||
const startTime = Date.now(); | ||
function handleResult({ result }) { | ||
const totalTime = (Date.now() - startTime) / 1000; | ||
const args = { params: fillLabelsFnParams, context, totalTime, result }; | ||
resultHandlers.forEach(({ handler }) => handler(args)); | ||
} | ||
const handleEnd = () => { | ||
const totalTime = (Date.now() - startTime) / 1000; | ||
const args = { params: fillLabelsFnParams, context, totalTime }; | ||
endHandlers.forEach(({ handler }) => handler(args)); | ||
}; | ||
const handleError = ({ error }) => { | ||
const totalTime = (Date.now() - startTime) / 1000; | ||
const args = { params: fillLabelsFnParams, context, totalTime, error }; | ||
errorHandlers.forEach(({ handler }) => handler(args)); | ||
}; | ||
return { | ||
onSubscribeResult: ({ result }) => { | ||
if (isAsyncIterable(result)) { | ||
return { | ||
onNext: resultHandlers.length ? handleResult : undefined, | ||
onEnd: endHandlers.length ? handleEnd : undefined, | ||
}; | ||
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; | ||
else { | ||
handleResult({ result }); | ||
handleEnd(); | ||
return undefined; | ||
} | ||
}, | ||
onSubscribeError: errorHandlers.length ? handleError : undefined, | ||
}; | ||
}; | ||
const onPluginInit = payload => { | ||
for (const handler of phasesToHook.pluginInit) { | ||
handler(payload); | ||
} | ||
: undefined; | ||
const countedSchemas = new WeakSet(); | ||
}; | ||
const onEnveloped = payload => { | ||
for (const handler of phasesToHook.enveloped) { | ||
handler(payload); | ||
} | ||
}; | ||
const onSchemaChange = payload => { | ||
for (const handler of phasesToHook.schema) { | ||
handler(payload); | ||
} | ||
}; | ||
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, | ||
onSchemaChange: phasesToHook.schema.length ? onSchemaChange : undefined, | ||
onPluginInit: phasesToHook.pluginInit.length ? onPluginInit : undefined, | ||
onEnveloped: phasesToHook.enveloped.length ? onEnveloped : undefined, | ||
onValidate: phasesToHook.validate.length ? onValidate : undefined, | ||
onContextBuilding: phasesToHook.context.length ? onContextBuilding : undefined, | ||
onExecute: phasesToHook.execute.end.length + phasesToHook.execute.result.length ? onExecute : undefined, | ||
onSubscribe: phasesToHook.subscribe.end.length + | ||
phasesToHook.subscribe.result.length + | ||
phasesToHook.subscribe.error.length | ||
? onSubscribe | ||
: undefined, | ||
}; | ||
}; |
190
esm/utils.js
@@ -43,7 +43,13 @@ import { visit, visitWithTypeInfo, } from 'graphql'; | ||
} | ||
/** | ||
* Histogram metric factory allowing to define custom metrics with advanced configuration. | ||
* @param options | ||
* @returns | ||
*/ | ||
export function createHistogram(options) { | ||
return { | ||
histogram: registerHistogram(options.registry, options.histogram), | ||
// histogram: new Histogram(options.histogram), | ||
fillLabelsFn: options.fillLabelsFn, | ||
phases: options.phases, | ||
shouldObserve: options.shouldObserve ?? (() => true), | ||
}; | ||
@@ -62,2 +68,7 @@ } | ||
} | ||
/** | ||
* Summary metric factory allowing to define custom metrics with advanced configuration. | ||
* @param options | ||
* @returns | ||
*/ | ||
export function createSummary(options) { | ||
@@ -67,4 +78,11 @@ return { | ||
fillLabelsFn: options.fillLabelsFn, | ||
phases: options.phases, | ||
shouldObserve: options.shouldObserve ?? (() => true), | ||
}; | ||
} | ||
/** | ||
* Counter metric factory allowing to define custom metrics with advanced configuration. | ||
* @param options | ||
* @returns | ||
*/ | ||
export function registerCounter(registry, conf) { | ||
@@ -85,5 +103,7 @@ if (!counters.has(registry)) { | ||
fillLabelsFn: options.fillLabelsFn, | ||
phases: options.phases, | ||
shouldObserve: options.shouldObserve, | ||
}; | ||
} | ||
export function getHistogramFromConfig(config, phase, histogram, fillLabelsFn = params => ({ | ||
export function getHistogramFromConfig(config, phase, availablePhases, histogram, fillLabelsFn = params => ({ | ||
operationName: params.operationName, | ||
@@ -93,20 +113,43 @@ operationType: params.operationType, | ||
const metric = config.metrics[phase]; | ||
if (Array.isArray(metric) && metric.length === 0) { | ||
histogram.buckets = metric; | ||
if (!metric) { | ||
return undefined; | ||
} | ||
return typeof metric === 'object' | ||
? metric | ||
: metric === true | ||
? createHistogram({ | ||
registry: config.registry || defaultRegistry, | ||
histogram: { | ||
name: typeof metric === 'string' ? metric : phase, | ||
...histogram, | ||
labelNames: (histogram.labelNames ?? ['operationType', 'operationName']).filter(label => labelExists(config, label)), | ||
}, | ||
fillLabelsFn: (...args) => filterFillParamsFnParams(config, fillLabelsFn(...args)), | ||
}) | ||
: undefined; | ||
let phases = availablePhases; | ||
if (Array.isArray(metric)) { | ||
if (metric.length === 0) { | ||
throw TypeError(`Bad value provided for 'metrics.${phase.toString()}': the array must contain at least one element`); | ||
} | ||
else if (isBucketsList(metric)) { | ||
histogram.buckets = metric; | ||
} | ||
else if (isPhasesList(metric)) { | ||
phases = filterAvailablePhases(metric, availablePhases); | ||
} | ||
else { | ||
throw TypeError(`Bad value provided for 'metrics.${phase.toString()}': the array must contain only numbers (buckets) or string (phases)`); | ||
} | ||
} | ||
else if (typeof metric === 'object') { | ||
const customMetric = metric; | ||
if (!customMetric.phases) { | ||
customMetric.phases = availablePhases; | ||
} | ||
if (!customMetric.shouldObserve) { | ||
customMetric.shouldObserve = () => true; | ||
} | ||
return customMetric; | ||
} | ||
return createHistogram({ | ||
registry: config.registry || defaultRegistry, | ||
histogram: { | ||
name: typeof metric === 'string' ? metric : phase, | ||
...histogram, | ||
labelNames: (histogram.labelNames ?? ['operationType', 'operationName']).filter(label => labelExists(config, label)), | ||
}, | ||
fillLabelsFn: (...args) => filterFillParamsFnParams(config, fillLabelsFn(...args)), | ||
phases, | ||
shouldObserve: () => true, | ||
}); | ||
} | ||
export function getSummaryFromConfig(config, phase, summary, fillLabelsFn = params => filterFillParamsFnParams(config, { | ||
export function getSummaryFromConfig(config, phase, availablePhases, summary, fillLabelsFn = params => filterFillParamsFnParams(config, { | ||
operationName: params.operationName, | ||
@@ -116,17 +159,40 @@ operationType: params.operationType, | ||
const metric = config.metrics[phase]; | ||
return typeof metric === 'object' | ||
? metric | ||
: metric === true | ||
? createSummary({ | ||
registry: config.registry || defaultRegistry, | ||
summary: { | ||
name: typeof metric === 'string' ? metric : phase, | ||
labelNames: ['operationType', 'operationName'].filter(label => labelExists(config, label)), | ||
...summary, | ||
}, | ||
fillLabelsFn, | ||
}) | ||
: undefined; | ||
if (!metric) { | ||
return undefined; | ||
} | ||
let phases = availablePhases; | ||
if (Array.isArray(metric)) { | ||
if (metric.length === 0) { | ||
throw TypeError(`Bad value provided for 'metrics.${phase.toString()}': the array must contain at least one element`); | ||
} | ||
else if (isPhasesList(metric)) { | ||
phases = filterAvailablePhases(metric, availablePhases); | ||
} | ||
else { | ||
throw new TypeError(`Bad value provided for 'metrics.${phase.toString()}': the array must contain only strings (phases)`); | ||
} | ||
} | ||
else if (typeof metric === 'object') { | ||
const customMetric = metric; | ||
if (!customMetric.phases) { | ||
customMetric.phases = availablePhases; | ||
} | ||
if (!customMetric.shouldObserve) { | ||
customMetric.shouldObserve = () => true; | ||
} | ||
return customMetric; | ||
} | ||
return createSummary({ | ||
registry: config.registry || defaultRegistry, | ||
summary: { | ||
name: typeof metric === 'string' ? metric : phase, | ||
labelNames: ['operationType', 'operationName'].filter(label => labelExists(config, label)), | ||
...summary, | ||
}, | ||
fillLabelsFn, | ||
phases, | ||
shouldObserve: () => true, | ||
}); | ||
} | ||
export function getCounterFromConfig(config, phase, counter, fillLabelsFn = params => filterFillParamsFnParams(config, { | ||
export function getCounterFromConfig(config, phase, availablePhases, counter, fillLabelsFn = params => filterFillParamsFnParams(config, { | ||
operationName: params.operationName, | ||
@@ -136,15 +202,38 @@ operationType: params.operationType, | ||
const metric = config.metrics[phase]; | ||
return typeof metric === 'object' | ||
? metric | ||
: metric === true | ||
? createCounter({ | ||
registry: config.registry || defaultRegistry, | ||
counter: { | ||
name: typeof metric === 'string' ? metric : phase, | ||
labelNames: ['operationType', 'operationName'].filter(label => labelExists(config, label)), | ||
...counter, | ||
}, | ||
fillLabelsFn, | ||
}) | ||
: undefined; | ||
let phases = availablePhases; | ||
if (!metric) { | ||
return undefined; | ||
} | ||
if (Array.isArray(metric)) { | ||
if (metric.length === 0) { | ||
throw TypeError(`Bad value provided for 'metrics.${phase.toString()}': the array must contain at least one element`); | ||
} | ||
else if (isPhasesList(metric)) { | ||
phases = filterAvailablePhases(metric, availablePhases); | ||
} | ||
else { | ||
throw new TypeError(`Bad value provided for 'metrics.${phase.toString()}': the array must contain only strings (phases)`); | ||
} | ||
} | ||
else if (typeof metric === 'object') { | ||
const customMetric = metric; | ||
if (!customMetric.phases) { | ||
customMetric.phases = availablePhases; | ||
} | ||
if (!customMetric.shouldObserve) { | ||
customMetric.shouldObserve = () => true; | ||
} | ||
return customMetric; | ||
} | ||
return createCounter({ | ||
registry: config.registry || defaultRegistry, | ||
counter: { | ||
name: typeof metric === 'string' ? metric : phase, | ||
labelNames: ['operationType', 'operationName'].filter(label => labelExists(config, label)), | ||
...counter, | ||
}, | ||
fillLabelsFn, | ||
phases, | ||
shouldObserve: () => true, | ||
}); | ||
} | ||
@@ -185,3 +274,3 @@ export function extractDeprecatedFields(node, typeInfo) { | ||
} | ||
return labelFlag; | ||
return !!labelFlag; | ||
} | ||
@@ -204,1 +293,10 @@ export function filterFillParamsFnParams(config, params) { | ||
} | ||
function isBucketsList(list) { | ||
return list.every(item => typeof item === 'number'); | ||
} | ||
function isPhasesList(list) { | ||
return list.every(item => typeof item === 'string'); | ||
} | ||
function filterAvailablePhases(phases, availablePhases) { | ||
return availablePhases.filter(phase => phases.includes(phase)); | ||
} |
{ | ||
"name": "@envelop/prometheus", | ||
"version": "11.0.0", | ||
"version": "11.1.0-alpha-20241122091727-adade563355e3d213f27427a9a1d86adf9431d41", | ||
"sideEffects": false, | ||
"peerDependencies": { | ||
"@envelop/core": "^5.0.1", | ||
"@envelop/core": "^5.0.2", | ||
"graphql": "^14.0.0 || ^15.0.0 || ^16.0.0", | ||
@@ -8,0 +8,0 @@ "prom-client": "^15.0.0" |
@@ -75,2 +75,7 @@ ## `@envelop/prometheus` | ||
A metric can observe events in different phases of of GraphQL request pipeline. By default, if a | ||
metric is available, it will observe timing or events in every available phases for this metric. You | ||
can configure this by either providing an array instead of `true` in the metrics config, or use the | ||
`phases` option in the custom metric factory. | ||
### `graphql_envelop_phase_parse` | ||
@@ -523,2 +528,3 @@ | ||
}), | ||
phases: ['parse'], // This is an array of phases that should be hooked for this metric | ||
fillLabelsFn: params => { | ||
@@ -537,2 +543,69 @@ // if you wish to fill your `labels` with metadata, you can use the params in order to get access to things like DocumentNode, operationName, operationType, `error` (for error metrics) and `info` (for resolvers metrics) | ||
### Configure metric phases | ||
Each metric observes timing or events in different phases of the GraphQL request pipeline. | ||
You can configure which phases are observed for a given metric by providing an array of phases | ||
instead of `true` for any metric configuration. You can also configure the phases when using custom | ||
metrics factories by providing the `phases` option. | ||
By default, all available phases are enabled when the metric is enabled. | ||
```ts | ||
import { execute, parse, specifiedRules, subscribe, validate } from 'graphql' | ||
import { envelop, useEngine } from '@envelop/core' | ||
import { usePrometheus } from '@envelop/prometheus' | ||
const getEnveloped = envelop({ | ||
plugins: [ | ||
useEngine({ parse, validate, specifiedRules, execute, subscribe }), | ||
usePrometheus({ | ||
metrics: { | ||
graphql_envelop_phase_error: ['execute', 'subscribe'] // only trace errors of execute and subscribe phases | ||
} | ||
}) | ||
] | ||
}) | ||
``` | ||
### Skip observation based on request context | ||
To save bandwidth or storage, you can reduce the amount of reported values by filtering which events | ||
are observed based on the request context. | ||
For example, you can only monitor a subset of operations, because they are critical or that you want | ||
to debug it's performance: | ||
```ts | ||
import { execute, parse, specifiedRules, subscribe, validate } from 'graphql' | ||
import { envelop, useEngine } from '@envelop/core' | ||
import { usePrometheus } from '@envelop/prometheus' | ||
const TRACKED_OPERATION_NAMES = [ | ||
// make a list of operation that you want to monitor | ||
] | ||
const getEnveloped = envelop({ | ||
plugins: [ | ||
useEngine({ parse, validate, specifiedRules, execute, subscribe }), | ||
usePrometheus({ | ||
metrics: { | ||
graphql_yoga_http_duration: createHistogram({ | ||
registry, | ||
histogram: { | ||
name: 'graphql_yoga_http_duration', | ||
help: 'Time spent on HTTP connection', | ||
labelNames: ['operation_name'] | ||
}, | ||
fillLabelsFn: ({ operationName }, _rawContext) => ({ | ||
operation_name: operationName | ||
}), | ||
shouldObserve: context => TRACKED_OPERATIONS.includes(context?.params?.operationName) | ||
}) | ||
} | ||
}) | ||
] | ||
}) | ||
``` | ||
## Caveats | ||
@@ -539,0 +612,0 @@ |
@@ -0,3 +1,4 @@ | ||
import type { GraphQLResolveInfo } from 'graphql'; | ||
import { Registry } from 'prom-client'; | ||
import { createCounter, createHistogram, createSummary } from './utils.js'; | ||
import { createCounter, createHistogram, createSummary, type AtLeastOne, type DeprecatedFieldInfo, type FillLabelsFnParams } from './utils.js'; | ||
export type PrometheusTracingPluginConfig = { | ||
@@ -44,4 +45,10 @@ /** | ||
* It is exposed as a counter. | ||
* | ||
* You can pass multiple type of values: | ||
* - boolean: Disable or Enable the metric with default configuration | ||
* - string: Enable the metric with custom name | ||
* - string[]: Enable the metric on a list of phases | ||
* - ReturnType<typeof createCounter>: Enable the metric with custom configuration | ||
*/ | ||
graphql_envelop_request?: boolean | string | ReturnType<typeof createCounter>; | ||
graphql_envelop_request?: CounterMetricOption<'execute' | 'subscribe'>; | ||
/** | ||
@@ -54,11 +61,18 @@ * Tracks the duration of the complete GraphQL operation execution. | ||
* - string: Enable the metric with custom name | ||
* - string[]: Enable the metric on a list of phases | ||
* - number[]: Enable the metric with custom buckets | ||
* - ReturnType<typeof createHistogram>: Enable the metric with custom configuration | ||
*/ | ||
graphql_envelop_request_duration?: boolean | string | number[] | ReturnType<typeof createHistogram>; | ||
graphql_envelop_request_duration?: HistogramMetricOption<'execute' | 'subscribe'>; | ||
/** | ||
* Provides a summary of the time spent on the GraphQL operation execution. | ||
* It reports the same timing than graphql_envelop_request_duration but as a summary. | ||
* | ||
* You can pass multiple type of values: | ||
* - boolean: Disable or Enable the metric with default configuration | ||
* - string: Enable the metric with custom name | ||
* - string[]: Enable the metric on a list of phases | ||
* - ReturnType<typeof createSummary>: Enable the metric with custom configuration | ||
*/ | ||
graphql_envelop_request_time_summary?: boolean | string | ReturnType<typeof createSummary>; | ||
graphql_envelop_request_time_summary?: SummaryMetricOption<'execute' | 'subscribe'>; | ||
/** | ||
@@ -72,6 +86,7 @@ * Tracks the duration of the parse phase of the GraphQL execution. | ||
* - string: Enable the metric with custom name | ||
* - string[]: Enable the metric on a list of phases | ||
* - number[]: Enable the metric with custom buckets | ||
* - ReturnType<typeof createHistogram>: Enable the metric with custom configuration | ||
*/ | ||
graphql_envelop_phase_parse?: boolean | string | number[] | ReturnType<typeof createHistogram>; | ||
graphql_envelop_phase_parse?: HistogramMetricOption<'parse'>; | ||
/** | ||
@@ -85,6 +100,7 @@ * Tracks the duration of the validate phase of the GraphQL execution. | ||
* - string: Enable the metric with custom name | ||
* - string[]: Enable the metric on a list of phases | ||
* - number[]: Enable the metric with custom buckets | ||
* - ReturnType<typeof createHistogram>: Enable the metric with custom configuration | ||
*/ | ||
graphql_envelop_phase_validate?: boolean | string | number[] | ReturnType<typeof createHistogram>; | ||
graphql_envelop_phase_validate?: HistogramMetricOption<'validate'>; | ||
/** | ||
@@ -98,6 +114,7 @@ * Tracks the duration of the context phase of the GraphQL execution. | ||
* - string: Enable the metric with custom name | ||
* - string[]: Enable the metric on a list of phases | ||
* - number[]: Enable the metric with custom buckets | ||
* - ReturnType<typeof createHistogram>: Enable the metric with custom configuration | ||
*/ | ||
graphql_envelop_phase_context?: boolean | string | number[] | ReturnType<typeof createHistogram>; | ||
graphql_envelop_phase_context?: HistogramMetricOption<'context'>; | ||
/** | ||
@@ -112,6 +129,7 @@ * Tracks the duration of the execute phase of the GraphQL execution. | ||
* - string: Enable the metric with custom name | ||
* - string[]: Enable the metric on a list of phases | ||
* - number[]: Enable the metric with custom buckets | ||
* - ReturnType<typeof createHistogram>: Enable the metric with custom configuration | ||
*/ | ||
graphql_envelop_phase_execute?: boolean | string | number[] | ReturnType<typeof createHistogram>; | ||
graphql_envelop_phase_execute?: HistogramMetricOption<'execute'>; | ||
/** | ||
@@ -125,6 +143,7 @@ * This metric tracks the duration of the subscribe phase of the GraphQL execution. | ||
* - string: Enable the metric with custom name | ||
* - string[]: Enable the metric on a list of phases | ||
* - number[]: Enable the metric with custom buckets | ||
* - ReturnType<typeof createHistogram>: Enable the metric with custom configuration | ||
*/ | ||
graphql_envelop_phase_subscribe?: boolean | string | number[] | ReturnType<typeof createHistogram>; | ||
graphql_envelop_phase_subscribe?: HistogramMetricOption<'subscribe'>; | ||
/** | ||
@@ -135,9 +154,26 @@ * This metric tracks the number of errors that returned by the GraphQL execution. | ||
* It is exposed as a counter. | ||
* | ||
* You can pass multiple type of values: | ||
* - boolean: Disable or Enable the metric with default configuration | ||
* - string: Enable the metric with custom name | ||
* - string[]: Enable the metric on a list of phases | ||
* - ReturnType<typeof createCounter>: Enable the metric with custom configuration | ||
*/ | ||
graphql_envelop_error_result?: boolean | string | ReturnType<typeof createCounter>; | ||
graphql_envelop_error_result?: CounterMetricOption<'parse' | 'validate' | 'context' | 'execute' | 'subscribe', string, FillLabelsFnParams & { | ||
error: unknown; | ||
errorPhase: 'parse' | 'validate' | 'context' | 'execute' | 'subscribe'; | ||
}>; | ||
/** | ||
* This metric tracks the number of deprecated fields used in the GraphQL operation. | ||
* It is exposed as a counter. | ||
* | ||
* You can pass multiple type of values: | ||
* - boolean: Disable or Enable the metric with default configuration | ||
* - string: Enable the metric with custom name | ||
* - string[]: Enable the metric on a list of phases | ||
* - ReturnType<typeof createCounter>: Enable the metric with custom configuration | ||
*/ | ||
graphql_envelop_deprecated_field?: boolean | string | ReturnType<typeof createCounter>; | ||
graphql_envelop_deprecated_field?: CounterMetricOption<'parse', string, FillLabelsFnParams & { | ||
deprecationInfo: DeprecatedFieldInfo; | ||
}>; | ||
/** | ||
@@ -149,4 +185,10 @@ * This metric tracks the number of schema changes that have occurred since the gateway started. | ||
* It is exposed as a counter. | ||
* | ||
* You can pass multiple type of values: | ||
* - boolean: Disable or Enable the metric with default configuration | ||
* - string: Enable the metric with custom name | ||
* - string[]: Enable the metric on a list of phases | ||
* - ReturnType<typeof createCounter>: Enable the metric with custom configuration | ||
*/ | ||
graphql_envelop_schema_change?: boolean | string | ReturnType<typeof createCounter>; | ||
graphql_envelop_schema_change?: CounterMetricOption<'schema', string, {}>; | ||
/** | ||
@@ -162,6 +204,9 @@ * This metric tracks the duration of each resolver execution. | ||
* - string: Enable the metric with custom name | ||
* - string[]: Enable the metric on a list of phases | ||
* - number[]: Enable the metric with custom buckets | ||
* - ReturnType<typeof createHistogram>: Enable the metric with custom configuration | ||
*/ | ||
graphql_envelop_execute_resolver?: boolean | string | number[] | ReturnType<typeof createHistogram>; | ||
graphql_envelop_execute_resolver?: HistogramMetricOption<'subscribe' | 'execute', string, FillLabelsFnParams & { | ||
info: GraphQLResolveInfo; | ||
}>; | ||
}; | ||
@@ -210,1 +255,5 @@ export type LabelsConfig = { | ||
}; | ||
export type HistogramMetricOption<Phases, LabelNames extends string = string, Params extends Record<string, unknown> = FillLabelsFnParams> = boolean | string | BucketsConfig | AtLeastOne<Phases> | ReturnType<typeof createHistogram<Phases, LabelNames, Params>>; | ||
export type BucketsConfig = AtLeastOne<number>; | ||
export type CounterMetricOption<Phases, LabelNames extends string = string, Params extends Record<string, unknown> = FillLabelsFnParams> = boolean | string | AtLeastOne<Phases> | ReturnType<typeof createCounter<Phases, LabelNames, Params>>; | ||
export type SummaryMetricOption<Phases, LabelNames extends string = string, Params extends Record<string, unknown> = FillLabelsFnParams> = boolean | string | AtLeastOne<Phases> | ReturnType<typeof createSummary<Phases, LabelNames, Params>>; |
import { Plugin } from '@envelop/core'; | ||
import { PrometheusTracingPluginConfig } from './config.js'; | ||
import { CounterMetricOption, HistogramMetricOption, PrometheusTracingPluginConfig, SummaryMetricOption } from './config.js'; | ||
import { createCounter, createHistogram, createSummary, FillLabelsFnParams, getCounterFromConfig, getHistogramFromConfig, getSummaryFromConfig, type CounterAndLabels, type HistogramAndLabels, type SummaryAndLabels } from './utils.js'; | ||
export { CounterAndLabels, FillLabelsFnParams, HistogramAndLabels, PrometheusTracingPluginConfig, SummaryAndLabels, createCounter, createHistogram, createSummary, getCounterFromConfig, getHistogramFromConfig, getSummaryFromConfig, }; | ||
export { CounterAndLabels, FillLabelsFnParams, HistogramAndLabels, PrometheusTracingPluginConfig, SummaryAndLabels, HistogramMetricOption, CounterMetricOption, SummaryMetricOption, createCounter, createHistogram, createSummary, getCounterFromConfig, getHistogramFromConfig, getSummaryFromConfig, }; | ||
export declare const fillLabelsFnParamsMap: WeakMap<any, FillLabelsFnParams | null>; | ||
export declare const execStartTimeMap: WeakMap<any, number>; | ||
export declare const usePrometheus: (config: PrometheusTracingPluginConfig) => Plugin; |
@@ -21,38 +21,141 @@ import { ASTNode, DocumentNode, GraphQLError, GraphQLResolveInfo, OperationDefinitionNode, TypeInfo } from 'graphql'; | ||
export type FillLabelsFn<LabelNames extends string, Params extends Record<string, any>> = (params: Params, rawContext: any) => Record<LabelNames, string | number>; | ||
export type HistogramAndLabels<LabelNames extends string, Params extends Record<string, any>> = { | ||
export type ShouldObservePredicate<Params extends Record<string, any>> = (params: Params, rawContext: any) => boolean; | ||
export type HistogramAndLabels<Phases, LabelNames extends string, Params extends Record<string, any>> = { | ||
histogram: Histogram<LabelNames>; | ||
fillLabelsFn: FillLabelsFn<LabelNames, Params>; | ||
phases?: AtLeastOne<Phases>; | ||
shouldObserve?: ShouldObservePredicate<Params>; | ||
}; | ||
export declare function registerHistogram<LabelNames extends string>(registry: Registry, conf: Omit<HistogramConfiguration<LabelNames>, 'registers'>): Histogram<LabelNames>; | ||
export declare function createHistogram<LabelNames extends string, Params extends Record<string, any> = FillLabelsFnParams>(options: { | ||
/** | ||
* Histogram metric factory allowing to define custom metrics with advanced configuration. | ||
* @param options | ||
* @returns | ||
*/ | ||
export declare function createHistogram<Phases, LabelNames extends string, Params extends Record<string, any> = FillLabelsFnParams>(options: { | ||
/** | ||
* The registry to be used by the plugin. If you don't have a custom registry, | ||
* use `register` exported variable from `prom-client`. | ||
*/ | ||
registry: Registry; | ||
/** | ||
* The configuration of the histogram, as expected by the `prom-client` library. | ||
*/ | ||
histogram: Omit<HistogramConfiguration<LabelNames>, 'registers'>; | ||
/** | ||
* A function called when an event is observed to extract labels values from the context. | ||
*/ | ||
fillLabelsFn: FillLabelsFn<LabelNames, Params>; | ||
}): HistogramAndLabels<LabelNames, Params>; | ||
export type SummaryAndLabels<LabelNames extends string, Params extends Record<string, any>> = { | ||
/** | ||
* A list of GraphQL pipeline phases which will be observed by this metric. | ||
* | ||
* The possible values accepted in this list depends on the metric, | ||
* please refer to metric type or documentation to know which phases ar available. | ||
* | ||
* By default, all available phases are observed | ||
*/ | ||
phases?: AtLeastOne<Phases>; | ||
/** | ||
* A function called for each event that can be observed. | ||
* If it is provided, an event will be observed only if it returns true. | ||
* | ||
* By default, all events are observed. | ||
*/ | ||
shouldObserve?: ShouldObservePredicate<Params>; | ||
}): HistogramAndLabels<Phases, LabelNames, Params>; | ||
export type SummaryAndLabels<Phases, LabelNames extends string, Params extends Record<string, any>> = { | ||
summary: Summary<LabelNames>; | ||
fillLabelsFn: FillLabelsFn<LabelNames, Params>; | ||
phases?: AtLeastOne<Phases>; | ||
shouldObserve?: ShouldObservePredicate<Params>; | ||
}; | ||
export declare function registerSummary<LabelNames extends string>(registry: Registry, conf: Omit<SummaryConfiguration<LabelNames>, 'registers'>): Summary<LabelNames>; | ||
export declare function createSummary<LabelNames extends string, Params extends Record<string, any> = FillLabelsFnParams>(options: { | ||
/** | ||
* Summary metric factory allowing to define custom metrics with advanced configuration. | ||
* @param options | ||
* @returns | ||
*/ | ||
export declare function createSummary<Phases, LabelNames extends string, Params extends Record<string, any> = FillLabelsFnParams>(options: { | ||
/** | ||
* The registry to be used by the plugin. If you don't have a custom registry, | ||
* use `register` exported variable from `prom-client`. | ||
*/ | ||
registry: Registry; | ||
/** | ||
* The configuration of the summary, as expected by the `prom-client` library. | ||
*/ | ||
summary: Omit<SummaryConfiguration<LabelNames>, 'registers'>; | ||
/** | ||
* A function called when an event is observed to extract labels values from the context. | ||
*/ | ||
fillLabelsFn: FillLabelsFn<LabelNames, Params>; | ||
}): SummaryAndLabels<LabelNames, Params>; | ||
export type CounterAndLabels<LabelNames extends string, Params extends Record<string, any>> = { | ||
/** | ||
* A list of GraphQL pipeline phases which will be observed by this metric. | ||
* | ||
* The possible values accepted in this list depends on the metric, | ||
* please refer to metric type or documentation to know which phases ar available. | ||
* | ||
* By default, all available phases are observed | ||
*/ | ||
phases?: AtLeastOne<Phases>; | ||
/** | ||
* A function called for each event that can be observed. | ||
* If it is provided, an event will be observed only if it returns true. | ||
* | ||
* By default, all events are observed. | ||
*/ | ||
shouldObserve?: ShouldObservePredicate<Params>; | ||
}): SummaryAndLabels<Phases, LabelNames, Params>; | ||
export type CounterAndLabels<Phases, LabelNames extends string, Params extends Record<string, any>> = { | ||
counter: Counter<LabelNames>; | ||
fillLabelsFn: FillLabelsFn<LabelNames, Params>; | ||
phases?: AtLeastOne<Phases>; | ||
shouldObserve?: ShouldObservePredicate<Params>; | ||
}; | ||
/** | ||
* Counter metric factory allowing to define custom metrics with advanced configuration. | ||
* @param options | ||
* @returns | ||
*/ | ||
export declare function registerCounter<LabelNames extends string>(registry: Registry, conf: Omit<CounterConfiguration<LabelNames>, 'registers'>): Counter<LabelNames>; | ||
export declare function createCounter<LabelNames extends string, Params extends Record<string, any> = FillLabelsFnParams>(options: { | ||
export declare function createCounter<Phases, LabelNames extends string, Params extends Record<string, any> = FillLabelsFnParams>(options: { | ||
/** | ||
* The registry to be used by the plugin. If you don't have a custom registry, | ||
* use `register` exported variable from `prom-client`. | ||
*/ | ||
registry: Registry; | ||
/** | ||
* The configuration of the counter, as expected by the `prom-client` library. | ||
*/ | ||
counter: Omit<CounterConfiguration<LabelNames>, 'registers'>; | ||
/** | ||
* A function called when an event is observed to extract labels values from the context. | ||
*/ | ||
fillLabelsFn: FillLabelsFn<LabelNames, Params>; | ||
}): CounterAndLabels<LabelNames, Params>; | ||
export declare function getHistogramFromConfig<MetricOptions, Params extends Record<string, any> = FillLabelsFnParams>(config: PrometheusTracingPluginConfig, phase: keyof MetricOptions, histogram: Omit<HistogramConfiguration<string>, 'registers' | 'name'>, fillLabelsFn?: FillLabelsFn<string, Params>): ReturnType<typeof createHistogram<string, Params>> | undefined; | ||
export declare function getSummaryFromConfig<MetricOptions, Params extends Record<string, any> = FillLabelsFnParams>(config: PrometheusTracingPluginConfig, phase: keyof MetricOptions, summary: Omit<SummaryConfiguration<string>, 'registers' | 'name'>, fillLabelsFn?: FillLabelsFn<string, Params>): ReturnType<typeof createSummary<string, Params>> | undefined; | ||
export declare function getCounterFromConfig<MetricOptions, Params extends Record<string, any> = FillLabelsFnParams>(config: PrometheusTracingPluginConfig, phase: keyof MetricOptions, counter: Omit<CounterConfiguration<string>, 'registers' | 'name'>, fillLabelsFn?: FillLabelsFn<string, Params>): ReturnType<typeof createCounter<string, Params>> | undefined; | ||
/** | ||
* A list of GraphQL pipeline phases which will be observed by this metric. | ||
* | ||
* The possible values accepted in this list depends on the metric, | ||
* please refer to metric type or documentation to know which phases ar available. | ||
* | ||
* By default, all available phases are observed | ||
*/ | ||
phases?: AtLeastOne<Phases>; | ||
/** | ||
* A function called for each event that can be observed. | ||
* If it is provided, an event will be observed only if it returns true. | ||
* | ||
* By default, all events are observed. | ||
*/ | ||
shouldObserve?: ShouldObservePredicate<Params>; | ||
}): CounterAndLabels<Phases, LabelNames, Params>; | ||
export declare function getHistogramFromConfig<Phases, MetricOptions, Params extends Record<string, any> = FillLabelsFnParams>(config: PrometheusTracingPluginConfig, phase: keyof MetricOptions, availablePhases: AtLeastOne<Phases>, histogram: Omit<HistogramConfiguration<string>, 'registers' | 'name'>, fillLabelsFn?: FillLabelsFn<string, Params>): Required<HistogramAndLabels<Phases, string, Params>> | undefined; | ||
export declare function getSummaryFromConfig<Phases, MetricOptions, Params extends Record<string, any> = FillLabelsFnParams>(config: PrometheusTracingPluginConfig, phase: keyof MetricOptions, availablePhases: AtLeastOne<Phases>, summary: Omit<SummaryConfiguration<string>, 'registers' | 'name'>, fillLabelsFn?: FillLabelsFn<string, Params>): Required<SummaryAndLabels<Phases, string, Params>> | undefined; | ||
export declare function getCounterFromConfig<Phases, MetricOptions, Params extends Record<string, any> = FillLabelsFnParams>(config: PrometheusTracingPluginConfig, phase: keyof MetricOptions, availablePhases: AtLeastOne<Phases>, counter: Omit<CounterConfiguration<string>, 'registers' | 'name'>, fillLabelsFn?: FillLabelsFn<string, Params>): Required<CounterAndLabels<Phases, string, Params>> | undefined; | ||
export declare function extractDeprecatedFields(node: ASTNode, typeInfo: TypeInfo): DeprecatedFieldInfo[]; | ||
export declare function labelExists(config: PrometheusTracingPluginConfig, label: string): any; | ||
export declare function labelExists(config: { | ||
labels?: Record<string, unknown>; | ||
}, label: string): boolean; | ||
export declare function filterFillParamsFnParams<T extends string>(config: PrometheusTracingPluginConfig, params: Partial<Record<T, any>>): Record<T, any>; | ||
export declare function instrumentRegistry(registry: Registry): Registry<"text/plain; version=0.0.4; charset=utf-8">; | ||
export type AtLeastOne<T> = [T, ...T[]]; |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
144870
1933
633
2