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