Skip to content

Commit f7fd1e0

Browse files
authored
Restoring metrics in requests (#1110)
1 parent 0007c90 commit f7fd1e0

File tree

5 files changed

+110
-4
lines changed

5 files changed

+110
-4
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
3636
([#1127](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1127))
3737
- Add metric instrumentation for WSGI
3838
([#1128](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1128))
39+
- `opentelemetry-instrumentation-requests` Restoring metrics in requests
40+
([#1110](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1110)
3941

4042

4143
## [1.12.0rc1-0.31b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.12.0rc1-0.31b0) - 2022-05-17

instrumentation/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
| [opentelemetry-instrumentation-pyramid](./opentelemetry-instrumentation-pyramid) | pyramid >= 1.7 | No
3232
| [opentelemetry-instrumentation-redis](./opentelemetry-instrumentation-redis) | redis >= 2.6 | No
3333
| [opentelemetry-instrumentation-remoulade](./opentelemetry-instrumentation-remoulade) | remoulade >= 0.50 | No
34-
| [opentelemetry-instrumentation-requests](./opentelemetry-instrumentation-requests) | requests ~= 2.0 | No
34+
| [opentelemetry-instrumentation-requests](./opentelemetry-instrumentation-requests) | requests ~= 2.0 | Yes
3535
| [opentelemetry-instrumentation-sklearn](./opentelemetry-instrumentation-sklearn) | scikit-learn ~= 0.24.0 | No
3636
| [opentelemetry-instrumentation-sqlalchemy](./opentelemetry-instrumentation-sqlalchemy) | sqlalchemy | No
3737
| [opentelemetry-instrumentation-sqlite3](./opentelemetry-instrumentation-sqlite3) | sqlite3 | No

instrumentation/opentelemetry-instrumentation-requests/src/opentelemetry/instrumentation/requests/__init__.py

Lines changed: 61 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,9 @@
5050

5151
import functools
5252
import types
53-
from typing import Collection
53+
from timeit import default_timer
54+
from typing import Callable, Collection, Iterable, Optional
55+
from urllib.parse import urlparse
5456

5557
from requests.models import Response
5658
from requests.sessions import Session
@@ -67,9 +69,11 @@
6769
_SUPPRESS_INSTRUMENTATION_KEY,
6870
http_status_to_status_code,
6971
)
72+
from opentelemetry.metrics import Histogram, get_meter
7073
from opentelemetry.propagate import inject
7174
from opentelemetry.semconv.trace import SpanAttributes
72-
from opentelemetry.trace import SpanKind, get_tracer
75+
from opentelemetry.trace import SpanKind, Tracer, get_tracer
76+
from opentelemetry.trace.span import Span
7377
from opentelemetry.trace.status import Status
7478
from opentelemetry.util.http import (
7579
get_excluded_urls,
@@ -84,7 +88,11 @@
8488
# pylint: disable=unused-argument
8589
# pylint: disable=R0915
8690
def _instrument(
87-
tracer, span_callback=None, name_callback=None, excluded_urls=None
91+
tracer: Tracer,
92+
duration_histogram: Histogram,
93+
span_callback: Optional[Callable[[Span, Response], str]] = None,
94+
name_callback: Optional[Callable[[str, str], str]] = None,
95+
excluded_urls: Iterable[str] = None,
8896
):
8997
"""Enables tracing of all requests calls that go through
9098
:code:`requests.session.Session.request` (this includes
@@ -140,6 +148,7 @@ def call_wrapped():
140148
request.method, request.url, call_wrapped, get_or_create_headers
141149
)
142150

151+
# pylint: disable-msg=too-many-locals,too-many-branches
143152
def _instrumented_requests_call(
144153
method: str, url: str, call_wrapped, get_or_create_headers
145154
):
@@ -164,6 +173,23 @@ def _instrumented_requests_call(
164173
SpanAttributes.HTTP_URL: url,
165174
}
166175

176+
metric_labels = {
177+
SpanAttributes.HTTP_METHOD: method,
178+
}
179+
180+
try:
181+
parsed_url = urlparse(url)
182+
metric_labels[SpanAttributes.HTTP_SCHEME] = parsed_url.scheme
183+
if parsed_url.hostname:
184+
metric_labels[SpanAttributes.HTTP_HOST] = parsed_url.hostname
185+
metric_labels[
186+
SpanAttributes.NET_PEER_NAME
187+
] = parsed_url.hostname
188+
if parsed_url.port:
189+
metric_labels[SpanAttributes.NET_PEER_PORT] = parsed_url.port
190+
except ValueError:
191+
pass
192+
167193
with tracer.start_as_current_span(
168194
span_name, kind=SpanKind.CLIENT, attributes=span_attributes
169195
) as span, set_ip_on_next_http_connection(span):
@@ -175,12 +201,18 @@ def _instrumented_requests_call(
175201
token = context.attach(
176202
context.set_value(_SUPPRESS_HTTP_INSTRUMENTATION_KEY, True)
177203
)
204+
205+
start_time = default_timer()
206+
178207
try:
179208
result = call_wrapped() # *** PROCEED
180209
except Exception as exc: # pylint: disable=W0703
181210
exception = exc
182211
result = getattr(exc, "response", None)
183212
finally:
213+
elapsed_time = max(
214+
round((default_timer() - start_time) * 1000), 0
215+
)
184216
context.detach(token)
185217

186218
if isinstance(result, Response):
@@ -191,9 +223,23 @@ def _instrumented_requests_call(
191223
span.set_status(
192224
Status(http_status_to_status_code(result.status_code))
193225
)
226+
227+
metric_labels[
228+
SpanAttributes.HTTP_STATUS_CODE
229+
] = result.status_code
230+
231+
if result.raw is not None:
232+
version = getattr(result.raw, "version", None)
233+
if version:
234+
metric_labels[SpanAttributes.HTTP_FLAVOR] = (
235+
"1.1" if version == 11 else "1.0"
236+
)
237+
194238
if span_callback is not None:
195239
span_callback(span, result)
196240

241+
duration_histogram.record(elapsed_time, attributes=metric_labels)
242+
197243
if exception is not None:
198244
raise exception.with_traceback(exception.__traceback__)
199245

@@ -258,8 +304,20 @@ def _instrument(self, **kwargs):
258304
tracer_provider = kwargs.get("tracer_provider")
259305
tracer = get_tracer(__name__, __version__, tracer_provider)
260306
excluded_urls = kwargs.get("excluded_urls")
307+
meter_provider = kwargs.get("meter_provider")
308+
meter = get_meter(
309+
__name__,
310+
__version__,
311+
meter_provider,
312+
)
313+
duration_histogram = meter.create_histogram(
314+
name="http.client.duration",
315+
unit="ms",
316+
description="measures the duration of the outbound HTTP request",
317+
)
261318
_instrument(
262319
tracer,
320+
duration_histogram,
263321
span_callback=kwargs.get("span_callback"),
264322
name_callback=kwargs.get("name_callback"),
265323
excluded_urls=_excluded_urls_from_env

instrumentation/opentelemetry-instrumentation-requests/src/opentelemetry/instrumentation/requests/package.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,5 @@
1414

1515

1616
_instruments = ("requests ~= 2.0",)
17+
18+
_supports_metrics = True

instrumentation/opentelemetry-instrumentation-requests/tests/test_requests_integration.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -487,3 +487,47 @@ def perform_request(url: str, session: requests.Session = None):
487487
request = requests.Request("GET", url)
488488
prepared_request = session.prepare_request(request)
489489
return session.send(prepared_request)
490+
491+
492+
class TestRequestsIntergrationMetric(TestBase):
493+
URL = "http://examplehost:8000/status/200"
494+
495+
def setUp(self):
496+
super().setUp()
497+
RequestsInstrumentor().instrument(meter_provider=self.meter_provider)
498+
499+
httpretty.enable()
500+
httpretty.register_uri(httpretty.GET, self.URL, body="Hello!")
501+
502+
def tearDown(self):
503+
super().tearDown()
504+
RequestsInstrumentor().uninstrument()
505+
httpretty.disable()
506+
507+
@staticmethod
508+
def perform_request(url: str) -> requests.Response:
509+
return requests.get(url)
510+
511+
def test_basic_metric_success(self):
512+
self.perform_request(self.URL)
513+
514+
expected_attributes = {
515+
"http.status_code": 200,
516+
"http.host": "examplehost",
517+
"net.peer.port": 8000,
518+
"net.peer.name": "examplehost",
519+
"http.method": "GET",
520+
"http.flavor": "1.1",
521+
"http.scheme": "http",
522+
}
523+
524+
for (
525+
resource_metrics
526+
) in self.memory_metrics_reader.get_metrics_data().resource_metrics:
527+
for scope_metrics in resource_metrics.scope_metrics:
528+
for metric in scope_metrics.metrics:
529+
for data_point in metric.data.data_points:
530+
self.assertDictEqual(
531+
expected_attributes, dict(data_point.attributes)
532+
)
533+
self.assertEqual(data_point.count, 1)

0 commit comments

Comments
 (0)