Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding metric collection as part of instrumentations - Requests #1116

Merged
merged 25 commits into from
Sep 25, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible to infer http.scheme as well here using url?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. However, if url is available, it takes priority since all components of the URI can be derived from it. See https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/metrics/semantic_conventions/http-metrics.md#parameterized-labels


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