-
Notifications
You must be signed in to change notification settings - Fork 192
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
adding LocalRootSpan #1310
adding LocalRootSpan #1310
Conversation
this is based on Java's implementation, /~https://github.com/open-telemetry/opentelemetry-java-instrumentation/blob/v2.3.0/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/LocalRootSpan.java and adds the ability to identify and locate the "local root span" in a trace. The local root span is the top-level active span which has either an invalid or remote parent. It's tracked automatically as part of making a span active, either via `Span::activate()` or `Span::storeInContext()`, and can be retrieved in a couple of ways, but most easily via `LocalRootSpan::current()`.
Codecov ReportAttention: Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #1310 +/- ##
=======================================
Coverage ? 74.21%
Complexity ? 2584
=======================================
Files ? 373
Lines ? 7430
Branches ? 0
=======================================
Hits ? 5514
Misses ? 1916
Partials ? 0
Flags with carried forward coverage won't be shown. Click here to find out more.
Continue to review full report in Codecov by Sentry.
|
The primary objective here is to make it easier to go back and modify the local root span of a transaction. We do this now (by stashing the root span in context and accessing it later) in some auto-instrumentation packages to do things like update the name once routing has taken place, and in a similar fashion we could also add further attributes that were not known at the time the root span was created. |
} | ||
|
||
/** @inheritDoc */ | ||
final public function storeInContext(ContextInterface $context): ContextInterface | ||
{ | ||
if (LocalRootSpan::isLocalRoot($context)) { |
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.
The local root span is the top-level active span which has either an invalid or remote parent.
The context given to ::storeInContext()
is not necessarily the parent context of the span -> local root cannot be set by ::storeInContext()
.
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.
That's unfortunate, because it's the primary use-case! When would this happen? And could that scenario be represented in a unit/integration test?
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.
A possible scenario would be batch receiving of messages w/ a child span for every message that is being processed; loosely based on the messaging semconv:
$tracer = (new TracerProviderBuilder())->build()->getTracer('example');
// “Receive” spans SHOULD be created for operations of passing messages to the application when those operations are initiated by the application code (pull-based scenarios).
$root = $tracer
->spanBuilder('receive')
->setSpanKind(SpanKind::KIND_CONSUMER)
->startSpan();
$context = Context::getCurrent();
/*
if (LocalRootSpan::isLocalRoot($context)) {
$context = LocalRootSpan::store($context, $root);
}
*/
$rootScope = $root
->storeInContext($context)
->activate();
// ...
$remoteContext = /* extract creation context */Span::getInvalid()->storeInContext(Context::getCurrent());
if (($remoteSpan = Span::fromContext($remoteContext)) !== Span::getCurrent()) {
// For each message it accounts for, the “Process” or “Receive” span SHOULD link to the message’s creation context.
$root->addLink($remoteSpan->getContext());
}
// “Process” spans MAY be created in addition to “Receive” spans for pull-based scenarios for operations of processing messages.
$childScope = $tracer
->spanBuilder('process')
->startSpan()
->storeInContext($remoteContext) // preserve remote baggage etc.
->activate();
try {
assert(LocalRootSpan::current() === $root);
assert(LocalRootSpan::current() !== Span::getCurrent());
} finally {
$childScope->detach();
$rootScope->detach();
}
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.
Hmm. But in that example, by creating and activating a new span with remote parent, we've context switched and the local root is the process
span. After ending the process
span, then receive
is the local root again.
I happened upon https://opentelemetry.io/docs/languages/java/automatic/configuration/#capturing-consumer-message-receive-telemetry-in-messaging-instrumentations which sounds like that behaviour is what happens with Java...
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.
by creating and activating a new span with remote parent
The process
span is a child span of the receive
span in the example above, the remote span is only recorded as link on the receive
span; exporting the example (w/ SpanContext::createFromRemoteParent('fabebb164f22d4afc51df50d9a3ff629', '87c6836d8610ac6d')
instead of an invalid span context to simulate an actual remote span; doesn't change behavior):
"scopeSpans": [
{
"scope": {
"name": "example"
},
"spans": [
{
"traceId": "a2ee09cae8ed2ddfb91ee679a50f18d0",
"spanId": "274517003024b34a",
"flags": 259,
"name": "receive",
"kind": 5,
"startTimeUnixNano": "1716403479097090643",
"endTimeUnixNano": "1716403479097304335",
"links": [
{
"traceId": "fabebb164f22d4afc51df50d9a3ff629",
"spanId": "87c6836d8610ac6d",
"flags": 768
}
],
"status": {}
},
{
"traceId": "a2ee09cae8ed2ddfb91ee679a50f18d0",
"spanId": "aca69b6ea434162d",
"parentSpanId": "274517003024b34a",
"flags": 259,
"name": "process",
"kind": 1,
"startTimeUnixNano": "1716403479097294259",
"endTimeUnixNano": "1716403479097313118",
"attributes": [
{
"key": "local_root",
"value": {
# set to LocalRootSpan::current() === Span::getCurrent()
# should be false, parent span is local
"boolValue": true
}
}
],
"status": {}
}
]
}
]
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.
I tried to replicate this with an example (committed and pushed), however I think I created the remote span differently, because my local_root check seems ok...
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.
I had another play with this PR today. Given that all of the use-cases that I've seen for needing the root span have been for kind=server (specifically http server spans), perhaps we can avoid this issue (at least for now), by only setting the root span for kind=server spans with a remote parent?
We can document it as such, and also mark it as experimental
.
this is based on Java's implementation, /~https://github.com/open-telemetry/opentelemetry-java-instrumentation/blob/v2.3.0/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/LocalRootSpan.java and adds the ability to identify and locate the "local root span" in a trace. The local root span is the top-level active span which has either an invalid or remote parent.
It's tracked automatically as part of making a span active, either via
Span::activate()
orSpan::storeInContext()
, and can be retrieved in a couple of ways, but most easily viaLocalRootSpan::current()
.