Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: graphql-otel trace directive support #1769

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 1 addition & 70 deletions packages/plugins/opentelemetry/README.md
Original file line number Diff line number Diff line change
@@ -1,70 +1 @@
## `@envelop/opentelemetry`

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

You can use this plugin with any kind of Open Telemetry
[tracer](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/api.md#tracer),
and integrate it to any tracing/metric platform that supports this standard.

## Getting Started

```
yarn add @envelop/opentelemetry
```

## Usage Example

By default, this plugin prints the collected telemetry to the console:

```ts
import { execute, parse, specifiedRules, subscribe, validate } from 'graphql'
import { envelop, useEngine } from '@envelop/core'
import { useOpenTelemetry } from '@envelop/opentelemetry'

const getEnveloped = envelop({
plugins: [
useEngine({ parse, validate, specifiedRules, execute, subscribe }),
// ... other plugins ...
useOpenTelemetry({
resolvers: true, // Tracks resolvers calls, and tracks resolvers thrown errors
variables: true, // Includes the operation variables values as part of the metadata collected
result: true // Includes execution result object as part of the metadata collected
})
]
})
```

If you wish to use custom tracer/exporter, create it and pass it. This example integrates Jaeger
tracer:

```ts
import { execute, parse, specifiedRules, subscribe, validate } from 'graphql'
import { envelop, useEngine } from '@envelop/core'
import { useOpenTelemetry } from '@envelop/opentelemetry'
import { JaegerExporter } from '@opentelemetry/exporter-jaeger'
import { BasicTracerProvider, SimpleSpanProcessor } from '@opentelemetry/sdk-trace-base'

const exporter = new JaegerExporter({
serviceName: 'my-service-name'
})

const provider = new BasicTracerProvider()
provider.addSpanProcessor(new SimpleSpanProcessor(exporter))
provider.register()

const getEnveloped = envelop({
plugins: [
useEngine({ parse, validate, specifiedRules, execute, subscribe }),
// ... other plugins ...
useOpenTelemetry(
{
resolvers: true, // Tracks resolvers calls, and tracks resolvers thrown errors
variables: true, // Includes the operation variables values as part of the metadata collected
result: true // Includes execution result object as part of the metadata collected
},
provider
)
]
})
```
TODO
12 changes: 10 additions & 2 deletions packages/plugins/opentelemetry/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,20 @@
},
"dependencies": {
"@envelop/on-resolve": "^2.0.6",
"@opentelemetry/api": "^1.0.0",
"@opentelemetry/sdk-trace-base": "^1.11.0",
"@opentelemetry/core": "^1.7.0",
"graphql-otel": "^0.0.9",
"tslib": "^2.5.0"
},
"devDependencies": {
"@envelop/core": "^3.0.6",
"@graphql-tools/schema": "8.5.1",
"@opentelemetry/api": "^1.2.0",
"@opentelemetry/context-async-hooks": "^1.7.0",
"@opentelemetry/instrumentation": "^0.33.0",
"@opentelemetry/otlp-exporter-base": "^0.33.0",
"@opentelemetry/resources": "^1.7.0",
"@opentelemetry/sdk-trace-base": "^1.7.0",
"@opentelemetry/semantic-conventions": "^1.12.0",
"graphql": "16.6.0",
"typescript": "4.8.4"
},
Expand Down
143 changes: 17 additions & 126 deletions packages/plugins/opentelemetry/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,143 +1,34 @@
import { print } from 'graphql';
import { isAsyncIterable, OnExecuteHookResult, Plugin } from '@envelop/core';
import { useOnResolve } from '@envelop/on-resolve';
import { SpanAttributes, SpanKind, TracerProvider } from '@opentelemetry/api';
import { GraphQLOTELContext, traceDirective } from 'graphql-otel';
import { Plugin } from '@envelop/core';
import * as opentelemetry from '@opentelemetry/api';
import {
BasicTracerProvider,
ConsoleSpanExporter,
SimpleSpanProcessor,
} from '@opentelemetry/sdk-trace-base';

export enum AttributeName {
EXECUTION_ERROR = 'graphql.execute.error',
EXECUTION_RESULT = 'graphql.execute.result',
RESOLVER_EXCEPTION = 'graphql.resolver.exception',
RESOLVER_FIELD_NAME = 'graphql.resolver.fieldName',
RESOLVER_TYPE_NAME = 'graphql.resolver.typeName',
RESOLVER_RESULT_TYPE = 'graphql.resolver.resultType',
RESOLVER_ARGS = 'graphql.resolver.args',
EXECUTION_OPERATION_NAME = 'graphql.execute.operationName',
EXECUTION_OPERATION_DOCUMENT = 'graphql.execute.document',
EXECUTION_VARIABLES = 'graphql.execute.variables',
}

const tracingSpanSymbol = Symbol('OPEN_TELEMETRY_GRAPHQL');

export type TracingOptions = {
resolvers: boolean;
variables: boolean;
result: boolean;
};

type PluginContext = {
[tracingSpanSymbol]: opentelemetry.Span;
};

export const useOpenTelemetry = (
options: TracingOptions,
tracingProvider?: TracerProvider,
spanKind: SpanKind = SpanKind.SERVER,
spanAdditionalAttributes: SpanAttributes = {},
serviceName = 'graphql',
): Plugin<PluginContext> => {
if (!tracingProvider) {
const basicTraceProvider = new BasicTracerProvider();
basicTraceProvider.addSpanProcessor(new SimpleSpanProcessor(new ConsoleSpanExporter()));
basicTraceProvider.register();
tracingProvider = basicTraceProvider;
}

const tracer = tracingProvider.getTracer(serviceName);
const graphqlMiddlewareAppliedTransformSymbol = Symbol('graphqlMiddleware.appliedTransform');

export const useOpenTelemetry = (): Plugin<PluginContext> => {
return {
onPluginInit({ addPlugin }) {
if (options.resolvers) {
addPlugin(
useOnResolve(({ info, context, args }) => {
if (context && typeof context === 'object' && context[tracingSpanSymbol]) {
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 ?? {}) }
: {}),
},
onContextBuilding({ extendContext }) {
extendContext({
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore - dafuck
GraphQLOTELContext: new GraphQLOTELContext(),
});
},
onSchemaChange({ schema, replaceSchema }) {
if (schema.extensions?.[graphqlMiddlewareAppliedTransformSymbol]) {
return;
}

const resultCbs: OnExecuteHookResult<PluginContext> = {
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();
},
};
const directive = traceDirective('trace');

if (options.resolvers) {
extendContext({
[tracingSpanSymbol]: executionSpan,
});
}
const transformedSchema = directive.transformer(schema);

return resultCbs;
replaceSchema(transformedSchema);
},
};
};
Loading