1212# See the License for the specific language governing permissions and
1313# limitations under the License.
1414
15+ import wsgiref .util as wsgiref_util
1516from logging import getLogger
1617from time import time_ns
1718from timeit import default_timer
2324
2425import opentelemetry .instrumentation .wsgi as otel_wsgi
2526from opentelemetry import context , trace
27+ from opentelemetry .instrumentation ._semconv import (
28+ HTTP_DURATION_HISTOGRAM_BUCKETS_NEW ,
29+ _get_schema_url ,
30+ _report_new ,
31+ _report_old ,
32+ _set_http_url ,
33+ _StabilityMode ,
34+ )
2635from opentelemetry .instrumentation .propagators import (
2736 get_global_response_propagator ,
2837)
2938from opentelemetry .instrumentation .pyramid .version import __version__
3039from opentelemetry .instrumentation .utils import _start_internal_or_server_span
3140from opentelemetry .metrics import get_meter
41+ from opentelemetry .semconv ._incubating .attributes .error_attributes import (
42+ ERROR_TYPE ,
43+ )
3244from opentelemetry .semconv ._incubating .attributes .http_attributes import (
3345 HTTP_STATUS_CODE ,
3446)
3547from opentelemetry .semconv .attributes .http_attributes import HTTP_ROUTE
3648from opentelemetry .semconv .metrics import MetricInstruments
49+ from opentelemetry .semconv .metrics .http_metrics import (
50+ HTTP_SERVER_REQUEST_DURATION ,
51+ )
3752from opentelemetry .trace .status import Status , StatusCode
38- from opentelemetry .util .http import get_excluded_urls
53+ from opentelemetry .util .http import get_excluded_urls , redact_url
3954
4055TWEEN_NAME = "opentelemetry.instrumentation.pyramid.trace_tween_factory"
4156SETTING_TRACE_ENABLED = "opentelemetry-pyramid.trace_enabled"
5065
5166
5267_excluded_urls = get_excluded_urls ("PYRAMID" )
68+ _sem_conv_opt_in_mode = _StabilityMode .DEFAULT
5369
5470
5571def includeme (config ):
@@ -91,7 +107,7 @@ def _before_traversal(event):
91107 tracer = trace .get_tracer (
92108 __name__ ,
93109 __version__ ,
94- schema_url = "https://opentelemetry.io/schemas/1.11.0" ,
110+ schema_url = _get_schema_url ( _sem_conv_opt_in_mode ) ,
95111 )
96112
97113 if request .matched_route :
@@ -108,9 +124,16 @@ def _before_traversal(event):
108124 )
109125
110126 if span .is_recording ():
111- attributes = otel_wsgi .collect_request_attributes (request_environ )
127+ attributes = otel_wsgi .collect_request_attributes (
128+ request_environ , _sem_conv_opt_in_mode
129+ )
112130 if request .matched_route :
113131 attributes [HTTP_ROUTE ] = request .matched_route .pattern
132+ _set_http_url (
133+ attributes ,
134+ redact_url (wsgiref_util .request_uri (request_environ )),
135+ _sem_conv_opt_in_mode ,
136+ )
114137 for key , value in attributes .items ():
115138 span .set_attribute (key , value )
116139 if span .kind == trace .SpanKind .SERVER :
@@ -134,20 +157,45 @@ def trace_tween_factory(handler, registry):
134157 # pylint: disable=too-many-statements
135158 settings = registry .settings
136159 enabled = asbool (settings .get (SETTING_TRACE_ENABLED , True ))
160+
161+ # Create meters and histograms based on opt-in mode
162+ duration_histogram_old = None
163+ if _report_old (_sem_conv_opt_in_mode ):
164+ meter_old = get_meter (
165+ __name__ ,
166+ __version__ ,
167+ schema_url = _get_schema_url (_StabilityMode .DEFAULT ),
168+ )
169+ duration_histogram_old = meter_old .create_histogram (
170+ name = MetricInstruments .HTTP_SERVER_DURATION ,
171+ unit = "ms" ,
172+ description = "Measures the duration of inbound HTTP requests." ,
173+ )
174+
175+ duration_histogram_new = None
176+ if _report_new (_sem_conv_opt_in_mode ):
177+ meter_new = get_meter (
178+ __name__ ,
179+ __version__ ,
180+ schema_url = _get_schema_url (_StabilityMode .HTTP ),
181+ )
182+ duration_histogram_new = meter_new .create_histogram (
183+ name = HTTP_SERVER_REQUEST_DURATION ,
184+ unit = "s" ,
185+ description = "Duration of HTTP server requests." ,
186+ explicit_bucket_boundaries_advisory = HTTP_DURATION_HISTOGRAM_BUCKETS_NEW ,
187+ )
188+
189+ # Use a single meter for active requests counter (attributes are compatible)
137190 meter = get_meter (
138191 __name__ ,
139192 __version__ ,
140- schema_url = "https://opentelemetry.io/schemas/1.11.0" ,
141- )
142- duration_histogram = meter .create_histogram (
143- name = MetricInstruments .HTTP_SERVER_DURATION ,
144- unit = "ms" ,
145- description = "Measures the duration of inbound HTTP requests." ,
193+ schema_url = _get_schema_url (_sem_conv_opt_in_mode ),
146194 )
147195 active_requests_counter = meter .create_up_down_counter (
148196 name = MetricInstruments .HTTP_SERVER_ACTIVE_REQUESTS ,
149- unit = "requests " ,
150- description = "measures the number of concurrent HTTP requests that are currently in-flight " ,
197+ unit = "{request} " ,
198+ description = "Number of active HTTP server requests. " ,
151199 )
152200
153201 if not enabled :
@@ -168,14 +216,20 @@ def trace_tween(request):
168216 # short-circuit when we don't want to trace anything
169217 return handler (request )
170218
171- attributes = otel_wsgi .collect_request_attributes (request .environ )
219+ attributes = otel_wsgi .collect_request_attributes (
220+ request .environ , _sem_conv_opt_in_mode
221+ )
172222
173223 request .environ [_ENVIRON_ENABLED_KEY ] = True
174224 request .environ [_ENVIRON_STARTTIME_KEY ] = time_ns ()
175225 active_requests_count_attrs = (
176- otel_wsgi ._parse_active_request_count_attrs (attributes )
226+ otel_wsgi ._parse_active_request_count_attrs (
227+ attributes , _sem_conv_opt_in_mode
228+ )
229+ )
230+ duration_attrs = otel_wsgi ._parse_duration_attrs (
231+ attributes , _sem_conv_opt_in_mode
177232 )
178- duration_attrs = otel_wsgi ._parse_duration_attrs (attributes )
179233
180234 start = default_timer ()
181235 active_requests_counter .add (1 , active_requests_count_attrs )
@@ -201,16 +255,36 @@ def trace_tween(request):
201255 # should infer a internal server error and raise
202256 status = "500 InternalServerError"
203257 recordable_exc = exc
258+ if _report_new (_sem_conv_opt_in_mode ):
259+ attributes [ERROR_TYPE ] = type (exc ).__qualname__
204260 raise
205261 finally :
206- duration = max ( round (( default_timer () - start ) * 1000 ), 0 )
262+ duration_s = default_timer () - start
207263 status = getattr (response , "status" , status )
208264 status_code = otel_wsgi ._parse_status_code (status )
209265 if status_code is not None :
210266 duration_attrs [HTTP_STATUS_CODE ] = (
211267 otel_wsgi ._parse_status_code (status )
212268 )
213- duration_histogram .record (duration , duration_attrs )
269+
270+ # Record metrics for old semconv (milliseconds)
271+ if duration_histogram_old :
272+ duration_attrs_old = otel_wsgi ._parse_duration_attrs (
273+ duration_attrs , _StabilityMode .DEFAULT
274+ )
275+ duration_histogram_old .record (
276+ max (round (duration_s * 1000 ), 0 ), duration_attrs_old
277+ )
278+
279+ # Record metrics for new semconv (seconds)
280+ if duration_histogram_new :
281+ duration_attrs_new = otel_wsgi ._parse_duration_attrs (
282+ duration_attrs , _StabilityMode .HTTP
283+ )
284+ duration_histogram_new .record (
285+ max (duration_s , 0 ), duration_attrs_new
286+ )
287+
214288 active_requests_counter .add (- 1 , active_requests_count_attrs )
215289 span = request .environ .get (_ENVIRON_SPAN_KEY )
216290 enabled = request .environ .get (_ENVIRON_ENABLED_KEY )
@@ -226,9 +300,19 @@ def trace_tween(request):
226300 span ,
227301 status ,
228302 getattr (response , "headerlist" , None ),
303+ duration_attrs ,
304+ _sem_conv_opt_in_mode ,
229305 )
230306
231307 if recordable_exc is not None :
308+ if (
309+ _report_new (_sem_conv_opt_in_mode )
310+ and span .is_recording ()
311+ ):
312+ span .set_attribute (
313+ ERROR_TYPE ,
314+ type (recordable_exc ).__qualname__ ,
315+ )
232316 span .set_status (
233317 Status (StatusCode .ERROR , str (recordable_exc ))
234318 )
0 commit comments