1+ # Copyright The OpenTelemetry Authors
2+ #
3+ # Licensed under the Apache License, Version 2.0 (the "License");
4+ # you may not use this file except in compliance with the License.
5+ # You may obtain a copy of the License at
6+ #
7+ # http://www.apache.org/licenses/LICENSE-2.0
8+ #
9+ # Unless required by applicable law or agreed to in writing, software
10+ # distributed under the License is distributed on an "AS IS" BASIS,
11+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+ # See the License for the specific language governing permissions and
13+ # limitations under the License.
14+
15+ from timeit import default_timer
16+ from unittest .mock import patch
17+
18+ import flask
19+
20+ from opentelemetry .instrumentation ._labeler import (
21+ clear_labeler ,
22+ get_labeler ,
23+ )
24+ from opentelemetry .instrumentation ._semconv import (
25+ HTTP_DURATION_HISTOGRAM_BUCKETS_NEW ,
26+ OTEL_SEMCONV_STABILITY_OPT_IN ,
27+ _OpenTelemetrySemanticConventionStability ,
28+ _server_active_requests_count_attrs_new ,
29+ _server_active_requests_count_attrs_old ,
30+ _server_duration_attrs_new ,
31+ _server_duration_attrs_old ,
32+ )
33+ from opentelemetry .instrumentation .flask import FlaskInstrumentor
34+ from opentelemetry .sdk .metrics .export import (
35+ HistogramDataPoint ,
36+ NumberDataPoint ,
37+ )
38+ from opentelemetry .test .wsgitestutil import WsgiTestBase
39+
40+ # pylint: disable=import-error
41+ from .base_test import InstrumentationTest
42+
43+
44+ _expected_metric_names_old = [
45+ "http.server.active_requests" ,
46+ "http.server.duration" ,
47+ ]
48+ _expected_metric_names_new = [
49+ "http.server.active_requests" ,
50+ "http.server.request.duration" ,
51+ ]
52+
53+ _custom_attributes = [
54+ "custom_attr" , "endpoint_type" , "feature_flag"
55+ ]
56+
57+ _server_duration_attrs_old_with_custom = _server_duration_attrs_old .copy ()
58+ _server_duration_attrs_old_with_custom .append ("http.target" )
59+ _server_duration_attrs_old_with_custom .extend (_custom_attributes )
60+ _server_duration_attrs_new_with_custom = _server_duration_attrs_new .copy ()
61+ _server_duration_attrs_new_with_custom .append ("http.route" )
62+ _server_duration_attrs_new_with_custom .extend (_custom_attributes )
63+
64+ _recommended_metrics_attrs_old_with_custom = {
65+ "http.server.active_requests" : _server_active_requests_count_attrs_old ,
66+ "http.server.duration" : _server_duration_attrs_old_with_custom ,
67+ }
68+ _recommended_metrics_attrs_new_with_custom = {
69+ "http.server.active_requests" : _server_active_requests_count_attrs_new ,
70+ "http.server.request.duration" : _server_duration_attrs_new_with_custom ,
71+ }
72+
73+
74+ class TestFlaskLabeler (InstrumentationTest , WsgiTestBase ):
75+ def setUp (self ):
76+ super ().setUp ()
77+
78+ test_name = ""
79+ if hasattr (self , "_testMethodName" ):
80+ test_name = self ._testMethodName
81+ sem_conv_mode = "default"
82+ if "new_semconv" in test_name :
83+ sem_conv_mode = "http"
84+ self .env_patch = patch .dict (
85+ "os.environ" ,
86+ {
87+ OTEL_SEMCONV_STABILITY_OPT_IN : sem_conv_mode ,
88+ },
89+ )
90+ _OpenTelemetrySemanticConventionStability ._initialized = False
91+ self .env_patch .start ()
92+
93+ clear_labeler ()
94+ self .app = flask .Flask (__name__ )
95+
96+ @self .app .route ("/test_labeler" )
97+ def test_labeler_route ():
98+ labeler = get_labeler ()
99+ labeler .add ("custom_attr" , "test_value" )
100+ labeler .add_attributes ({
101+ "endpoint_type" : "test" ,
102+ "feature_flag" : True
103+ })
104+ return "OK"
105+
106+ @self .app .route ("/no_labeler" )
107+ def test_no_labeler_route ():
108+ return "No labeler"
109+
110+ FlaskInstrumentor ().instrument_app (self .app )
111+ self ._common_initialization ()
112+
113+ def tearDown (self ):
114+ super ().tearDown ()
115+ clear_labeler ()
116+ with self .disable_logging ():
117+ FlaskInstrumentor ().uninstrument_app (self .app )
118+
119+ def test_flask_metrics_custom_attributes (self ):
120+ start = default_timer ()
121+ self .client .get ("/test_labeler" )
122+ self .client .get ("/test_labeler" )
123+ self .client .get ("/test_labeler" )
124+ duration = max (round ((default_timer () - start ) * 1000 ), 0 )
125+ metrics_list = self .memory_metrics_reader .get_metrics_data ()
126+ number_data_point_seen = False
127+ histogram_data_point_seen = False
128+ self .assertTrue (len (metrics_list .resource_metrics ) != 0 )
129+ for resource_metric in metrics_list .resource_metrics :
130+ self .assertTrue (len (resource_metric .scope_metrics ) != 0 )
131+ for scope_metric in resource_metric .scope_metrics :
132+ self .assertTrue (len (scope_metric .metrics ) != 0 )
133+ for metric in scope_metric .metrics :
134+ self .assertIn (metric .name , _expected_metric_names_old )
135+ data_points = list (metric .data .data_points )
136+ self .assertEqual (len (data_points ), 1 )
137+ for point in data_points :
138+ if isinstance (point , HistogramDataPoint ):
139+ self .assertEqual (point .count , 3 )
140+ self .assertAlmostEqual (
141+ duration , point .sum , delta = 10
142+ )
143+ histogram_data_point_seen = True
144+ if isinstance (point , NumberDataPoint ):
145+ number_data_point_seen = True
146+ for attr in point .attributes :
147+ self .assertIn (
148+ attr ,
149+ _recommended_metrics_attrs_old_with_custom [metric .name ],
150+ )
151+ self .assertTrue (number_data_point_seen and histogram_data_point_seen )
152+
153+ def test_flask_metrics_custom_attributes_new_semconv (self ):
154+ start = default_timer ()
155+ self .client .get ("/test_labeler" )
156+ self .client .get ("/test_labeler" )
157+ self .client .get ("/test_labeler" )
158+ duration_s = max (default_timer () - start , 0 )
159+ metrics_list = self .memory_metrics_reader .get_metrics_data ()
160+ number_data_point_seen = False
161+ histogram_data_point_seen = False
162+ self .assertTrue (len (metrics_list .resource_metrics ) != 0 )
163+ for resource_metric in metrics_list .resource_metrics :
164+ self .assertTrue (len (resource_metric .scope_metrics ) != 0 )
165+ for scope_metric in resource_metric .scope_metrics :
166+ self .assertTrue (len (scope_metric .metrics ) != 0 )
167+ for metric in scope_metric .metrics :
168+ self .assertIn (metric .name , _expected_metric_names_new )
169+ data_points = list (metric .data .data_points )
170+ self .assertEqual (len (data_points ), 1 )
171+ for point in data_points :
172+ if isinstance (point , HistogramDataPoint ):
173+ self .assertEqual (point .count , 3 )
174+ self .assertAlmostEqual (
175+ duration_s , point .sum , places = 1
176+ )
177+ self .assertEqual (
178+ point .explicit_bounds ,
179+ HTTP_DURATION_HISTOGRAM_BUCKETS_NEW ,
180+ )
181+ histogram_data_point_seen = True
182+ if isinstance (point , NumberDataPoint ):
183+ number_data_point_seen = True
184+ for attr in point .attributes :
185+ self .assertIn (
186+ attr ,
187+ _recommended_metrics_attrs_new_with_custom [metric .name ],
188+ )
189+ self .assertTrue (number_data_point_seen and histogram_data_point_seen )
0 commit comments