Skip to content

Commit 2d7a0d3

Browse files
authored
feat(node-experimental): Sync OTEL context with Sentry AsyncContext (#8797)
This PR implements a strategy to sync the OpenTelemetry Context with our own Hub forking for AsyncContext. This works by fully relying on OpenTelemetry to handle async context isolation/forking. 1. We implement a custom OTEL ContextManager that wraps the default AsyncHooks manager, but makes sure to also fork the hub for each context change & puts the hub on the OTEL context so we can retrieve it. 2. Then, we also have a custom Sentry AsyncContextStrategy which just refers to OTEL context and picks the hub from there we put there in 1. This means we do not need to do any context forking ourselves anymore, so no need for e.g. `Sentry.Handlers.requestHandler()` and stuff like this. It _should_ also mean that Sentry & OTEL should be as in sync as possible. Some notes: * Currently only works for AsyncHooks, which should be fine I guess. Could also be exteded to work with other implementations as well.
1 parent 67822fb commit 2d7a0d3

File tree

6 files changed

+90
-0
lines changed

6 files changed

+90
-0
lines changed

packages/node-experimental/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
"@opentelemetry/instrumentation-pg": "~0.36.0",
3838
"@opentelemetry/sdk-trace-node": "~1.15.0",
3939
"@opentelemetry/semantic-conventions": "~1.15.0",
40+
"@opentelemetry/context-async-hooks": "~1.15.0",
4041
"@prisma/instrumentation": "~5.0.0",
4142
"@sentry/core": "7.66.0",
4243
"@sentry/node": "7.66.0",

packages/node-experimental/src/sdk/init.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { Http } from '../integrations/http';
66
import type { NodeExperimentalOptions } from '../types';
77
import { NodeExperimentalClient } from './client';
88
import { initOtel } from './initOtel';
9+
import { setOtelContextAsyncContextStrategy } from './otelAsyncContextStrategy';
910

1011
const ignoredDefaultIntegrations = ['Http', 'Undici'];
1112

@@ -35,4 +36,5 @@ export function init(options: NodeExperimentalOptions | undefined = {}): void {
3536

3637
// Always init Otel, even if tracing is disabled, because we need it for trace propagation & the HTTP integration
3738
initOtel();
39+
setOtelContextAsyncContextStrategy();
3840
}

packages/node-experimental/src/sdk/initOtel.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { getCurrentHub } from '@sentry/core';
44
import { SentryPropagator, SentrySpanProcessor } from '@sentry/opentelemetry-node';
55

66
import type { NodeExperimentalClient } from './client';
7+
import { SentryContextManager } from './otelContextManager';
78

89
/**
910
* Initialize OpenTelemetry for Node.
@@ -22,9 +23,14 @@ export function initOtel(): () => void {
2223
});
2324
provider.addSpanProcessor(new SentrySpanProcessor());
2425

26+
// We use a custom context manager to keep context in sync with sentry scope
27+
const contextManager = new SentryContextManager();
28+
contextManager.enable();
29+
2530
// Initialize the provider
2631
provider.register({
2732
propagator: new SentryPropagator(),
33+
contextManager,
2834
});
2935

3036
// Cleanup function
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import * as api from '@opentelemetry/api';
2+
import type { Hub, RunWithAsyncContextOptions } from '@sentry/core';
3+
import { setAsyncContextStrategy } from '@sentry/core';
4+
5+
import { OTEL_CONTEXT_HUB_KEY } from './otelContextManager';
6+
7+
/**
8+
* Sets the async context strategy to use follow the OTEL context under the hood.
9+
* We handle forking a hub inside of our custom OTEL Context Manager (./otelContextManager.ts)
10+
*/
11+
export function setOtelContextAsyncContextStrategy(): void {
12+
function getCurrentHub(): Hub | undefined {
13+
const ctx = api.context.active();
14+
15+
// Returning undefined means the global hub will be used
16+
return ctx.getValue(OTEL_CONTEXT_HUB_KEY) as Hub | undefined;
17+
}
18+
19+
/* This is more or less a NOOP - we rely on the OTEL context manager for this */
20+
function runWithAsyncContext<T>(callback: () => T, options: RunWithAsyncContextOptions): T {
21+
const existingHub = getCurrentHub();
22+
23+
if (existingHub && options?.reuseExisting) {
24+
// We're already in an async context, so we don't need to create a new one
25+
// just call the callback with the current hub
26+
return callback();
27+
}
28+
29+
const ctx = api.context.active();
30+
31+
// We depend on the otelContextManager to handle the context/hub
32+
return api.context.with(ctx, () => {
33+
return callback();
34+
});
35+
}
36+
37+
setAsyncContextStrategy({ getCurrentHub, runWithAsyncContext });
38+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import type { Context } from '@opentelemetry/api';
2+
import * as api from '@opentelemetry/api';
3+
import { AsyncLocalStorageContextManager } from '@opentelemetry/context-async-hooks';
4+
import type { Carrier, Hub } from '@sentry/core';
5+
import { ensureHubOnCarrier, getCurrentHub, getHubFromCarrier } from '@sentry/core';
6+
7+
export const OTEL_CONTEXT_HUB_KEY = api.createContextKey('sentry_hub');
8+
9+
function createNewHub(parent: Hub | undefined): Hub {
10+
const carrier: Carrier = {};
11+
ensureHubOnCarrier(carrier, parent);
12+
return getHubFromCarrier(carrier);
13+
}
14+
15+
/**
16+
* This is a custom ContextManager for OpenTelemetry, which extends the default AsyncLocalStorageContextManager.
17+
* It ensures that we create a new hub per context, so that the OTEL Context & the Sentry Hub are always in sync.
18+
*
19+
* Note that we currently only support AsyncHooks with this,
20+
* but since this should work for Node 14+ anyhow that should be good enough.
21+
*/
22+
export class SentryContextManager extends AsyncLocalStorageContextManager {
23+
/**
24+
* Overwrite with() of the original AsyncLocalStorageContextManager
25+
* to ensure we also create a new hub per context.
26+
*/
27+
public with<A extends unknown[], F extends (...args: A) => ReturnType<F>>(
28+
context: Context,
29+
fn: F,
30+
thisArg?: ThisParameterType<F>,
31+
...args: A
32+
): ReturnType<F> {
33+
const existingHub = getCurrentHub();
34+
const newHub = createNewHub(existingHub);
35+
36+
return super.with(context.setValue(OTEL_CONTEXT_HUB_KEY, newHub), fn, thisArg, ...args);
37+
}
38+
}

yarn.lock

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3733,6 +3733,11 @@
37333733
dependencies:
37343734
tslib "^2.3.1"
37353735

3736+
"@opentelemetry/context-async-hooks@~1.15.0":
3737+
version "1.15.2"
3738+
resolved "https://registry.yarnpkg.com/@opentelemetry/context-async-hooks/-/context-async-hooks-1.15.2.tgz#116bd5fef231137198d5bf551e8c0521fbdfe928"
3739+
integrity sha512-VAMHG67srGFQDG/N2ns5AyUT9vUcoKpZ/NpJ5fDQIPfJd7t3ju+aHwvDsMcrYBWuCh03U3Ky6o16+872CZchBg==
3740+
37363741
"@opentelemetry/context-base@^0.12.0":
37373742
version "0.12.0"
37383743
resolved "https://registry.yarnpkg.com/@opentelemetry/context-base/-/context-base-0.12.0.tgz#4906ae27359d3311e3dea1b63770a16f60848550"

0 commit comments

Comments
 (0)