Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: graphql responseHook support #508

Merged
merged 20 commits into from
Jun 8, 2021
Merged
Show file tree
Hide file tree
Changes from 2 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
Original file line number Diff line number Diff line change
Expand Up @@ -229,15 +229,55 @@ export class GraphQLInstrumentation extends InstrumentationBase {
processedArgs,
]);
},
err => {
endSpan(span, err);
(err, result) => {
instrumentation.handleExecutionResult(span, err, result);
}
);
});
};
};
}

private handleExecutionResult(
span: api.Span,
err: Error | undefined,
result: PromiseOrValue<graphqlTypes.ExecutionResult> | undefined
) {
const config = this._getConfig();
if (config.responseHook === undefined || result === undefined || err) {
endSpan(span, err);
return;
}

if (result.constructor.name === 'Promise') {
(result as Promise<graphqlTypes.ExecutionResult>).then(resultData => {
this.executeResponseHook(span, resultData);
});
} else {
this.executeResponseHook(span, result as graphqlTypes.ExecutionResult);
}
}

private executeResponseHook(
span: api.Span,
result: graphqlTypes.ExecutionResult
) {
const config = this._getConfig();
safeExecuteInTheMiddle(
() => {
config.responseHook(span, result);
},
err => {
if (err) {
api.diag.error('Error running response hook', err);
}

endSpan(span, undefined);
},
true
);
}

private _patchParse(): (original: parseType) => parseType {
const instrumentation = this;
return function parse(original) {
Expand Down
12 changes: 12 additions & 0 deletions plugins/node/opentelemetry-instrumentation-graphql/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ import { OTEL_GRAPHQL_DATA_SYMBOL, OTEL_PATCHED_SYMBOL } from './symbols';
export const OPERATION_NOT_SUPPORTED =
'Operation$operationName$not' + ' supported';

export interface GraphQLInstrumentationExecutionResponseHook {
(span: api.Span, data: graphqlTypes.ExecutionResult): void;
}

export interface GraphQLInstrumentationConfig extends InstrumentationConfig {
/**
* When set to true it will not remove attributes values from schema source.
Expand All @@ -53,6 +57,14 @@ export interface GraphQLInstrumentationConfig extends InstrumentationConfig {
* @default false
*/
mergeItems?: boolean;

/**
* Hook that allows adding custom span attributes based on the data
* returned from "execute" GraphQL actions.
*
* @default undefined
*/
responseHook?: GraphQLInstrumentationExecutionResponseHook;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@ import {
ReadableSpan,
SimpleSpanProcessor,
} from '@opentelemetry/tracing';
import { Span } from '@opentelemetry/api';
import * as assert from 'assert';
import type * as graphqlTypes from 'graphql';
import { GraphQLInstrumentation } from '../src';
import { SpanNames } from '../src/enum';
import { AttributeNames } from '../src/enums/AttributeNames';
Expand Down Expand Up @@ -971,6 +973,57 @@ describe('graphql', () => {
});
});

describe('when specifying a responseHook configuration', () => {
let spans: ReadableSpan[];
let graphqlResult: graphqlTypes.ExecutionResult;
const dataAttributeName = 'graphql_data';

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

describe('AND valid responseHook', () => {
beforeEach(async () => {
create({
responseHook: (span: Span, data: graphqlTypes.ExecutionResult) => {
span.setAttribute(dataAttributeName, JSON.stringify(data));
},
});
graphqlResult = await graphql(schema, sourceList1);
spans = exporter.getFinishedSpans();
});

it('should attach response hook data to the resulting spans', () => {
const querySpan = spans.find(
span => span.attributes['graphql.operation.name'] == 'query'
);
const instrumentationResult = querySpan?.attributes[dataAttributeName];
assert.deepStrictEqual(
instrumentationResult,
JSON.stringify(graphqlResult)
);
});
});

describe('AND invalid responseHook', () => {
beforeEach(async () => {
create({
responseHook: (_span: Span, _data: graphqlTypes.ExecutionResult) => {
throw 'some kind of failure!';
},
});
graphqlResult = await graphql(schema, sourceList1);
spans = exporter.getFinishedSpans();
});

it('should not do any harm when throwing an exception', () => {
assert.deepStrictEqual(graphqlResult.data?.books?.length, 13);
});
});
});

describe('when query operation is not supported', () => {
let spans: ReadableSpan[];

Expand Down