HTTP semantic convention stability migration for fastapi (#2682)

This commit is contained in:
Leighton Chen
2024-07-12 11:48:04 -07:00
committed by GitHub
parent 5a27946920
commit 6293d6a991
6 changed files with 527 additions and 40 deletions

View File

@ -21,6 +21,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
([#2638](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2638)) ([#2638](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2638))
- `opentelemetry-instrumentation-asgi` Implement new semantic convention opt-in with stable http semantic conventions - `opentelemetry-instrumentation-asgi` Implement new semantic convention opt-in with stable http semantic conventions
([#2610](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2610)) ([#2610](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2610))
- `opentelemetry-instrumentation-fastapi` Implement new semantic convention opt-in with stable http semantic conventions
([#2682](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2682))
- `opentelemetry-instrumentation-httpx` Implement new semantic convention opt-in migration with stable http semantic conventions - `opentelemetry-instrumentation-httpx` Implement new semantic convention opt-in migration with stable http semantic conventions
([#2631](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2631)) ([#2631](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2631))
- `opentelemetry-instrumentation-system-metrics` Permit to use psutil 6.0+. - `opentelemetry-instrumentation-system-metrics` Permit to use psutil 6.0+.
@ -32,9 +34,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `opentelemetry-instrumentation-asgi`, `opentelemetry-instrumentation-fastapi`, `opentelemetry-instrumentation-starlette` Use `tracer` and `meter` of originating components instead of one from `asgi` middleware - `opentelemetry-instrumentation-asgi`, `opentelemetry-instrumentation-fastapi`, `opentelemetry-instrumentation-starlette` Use `tracer` and `meter` of originating components instead of one from `asgi` middleware
([#2580](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2580)) ([#2580](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2580))
- Populate `{method}` as `HTTP` on `_OTHER` methods from scope - Populate `{method}` as `HTTP` on `_OTHER` methods from scope for `asgi` middleware
([#2610](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2610)) ([#2610](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2610))
- Populate `{method}` as `HTTP` on `_OTHER` methods from scope for `fastapi` middleware
([#2682](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2682))
### Fixed ### Fixed

View File

@ -19,7 +19,7 @@
| [opentelemetry-instrumentation-django](./opentelemetry-instrumentation-django) | django >= 1.10 | Yes | experimental | [opentelemetry-instrumentation-django](./opentelemetry-instrumentation-django) | django >= 1.10 | Yes | experimental
| [opentelemetry-instrumentation-elasticsearch](./opentelemetry-instrumentation-elasticsearch) | elasticsearch >= 6.0 | No | experimental | [opentelemetry-instrumentation-elasticsearch](./opentelemetry-instrumentation-elasticsearch) | elasticsearch >= 6.0 | No | experimental
| [opentelemetry-instrumentation-falcon](./opentelemetry-instrumentation-falcon) | falcon >= 1.4.1, < 4.0.0 | Yes | experimental | [opentelemetry-instrumentation-falcon](./opentelemetry-instrumentation-falcon) | falcon >= 1.4.1, < 4.0.0 | Yes | experimental
| [opentelemetry-instrumentation-fastapi](./opentelemetry-instrumentation-fastapi) | fastapi ~= 0.58 | Yes | experimental | [opentelemetry-instrumentation-fastapi](./opentelemetry-instrumentation-fastapi) | fastapi ~= 0.58 | Yes | migration
| [opentelemetry-instrumentation-flask](./opentelemetry-instrumentation-flask) | flask >= 1.0 | Yes | migration | [opentelemetry-instrumentation-flask](./opentelemetry-instrumentation-flask) | flask >= 1.0 | Yes | migration
| [opentelemetry-instrumentation-grpc](./opentelemetry-instrumentation-grpc) | grpcio ~= 1.27 | No | experimental | [opentelemetry-instrumentation-grpc](./opentelemetry-instrumentation-grpc) | grpcio ~= 1.27 | No | experimental
| [opentelemetry-instrumentation-httpx](./opentelemetry-instrumentation-httpx) | httpx >= 0.18.0 | No | migration | [opentelemetry-instrumentation-httpx](./opentelemetry-instrumentation-httpx) | httpx >= 0.18.0 | No | migration

View File

@ -177,6 +177,12 @@ from typing import Collection
import fastapi import fastapi
from starlette.routing import Match from starlette.routing import Match
from opentelemetry.instrumentation._semconv import (
_get_schema_url,
_HTTPStabilityMode,
_OpenTelemetrySemanticConventionStability,
_OpenTelemetryStabilitySignalType,
)
from opentelemetry.instrumentation.asgi import OpenTelemetryMiddleware from opentelemetry.instrumentation.asgi import OpenTelemetryMiddleware
from opentelemetry.instrumentation.asgi.types import ( from opentelemetry.instrumentation.asgi.types import (
ClientRequestHook, ClientRequestHook,
@ -189,7 +195,11 @@ from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
from opentelemetry.metrics import get_meter from opentelemetry.metrics import get_meter
from opentelemetry.semconv.trace import SpanAttributes from opentelemetry.semconv.trace import SpanAttributes
from opentelemetry.trace import get_tracer from opentelemetry.trace import get_tracer
from opentelemetry.util.http import get_excluded_urls, parse_excluded_urls from opentelemetry.util.http import (
get_excluded_urls,
parse_excluded_urls,
sanitize_method,
)
_excluded_urls_from_env = get_excluded_urls("FASTAPI") _excluded_urls_from_env = get_excluded_urls("FASTAPI")
_logger = logging.getLogger(__name__) _logger = logging.getLogger(__name__)
@ -218,6 +228,11 @@ class FastAPIInstrumentor(BaseInstrumentor):
app._is_instrumented_by_opentelemetry = False app._is_instrumented_by_opentelemetry = False
if not getattr(app, "_is_instrumented_by_opentelemetry", False): if not getattr(app, "_is_instrumented_by_opentelemetry", False):
# initialize semantic conventions opt-in if needed
_OpenTelemetrySemanticConventionStability._initialize()
sem_conv_opt_in_mode = _OpenTelemetrySemanticConventionStability._get_opentelemetry_stability_opt_in_mode(
_OpenTelemetryStabilitySignalType.HTTP,
)
if excluded_urls is None: if excluded_urls is None:
excluded_urls = _excluded_urls_from_env excluded_urls = _excluded_urls_from_env
else: else:
@ -226,13 +241,13 @@ class FastAPIInstrumentor(BaseInstrumentor):
__name__, __name__,
__version__, __version__,
tracer_provider, tracer_provider,
schema_url="https://opentelemetry.io/schemas/1.11.0", schema_url=_get_schema_url(sem_conv_opt_in_mode),
) )
meter = get_meter( meter = get_meter(
__name__, __name__,
__version__, __version__,
meter_provider, meter_provider,
schema_url="https://opentelemetry.io/schemas/1.11.0", schema_url=_get_schema_url(sem_conv_opt_in_mode),
) )
app.add_middleware( app.add_middleware(
@ -303,6 +318,7 @@ class _InstrumentedFastAPI(fastapi.FastAPI):
_client_request_hook: ClientRequestHook = None _client_request_hook: ClientRequestHook = None
_client_response_hook: ClientResponseHook = None _client_response_hook: ClientResponseHook = None
_instrumented_fastapi_apps = set() _instrumented_fastapi_apps = set()
_sem_conv_opt_in_mode = _HTTPStabilityMode.DEFAULT
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
@ -310,13 +326,17 @@ class _InstrumentedFastAPI(fastapi.FastAPI):
__name__, __name__,
__version__, __version__,
_InstrumentedFastAPI._tracer_provider, _InstrumentedFastAPI._tracer_provider,
schema_url="https://opentelemetry.io/schemas/1.11.0", schema_url=_get_schema_url(
_InstrumentedFastAPI._sem_conv_opt_in_mode
),
) )
meter = get_meter( meter = get_meter(
__name__, __name__,
__version__, __version__,
_InstrumentedFastAPI._meter_provider, _InstrumentedFastAPI._meter_provider,
schema_url="https://opentelemetry.io/schemas/1.11.0", schema_url=_get_schema_url(
_InstrumentedFastAPI._sem_conv_opt_in_mode
),
) )
self.add_middleware( self.add_middleware(
OpenTelemetryMiddleware, OpenTelemetryMiddleware,
@ -373,8 +393,10 @@ def _get_default_span_details(scope):
A tuple of span name and attributes A tuple of span name and attributes
""" """
route = _get_route_details(scope) route = _get_route_details(scope)
method = scope.get("method", "") method = sanitize_method(scope.get("method", "").strip())
attributes = {} attributes = {}
if method == "_OTHER":
method = "HTTP"
if route: if route:
attributes[SpanAttributes.HTTP_ROUTE] = route attributes[SpanAttributes.HTTP_ROUTE] = route
if method and route: # http if method and route: # http

View File

@ -16,3 +16,5 @@
_instruments = ("fastapi ~= 0.58",) _instruments = ("fastapi ~= 0.58",)
_supports_metrics = True _supports_metrics = True
_semconv_status = "migration"

View File

@ -11,6 +11,9 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
# pylint: disable=too-many-lines
import unittest import unittest
from timeit import default_timer from timeit import default_timer
from unittest.mock import patch from unittest.mock import patch
@ -20,39 +23,77 @@ from fastapi.middleware.httpsredirect import HTTPSRedirectMiddleware
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
import opentelemetry.instrumentation.fastapi as otel_fastapi import opentelemetry.instrumentation.fastapi as otel_fastapi
from opentelemetry.instrumentation._semconv import (
OTEL_SEMCONV_STABILITY_OPT_IN,
_OpenTelemetrySemanticConventionStability,
_server_active_requests_count_attrs_new,
_server_active_requests_count_attrs_old,
_server_duration_attrs_new,
_server_duration_attrs_old,
)
from opentelemetry.instrumentation.asgi import OpenTelemetryMiddleware from opentelemetry.instrumentation.asgi import OpenTelemetryMiddleware
from opentelemetry.sdk.metrics.export import ( from opentelemetry.sdk.metrics.export import (
HistogramDataPoint, HistogramDataPoint,
NumberDataPoint, NumberDataPoint,
) )
from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.resources import Resource
from opentelemetry.semconv.attributes.http_attributes import (
HTTP_REQUEST_METHOD,
HTTP_RESPONSE_STATUS_CODE,
HTTP_ROUTE,
)
from opentelemetry.semconv.attributes.network_attributes import (
NETWORK_PROTOCOL_VERSION,
)
from opentelemetry.semconv.attributes.url_attributes import URL_SCHEME
from opentelemetry.semconv.trace import SpanAttributes from opentelemetry.semconv.trace import SpanAttributes
from opentelemetry.test.test_base import TestBase from opentelemetry.test.test_base import TestBase
from opentelemetry.util.http import ( from opentelemetry.util.http import get_excluded_urls
_active_requests_count_attrs,
_duration_attrs,
get_excluded_urls,
)
_expected_metric_names = [ _expected_metric_names_old = [
"http.server.active_requests", "http.server.active_requests",
"http.server.duration", "http.server.duration",
"http.server.response.size", "http.server.response.size",
"http.server.request.size", "http.server.request.size",
] ]
_recommended_attrs = { _expected_metric_names_new = [
"http.server.active_requests": _active_requests_count_attrs, "http.server.active_requests",
"http.server.duration": {*_duration_attrs, SpanAttributes.HTTP_TARGET}, "http.server.request.duration",
"http.server.response.body.size",
"http.server.request.body.size",
]
_expected_metric_names_both = _expected_metric_names_old
_expected_metric_names_both.extend(_expected_metric_names_new)
_recommended_attrs_old = {
"http.server.active_requests": _server_active_requests_count_attrs_old,
"http.server.duration": {
*_server_duration_attrs_old,
SpanAttributes.HTTP_TARGET,
},
"http.server.response.size": { "http.server.response.size": {
*_duration_attrs, *_server_duration_attrs_old,
SpanAttributes.HTTP_TARGET, SpanAttributes.HTTP_TARGET,
}, },
"http.server.request.size": { "http.server.request.size": {
*_duration_attrs, *_server_duration_attrs_old,
SpanAttributes.HTTP_TARGET, SpanAttributes.HTTP_TARGET,
}, },
} }
_recommended_attrs_new = {
"http.server.active_requests": _server_active_requests_count_attrs_new,
"http.server.request.duration": _server_duration_attrs_new,
"http.server.response.body.size": _server_duration_attrs_new,
"http.server.request.body.size": _server_duration_attrs_new,
}
_recommended_attrs_both = _recommended_attrs_old.copy()
_recommended_attrs_both.update(_recommended_attrs_new)
_recommended_attrs_both["http.server.active_requests"].extend(
_server_active_requests_count_attrs_old
)
class TestBaseFastAPI(TestBase): class TestBaseFastAPI(TestBase):
def _create_app(self): def _create_app(self):
@ -88,10 +129,23 @@ class TestBaseFastAPI(TestBase):
def setUp(self): def setUp(self):
super().setUp() super().setUp()
test_name = ""
if hasattr(self, "_testMethodName"):
test_name = self._testMethodName
sem_conv_mode = "default"
if "new_semconv" in test_name:
sem_conv_mode = "http"
elif "both_semconv" in test_name:
sem_conv_mode = "http/dup"
self.env_patch = patch.dict( self.env_patch = patch.dict(
"os.environ", "os.environ",
{"OTEL_PYTHON_FASTAPI_EXCLUDED_URLS": "/exclude/123,healthzz"}, {
"OTEL_PYTHON_FASTAPI_EXCLUDED_URLS": "/exclude/123,healthzz",
OTEL_SEMCONV_STABILITY_OPT_IN: sem_conv_mode,
},
) )
_OpenTelemetrySemanticConventionStability._initialized = False
self.env_patch.start() self.env_patch.start()
self.exclude_patch = patch( self.exclude_patch = patch(
"opentelemetry.instrumentation.fastapi._excluded_urls_from_env", "opentelemetry.instrumentation.fastapi._excluded_urls_from_env",
@ -142,7 +196,6 @@ class TestBaseFastAPI(TestBase):
class TestBaseManualFastAPI(TestBaseFastAPI): class TestBaseManualFastAPI(TestBaseFastAPI):
@classmethod @classmethod
def setUpClass(cls): def setUpClass(cls):
if cls is TestBaseManualFastAPI: if cls is TestBaseManualFastAPI:
@ -196,7 +249,6 @@ class TestBaseManualFastAPI(TestBaseFastAPI):
class TestBaseAutoFastAPI(TestBaseFastAPI): class TestBaseAutoFastAPI(TestBaseFastAPI):
@classmethod @classmethod
def setUpClass(cls): def setUpClass(cls):
if cls is TestBaseAutoFastAPI: if cls is TestBaseAutoFastAPI:
@ -259,6 +311,7 @@ class TestBaseAutoFastAPI(TestBaseFastAPI):
) )
# pylint: disable=too-many-public-methods
class TestFastAPIManualInstrumentation(TestBaseManualFastAPI): class TestFastAPIManualInstrumentation(TestBaseManualFastAPI):
def test_instrument_app_with_instrument(self): def test_instrument_app_with_instrument(self):
if not isinstance(self, TestAutoInstrumentation): if not isinstance(self, TestAutoInstrumentation):
@ -358,7 +411,7 @@ class TestFastAPIManualInstrumentation(TestBaseManualFastAPI):
) )
self.assertTrue(len(scope_metric.metrics) == 3) self.assertTrue(len(scope_metric.metrics) == 3)
for metric in scope_metric.metrics: for metric in scope_metric.metrics:
self.assertIn(metric.name, _expected_metric_names) self.assertIn(metric.name, _expected_metric_names_old)
data_points = list(metric.data.data_points) data_points = list(metric.data.data_points)
self.assertEqual(len(data_points), 1) self.assertEqual(len(data_points), 1)
for point in data_points: for point in data_points:
@ -369,7 +422,71 @@ class TestFastAPIManualInstrumentation(TestBaseManualFastAPI):
number_data_point_seen = True number_data_point_seen = True
for attr in point.attributes: for attr in point.attributes:
self.assertIn( self.assertIn(
attr, _recommended_attrs[metric.name] attr, _recommended_attrs_old[metric.name]
)
self.assertTrue(number_data_point_seen and histogram_data_point_seen)
def test_fastapi_metrics_new_semconv(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.assertEqual(
scope_metric.scope.name,
"opentelemetry.instrumentation.fastapi",
)
self.assertTrue(len(scope_metric.metrics) == 3)
for metric in scope_metric.metrics:
self.assertIn(metric.name, _expected_metric_names_new)
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_new[metric.name]
)
self.assertTrue(number_data_point_seen and histogram_data_point_seen)
def test_fastapi_metrics_both_semconv(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.assertEqual(
scope_metric.scope.name,
"opentelemetry.instrumentation.fastapi",
)
self.assertTrue(len(scope_metric.metrics) == 5)
for metric in scope_metric.metrics:
self.assertIn(metric.name, _expected_metric_names_both)
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_both[metric.name]
) )
self.assertTrue(number_data_point_seen and histogram_data_point_seen) self.assertTrue(number_data_point_seen and histogram_data_point_seen)
@ -378,21 +495,21 @@ class TestFastAPIManualInstrumentation(TestBaseManualFastAPI):
self._client.get("/foobar") self._client.get("/foobar")
duration = max(round((default_timer() - start) * 1000), 0) duration = max(round((default_timer() - start) * 1000), 0)
expected_duration_attributes = { expected_duration_attributes = {
"http.method": "GET", SpanAttributes.HTTP_METHOD: "GET",
"http.host": "testserver:443", SpanAttributes.HTTP_HOST: "testserver:443",
"http.scheme": "https", SpanAttributes.HTTP_SCHEME: "https",
"http.flavor": "1.1", SpanAttributes.HTTP_FLAVOR: "1.1",
"http.server_name": "testserver", SpanAttributes.HTTP_SERVER_NAME: "testserver",
"net.host.port": 443, SpanAttributes.NET_HOST_PORT: 443,
"http.status_code": 200, SpanAttributes.HTTP_STATUS_CODE: 200,
"http.target": "/foobar", SpanAttributes.HTTP_TARGET: "/foobar",
} }
expected_requests_count_attributes = { expected_requests_count_attributes = {
"http.method": "GET", SpanAttributes.HTTP_METHOD: "GET",
"http.host": "testserver:443", SpanAttributes.HTTP_HOST: "testserver:443",
"http.scheme": "https", SpanAttributes.HTTP_SCHEME: "https",
"http.flavor": "1.1", SpanAttributes.HTTP_FLAVOR: "1.1",
"http.server_name": "testserver", SpanAttributes.HTTP_SERVER_NAME: "testserver",
} }
metrics_list = self.memory_metrics_reader.get_metrics_data() metrics_list = self.memory_metrics_reader.get_metrics_data()
for metric in ( for metric in (
@ -413,6 +530,288 @@ class TestFastAPIManualInstrumentation(TestBaseManualFastAPI):
) )
self.assertEqual(point.value, 0) self.assertEqual(point.value, 0)
def test_basic_metric_success_new_semconv(self):
start = default_timer()
self._client.get("/foobar")
duration_s = max(default_timer() - start, 0)
expected_duration_attributes = {
HTTP_REQUEST_METHOD: "GET",
URL_SCHEME: "https",
NETWORK_PROTOCOL_VERSION: "1.1",
HTTP_RESPONSE_STATUS_CODE: 200,
HTTP_ROUTE: "/foobar",
}
expected_requests_count_attributes = {
HTTP_REQUEST_METHOD: "GET",
URL_SCHEME: "https",
}
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)
if metric.name == "http.server.request.duration":
self.assertAlmostEqual(duration_s, point.sum, places=1)
elif metric.name == "http.server.response.body.size":
self.assertEqual(25, point.sum)
elif metric.name == "http.server.request.body.size":
self.assertEqual(25, point.sum)
if isinstance(point, NumberDataPoint):
self.assertDictEqual(
expected_requests_count_attributes,
dict(point.attributes),
)
self.assertEqual(point.value, 0)
def test_basic_metric_success_both_semconv(self):
start = default_timer()
self._client.get("/foobar")
duration = max(round((default_timer() - start) * 1000), 0)
duration_s = max(default_timer() - start, 0)
expected_duration_attributes_old = {
SpanAttributes.HTTP_METHOD: "GET",
SpanAttributes.HTTP_HOST: "testserver:443",
SpanAttributes.HTTP_SCHEME: "https",
SpanAttributes.HTTP_FLAVOR: "1.1",
SpanAttributes.HTTP_SERVER_NAME: "testserver",
SpanAttributes.NET_HOST_PORT: 443,
SpanAttributes.HTTP_STATUS_CODE: 200,
SpanAttributes.HTTP_TARGET: "/foobar",
}
expected_duration_attributes_new = {
HTTP_REQUEST_METHOD: "GET",
URL_SCHEME: "https",
NETWORK_PROTOCOL_VERSION: "1.1",
HTTP_RESPONSE_STATUS_CODE: 200,
HTTP_ROUTE: "/foobar",
}
expected_requests_count_attributes = {
SpanAttributes.HTTP_METHOD: "GET",
SpanAttributes.HTTP_HOST: "testserver:443",
SpanAttributes.HTTP_SCHEME: "https",
SpanAttributes.HTTP_FLAVOR: "1.1",
SpanAttributes.HTTP_SERVER_NAME: "testserver",
HTTP_REQUEST_METHOD: "GET",
URL_SCHEME: "https",
}
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=40)
if metric.name == "http.server.request.duration":
self.assertAlmostEqual(duration_s, point.sum, places=1)
self.assertDictEqual(
expected_duration_attributes_new,
dict(point.attributes),
)
elif metric.name == "http.server.response.body.size":
self.assertEqual(25, point.sum)
self.assertDictEqual(
expected_duration_attributes_new,
dict(point.attributes),
)
elif metric.name == "http.server.request.body.size":
self.assertEqual(25, point.sum)
self.assertDictEqual(
expected_duration_attributes_new,
dict(point.attributes),
)
elif metric.name == "http.server.duration":
self.assertAlmostEqual(duration, point.sum, delta=40)
self.assertDictEqual(
expected_duration_attributes_old,
dict(point.attributes),
)
elif metric.name == "http.server.response.size":
self.assertEqual(25, point.sum)
self.assertDictEqual(
expected_duration_attributes_old,
dict(point.attributes),
)
elif metric.name == "http.server.request.size":
self.assertEqual(25, point.sum)
self.assertDictEqual(
expected_duration_attributes_old,
dict(point.attributes),
)
if isinstance(point, NumberDataPoint):
self.assertDictEqual(
expected_requests_count_attributes,
dict(point.attributes),
)
self.assertEqual(point.value, 0)
def test_basic_metric_nonstandard_http_method_success(self):
start = default_timer()
self._client.request("NONSTANDARD", "/foobar")
duration = max(round((default_timer() - start) * 1000), 0)
expected_duration_attributes = {
SpanAttributes.HTTP_METHOD: "_OTHER",
SpanAttributes.HTTP_HOST: "testserver:443",
SpanAttributes.HTTP_SCHEME: "https",
SpanAttributes.HTTP_FLAVOR: "1.1",
SpanAttributes.HTTP_SERVER_NAME: "testserver",
SpanAttributes.NET_HOST_PORT: 443,
SpanAttributes.HTTP_STATUS_CODE: 405,
SpanAttributes.HTTP_TARGET: "/foobar",
}
expected_requests_count_attributes = {
SpanAttributes.HTTP_METHOD: "_OTHER",
SpanAttributes.HTTP_HOST: "testserver:443",
SpanAttributes.HTTP_SCHEME: "https",
SpanAttributes.HTTP_FLAVOR: "1.1",
SpanAttributes.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=40)
if isinstance(point, NumberDataPoint):
self.assertDictEqual(
expected_requests_count_attributes,
dict(point.attributes),
)
self.assertEqual(point.value, 0)
def test_basic_metric_nonstandard_http_method_success_new_semconv(self):
start = default_timer()
self._client.request("NONSTANDARD", "/foobar")
duration_s = max(default_timer() - start, 0)
expected_duration_attributes = {
HTTP_REQUEST_METHOD: "_OTHER",
URL_SCHEME: "https",
NETWORK_PROTOCOL_VERSION: "1.1",
HTTP_RESPONSE_STATUS_CODE: 405,
HTTP_ROUTE: "/foobar",
}
expected_requests_count_attributes = {
HTTP_REQUEST_METHOD: "_OTHER",
URL_SCHEME: "https",
}
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)
if metric.name == "http.server.request.duration":
self.assertAlmostEqual(duration_s, point.sum, places=1)
elif metric.name == "http.server.response.body.size":
self.assertEqual(31, point.sum)
elif metric.name == "http.server.request.body.size":
self.assertEqual(25, point.sum)
if isinstance(point, NumberDataPoint):
self.assertDictEqual(
expected_requests_count_attributes,
dict(point.attributes),
)
self.assertEqual(point.value, 0)
def test_basic_metric_nonstandard_http_method_success_both_semconv(self):
start = default_timer()
self._client.request("NONSTANDARD", "/foobar")
duration = max(round((default_timer() - start) * 1000), 0)
duration_s = max(default_timer() - start, 0)
expected_duration_attributes_old = {
SpanAttributes.HTTP_METHOD: "_OTHER",
SpanAttributes.HTTP_HOST: "testserver:443",
SpanAttributes.HTTP_SCHEME: "https",
SpanAttributes.HTTP_FLAVOR: "1.1",
SpanAttributes.HTTP_SERVER_NAME: "testserver",
SpanAttributes.NET_HOST_PORT: 443,
SpanAttributes.HTTP_STATUS_CODE: 405,
SpanAttributes.HTTP_TARGET: "/foobar",
}
expected_duration_attributes_new = {
HTTP_REQUEST_METHOD: "_OTHER",
URL_SCHEME: "https",
NETWORK_PROTOCOL_VERSION: "1.1",
HTTP_RESPONSE_STATUS_CODE: 405,
HTTP_ROUTE: "/foobar",
}
expected_requests_count_attributes = {
SpanAttributes.HTTP_METHOD: "_OTHER",
SpanAttributes.HTTP_HOST: "testserver:443",
SpanAttributes.HTTP_SCHEME: "https",
SpanAttributes.HTTP_FLAVOR: "1.1",
SpanAttributes.HTTP_SERVER_NAME: "testserver",
HTTP_REQUEST_METHOD: "_OTHER",
URL_SCHEME: "https",
}
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=40)
if metric.name == "http.server.request.duration":
self.assertAlmostEqual(duration_s, point.sum, places=1)
self.assertDictEqual(
expected_duration_attributes_new,
dict(point.attributes),
)
elif metric.name == "http.server.response.body.size":
self.assertEqual(31, point.sum)
self.assertDictEqual(
expected_duration_attributes_new,
dict(point.attributes),
)
elif metric.name == "http.server.request.body.size":
self.assertEqual(25, point.sum)
self.assertDictEqual(
expected_duration_attributes_new,
dict(point.attributes),
)
elif metric.name == "http.server.duration":
self.assertAlmostEqual(duration, point.sum, delta=40)
self.assertDictEqual(
expected_duration_attributes_old,
dict(point.attributes),
)
elif metric.name == "http.server.response.size":
self.assertEqual(31, point.sum)
self.assertDictEqual(
expected_duration_attributes_old,
dict(point.attributes),
)
elif metric.name == "http.server.request.size":
self.assertEqual(25, point.sum)
self.assertDictEqual(
expected_duration_attributes_old,
dict(point.attributes),
)
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): def test_basic_post_request_metric_success(self):
start = default_timer() start = default_timer()
response = self._client.post( response = self._client.post(
@ -438,6 +837,63 @@ class TestFastAPIManualInstrumentation(TestBaseManualFastAPI):
if isinstance(point, NumberDataPoint): if isinstance(point, NumberDataPoint):
self.assertEqual(point.value, 0) self.assertEqual(point.value, 0)
def test_basic_post_request_metric_success_new_semconv(self):
start = default_timer()
response = self._client.post(
"/foobar",
json={"foo": "bar"},
)
duration_s = max(default_timer() - start, 0)
response_size = int(response.headers.get("content-length"))
request_size = int(response.request.headers.get("content-length"))
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 metric.name == "http.server.request.duration":
self.assertAlmostEqual(duration_s, point.sum, places=1)
elif metric.name == "http.server.response.body.size":
self.assertEqual(response_size, point.sum)
elif metric.name == "http.server.request.body.size":
self.assertEqual(request_size, point.sum)
if isinstance(point, NumberDataPoint):
self.assertEqual(point.value, 0)
def test_basic_post_request_metric_success_both_semconv(self):
start = default_timer()
response = self._client.post(
"/foobar",
json={"foo": "bar"},
)
duration = max(round((default_timer() - start) * 1000), 0)
duration_s = max(default_timer() - start, 0)
response_size = int(response.headers.get("content-length"))
request_size = int(response.request.headers.get("content-length"))
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 metric.name == "http.server.request.duration":
self.assertAlmostEqual(duration_s, point.sum, places=1)
elif metric.name == "http.server.response.body.size":
self.assertEqual(response_size, point.sum)
elif metric.name == "http.server.request.body.size":
self.assertEqual(request_size, point.sum)
elif metric.name == "http.server.duration":
self.assertAlmostEqual(duration, point.sum, delta=40)
elif metric.name == "http.server.response.size":
self.assertEqual(response_size, point.sum)
elif metric.name == "http.server.request.size":
self.assertEqual(request_size, point.sum)
if isinstance(point, NumberDataPoint):
self.assertEqual(point.value, 0)
def test_metric_uninstrument_app(self): def test_metric_uninstrument_app(self):
self._client.get("/foobar") self._client.get("/foobar")
self._instrumentor.uninstrument_app(self._app) self._instrumentor.uninstrument_app(self._app)

View File

@ -543,7 +543,9 @@ class _InstrumentedFlask(flask.Flask):
__name__, __name__,
__version__, __version__,
_InstrumentedFlask._meter_provider, _InstrumentedFlask._meter_provider,
schema_url="https://opentelemetry.io/schemas/1.11.0", schema_url=_get_schema_url(
_InstrumentedFlask._sem_conv_opt_in_mode
),
) )
duration_histogram_old = None duration_histogram_old = None
if _report_old(_InstrumentedFlask._sem_conv_opt_in_mode): if _report_old(_InstrumentedFlask._sem_conv_opt_in_mode):
@ -579,7 +581,9 @@ class _InstrumentedFlask(flask.Flask):
__name__, __name__,
__version__, __version__,
_InstrumentedFlask._tracer_provider, _InstrumentedFlask._tracer_provider,
schema_url="https://opentelemetry.io/schemas/1.11.0", schema_url=_get_schema_url(
_InstrumentedFlask._sem_conv_opt_in_mode
),
) )
_before_request = _wrapped_before_request( _before_request = _wrapped_before_request(