Skip to content

Commit

Permalink
fix(gql): conform GraphQL span name to spec
Browse files Browse the repository at this point in the history
Rename graphql "execute" spans to "<operation.type> <operation.name>"
to conform to the spec.

In addition to conforming to the spec, this change can provide
additional value for monitoring purposes. In cases where o11y providers
generate metrics from pre-ingested traces (without all the attributes),
the new naming convention makes it easier to group and filter metrics
by operation type and name, since it is now reflected in the span name.
This can be particularly useful for setting up monitors or alerts.

As part of this change, I also renamed the SpanNames enum to
GraphQLQueryStep to better reflect its purpose. While this enum is still
used for query steps other than "execute", I anticipate updating the
naming convention for these steps once the OpenTelemetry specification
is updated to provide guidance on how to name them.
  • Loading branch information
naseemkullah committed Mar 29, 2023
1 parent 9605528 commit fd83454
Show file tree
Hide file tree
Showing 5 changed files with 111 additions and 66 deletions.
47 changes: 42 additions & 5 deletions plugins/node/opentelemetry-instrumentation-graphql/src/enum.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,48 @@ export enum TokenKind {
COMMENT = 'Comment',
}

export enum SpanNames {
EXECUTE = 'graphql.execute',
/**
* Represents the different steps involved in executing a GraphQL query.
*/
export enum GraphQLQueryStep {
/**
* The step where the incoming query string is parsed and converted into an abstract syntax tree (AST).
* The AST represents the structure of the query and can be used to validate the query and generate a query plan.
*/
PARSE = 'graphql.parse',
RESOLVE = 'graphql.resolve',

/**
* The step where the query is validated against the GraphQL schema.
* This involves checking that the query only includes valid fields, arguments, and types,
* and that the query satisfies any constraints or directives defined in the schema.
*/
VALIDATE = 'graphql.validate',
SCHEMA_VALIDATE = 'graphql.validateSchema',
SCHEMA_PARSE = 'graphql.parseSchema',

/**
* The step where the GraphQL schema is parsed into an executable schema object that can be used to execute queries.
* The schema is usually defined in a separate file or module and needs to be parsed before it can be used to execute queries.
*/
PARSE_SCHEMA = 'graphql.parseSchema',

/**
* The step where the parsed schema is validated to ensure that it conforms to the GraphQL specification,
* and that it's consistent with the types and fields defined in the application.
* This step can catch errors such as missing or conflicting types, incorrect field types, or invalid directives.
*/
VALIDATE_SCHEMA = 'graphql.validateSchema',

/**
* The step where the GraphQL server generates a query plan that describes how to execute the query.
* The query plan is a tree-like structure that specifies which fields to fetch, how to fetch them, and in what order.
* The execution engine then traverses the query plan and resolves each field by invoking the appropriate resolver function.
* The result of each resolver is then combined into a JSON response object that matches the shape of the query.
*/
EXECUTE = 'graphql.execute',

/**
* The step where each field in the query is resolved by invoking a resolver function.
* The resolver is responsible for fetching data from the data source, applying any transformations or filters, and returning the result.
* Resolvers can be synchronous or asynchronous, and they can be implemented at the field level or at a higher level in the query plan.
*/
RESOLVE = 'graphql.resolve',
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import {
safeExecuteInTheMiddle,
} from '@opentelemetry/instrumentation';
import type * as graphqlTypes from 'graphql';
import { SpanNames } from './enum';
import { GraphQLQueryStep } from './enum';
import { AttributeNames } from './enums/AttributeNames';
import { OTEL_GRAPHQL_DATA_SYMBOL } from './symbols';

Expand Down Expand Up @@ -333,7 +333,7 @@ export class GraphQLInstrumentation extends InstrumentationBase {
options?: graphqlTypes.ParseOptions
): graphqlTypes.DocumentNode {
const config = this._getConfig();
const span = this.tracer.startSpan(SpanNames.PARSE);
const span = this.tracer.startSpan(GraphQLQueryStep.PARSE);

return context.with(trace.setSpan(context.active(), span), () => {
return safeExecuteInTheMiddle<
Expand All @@ -346,7 +346,7 @@ export class GraphQLInstrumentation extends InstrumentationBase {
if (result) {
const operation = getOperation(result);
if (!operation) {
span.updateName(SpanNames.SCHEMA_PARSE);
span.updateName(GraphQLQueryStep.PARSE_SCHEMA);
} else if (result.loc) {
addSpanSource(span, result.loc, config.allowValues);
}
Expand All @@ -366,7 +366,7 @@ export class GraphQLInstrumentation extends InstrumentationBase {
typeInfo?: graphqlTypes.TypeInfo,
options?: { maxErrors?: number }
): ReadonlyArray<graphqlTypes.GraphQLError> {
const span = this.tracer.startSpan(SpanNames.VALIDATE, {});
const span = this.tracer.startSpan(GraphQLQueryStep.VALIDATE, {});

return context.with(trace.setSpan(context.active(), span), () => {
return safeExecuteInTheMiddle<ReadonlyArray<graphqlTypes.GraphQLError>>(
Expand All @@ -382,7 +382,7 @@ export class GraphQLInstrumentation extends InstrumentationBase {
},
(err, errors) => {
if (!documentAST.loc) {
span.updateName(SpanNames.SCHEMA_VALIDATE);
span.updateName(GraphQLQueryStep.VALIDATE_SCHEMA);
}
if (errors && errors.length) {
span.recordException({
Expand All @@ -402,21 +402,22 @@ export class GraphQLInstrumentation extends InstrumentationBase {
): api.Span {
const config = this._getConfig();

const span = this.tracer.startSpan(SpanNames.EXECUTE, {});
const span = this.tracer.startSpan(GraphQLQueryStep.EXECUTE, {});
if (operation) {
const operationDefinition =
const { operation: operationType, name: nameNode } =
operation as graphqlTypes.OperationDefinitionNode;
span.setAttribute(
AttributeNames.OPERATION_TYPE,
operationDefinition.operation
);
const operationName = nameNode?.value;

if (operationDefinition.name) {
span.setAttribute(
AttributeNames.OPERATION_NAME,
operationDefinition.name.value
);
span.setAttribute(AttributeNames.OPERATION_TYPE, operationType);

if (operationName) {
span.setAttribute(AttributeNames.OPERATION_NAME, operationName);
}

// https://opentelemetry.io/docs/reference/specification/trace/semantic_conventions/instrumentation/graphql/
// > The span name MUST be of the format <graphql.operation.type> <graphql.operation.name> provided that graphql.operation.type and graphql.operation.name are available.
// > If graphql.operation.name is not available, the span SHOULD be named <graphql.operation.type>.
span.updateName(`${operationType} ${operationName ?? ''}`.trim());
} else {
let operationName = ' ';
if (processedArgs.operationName) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

import type * as graphqlTypes from 'graphql';
import * as api from '@opentelemetry/api';
import { AllowedOperationTypes, SpanNames, TokenKind } from './enum';
import { AllowedOperationTypes, GraphQLQueryStep, TokenKind } from './enum';
import { AttributeNames } from './enums/AttributeNames';
import { OTEL_GRAPHQL_DATA_SYMBOL, OTEL_PATCHED_SYMBOL } from './symbols';
import {
Expand Down Expand Up @@ -129,7 +129,7 @@ function createResolverSpan(
};

const span = tracer.startSpan(
SpanNames.RESOLVE,
GraphQLQueryStep.RESOLVE,
{
attributes,
},
Expand Down
Loading

0 comments on commit fd83454

Please sign in to comment.