Skip to content

Commit

Permalink
Merge branch 'main' into feat/knex-instrumentation
Browse files Browse the repository at this point in the history
  • Loading branch information
dyladan authored Jun 8, 2021
2 parents cdbd4e2 + c7df125 commit 7ace5d8
Show file tree
Hide file tree
Showing 7 changed files with 275 additions and 46 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -229,15 +229,59 @@ export class GraphQLInstrumentation extends InstrumentationBase {
processedArgs,
]);
},
err => {
endSpan(span, err);
(err, result) => {
instrumentation._handleExecutionResult(span, err, result);
}
);
});
};
};
}

private _handleExecutionResult(
span: api.Span,
err?: Error,
result?: PromiseOrValue<graphqlTypes.ExecutionResult>
) {
const config = this._getConfig();
if (
typeof config.responseHook !== 'function' ||
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 action.
*
* @default undefined
*/
responseHook?: GraphQLInstrumentationExecutionResponseHook;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,16 @@ 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';
import { GraphQLInstrumentationConfig } from '../src/types';
import {
GraphQLInstrumentationConfig,
GraphQLInstrumentationExecutionResponseHook,
} from '../src/types';
import { assertResolveSpan } from './helper';

const defaultConfig: GraphQLInstrumentationConfig = {};
Expand Down Expand Up @@ -971,6 +976,74 @@ describe('graphql', () => {
});
});

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

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

describe('when responseHook is valid', () => {
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('when responseHook throws an error', () => {
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', () => {
assert.deepStrictEqual(graphqlResult.data?.books?.length, 13);
});
});

describe('when responseHook is not a function', () => {
beforeEach(async () => {
// Cast to unknown so that it's possible to cast to GraphQLInstrumentationExecutionResponseHook later
const invalidTypeHook = 1234 as unknown;
create({
responseHook:
invalidTypeHook as GraphQLInstrumentationExecutionResponseHook,
});
graphqlResult = await graphql(schema, sourceList1);
spans = exporter.getFinishedSpans();
});

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

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

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/*
* Copyright The OpenTelemetry Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

export enum EventNames {
FIRST_PAINT = 'firstPaint',
FIRST_CONTENTFUL_PAINT = 'firstContentfulPaint',
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ import {
addSpanNetworkEvents,
hasKey,
PerformanceEntries,
PerformanceLegacy,
PerformanceTimingNames as PTN,
} from '@opentelemetry/web';
import {
Expand All @@ -37,6 +36,10 @@ import {
import { AttributeNames } from './enums/AttributeNames';
import { VERSION } from './version';
import { SemanticAttributes } from '@opentelemetry/semantic-conventions';
import {
addSpanPerformancePaintEvents,
getPerformanceNavigationEntries,
} from './utils';

/**
* This class represents a document load plugin
Expand Down Expand Up @@ -90,7 +93,7 @@ export class DocumentLoadInstrumentation extends InstrumentationBase<unknown> {
const metaElement = [...document.getElementsByTagName('meta')].find(
e => e.getAttribute('name') === TRACE_PARENT_HEADER
);
const entries = this._getEntries();
const entries = getPerformanceNavigationEntries();
const traceparent = (metaElement && metaElement.content) || '';
context.with(propagation.extract(ROOT_CONTEXT, { traceparent }), () => {
const rootSpan = this._startSpan(
Expand Down Expand Up @@ -137,6 +140,8 @@ export class DocumentLoadInstrumentation extends InstrumentationBase<unknown> {
addSpanNetworkEvent(rootSpan, PTN.LOAD_EVENT_START, entries);
addSpanNetworkEvent(rootSpan, PTN.LOAD_EVENT_END, entries);

addSpanPerformancePaintEvents(rootSpan);

this._endSpan(rootSpan, PTN.LOAD_EVENT_END, entries);
});
}
Expand All @@ -163,44 +168,6 @@ export class DocumentLoadInstrumentation extends InstrumentationBase<unknown> {
}
}

/**
* gets performance entries of navigation
*/
private _getEntries() {
const entries: PerformanceEntries = {};
const performanceNavigationTiming = (
otperformance as unknown as Performance
).getEntriesByType?.('navigation')[0] as PerformanceEntries;

if (performanceNavigationTiming) {
const keys = Object.values(PTN);
keys.forEach((key: string) => {
if (hasKey(performanceNavigationTiming, key)) {
const value = performanceNavigationTiming[key];
if (typeof value === 'number') {
entries[key] = value;
}
}
});
} else {
// // fallback to previous version
const perf: typeof otperformance & PerformanceLegacy = otperformance;
const performanceTiming = perf.timing;
if (performanceTiming) {
const keys = Object.values(PTN);
keys.forEach((key: string) => {
if (hasKey(performanceTiming, key)) {
const value = performanceTiming[key];
if (typeof value === 'number') {
entries[key] = value;
}
}
});
}
}
return entries;
}

/**
* Creates and ends a span with network information about resource added as timed events
* @param resource
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/*
* Copyright The OpenTelemetry Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { Span } from '@opentelemetry/api';
import { otperformance } from '@opentelemetry/core';
import {
hasKey,
PerformanceEntries,
PerformanceLegacy,
PerformanceTimingNames as PTN,
} from '@opentelemetry/web';
import { EventNames } from './enums/EventNames';

export const getPerformanceNavigationEntries = (): PerformanceEntries => {
const entries: PerformanceEntries = {};
const performanceNavigationTiming = (
otperformance as unknown as Performance
).getEntriesByType?.('navigation')[0] as PerformanceEntries;

if (performanceNavigationTiming) {
const keys = Object.values(PTN);
keys.forEach((key: string) => {
if (hasKey(performanceNavigationTiming, key)) {
const value = performanceNavigationTiming[key];
if (typeof value === 'number') {
entries[key] = value;
}
}
});
} else {
// // fallback to previous version
const perf: typeof otperformance & PerformanceLegacy = otperformance;
const performanceTiming = perf.timing;
if (performanceTiming) {
const keys = Object.values(PTN);
keys.forEach((key: string) => {
if (hasKey(performanceTiming, key)) {
const value = performanceTiming[key];
if (typeof value === 'number') {
entries[key] = value;
}
}
});
}
}

return entries;
};

const performancePaintNames = {
'first-paint': EventNames.FIRST_PAINT,
'first-contentful-paint': EventNames.FIRST_CONTENTFUL_PAINT,
};

export const addSpanPerformancePaintEvents = (span: Span) => {
const performancePaintTiming = (
otperformance as unknown as Performance
).getEntriesByType?.('paint');
if (performancePaintTiming) {
performancePaintTiming.forEach(({ name, startTime }) => {
if (hasKey(performancePaintNames, name)) {
span.addEvent(performancePaintNames[name], startTime);
}
});
}
};
Loading

0 comments on commit 7ace5d8

Please sign in to comment.