Skip to content

Commit 212045a

Browse files
tammy-baylis-swixrmx
authored andcommitted
HTTP semantic convention stability migration for Pyramid (open-telemetry#3982)
* wip * Fix opt-in mode set at init * Add tests * Changelog * Style * Update CHANGELOG.md * pyramid semconv migration * Revise test * Add _set_http_url to Pyramid instr * adjust test imports --------- Co-authored-by: Riccardo Magliocchetti <riccardo.magliocchetti@gmail.com>
1 parent 74f742f commit 212045a

7 files changed

Lines changed: 414 additions & 21 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
3939
([#3980](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3980))
4040
- `opentelemetry-instrumentation`: add database stability attribute setters in `_semconv` utilities
4141
([#4108](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4108))
42+
- `opentelemetry-instrumentation-pyramid` Implement new semantic convention opt-in migration
43+
([#3982](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3982))
4244

4345
### Fixed
4446

instrumentation/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838
| [opentelemetry-instrumentation-pymongo](./opentelemetry-instrumentation-pymongo) | pymongo >= 3.1, < 5.0 | No | development
3939
| [opentelemetry-instrumentation-pymssql](./opentelemetry-instrumentation-pymssql) | pymssql >= 2.1.5, < 3 | No | development
4040
| [opentelemetry-instrumentation-pymysql](./opentelemetry-instrumentation-pymysql) | PyMySQL < 2 | No | development
41-
| [opentelemetry-instrumentation-pyramid](./opentelemetry-instrumentation-pyramid) | pyramid >= 1.7 | Yes | development
41+
| [opentelemetry-instrumentation-pyramid](./opentelemetry-instrumentation-pyramid) | pyramid >= 1.7 | Yes | migration
4242
| [opentelemetry-instrumentation-redis](./opentelemetry-instrumentation-redis) | redis >= 2.6 | No | development
4343
| [opentelemetry-instrumentation-remoulade](./opentelemetry-instrumentation-remoulade) | remoulade >= 0.50 | No | development
4444
| [opentelemetry-instrumentation-requests](./opentelemetry-instrumentation-requests) | requests ~= 2.0 | Yes | migration

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

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,13 @@
191191
from pyramid.settings import aslist
192192
from wrapt import wrap_function_wrapper as _wrap
193193

194+
from opentelemetry.instrumentation._semconv import (
195+
_OpenTelemetrySemanticConventionStability,
196+
_OpenTelemetryStabilitySignalType,
197+
_StabilityMode,
198+
)
194199
from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
200+
from opentelemetry.instrumentation.pyramid import callbacks
195201
from opentelemetry.instrumentation.pyramid.callbacks import (
196202
SETTING_TRACE_ENABLED,
197203
TWEEN_NAME,
@@ -248,10 +254,20 @@ def _instrument(self, **kwargs):
248254
"""Integrate with Pyramid Python library.
249255
https://docs.pylonsproject.org/projects/pyramid/en/latest/
250256
"""
257+
# Initialize semantic conventions opt-in mode
258+
_OpenTelemetrySemanticConventionStability._initialize()
259+
sem_conv_opt_in_mode = _OpenTelemetrySemanticConventionStability._get_opentelemetry_stability_opt_in_mode(
260+
_OpenTelemetryStabilitySignalType.HTTP,
261+
)
262+
# Set module-level opt-in mode in callbacks
263+
callbacks._sem_conv_opt_in_mode = sem_conv_opt_in_mode
264+
251265
_wrap("pyramid.config", "Configurator.__init__", _traced_init)
252266

253267
def _uninstrument(self, **kwargs):
254268
""" "Disable Pyramid instrumentation"""
269+
# Reset module-level opt-in mode to default
270+
callbacks._sem_conv_opt_in_mode = _StabilityMode.DEFAULT
255271
unwrap(Configurator, "__init__")
256272

257273
@staticmethod
@@ -261,8 +277,18 @@ def instrument_config(config):
261277
Args:
262278
config: The Configurator to instrument.
263279
"""
280+
# Initialize semantic conventions opt-in mode
281+
_OpenTelemetrySemanticConventionStability._initialize()
282+
sem_conv_opt_in_mode = _OpenTelemetrySemanticConventionStability._get_opentelemetry_stability_opt_in_mode(
283+
_OpenTelemetryStabilitySignalType.HTTP,
284+
)
285+
# Set module-level opt-in mode in callbacks
286+
callbacks._sem_conv_opt_in_mode = sem_conv_opt_in_mode
287+
264288
config.include("opentelemetry.instrumentation.pyramid.callbacks")
265289

266290
@staticmethod
267291
def uninstrument_config(config):
292+
# Reset module-level opt-in mode to default
293+
callbacks._sem_conv_opt_in_mode = _StabilityMode.DEFAULT
268294
config.add_settings({SETTING_TRACE_ENABLED: False})

instrumentation/opentelemetry-instrumentation-pyramid/src/opentelemetry/instrumentation/pyramid/callbacks.py

Lines changed: 100 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15+
import wsgiref.util as wsgiref_util
1516
from logging import getLogger
1617
from time import time_ns
1718
from timeit import default_timer
@@ -23,19 +24,33 @@
2324

2425
import opentelemetry.instrumentation.wsgi as otel_wsgi
2526
from 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+
)
2635
from opentelemetry.instrumentation.propagators import (
2736
get_global_response_propagator,
2837
)
2938
from opentelemetry.instrumentation.pyramid.version import __version__
3039
from opentelemetry.instrumentation.utils import _start_internal_or_server_span
3140
from opentelemetry.metrics import get_meter
41+
from opentelemetry.semconv._incubating.attributes.error_attributes import (
42+
ERROR_TYPE,
43+
)
3244
from opentelemetry.semconv._incubating.attributes.http_attributes import (
3345
HTTP_STATUS_CODE,
3446
)
3547
from opentelemetry.semconv.attributes.http_attributes import HTTP_ROUTE
3648
from opentelemetry.semconv.metrics import MetricInstruments
49+
from opentelemetry.semconv.metrics.http_metrics import (
50+
HTTP_SERVER_REQUEST_DURATION,
51+
)
3752
from 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

4055
TWEEN_NAME = "opentelemetry.instrumentation.pyramid.trace_tween_factory"
4156
SETTING_TRACE_ENABLED = "opentelemetry-pyramid.trace_enabled"
@@ -50,6 +65,7 @@
5065

5166

5267
_excluded_urls = get_excluded_urls("PYRAMID")
68+
_sem_conv_opt_in_mode = _StabilityMode.DEFAULT
5369

5470

5571
def 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
)

instrumentation/opentelemetry-instrumentation-pyramid/src/opentelemetry/instrumentation/pyramid/package.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,5 @@
1616
_instruments = ("pyramid >= 1.7",)
1717

1818
_supports_metrics = True
19+
20+
_semconv_status = "migration"

0 commit comments

Comments
 (0)