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

Metric instrumentation fastapi #1199

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
8684258
add metric instumentation
TheAnshul756 Jul 12, 2022
a01f115
add metric instrumentation changes I
TheAnshul756 Jul 18, 2022
1cbf9eb
add metric instrumentation in asgi
TheAnshul756 Jul 19, 2022
8a452c0
add change log
TheAnshul756 Jul 19, 2022
5bf7fb9
fix lint
TheAnshul756 Jul 19, 2022
6a7d1fe
Merge branch 'main' of https://github.com/TheAnshul756/opentelemetry-…
TheAnshul756 Jul 19, 2022
a147c07
Merge branch 'metric-instrumentation-asgi' into metric-instrumentatio…
TheAnshul756 Jul 19, 2022
8f6a5a9
add test for fastapi metrics
TheAnshul756 Jul 19, 2022
b7e1b33
add change log
TheAnshul756 Jul 20, 2022
ab0c407
Merge branch 'main' into metric-instrumentation-fastapi
TheAnshul756 Jul 20, 2022
210dbbd
Merge branch 'main' into metric-instrumentation-asgi
lzchen Jul 20, 2022
67258fa
Merge branch 'main' into metric-instrumentation-asgi
ocelotl Jul 22, 2022
d2df8b1
change time duration start time position for metric
TheAnshul756 Jul 27, 2022
c4d2b73
Merge branch 'metric-instrumentation-asgi' of https://github.com/TheA…
TheAnshul756 Jul 27, 2022
5f3dda3
Merge branch 'metric-instrumentation-asgi' into metric-instrumentatio…
TheAnshul756 Jul 27, 2022
f4b8962
add basic success test
TheAnshul756 Jul 28, 2022
0874f23
remove metric instrumentation for websockets
TheAnshul756 Jul 29, 2022
6819509
Merge branch 'main' into metric-instrumentation-asgi
ashu658 Aug 1, 2022
5b951d9
Merge branch 'main' of https://github.com/TheAnshul756/opentelemetry-…
TheAnshul756 Aug 1, 2022
363242c
Merge branch 'metric-instrumentation-asgi' of https://github.com/TheA…
TheAnshul756 Aug 1, 2022
1aef36e
add value test and websocket test for metric
TheAnshul756 Aug 2, 2022
a466090
Merge branch 'metric-instrumentation-asgi' into metric-instrumentatio…
TheAnshul756 Aug 2, 2022
4f8dfe3
add metric tests for fastapi
TheAnshul756 Aug 2, 2022
4d18185
Merge branch 'main' into metric-instrumentation-fastapi
srikanthccv Aug 4, 2022
e724a98
fix error
TheAnshul756 Aug 9, 2022
ecb42f0
fix merge conflict with main
TheAnshul756 Aug 16, 2022
48dbe5f
add meter in asgi instead of just passing meter provider
TheAnshul756 Aug 16, 2022
e2d3e49
Merge branch 'main' of https://github.com/TheAnshul756/opentelemetry-…
TheAnshul756 Aug 18, 2022
975dde6
fix asgi duplicate changes
TheAnshul756 Aug 18, 2022
6550752
add uninstrument app test
TheAnshul756 Aug 23, 2022
7d7b2c2
Merge branch 'main' of https://github.com/TheAnshul756/opentelemetry-…
TheAnshul756 Aug 24, 2022
6e44836
Merge branch 'main' into metric-instrumentation-fastapi
srikanthccv Aug 24, 2022
34ca9d6
Update instrumentation/opentelemetry-instrumentation-fastapi/tests/te…
TheAnshul756 Aug 26, 2022
8840a43
Merge branch 'main' into metric-instrumentation-fastapi
TheAnshul756 Aug 26, 2022
1390acc
Merge branch 'main' into metric-instrumentation-fastapi
TheAnshul756 Aug 29, 2022
6bfef20
Merge branch 'main' into metric-instrumentation-fastapi
TheAnshul756 Sep 1, 2022
e2e8e40
Merge branch 'main' into metric-instrumentation-fastapi
TheAnshul756 Sep 6, 2022
ac47ef0
add readme
TheAnshul756 Sep 6, 2022
4a04d38
Merge branch 'metric-instrumentation-fastapi' of https://github.com/T…
TheAnshul756 Sep 6, 2022
ee50a8f
Merge branch 'main' into metric-instrumentation-fastapi
TheAnshul756 Sep 7, 2022
a4428bb
Merge branch 'main' into metric-instrumentation-fastapi
TheAnshul756 Sep 8, 2022
8d6e791
Merge branch 'main' of https://github.com/TheAnshul756/opentelemetry-…
TheAnshul756 Sep 9, 2022
04e4e7d
Merge branch 'main' into metric-instrumentation-fastapi
srikanthccv Sep 12, 2022
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- Flask sqlalchemy psycopg2 integration
([#1224](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1224))
- Add metric instrumentation in fastapi
([#1199](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1199))
- Add metric instrumentation in Pyramid
([#1242](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1242))

Expand Down
2 changes: 1 addition & 1 deletion instrumentation/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
| [opentelemetry-instrumentation-django](./opentelemetry-instrumentation-django) | django >= 1.10 | Yes
| [opentelemetry-instrumentation-elasticsearch](./opentelemetry-instrumentation-elasticsearch) | elasticsearch >= 2.0 | No
| [opentelemetry-instrumentation-falcon](./opentelemetry-instrumentation-falcon) | falcon >= 1.4.1, < 4.0.0 | No
| [opentelemetry-instrumentation-fastapi](./opentelemetry-instrumentation-fastapi) | fastapi ~= 0.58 | No
| [opentelemetry-instrumentation-fastapi](./opentelemetry-instrumentation-fastapi) | fastapi ~= 0.58 | Yes
| [opentelemetry-instrumentation-flask](./opentelemetry-instrumentation-flask) | flask >= 1.0, < 3.0 | Yes
| [opentelemetry-instrumentation-grpc](./opentelemetry-instrumentation-grpc) | grpcio ~= 1.27 | No
| [opentelemetry-instrumentation-httpx](./opentelemetry-instrumentation-httpx) | httpx >= 0.18.0 | No
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -396,10 +396,15 @@ def __init__(
client_response_hook: _ClientResponseHookT = None,
tracer_provider=None,
meter_provider=None,
meter=None,
):
self.app = guarantee_single_callable(app)
self.tracer = trace.get_tracer(__name__, __version__, tracer_provider)
self.meter = get_meter(__name__, __version__, meter_provider)
self.meter = (
get_meter(__name__, __version__, meter_provider)
if meter is None
else meter
)
self.duration_histogram = self.meter.create_histogram(
name="http.server.duration",
unit="ms",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,9 @@ def client_response_hook(span: Span, message: dict):

from opentelemetry.instrumentation.asgi import OpenTelemetryMiddleware
from opentelemetry.instrumentation.asgi.package import _instruments
from opentelemetry.instrumentation.fastapi.version import __version__
from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
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, parse_excluded_urls
Expand Down Expand Up @@ -165,6 +167,7 @@ def instrument_app(
client_request_hook: _ClientRequestHookT = None,
client_response_hook: _ClientResponseHookT = None,
tracer_provider=None,
meter_provider=None,
excluded_urls=None,
):
"""Instrument an uninstrumented FastAPI application."""
Expand All @@ -176,6 +179,7 @@ def instrument_app(
excluded_urls = _excluded_urls_from_env
else:
excluded_urls = parse_excluded_urls(excluded_urls)
meter = get_meter(__name__, __version__, meter_provider)

app.add_middleware(
OpenTelemetryMiddleware,
Expand All @@ -185,6 +189,7 @@ 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
else:
Expand Down Expand Up @@ -223,6 +228,7 @@ def _instrument(self, **kwargs):
if _excluded_urls is None
else parse_excluded_urls(_excluded_urls)
)
_InstrumentedFastAPI._meter_provider = kwargs.get("meter_provider")
fastapi.FastAPI = _InstrumentedFastAPI

def _uninstrument(self, **kwargs):
Expand All @@ -231,13 +237,17 @@ def _uninstrument(self, **kwargs):

class _InstrumentedFastAPI(fastapi.FastAPI):
_tracer_provider = None
_meter_provider = None
_excluded_urls = None
_server_request_hook: _ServerRequestHookT = None
_client_request_hook: _ClientRequestHookT = None
_client_response_hook: _ClientResponseHookT = None

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
meter = get_meter(
__name__, __version__, _InstrumentedFastAPI._meter_provider
)
self.add_middleware(
OpenTelemetryMiddleware,
excluded_urls=_InstrumentedFastAPI._excluded_urls,
Expand All @@ -246,6 +256,7 @@ def __init__(self, *args, **kwargs):
client_request_hook=_InstrumentedFastAPI._client_request_hook,
client_response_hook=_InstrumentedFastAPI._client_response_hook,
tracer_provider=_InstrumentedFastAPI._tracer_provider,
meter=meter,
)
self._is_instrumented_by_opentelemetry = True

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,5 @@


_instruments = ("fastapi ~= 0.58",)

_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

import fastapi
Expand All @@ -22,16 +23,31 @@
import opentelemetry.instrumentation.fastapi as otel_fastapi
from opentelemetry import trace
from opentelemetry.instrumentation.asgi import OpenTelemetryMiddleware
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
from opentelemetry.test.test_base import TestBase
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 TestFastAPIManualInstrumentation(TestBase):
def _create_app(self):
Expand Down Expand Up @@ -161,6 +177,124 @@ def test_fastapi_excluded_urls_not_env(self):
spans = self.memory_exporter.get_finished_spans()
self.assertEqual(len(spans), 0)

def test_fastapi_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)
TheAnshul756 marked this conversation as resolved.
Show resolved Hide resolved
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)
TheAnshul756 marked this conversation as resolved.
Show resolved Hide resolved
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_metric_success(self):
start = default_timer()
self._client.get("/foobar")
duration = max(round((default_timer() - start) * 1000), 0)
expected_duration_attributes = {
"http.method": "GET",
"http.host": "testserver",
"http.scheme": "http",
"http.flavor": "1.1",
"http.server_name": "testserver",
"net.host.port": 80,
"http.status_code": 200,
}
expected_requests_count_attributes = {
"http.method": "GET",
"http.host": "testserver",
"http.scheme": "http",
"http.flavor": "1.1",
"http.server_name": "testserver",
}
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.assertDictEqual(
expected_duration_attributes,
dict(point.attributes),
)
self.assertEqual(point.count, 1)
self.assertAlmostEqual(duration, point.sum, delta=20)
if isinstance(point, NumberDataPoint):
self.assertDictEqual(
expected_requests_count_attributes,
dict(point.attributes),
)
self.assertEqual(point.value, 0)

def test_basic_post_request_metric_success(self):
start = default_timer()
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)
if isinstance(point, NumberDataPoint):
self.assertEqual(point.value, 0)

def test_metric_uninstruemnt_app(self):
self._client.get("/foobar")
self._instrumentor.uninstrument_app(self._app)
TheAnshul756 marked this conversation as resolved.
Show resolved Hide resolved
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(self):
# instrumenting class and creating app to send request
self._instrumentor.instrument()
app = self._create_fastapi_app()
client = TestClient(app)
client.get("/foobar")
# uninstrumenting class and creating the app again
self._instrumentor.uninstrument()
app = self._create_fastapi_app()
client = TestClient(app)
client.get("/foobar")
srikanthccv marked this conversation as resolved.
Show resolved Hide resolved

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_fastapi_app():
app = fastapi.FastAPI()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
)

# List of recommended metrics attributes
_duration_attrs = [
_duration_attrs = {
SpanAttributes.HTTP_METHOD,
SpanAttributes.HTTP_HOST,
SpanAttributes.HTTP_SCHEME,
Expand All @@ -37,15 +37,15 @@
SpanAttributes.HTTP_SERVER_NAME,
SpanAttributes.NET_HOST_NAME,
SpanAttributes.NET_HOST_PORT,
]
}

_active_requests_count_attrs = [
_active_requests_count_attrs = {
SpanAttributes.HTTP_METHOD,
SpanAttributes.HTTP_HOST,
SpanAttributes.HTTP_SCHEME,
SpanAttributes.HTTP_FLAVOR,
SpanAttributes.HTTP_SERVER_NAME,
]
}


class ExcludeList:
Expand Down Expand Up @@ -150,16 +150,16 @@ def get_custom_headers(env_var: str) -> List[str]:


def _parse_active_request_count_attrs(req_attrs):
active_requests_count_attrs = {}
for attr_key in _active_requests_count_attrs:
if req_attrs.get(attr_key) is not None:
active_requests_count_attrs[attr_key] = req_attrs[attr_key]
active_requests_count_attrs = {
key: req_attrs[key]
for key in _active_requests_count_attrs.intersection(req_attrs.keys())
}
return active_requests_count_attrs


def _parse_duration_attrs(req_attrs):
duration_attrs = {}
for attr_key in _duration_attrs:
if req_attrs.get(attr_key) is not None:
duration_attrs[attr_key] = req_attrs[attr_key]
duration_attrs = {
key: req_attrs[key]
for key in _duration_attrs.intersection(req_attrs.keys())
}
return duration_attrs