Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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