Skip to content
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
2 changes: 0 additions & 2 deletions packages/instrumentation-graphql/src/internal-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,9 +80,7 @@ export type validateType = (
) => ReadonlyArray<graphqlTypes.GraphQLError>;

export interface GraphQLField {
parent: api.Span;
span: api.Span;
error: Error | null;
}

interface OtelGraphQLData {
Expand Down
7 changes: 7 additions & 0 deletions packages/instrumentation-graphql/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,13 @@ export interface GraphQLInstrumentationConfig extends InstrumentationConfig {
*/
ignoreTrivialResolveSpans?: boolean;

/**
* Place all resolve spans under the same parent instead of producing a nested tree structure.
*
* @default false
*/
flatResolveSpans?: boolean;

/**
* Whether to merge list items into a single element.
*
Expand Down
77 changes: 39 additions & 38 deletions packages/instrumentation-graphql/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,34 +83,33 @@ function createFieldIfNotExists(
info: graphqlTypes.GraphQLResolveInfo,
path: string[]
): {
field: any;
field: GraphQLField;
spanAdded: boolean;
} {
let field = getField(contextValue, path);
if (field) {
return { field, spanAdded: false };
}

let spanAdded = false;

if (!field) {
spanAdded = true;
const parent = getParentField(contextValue, path);

field = {
parent,
span: createResolverSpan(
tracer,
getConfig,
contextValue,
info,
path,
parent.span
),
error: null,
};
const config = getConfig();
const parentSpan = config.flatResolveSpans
? getRootSpan(contextValue)
: getParentFieldSpan(contextValue, path);

field = {
span: createResolverSpan(
tracer,
getConfig,
contextValue,
info,
path,
parentSpan
),
};

addField(contextValue, path, field);
}
addField(contextValue, path, field);

return { spanAdded, field };
return { field, spanAdded: true };
}

function createResolverSpan(
Expand Down Expand Up @@ -188,22 +187,24 @@ function addField(contextValue: any, path: string[], field: GraphQLField) {
field);
}

function getField(contextValue: any, path: string[]) {
function getField(contextValue: any, path: string[]): GraphQLField {
return contextValue[OTEL_GRAPHQL_DATA_SYMBOL].fields[path.join('.')];
}

function getParentField(contextValue: any, path: string[]) {
function getParentFieldSpan(contextValue: any, path: string[]): api.Span {
for (let i = path.length - 1; i > 0; i--) {
const field = getField(contextValue, path.slice(0, i));

if (field) {
return field;
return field.span;
}
}

return {
span: contextValue[OTEL_GRAPHQL_DATA_SYMBOL].span,
};
return getRootSpan(contextValue);
}

function getRootSpan(contextValue: any): api.Span {
return contextValue[OTEL_GRAPHQL_DATA_SYMBOL].span;
}

function pathToArray(mergeItems: boolean, path: GraphQLPath): string[] {
Expand Down Expand Up @@ -444,24 +445,24 @@ export function wrapFieldResolver<TSource = any, TContext = any, TArgs = any>(
const path = pathToArray(config.mergeItems, info && info.path);
const depth = path.filter((item: any) => typeof item === 'string').length;

let field: any;
let span: api.Span;
let shouldEndSpan = false;
if (config.depth >= 0 && config.depth < depth) {
field = getParentField(contextValue, path);
span = getParentFieldSpan(contextValue, path);
} else {
const newField = createFieldIfNotExists(
const { field, spanAdded } = createFieldIfNotExists(
tracer,
getConfig,
contextValue,
info,
path
);
field = newField.field;
shouldEndSpan = newField.spanAdded;
span = field.span;
shouldEndSpan = spanAdded;
}

return api.context.with(
api.trace.setSpan(api.context.active(), field.span),
api.trace.setSpan(api.context.active(), span),
() => {
try {
const res = fieldResolver.call(
Expand All @@ -474,20 +475,20 @@ export function wrapFieldResolver<TSource = any, TContext = any, TArgs = any>(
if (isPromise(res)) {
return res.then(
(r: any) => {
handleResolveSpanSuccess(field.span, shouldEndSpan);
handleResolveSpanSuccess(span, shouldEndSpan);
return r;
},
(err: Error) => {
handleResolveSpanError(field.span, err, shouldEndSpan);
handleResolveSpanError(span, err, shouldEndSpan);
throw err;
}
);
} else {
handleResolveSpanSuccess(field.span, shouldEndSpan);
handleResolveSpanSuccess(span, shouldEndSpan);
return res;
}
} catch (err: any) {
handleResolveSpanError(field.span, err, shouldEndSpan);
handleResolveSpanError(span, err, shouldEndSpan);
throw err;
}
}
Expand Down
60 changes: 60 additions & 0 deletions packages/instrumentation-graphql/test/graphql.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -872,6 +872,66 @@ describe('graphql', () => {
});
});

describe('when flatResolveSpans is set to true', () => {
beforeEach(async () => {
create({ flatResolveSpans: true });
});

afterEach(() => {
exporter.reset();
graphQLInstrumentation.disable();
});

it('should create a flat structure for resolver spans', async () => {
await graphql({ schema, source: sourceList1 });
const spans = exporter.getFinishedSpans();

assert.deepStrictEqual(spans.length, 7);
const executeSpan = spans[6];
const resolveSpan = spans[2];
const subResolveSpan1 = spans[3];
const subResolveSpan2 = spans[4];
const subResolveSpan3 = spans[5];

const executeSpanId = executeSpan.spanContext().spanId;
assertResolveSpan(
resolveSpan,
'books',
'books',
'[Book]',
'books {\n name\n }',
executeSpanId
);

assertResolveSpan(
subResolveSpan1,
'name',
'books.0.name',
'String',
'name',
executeSpanId
);

assertResolveSpan(
subResolveSpan2,
'name',
'books.1.name',
'String',
'name',
executeSpanId
);

assertResolveSpan(
subResolveSpan3,
'name',
'books.2.name',
'String',
'name',
executeSpanId
);
});
});

describe('when allowValues is set to true', () => {
describe('AND source is query with param', () => {
let spans: ReadableSpan[];
Expand Down
Loading