Skip to content

Commit 035ae2e

Browse files
Add FalconInstrumentor labeler custom attrs metrics support
1 parent f7df7db commit 035ae2e

File tree

4 files changed

+184
-9
lines changed

4 files changed

+184
-9
lines changed

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2828
([#3666](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3666))
2929
- `opentelemetry-sdk-extension-aws` Add AWS X-Ray Remote Sampler with initial Rules Poller implementation
3030
([#3366](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3366))
31-
- `opentelemetry-instrumentation`, `opentelemetry-instrumentation-flask`, `opentelemetry-instrumentation-wsgi`, `opentelemetry-instrumentation-django`: Add Labeler utility. Add FlaskInstrumentor, DjangoInstrumentor, WsgiInstrumentor support of custom attributes merging for HTTP duration metrics.
31+
- `opentelemetry-instrumentation`, `opentelemetry-instrumentation-flask`, `opentelemetry-instrumentation-wsgi`, `opentelemetry-instrumentation-django`, `opentelemetry-instrumentation-falcon`: Add Labeler utility. Add FalconInstrumentor, FlaskInstrumentor, DjangoInstrumentor, WsgiInstrumentor support of custom attributes merging for HTTP duration metrics.
3232
([#3689](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3689))
3333

3434
## Version 1.36.0/0.57b0 (2025-07-29)

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

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,38 @@ def response_hook(span, req, resp):
180180
Note:
181181
The environment variable names used to capture HTTP headers are still experimental, and thus are subject to change.
182182
183+
Custom Metrics Attributes using Labeler
184+
***************************************
185+
The Falcon instrumentation reads from a Labeler utility that supports adding custom attributes
186+
to the HTTP duration metrics recorded by the instrumentation.
187+
188+
189+
.. code-block:: python
190+
191+
import falcon
192+
from opentelemetry.instrumentation._labeler import get_labeler
193+
from opentelemetry.instrumentation.falcon import FalconInstrumentor
194+
195+
FalconInstrumentor().instrument()
196+
197+
app = falcon.App()
198+
199+
class UserProfileResource:
200+
def on_get(self, req, resp, user_id):
201+
# Get the labeler for the current request
202+
labeler = get_labeler()
203+
# Add custom attributes to Falcon instrumentation metrics
204+
labeler.add("user_id", user_id)
205+
labeler.add("user_type", "registered")
206+
# Or, add multiple attributes at once
207+
labeler.add_attributes({
208+
"feature_flag": "new_ui",
209+
"experiment_group": "control"
210+
})
211+
resp.text = f'User profile for {user_id}'
212+
213+
app.add_route('/user/{user_id}', UserProfileResource())
214+
183215
API
184216
---
185217
"""
@@ -195,6 +227,7 @@ def response_hook(span, req, resp):
195227

196228
import opentelemetry.instrumentation.wsgi as otel_wsgi
197229
from opentelemetry import context, trace
230+
from opentelemetry.instrumentation._labeler import enhance_metric_attributes
198231
from opentelemetry.instrumentation._semconv import (
199232
HTTP_DURATION_HISTOGRAM_BUCKETS_NEW,
200233
_get_schema_url,
@@ -420,13 +453,17 @@ def _start_response(status, response_headers, *args, **kwargs):
420453
duration_attrs = otel_wsgi._parse_duration_attrs(
421454
attributes, _StabilityMode.DEFAULT
422455
)
456+
# Enhance attributes with any custom labeler attributes
457+
duration_attrs = enhance_metric_attributes(duration_attrs)
423458
self.duration_histogram_old.record(
424459
max(round(duration_s * 1000), 0), duration_attrs
425460
)
426461
if self.duration_histogram_new:
427462
duration_attrs = otel_wsgi._parse_duration_attrs(
428463
attributes, _StabilityMode.HTTP
429464
)
465+
# Enhance attributes with any custom labeler attributes
466+
duration_attrs = enhance_metric_attributes(duration_attrs)
430467
self.duration_histogram_new.record(
431468
max(duration_s, 0), duration_attrs
432469
)

instrumentation/opentelemetry-instrumentation-falcon/tests/app.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import falcon
22
from packaging import version as package_version
33

4+
from opentelemetry.instrumentation._labeler import (
5+
get_labeler,
6+
)
7+
48
# pylint:disable=R0201,W0613,E0602
59

610

@@ -75,6 +79,21 @@ def on_get(self, req, resp, user_id):
7579
resp.text = f"Hello user {user_id}"
7680

7781

82+
class UserLabelerResource:
83+
def on_get(self, req, resp, user_id):
84+
labeler = get_labeler()
85+
labeler.add("custom_attr", "test_value")
86+
labeler.add_attributes({"endpoint_type": "test", "feature_flag": True})
87+
# pylint: disable=no-member
88+
resp.status = falcon.HTTP_200
89+
90+
if _parsed_falcon_version < package_version.parse("3.0.0"):
91+
# Falcon 1 and Falcon 2
92+
resp.body = f"Hello user {user_id}"
93+
else:
94+
resp.text = f"Hello user {user_id}"
95+
96+
7897
def make_app():
7998
if _parsed_falcon_version < package_version.parse("3.0.0"):
8099
# Falcon 1 and Falcon 2
@@ -90,5 +109,6 @@ def make_app():
90109
"/test_custom_response_headers", CustomResponseHeaderResource()
91110
)
92111
app.add_route("/user/{user_id}", UserResource())
112+
app.add_route("/user_custom_attr/{user_id}", UserLabelerResource())
93113

94114
return app

instrumentation/opentelemetry-instrumentation-falcon/tests/test_falcon.py

Lines changed: 126 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
from packaging import version as package_version
2222

2323
from opentelemetry import trace
24+
from opentelemetry.instrumentation._labeler import clear_labeler
2425
from opentelemetry.instrumentation._semconv import (
2526
HTTP_DURATION_HISTOGRAM_BUCKETS_NEW,
2627
OTEL_SEMCONV_STABILITY_OPT_IN,
@@ -103,12 +104,35 @@
103104
"http.server.request.duration": _server_duration_attrs_new,
104105
}
105106

107+
_custom_attributes = ["custom_attr", "endpoint_type", "feature_flag"]
108+
_server_duration_attrs_old_with_custom = _server_duration_attrs_old.copy()
109+
_server_duration_attrs_old_with_custom.append("http.target")
110+
_server_duration_attrs_old_with_custom.extend(_custom_attributes)
111+
_server_duration_attrs_new_with_custom = _server_duration_attrs_new.copy()
112+
_server_duration_attrs_new_with_custom.append("http.route")
113+
_server_duration_attrs_new_with_custom.extend(_custom_attributes)
114+
115+
_recommended_metrics_attrs_old_with_custom = {
116+
"http.server.active_requests": _server_active_requests_count_attrs_old,
117+
"http.server.duration": _server_duration_attrs_old_with_custom,
118+
}
119+
_recommended_metrics_attrs_new_with_custom = {
120+
"http.server.active_requests": _server_active_requests_count_attrs_new,
121+
"http.server.request.duration": _server_duration_attrs_new_with_custom,
122+
}
123+
_recommended_metrics_attrs_both_with_custom = {
124+
"http.server.active_requests": _server_active_requests_count_attrs_both,
125+
"http.server.duration": _server_duration_attrs_old_with_custom,
126+
"http.server.request.duration": _server_duration_attrs_new_with_custom,
127+
}
128+
106129
_parsed_falcon_version = package_version.parse(_falcon_version)
107130

108131

109132
class TestFalconBase(TestBase):
110133
def setUp(self):
111134
super().setUp()
135+
clear_labeler()
112136

113137
test_name = ""
114138
if hasattr(self, "_testMethodName"):
@@ -544,6 +568,38 @@ def test_falcon_metrics(self):
544568
)
545569
self.assertTrue(number_data_point_seen and histogram_data_point_seen)
546570

571+
def test_falcon_metrics_custom_attributes(self):
572+
self.client().simulate_get("/user_custom_attr/123")
573+
self.client().simulate_get("/user_custom_attr/123")
574+
self.client().simulate_get("/user_custom_attr/123")
575+
metrics_list = self.memory_metrics_reader.get_metrics_data()
576+
number_data_point_seen = False
577+
histogram_data_point_seen = False
578+
579+
self.assertTrue(len(metrics_list.resource_metrics) != 0)
580+
for resource_metric in metrics_list.resource_metrics:
581+
self.assertTrue(len(resource_metric.scope_metrics) != 0)
582+
for scope_metric in resource_metric.scope_metrics:
583+
self.assertTrue(len(scope_metric.metrics) != 0)
584+
for metric in scope_metric.metrics:
585+
self.assertIn(metric.name, _expected_metric_names)
586+
data_points = list(metric.data.data_points)
587+
self.assertEqual(len(data_points), 1)
588+
for point in data_points:
589+
if isinstance(point, HistogramDataPoint):
590+
self.assertEqual(point.count, 3)
591+
histogram_data_point_seen = True
592+
if isinstance(point, NumberDataPoint):
593+
number_data_point_seen = True
594+
for attr in point.attributes:
595+
self.assertIn(
596+
attr,
597+
_recommended_metrics_attrs_old_with_custom[
598+
metric.name
599+
],
600+
)
601+
self.assertTrue(number_data_point_seen and histogram_data_point_seen)
602+
547603
def test_falcon_metric_values_new_semconv(self):
548604
number_data_point_seen = False
549605
histogram_data_point_seen = False
@@ -580,6 +636,43 @@ def test_falcon_metric_values_new_semconv(self):
580636

581637
self.assertTrue(number_data_point_seen and histogram_data_point_seen)
582638

639+
def test_falcon_metric_values_new_semconv_custom_attributes(self):
640+
number_data_point_seen = False
641+
histogram_data_point_seen = False
642+
643+
start = default_timer()
644+
self.client().simulate_get("/user_custom_attr/123")
645+
duration = max(default_timer() - start, 0)
646+
647+
metrics_list = self.memory_metrics_reader.get_metrics_data()
648+
for resource_metric in metrics_list.resource_metrics:
649+
for scope_metric in resource_metric.scope_metrics:
650+
for metric in scope_metric.metrics:
651+
data_points = list(metric.data.data_points)
652+
self.assertEqual(len(data_points), 1)
653+
for point in data_points:
654+
if isinstance(point, HistogramDataPoint):
655+
self.assertEqual(point.count, 1)
656+
histogram_data_point_seen = True
657+
self.assertAlmostEqual(
658+
duration, point.sum, delta=10
659+
)
660+
self.assertEqual(
661+
point.explicit_bounds,
662+
HTTP_DURATION_HISTOGRAM_BUCKETS_NEW,
663+
)
664+
if isinstance(point, NumberDataPoint):
665+
self.assertEqual(point.value, 0)
666+
number_data_point_seen = True
667+
for attr in point.attributes:
668+
self.assertIn(
669+
attr,
670+
_recommended_metrics_attrs_new_with_custom[
671+
metric.name
672+
],
673+
)
674+
self.assertTrue(number_data_point_seen and histogram_data_point_seen)
675+
583676
def test_falcon_metric_values_both_semconv(self):
584677
number_data_point_seen = False
585678
histogram_data_point_seen = False
@@ -635,34 +728,59 @@ def test_falcon_metric_values_both_semconv(self):
635728
)
636729
self.assertTrue(number_data_point_seen and histogram_data_point_seen)
637730

638-
def test_falcon_metric_values(self):
731+
def test_falcon_metric_values_both_semconv_custom_attributes(self):
639732
number_data_point_seen = False
640733
histogram_data_point_seen = False
641734

642735
start = default_timer()
643-
self.client().simulate_get("/hello/756")
644-
duration = max(round((default_timer() - start) * 1000), 0)
736+
self.client().simulate_get("/user_custom_attr/123")
737+
duration_s = default_timer() - start
645738

646739
metrics_list = self.memory_metrics_reader.get_metrics_data()
740+
741+
# pylint: disable=too-many-nested-blocks
647742
for resource_metric in metrics_list.resource_metrics:
648743
for scope_metric in resource_metric.scope_metrics:
649744
for metric in scope_metric.metrics:
745+
if metric.unit == "ms":
746+
self.assertEqual(metric.name, "http.server.duration")
747+
elif metric.unit == "s":
748+
self.assertEqual(
749+
metric.name, "http.server.request.duration"
750+
)
751+
else:
752+
self.assertEqual(
753+
metric.name, "http.server.active_requests"
754+
)
650755
data_points = list(metric.data.data_points)
651756
self.assertEqual(len(data_points), 1)
652-
for point in list(metric.data.data_points):
757+
for point in data_points:
653758
if isinstance(point, HistogramDataPoint):
654759
self.assertEqual(point.count, 1)
760+
if metric.unit == "ms":
761+
self.assertAlmostEqual(
762+
max(round(duration_s * 1000), 0),
763+
point.sum,
764+
delta=10,
765+
)
766+
elif metric.unit == "s":
767+
self.assertAlmostEqual(
768+
max(duration_s, 0), point.sum, delta=10
769+
)
770+
self.assertEqual(
771+
point.explicit_bounds,
772+
HTTP_DURATION_HISTOGRAM_BUCKETS_NEW,
773+
)
655774
histogram_data_point_seen = True
656-
self.assertAlmostEqual(
657-
duration, point.sum, delta=10
658-
)
659775
if isinstance(point, NumberDataPoint):
660776
self.assertEqual(point.value, 0)
661777
number_data_point_seen = True
662778
for attr in point.attributes:
663779
self.assertIn(
664780
attr,
665-
_recommended_metrics_attrs_old[metric.name],
781+
_recommended_metrics_attrs_both_with_custom[
782+
metric.name
783+
],
666784
)
667785

668786
self.assertTrue(number_data_point_seen and histogram_data_point_seen)

0 commit comments

Comments
 (0)