Skip to content

Commit f1f8bb8

Browse files
committed
Changed to the propagator API
Adding a UnifiedContext, composing DistributedContext and SpanContext. This will enable propagators to extract and inject values from either system, enabling more sophisticated schemes and standards to propagate data. This also removes the need for generics and propagators that only consume one or the other, requiring integrators to do extra work to wire propagators appropriately. Modifying the API of the propagators to consume the context as a mutable argument. By passing in the context rather than returning, this enables the chained use of propagators, allowing for situations such as supporting multiple trace propagation standards simulatenously.
1 parent b7b38a5 commit f1f8bb8

File tree

8 files changed

+159
-83
lines changed

8 files changed

+159
-83
lines changed

opentelemetry-api/src/opentelemetry/context/__init__.py

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,25 +11,22 @@
1111
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
14-
15-
1614
"""
1715
The OpenTelemetry context module provides abstraction layer on top of
1816
thread-local storage and contextvars. The long term direction is to switch to
1917
contextvars provided by the Python runtime library.
2018
2119
A global object ``Context`` is provided to access all the context related
22-
functionalities::
20+
functionalities:
2321
2422
>>> from opentelemetry.context import Context
2523
>>> Context.foo = 1
2624
>>> Context.foo = 2
2725
>>> Context.foo
2826
2
2927
30-
When explicit thread is used, a helper function
31-
``Context.with_current_context`` can be used to carry the context across
32-
threads::
28+
When explicit thread is used, a helper function `Context.with_current_context`
29+
can be used to carry the context across threads:
3330
3431
from threading import Thread
3532
from opentelemetry.context import Context
@@ -62,7 +59,7 @@ def work(name):
6259
6360
print('Main thread:', Context)
6461
65-
Here goes another example using thread pool::
62+
Here goes another example using thread pool:
6663
6764
import time
6865
import threading
@@ -97,7 +94,7 @@ def work(name):
9794
pool.join()
9895
println('Main thread: {}'.format(Context))
9996
100-
Here goes a simple demo of how async could work in Python 3.7+::
97+
Here goes a simple demo of how async could work in Python 3.7+:
10198
10299
import asyncio
103100
@@ -141,9 +138,9 @@ async def main():
141138
import typing
142139

143140
from .base_context import BaseRuntimeContext
141+
from .unified_context import UnifiedContext
144142

145-
__all__ = ['Context']
146-
143+
__all__ = ['Context', 'UnifiedContext']
147144

148145
Context: typing.Optional[BaseRuntimeContext]
149146

opentelemetry-api/src/opentelemetry/context/base_context.py

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ def set(self, value: 'object') -> None:
3737
raise NotImplementedError
3838

3939
_lock = threading.Lock()
40-
_slots: typing.Dict[str, 'BaseRuntimeContext.Slot'] = {}
40+
_slots: typing.Dict[str, Slot] = {}
4141

4242
@classmethod
4343
def clear(cls) -> None:
@@ -48,11 +48,7 @@ def clear(cls) -> None:
4848
slot.clear()
4949

5050
@classmethod
51-
def register_slot(
52-
cls,
53-
name: str,
54-
default: 'object' = None,
55-
) -> 'BaseRuntimeContext.Slot':
51+
def register_slot(cls, name: str, default: 'object' = None) -> 'Slot':
5652
"""Register a context slot with an optional default value.
5753
5854
:type name: str

opentelemetry-api/src/opentelemetry/context/propagation/binaryformat.py

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,8 @@
1313
# limitations under the License.
1414

1515
import abc
16-
import typing
1716

18-
from opentelemetry.trace import SpanContext
17+
from opentelemetry.context import UnifiedContext
1918

2019

2120
class BinaryFormat(abc.ABC):
@@ -26,30 +25,32 @@ class BinaryFormat(abc.ABC):
2625
"""
2726
@staticmethod
2827
@abc.abstractmethod
29-
def to_bytes(context: SpanContext) -> bytes:
28+
def to_bytes(context: UnifiedContext) -> bytes:
3029
"""Creates a byte representation of a SpanContext.
3130
3231
to_bytes should read values from a SpanContext and return a data
3332
format to represent it, in bytes.
3433
3534
Args:
36-
context: the SpanContext to serialize
35+
context: the SpanContext to serialize.
3736
3837
Returns:
3938
A bytes representation of the SpanContext.
4039
4140
"""
4241
@staticmethod
4342
@abc.abstractmethod
44-
def from_bytes(byte_representation: bytes) -> typing.Optional[SpanContext]:
45-
"""Return a SpanContext that was represented by bytes.
43+
def from_bytes(context: UnifiedContext,
44+
byte_representation: bytes) -> None:
45+
"""Populate UnifiedContext that was represented by bytes.
4646
47-
from_bytes should return back a SpanContext that was constructed from
48-
the data serialized in the byte_representation passed. If it is not
47+
from_bytes should populated UnifiedContext with data that was
48+
serialized in the byte_representation passed. If it is not
4949
possible to read in a proper SpanContext, return None.
5050
5151
Args:
52-
byte_representation: the bytes to deserialize
52+
context: The UnifiedContext to populate.
53+
byte_representation: the bytes to deserialize.
5354
5455
Returns:
5556
A bytes representation of the SpanContext if it is valid.

opentelemetry-api/src/opentelemetry/context/propagation/httptextformat.py

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import abc
1616
import typing
1717

18+
from opentelemetry.context import UnifiedContext
1819
from opentelemetry.trace import SpanContext
1920

2021
Setter = typing.Callable[[object, str, str], None]
@@ -35,11 +36,12 @@ class HTTPTextFormat(abc.ABC):
3536
import flask
3637
import requests
3738
from opentelemetry.context.propagation import HTTPTextFormat
39+
from opentelemetry.trace import tracer
40+
from opentelemetry.context import UnifiedContext
3841
3942
PROPAGATOR = HTTPTextFormat()
4043
4144
42-
4345
def get_header_from_flask_request(request, key):
4446
return request.headers.get_all(key)
4547
@@ -48,15 +50,17 @@ def set_header_into_requests_request(request: requests.Request,
4850
request.headers[key] = value
4951
5052
def example_route():
51-
span_context = PROPAGATOR.extract(
52-
get_header_from_flask_request,
53+
span = tracer().create_span("")
54+
context = UnifiedContext.create(span)
55+
PROPAGATOR.extract(
56+
context, get_header_from_flask_request,
5357
flask.request
5458
)
5559
request_to_downstream = requests.Request(
5660
"GET", "http://httpbin.org/get"
5761
)
5862
PROPAGATOR.inject(
59-
span_context,
63+
context,
6064
set_header_into_requests_request,
6165
request_to_downstream
6266
)
@@ -68,15 +72,17 @@ def example_route():
6872
https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/api-propagators.md
6973
"""
7074
@abc.abstractmethod
71-
def extract(self, get_from_carrier: Getter,
72-
carrier: object) -> SpanContext:
73-
"""Create a SpanContext from values in the carrier.
75+
def extract(self, context: UnifiedContext, get_from_carrier: Getter,
76+
carrier: object) -> None:
77+
"""Extract values from the carrier into the context.
7478
7579
The extract function should retrieve values from the carrier
76-
object using get_from_carrier, and use values to populate a
77-
SpanContext value and return it.
80+
object using get_from_carrier, and use values to populate
81+
attributes of the UnifiedContext passed in.
7882
7983
Args:
84+
context: A UnifiedContext instance that will be
85+
populated with values from the carrier.
8086
get_from_carrier: a function that can retrieve zero
8187
or more values from the carrier. In the case that
8288
the value does not exist, return an empty list.
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
# Copyright 2019, OpenTelemetry Authors
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from opentelemetry.distributedcontext import DistributedContext
16+
from opentelemetry.trace import SpanContext
17+
18+
19+
class UnifiedContext:
20+
"""A unified context object that contains all context relevant to
21+
telemetry.
22+
23+
The UnifiedContext is a single object that composes all contexts that
24+
are needed by the various forms of telemetry. It is intended to be an
25+
object that can be passed as the argument to any component that needs
26+
to read or modify content values (such as propagators). By unifying
27+
all context in a composed data structure, it expands the flexibility
28+
of the APIs that modify it.
29+
30+
As it is designed to carry context specific to all telemetry use
31+
cases, it's schema is explicit. Note that this is not intended to
32+
be an object that acts as a singleton that returns different results
33+
based on the thread or coroutine of execution. For that, see `Context`.
34+
35+
36+
Args:
37+
distributed: The DistributedContext for this instance.
38+
span: The SpanContext for this instance.
39+
"""
40+
__slots__ = ["distributed", "span"]
41+
42+
def __init__(self, distributed: DistributedContext, span: SpanContext):
43+
self.distributed = distributed
44+
self.span = span
45+
46+
@staticmethod
47+
def create(span: SpanContext) -> "UnifiedContext":
48+
"""Create an unpopulated UnifiedContext object.
49+
50+
Example:
51+
52+
from opentelemetry.trace import tracer
53+
span = tracer.create_span("")
54+
context = UnifiedContext.create(span)
55+
56+
57+
Args:
58+
parent_span: the parent SpanContext that will be the
59+
parent of the span in the UnifiedContext.
60+
"""
61+
return UnifiedContext(DistributedContext(), span)
62+
63+
def __repr__(self) -> str:
64+
return "{}(distributed={}, span={})".format(
65+
type(self).__name__, repr(self.distributed), repr(self.span))

opentelemetry-api/src/opentelemetry/distributedcontext/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,9 @@
1111
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
14+
15+
16+
class DistributedContext:
17+
"""A container for values for w3c's Correlation Context."""
18+
def __repr__(self) -> str:
19+
return "{}()".format(type(self).__name__)

opentelemetry-sdk/src/opentelemetry/sdk/context/propagation/b3_format.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ class B3Format(HTTPTextFormat):
3232
_SAMPLE_PROPAGATE_VALUES = set(["1", "True", "true", "d"])
3333

3434
@classmethod
35-
def extract(cls, get_from_carrier, carrier):
35+
def extract(cls, context, get_from_carrier, carrier):
3636
trace_id = format_trace_id(trace.INVALID_TRACE_ID)
3737
span_id = format_span_id(trace.INVALID_SPAN_ID)
3838
sampled = 0
@@ -56,7 +56,7 @@ def extract(cls, get_from_carrier, carrier):
5656
elif len(fields) == 4:
5757
trace_id, span_id, sampled, _parent_span_id = fields
5858
else:
59-
return trace.INVALID_SPAN_CONTEXT
59+
return
6060
else:
6161
trace_id = _extract_first_element(
6262
get_from_carrier(carrier, cls.TRACE_ID_KEY)) or trace_id
@@ -75,7 +75,7 @@ def extract(cls, get_from_carrier, carrier):
7575
if sampled in cls._SAMPLE_PROPAGATE_VALUES or flags == "1":
7676
options |= trace.TraceOptions.RECORDED
7777

78-
return trace.SpanContext(
78+
context.span = trace.SpanContext(
7979
# trace an span ids are encoded in hex, so must be converted
8080
trace_id=int(trace_id, 16),
8181
span_id=int(span_id, 16),
@@ -85,11 +85,12 @@ def extract(cls, get_from_carrier, carrier):
8585

8686
@classmethod
8787
def inject(cls, context, set_in_carrier, carrier):
88-
sampled = (trace.TraceOptions.RECORDED & context.trace_options) != 0
88+
sampled = (trace.TraceOptions.RECORDED
89+
& context.span.trace_options) != 0
8990
set_in_carrier(carrier, cls.TRACE_ID_KEY,
90-
format_trace_id(context.trace_id))
91+
format_trace_id(context.span.trace_id))
9192
set_in_carrier(carrier, cls.SPAN_ID_KEY,
92-
format_span_id(context.span_id))
93+
format_span_id(context.span.span_id))
9394
set_in_carrier(carrier, cls.SAMPLED_KEY, "1" if sampled else "0")
9495

9596

0 commit comments

Comments
 (0)