Skip to content

Commit 8e3ed35

Browse files
ext/requests: Add instrumentor (#597)
Implement the BaseInstrumentor interface to make this library compatible with the opentelemetry-auto-instr command. There is an issue about getting the span when the global tracer provider hasn't been configured, this should be changed in the future once we extend the opentelemetry-auto-instr command to also configure the SDK.
1 parent 5d675ee commit 8e3ed35

File tree

10 files changed

+155
-50
lines changed

10 files changed

+155
-50
lines changed

docs/examples/http/client.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,15 +28,18 @@
2828

2929
# The preferred tracer implementation must be set, as the opentelemetry-api
3030
# defines the interface with a no-op implementation.
31+
# It must be done before instrumenting any library.
3132
trace.set_tracer_provider(TracerProvider())
32-
tracer_provider = trace.get_tracer_provider()
3333

34+
# Enable instrumentation in the requests library.
35+
http_requests.RequestsInstrumentor().instrument()
36+
37+
# Configure a console span exporter.
3438
exporter = ConsoleSpanExporter()
3539
span_processor = BatchExportSpanProcessor(exporter)
36-
tracer_provider.add_span_processor(span_processor)
40+
trace.get_tracer_provider().add_span_processor(span_processor)
3741

3842
# Integrations are the glue that binds the OpenTelemetry API and the
3943
# frameworks and libraries that are used together, automatically creating
4044
# Spans and propagating context as appropriate.
41-
http_requests.enable(tracer_provider)
4245
response = requests.get(url="http://127.0.0.1:5000/")

docs/examples/http/server.py

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,20 +30,23 @@
3030

3131
# The preferred tracer implementation must be set, as the opentelemetry-api
3232
# defines the interface with a no-op implementation.
33+
# It must be done before instrumenting any library.
3334
trace.set_tracer_provider(TracerProvider())
34-
tracer = trace.get_tracer(__name__)
35-
36-
exporter = ConsoleSpanExporter()
37-
span_processor = BatchExportSpanProcessor(exporter)
38-
trace.get_tracer_provider().add_span_processor(span_processor)
3935

4036
# Integrations are the glue that binds the OpenTelemetry API and the
4137
# frameworks and libraries that are used together, automatically creating
4238
# Spans and propagating context as appropriate.
43-
http_requests.enable(trace.get_tracer_provider())
39+
http_requests.RequestsInstrumentor().instrument()
4440
app = flask.Flask(__name__)
4541
app.wsgi_app = OpenTelemetryMiddleware(app.wsgi_app)
4642

43+
# Configure a console span exporter.
44+
exporter = ConsoleSpanExporter()
45+
span_processor = BatchExportSpanProcessor(exporter)
46+
trace.get_tracer_provider().add_span_processor(span_processor)
47+
48+
tracer = trace.get_tracer(__name__)
49+
4750

4851
@app.route("/")
4952
def hello():

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

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,14 +28,20 @@
2828
SimpleExportSpanProcessor,
2929
)
3030

31+
# The preferred tracer implementation must be set, as the opentelemetry-api
32+
# defines the interface with a no-op implementation.
33+
# It must be done before instrumenting any library
3134
trace.set_tracer_provider(TracerProvider())
35+
36+
opentelemetry.ext.http_requests.RequestsInstrumentor().instrument()
37+
FlaskInstrumentor().instrument()
38+
3239
trace.get_tracer_provider().add_span_processor(
3340
SimpleExportSpanProcessor(ConsoleSpanExporter())
3441
)
3542

36-
FlaskInstrumentor().instrument()
43+
3744
app = flask.Flask(__name__)
38-
opentelemetry.ext.http_requests.enable(trace.get_tracer_provider())
3945

4046

4147
@app.route("/")

docs/getting-started.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,7 @@ And let's write a small Flask application that sends an HTTP request, activating
202202
)
203203
204204
app = flask.Flask(__name__)
205-
opentelemetry.ext.http_requests.enable(trace.get_tracer_provider())
205+
opentelemetry.ext.http_requests.RequestsInstrumentor().instrument()
206206
207207
@app.route("/")
208208
def hello():

ext/opentelemetry-ext-http-requests/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
## Unreleased
44

5+
- Implement instrumentor interface ([#597](https://github.com/open-telemetry/opentelemetry-python/pull/597))
6+
57
## 0.3a0
68

79
Released 2019-10-29

ext/opentelemetry-ext-http-requests/setup.cfg

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ package_dir=
4141
packages=find_namespace:
4242
install_requires =
4343
opentelemetry-api == 0.7.dev0
44+
opentelemetry-auto-instrumentation == 0.7.dev0
4445
requests ~= 2.0
4546

4647
[options.extras_require]
@@ -50,3 +51,7 @@ test =
5051

5152
[options.packages.find]
5253
where = src
54+
55+
[options.entry_points]
56+
opentelemetry_instrumentor =
57+
requests = opentelemetry.ext.http_requests:RequestsInstrumentor

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

Lines changed: 64 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414

1515
"""
1616
This library allows tracing HTTP requests made by the
17-
`requests <https://requests.kennethreitz.org/en/master/>`_ library.
17+
`requests <https://requests.readthedocs.io/en/master/>`_ library.
1818
1919
Usage
2020
-----
@@ -23,10 +23,10 @@
2323
2424
import requests
2525
import opentelemetry.ext.http_requests
26-
from opentelemetry.trace import TracerProvider
2726
28-
opentelemetry.ext.http_requests.enable(TracerProvider())
29-
response = requests.get(url='https://www.example.org/')
27+
# You can optionally pass a custom TracerProvider to RequestInstrumentor.instrument()
28+
opentelemetry.ext.http_requests.RequestInstrumentor.instrument()
29+
response = requests.get(url="https://www.example.org/")
3030
3131
Limitations
3232
-----------
@@ -47,17 +47,15 @@
4747

4848
from requests.sessions import Session
4949

50-
from opentelemetry import context, propagators
50+
from opentelemetry import context, propagators, trace
51+
from opentelemetry.auto_instrumentation.instrumentor import BaseInstrumentor
5152
from opentelemetry.ext.http_requests.version import __version__
52-
from opentelemetry.trace import SpanKind
53+
from opentelemetry.trace import SpanKind, get_tracer
54+
from opentelemetry.trace.status import Status, StatusCanonicalCode
5355

5456

55-
# NOTE: Currently we force passing a tracer. But in turn, this forces the user
56-
# to configure a SDK before enabling this integration. In turn, this means that
57-
# if the SDK/tracer is already using `requests` they may, in theory, bypass our
58-
# instrumentation when using `import from`, etc. (currently we only instrument
59-
# a instance method so the probability for that is very low).
60-
def enable(tracer_provider):
57+
# pylint: disable=unused-argument
58+
def _instrument(tracer_provider=None):
6159
"""Enables tracing of all requests calls that go through
6260
:code:`requests.session.Session.request` (this includes
6361
:code:`requests.get`, etc.)."""
@@ -69,20 +67,17 @@ def enable(tracer_provider):
6967
# before v1.0.0, Dec 17, 2012, see
7068
# https://github.com/psf/requests/commit/4e5c4a6ab7bb0195dececdd19bb8505b872fe120)
7169

72-
# Guard against double instrumentation
73-
disable()
74-
75-
tracer = tracer_provider.get_tracer(__name__, __version__)
76-
7770
wrapped = Session.request
7871

72+
tracer = trace.get_tracer(__name__, __version__, tracer_provider)
73+
7974
@functools.wraps(wrapped)
8075
def instrumented_request(self, method, url, *args, **kwargs):
8176
if context.get_value("suppress_instrumentation"):
8277
return wrapped(self, method, url, *args, **kwargs)
8378

8479
# See
85-
# https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/data-semantic-conventions.md#http-client
80+
# https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/trace/semantic_conventions/http.md#http-client
8681
try:
8782
parsed_url = urlparse(url)
8883
except ValueError as exc: # Invalid URL
@@ -103,12 +98,12 @@ def instrumented_request(self, method, url, *args, **kwargs):
10398

10499
span.set_attribute("http.status_code", result.status_code)
105100
span.set_attribute("http.status_text", result.reason)
101+
span.set_status(
102+
Status(_http_status_to_canonical_code(result.status_code))
103+
)
106104

107105
return result
108106

109-
# TODO: How to handle exceptions? Should we create events for them? Set
110-
# certain attributes?
111-
112107
instrumented_request.opentelemetry_ext_requests_applied = True
113108

114109
Session.request = instrumented_request
@@ -119,18 +114,59 @@ def instrumented_request(self, method, url, *args, **kwargs):
119114
# different, then push the current URL, pop it afterwards)
120115

121116

122-
def disable():
117+
def _uninstrument():
118+
# pylint: disable=global-statement
123119
"""Disables instrumentation of :code:`requests` through this module.
124120
125121
Note that this only works if no other module also patches requests."""
126-
127122
if getattr(Session.request, "opentelemetry_ext_requests_applied", False):
128123
original = Session.request.__wrapped__ # pylint:disable=no-member
129124
Session.request = original
130125

131126

132-
def disable_session(session):
133-
"""Disables instrumentation on the session object."""
134-
if getattr(session.request, "opentelemetry_ext_requests_applied", False):
135-
original = session.request.__wrapped__ # pylint:disable=no-member
136-
session.request = types.MethodType(original, session)
127+
def _http_status_to_canonical_code(code: int, allow_redirect: bool = True):
128+
# pylint:disable=too-many-branches,too-many-return-statements
129+
if code < 100:
130+
return StatusCanonicalCode.UNKNOWN
131+
if code <= 299:
132+
return StatusCanonicalCode.OK
133+
if code <= 399:
134+
if allow_redirect:
135+
return StatusCanonicalCode.OK
136+
return StatusCanonicalCode.DEADLINE_EXCEEDED
137+
if code <= 499:
138+
if code == 401: # HTTPStatus.UNAUTHORIZED:
139+
return StatusCanonicalCode.UNAUTHENTICATED
140+
if code == 403: # HTTPStatus.FORBIDDEN:
141+
return StatusCanonicalCode.PERMISSION_DENIED
142+
if code == 404: # HTTPStatus.NOT_FOUND:
143+
return StatusCanonicalCode.NOT_FOUND
144+
if code == 429: # HTTPStatus.TOO_MANY_REQUESTS:
145+
return StatusCanonicalCode.RESOURCE_EXHAUSTED
146+
return StatusCanonicalCode.INVALID_ARGUMENT
147+
if code <= 599:
148+
if code == 501: # HTTPStatus.NOT_IMPLEMENTED:
149+
return StatusCanonicalCode.UNIMPLEMENTED
150+
if code == 503: # HTTPStatus.SERVICE_UNAVAILABLE:
151+
return StatusCanonicalCode.UNAVAILABLE
152+
if code == 504: # HTTPStatus.GATEWAY_TIMEOUT:
153+
return StatusCanonicalCode.DEADLINE_EXCEEDED
154+
return StatusCanonicalCode.INTERNAL
155+
return StatusCanonicalCode.UNKNOWN
156+
157+
158+
class RequestsInstrumentor(BaseInstrumentor):
159+
def _instrument(self, **kwargs):
160+
_instrument(tracer_provider=kwargs.get("tracer_provider"))
161+
162+
def _uninstrument(self, **kwargs):
163+
_uninstrument()
164+
165+
@staticmethod
166+
def uninstrument_session(session):
167+
"""Disables instrumentation on the session object."""
168+
if getattr(
169+
session.request, "opentelemetry_ext_requests_applied", False
170+
):
171+
original = session.request.__wrapped__ # pylint:disable=no-member
172+
session.request = types.MethodType(original, session)

ext/opentelemetry-ext-http-requests/tests/test_requests_integration.py

Lines changed: 56 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,9 @@
1818
import requests
1919
import urllib3
2020

21-
import opentelemetry.ext.http_requests
2221
from opentelemetry import context, propagators, trace
22+
from opentelemetry.ext import http_requests
23+
from opentelemetry.sdk import resources
2324
from opentelemetry.test.mock_httptextformat import MockHTTPTextFormat
2425
from opentelemetry.test.test_base import TestBase
2526

@@ -29,23 +30,25 @@ class TestRequestsIntegration(TestBase):
2930

3031
def setUp(self):
3132
super().setUp()
32-
opentelemetry.ext.http_requests.enable(self.tracer_provider)
33+
http_requests.RequestsInstrumentor().instrument()
3334
httpretty.enable()
3435
httpretty.register_uri(
3536
httpretty.GET, self.URL, body="Hello!",
3637
)
3738

3839
def tearDown(self):
3940
super().tearDown()
40-
opentelemetry.ext.http_requests.disable()
41+
http_requests.RequestsInstrumentor().uninstrument()
4142
httpretty.disable()
4243

4344
def test_basic(self):
4445
result = requests.get(self.URL)
4546
self.assertEqual(result.text, "Hello!")
47+
4648
span_list = self.memory_exporter.get_finished_spans()
4749
self.assertEqual(len(span_list), 1)
4850
span = span_list[0]
51+
4952
self.assertIs(span.kind, trace.SpanKind.CLIENT)
5053
self.assertEqual(span.name, "/status/200")
5154

@@ -60,6 +63,32 @@ def test_basic(self):
6063
},
6164
)
6265

66+
self.assertIs(
67+
span.status.canonical_code, trace.status.StatusCanonicalCode.OK
68+
)
69+
70+
self.check_span_instrumentation_info(span, http_requests)
71+
72+
def test_not_foundbasic(self):
73+
url_404 = "http://httpbin.org/status/404"
74+
httpretty.register_uri(
75+
httpretty.GET, url_404, status=404,
76+
)
77+
result = requests.get(url_404)
78+
self.assertEqual(result.status_code, 404)
79+
80+
span_list = self.memory_exporter.get_finished_spans()
81+
self.assertEqual(len(span_list), 1)
82+
span = span_list[0]
83+
84+
self.assertEqual(span.attributes.get("http.status_code"), 404)
85+
self.assertEqual(span.attributes.get("http.status_text"), "Not Found")
86+
87+
self.assertIs(
88+
span.status.canonical_code,
89+
trace.status.StatusCanonicalCode.NOT_FOUND,
90+
)
91+
6392
def test_invalid_url(self):
6493
url = "http://[::1/nope"
6594
exception_type = requests.exceptions.InvalidURL
@@ -81,18 +110,18 @@ def test_invalid_url(self):
81110
{"component": "http", "http.method": "POST", "http.url": url},
82111
)
83112

84-
def test_disable(self):
85-
opentelemetry.ext.http_requests.disable()
113+
def test_uninstrument(self):
114+
http_requests.RequestsInstrumentor().uninstrument()
86115
result = requests.get(self.URL)
87116
self.assertEqual(result.text, "Hello!")
88117
span_list = self.memory_exporter.get_finished_spans()
89118
self.assertEqual(len(span_list), 0)
119+
# instrument again to avoid annoying warning message
120+
http_requests.RequestsInstrumentor().instrument()
90121

91-
opentelemetry.ext.http_requests.disable()
92-
93-
def test_disable_session(self):
122+
def test_uninstrument_session(self):
94123
session1 = requests.Session()
95-
opentelemetry.ext.http_requests.disable_session(session1)
124+
http_requests.RequestsInstrumentor().uninstrument_session(session1)
96125

97126
result = session1.get(self.URL)
98127
self.assertEqual(result.text, "Hello!")
@@ -152,3 +181,21 @@ def test_distributed_context(self):
152181

153182
finally:
154183
propagators.set_global_httptextformat(previous_propagator)
184+
185+
def test_custom_tracer_provider(self):
186+
resource = resources.Resource.create({})
187+
result = self.create_tracer_provider(resource=resource)
188+
tracer_provider, exporter = result
189+
http_requests.RequestsInstrumentor().uninstrument()
190+
http_requests.RequestsInstrumentor().instrument(
191+
tracer_provider=tracer_provider
192+
)
193+
194+
result = requests.get(self.URL)
195+
self.assertEqual(result.text, "Hello!")
196+
197+
span_list = exporter.get_finished_spans()
198+
self.assertEqual(len(span_list), 1)
199+
span = span_list[0]
200+
201+
self.assertIs(span.resource, resource)

tests/w3c_tracecontext_validation_server.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@
4040
# frameworks and libraries that are used together, automatically creating
4141
# Spans and propagating context as appropriate.
4242
trace.set_tracer_provider(TracerProvider())
43-
http_requests.enable(trace.get_tracer_provider())
43+
http_requests.RequestsInstrumentor().instrument()
4444

4545
# SpanExporter receives the spans and send them to the target location.
4646
span_processor = SimpleExportSpanProcessor(ConsoleSpanExporter())

0 commit comments

Comments
 (0)