Skip to content

Commit 889a129

Browse files
authored
Merge pull request #1 from Chromatius/fix/add-exemplars
Fix/add exemplars
2 parents dba7521 + ebefb45 commit 889a129

File tree

4 files changed

+171
-8
lines changed

4 files changed

+171
-8
lines changed

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

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -814,25 +814,34 @@ async def __call__(
814814
duration_attrs_new = _parse_duration_attrs(
815815
attributes, _StabilityMode.HTTP
816816
)
817+
span_ctx = set_span_in_context(span)
817818
if self.duration_histogram_old:
818819
self.duration_histogram_old.record(
819-
max(round(duration_s * 1000), 0), duration_attrs_old
820+
max(round(duration_s * 1000), 0),
821+
duration_attrs_old,
822+
context=span_ctx,
820823
)
821824
if self.duration_histogram_new:
822825
self.duration_histogram_new.record(
823-
max(duration_s, 0), duration_attrs_new
826+
max(duration_s, 0),
827+
duration_attrs_new,
828+
context=span_ctx,
824829
)
825830
self.active_requests_counter.add(
826831
-1, active_requests_count_attrs
827832
)
828833
if self.content_length_header:
829834
if self.server_response_size_histogram:
830835
self.server_response_size_histogram.record(
831-
self.content_length_header, duration_attrs_old
836+
self.content_length_header,
837+
duration_attrs_old,
838+
context=span_ctx,
832839
)
833840
if self.server_response_body_size_histogram:
834841
self.server_response_body_size_histogram.record(
835-
self.content_length_header, duration_attrs_new
842+
self.content_length_header,
843+
duration_attrs_new,
844+
context=span_ctx,
836845
)
837846

838847
request_size = asgi_getter.get(scope, "content-length")
@@ -844,11 +853,15 @@ async def __call__(
844853
else:
845854
if self.server_request_size_histogram:
846855
self.server_request_size_histogram.record(
847-
request_size_amount, duration_attrs_old
856+
request_size_amount,
857+
duration_attrs_old,
858+
context=span_ctx,
848859
)
849860
if self.server_request_body_size_histogram:
850861
self.server_request_body_size_histogram.record(
851-
request_size_amount, duration_attrs_new
862+
request_size_amount,
863+
duration_attrs_new,
864+
context=span_ctx,
852865
)
853866
if token:
854867
context.detach(token)

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

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,46 @@ def setUp(self):
312312

313313
self.env_patch.start()
314314

315+
# Helper to assert exemplars presence across specified histogram metric names.
316+
def _assert_exemplars_present(
317+
self, metric_names: set[str], context: str = ""
318+
):
319+
metrics_list = self.memory_metrics_reader.get_metrics_data()
320+
print(metrics_list)
321+
found = {name: 0 for name in metric_names}
322+
for resource_metric in (
323+
getattr(metrics_list, "resource_metrics", []) or []
324+
):
325+
for scope_metric in (
326+
getattr(resource_metric, "scope_metrics", []) or []
327+
):
328+
for metric in getattr(scope_metric, "metrics", []) or []:
329+
if metric.name not in metric_names:
330+
continue
331+
for point in metric.data.data_points:
332+
found[metric.name] += 1
333+
exemplars = getattr(point, "exemplars", None)
334+
self.assertIsNotNone(
335+
exemplars,
336+
msg=f"Expected exemplars list attribute on histogram data point for {metric.name} ({context})",
337+
)
338+
self.assertGreater(
339+
len(exemplars or []),
340+
0,
341+
msg=f"Expected at least one exemplar on histogram data point for {metric.name} ({context}) but none found.",
342+
)
343+
for ex in exemplars or []:
344+
if hasattr(ex, "span_id"):
345+
self.assertNotEqual(ex.span_id, 0)
346+
if hasattr(ex, "trace_id"):
347+
self.assertNotEqual(ex.trace_id, 0)
348+
for name, count in found.items():
349+
self.assertGreater(
350+
count,
351+
0,
352+
msg=f"Did not encounter any data points for metric {name} while checking exemplars ({context}).",
353+
)
354+
315355
# pylint: disable=too-many-locals
316356
def validate_outputs(
317357
self,
@@ -1392,6 +1432,40 @@ async def test_asgi_metrics_both_semconv(self):
13921432
)
13931433
self.assertTrue(number_data_point_seen and histogram_data_point_seen)
13941434

1435+
async def test_asgi_metrics_exemplars_expected_old_semconv(self):
1436+
"""Failing test placeholder asserting exemplars should be present for duration histogram (old semconv)."""
1437+
app = otel_asgi.OpenTelemetryMiddleware(simple_asgi)
1438+
for _ in range(5):
1439+
self.seed_app(app)
1440+
await self.send_default_request()
1441+
await self.get_all_output()
1442+
self._assert_exemplars_present(
1443+
{"http.server.duration"}, context="old semconv"
1444+
)
1445+
1446+
async def test_asgi_metrics_exemplars_expected_new_semconv(self):
1447+
"""Failing test placeholder asserting exemplars should be present for request duration histogram (new semconv)."""
1448+
app = otel_asgi.OpenTelemetryMiddleware(simple_asgi)
1449+
for _ in range(5):
1450+
self.seed_app(app)
1451+
await self.send_default_request()
1452+
await self.get_all_output()
1453+
self._assert_exemplars_present(
1454+
{"http.server.request.duration"}, context="new semconv"
1455+
)
1456+
1457+
async def test_asgi_metrics_exemplars_expected_both_semconv(self):
1458+
"""Failing test placeholder asserting exemplars should be present for both duration histograms when both semconv modes enabled."""
1459+
app = otel_asgi.OpenTelemetryMiddleware(simple_asgi)
1460+
for _ in range(5):
1461+
self.seed_app(app)
1462+
await self.send_default_request()
1463+
await self.get_all_output()
1464+
self._assert_exemplars_present(
1465+
{"http.server.duration", "http.server.request.duration"},
1466+
context="both semconv",
1467+
)
1468+
13951469
async def test_basic_metric_success(self):
13961470
app = otel_asgi.OpenTelemetryMiddleware(simple_asgi)
13971471
self.seed_app(app)

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

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -705,19 +705,24 @@ def __call__(
705705
raise
706706
finally:
707707
duration_s = default_timer() - start
708+
active_metric_ctx = trace.set_span_in_context(span)
708709
if self.duration_histogram_old:
709710
duration_attrs_old = _parse_duration_attrs(
710711
req_attrs, _StabilityMode.DEFAULT
711712
)
712713
self.duration_histogram_old.record(
713-
max(round(duration_s * 1000), 0), duration_attrs_old
714+
max(round(duration_s * 1000), 0),
715+
duration_attrs_old,
716+
context=active_metric_ctx,
714717
)
715718
if self.duration_histogram_new:
716719
duration_attrs_new = _parse_duration_attrs(
717720
req_attrs, _StabilityMode.HTTP
718721
)
719722
self.duration_histogram_new.record(
720-
max(duration_s, 0), duration_attrs_new
723+
max(duration_s, 0),
724+
duration_attrs_new,
725+
context=active_metric_ctx,
721726
)
722727
self.active_requests_counter.add(-1, active_requests_count_attrs)
723728
span.end()

instrumentation/opentelemetry-instrumentation-wsgi/tests/test_wsgi_middleware.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,41 @@ def validate_response(
291291
expected_attributes[HTTP_REQUEST_METHOD] = http_method
292292
self.assertEqual(span_list[0].attributes, expected_attributes)
293293

294+
# Helper modeled after ASGI test suite to assert presence of exemplars on histogram metrics
295+
def _assert_exemplars_present(self, metric_names, context=""):
296+
metrics_data = self.memory_metrics_reader.get_metrics_data()
297+
self.assertTrue(
298+
len(metrics_data.resource_metrics) > 0,
299+
f"No resource metrics collected while checking exemplars ({context})",
300+
)
301+
checked = set()
302+
for resource_metric in metrics_data.resource_metrics:
303+
for scope_metric in resource_metric.scope_metrics:
304+
for metric in scope_metric.metrics:
305+
if metric.name not in metric_names:
306+
continue
307+
checked.add(metric.name)
308+
# Expect exactly one datapoint per histogram metric in these tests
309+
data_points = list(metric.data.data_points)
310+
self.assertGreater(
311+
len(data_points),
312+
0,
313+
f"No data points for {metric.name} while checking exemplars ({context})",
314+
)
315+
for point in data_points:
316+
if isinstance(point, HistogramDataPoint):
317+
self.assertGreater(
318+
len(point.exemplars),
319+
0,
320+
f"Expected at least one exemplar on histogram data point for {metric.name} ({context}) but none found.",
321+
)
322+
# Ensure we actually saw all targeted metrics
323+
self.assertSetEqual(
324+
set(metric_names),
325+
checked,
326+
f"Did not observe all targeted metrics when asserting exemplars ({context}). Expected {metric_names} got {checked}",
327+
)
328+
294329
def test_basic_wsgi_call(self):
295330
app = otel_wsgi.OpenTelemetryMiddleware(simple_wsgi)
296331
response = app(self.environ, self.start_response)
@@ -415,6 +450,42 @@ def test_wsgi_metrics(self):
415450
)
416451
self.assertTrue(number_data_point_seen and histogram_data_point_seen)
417452

453+
def test_wsgi_metrics_exemplars_expected_old_semconv(self): # type: ignore[func-returns-value]
454+
"""Failing test asserting exemplars should be present for duration histogram (old semconv)."""
455+
app = otel_wsgi.OpenTelemetryMiddleware(simple_wsgi)
456+
# generate several requests to increase chance of exemplar sampling
457+
for _ in range(5):
458+
response = app(self.environ, self.start_response)
459+
# exhaust response iterable
460+
for _ in response:
461+
pass
462+
self._assert_exemplars_present(
463+
{"http.server.duration"}, context="old semconv"
464+
)
465+
466+
def test_wsgi_metrics_exemplars_expected_new_semconv(self): # type: ignore[func-returns-value]
467+
"""Failing test asserting exemplars should be present for request duration histogram (new semconv)."""
468+
app = otel_wsgi.OpenTelemetryMiddleware(simple_wsgi)
469+
for _ in range(5):
470+
response = app(self.environ, self.start_response)
471+
for _ in response:
472+
pass
473+
self._assert_exemplars_present(
474+
{"http.server.request.duration"}, context="new semconv"
475+
)
476+
477+
def test_wsgi_metrics_exemplars_expected_both_semconv(self): # type: ignore[func-returns-value]
478+
"""Failing test asserting exemplars should be present for both duration histograms when both semconv modes enabled."""
479+
app = otel_wsgi.OpenTelemetryMiddleware(simple_wsgi)
480+
for _ in range(5):
481+
response = app(self.environ, self.start_response)
482+
for _ in response:
483+
pass
484+
self._assert_exemplars_present(
485+
{"http.server.duration", "http.server.request.duration"},
486+
context="both semconv",
487+
)
488+
418489
def test_wsgi_metrics_new_semconv(self):
419490
# pylint: disable=too-many-nested-blocks
420491
app = otel_wsgi.OpenTelemetryMiddleware(error_wsgi_unhandled)

0 commit comments

Comments
 (0)