Skip to content

Commit d44e0ca

Browse files
feat(opentelemetry): Add getters for root spans contexts (#1360)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
1 parent b433105 commit d44e0ca

18 files changed

+275
-136
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'@graphql-hive/gateway-runtime': patch
3+
---
4+
5+
dependencies updates:
6+
7+
- Added dependency [`@opentelemetry/api@^1.9.0` ↗︎](https://www.npmjs.com/package/@opentelemetry/api/v/1.9.0) (to `dependencies`)
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'@graphql-mesh/plugin-opentelemetry': patch
3+
---
4+
5+
dependencies updates:
6+
7+
- Updated dependency [`@opentelemetry/auto-instrumentations-node@^0.62.1` ↗︎](https://www.npmjs.com/package/@opentelemetry/auto-instrumentations-node/v/0.62.1) (from `^0.62.0`, in `dependencies`)

.yarn/patches/@opentelemetry-otlp-exporter-base-npm-0.202.0-f6f29c2eeb.patch renamed to .yarn/patches/@opentelemetry-otlp-exporter-base-npm-0.203.0-183dcac0e6.patch

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
diff --git a/build/esnext/transport/http-exporter-transport.js b/build/esnext/transport/http-exporter-transport.js
2-
index 7977489487a2236fbd0e4c2273ef53fd3c7b93a8..93e9701d10f6aea530f38330daea87199e514452 100644
2+
index e63fda75feb67686bad9b692a046ebb1af2bd8a0..93e9701d10f6aea530f38330daea87199e514452 100644
33
--- a/build/esnext/transport/http-exporter-transport.js
44
+++ b/build/esnext/transport/http-exporter-transport.js
55
@@ -20,7 +20,7 @@ class HttpExporterTransport {
@@ -21,7 +21,7 @@ index 7977489487a2236fbd0e4c2273ef53fd3c7b93a8..93e9701d10f6aea530f38330daea8719
2121
if (utils === null) {
2222
// Lazy require to ensure that http/https is not required before instrumentations can wrap it.
2323
- const { sendWithHttp, createHttpAgent,
24-
- // eslint-disable-next-line @typescript-eslint/no-var-requires
24+
- // eslint-disable-next-line @typescript-eslint/no-require-imports
2525
- } = require('./http-transport-utils');
2626
+ const { sendWithHttp, createHttpAgent } = await import('./http-transport-utils');
2727
utils = this._utils = {

e2e/opentelemetry/opentelemetry.e2e.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ type JaegerTraceSpan = {
6060
};
6161

6262
describe('OpenTelemetry', () => {
63-
(['grpc', 'http'] as const).forEach((OTLP_EXPORTER_TYPE) => {
63+
(['http'] as const).forEach((OTLP_EXPORTER_TYPE) => {
6464
describe(`exporter > ${OTLP_EXPORTER_TYPE}`, () => {
6565
let jaeger: Container;
6666
beforeAll(async () => {

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@
6767
"@graphql-mesh/utils": "0.104.6",
6868
"@graphql-tools/delegate": "workspace:^",
6969
"@graphql-tools/utils": "10.9.0-alpha-20250710200000-fde1c74a0c2fa4f651cbeed5b2091aeda7afb162",
70-
"@opentelemetry/otlp-exporter-base@npm:0.202.0": "patch:@opentelemetry/otlp-exporter-base@npm%3A0.202.0#~/.yarn/patches/@opentelemetry-otlp-exporter-base-npm-0.202.0-f6f29c2eeb.patch",
70+
"@opentelemetry/otlp-exporter-base@npm:0.203.0": "patch:@opentelemetry/otlp-exporter-base@npm%3A0.203.0#~/.yarn/patches/@opentelemetry-otlp-exporter-base-npm-0.203.0-183dcac0e6.patch",
7171
"@rollup/plugin-node-resolve@npm:^15.2.3": "patch:@rollup/plugin-node-resolve@npm%3A16.0.1#~/.yarn/patches/@rollup-plugin-node-resolve-npm-16.0.1-2936474bab.patch",
7272
"@vitest/snapshot": "patch:@vitest/snapshot@npm:3.1.2#~/.yarn/patches/@vitest-snapshot-npm-3.1.1-4d18cf86dc.patch",
7373
"ansi-color@npm:^0.2.1": "patch:ansi-color@npm%3A0.2.1#~/.yarn/patches/ansi-color-npm-0.2.1-f7243d10a4.patch",

packages/plugins/opentelemetry/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@
6262
"@graphql-tools/utils": "^10.9.1",
6363
"@opentelemetry/api": "^1.9.0",
6464
"@opentelemetry/api-logs": "^0.203.0",
65-
"@opentelemetry/auto-instrumentations-node": "^0.62.0",
65+
"@opentelemetry/auto-instrumentations-node": "^0.62.1",
6666
"@opentelemetry/context-async-hooks": "^2.0.1",
6767
"@opentelemetry/core": "^2.0.1",
6868
"@opentelemetry/exporter-trace-otlp-grpc": "^0.203.0",

packages/plugins/opentelemetry/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {
33
useOpenTelemetry,
44
type OpenTelemetryGatewayPluginOptions,
55
type OpenTelemetryPlugin,
6+
type OpenTelemetryPluginUtils,
67
} from './plugin';
78

89
export * from './attributes';
@@ -13,4 +14,5 @@ export {
1314
useOpenTelemetry,
1415
OpenTelemetryPlugin,
1516
OpenTelemetryGatewayPluginOptions,
17+
OpenTelemetryPluginUtils,
1618
};

packages/plugins/opentelemetry/src/plugin-utils.ts

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -55,9 +55,16 @@ export function withState<
5555

5656
function addStateGetters(src: any) {
5757
const result: any = {};
58-
for (const [hookName, hook] of Object.entries(src) as any) {
58+
// Use the property descriptors to keep potential getters and setters, or not enumerable props
59+
const properties = Object.entries(Object.getOwnPropertyDescriptors(src));
60+
for (const [hookName, descriptor] of properties) {
61+
const hook = descriptor.value;
5962
if (typeof hook !== 'function') {
60-
result[hookName] = hook;
63+
descriptor.get &&= () => src[hookName];
64+
descriptor.set &&= (value) => {
65+
src[hookName] = value;
66+
};
67+
Object.defineProperty(result, hookName, descriptor);
6168
} else {
6269
result[hookName] = {
6370
[hook.name](payload: any, ...args: any[]) {
@@ -77,10 +84,10 @@ export function withState<
7784
return result;
7885
}
7986

80-
const { instrumentation, ...hooks } = pluginFactory(getState as any);
87+
const plugin = pluginFactory(getState as any);
8188

82-
const pluginWithState = addStateGetters(hooks);
83-
pluginWithState.instrumentation = addStateGetters(instrumentation);
89+
const pluginWithState = addStateGetters(plugin);
90+
pluginWithState.instrumentation = addStateGetters(plugin.instrumentation);
8491

8592
return pluginWithState as P;
8693
}

packages/plugins/opentelemetry/src/plugin.ts

Lines changed: 85 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import {
22
Attributes,
3+
GatewayConfigContext,
34
getRetryInfo,
45
isRetryExecutionRequest,
56
Logger,
@@ -214,10 +215,31 @@ const HeadersTextMapGetter: TextMapGetter<Headers> = {
214215
},
215216
};
216217

218+
export type ContextMatcher = {
219+
request?: Request;
220+
context?: any;
221+
executionRequest?: ExecutionRequest;
222+
};
223+
224+
export type OpenTelemetryPluginUtils = {
225+
tracer?: Tracer;
226+
getActiveContext: (payload: ContextMatcher) => Context;
227+
getHttpContext: (request: Request) => Context | undefined;
228+
getOperationContext: (context: any) => Context | undefined;
229+
getExecutionRequestContext: (
230+
ExecutionRequest: ExecutionRequest,
231+
) => Context | undefined;
232+
};
233+
217234
export type OpenTelemetryContextExtension = {
218235
openTelemetry: {
219236
tracer: Tracer;
220-
activeContext: () => Context;
237+
getActiveContext: (payload?: ContextMatcher) => Context;
238+
getHttpContext: (request?: Request) => Context | undefined;
239+
getOperationContext: (context?: any) => Context | undefined;
240+
getExecutionRequestContext: (
241+
ExecutionRequest: ExecutionRequest,
242+
) => Context | undefined;
221243
};
222244
};
223245

@@ -229,30 +251,26 @@ type State = Partial<
229251
HttpState<OtelState> & GraphQLState<OtelState> & GatewayState<OtelState>
230252
>;
231253

232-
export type OpenTelemetryPlugin =
233-
GatewayPlugin<OpenTelemetryContextExtension> & {
234-
getOtelContext: (payload: {
235-
request?: Request;
236-
context?: any;
237-
executionRequest?: ExecutionRequest;
238-
}) => Context;
239-
getTracer(): Tracer;
240-
};
254+
export type OpenTelemetryPlugin = GatewayPlugin<OpenTelemetryContextExtension> &
255+
OpenTelemetryPluginUtils;
241256

242257
export function useOpenTelemetry(
243-
options: OpenTelemetryGatewayPluginOptions & {
244-
log: Logger;
245-
},
258+
options: OpenTelemetryGatewayPluginOptions &
259+
// We ask for a Partial context to still allow the usage as a Yoga plugin
260+
Partial<GatewayConfigContext>,
246261
): OpenTelemetryPlugin {
247262
const inheritContext = options.inheritContext ?? true;
248263
const propagateContext = options.propagateContext ?? true;
249264
let useContextManager: boolean;
250265
const traces = typeof options.traces === 'object' ? options.traces : {};
251266

252267
let tracer: Tracer;
253-
let pluginLogger: Logger;
254268
let initSpan: Context | null;
255269

270+
// TODO: Make it const once Yoga has the Hive Logger
271+
let pluginLogger: Logger | undefined =
272+
options.log && options.log.child('[OpenTelemetry] ');
273+
256274
function isParentEnabled(state: State): boolean {
257275
const parentState = getMostSpecificState(state);
258276
return !parentState || !!parentState.otel;
@@ -304,7 +322,7 @@ export function useOpenTelemetry(
304322

305323
if (!useContextManager) {
306324
if (traces.spans?.schema) {
307-
pluginLogger.warn(
325+
pluginLogger?.warn(
308326
'Schema loading spans are disabled because no context manager is available',
309327
);
310328
}
@@ -314,14 +332,25 @@ export function useOpenTelemetry(
314332
}
315333
}
316334

317-
return withState<
335+
const plugin = withState<
318336
OpenTelemetryPlugin,
319337
OtelState,
320338
OtelState & { skipExecuteSpan?: true; subgraphNames: string[] },
321339
OtelState
322340
>((getState) => ({
323-
getTracer: () => tracer,
324-
getOtelContext: ({ state }) => getContext(state),
341+
get tracer() {
342+
return tracer;
343+
},
344+
getActiveContext: ({ state }) => getContext(state),
345+
getHttpContext: (request) => {
346+
return getState({ request }).forRequest.otel?.root;
347+
},
348+
getOperationContext: (context) => {
349+
return getState({ context }).forOperation.otel?.root;
350+
},
351+
getExecutionRequestContext: (executionRequest) => {
352+
return getState({ executionRequest }).forSubgraphExecution.otel?.root;
353+
},
325354
instrumentation: {
326355
request({ state: { forRequest }, request }, wrapped) {
327356
if (!shouldTrace(traces.spans?.http, { request })) {
@@ -648,21 +677,17 @@ export function useOpenTelemetry(
648677
},
649678

650679
onYogaInit({ yoga }) {
651-
const log =
652-
options.log ??
653-
//TODO remove this when Yoga will also use the new Logger API
654-
new Logger({
655-
writers: [
656-
{
657-
write(level, attrs, msg) {
658-
level = level === 'trace' ? 'debug' : level;
659-
yoga.logger[level](msg, attrs);
660-
},
680+
//TODO remove this when Yoga will also use the new Logger API
681+
pluginLogger ??= new Logger({
682+
writers: [
683+
{
684+
write(level, attrs, msg) {
685+
level = level === 'trace' ? 'debug' : level;
686+
yoga.logger[level](msg, attrs);
661687
},
662-
],
663-
});
664-
665-
pluginLogger = log.child('[OpenTelemetry] ');
688+
},
689+
],
690+
}).child('[OpenTelemetry] ');
666691

667692
if (options.configureDiagLogger !== false) {
668693
const logLevel = diagLogLevelFromEnv(); // We enable the diag only if it is explicitly enabled, as NodeSDK does
@@ -685,7 +710,9 @@ export function useOpenTelemetry(
685710
try {
686711
const requestId = requestIdByRequest.get(request);
687712
if (requestId) {
688-
loggerForRequest(options.log.child({ requestId }), request);
713+
if (options.log) {
714+
loggerForRequest(options.log.child({ requestId }), request);
715+
}
689716

690717
// When running in a runtime without a context manager, we have to keep track of the
691718
// span correlated to a log manually. For now, we just link all logs for a request to
@@ -695,7 +722,7 @@ export function useOpenTelemetry(
695722
}
696723
}
697724
} catch (error) {
698-
pluginLogger.error(
725+
pluginLogger!.error(
699726
{ error },
700727
'Error while setting up logger for request',
701728
);
@@ -706,7 +733,20 @@ export function useOpenTelemetry(
706733
extendContext({
707734
openTelemetry: {
708735
tracer,
709-
activeContext: () => getContext(state),
736+
getHttpContext: (request) => {
737+
const { forRequest } = request ? getState({ request }) : state;
738+
return forRequest.otel?.root;
739+
},
740+
getOperationContext: (context) => {
741+
const { forOperation } = context ? getState({ context }) : state;
742+
return forOperation.otel?.root;
743+
},
744+
getExecutionRequestContext: (executionRequest) => {
745+
return getState({ executionRequest }).forSubgraphExecution.otel
746+
?.root;
747+
},
748+
getActiveContext: (contextMatcher?: Parameters<typeof getState>[0]) =>
749+
getContext(contextMatcher ? getState(contextMatcher) : state),
710750
},
711751
});
712752
},
@@ -903,6 +943,16 @@ export function useOpenTelemetry(
903943
}
904944
},
905945
}));
946+
947+
if (options.openTelemetry) {
948+
if (options.openTelemetry.register) {
949+
options.openTelemetry?.register?.(plugin);
950+
} else {
951+
options.log?.warn('An OpenTelemetry plugin is already registered');
952+
}
953+
}
954+
955+
return plugin;
906956
}
907957

908958
function shouldTrace<Args>(

0 commit comments

Comments
 (0)