Skip to content

Commit 872975b

Browse files
authored
Adding metric collection as part of instrumentations - Requests (open-telemetry#1116)
1 parent c1ec444 commit 872975b

File tree

11 files changed

+353
-36
lines changed

11 files changed

+353
-36
lines changed

docs/examples/basic_meter/http.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# Copyright The 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+
This module shows how you can enable collection and exporting of http metrics
17+
related to instrumentations.
18+
"""
19+
import requests
20+
21+
from opentelemetry import metrics
22+
from opentelemetry.instrumentation.requests import RequestsInstrumentor
23+
from opentelemetry.sdk.metrics import MeterProvider
24+
from opentelemetry.sdk.metrics.export import ConsoleMetricsExporter
25+
26+
# Sets the global MeterProvider instance
27+
metrics.set_meter_provider(MeterProvider())
28+
29+
# Exporter to export metrics to the console
30+
exporter = ConsoleMetricsExporter()
31+
32+
# Instrument the requests library
33+
RequestsInstrumentor().instrument()
34+
35+
# Indicate to start collecting and exporting requests related metrics
36+
metrics.get_meter_provider().start_pipeline(
37+
RequestsInstrumentor().meter, exporter, 5
38+
)
39+
40+
response = requests.get("http://example.com")
41+
42+
input("...\n")

docs/instrumentation/instrumentation.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,4 @@ Submodules
1313
:maxdepth: 1
1414

1515
instrumentor
16+
metric

docs/instrumentation/metric.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
opentelemetry.instrumentation.metric package
2+
============================================
3+
4+
.. automodule:: opentelemetry.instrumentation.metric
5+
:members:
6+
:undoc-members:
7+
:show-inheritance:

instrumentation/opentelemetry-instrumentation-requests/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ Released 2020-09-17
1010
([#1040](https://github.com/open-telemetry/opentelemetry-python/pull/1040))
1111
- Drop support for Python 3.4
1212
([#1099](https://github.com/open-telemetry/opentelemetry-python/pull/1099))
13+
- Add support for http metrics
14+
([#1116](https://github.com/open-telemetry/opentelemetry-python/pull/1116))
1315

1416
## Version 0.12b0
1517

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

Lines changed: 68 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
import opentelemetry.instrumentation.requests
2626
2727
# You can optionally pass a custom TracerProvider to
28-
RequestInstrumentor.instrument()
28+
# RequestInstrumentor.instrument()
2929
opentelemetry.instrumentation.requests.RequestsInstrumentor().instrument()
3030
response = requests.get(url="https://www.example.org/")
3131
@@ -43,6 +43,10 @@
4343

4444
from opentelemetry import context, propagators
4545
from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
46+
from opentelemetry.instrumentation.metric import (
47+
HTTPMetricRecorder,
48+
MetricMixin,
49+
)
4650
from opentelemetry.instrumentation.requests.version import __version__
4751
from opentelemetry.instrumentation.utils import http_status_to_canonical_code
4852
from opentelemetry.trace import SpanKind, get_tracer
@@ -54,6 +58,7 @@
5458

5559

5660
# pylint: disable=unused-argument
61+
# pylint: disable=R0915
5762
def _instrument(tracer_provider=None, span_callback=None):
5863
"""Enables tracing of all requests calls that go through
5964
:code:`requests.session.Session.request` (this includes
@@ -118,43 +123,66 @@ def _instrumented_requests_call(
118123

119124
exception = None
120125

126+
recorder = RequestsInstrumentor().metric_recorder
127+
128+
labels = {}
129+
labels["http.method"] = method
130+
labels["http.url"] = url
131+
121132
with get_tracer(
122133
__name__, __version__, tracer_provider
123134
).start_as_current_span(span_name, kind=SpanKind.CLIENT) as span:
124-
if span.is_recording():
125-
span.set_attribute("component", "http")
126-
span.set_attribute("http.method", method.upper())
127-
span.set_attribute("http.url", url)
128-
129-
headers = get_or_create_headers()
130-
propagators.inject(type(headers).__setitem__, headers)
131-
132-
token = context.attach(
133-
context.set_value(_SUPPRESS_REQUESTS_INSTRUMENTATION_KEY, True)
134-
)
135-
try:
136-
result = call_wrapped() # *** PROCEED
137-
except Exception as exc: # pylint: disable=W0703
138-
exception = exc
139-
result = getattr(exc, "response", None)
140-
finally:
141-
context.detach(token)
142-
143-
if exception is not None and span.is_recording():
144-
span.set_status(
145-
Status(_exception_to_canonical_code(exception))
135+
with recorder.record_duration(labels):
136+
if span.is_recording():
137+
span.set_attribute("component", "http")
138+
span.set_attribute("http.method", method)
139+
span.set_attribute("http.url", url)
140+
141+
headers = get_or_create_headers()
142+
propagators.inject(type(headers).__setitem__, headers)
143+
144+
token = context.attach(
145+
context.set_value(
146+
_SUPPRESS_REQUESTS_INSTRUMENTATION_KEY, True
147+
)
146148
)
147-
span.record_exception(exception)
148-
149-
if result is not None and span.is_recording():
150-
span.set_attribute("http.status_code", result.status_code)
151-
span.set_attribute("http.status_text", result.reason)
152-
span.set_status(
153-
Status(http_status_to_canonical_code(result.status_code))
154-
)
155-
156-
if span_callback is not None:
157-
span_callback(span, result)
149+
try:
150+
result = call_wrapped() # *** PROCEED
151+
except Exception as exc: # pylint: disable=W0703
152+
exception = exc
153+
result = getattr(exc, "response", None)
154+
finally:
155+
context.detach(token)
156+
157+
if exception is not None and span.is_recording():
158+
span.set_status(
159+
Status(_exception_to_canonical_code(exception))
160+
)
161+
span.record_exception(exception)
162+
163+
if result is not None:
164+
if span.is_recording():
165+
span.set_attribute(
166+
"http.status_code", result.status_code
167+
)
168+
span.set_attribute("http.status_text", result.reason)
169+
span.set_status(
170+
Status(
171+
http_status_to_canonical_code(
172+
result.status_code
173+
)
174+
)
175+
)
176+
labels["http.status_code"] = str(result.status_code)
177+
labels["http.status_text"] = result.reason
178+
if result.raw and result.raw.version:
179+
labels["http.flavor"] = (
180+
str(result.raw.version)[:1]
181+
+ "."
182+
+ str(result.raw.version)[:-1]
183+
)
184+
if span_callback is not None:
185+
span_callback(span, result)
158186

159187
if exception is not None:
160188
raise exception.with_traceback(exception.__traceback__)
@@ -202,7 +230,7 @@ def _exception_to_canonical_code(exc: Exception) -> StatusCanonicalCode:
202230
return StatusCanonicalCode.UNKNOWN
203231

204232

205-
class RequestsInstrumentor(BaseInstrumentor):
233+
class RequestsInstrumentor(BaseInstrumentor, MetricMixin):
206234
"""An instrumentor for requests
207235
See `BaseInstrumentor`
208236
"""
@@ -219,6 +247,11 @@ def _instrument(self, **kwargs):
219247
tracer_provider=kwargs.get("tracer_provider"),
220248
span_callback=kwargs.get("span_callback"),
221249
)
250+
self.init_metrics(
251+
__name__, __version__,
252+
)
253+
# pylint: disable=W0201
254+
self.metric_recorder = HTTPMetricRecorder(self.meter, SpanKind.CLIENT)
222255

223256
def _uninstrument(self, **kwargs):
224257
_uninstrument()

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

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
from opentelemetry import context, propagators, trace
2323
from opentelemetry.instrumentation.requests import RequestsInstrumentor
2424
from opentelemetry.sdk import resources
25+
from opentelemetry.sdk.util import get_dict_as_key
2526
from opentelemetry.test.mock_textmap import MockTextMapPropagator
2627
from opentelemetry.test.test_base import TestBase
2728
from opentelemetry.trace.status import StatusCanonicalCode
@@ -88,6 +89,27 @@ def test_basic(self):
8889
span, opentelemetry.instrumentation.requests
8990
)
9091

92+
self.assertIsNotNone(RequestsInstrumentor().meter)
93+
self.assertEqual(len(RequestsInstrumentor().meter.metrics), 1)
94+
recorder = RequestsInstrumentor().meter.metrics.pop()
95+
match_key = get_dict_as_key(
96+
{
97+
"http.flavor": "1.1",
98+
"http.method": "GET",
99+
"http.status_code": "200",
100+
"http.status_text": "OK",
101+
"http.url": "http://httpbin.org/status/200",
102+
}
103+
)
104+
for key in recorder.bound_instruments.keys():
105+
self.assertEqual(key, match_key)
106+
# pylint: disable=protected-access
107+
bound = recorder.bound_instruments.get(key)
108+
for view_data in bound.view_datas:
109+
self.assertEqual(view_data.labels, key)
110+
self.assertEqual(view_data.aggregator.current.count, 1)
111+
self.assertGreater(view_data.aggregator.current.sum, 0)
112+
91113
def test_not_foundbasic(self):
92114
url_404 = "http://httpbin.org/status/404"
93115
httpretty.register_uri(
@@ -246,6 +268,23 @@ def test_requests_exception_without_response(self, *_, **__):
246268
span.status.canonical_code, StatusCanonicalCode.UNKNOWN
247269
)
248270

271+
self.assertIsNotNone(RequestsInstrumentor().meter)
272+
self.assertEqual(len(RequestsInstrumentor().meter.metrics), 1)
273+
recorder = RequestsInstrumentor().meter.metrics.pop()
274+
match_key = get_dict_as_key(
275+
{
276+
"http.method": "GET",
277+
"http.url": "http://httpbin.org/status/200",
278+
}
279+
)
280+
for key in recorder.bound_instruments.keys():
281+
self.assertEqual(key, match_key)
282+
# pylint: disable=protected-access
283+
bound = recorder.bound_instruments.get(key)
284+
for view_data in bound.view_datas:
285+
self.assertEqual(view_data.labels, key)
286+
self.assertEqual(view_data.aggregator.current.count, 1)
287+
249288
mocked_response = requests.Response()
250289
mocked_response.status_code = 500
251290
mocked_response.reason = "Internal Server Error"
@@ -272,6 +311,24 @@ def test_requests_exception_with_response(self, *_, **__):
272311
self.assertEqual(
273312
span.status.canonical_code, StatusCanonicalCode.INTERNAL
274313
)
314+
self.assertIsNotNone(RequestsInstrumentor().meter)
315+
self.assertEqual(len(RequestsInstrumentor().meter.metrics), 1)
316+
recorder = RequestsInstrumentor().meter.metrics.pop()
317+
match_key = get_dict_as_key(
318+
{
319+
"http.method": "GET",
320+
"http.status_code": "500",
321+
"http.status_text": "Internal Server Error",
322+
"http.url": "http://httpbin.org/status/200",
323+
}
324+
)
325+
for key in recorder.bound_instruments.keys():
326+
self.assertEqual(key, match_key)
327+
# pylint: disable=protected-access
328+
bound = recorder.bound_instruments.get(key)
329+
for view_data in bound.view_datas:
330+
self.assertEqual(view_data.labels, key)
331+
self.assertEqual(view_data.aggregator.current.count, 1)
275332

276333
@mock.patch("requests.adapters.HTTPAdapter.send", side_effect=Exception)
277334
def test_requests_basic_exception(self, *_, **__):

opentelemetry-instrumentation/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ Released 2020-09-17
1010

1111
- Drop support for Python 3.4
1212
([#1099](https://github.com/open-telemetry/opentelemetry-python/pull/1099))
13+
- Add support for http metrics
14+
([#1116](https://github.com/open-telemetry/opentelemetry-python/pull/1116))
1315

1416
## 0.9b0
1517

opentelemetry-instrumentation/setup.cfg

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ zip_safe = False
4242
include_package_data = True
4343
install_requires =
4444
opentelemetry-api == 0.14.dev0
45+
opentelemetry-sdk == 0.14.dev0
4546
wrapt >= 1.0.0, < 2.0.0
4647

4748
[options.packages.find]

0 commit comments

Comments
 (0)