Skip to content

Commit

Permalink
Adding metric collection as part of instrumentations - Requests (#1116)
Browse files Browse the repository at this point in the history
  • Loading branch information
lzchen authored Sep 25, 2020
1 parent c1ec444 commit 872975b
Show file tree
Hide file tree
Showing 11 changed files with 353 additions and 36 deletions.
42 changes: 42 additions & 0 deletions docs/examples/basic_meter/http.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Copyright The OpenTelemetry Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
"""
This module shows how you can enable collection and exporting of http metrics
related to instrumentations.
"""
import requests

from opentelemetry import metrics
from opentelemetry.instrumentation.requests import RequestsInstrumentor
from opentelemetry.sdk.metrics import MeterProvider
from opentelemetry.sdk.metrics.export import ConsoleMetricsExporter

# Sets the global MeterProvider instance
metrics.set_meter_provider(MeterProvider())

# Exporter to export metrics to the console
exporter = ConsoleMetricsExporter()

# Instrument the requests library
RequestsInstrumentor().instrument()

# Indicate to start collecting and exporting requests related metrics
metrics.get_meter_provider().start_pipeline(
RequestsInstrumentor().meter, exporter, 5
)

response = requests.get("http://example.com")

input("...\n")
1 change: 1 addition & 0 deletions docs/instrumentation/instrumentation.rst
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@ Submodules
:maxdepth: 1

instrumentor
metric
7 changes: 7 additions & 0 deletions docs/instrumentation/metric.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
opentelemetry.instrumentation.metric package
============================================

.. automodule:: opentelemetry.instrumentation.metric
:members:
:undoc-members:
:show-inheritance:
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ Released 2020-09-17
([#1040](https://github.com/open-telemetry/opentelemetry-python/pull/1040))
- Drop support for Python 3.4
([#1099](https://github.com/open-telemetry/opentelemetry-python/pull/1099))
- Add support for http metrics
([#1116](https://github.com/open-telemetry/opentelemetry-python/pull/1116))

## Version 0.12b0

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
import opentelemetry.instrumentation.requests
# You can optionally pass a custom TracerProvider to
RequestInstrumentor.instrument()
# RequestInstrumentor.instrument()
opentelemetry.instrumentation.requests.RequestsInstrumentor().instrument()
response = requests.get(url="https://www.example.org/")
Expand All @@ -43,6 +43,10 @@

from opentelemetry import context, propagators
from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
from opentelemetry.instrumentation.metric import (
HTTPMetricRecorder,
MetricMixin,
)
from opentelemetry.instrumentation.requests.version import __version__
from opentelemetry.instrumentation.utils import http_status_to_canonical_code
from opentelemetry.trace import SpanKind, get_tracer
Expand All @@ -54,6 +58,7 @@


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

exception = None

recorder = RequestsInstrumentor().metric_recorder

labels = {}
labels["http.method"] = method
labels["http.url"] = url

with get_tracer(
__name__, __version__, tracer_provider
).start_as_current_span(span_name, kind=SpanKind.CLIENT) as span:
if span.is_recording():
span.set_attribute("component", "http")
span.set_attribute("http.method", method.upper())
span.set_attribute("http.url", url)

headers = get_or_create_headers()
propagators.inject(type(headers).__setitem__, headers)

token = context.attach(
context.set_value(_SUPPRESS_REQUESTS_INSTRUMENTATION_KEY, True)
)
try:
result = call_wrapped() # *** PROCEED
except Exception as exc: # pylint: disable=W0703
exception = exc
result = getattr(exc, "response", None)
finally:
context.detach(token)

if exception is not None and span.is_recording():
span.set_status(
Status(_exception_to_canonical_code(exception))
with recorder.record_duration(labels):
if span.is_recording():
span.set_attribute("component", "http")
span.set_attribute("http.method", method)
span.set_attribute("http.url", url)

headers = get_or_create_headers()
propagators.inject(type(headers).__setitem__, headers)

token = context.attach(
context.set_value(
_SUPPRESS_REQUESTS_INSTRUMENTATION_KEY, True
)
)
span.record_exception(exception)

if result is not None and span.is_recording():
span.set_attribute("http.status_code", result.status_code)
span.set_attribute("http.status_text", result.reason)
span.set_status(
Status(http_status_to_canonical_code(result.status_code))
)

if span_callback is not None:
span_callback(span, result)
try:
result = call_wrapped() # *** PROCEED
except Exception as exc: # pylint: disable=W0703
exception = exc
result = getattr(exc, "response", None)
finally:
context.detach(token)

if exception is not None and span.is_recording():
span.set_status(
Status(_exception_to_canonical_code(exception))
)
span.record_exception(exception)

if result is not None:
if span.is_recording():
span.set_attribute(
"http.status_code", result.status_code
)
span.set_attribute("http.status_text", result.reason)
span.set_status(
Status(
http_status_to_canonical_code(
result.status_code
)
)
)
labels["http.status_code"] = str(result.status_code)
labels["http.status_text"] = result.reason
if result.raw and result.raw.version:
labels["http.flavor"] = (
str(result.raw.version)[:1]
+ "."
+ str(result.raw.version)[:-1]
)
if span_callback is not None:
span_callback(span, result)

if exception is not None:
raise exception.with_traceback(exception.__traceback__)
Expand Down Expand Up @@ -202,7 +230,7 @@ def _exception_to_canonical_code(exc: Exception) -> StatusCanonicalCode:
return StatusCanonicalCode.UNKNOWN


class RequestsInstrumentor(BaseInstrumentor):
class RequestsInstrumentor(BaseInstrumentor, MetricMixin):
"""An instrumentor for requests
See `BaseInstrumentor`
"""
Expand All @@ -219,6 +247,11 @@ def _instrument(self, **kwargs):
tracer_provider=kwargs.get("tracer_provider"),
span_callback=kwargs.get("span_callback"),
)
self.init_metrics(
__name__, __version__,
)
# pylint: disable=W0201
self.metric_recorder = HTTPMetricRecorder(self.meter, SpanKind.CLIENT)

def _uninstrument(self, **kwargs):
_uninstrument()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
from opentelemetry import context, propagators, trace
from opentelemetry.instrumentation.requests import RequestsInstrumentor
from opentelemetry.sdk import resources
from opentelemetry.sdk.util import get_dict_as_key
from opentelemetry.test.mock_textmap import MockTextMapPropagator
from opentelemetry.test.test_base import TestBase
from opentelemetry.trace.status import StatusCanonicalCode
Expand Down Expand Up @@ -88,6 +89,27 @@ def test_basic(self):
span, opentelemetry.instrumentation.requests
)

self.assertIsNotNone(RequestsInstrumentor().meter)
self.assertEqual(len(RequestsInstrumentor().meter.metrics), 1)
recorder = RequestsInstrumentor().meter.metrics.pop()
match_key = get_dict_as_key(
{
"http.flavor": "1.1",
"http.method": "GET",
"http.status_code": "200",
"http.status_text": "OK",
"http.url": "http://httpbin.org/status/200",
}
)
for key in recorder.bound_instruments.keys():
self.assertEqual(key, match_key)
# pylint: disable=protected-access
bound = recorder.bound_instruments.get(key)
for view_data in bound.view_datas:
self.assertEqual(view_data.labels, key)
self.assertEqual(view_data.aggregator.current.count, 1)
self.assertGreater(view_data.aggregator.current.sum, 0)

def test_not_foundbasic(self):
url_404 = "http://httpbin.org/status/404"
httpretty.register_uri(
Expand Down Expand Up @@ -246,6 +268,23 @@ def test_requests_exception_without_response(self, *_, **__):
span.status.canonical_code, StatusCanonicalCode.UNKNOWN
)

self.assertIsNotNone(RequestsInstrumentor().meter)
self.assertEqual(len(RequestsInstrumentor().meter.metrics), 1)
recorder = RequestsInstrumentor().meter.metrics.pop()
match_key = get_dict_as_key(
{
"http.method": "GET",
"http.url": "http://httpbin.org/status/200",
}
)
for key in recorder.bound_instruments.keys():
self.assertEqual(key, match_key)
# pylint: disable=protected-access
bound = recorder.bound_instruments.get(key)
for view_data in bound.view_datas:
self.assertEqual(view_data.labels, key)
self.assertEqual(view_data.aggregator.current.count, 1)

mocked_response = requests.Response()
mocked_response.status_code = 500
mocked_response.reason = "Internal Server Error"
Expand All @@ -272,6 +311,24 @@ def test_requests_exception_with_response(self, *_, **__):
self.assertEqual(
span.status.canonical_code, StatusCanonicalCode.INTERNAL
)
self.assertIsNotNone(RequestsInstrumentor().meter)
self.assertEqual(len(RequestsInstrumentor().meter.metrics), 1)
recorder = RequestsInstrumentor().meter.metrics.pop()
match_key = get_dict_as_key(
{
"http.method": "GET",
"http.status_code": "500",
"http.status_text": "Internal Server Error",
"http.url": "http://httpbin.org/status/200",
}
)
for key in recorder.bound_instruments.keys():
self.assertEqual(key, match_key)
# pylint: disable=protected-access
bound = recorder.bound_instruments.get(key)
for view_data in bound.view_datas:
self.assertEqual(view_data.labels, key)
self.assertEqual(view_data.aggregator.current.count, 1)

@mock.patch("requests.adapters.HTTPAdapter.send", side_effect=Exception)
def test_requests_basic_exception(self, *_, **__):
Expand Down
2 changes: 2 additions & 0 deletions opentelemetry-instrumentation/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ Released 2020-09-17

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

## 0.9b0

Expand Down
1 change: 1 addition & 0 deletions opentelemetry-instrumentation/setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ zip_safe = False
include_package_data = True
install_requires =
opentelemetry-api == 0.14.dev0
opentelemetry-sdk == 0.14.dev0
wrapt >= 1.0.0, < 2.0.0

[options.packages.find]
Expand Down
Loading

0 comments on commit 872975b

Please sign in to comment.