Skip to content

Commit 267d0de

Browse files
committed
WSGI: add span context to metrics
1 parent 2ca9bf9 commit 267d0de

File tree

2 files changed

+78
-2
lines changed

2 files changed

+78
-2
lines changed

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

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -708,19 +708,24 @@ def __call__(
708708
raise
709709
finally:
710710
duration_s = default_timer() - start
711+
active_metric_ctx = trace.set_span_in_context(span)
711712
if self.duration_histogram_old:
712713
duration_attrs_old = _parse_duration_attrs(
713714
req_attrs, _StabilityMode.DEFAULT
714715
)
715716
self.duration_histogram_old.record(
716-
max(round(duration_s * 1000), 0), duration_attrs_old
717+
max(round(duration_s * 1000), 0),
718+
duration_attrs_old,
719+
context=active_metric_ctx,
717720
)
718721
if self.duration_histogram_new:
719722
duration_attrs_new = _parse_duration_attrs(
720723
req_attrs, _StabilityMode.HTTP
721724
)
722725
self.duration_histogram_new.record(
723-
max(duration_s, 0), duration_attrs_new
726+
max(duration_s, 0),
727+
duration_attrs_new,
728+
context=active_metric_ctx,
724729
)
725730
self.active_requests_counter.add(-1, active_requests_count_attrs)
726731

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)