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 14 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -229,15 +229,55 @@ export class GraphQLInstrumentation extends InstrumentationBase {
processedArgs,
]);
},
err => {
endSpan(span, err);
(err, result) => {
instrumentation.handleExecutionResult(span, err, result);
nozik marked this conversation as resolved.
Show resolved Hide resolved
}
);
});
};
};
}

private handleExecutionResult(
nozik marked this conversation as resolved.
Show resolved Hide resolved
span: api.Span,
err: Error | undefined,
nozik marked this conversation as resolved.
Show resolved Hide resolved
result: PromiseOrValue<graphqlTypes.ExecutionResult> | undefined
nozik marked this conversation as resolved.
Show resolved Hide resolved
) {
const config = this._getConfig();
if (config.responseHook === undefined || result === undefined || err) {
nozik marked this conversation as resolved.
Show resolved Hide resolved
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(
nozik marked this conversation as resolved.
Show resolved Hide resolved
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
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.
nozik marked this conversation as resolved.
Show resolved Hide resolved
*
* @default undefined
*/
responseHook?: GraphQLInstrumentationExecutionResponseHook;
}

/**
Expand Down
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', () => {
nozik marked this conversation as resolved.
Show resolved Hide resolved
let spans: ReadableSpan[];
let graphqlResult: graphqlTypes.ExecutionResult;
const dataAttributeName = 'graphql_data';

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

describe('AND valid responseHook', () => {
nozik marked this conversation as resolved.
Show resolved Hide resolved
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', () => {
nozik marked this conversation as resolved.
Show resolved Hide resolved
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', () => {
nozik marked this conversation as resolved.
Show resolved Hide resolved
assert.deepStrictEqual(graphqlResult.data?.books?.length, 13);
});
});
});

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

Expand Down