UNPKG

@pothos/plugin-tracing

Version:

A Pothos plugin for tracing and logging resolver invocations

179 lines (139 loc) 4.27 kB
import { isThenable, type PothosOutputFieldConfig, type PothosOutputFieldType, type SchemaTypes, } from '@pothos/core'; import { defaultFieldResolver, type GraphQLFieldResolver, type GraphQLResolveInfo } from 'graphql'; export function isRootField<Types extends SchemaTypes>(config: PothosOutputFieldConfig<Types>) { return ( config.parentType === 'Query' || config.parentType === 'Mutation' || config.parentType === 'Subscription' ); } export function isScalarField<Types extends SchemaTypes>(config: PothosOutputFieldConfig<Types>) { return resolveFieldType(config.type) === 'Scalar'; } export function isEnumField<Types extends SchemaTypes>(config: PothosOutputFieldConfig<Types>) { return resolveFieldType(config.type) === 'Enum'; } export function isExposedField<Types extends SchemaTypes>(config: PothosOutputFieldConfig<Types>) { return ( !!config.extensions?.pothosExposedField || !config.resolve || config.resolve === defaultFieldResolver ); } export function resolveFieldType<Types extends SchemaTypes>( type: PothosOutputFieldType<Types>, ): 'Enum' | 'Interface' | 'Object' | 'Scalar' | 'Union' { if (type.kind === 'List') { return resolveFieldType(type.type); } return type.kind; } const spanCacheSymbol = Symbol.for('Pothos.tracing.spanCache'); interface InternalContext<T> { [spanCacheSymbol]?: Record<string, T>; } export function pathToString(info: GraphQLResolveInfo) { let current = info.path; let path = String(current.key); while (current.prev) { current = current.prev; path = `${current.key}.${path}`; } return path; } function getParentPaths(path: GraphQLResolveInfo['path']): [string, ...string[]] { if (!path.prev) { return [String(path.key)]; } const parentPaths = getParentPaths(path.prev); return [`${parentPaths[0]}.${path.key}`, ...parentPaths]; } export function getParentSpan<T>(context: InternalContext<T>, info: GraphQLResolveInfo) { if (!info.path.prev) { return null; } const paths = getParentPaths(info.path.prev); const spanCache = context[spanCacheSymbol]; if (!spanCache) { return null; } for (const path of paths) { if (spanCache[path]) { return spanCache[path]; } } return null; } export function createSpanWithParent<T>( context: object, info: GraphQLResolveInfo, createSpan: (path: string, parent: T | null) => T, ) { const parentSpan = getParentSpan<T>(context, info); const stringPath = pathToString(info); const span = createSpan(stringPath, parentSpan); if (!(context as InternalContext<T>)[spanCacheSymbol]) { (context as InternalContext<T>)[spanCacheSymbol] = {}; } (context as InternalContext<T>)[spanCacheSymbol]![stringPath] = span; return span; } const { performance } = globalThis as unknown as { performance: { now: () => number } }; export function wrapResolver<C>( resolver: GraphQLFieldResolver<unknown, C, {}>, end: (error: unknown, duration: number) => void, ): GraphQLFieldResolver<unknown, C, {}> { return (source, args, ctx, info) => { const start = performance.now(); let result: unknown; try { result = resolver(source, args, ctx, info); } catch (error: unknown) { end(error, performance.now() - start); throw error; } if (isThenable(result)) { return result.then( (value) => { end(null, performance.now() - start); return value; }, (error: Error) => { end(error, performance.now() - start); throw error; }, ); } end(null, performance.now() - start); return result; }; } export function runFunction<T>(next: () => T, end: (error: unknown, duration: number) => void) { const start = performance.now(); let result: unknown; try { result = next(); } catch (error: unknown) { end(error, performance.now() - start); throw error; } if (isThenable(result)) { return result.then( (value) => { end(null, performance.now() - start); return value; }, (error: Error) => { end(error, performance.now() - start); throw error; }, ); } end(null, performance.now() - start); return result; }