Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -829,25 +829,34 @@ async def __call__(
duration_attrs_new = _parse_duration_attrs(
attributes, _StabilityMode.HTTP
)
span_ctx = set_span_in_context(span)
if self.duration_histogram_old:
self.duration_histogram_old.record(
max(round(duration_s * 1000), 0), duration_attrs_old
max(round(duration_s * 1000), 0),
duration_attrs_old,
context=span_ctx,
)
if self.duration_histogram_new:
self.duration_histogram_new.record(
max(duration_s, 0), duration_attrs_new
max(duration_s, 0),
duration_attrs_new,
context=span_ctx,
)
self.active_requests_counter.add(
-1, active_requests_count_attrs
)
if self.content_length_header:
if self.server_response_size_histogram:
self.server_response_size_histogram.record(
self.content_length_header, duration_attrs_old
self.content_length_header,
duration_attrs_old,
context=span_ctx,
)
if self.server_response_body_size_histogram:
self.server_response_body_size_histogram.record(
self.content_length_header, duration_attrs_new
self.content_length_header,
duration_attrs_new,
context=span_ctx,
)

request_size = asgi_getter.get(scope, "content-length")
Expand All @@ -859,11 +868,15 @@ async def __call__(
else:
if self.server_request_size_histogram:
self.server_request_size_histogram.record(
request_size_amount, duration_attrs_old
request_size_amount,
duration_attrs_old,
context=span_ctx,
)
if self.server_request_body_size_histogram:
self.server_request_body_size_histogram.record(
request_size_amount, duration_attrs_new
request_size_amount,
duration_attrs_new,
context=span_ctx,
)
if token:
context.detach(token)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,55 @@ def setUp(self):

self.env_patch.start()

def subTest(self, msg=..., **params):
sub = super().subTest(msg, **params)
# Reinitialize test state to avoid state pollution
self.setUp()
return sub

# Helper to assert exemplars presence across specified histogram metric names.
def _assert_exemplars_present(
self, metric_names: set[str], context: str = ""
):
metrics_list = self.memory_metrics_reader.get_metrics_data()
print(metrics_list)
metrics = []
for resource_metric in (
getattr(metrics_list, "resource_metrics", []) or []
):
for scope_metric in (
getattr(resource_metric, "scope_metrics", []) or []
):
metrics.extend(getattr(scope_metric, "metrics", []) or [])

found = {name: 0 for name in metric_names}
for metric in metrics:
if metric.name not in metric_names:
continue
for point in metric.data.data_points:
found[metric.name] += 1
exemplars = getattr(point, "exemplars", None)
self.assertIsNotNone(
exemplars,
msg=f"Expected exemplars list attribute on histogram data point for {metric.name} ({context})",
)
self.assertGreater(
len(exemplars or []),
0,
msg=f"Expected at least one exemplar on histogram data point for {metric.name} ({context}) but none found.",
)
for ex in exemplars or []:
if hasattr(ex, "span_id"):
self.assertNotEqual(ex.span_id, 0)
if hasattr(ex, "trace_id"):
self.assertNotEqual(ex.trace_id, 0)
for name, count in found.items():
self.assertGreater(
count,
0,
msg=f"Did not encounter any data points for metric {name} while checking exemplars ({context}).",
)

# pylint: disable=too-many-locals
def validate_outputs(
self,
Expand Down Expand Up @@ -921,9 +970,6 @@ def update_expected_synthetic_bot(
outputs, modifiers=[update_expected_synthetic_bot]
)

# Clear spans after each test case to prevent accumulation
self.memory_exporter.clear()

async def test_user_agent_synthetic_test_detection(self):
"""Test that test user agents are detected as synthetic with type 'test'"""
test_cases = [
Expand Down Expand Up @@ -958,9 +1004,6 @@ def update_expected_synthetic_test(
outputs, modifiers=[update_expected_synthetic_test]
)

# Clear spans after each test case to prevent accumulation
self.memory_exporter.clear()

async def test_user_agent_non_synthetic(self):
"""Test that normal user agents are not marked as synthetic"""
test_cases = [
Expand Down Expand Up @@ -996,9 +1039,6 @@ def update_expected_non_synthetic(
outputs, modifiers=[update_expected_non_synthetic]
)

# Clear spans after each test case to prevent accumulation
self.memory_exporter.clear()

async def test_user_agent_synthetic_new_semconv(self):
"""Test synthetic user agent detection with new semantic conventions"""
user_agent = b"Mozilla/5.0 (compatible; Googlebot/2.1)"
Expand Down Expand Up @@ -1534,6 +1574,40 @@ async def test_asgi_metrics_both_semconv(self):
)
self.assertTrue(number_data_point_seen and histogram_data_point_seen)

async def test_asgi_metrics_exemplars_expected_old_semconv(self):
"""Failing test placeholder asserting exemplars should be present for duration histogram (old semconv)."""
app = otel_asgi.OpenTelemetryMiddleware(simple_asgi)
for _ in range(5):
self.seed_app(app)
await self.send_default_request()
await self.get_all_output()
self._assert_exemplars_present(
{"http.server.duration"}, context="old semconv"
)

async def test_asgi_metrics_exemplars_expected_new_semconv(self):
"""Failing test placeholder asserting exemplars should be present for request duration histogram (new semconv)."""
app = otel_asgi.OpenTelemetryMiddleware(simple_asgi)
for _ in range(5):
self.seed_app(app)
await self.send_default_request()
await self.get_all_output()
self._assert_exemplars_present(
{"http.server.request.duration"}, context="new semconv"
)

async def test_asgi_metrics_exemplars_expected_both_semconv(self):
"""Failing test placeholder asserting exemplars should be present for both duration histograms when both semconv modes enabled."""
app = otel_asgi.OpenTelemetryMiddleware(simple_asgi)
for _ in range(5):
self.seed_app(app)
await self.send_default_request()
await self.get_all_output()
self._assert_exemplars_present(
{"http.server.duration", "http.server.request.duration"},
context="both semconv",
)

async def test_basic_metric_success(self):
app = otel_asgi.OpenTelemetryMiddleware(simple_asgi)
self.seed_app(app)
Expand Down Expand Up @@ -1569,7 +1643,7 @@ async def test_basic_metric_success(self):
self.assertEqual(point.count, 1)
if metric.name == "http.server.duration":
self.assertAlmostEqual(
duration, point.sum, delta=5
duration, point.sum, delta=30
)
elif metric.name == "http.server.response.size":
self.assertEqual(1024, point.sum)
Expand Down Expand Up @@ -1754,7 +1828,7 @@ async def test_basic_metric_success_both_semconv(self):
)
elif metric.name == "http.server.duration":
self.assertAlmostEqual(
duration, point.sum, delta=5
duration, point.sum, delta=30
)
self.assertDictEqual(
expected_duration_attributes_old,
Expand Down