-
-
Notifications
You must be signed in to change notification settings - Fork 1.7k
feat(opentelemetry): Fix & align isolation scope usage in node-experimental #10570
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
Changes from all commits
8eccfe9
dad76f8
13f9f23
f0b98fb
822ec12
38677d9
09e9f6f
9a6bc55
30abc45
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,13 +1,11 @@ | ||
import type { Context } from '@opentelemetry/api'; | ||
import { SpanKind } from '@opentelemetry/api'; | ||
import type { Span } from '@opentelemetry/sdk-trace-base'; | ||
import { SemanticAttributes } from '@opentelemetry/semantic-conventions'; | ||
import { SentrySpanProcessor, getClient, getSpanFinishScope } from '@sentry/opentelemetry'; | ||
import { SentrySpanProcessor, getClient } from '@sentry/opentelemetry'; | ||
|
||
import type { Http } from '../integrations/http'; | ||
import type { NodeFetch } from '../integrations/node-fetch'; | ||
import type { NodeExperimentalClient } from '../types'; | ||
import { getIsolationScope } from './api'; | ||
import { Scope } from './scope'; | ||
|
||
/** | ||
|
@@ -18,18 +16,6 @@ export class NodeExperimentalSentrySpanProcessor extends SentrySpanProcessor { | |
super({ scopeClass: Scope }); | ||
} | ||
|
||
/** @inheritDoc */ | ||
public onStart(span: Span, parentContext: Context): void { | ||
super.onStart(span, parentContext); | ||
|
||
// We need to make sure that we use the correct isolation scope when finishing the span | ||
// so we store it on the span finish scope for later use | ||
const scope = getSpanFinishScope(span) as Scope | undefined; | ||
if (scope) { | ||
scope.isolationScope = getIsolationScope(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this is simply handled in the base span processor now. |
||
} | ||
} | ||
|
||
/** @inheritDoc */ | ||
protected _shouldSendSpanToSentry(span: Span): boolean { | ||
const client = getClient<NodeExperimentalClient>(); | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -99,7 +99,7 @@ describe('Integration | Transactions', () => { | |
environment: 'production', | ||
event_id: expect.any(String), | ||
platform: 'node', | ||
sdkProcessingMetadata: { | ||
sdkProcessingMetadata: expect.objectContaining({ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this has some more fields now, but we don't care about that. |
||
dynamicSamplingContext: expect.objectContaining({ | ||
environment: 'production', | ||
public_key: expect.any(String), | ||
|
@@ -112,7 +112,7 @@ describe('Integration | Transactions', () => { | |
source: 'task', | ||
spanMetadata: expect.any(Object), | ||
requestPath: 'test-path', | ||
}, | ||
}), | ||
server_name: expect.any(String), | ||
// spans are circular (they have a reference to the transaction), which leads to jest choking on this | ||
// instead we compare them in detail below | ||
|
@@ -329,6 +329,159 @@ describe('Integration | Transactions', () => { | |
); | ||
}); | ||
|
||
it('correctly creates concurrent transaction & spans when using native OTEL tracer', async () => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. added a test to actually check that it works with native otel tracing too. |
||
const beforeSendTransaction = jest.fn(() => null); | ||
|
||
mockSdkInit({ enableTracing: true, beforeSendTransaction }); | ||
|
||
const client = Sentry.getClient<NodeExperimentalClient>(); | ||
|
||
Sentry.addBreadcrumb({ message: 'test breadcrumb 1', timestamp: 123456 }); | ||
|
||
Sentry.withIsolationScope(() => { | ||
client.tracer.startActiveSpan('test name', span => { | ||
Sentry.addBreadcrumb({ message: 'test breadcrumb 2', timestamp: 123456 }); | ||
|
||
span.setAttributes({ | ||
'test.outer': 'test value', | ||
}); | ||
|
||
const subSpan = Sentry.startInactiveSpan({ name: 'inner span 1' }); | ||
subSpan.end(); | ||
|
||
Sentry.setTag('test.tag', 'test value'); | ||
|
||
client.tracer.startActiveSpan('inner span 2', innerSpan => { | ||
Sentry.addBreadcrumb({ message: 'test breadcrumb 3', timestamp: 123456 }); | ||
|
||
innerSpan.setAttributes({ | ||
'test.inner': 'test value', | ||
}); | ||
|
||
innerSpan.end(); | ||
}); | ||
|
||
span.end(); | ||
}); | ||
}); | ||
|
||
Sentry.withIsolationScope(() => { | ||
client.tracer.startActiveSpan('test name b', span => { | ||
Sentry.addBreadcrumb({ message: 'test breadcrumb 2b', timestamp: 123456 }); | ||
|
||
span.setAttributes({ | ||
'test.outer': 'test value b', | ||
}); | ||
|
||
const subSpan = Sentry.startInactiveSpan({ name: 'inner span 1b' }); | ||
subSpan.end(); | ||
|
||
Sentry.setTag('test.tag', 'test value b'); | ||
|
||
client.tracer.startActiveSpan('inner span 2b', innerSpan => { | ||
Sentry.addBreadcrumb({ message: 'test breadcrumb 3b', timestamp: 123456 }); | ||
|
||
innerSpan.setAttributes({ | ||
'test.inner': 'test value b', | ||
}); | ||
|
||
innerSpan.end(); | ||
}); | ||
|
||
span.end(); | ||
}); | ||
}); | ||
|
||
await client.flush(); | ||
|
||
expect(beforeSendTransaction).toHaveBeenCalledTimes(2); | ||
expect(beforeSendTransaction).toHaveBeenCalledWith( | ||
expect.objectContaining({ | ||
breadcrumbs: [ | ||
{ message: 'test breadcrumb 1', timestamp: 123456 }, | ||
{ message: 'test breadcrumb 2', timestamp: 123456 }, | ||
{ message: 'test breadcrumb 3', timestamp: 123456 }, | ||
], | ||
contexts: expect.objectContaining({ | ||
otel: expect.objectContaining({ | ||
attributes: { | ||
'test.outer': 'test value', | ||
}, | ||
}), | ||
trace: { | ||
data: { | ||
'otel.kind': 'INTERNAL', | ||
'sentry.origin': 'manual', | ||
}, | ||
span_id: expect.any(String), | ||
status: 'ok', | ||
trace_id: expect.any(String), | ||
origin: 'manual', | ||
}, | ||
}), | ||
spans: [ | ||
expect.objectContaining({ | ||
description: 'inner span 1', | ||
}), | ||
expect.objectContaining({ | ||
description: 'inner span 2', | ||
}), | ||
], | ||
start_timestamp: expect.any(Number), | ||
tags: { 'test.tag': 'test value' }, | ||
timestamp: expect.any(Number), | ||
transaction: 'test name', | ||
type: 'transaction', | ||
}), | ||
{ | ||
event_id: expect.any(String), | ||
}, | ||
); | ||
|
||
expect(beforeSendTransaction).toHaveBeenCalledWith( | ||
expect.objectContaining({ | ||
breadcrumbs: [ | ||
{ message: 'test breadcrumb 1', timestamp: 123456 }, | ||
{ message: 'test breadcrumb 2b', timestamp: 123456 }, | ||
{ message: 'test breadcrumb 3b', timestamp: 123456 }, | ||
], | ||
contexts: expect.objectContaining({ | ||
otel: expect.objectContaining({ | ||
attributes: { | ||
'test.outer': 'test value b', | ||
}, | ||
}), | ||
trace: { | ||
data: { | ||
'otel.kind': 'INTERNAL', | ||
'sentry.origin': 'manual', | ||
}, | ||
span_id: expect.any(String), | ||
status: 'ok', | ||
trace_id: expect.any(String), | ||
origin: 'manual', | ||
}, | ||
}), | ||
spans: [ | ||
expect.objectContaining({ | ||
description: 'inner span 1b', | ||
}), | ||
expect.objectContaining({ | ||
description: 'inner span 2b', | ||
}), | ||
], | ||
start_timestamp: expect.any(Number), | ||
tags: { 'test.tag': 'test value b' }, | ||
timestamp: expect.any(Number), | ||
transaction: 'test name b', | ||
type: 'transaction', | ||
}), | ||
{ | ||
event_id: expect.any(String), | ||
}, | ||
); | ||
}); | ||
|
||
it('correctly creates transaction & spans with a trace header data', async () => { | ||
const beforeSendTransaction = jest.fn(() => null); | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,7 @@ | ||
import * as api from '@opentelemetry/api'; | ||
import type { Hub, RunWithAsyncContextOptions } from '@sentry/core'; | ||
import { setAsyncContextStrategy } from '@sentry/core'; | ||
import { SENTRY_FORK_ISOLATION_SCOPE_CONTEXT_KEY } from './constants'; | ||
|
||
import { getHubFromContext } from './utils/contextData'; | ||
|
||
|
@@ -30,7 +31,11 @@ export function setOpenTelemetryContextAsyncContextStrategy(): void { | |
const ctx = api.context.active(); | ||
|
||
// We depend on the otelContextManager to handle the context/hub | ||
return api.context.with(ctx, () => { | ||
// We set the `SENTRY_FORK_ISOLATION_SCOPE_CONTEXT_KEY` context value, which is picked up by | ||
// the OTEL context manager, which uses the presence of this key to determine if it should | ||
// fork the isolation scope, or not | ||
// as by default, we don't want to fork this, unless triggered explicitly by `runWithAsyncContext` | ||
return api.context.with(ctx.setValue(SENTRY_FORK_ISOLATION_SCOPE_CONTEXT_KEY, true), () => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we use this attribute to tell the otel context manager if the isolation scope should be forked or not. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we explain this a bit further in comments? I still don't fully get it. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. will do! |
||
return callback(); | ||
}); | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,14 +1,28 @@ | ||
import type { Context, ContextManager } from '@opentelemetry/api'; | ||
import type { Carrier, Hub } from '@sentry/core'; | ||
import { getCurrentHub, getHubFromCarrier } from '@sentry/core'; | ||
import { ensureHubOnCarrier } from '@sentry/core'; | ||
import type { Hub } from '@sentry/core'; | ||
import { getCurrentHub } from '@sentry/core'; | ||
import { SENTRY_FORK_ISOLATION_SCOPE_CONTEXT_KEY } from './constants'; | ||
import { OpenTelemetryHub } from './custom/hub'; | ||
|
||
import { setHubOnContext } from './utils/contextData'; | ||
|
||
function createNewHub(parent: Hub | undefined): Hub { | ||
const carrier: Carrier = {}; | ||
ensureHubOnCarrier(carrier, parent); | ||
return getHubFromCarrier(carrier); | ||
function createNewHub(parent: Hub | undefined, shouldForkIsolationScope: boolean): Hub { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
if (parent) { | ||
// eslint-disable-next-line deprecation/deprecation | ||
const client = parent.getClient(); | ||
// eslint-disable-next-line deprecation/deprecation | ||
const scope = parent.getScope(); | ||
// eslint-disable-next-line deprecation/deprecation | ||
const isolationScope = parent.getIsolationScope(); | ||
|
||
return new OpenTelemetryHub( | ||
client, | ||
scope.clone(), | ||
shouldForkIsolationScope ? isolationScope.clone() : isolationScope, | ||
); | ||
} | ||
|
||
return new OpenTelemetryHub(); | ||
} | ||
|
||
// Typescript complains if we do not use `...args: any[]` for the mixin, with: | ||
|
@@ -46,11 +60,18 @@ export function wrapContextManagerClass<ContextManagerInstance extends ContextMa | |
thisArg?: ThisParameterType<F>, | ||
...args: A | ||
): ReturnType<F> { | ||
const shouldForkIsolationScope = context.getValue(SENTRY_FORK_ISOLATION_SCOPE_CONTEXT_KEY) === true; | ||
|
||
// eslint-disable-next-line deprecation/deprecation | ||
const existingHub = getCurrentHub(); | ||
const newHub = createNewHub(existingHub); | ||
const newHub = createNewHub(existingHub, shouldForkIsolationScope); | ||
|
||
return super.with(setHubOnContext(context, newHub), fn, thisArg, ...args); | ||
return super.with( | ||
setHubOnContext(context.deleteValue(SENTRY_FORK_ISOLATION_SCOPE_CONTEXT_KEY), newHub), | ||
fn, | ||
thisArg, | ||
...args, | ||
); | ||
} | ||
} | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -71,7 +71,7 @@ export function wrapClientClass< | |
event: Event, | ||
hint: EventHint, | ||
scope?: Scope, | ||
_isolationScope?: Scope, | ||
isolationScope?: Scope, | ||
): PromiseLike<Event | null> { | ||
let actualScope = scope; | ||
|
||
|
@@ -81,7 +81,7 @@ export function wrapClientClass< | |
delete hint.captureContext; | ||
} | ||
|
||
return super._prepareEvent(event, hint, actualScope); | ||
return super._prepareEvent(event, hint, actualScope, isolationScope); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this was not correctly passed through here before, oops. |
||
} | ||
} | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -10,8 +10,8 @@ import { OpenTelemetryScope } from './scope'; | |
* Exported only for testing | ||
*/ | ||
export class OpenTelemetryHub extends Hub { | ||
public constructor(client?: Client, scope: Scope = new OpenTelemetryScope()) { | ||
super(client, scope); | ||
public constructor(client?: Client, scope: Scope = new OpenTelemetryScope(), isolationScope?: Scope) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we accept this in the upstream implementation too. |
||
super(client, scope, isolationScope); | ||
} | ||
} | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we don't need this anymore 🚀