Skip to content

Commit b7e7d0c

Browse files
Implement new HTTP semantic convention opt-in for Falcon (#2790)
1 parent 406707b commit b7e7d0c

File tree

5 files changed

+406
-100
lines changed

5 files changed

+406
-100
lines changed

CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2727
([#3133](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3133))
2828
- `opentelemetry-instrumentation-falcon` add support version to v4
2929
([#3086](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3086))
30+
- `opentelemetry-instrumentation-falcon` Implement new HTTP semantic convention opt-in for Falcon
31+
([#2790](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2790))
3032
- `opentelemetry-instrumentation-wsgi` always record span status code to have it available in metrics
3133
([#3148](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3148))
3234
- add support to Python 3.13

instrumentation/README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
| [opentelemetry-instrumentation-dbapi](./opentelemetry-instrumentation-dbapi) | dbapi | No | experimental
2121
| [opentelemetry-instrumentation-django](./opentelemetry-instrumentation-django) | django >= 1.10 | Yes | experimental
2222
| [opentelemetry-instrumentation-elasticsearch](./opentelemetry-instrumentation-elasticsearch) | elasticsearch >= 6.0 | No | experimental
23-
| [opentelemetry-instrumentation-falcon](./opentelemetry-instrumentation-falcon) | falcon >= 1.4.1, < 5.0.0 | Yes | experimental
23+
| [opentelemetry-instrumentation-falcon](./opentelemetry-instrumentation-falcon) | falcon >= 1.4.1, < 5.0.0 | Yes | migration
2424
| [opentelemetry-instrumentation-fastapi](./opentelemetry-instrumentation-fastapi) | fastapi ~= 0.58 | Yes | migration
2525
| [opentelemetry-instrumentation-flask](./opentelemetry-instrumentation-flask) | flask >= 1.0 | Yes | migration
2626
| [opentelemetry-instrumentation-grpc](./opentelemetry-instrumentation-grpc) | grpcio >= 1.42.0 | No | experimental

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

+98-52
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,14 @@ def response_hook(span, req, resp):
193193

194194
import opentelemetry.instrumentation.wsgi as otel_wsgi
195195
from opentelemetry import context, trace
196+
from opentelemetry.instrumentation._semconv import (
197+
_get_schema_url,
198+
_OpenTelemetrySemanticConventionStability,
199+
_OpenTelemetryStabilitySignalType,
200+
_report_new,
201+
_report_old,
202+
_StabilityMode,
203+
)
196204
from opentelemetry.instrumentation.falcon.package import _instruments
197205
from opentelemetry.instrumentation.falcon.version import __version__
198206
from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
@@ -203,18 +211,22 @@ def response_hook(span, req, resp):
203211
from opentelemetry.instrumentation.utils import (
204212
_start_internal_or_server_span,
205213
extract_attributes_from_object,
206-
http_status_to_status_code,
207214
)
208215
from opentelemetry.metrics import get_meter
216+
from opentelemetry.semconv.attributes.http_attributes import (
217+
HTTP_ROUTE,
218+
)
209219
from opentelemetry.semconv.metrics import MetricInstruments
210-
from opentelemetry.semconv.trace import SpanAttributes
211-
from opentelemetry.trace.status import Status, StatusCode
220+
from opentelemetry.semconv.metrics.http_metrics import (
221+
HTTP_SERVER_REQUEST_DURATION,
222+
)
212223
from opentelemetry.util.http import get_excluded_urls, get_traced_request_attrs
213224

214225
_logger = getLogger(__name__)
215226

216227
_ENVIRON_STARTTIME_KEY = "opentelemetry-falcon.starttime_key"
217228
_ENVIRON_SPAN_KEY = "opentelemetry-falcon.span_key"
229+
_ENVIRON_REQ_ATTRS = "opentelemetry-falcon.req_attrs"
218230
_ENVIRON_ACTIVATION_KEY = "opentelemetry-falcon.activation_key"
219231
_ENVIRON_TOKEN = "opentelemetry-falcon.token"
220232
_ENVIRON_EXC = "opentelemetry-falcon.exc"
@@ -243,6 +255,10 @@ class _InstrumentedFalconAPI(getattr(falcon, _instrument_app)):
243255
def __init__(self, *args, **kwargs):
244256
otel_opts = kwargs.pop("_otel_opts", {})
245257

258+
self._sem_conv_opt_in_mode = _OpenTelemetrySemanticConventionStability._get_opentelemetry_stability_opt_in_mode(
259+
_OpenTelemetryStabilitySignalType.HTTP,
260+
)
261+
246262
# inject trace middleware
247263
self._middlewares_list = kwargs.pop("middleware", [])
248264
if self._middlewares_list is None:
@@ -257,19 +273,30 @@ def __init__(self, *args, **kwargs):
257273
__name__,
258274
__version__,
259275
tracer_provider,
260-
schema_url="https://opentelemetry.io/schemas/1.11.0",
276+
schema_url=_get_schema_url(self._sem_conv_opt_in_mode),
261277
)
262278
self._otel_meter = get_meter(
263279
__name__,
264280
__version__,
265281
meter_provider,
266-
schema_url="https://opentelemetry.io/schemas/1.11.0",
267-
)
268-
self.duration_histogram = self._otel_meter.create_histogram(
269-
name=MetricInstruments.HTTP_SERVER_DURATION,
270-
unit="ms",
271-
description="Measures the duration of inbound HTTP requests.",
282+
schema_url=_get_schema_url(self._sem_conv_opt_in_mode),
272283
)
284+
285+
self.duration_histogram_old = None
286+
if _report_old(self._sem_conv_opt_in_mode):
287+
self.duration_histogram_old = self._otel_meter.create_histogram(
288+
name=MetricInstruments.HTTP_SERVER_DURATION,
289+
unit="ms",
290+
description="Measures the duration of inbound HTTP requests.",
291+
)
292+
self.duration_histogram_new = None
293+
if _report_new(self._sem_conv_opt_in_mode):
294+
self.duration_histogram_new = self._otel_meter.create_histogram(
295+
name=HTTP_SERVER_REQUEST_DURATION,
296+
description="Duration of HTTP server requests.",
297+
unit="s",
298+
)
299+
273300
self.active_requests_counter = self._otel_meter.create_up_down_counter(
274301
name=MetricInstruments.HTTP_SERVER_ACTIVE_REQUESTS,
275302
unit="requests",
@@ -283,6 +310,7 @@ def __init__(self, *args, **kwargs):
283310
),
284311
otel_opts.pop("request_hook", None),
285312
otel_opts.pop("response_hook", None),
313+
self._sem_conv_opt_in_mode,
286314
)
287315
self._middlewares_list.insert(0, trace_middleware)
288316
kwargs["middleware"] = self._middlewares_list
@@ -343,11 +371,14 @@ def __call__(self, env, start_response):
343371
context_carrier=env,
344372
context_getter=otel_wsgi.wsgi_getter,
345373
)
346-
attributes = otel_wsgi.collect_request_attributes(env)
374+
attributes = otel_wsgi.collect_request_attributes(
375+
env, self._sem_conv_opt_in_mode
376+
)
347377
active_requests_count_attrs = (
348-
otel_wsgi._parse_active_request_count_attrs(attributes)
378+
otel_wsgi._parse_active_request_count_attrs(
379+
attributes, self._sem_conv_opt_in_mode
380+
)
349381
)
350-
duration_attrs = otel_wsgi._parse_duration_attrs(attributes)
351382
self.active_requests_counter.add(1, active_requests_count_attrs)
352383

353384
if span.is_recording():
@@ -364,6 +395,7 @@ def __call__(self, env, start_response):
364395
activation.__enter__()
365396
env[_ENVIRON_SPAN_KEY] = span
366397
env[_ENVIRON_ACTIVATION_KEY] = activation
398+
env[_ENVIRON_REQ_ATTRS] = attributes
367399
exception = None
368400

369401
def _start_response(status, response_headers, *args, **kwargs):
@@ -379,12 +411,22 @@ def _start_response(status, response_headers, *args, **kwargs):
379411
exception = exc
380412
raise
381413
finally:
382-
if span.is_recording():
383-
duration_attrs[SpanAttributes.HTTP_STATUS_CODE] = (
384-
span.attributes.get(SpanAttributes.HTTP_STATUS_CODE)
414+
duration_s = default_timer() - start
415+
if self.duration_histogram_old:
416+
duration_attrs = otel_wsgi._parse_duration_attrs(
417+
attributes, _StabilityMode.DEFAULT
418+
)
419+
self.duration_histogram_old.record(
420+
max(round(duration_s * 1000), 0), duration_attrs
421+
)
422+
if self.duration_histogram_new:
423+
duration_attrs = otel_wsgi._parse_duration_attrs(
424+
attributes, _StabilityMode.HTTP
425+
)
426+
self.duration_histogram_new.record(
427+
max(duration_s, 0), duration_attrs
385428
)
386-
duration = max(round((default_timer() - start) * 1000), 0)
387-
self.duration_histogram.record(duration, duration_attrs)
429+
388430
self.active_requests_counter.add(-1, active_requests_count_attrs)
389431
if exception is None:
390432
activation.__exit__(None, None, None)
@@ -407,11 +449,13 @@ def __init__(
407449
traced_request_attrs=None,
408450
request_hook=None,
409451
response_hook=None,
452+
sem_conv_opt_in_mode: _StabilityMode = _StabilityMode.DEFAULT,
410453
):
411454
self.tracer = tracer
412455
self._traced_request_attrs = traced_request_attrs
413456
self._request_hook = request_hook
414457
self._response_hook = response_hook
458+
self._sem_conv_opt_in_mode = sem_conv_opt_in_mode
415459

416460
def process_request(self, req, resp):
417461
span = req.env.get(_ENVIRON_SPAN_KEY)
@@ -437,58 +481,60 @@ def process_resource(self, req, resp, resource, params):
437481

438482
def process_response(self, req, resp, resource, req_succeeded=None): # pylint:disable=R0201,R0912
439483
span = req.env.get(_ENVIRON_SPAN_KEY)
484+
req_attrs = req.env.get(_ENVIRON_REQ_ATTRS)
440485

441-
if not span or not span.is_recording():
486+
if not span:
442487
return
443488

444489
status = resp.status
445-
reason = None
446490
if resource is None:
447-
status = "404"
448-
reason = "NotFound"
491+
status = falcon.HTTP_404
449492
else:
493+
exc_type, exc = None, None
450494
if _ENVIRON_EXC in req.env:
451495
exc = req.env[_ENVIRON_EXC]
452496
exc_type = type(exc)
453-
else:
454-
exc_type, exc = None, None
497+
455498
if exc_type and not req_succeeded:
456499
if "HTTPNotFound" in exc_type.__name__:
457-
status = "404"
458-
reason = "NotFound"
500+
status = falcon.HTTP_404
501+
elif isinstance(exc, (falcon.HTTPError, falcon.HTTPStatus)):
502+
try:
503+
if _falcon_version > 2:
504+
status = falcon.code_to_http_status(exc.status)
505+
else:
506+
status = exc.status
507+
except ValueError:
508+
status = falcon.HTTP_500
459509
else:
460-
status = "500"
461-
reason = f"{exc_type.__name__}: {exc}"
510+
status = falcon.HTTP_500
511+
512+
# Falcon 1 does not support response headers. So
513+
# send an empty dict.
514+
response_headers = {}
515+
if _falcon_version > 1:
516+
response_headers = resp.headers
517+
518+
otel_wsgi.add_response_attributes(
519+
span,
520+
status,
521+
response_headers,
522+
req_attrs,
523+
self._sem_conv_opt_in_mode,
524+
)
462525

463-
status = status.split(" ")[0]
526+
if (
527+
_report_new(self._sem_conv_opt_in_mode)
528+
and req.uri_template
529+
and req_attrs is not None
530+
):
531+
req_attrs[HTTP_ROUTE] = req.uri_template
464532
try:
465-
status_code = int(status)
466-
span.set_attribute(SpanAttributes.HTTP_STATUS_CODE, status_code)
467-
otel_status_code = http_status_to_status_code(
468-
status_code, server_span=True
469-
)
470-
471-
# set the description only when the status code is ERROR
472-
if otel_status_code is not StatusCode.ERROR:
473-
reason = None
474-
475-
span.set_status(
476-
Status(
477-
status_code=otel_status_code,
478-
description=reason,
479-
)
480-
)
481-
482-
# Falcon 1 does not support response headers. So
483-
# send an empty dict.
484-
response_headers = {}
485-
if _falcon_version > 1:
486-
response_headers = resp.headers
487-
488533
if span.is_recording() and span.kind == trace.SpanKind.SERVER:
489534
# Check if low-cardinality route is available as per semantic-conventions
490535
if req.uri_template:
491536
span.update_name(f"{req.method} {req.uri_template}")
537+
span.set_attribute(HTTP_ROUTE, req.uri_template)
492538
else:
493539
span.update_name(f"{req.method}")
494540

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

+2
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,5 @@
1616
_instruments = ("falcon >= 1.4.1, < 5.0.0",)
1717

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

0 commit comments

Comments
 (0)