import { PluginDefinition } from 'apollo-server-core'; import { GraphQLError } from 'graphql'; import { IResolvers } from 'graphql-tools'; import * as prom from 'prom-client'; import { TracingFormat } from 'apollo-tracing'; export const ResolverMetricsPlugin = (resolvers: IResolvers, metricPrefix: string = "gql"): PluginDefinition => { const resolverHistogram = new prom.Histogram({ name: `${metricPrefix}_resolver_seconds`, help: "measures gql resolver duration", labelNames: ["path"], buckets: [0.2, 0.5, 1, 1.5, 3, 5, 10], }); const resolverErrors = new prom.Counter({ name: `${metricPrefix}_resolver_errors`, help: "measures number of resolver errors", labelNames: ["path", "type"], }); return { requestDidStart() { return { didEncounterErrors(gqlContext) { const originalError: any = gqlContext.errors; originalError.forEach((error: GraphQLError) => { resolverErrors.inc({ path: (error.path && error.path.filter((e: any) => typeof(e) === "string").join(".")) || "unknown", type: (error as any).type || error.constructor.name, }); }); }, willSendResponse(gqlResponseContext) { const response = gqlResponseContext.response; if ( response && response.extensions && response.extensions.tracing && response.errors === undefined ) { const implementedResolvers = response.extensions.tracing.execution.resolvers.filter((e: any) => { return Boolean(hasPath(resolvers, `${e.parentType}.${e.path.join('.')}`)) }); implementedResolvers.forEach((l: TracingFormat['execution']['resolvers'][0]) => { resolverHistogram.observe({ path: l.path.filter((e: any) => typeof(e) === "string").join(".") }, l.duration / 1e9); }); } } } } } }; function hasPath(obj: object, path: string, separator: string ='.'): T | undefined { const properties: string[] = Array.isArray(path) ? path : path.split(separator); return properties.reduce( (prev: any, curr: any) => prev && prev[curr], obj ) as T }