mirror of
https://github.com/open-telemetry/opentelemetry-python-contrib.git
synced 2025-07-29 21:23:55 +08:00
HTTP semantic convention stability migration for fastapi (#2682)
This commit is contained in:
@ -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))
|
||||
- `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))
|
||||
- `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
|
||||
([#2631](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2631))
|
||||
- `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
|
||||
([#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))
|
||||
|
||||
- Populate `{method}` as `HTTP` on `_OTHER` methods from scope for `fastapi` middleware
|
||||
([#2682](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2682))
|
||||
|
||||
### Fixed
|
||||
|
||||
|
@ -19,7 +19,7 @@
|
||||
| [opentelemetry-instrumentation-django](./opentelemetry-instrumentation-django) | django >= 1.10 | Yes | 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-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-grpc](./opentelemetry-instrumentation-grpc) | grpcio ~= 1.27 | No | experimental
|
||||
| [opentelemetry-instrumentation-httpx](./opentelemetry-instrumentation-httpx) | httpx >= 0.18.0 | No | migration
|
||||
|
@ -177,6 +177,12 @@ from typing import Collection
|
||||
import fastapi
|
||||
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.types import (
|
||||
ClientRequestHook,
|
||||
@ -189,7 +195,11 @@ from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
|
||||
from opentelemetry.metrics import get_meter
|
||||
from opentelemetry.semconv.trace import SpanAttributes
|
||||
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")
|
||||
_logger = logging.getLogger(__name__)
|
||||
@ -218,6 +228,11 @@ class FastAPIInstrumentor(BaseInstrumentor):
|
||||
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:
|
||||
excluded_urls = _excluded_urls_from_env
|
||||
else:
|
||||
@ -226,13 +241,13 @@ class FastAPIInstrumentor(BaseInstrumentor):
|
||||
__name__,
|
||||
__version__,
|
||||
tracer_provider,
|
||||
schema_url="https://opentelemetry.io/schemas/1.11.0",
|
||||
schema_url=_get_schema_url(sem_conv_opt_in_mode),
|
||||
)
|
||||
meter = get_meter(
|
||||
__name__,
|
||||
__version__,
|
||||
meter_provider,
|
||||
schema_url="https://opentelemetry.io/schemas/1.11.0",
|
||||
schema_url=_get_schema_url(sem_conv_opt_in_mode),
|
||||
)
|
||||
|
||||
app.add_middleware(
|
||||
@ -303,6 +318,7 @@ class _InstrumentedFastAPI(fastapi.FastAPI):
|
||||
_client_request_hook: ClientRequestHook = None
|
||||
_client_response_hook: ClientResponseHook = None
|
||||
_instrumented_fastapi_apps = set()
|
||||
_sem_conv_opt_in_mode = _HTTPStabilityMode.DEFAULT
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
@ -310,13 +326,17 @@ class _InstrumentedFastAPI(fastapi.FastAPI):
|
||||
__name__,
|
||||
__version__,
|
||||
_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(
|
||||
__name__,
|
||||
__version__,
|
||||
_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(
|
||||
OpenTelemetryMiddleware,
|
||||
@ -373,8 +393,10 @@ def _get_default_span_details(scope):
|
||||
A tuple of span name and attributes
|
||||
"""
|
||||
route = _get_route_details(scope)
|
||||
method = scope.get("method", "")
|
||||
method = sanitize_method(scope.get("method", "").strip())
|
||||
attributes = {}
|
||||
if method == "_OTHER":
|
||||
method = "HTTP"
|
||||
if route:
|
||||
attributes[SpanAttributes.HTTP_ROUTE] = route
|
||||
if method and route: # http
|
||||
|
@ -16,3 +16,5 @@
|
||||
_instruments = ("fastapi ~= 0.58",)
|
||||
|
||||
_supports_metrics = True
|
||||
|
||||
_semconv_status = "migration"
|
||||
|
@ -11,6 +11,9 @@
|
||||
# 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.
|
||||
|
||||
# pylint: disable=too-many-lines
|
||||
|
||||
import unittest
|
||||
from timeit import default_timer
|
||||
from unittest.mock import patch
|
||||
@ -20,39 +23,77 @@ from fastapi.middleware.httpsredirect import HTTPSRedirectMiddleware
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
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.sdk.metrics.export import (
|
||||
HistogramDataPoint,
|
||||
NumberDataPoint,
|
||||
)
|
||||
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.test.test_base import TestBase
|
||||
from opentelemetry.util.http import (
|
||||
_active_requests_count_attrs,
|
||||
_duration_attrs,
|
||||
get_excluded_urls,
|
||||
)
|
||||
from opentelemetry.util.http import get_excluded_urls
|
||||
|
||||
_expected_metric_names = [
|
||||
_expected_metric_names_old = [
|
||||
"http.server.active_requests",
|
||||
"http.server.duration",
|
||||
"http.server.response.size",
|
||||
"http.server.request.size",
|
||||
]
|
||||
_recommended_attrs = {
|
||||
"http.server.active_requests": _active_requests_count_attrs,
|
||||
"http.server.duration": {*_duration_attrs, SpanAttributes.HTTP_TARGET},
|
||||
_expected_metric_names_new = [
|
||||
"http.server.active_requests",
|
||||
"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": {
|
||||
*_duration_attrs,
|
||||
*_server_duration_attrs_old,
|
||||
SpanAttributes.HTTP_TARGET,
|
||||
},
|
||||
"http.server.request.size": {
|
||||
*_duration_attrs,
|
||||
*_server_duration_attrs_old,
|
||||
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):
|
||||
def _create_app(self):
|
||||
@ -88,10 +129,23 @@ class TestBaseFastAPI(TestBase):
|
||||
|
||||
def setUp(self):
|
||||
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(
|
||||
"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.exclude_patch = patch(
|
||||
"opentelemetry.instrumentation.fastapi._excluded_urls_from_env",
|
||||
@ -142,7 +196,6 @@ class TestBaseFastAPI(TestBase):
|
||||
|
||||
|
||||
class TestBaseManualFastAPI(TestBaseFastAPI):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
if cls is TestBaseManualFastAPI:
|
||||
@ -196,7 +249,6 @@ class TestBaseManualFastAPI(TestBaseFastAPI):
|
||||
|
||||
|
||||
class TestBaseAutoFastAPI(TestBaseFastAPI):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
if cls is TestBaseAutoFastAPI:
|
||||
@ -259,6 +311,7 @@ class TestBaseAutoFastAPI(TestBaseFastAPI):
|
||||
)
|
||||
|
||||
|
||||
# pylint: disable=too-many-public-methods
|
||||
class TestFastAPIManualInstrumentation(TestBaseManualFastAPI):
|
||||
def test_instrument_app_with_instrument(self):
|
||||
if not isinstance(self, TestAutoInstrumentation):
|
||||
@ -358,7 +411,7 @@ class TestFastAPIManualInstrumentation(TestBaseManualFastAPI):
|
||||
)
|
||||
self.assertTrue(len(scope_metric.metrics) == 3)
|
||||
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)
|
||||
self.assertEqual(len(data_points), 1)
|
||||
for point in data_points:
|
||||
@ -369,7 +422,71 @@ class TestFastAPIManualInstrumentation(TestBaseManualFastAPI):
|
||||
number_data_point_seen = True
|
||||
for attr in point.attributes:
|
||||
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)
|
||||
|
||||
@ -378,21 +495,21 @@ class TestFastAPIManualInstrumentation(TestBaseManualFastAPI):
|
||||
self._client.get("/foobar")
|
||||
duration = max(round((default_timer() - start) * 1000), 0)
|
||||
expected_duration_attributes = {
|
||||
"http.method": "GET",
|
||||
"http.host": "testserver:443",
|
||||
"http.scheme": "https",
|
||||
"http.flavor": "1.1",
|
||||
"http.server_name": "testserver",
|
||||
"net.host.port": 443,
|
||||
"http.status_code": 200,
|
||||
"http.target": "/foobar",
|
||||
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_requests_count_attributes = {
|
||||
"http.method": "GET",
|
||||
"http.host": "testserver:443",
|
||||
"http.scheme": "https",
|
||||
"http.flavor": "1.1",
|
||||
"http.server_name": "testserver",
|
||||
SpanAttributes.HTTP_METHOD: "GET",
|
||||
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 (
|
||||
@ -413,6 +530,288 @@ class TestFastAPIManualInstrumentation(TestBaseManualFastAPI):
|
||||
)
|
||||
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):
|
||||
start = default_timer()
|
||||
response = self._client.post(
|
||||
@ -438,6 +837,63 @@ class TestFastAPIManualInstrumentation(TestBaseManualFastAPI):
|
||||
if isinstance(point, NumberDataPoint):
|
||||
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):
|
||||
self._client.get("/foobar")
|
||||
self._instrumentor.uninstrument_app(self._app)
|
||||
|
@ -543,7 +543,9 @@ class _InstrumentedFlask(flask.Flask):
|
||||
__name__,
|
||||
__version__,
|
||||
_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
|
||||
if _report_old(_InstrumentedFlask._sem_conv_opt_in_mode):
|
||||
@ -579,7 +581,9 @@ class _InstrumentedFlask(flask.Flask):
|
||||
__name__,
|
||||
__version__,
|
||||
_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(
|
||||
|
Reference in New Issue
Block a user