Skip to content

Commit 4b9e3ad

Browse files
feat(instrumentation-graphql): Add option to put all resolve spans under the same parent span (#3085)
Co-authored-by: David Luna <[email protected]>
1 parent 06c503c commit 4b9e3ad

File tree

5 files changed

+115
-48
lines changed

5 files changed

+115
-48
lines changed

packages/instrumentation-graphql/README.md

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -52,14 +52,15 @@ registerInstrumentations({
5252

5353
## Optional Parameters
5454

55-
| Param | type | Default Value | Description |
56-
|:-----------:|:-------:|:-------------:|:---------------------------------------------------------------------------------------------------------------------------------------------------------:|
57-
| mergeItems | boolean | false | Whether to merge list items into a single element. example: `users.*.name` instead of `users.0.name`, `users.1.name` |
58-
| depth | number | -1 | The maximum depth of fields/resolvers to instrument. When set to 0 it will not instrument fields and resolvers. When set to -1 it will instrument all fields and resolvers. |
59-
| allowValues | boolean | false | When set to true it will not remove attributes values from schema source. By default all values that can be sensitive are removed and replaced with "*" |
60-
| ignoreTrivialResolveSpans | boolean | false | Don't create spans for the execution of the default resolver on object properties. |
61-
| ignoreResolveSpans | boolean | false | Don't create spans for resolvers, regardless if they are trivial or not. |
62-
| responseHook | GraphQLInstrumentationExecutionResponseHook | undefined | Hook that allows adding custom span attributes based on the data returned from "execute" GraphQL action. |
55+
| Param | type | Default Value | Description |
56+
|:-------------------------:|:-------------------------------------------:|:-------------:|:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------:|
57+
| mergeItems | boolean | false | Whether to merge list items into a single element. example: `users.*.name` instead of `users.0.name`, `users.1.name` |
58+
| depth | number | -1 | The maximum depth of fields/resolvers to instrument. When set to 0 it will not instrument fields and resolvers. When set to -1 it will instrument all fields and resolvers. |
59+
| allowValues | boolean | false | When set to true it will not remove attributes values from schema source. By default all values that can be sensitive are removed and replaced with "*" |
60+
| ignoreTrivialResolveSpans | boolean | false | Don't create spans for the execution of the default resolver on object properties. |
61+
| ignoreResolveSpans | boolean | false | Don't create spans for resolvers, regardless if they are trivial or not. |
62+
| flatResolveSpans | boolean | false | Place all resolve spans under the same parent instead of producing a nested tree structure. |
63+
| responseHook | GraphQLInstrumentationExecutionResponseHook | undefined | Hook that allows adding custom span attributes based on the data returned from "execute" GraphQL action. |
6364

6465
## Verbosity
6566

packages/instrumentation-graphql/src/internal-types.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,9 +80,7 @@ export type validateType = (
8080
) => ReadonlyArray<graphqlTypes.GraphQLError>;
8181

8282
export interface GraphQLField {
83-
parent: api.Span;
8483
span: api.Span;
85-
error: Error | null;
8684
}
8785

8886
interface OtelGraphQLData {

packages/instrumentation-graphql/src/types.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,13 @@ export interface GraphQLInstrumentationConfig extends InstrumentationConfig {
5858
*/
5959
ignoreTrivialResolveSpans?: boolean;
6060

61+
/**
62+
* Place all resolve spans under the same parent instead of producing a nested tree structure.
63+
*
64+
* @default false
65+
*/
66+
flatResolveSpans?: boolean;
67+
6168
/**
6269
* Whether to merge list items into a single element.
6370
*

packages/instrumentation-graphql/src/utils.ts

Lines changed: 39 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -82,34 +82,33 @@ function createFieldIfNotExists(
8282
info: graphqlTypes.GraphQLResolveInfo,
8383
path: string[]
8484
): {
85-
field: any;
85+
field: GraphQLField;
8686
spanAdded: boolean;
8787
} {
8888
let field = getField(contextValue, path);
89+
if (field) {
90+
return { field, spanAdded: false };
91+
}
8992

90-
let spanAdded = false;
91-
92-
if (!field) {
93-
spanAdded = true;
94-
const parent = getParentField(contextValue, path);
95-
96-
field = {
97-
parent,
98-
span: createResolverSpan(
99-
tracer,
100-
getConfig,
101-
contextValue,
102-
info,
103-
path,
104-
parent.span
105-
),
106-
error: null,
107-
};
93+
const config = getConfig();
94+
const parentSpan = config.flatResolveSpans
95+
? getRootSpan(contextValue)
96+
: getParentFieldSpan(contextValue, path);
97+
98+
field = {
99+
span: createResolverSpan(
100+
tracer,
101+
getConfig,
102+
contextValue,
103+
info,
104+
path,
105+
parentSpan
106+
),
107+
};
108108

109-
addField(contextValue, path, field);
110-
}
109+
addField(contextValue, path, field);
111110

112-
return { spanAdded, field };
111+
return { field, spanAdded: true };
113112
}
114113

115114
function createResolverSpan(
@@ -187,22 +186,24 @@ function addField(contextValue: any, path: string[], field: GraphQLField) {
187186
field);
188187
}
189188

190-
function getField(contextValue: any, path: string[]) {
189+
function getField(contextValue: any, path: string[]): GraphQLField {
191190
return contextValue[OTEL_GRAPHQL_DATA_SYMBOL].fields[path.join('.')];
192191
}
193192

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

198197
if (field) {
199-
return field;
198+
return field.span;
200199
}
201200
}
202201

203-
return {
204-
span: contextValue[OTEL_GRAPHQL_DATA_SYMBOL].span,
205-
};
202+
return getRootSpan(contextValue);
203+
}
204+
205+
function getRootSpan(contextValue: any): api.Span {
206+
return contextValue[OTEL_GRAPHQL_DATA_SYMBOL].span;
206207
}
207208

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

446-
let field: any;
447+
let span: api.Span;
447448
let shouldEndSpan = false;
448449
if (config.depth >= 0 && config.depth < depth) {
449-
field = getParentField(contextValue, path);
450+
span = getParentFieldSpan(contextValue, path);
450451
} else {
451-
const newField = createFieldIfNotExists(
452+
const { field, spanAdded } = createFieldIfNotExists(
452453
tracer,
453454
getConfig,
454455
contextValue,
455456
info,
456457
path
457458
);
458-
field = newField.field;
459-
shouldEndSpan = newField.spanAdded;
459+
span = field.span;
460+
shouldEndSpan = spanAdded;
460461
}
461462

462463
return api.context.with(
463-
api.trace.setSpan(api.context.active(), field.span),
464+
api.trace.setSpan(api.context.active(), span),
464465
() => {
465466
try {
466467
const res = fieldResolver.call(
@@ -473,20 +474,20 @@ export function wrapFieldResolver<TSource = any, TContext = any, TArgs = any>(
473474
if (isPromise(res)) {
474475
return res.then(
475476
(r: any) => {
476-
handleResolveSpanSuccess(field.span, shouldEndSpan);
477+
handleResolveSpanSuccess(span, shouldEndSpan);
477478
return r;
478479
},
479480
(err: Error) => {
480-
handleResolveSpanError(field.span, err, shouldEndSpan);
481+
handleResolveSpanError(span, err, shouldEndSpan);
481482
throw err;
482483
}
483484
);
484485
} else {
485-
handleResolveSpanSuccess(field.span, shouldEndSpan);
486+
handleResolveSpanSuccess(span, shouldEndSpan);
486487
return res;
487488
}
488489
} catch (err: any) {
489-
handleResolveSpanError(field.span, err, shouldEndSpan);
490+
handleResolveSpanError(span, err, shouldEndSpan);
490491
throw err;
491492
}
492493
}

packages/instrumentation-graphql/test/graphql.test.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -872,6 +872,66 @@ describe('graphql', () => {
872872
});
873873
});
874874

875+
describe('when flatResolveSpans is set to true', () => {
876+
beforeEach(async () => {
877+
create({ flatResolveSpans: true });
878+
});
879+
880+
afterEach(() => {
881+
exporter.reset();
882+
graphQLInstrumentation.disable();
883+
});
884+
885+
it('should create a flat structure for resolver spans', async () => {
886+
await graphql({ schema, source: sourceList1 });
887+
const spans = exporter.getFinishedSpans();
888+
889+
assert.deepStrictEqual(spans.length, 7);
890+
const executeSpan = spans[6];
891+
const resolveSpan = spans[2];
892+
const subResolveSpan1 = spans[3];
893+
const subResolveSpan2 = spans[4];
894+
const subResolveSpan3 = spans[5];
895+
896+
const executeSpanId = executeSpan.spanContext().spanId;
897+
assertResolveSpan(
898+
resolveSpan,
899+
'books',
900+
'books',
901+
'[Book]',
902+
'books {\n name\n }',
903+
executeSpanId
904+
);
905+
906+
assertResolveSpan(
907+
subResolveSpan1,
908+
'name',
909+
'books.0.name',
910+
'String',
911+
'name',
912+
executeSpanId
913+
);
914+
915+
assertResolveSpan(
916+
subResolveSpan2,
917+
'name',
918+
'books.1.name',
919+
'String',
920+
'name',
921+
executeSpanId
922+
);
923+
924+
assertResolveSpan(
925+
subResolveSpan3,
926+
'name',
927+
'books.2.name',
928+
'String',
929+
'name',
930+
executeSpanId
931+
);
932+
});
933+
});
934+
875935
describe('when allowValues is set to true', () => {
876936
describe('AND source is query with param', () => {
877937
let spans: ReadableSpan[];

0 commit comments

Comments
 (0)