From f8ec05c7d0559f5862649b27177964672ace778c Mon Sep 17 00:00:00 2001 From: Vincent Fortin Date: Tue, 3 Jun 2025 11:25:17 -0400 Subject: [PATCH] flat resolve spans --- .../src/internal-types.ts | 2 - packages/instrumentation-graphql/src/types.ts | 7 ++ packages/instrumentation-graphql/src/utils.ts | 77 ++++++++++--------- .../test/graphql.test.ts | 60 +++++++++++++++ 4 files changed, 106 insertions(+), 40 deletions(-) diff --git a/packages/instrumentation-graphql/src/internal-types.ts b/packages/instrumentation-graphql/src/internal-types.ts index 59cd1b5b31..a5e4a2df76 100644 --- a/packages/instrumentation-graphql/src/internal-types.ts +++ b/packages/instrumentation-graphql/src/internal-types.ts @@ -80,9 +80,7 @@ export type validateType = ( ) => ReadonlyArray; export interface GraphQLField { - parent: api.Span; span: api.Span; - error: Error | null; } interface OtelGraphQLData { diff --git a/packages/instrumentation-graphql/src/types.ts b/packages/instrumentation-graphql/src/types.ts index 0ec82003aa..b6cdaf60e4 100644 --- a/packages/instrumentation-graphql/src/types.ts +++ b/packages/instrumentation-graphql/src/types.ts @@ -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. * diff --git a/packages/instrumentation-graphql/src/utils.ts b/packages/instrumentation-graphql/src/utils.ts index fbea7248d6..8302b5adf0 100644 --- a/packages/instrumentation-graphql/src/utils.ts +++ b/packages/instrumentation-graphql/src/utils.ts @@ -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( @@ -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[] { @@ -444,24 +445,24 @@ export function wrapFieldResolver( 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( @@ -474,20 +475,20 @@ export function wrapFieldResolver( 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; } } diff --git a/packages/instrumentation-graphql/test/graphql.test.ts b/packages/instrumentation-graphql/test/graphql.test.ts index 9af85810be..75b489d238 100644 --- a/packages/instrumentation-graphql/test/graphql.test.ts +++ b/packages/instrumentation-graphql/test/graphql.test.ts @@ -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[];