Skip to content

Commit 4450d3a

Browse files
authored
Merge branch 'main' into update_prom_rw_exporter
2 parents 4944452 + d5369a4 commit 4450d3a

File tree

3 files changed

+101
-0
lines changed

3 files changed

+101
-0
lines changed

CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1818
([#1369](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1369))
1919
- `opentelemetry-instrumentation-system-metrics` add supports to collect system thread count. ([#1339](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1339))
2020
- `opentelemetry-exporter-richconsole` Fixing RichConsoleExpoter to allow multiple traces, fixing duplicate spans and include resources ([#1336](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1336))
21+
- `opentelemetry-instrumentation-asgi` metrics record target attribute (FastAPI only)
22+
([#1323](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1323))
2123

2224
## [1.13.0-0.34b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.13.0-0.34b0) - 2022-09-26
2325

instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/__init__.py

+35
Original file line numberDiff line numberDiff line change
@@ -366,6 +366,32 @@ def get_default_span_details(scope: dict) -> Tuple[str, dict]:
366366
return span_name, {}
367367

368368

369+
def _collect_target_attribute(
370+
scope: typing.Dict[str, typing.Any]
371+
) -> typing.Optional[str]:
372+
"""
373+
Returns the target path as defined by the Semantic Conventions.
374+
375+
This value is suitable to use in metrics as it should replace concrete
376+
values with a parameterized name. Example: /api/users/{user_id}
377+
378+
Refer to the specification
379+
https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/semantic_conventions/http-metrics.md#parameterized-attributes
380+
381+
Note: this function requires specific code for each framework, as there's no
382+
standard attribute to use.
383+
"""
384+
# FastAPI
385+
root_path = scope.get("root_path", "")
386+
387+
route = scope.get("route")
388+
path_format = getattr(route, "path_format", None)
389+
if path_format:
390+
return f"{root_path}{path_format}"
391+
392+
return None
393+
394+
369395
class OpenTelemetryMiddleware:
370396
"""The ASGI application middleware.
371397
@@ -387,6 +413,7 @@ class OpenTelemetryMiddleware:
387413
the current globally configured one is used.
388414
"""
389415

416+
# pylint: disable=too-many-branches
390417
def __init__(
391418
self,
392419
app,
@@ -454,6 +481,12 @@ async def __call__(self, scope, receive, send):
454481
attributes
455482
)
456483
duration_attrs = _parse_duration_attrs(attributes)
484+
485+
target = _collect_target_attribute(scope)
486+
if target:
487+
active_requests_count_attrs[SpanAttributes.HTTP_TARGET] = target
488+
duration_attrs[SpanAttributes.HTTP_TARGET] = target
489+
457490
if scope["type"] == "http":
458491
self.active_requests_counter.add(1, active_requests_count_attrs)
459492
try:
@@ -496,6 +529,8 @@ async def __call__(self, scope, receive, send):
496529
if token:
497530
context.detach(token)
498531

532+
# pylint: enable=too-many-branches
533+
499534
def _get_otel_receive(self, server_span_name, scope, receive):
500535
@wraps(receive)
501536
async def otel_receive():

instrumentation/opentelemetry-instrumentation-asgi/tests/test_asgi_middleware.py

+64
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15+
# pylint: disable=too-many-lines
16+
1517
import sys
1618
import unittest
1719
from timeit import default_timer
@@ -626,6 +628,37 @@ def test_basic_metric_success(self):
626628
)
627629
self.assertEqual(point.value, 0)
628630

631+
def test_metric_target_attribute(self):
632+
expected_target = "/api/user/{id}"
633+
634+
class TestRoute:
635+
path_format = expected_target
636+
637+
self.scope["route"] = TestRoute()
638+
app = otel_asgi.OpenTelemetryMiddleware(simple_asgi)
639+
self.seed_app(app)
640+
self.send_default_request()
641+
642+
metrics_list = self.memory_metrics_reader.get_metrics_data()
643+
assertions = 0
644+
for resource_metric in metrics_list.resource_metrics:
645+
for scope_metrics in resource_metric.scope_metrics:
646+
for metric in scope_metrics.metrics:
647+
for point in metric.data.data_points:
648+
if isinstance(point, HistogramDataPoint):
649+
self.assertEqual(
650+
point.attributes["http.target"],
651+
expected_target,
652+
)
653+
assertions += 1
654+
elif isinstance(point, NumberDataPoint):
655+
self.assertEqual(
656+
point.attributes["http.target"],
657+
expected_target,
658+
)
659+
assertions += 1
660+
self.assertEqual(assertions, 2)
661+
629662
def test_no_metric_for_websockets(self):
630663
self.scope = {
631664
"type": "websocket",
@@ -719,6 +752,37 @@ def test_credential_removal(self):
719752
attrs[SpanAttributes.HTTP_URL], "http://httpbin.org/status/200"
720753
)
721754

755+
def test_collect_target_attribute_missing(self):
756+
self.assertIsNone(otel_asgi._collect_target_attribute(self.scope))
757+
758+
def test_collect_target_attribute_fastapi(self):
759+
class TestRoute:
760+
path_format = "/api/users/{user_id}"
761+
762+
self.scope["route"] = TestRoute()
763+
self.assertEqual(
764+
otel_asgi._collect_target_attribute(self.scope),
765+
"/api/users/{user_id}",
766+
)
767+
768+
def test_collect_target_attribute_fastapi_mounted(self):
769+
class TestRoute:
770+
path_format = "/users/{user_id}"
771+
772+
self.scope["route"] = TestRoute()
773+
self.scope["root_path"] = "/api/v2"
774+
self.assertEqual(
775+
otel_asgi._collect_target_attribute(self.scope),
776+
"/api/v2/users/{user_id}",
777+
)
778+
779+
def test_collect_target_attribute_fastapi_starlette_invalid(self):
780+
self.scope["route"] = object()
781+
self.assertIsNone(
782+
otel_asgi._collect_target_attribute(self.scope),
783+
"HTTP_TARGET values is not None",
784+
)
785+
722786

723787
class TestWrappedApplication(AsgiTestBase):
724788
def test_mark_span_internal_in_presence_of_span_from_other_framework(self):

0 commit comments

Comments
 (0)