Skip to content

Commit 1d0e4b4

Browse files
authored
feat(opentelemetry): add background spans (schema + init) (#1071)
1 parent 23b332d commit 1d0e4b4

File tree

7 files changed

+206
-75
lines changed

7 files changed

+206
-75
lines changed

e2e/cloudflare-workers/cloudflare-workers.e2e.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ describe.skipIf(gatewayRunner !== 'node')('Cloudflare Workers', () => {
135135
`);
136136

137137
const traces = await getJaegerTraces(serviceName, 2);
138-
expect(traces.data.length).toBe(3);
138+
expect(traces.data.length).toBe(2);
139139
const relevantTraces = traces.data.filter((trace) =>
140140
trace.spans.some((span) => span.operationName === 'POST /graphql'),
141141
);
@@ -234,7 +234,7 @@ describe.skipIf(gatewayRunner !== 'node')('Cloudflare Workers', () => {
234234
);
235235

236236
const traces = await getJaegerTraces(serviceName, 3);
237-
expect(traces.data.length).toBe(4);
237+
expect(traces.data.length).toBe(3);
238238

239239
const relevantTraces = traces.data.filter((trace) =>
240240
trace.spans.some((span) => span.operationName === 'POST /graphql'),

packages/fusion-runtime/src/unifiedGraphManager.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,15 @@ export type Instrumentation = {
117117
payload: { executionRequest: ExecutionRequest; subgraphName: string },
118118
wrapped: () => MaybePromise<void>,
119119
) => MaybePromise<void>;
120+
/**
121+
* Wrap each supergraph schema loading.
122+
*
123+
* Note: this span is only available when an Async compatible context manager is available
124+
*/
125+
schema?: (
126+
payload: null,
127+
wrapped: () => MaybePromise<void>,
128+
) => MaybePromise<void>;
120129
};
121130

122131
const UNIFIEDGRAPH_CACHE_KEY = 'hive-gateway:supergraph';

packages/plugins/opentelemetry/src/plugin.ts

Lines changed: 113 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ import {
5454
createGraphQLSpan,
5555
createGraphQLValidateSpan,
5656
createHttpSpan,
57+
createSchemaLoadingSpan,
5758
startSubgraphExecuteFetchSpan as createSubgraphExecuteFetchSpan,
5859
createUpstreamHttpFetchSpan,
5960
recordCacheError,
@@ -67,11 +68,15 @@ import {
6768
setGraphQLValidateAttributes,
6869
setParamsAttributes,
6970
setResponseAttributes,
71+
setSchemaAttributes,
7072
setUpstreamFetchAttributes,
7173
setUpstreamFetchResponseAttributes,
7274
} from './spans';
7375
import { getEnvVar, tryContextManagerSetup } from './utils';
7476

77+
const initializationTime =
78+
'performance' in globalThis ? performance.now() : undefined;
79+
7580
type BooleanOrPredicate<TInput = never> =
7681
| boolean
7782
| ((input: TInput) => boolean);
@@ -177,13 +182,6 @@ export type OpenTelemetryGatewayPluginOptions =
177182
* You may specify a boolean value to enable/disable all spans, or a function to dynamically enable/disable spans based on the input.
178183
*/
179184
spans?: {
180-
/**
181-
* Enable/disable Spans of internal introspection queries in proxy mode (default: true).
182-
*/
183-
introspection?: BooleanOrPredicate<{
184-
executionRequest: ExecutionRequest;
185-
subgraphName: string;
186-
}>;
187185
/**
188186
* Enable/disable HTTP request spans (default: true).
189187
*
@@ -231,6 +229,16 @@ export type OpenTelemetryGatewayPluginOptions =
231229
* Enable/Disable cache related span events (default: true).
232230
*/
233231
cache?: BooleanOrPredicate<{ key: string; action: 'read' | 'write' }>;
232+
/**
233+
* Enable/disable schema loading spans (default: true if context manager available).
234+
*
235+
* Note: This span requires an Async compatible context manager
236+
*/
237+
schema?: boolean;
238+
/**
239+
* Enable/disable initialization span (default: true).
240+
*/
241+
initialization?: boolean;
234242
};
235243
};
236244

@@ -283,16 +291,25 @@ export function useOpenTelemetry(
283291
let provider: WebTracerProvider;
284292

285293
const yogaVersion = createDeferred<string>();
294+
let initSpan: Context | null;
286295

287296
function isParentEnabled(state: State): boolean {
288297
const parentState = getMostSpecificState(state);
289298
return !parentState || !!parentState.otel;
290299
}
291300

292301
function getContext(state?: State): Context {
293-
return useContextManager
294-
? context.active()
295-
: (getMostSpecificState(state)?.otel?.current ?? ROOT_CONTEXT);
302+
const specificState = getMostSpecificState(state)?.otel;
303+
304+
if (initSpan && !specificState) {
305+
return initSpan;
306+
}
307+
308+
if (useContextManager) {
309+
return context.active();
310+
}
311+
312+
return specificState?.current ?? ROOT_CONTEXT;
296313
}
297314

298315
const yogaLogger = createDeferred<YogaLogger>();
@@ -370,12 +387,27 @@ export function useOpenTelemetry(
370387
preparation$ = init().then((contextManager) => {
371388
useContextManager = contextManager;
372389
tracer = options.tracer || trace.getTracer('gateway');
390+
initSpan = trace.setSpan(
391+
context.active(),
392+
tracer.startSpan('gateway.initialization', {
393+
startTime: initializationTime,
394+
}),
395+
);
373396
preparation$ = fakePromise();
374397
return pluginLogger.then((logger) => {
375398
pluginLogger = fakePromise(logger);
376399
logger.debug(
377400
`context manager is ${useContextManager ? 'enabled' : 'disabled'}`,
378401
);
402+
if (!useContextManager) {
403+
if (options.spans?.schema) {
404+
logger.warn(
405+
'Schema loading spans are disabled because no context manager is available',
406+
);
407+
}
408+
options.spans = options.spans ?? {};
409+
options.spans.schema = false;
410+
}
379411
diag.setLogger(
380412
{
381413
error: (message, ...args) =>
@@ -453,23 +485,25 @@ export function useOpenTelemetry(
453485
return wrapped();
454486
}
455487

456-
const ctx = getContext(parentState);
457-
forOperation.otel = new OtelContextStack(
458-
createGraphQLSpan({ tracer, ctx }),
459-
);
460-
461-
if (useContextManager) {
462-
wrapped = context.bind(forOperation.otel.current, wrapped);
463-
}
464-
465488
return unfakePromise(
466-
fakePromise()
467-
.then(wrapped)
468-
.catch((err) => {
469-
registerException(forOperation.otel?.current, err);
470-
throw err;
471-
})
472-
.finally(() => trace.getSpan(forOperation.otel!.current)?.end()),
489+
preparation$.then(() => {
490+
const ctx = getContext(parentState);
491+
forOperation.otel = new OtelContextStack(
492+
createGraphQLSpan({ tracer, ctx }),
493+
);
494+
495+
if (useContextManager) {
496+
wrapped = context.bind(forOperation.otel.current, wrapped);
497+
}
498+
499+
return fakePromise()
500+
.then(wrapped)
501+
.catch((err) => {
502+
registerException(forOperation.otel?.current, err);
503+
throw err;
504+
})
505+
.finally(() => trace.getSpan(forOperation.otel!.current)?.end());
506+
}),
473507
);
474508
},
475509

@@ -610,7 +644,7 @@ export function useOpenTelemetry(
610644
parentState.forOperation?.skipExecuteSpan ||
611645
!shouldTrace(
612646
isIntrospection
613-
? options.spans?.introspection
647+
? options.spans?.schema
614648
: options.spans?.subgraphExecute,
615649
{
616650
subgraphName,
@@ -625,7 +659,7 @@ export function useOpenTelemetry(
625659
// (such as Introspection requests in proxy mode), we don't want to use the active context,
626660
// we want the span to be in it's own trace.
627661
const parentContext = isIntrospection
628-
? ROOT_CONTEXT
662+
? context.active()
629663
: getContext(parentState);
630664

631665
forSubgraphExecution.otel = new OtelContextStack(
@@ -671,29 +705,51 @@ export function useOpenTelemetry(
671705
return wrapped();
672706
}
673707

674-
const { forSubgraphExecution } = state;
675-
const ctx = createUpstreamHttpFetchSpan({
676-
ctx: getContext(state),
677-
tracer,
678-
});
679-
680-
forSubgraphExecution?.otel!.push(ctx);
708+
return unfakePromise(
709+
preparation$.then(() => {
710+
const { forSubgraphExecution } = state;
711+
const ctx = createUpstreamHttpFetchSpan({
712+
ctx: getContext(state),
713+
tracer,
714+
});
715+
716+
forSubgraphExecution?.otel!.push(ctx);
717+
718+
if (useContextManager) {
719+
wrapped = context.bind(ctx, wrapped);
720+
}
721+
722+
return fakePromise()
723+
.then(wrapped)
724+
.catch((err) => {
725+
registerException(ctx, err);
726+
throw err;
727+
})
728+
.finally(() => {
729+
trace.getSpan(ctx)?.end();
730+
forSubgraphExecution?.otel!.pop();
731+
});
732+
}),
733+
);
734+
},
681735

682-
if (useContextManager) {
683-
wrapped = context.bind(ctx, wrapped);
736+
schema(_, wrapped) {
737+
if (!shouldTrace(options.spans?.schema, null)) {
738+
return wrapped();
684739
}
685740

686741
return unfakePromise(
687-
fakePromise()
688-
.then(wrapped)
689-
.catch((err) => {
690-
registerException(ctx, err);
691-
throw err;
692-
})
693-
.finally(() => {
694-
trace.getSpan(ctx)?.end();
695-
forSubgraphExecution?.otel!.pop();
696-
}),
742+
preparation$.then(() => {
743+
const ctx = createSchemaLoadingSpan({ tracer });
744+
return fakePromise()
745+
.then(() => context.with(ctx, wrapped))
746+
.catch((err) => {
747+
trace.getSpan(ctx)?.recordException(err);
748+
})
749+
.finally(() => {
750+
trace.getSpan(ctx)?.end();
751+
});
752+
}),
697753
);
698754
},
699755
},
@@ -863,6 +919,16 @@ export function useOpenTelemetry(
863919
setUpstreamFetchResponseAttributes({ ctx, response });
864920
};
865921
},
922+
923+
onSchemaChange(payload) {
924+
setSchemaAttributes(payload);
925+
926+
if (initSpan) {
927+
trace.getSpan(initSpan)?.end();
928+
initSpan = null;
929+
}
930+
},
931+
866932
async onDispose() {
867933
if (options.initializeNodeSDK) {
868934
await provider?.forceFlush?.();

packages/plugins/opentelemetry/src/spans.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
type ExecutionResult,
88
} from '@graphql-tools/utils';
99
import {
10+
ROOT_CONTEXT,
1011
SpanKind,
1112
SpanStatusCode,
1213
trace,
@@ -18,7 +19,7 @@ import {
1819
SEMATTRS_EXCEPTION_STACKTRACE,
1920
SEMATTRS_EXCEPTION_TYPE,
2021
} from '@opentelemetry/semantic-conventions';
21-
import type { ExecutionArgs } from 'graphql';
22+
import { printSchema, type ExecutionArgs, type GraphQLSchema } from 'graphql';
2223
import type { GraphQLParams } from 'graphql-yoga';
2324
import {
2425
getRetryInfo,
@@ -435,6 +436,24 @@ export function setExecutionResultAttributes(input: {
435436
}
436437
}
437438

439+
export function createSchemaLoadingSpan(inputs: { tracer: Tracer }) {
440+
const span = inputs.tracer.startSpan(
441+
'gateway.schema',
442+
{ attributes: { 'gateway.schema.changed': false } },
443+
ROOT_CONTEXT,
444+
);
445+
return trace.setSpan(ROOT_CONTEXT, span);
446+
}
447+
448+
export function setSchemaAttributes(inputs: { schema: GraphQLSchema }) {
449+
const span = trace.getActiveSpan();
450+
if (!span) {
451+
return;
452+
}
453+
span.setAttribute('gateway.schema.changed', true);
454+
span.setAttribute('graphql.schema', printSchema(inputs.schema));
455+
}
456+
438457
export function registerException(ctx: Context | undefined, error: any) {
439458
const span = ctx && trace.getSpan(ctx);
440459
if (!span) {

0 commit comments

Comments
 (0)