Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 8258927

Browse files
authored
Adding a working propagator, adding to integrations and example (open-telemetry#137)
Adding a full, end-to-end example of propagation at work in the example application, including a test. Adding the use of propagators into the integrations.
1 parent 2290df7 commit 8258927

File tree

9 files changed

+201
-21
lines changed

9 files changed

+201
-21
lines changed

ext/opentelemetry-ext-http-requests/src/opentelemetry/ext/http_requests/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222

2323
from requests.sessions import Session
2424

25+
from opentelemetry import propagators
2526
from opentelemetry.trace import SpanKind
2627

2728

@@ -71,6 +72,8 @@ def instrumented_request(self, method, url, *args, **kwargs):
7172
# TODO: Propagate the trace context via headers once we have a way
7273
# to access propagators.
7374

75+
headers = kwargs.setdefault("headers", {})
76+
propagators.inject(tracer, type(headers).__setitem__, headers)
7477
result = wrapped(self, method, url, *args, **kwargs) # *** PROCEED
7578

7679
span.set_attribute("http.status_code", result.status_code)

ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/__init__.py

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,10 @@
1919
"""
2020

2121
import functools
22+
import typing
2223
import wsgiref.util as wsgiref_util
2324

24-
from opentelemetry import trace
25+
from opentelemetry import propagators, trace
2526
from opentelemetry.ext.wsgi.version import __version__ # noqa
2627

2728

@@ -35,12 +36,9 @@ class OpenTelemetryMiddleware:
3536
wsgi: The WSGI application callable.
3637
"""
3738

38-
def __init__(self, wsgi, propagators=None):
39+
def __init__(self, wsgi):
3940
self.wsgi = wsgi
4041

41-
# TODO: implement context propagation
42-
self.propagators = propagators
43-
4442
@staticmethod
4543
def _add_request_attributes(span, environ):
4644
span.set_attribute("component", "http")
@@ -87,8 +85,11 @@ def __call__(self, environ, start_response):
8785

8886
tracer = trace.tracer()
8987
path_info = environ["PATH_INFO"] or "/"
88+
parent_span = propagators.extract(get_header_from_environ, environ)
9089

91-
with tracer.start_span(path_info, kind=trace.SpanKind.SERVER) as span:
90+
with tracer.start_span(
91+
path_info, parent_span, kind=trace.SpanKind.SERVER
92+
) as span:
9293
self._add_request_attributes(span, environ)
9394
start_response = self._create_start_response(span, start_response)
9495

@@ -99,3 +100,18 @@ def __call__(self, environ, start_response):
99100
finally:
100101
if hasattr(iterable, "close"):
101102
iterable.close()
103+
104+
105+
def get_header_from_environ(
106+
environ: dict, header_name: str
107+
) -> typing.List[str]:
108+
"""Retrieve the header value from the wsgi environ dictionary.
109+
110+
Returns:
111+
A string with the header value if it exists, else None.
112+
"""
113+
environ_key = "HTTP_" + header_name.upper().replace("-", "_")
114+
value = environ.get(environ_key)
115+
if value:
116+
return [value]
117+
return []

ext/opentelemetry-ext-wsgi/tests/test_wsgi_middleware.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ def validate_response(self, response, error=None):
126126

127127
# Verify that start_span has been called
128128
self.start_span.assert_called_once_with(
129-
"/", kind=trace_api.SpanKind.SERVER
129+
"/", trace_api.INVALID_SPAN_CONTEXT, kind=trace_api.SpanKind.SERVER
130130
)
131131

132132
def test_basic_wsgi_call(self):

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

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,10 @@
1717

1818
from opentelemetry.trace import SpanContext
1919

20-
Setter = typing.Callable[[object, str, str], None]
21-
Getter = typing.Callable[[object, str], typing.List[str]]
20+
_T = typing.TypeVar("_T")
21+
22+
Setter = typing.Callable[[typing.Type[_T], str, str], None]
23+
Getter = typing.Callable[[typing.Type[_T], str], typing.List[str]]
2224

2325

2426
class HTTPTextFormat(abc.ABC):
@@ -70,7 +72,7 @@ def example_route():
7072

7173
@abc.abstractmethod
7274
def extract(
73-
self, get_from_carrier: Getter, carrier: object
75+
self, get_from_carrier: Getter[_T], carrier: _T
7476
) -> SpanContext:
7577
"""Create a SpanContext from values in the carrier.
7678
@@ -93,7 +95,7 @@ def extract(
9395

9496
@abc.abstractmethod
9597
def inject(
96-
self, context: SpanContext, set_in_carrier: Setter, carrier: object
98+
self, context: SpanContext, set_in_carrier: Setter[_T], carrier: _T
9799
) -> None:
98100
"""Inject values from a SpanContext into a carrier.
99101
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
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+
16+
import typing
17+
18+
import opentelemetry.trace as trace
19+
from opentelemetry.context.propagation import httptextformat
20+
21+
_T = typing.TypeVar("_T")
22+
23+
24+
class TraceContextHTTPTextFormat(httptextformat.HTTPTextFormat):
25+
"""TODO: extracts and injects using w3c TraceContext's headers.
26+
"""
27+
28+
def extract(
29+
self, _get_from_carrier: httptextformat.Getter[_T], _carrier: _T
30+
) -> trace.SpanContext:
31+
return trace.INVALID_SPAN_CONTEXT
32+
33+
def inject(
34+
self,
35+
context: trace.SpanContext,
36+
set_in_carrier: httptextformat.Setter[_T],
37+
carrier: _T,
38+
) -> None:
39+
pass
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import typing
2+
3+
import opentelemetry.context.propagation.httptextformat as httptextformat
4+
import opentelemetry.trace as trace
5+
from opentelemetry.context.propagation.tracecontexthttptextformat import (
6+
TraceContextHTTPTextFormat,
7+
)
8+
9+
_T = typing.TypeVar("_T")
10+
11+
12+
def extract(
13+
get_from_carrier: httptextformat.Getter[_T], carrier: _T
14+
) -> trace.SpanContext:
15+
"""Load the parent SpanContext from values in the carrier.
16+
17+
Using the specified HTTPTextFormatter, the propagator will
18+
extract a SpanContext from the carrier. If one is found,
19+
it will be set as the parent context of the current span.
20+
21+
Args:
22+
get_from_carrier: a function that can retrieve zero
23+
or more values from the carrier. In the case that
24+
the value does not exist, return an empty list.
25+
carrier: and object which contains values that are
26+
used to construct a SpanContext. This object
27+
must be paired with an appropriate get_from_carrier
28+
which understands how to extract a value from it.
29+
"""
30+
return get_global_httptextformat().extract(get_from_carrier, carrier)
31+
32+
33+
def inject(
34+
tracer: trace.Tracer,
35+
set_in_carrier: httptextformat.Setter[_T],
36+
carrier: _T,
37+
) -> None:
38+
"""Inject values from the current context into the carrier.
39+
40+
inject enables the propagation of values into HTTP clients or
41+
other objects which perform an HTTP request. Implementations
42+
should use the set_in_carrier method to set values on the
43+
carrier.
44+
45+
Args:
46+
set_in_carrier: A setter function that can set values
47+
on the carrier.
48+
carrier: An object that contains a representation of HTTP
49+
headers. Should be paired with set_in_carrier, which
50+
should know how to set header values on the carrier.
51+
"""
52+
get_global_httptextformat().inject(
53+
tracer.get_current_span().get_context(), set_in_carrier, carrier
54+
)
55+
56+
57+
_HTTP_TEXT_FORMAT = (
58+
TraceContextHTTPTextFormat()
59+
) # type: httptextformat.HTTPTextFormat
60+
61+
62+
def get_global_httptextformat() -> httptextformat.HTTPTextFormat:
63+
return _HTTP_TEXT_FORMAT
64+
65+
66+
def set_global_httptextformat(
67+
http_text_format: httptextformat.HTTPTextFormat
68+
) -> None:
69+
global _HTTP_TEXT_FORMAT # pylint:disable=global-statement
70+
_HTTP_TEXT_FORMAT = http_text_format

opentelemetry-example-app/src/opentelemetry_example_app/flask_example.py

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,13 @@
1616
This module serves as an example to integrate with flask, using
1717
the requests library to perform downstream requests
1818
"""
19-
import time
20-
2119
import flask
20+
import requests
2221

2322
import opentelemetry.ext.http_requests
24-
from opentelemetry import trace
23+
from opentelemetry import propagators, trace
2524
from opentelemetry.ext.wsgi import OpenTelemetryMiddleware
25+
from opentelemetry.sdk.context.propagation.b3_format import B3Format
2626
from opentelemetry.sdk.trace import Tracer
2727

2828

@@ -39,14 +39,20 @@ def configure_opentelemetry(flask_app: flask.Flask):
3939
4040
* processors?
4141
* exporters?
42-
* propagators?
4342
"""
4443
# Start by configuring all objects required to ensure
4544
# a complete end to end workflow.
4645
# the preferred implementation of these objects must be set,
4746
# as the opentelemetry-api defines the interface with a no-op
4847
# implementation.
4948
trace.set_preferred_tracer_implementation(lambda _: Tracer())
49+
# Next, we need to configure how the values that are used by
50+
# traces and metrics are propagated (such as what specific headers
51+
# carry this value).
52+
53+
# TBD: can remove once default TraceContext propagators are installed.
54+
propagators.set_global_httptextformat(B3Format())
55+
5056
# Integrations are the glue that binds the OpenTelemetry API
5157
# and the frameworks and libraries that are used together, automatically
5258
# creating Spans and propagating context as appropriate.
@@ -61,8 +67,8 @@ def configure_opentelemetry(flask_app: flask.Flask):
6167
def hello():
6268
# emit a trace that measures how long the
6369
# sleep takes
64-
with trace.tracer().start_span("sleep"):
65-
time.sleep(0.001)
70+
with trace.tracer().start_span("example-request"):
71+
requests.get("http://www.example.com")
6672
return "hello"
6773

6874

Lines changed: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,59 @@
11
import unittest
2+
from unittest import mock
3+
4+
import requests
5+
from werkzeug.test import Client
6+
from werkzeug.wrappers import BaseResponse
27

38
import opentelemetry_example_app.flask_example as flask_example
9+
from opentelemetry.sdk import trace
10+
from opentelemetry.sdk.context.propagation import b3_format
411

512

613
class TestFlaskExample(unittest.TestCase):
714
@classmethod
815
def setUpClass(cls):
916
cls.app = flask_example.app
1017

18+
def setUp(self):
19+
mocked_response = requests.models.Response()
20+
mocked_response.status_code = 200
21+
mocked_response.reason = "Roger that!"
22+
self.send_patcher = mock.patch.object(
23+
requests.Session,
24+
"send",
25+
autospec=True,
26+
spec_set=True,
27+
return_value=mocked_response,
28+
)
29+
self.send = self.send_patcher.start()
30+
31+
def tearDown(self):
32+
self.send_patcher.stop()
33+
1134
def test_full_path(self):
12-
with self.app.test_client() as client:
13-
response = client.get("/")
14-
assert response.data.decode() == "hello"
35+
trace_id = trace.generate_trace_id()
36+
# We need to use the Werkzeug test app because
37+
# The headers are injected at the wsgi layer.
38+
# The flask test app will not include these, and
39+
# result in the values not propagated.
40+
client = Client(self.app.wsgi_app, BaseResponse)
41+
# emulate b3 headers
42+
client.get(
43+
"/",
44+
headers={
45+
"x-b3-traceid": b3_format.format_trace_id(trace_id),
46+
"x-b3-spanid": b3_format.format_span_id(
47+
trace.generate_span_id()
48+
),
49+
"x-b3-sampled": "1",
50+
},
51+
)
52+
# assert the http request header was propagated through.
53+
prepared_request = self.send.call_args[0][1]
54+
headers = prepared_request.headers
55+
for required_header in {"x-b3-traceid", "x-b3-spanid", "x-b3-sampled"}:
56+
self.assertIn(required_header, headers)
57+
self.assertEqual(
58+
headers["x-b3-traceid"], b3_format.format_trace_id(trace_id)
59+
)

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,6 @@ def extract(cls, get_from_carrier, carrier):
9191
# header is set to allow.
9292
if sampled in cls._SAMPLE_PROPAGATE_VALUES or flags == "1":
9393
options |= trace.TraceOptions.RECORDED
94-
9594
return trace.SpanContext(
9695
# trace an span ids are encoded in hex, so must be converted
9796
trace_id=int(trace_id, 16),

0 commit comments

Comments
 (0)