Skip to content

Commit

Permalink
Metrics instrumentation starlette (#1327)
Browse files Browse the repository at this point in the history
  • Loading branch information
rahulmukherjee68 committed Sep 27, 2022
1 parent 65329a8 commit fc98f08
Show file tree
Hide file tree
Showing 5 changed files with 163 additions and 1 deletion.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
([#1242](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1242))
- `opentelemetry-util-http` Add support for sanitizing HTTP header values.
([#1253](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1253))
- Add metric instrumentation in starlette
([#1327](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1327))


### Fixed

Expand Down
2 changes: 1 addition & 1 deletion instrumentation/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
| [opentelemetry-instrumentation-sklearn](./opentelemetry-instrumentation-sklearn) | scikit-learn ~= 0.24.0 | No
| [opentelemetry-instrumentation-sqlalchemy](./opentelemetry-instrumentation-sqlalchemy) | sqlalchemy | No
| [opentelemetry-instrumentation-sqlite3](./opentelemetry-instrumentation-sqlite3) | sqlite3 | No
| [opentelemetry-instrumentation-starlette](./opentelemetry-instrumentation-starlette) | starlette ~= 0.13.0 | No
| [opentelemetry-instrumentation-starlette](./opentelemetry-instrumentation-starlette) | starlette ~= 0.13.0 | Yes
| [opentelemetry-instrumentation-system-metrics](./opentelemetry-instrumentation-system-metrics) | psutil >= 5 | No
| [opentelemetry-instrumentation-tornado](./opentelemetry-instrumentation-tornado) | tornado >= 5.1.1 | No
| [opentelemetry-instrumentation-urllib](./opentelemetry-instrumentation-urllib) | urllib | No
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,8 @@ def client_response_hook(span: Span, message: dict):
from opentelemetry.instrumentation.asgi import OpenTelemetryMiddleware
from opentelemetry.instrumentation.asgi.package import _instruments
from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
from opentelemetry.instrumentation.starlette.version import __version__
from opentelemetry.metrics import get_meter
from opentelemetry.semconv.trace import SpanAttributes
from opentelemetry.trace import Span
from opentelemetry.util.http import get_excluded_urls
Expand All @@ -156,9 +158,11 @@ def instrument_app(
server_request_hook: _ServerRequestHookT = None,
client_request_hook: _ClientRequestHookT = None,
client_response_hook: _ClientResponseHookT = None,
meter_provider=None,
tracer_provider=None,
):
"""Instrument an uninstrumented Starlette application."""
meter = get_meter(__name__, __version__, meter_provider)
if not getattr(app, "is_instrumented_by_opentelemetry", False):
app.add_middleware(
OpenTelemetryMiddleware,
Expand All @@ -168,9 +172,24 @@ def instrument_app(
client_request_hook=client_request_hook,
client_response_hook=client_response_hook,
tracer_provider=tracer_provider,
meter=meter,
)
app.is_instrumented_by_opentelemetry = True

# adding apps to set for uninstrumenting
if app not in _InstrumentedStarlette._instrumented_starlette_apps:
_InstrumentedStarlette._instrumented_starlette_apps.add(app)

@staticmethod
def uninstrument_app(app: applications.Starlette):
app.user_middleware = [
x
for x in app.user_middleware
if x.cls is not OpenTelemetryMiddleware
]
app.middleware_stack = app.build_middleware_stack()
app._is_instrumented_by_opentelemetry = False

def instrumentation_dependencies(self) -> Collection[str]:
return _instruments

Expand All @@ -186,20 +205,32 @@ def _instrument(self, **kwargs):
_InstrumentedStarlette._client_response_hook = kwargs.get(
"client_response_hook"
)
_InstrumentedStarlette._meter_provider = kwargs.get("_meter_provider")

applications.Starlette = _InstrumentedStarlette

def _uninstrument(self, **kwargs):

"""uninstrumenting all created apps by user"""
for instance in _InstrumentedStarlette._instrumented_starlette_apps:
self.uninstrument_app(instance)
_InstrumentedStarlette._instrumented_starlette_apps.clear()
applications.Starlette = self._original_starlette


class _InstrumentedStarlette(applications.Starlette):
_tracer_provider = None
_meter_provider = None
_server_request_hook: _ServerRequestHookT = None
_client_request_hook: _ClientRequestHookT = None
_client_response_hook: _ClientResponseHookT = None
_instrumented_starlette_apps = set()

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
meter = get_meter(
__name__, __version__, _InstrumentedStarlette._meter_provider
)
self.add_middleware(
OpenTelemetryMiddleware,
excluded_urls=_excluded_urls,
Expand All @@ -208,7 +239,14 @@ def __init__(self, *args, **kwargs):
client_request_hook=_InstrumentedStarlette._client_request_hook,
client_response_hook=_InstrumentedStarlette._client_response_hook,
tracer_provider=_InstrumentedStarlette._tracer_provider,
meter=meter,
)
self._is_instrumented_by_opentelemetry = True
# adding apps to set for uninstrumenting
_InstrumentedStarlette._instrumented_starlette_apps.add(self)

def __del__(self):
_InstrumentedStarlette._instrumented_starlette_apps.remove(self)


def _get_route_details(scope):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,5 @@


_instruments = ("starlette ~= 0.13.0",)

_supports_metrics = True
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
# limitations under the License.

import unittest
from timeit import default_timer
from unittest.mock import patch

from starlette import applications
Expand All @@ -22,6 +23,10 @@
from starlette.websockets import WebSocket

import opentelemetry.instrumentation.starlette as otel_starlette
from opentelemetry.sdk.metrics.export import (
HistogramDataPoint,
NumberDataPoint,
)
from opentelemetry.sdk.resources import Resource
from opentelemetry.semconv.trace import SpanAttributes
from opentelemetry.test.globals_test import reset_trace_globals
Expand All @@ -35,9 +40,20 @@
from opentelemetry.util.http import (
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST,
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE,
_active_requests_count_attrs,
_duration_attrs,
get_excluded_urls,
)

_expected_metric_names = [
"http.server.active_requests",
"http.server.duration",
]
_recommended_attrs = {
"http.server.active_requests": _active_requests_count_attrs,
"http.server.duration": _duration_attrs,
}


class TestStarletteManualInstrumentation(TestBase):
def _create_app(self):
Expand Down Expand Up @@ -100,6 +116,109 @@ def test_starlette_excluded_urls(self):
spans = self.memory_exporter.get_finished_spans()
self.assertEqual(len(spans), 0)

def test_starlette_metrics(self):
self._client.get("/foobar")
self._client.get("/foobar")
self._client.get("/foobar")
metrics_list = self.memory_metrics_reader.get_metrics_data()
number_data_point_seen = False
histogram_data_point_seen = False
self.assertTrue(len(metrics_list.resource_metrics) == 1)
for resource_metric in metrics_list.resource_metrics:
self.assertTrue(len(resource_metric.scope_metrics) == 1)
for scope_metric in resource_metric.scope_metrics:
self.assertTrue(len(scope_metric.metrics) == 2)
for metric in scope_metric.metrics:
self.assertIn(metric.name, _expected_metric_names)
data_points = list(metric.data.data_points)
self.assertEqual(len(data_points), 1)
for point in data_points:
if isinstance(point, HistogramDataPoint):
self.assertEqual(point.count, 3)
histogram_data_point_seen = True
if isinstance(point, NumberDataPoint):
number_data_point_seen = True
for attr in point.attributes:
self.assertIn(
attr, _recommended_attrs[metric.name]
)
self.assertTrue(number_data_point_seen and histogram_data_point_seen)

def test_basic_post_request_metric_success(self):
start = default_timer()
expected_duration_attributes = {
"http.flavor": "1.1",
"http.host": "testserver",
"http.method": "POST",
"http.scheme": "http",
"http.server_name": "testserver",
"http.status_code": 405,
"net.host.port": 80,
}
expected_requests_count_attributes = {
"http.flavor": "1.1",
"http.host": "testserver",
"http.method": "POST",
"http.scheme": "http",
"http.server_name": "testserver",
}
self._client.post("/foobar")
duration = max(round((default_timer() - start) * 1000), 0)
metrics_list = self.memory_metrics_reader.get_metrics_data()
for metric in (
metrics_list.resource_metrics[0].scope_metrics[0].metrics
):
for point in list(metric.data.data_points):
if isinstance(point, HistogramDataPoint):
self.assertEqual(point.count, 1)
self.assertAlmostEqual(duration, point.sum, delta=30)
self.assertDictEqual(
dict(point.attributes), expected_duration_attributes
)
if isinstance(point, NumberDataPoint):
self.assertDictEqual(
expected_requests_count_attributes,
dict(point.attributes),
)
self.assertEqual(point.value, 0)

def test_metric_for_uninstrment_app_method(self):
self._client.get("/foobar")
# uninstrumenting the existing client app
self._instrumentor.uninstrument_app(self._app)
self._client.get("/foobar")
self._client.get("/foobar")
metrics_list = self.memory_metrics_reader.get_metrics_data()
for metric in (
metrics_list.resource_metrics[0].scope_metrics[0].metrics
):
for point in list(metric.data.data_points):
if isinstance(point, HistogramDataPoint):
self.assertEqual(point.count, 1)
if isinstance(point, NumberDataPoint):
self.assertEqual(point.value, 0)

def test_metric_uninstrument_inherited_by_base(self):
# instrumenting class and creating app to send request
self._instrumentor.instrument()
app = self._create_starlette_app()
client = TestClient(app)
client.get("/foobar")
# calling uninstrument and checking for telemetry data
self._instrumentor.uninstrument()
client.get("/foobar")
client.get("/foobar")
client.get("/foobar")
metrics_list = self.memory_metrics_reader.get_metrics_data()
for metric in (
metrics_list.resource_metrics[0].scope_metrics[0].metrics
):
for point in list(metric.data.data_points):
if isinstance(point, HistogramDataPoint):
self.assertEqual(point.count, 1)
if isinstance(point, NumberDataPoint):
self.assertEqual(point.value, 0)

@staticmethod
def _create_starlette_app():
def home(_):
Expand Down

0 comments on commit fc98f08

Please sign in to comment.