UNPKG

@envelop/opentelemetry

Version:

This plugins integrates [Open Telemetry](https://opentelemetry.io/) tracing with your GraphQL execution. It also collects GraphQL execution errors and reports it as Exceptions.

101 lines (100 loc) 5.23 kB
import { isAsyncIterable } from '@envelop/core'; import { useOnResolve } from '@envelop/on-resolve'; import { SpanKind } from '@opentelemetry/api'; import * as opentelemetry from '@opentelemetry/api'; import { BasicTracerProvider, ConsoleSpanExporter, SimpleSpanProcessor } from '@opentelemetry/tracing'; import { print } from 'graphql'; export var AttributeName; (function (AttributeName) { AttributeName["EXECUTION_ERROR"] = "graphql.execute.error"; AttributeName["EXECUTION_RESULT"] = "graphql.execute.result"; AttributeName["RESOLVER_EXCEPTION"] = "graphql.resolver.exception"; AttributeName["RESOLVER_FIELD_NAME"] = "graphql.resolver.fieldName"; AttributeName["RESOLVER_TYPE_NAME"] = "graphql.resolver.typeName"; AttributeName["RESOLVER_RESULT_TYPE"] = "graphql.resolver.resultType"; AttributeName["RESOLVER_ARGS"] = "graphql.resolver.args"; AttributeName["EXECUTION_OPERATION_NAME"] = "graphql.execute.operationName"; AttributeName["EXECUTION_OPERATION_DOCUMENT"] = "graphql.execute.document"; AttributeName["EXECUTION_VARIABLES"] = "graphql.execute.variables"; })(AttributeName || (AttributeName = {})); const tracingSpanSymbol = Symbol('OPEN_TELEMETRY_GRAPHQL'); export const useOpenTelemetry = (options, tracingProvider, spanKind = SpanKind.SERVER, spanAdditionalAttributes = {}, serviceName = 'graphql') => { if (!tracingProvider) { tracingProvider = new BasicTracerProvider(); tracingProvider.addSpanProcessor(new SimpleSpanProcessor(new ConsoleSpanExporter())); tracingProvider.register(); } const tracer = tracingProvider.getTracer(serviceName); return { onPluginInit({ addPlugin }) { if (options.resolvers) { addPlugin(useOnResolve(({ info, context, args }) => { if (context && typeof context === 'object' && context[tracingSpanSymbol]) { tracer.getActiveSpanProcessor(); const ctx = opentelemetry.trace.setSpan(opentelemetry.context.active(), context[tracingSpanSymbol]); const { fieldName, returnType, parentType } = info; const resolverSpan = tracer.startSpan(`${parentType.name}.${fieldName}`, { attributes: { [AttributeName.RESOLVER_FIELD_NAME]: fieldName, [AttributeName.RESOLVER_TYPE_NAME]: parentType.toString(), [AttributeName.RESOLVER_RESULT_TYPE]: returnType.toString(), [AttributeName.RESOLVER_ARGS]: JSON.stringify(args || {}), }, }, ctx); return ({ result }) => { if (result instanceof Error) { resolverSpan.recordException({ name: AttributeName.RESOLVER_EXCEPTION, message: JSON.stringify(result), }); } else { resolverSpan.end(); } }; } return () => { }; })); } }, onExecute({ args, extendContext }) { const executionSpan = tracer.startSpan(`${args.operationName || 'Anonymous Operation'}`, { kind: spanKind, attributes: { ...spanAdditionalAttributes, [AttributeName.EXECUTION_OPERATION_NAME]: args.operationName ?? undefined, [AttributeName.EXECUTION_OPERATION_DOCUMENT]: print(args.document), ...(options.variables ? { [AttributeName.EXECUTION_VARIABLES]: JSON.stringify(args.variableValues ?? {}) } : {}), }, }); const resultCbs = { onExecuteDone({ result }) { if (isAsyncIterable(result)) { executionSpan.end(); // eslint-disable-next-line no-console console.warn(`Plugin "newrelic" encountered a AsyncIterator which is not supported yet, so tracing data is not available for the operation.`); return; } if (result.data && options.result) { executionSpan.setAttribute(AttributeName.EXECUTION_RESULT, JSON.stringify(result)); } if (result.errors && result.errors.length > 0) { executionSpan.recordException({ name: AttributeName.EXECUTION_ERROR, message: JSON.stringify(result.errors), }); } executionSpan.end(); }, }; if (options.resolvers) { extendContext({ [tracingSpanSymbol]: executionSpan, }); } return resultCbs; }, }; };