-
Notifications
You must be signed in to change notification settings - Fork 698
Default to the current context when context is not given for B3 propagator extraction #1732
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
Default to the current context when context is not given for B3 propagator extraction #1732
Conversation
Is this mandated by the spec? If I understood the change correctly, I think this would result in very surprising behavior.
Will print If I understand correctly, after this PR, If so, I'm not sure about this change. Think of propagation API in isolation. It is supposed to extract remove span context from a serialized form and inject serialized form of the active span into some carrier. I think extract returning anything other than what is inside the carrier would be surprising. I see it might be convenient to implement something like, "Use remote span as parent if present, otherwise use local active span" but I don't think propagation API should have to worry about it. Does that make sense or did I completely misunderstand the issue? |
That makes sense.
No, however it seems to be mandated by sofar design of the Python API and it seems to be a potential design issue in general. It is enforced/documented by TextMapPropagator itself:
I think it's hard to think about propagation API in isolation when by default from opentelemetry import context as otel_context
old_ctx = otel_context.get_current()
new_ctx = propagator.extract(carrier, old_ctx)
# Now we supposedly two contexts in isolation. How can one then merge them?
merged_new_ctx = otel_context.merge(old_ctx, new_ctx) # hypothetical API This does not happen. So instead a recipient of a context is responsible for a "merge" operation which in this discussion are
Yes, but that's why Additionally this design ambiguity seems to have a toll of inconsistent implementations. Looking at the Jaeger propagator, it does exactly said behavior of default to the current context: https://github.com/open-telemetry/opentelemetry-python/blob/main/propagator/opentelemetry-propagator-jaeger/src/opentelemetry/propagators/jaeger/__init__.py#L49 and is functioning within main for a great deal of time. Naturally, what is considered a correct implementation here? |
Defaulting to current context happens down the line when set_span_in_context is called in propagator which in turn calls set_value. opentelemetry-python/opentelemetry-api/src/opentelemetry/context/__init__.py Lines 86 to 106 in f5d10d4
I just realised that b3 doesn't pass the received context from |
extract is not meant to merge anything. It is a very isolated or self-contained operation. It takes a carrier and extracts trace context from it. It's basically just a de-serializer. It should always only de-serialize a previously serialized span context. Explicitly passing a default context is fine as it is very obvious and there is no surprise. If the extract method automatically defaults to current context, how is the caller supposed to know whether the carrier contained a valid context or not?
How would one do this after the change? It seems only way would be to fetch the current context and compare the result? If the docs say that it'll fallback on the global active context then IMO the docs are wrong and should be updated.
Users who need to fallback to current context can easily do it: ctx = extract(carrier) or current_context
# or with convenience provided by function
ctx = extract(carrier, current_context) To me this is very obvious for anyone having written a bit of Python. Dict.get, getattr, etc all behave like this. It is very explicit and always does exactly what the code suggests without any side-effects. I see zero value in defaulting to a global context TBH. IMO if we want a function to behave like this, it should probably be a convenience function in the context package or elsewhere. That's how I see it. May be others have a different opinion. BTW do you have a specific use case that is not possible or hard to implement without this change? Perhaps discussing in that context might help. |
According to the current interface @abc.abstractmethod
def extract(
self,
carrier: CarrierT,
context: typing.Optional[Context] = None,
getter: Getter = default_getter,
) -> Context: to @abc.abstractmethod
def extract(
self,
carrier: CarrierT,
context: typing.Optional[Context] = None,
getter: Getter = default_getter,
) -> Optional[Context]:
Look at the above. The examples that you presented above are in violation with the current type annotation, because you assume
Excluding the "written bit of Python" bit, I am just referring to the code that is already in the code base, which is said type annotation that basically disallow the operation that you just described. Otherwise it would completely fine to do what proposed. However current interfaces and documentation seems to be suggesting otherwise for the potential implementers of other propagators. @lonewolf3739 That's a good suggestion indeed. Now I understand better...the context of it :D Thanks for your insight. |
There can be many situations where users would want to differentiate between the two but it's irrelevant because not defaulting to a global context makes all cases possible and easy but more important obvious enough to implement. Few of situations that come to mind:
Extract defaulting to current context would make that awkward to implement. Consider: remote_ctx = extract(carrier)
if remote_ctx:
...
... vs ctx = extract(carrier)
if condition_to_detect_ctx_as_remote_or_local:
...
... The later one doesn't seem obvious/intuitive to me. On the other hand users who don't want any such checks and want to default to current context can simply write If we automatically return current context, we make some cases very awkward to implement and due to what the API looks like, very surprising IMO. On the other hand, not defaulting makes all cases possible in a straightforward manner. That's how I personally would expect the extract method to work. May be others think differently. We can discuss in the SIG meeting with everyone else later today. |
@marcinzaremba I'm not concerned about type-annotation or returning None vs InvalidSpan but about returning a hidden global value in case the function is called with bad input. |
I am not in favor of it either, don't get me wrong. What I am saying is this seems to be a current hint from the codebase (both from docstring and type annotation of the interface) that this is intended to work like that (which I follow + spec obviously). What is more it is implemented inconsistently among the propagators (B3 vs Jaeger) and what I would expect to see from the API interfaces is the following:
This would make a potential contribution experience much nicer and intention much clearer right from the start, which would avoid any potential discussions and ambiguity towards the matter. |
In the end I can contribute to the above if decisions are taken to make it consistent and issues created to make it happen. Thanks a lot for the discussion. |
Makes sense. All these points can be addressed easily. We have a couple of types called InvalidSpan and InvalidContext which can be returned instead of None. None vs InvalidSpanContext is not an issue. Docs can be updated as well. Main point of contention for me is returning the global context from a function that looks like is supposed to deserialize a bunch of strings to a SpanContext instance. I'll bring it up in the SIG meeting today and see what others have to say about this. You're most welcome to join and share your thoughts if you'd like. |
Sounds good. Please also consider how it all plays out with #1727 (+ discussion in the PR). Returning
This can also be addressed by having a helper method/function within the base class/SDK to handle the obvious, not to duplicate the effort among the propagators. |
Another alternative solution is to introduce one more kind of a |
@marcinzaremba If we cannot get span context from carrier because of empty headers/parsing failure/invalid value or any other reason I suggest we do this. if context is None:
return trace.set_span_in_context(trace.INVALID_SPAN, context)
else:
return context # Return context as is so we don't accidentally override with invalid This is simple, spec-compliant and we don't need to introduce one more kind of a |
@marcinzaremba What do you think about what @lonewolf3739 suggested above? This doesn't magically return active/parent context so Since this is returning Will @lonewolf3739's suggestion solve the problem you are facing with composite propagators? |
Superseded by #1750 |
Description
This is a follow up to #1728.
With the current design of the propagators pipeline every single propagator is responsible for defaulting to the current context in case the context is not provided as an input, which
TextMapPropagator
interface mandates: https://github.com/open-telemetry/opentelemetry-python/blob/main/opentelemetry-api/src/opentelemetry/propagators/textmap.py#L153.Recent changes to the B3 format propagator amplified this breach of contract, because there is a possibility that no context is given and in the case of an empty carrier, the context is returned as is, which then would result in a caller receiving
None
instead ofContext
promised in the signature (see unit test).Previously this behavior was masked, because some
Context
was always returned (possibly invalid) even if it was not the current one, giving an impression that it is what is expected.Contributes to #1727
Type of change
Please delete options that are not relevant.
How Has This Been Tested?
See implemented unit test.
Does This PR Require a Contrib Repo Change?
Checklist: