Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement new HTTP semantic convention opt-in for Falcon #2790

Open
wants to merge 33 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
2d68dc5
Implement new HTTP semantic convention opt-in for Falcon
FilipNikolovski Aug 9, 2024
228897d
fix tests for falcon 1 & 2
FilipNikolovski Aug 9, 2024
2d70a72
resolve conflicts
FilipNikolovski Dec 2, 2024
a9a30a8
add semconv status as migration for falcon in instrumentations readme
FilipNikolovski Dec 2, 2024
b3db908
set schema_url based on the opt-in mode
FilipNikolovski Dec 2, 2024
aa54865
add histograms based on opt-in mode; set metrics attributes; update t…
FilipNikolovski Dec 2, 2024
2ed1461
run ruff
FilipNikolovski Dec 2, 2024
e8d3e6c
disable too-many-public-methods lint rule for TestFalconInstrumentation
FilipNikolovski Dec 2, 2024
d1e81c0
Merge branch 'main' into falcon_new_semconv
FilipNikolovski Dec 3, 2024
1815495
add _semconv_status in package.py
FilipNikolovski Dec 3, 2024
0307008
Merge branch 'falcon_new_semconv' of https://github.com/FilipNikolovs…
FilipNikolovski Dec 3, 2024
a4419a6
Merge branch 'main' into falcon_new_semconv
emdneto Dec 3, 2024
7754f0d
cleanup imports
FilipNikolovski Dec 10, 2024
b941569
set request attributes in env, set status code in metric attributes i…
FilipNikolovski Dec 10, 2024
e14eaf9
Merge branch 'main' into falcon_new_semconv
FilipNikolovski Dec 10, 2024
c44244c
fix exceptions to not be masked as 500 on every thrown exception
FilipNikolovski Dec 10, 2024
1cecee6
Merge branch 'falcon_new_semconv' of https://github.com/FilipNikolovs…
FilipNikolovski Dec 10, 2024
59586fd
resolve lint issues
FilipNikolovski Dec 10, 2024
6ac9e98
run tox -e generate; fix typo
FilipNikolovski Dec 10, 2024
d0757ec
Merge branch 'main' into falcon_new_semconv
emdneto Dec 16, 2024
2d6b160
sort imports
FilipNikolovski Dec 31, 2024
ecd351f
Merge branch 'falcon_new_semconv' of https://github.com/FilipNikolovs…
FilipNikolovski Dec 31, 2024
3d5ec55
fix tests
FilipNikolovski Dec 31, 2024
e51f79d
Merge branch 'open-telemetry:main' into falcon_new_semconv
FilipNikolovski Dec 31, 2024
bfb8a53
Merge branch 'main' of https://github.com/FilipNikolovski/opentelemet…
FilipNikolovski Dec 31, 2024
606958a
update changelog
FilipNikolovski Dec 31, 2024
6ee7d35
Merge branch 'falcon_new_semconv' of https://github.com/FilipNikolovs…
FilipNikolovski Dec 31, 2024
e12ace6
Merge branch 'main' into falcon_new_semconv
FilipNikolovski Jan 2, 2025
8b281b2
update tests
FilipNikolovski Jan 2, 2025
c662eae
disable lint check on too-many-nested-blocks in metrics test
FilipNikolovski Jan 2, 2025
7702a59
use add_response_attributes function from wsgi
FilipNikolovski Jan 3, 2025
174e144
resolve conflicts
FilipNikolovski Jan 3, 2025
241a849
cleanup unused functions
FilipNikolovski Jan 3, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
([#3133](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3133))
- `opentelemetry-instrumentation-falcon` add support version to v4
([#3086](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3086))
- `opentelemetry-instrumentation-falcon` Implement new HTTP semantic convention opt-in for Falcon
emdneto marked this conversation as resolved.
Show resolved Hide resolved
([#2790](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2790))
- `opentelemetry-instrumentation-wsgi` always record span status code to have it available in metrics
([#3148](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3148))
- add support to Python 3.13
Expand Down
2 changes: 1 addition & 1 deletion instrumentation/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
| [opentelemetry-instrumentation-dbapi](./opentelemetry-instrumentation-dbapi) | dbapi | No | experimental
| [opentelemetry-instrumentation-django](./opentelemetry-instrumentation-django) | django >= 1.10 | Yes | experimental
| [opentelemetry-instrumentation-elasticsearch](./opentelemetry-instrumentation-elasticsearch) | elasticsearch >= 6.0 | No | experimental
| [opentelemetry-instrumentation-falcon](./opentelemetry-instrumentation-falcon) | falcon >= 1.4.1, < 5.0.0 | Yes | experimental
| [opentelemetry-instrumentation-falcon](./opentelemetry-instrumentation-falcon) | falcon >= 1.4.1, < 5.0.0 | Yes | migration
| [opentelemetry-instrumentation-fastapi](./opentelemetry-instrumentation-fastapi) | fastapi ~= 0.58 | Yes | migration
| [opentelemetry-instrumentation-flask](./opentelemetry-instrumentation-flask) | flask >= 1.0 | Yes | migration
| [opentelemetry-instrumentation-grpc](./opentelemetry-instrumentation-grpc) | grpcio >= 1.42.0 | No | experimental
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,14 @@ def response_hook(span, req, resp):

import opentelemetry.instrumentation.wsgi as otel_wsgi
from opentelemetry import context, trace
from opentelemetry.instrumentation._semconv import (
_get_schema_url,
_OpenTelemetrySemanticConventionStability,
_OpenTelemetryStabilitySignalType,
_report_new,
_report_old,
_StabilityMode,
)
from opentelemetry.instrumentation.falcon.package import _instruments
from opentelemetry.instrumentation.falcon.version import __version__
from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
Expand All @@ -203,18 +211,22 @@ def response_hook(span, req, resp):
from opentelemetry.instrumentation.utils import (
_start_internal_or_server_span,
extract_attributes_from_object,
http_status_to_status_code,
)
from opentelemetry.metrics import get_meter
from opentelemetry.semconv.attributes.http_attributes import (
HTTP_ROUTE,
)
from opentelemetry.semconv.metrics import MetricInstruments
from opentelemetry.semconv.trace import SpanAttributes
from opentelemetry.trace.status import Status, StatusCode
from opentelemetry.semconv.metrics.http_metrics import (
HTTP_SERVER_REQUEST_DURATION,
)
from opentelemetry.util.http import get_excluded_urls, get_traced_request_attrs

_logger = getLogger(__name__)

_ENVIRON_STARTTIME_KEY = "opentelemetry-falcon.starttime_key"
_ENVIRON_SPAN_KEY = "opentelemetry-falcon.span_key"
_ENVIRON_REQ_ATTRS = "opentelemetry-falcon.req_attrs"
_ENVIRON_ACTIVATION_KEY = "opentelemetry-falcon.activation_key"
_ENVIRON_TOKEN = "opentelemetry-falcon.token"
_ENVIRON_EXC = "opentelemetry-falcon.exc"
Expand Down Expand Up @@ -243,6 +255,10 @@ class _InstrumentedFalconAPI(getattr(falcon, _instrument_app)):
def __init__(self, *args, **kwargs):
otel_opts = kwargs.pop("_otel_opts", {})

self._sem_conv_opt_in_mode = _OpenTelemetrySemanticConventionStability._get_opentelemetry_stability_opt_in_mode(
_OpenTelemetryStabilitySignalType.HTTP,
)

# inject trace middleware
self._middlewares_list = kwargs.pop("middleware", [])
if self._middlewares_list is None:
Expand All @@ -257,19 +273,30 @@ def __init__(self, *args, **kwargs):
__name__,
__version__,
tracer_provider,
schema_url="https://opentelemetry.io/schemas/1.11.0",
schema_url=_get_schema_url(self._sem_conv_opt_in_mode),
)
self._otel_meter = get_meter(
__name__,
__version__,
meter_provider,
schema_url="https://opentelemetry.io/schemas/1.11.0",
)
self.duration_histogram = self._otel_meter.create_histogram(
name=MetricInstruments.HTTP_SERVER_DURATION,
unit="ms",
description="Measures the duration of inbound HTTP requests.",
schema_url=_get_schema_url(self._sem_conv_opt_in_mode),
)

self.duration_histogram_old = None
if _report_old(self._sem_conv_opt_in_mode):
self.duration_histogram_old = self._otel_meter.create_histogram(
name=MetricInstruments.HTTP_SERVER_DURATION,
unit="ms",
description="Measures the duration of inbound HTTP requests.",
)
self.duration_histogram_new = None
if _report_new(self._sem_conv_opt_in_mode):
self.duration_histogram_new = self._otel_meter.create_histogram(
name=HTTP_SERVER_REQUEST_DURATION,
description="Duration of HTTP server requests.",
unit="s",
)

self.active_requests_counter = self._otel_meter.create_up_down_counter(
name=MetricInstruments.HTTP_SERVER_ACTIVE_REQUESTS,
unit="requests",
Expand All @@ -283,6 +310,7 @@ def __init__(self, *args, **kwargs):
),
otel_opts.pop("request_hook", None),
otel_opts.pop("response_hook", None),
self._sem_conv_opt_in_mode,
)
self._middlewares_list.insert(0, trace_middleware)
kwargs["middleware"] = self._middlewares_list
Expand Down Expand Up @@ -343,11 +371,14 @@ def __call__(self, env, start_response):
context_carrier=env,
context_getter=otel_wsgi.wsgi_getter,
)
attributes = otel_wsgi.collect_request_attributes(env)
attributes = otel_wsgi.collect_request_attributes(
env, self._sem_conv_opt_in_mode
)
active_requests_count_attrs = (
otel_wsgi._parse_active_request_count_attrs(attributes)
otel_wsgi._parse_active_request_count_attrs(
attributes, self._sem_conv_opt_in_mode
)
)
duration_attrs = otel_wsgi._parse_duration_attrs(attributes)
self.active_requests_counter.add(1, active_requests_count_attrs)

if span.is_recording():
Expand All @@ -364,6 +395,7 @@ def __call__(self, env, start_response):
activation.__enter__()
env[_ENVIRON_SPAN_KEY] = span
env[_ENVIRON_ACTIVATION_KEY] = activation
env[_ENVIRON_REQ_ATTRS] = attributes
exception = None

def _start_response(status, response_headers, *args, **kwargs):
Expand All @@ -379,12 +411,22 @@ def _start_response(status, response_headers, *args, **kwargs):
exception = exc
raise
finally:
if span.is_recording():
duration_attrs[SpanAttributes.HTTP_STATUS_CODE] = (
span.attributes.get(SpanAttributes.HTTP_STATUS_CODE)
duration_s = default_timer() - start
if self.duration_histogram_old:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: I think you can use add_response_attributes instead to populate duration attributes with status.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if we'll be able to use this one, since it returns early if span is not recording.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've made the necessary updates.

duration_attrs = otel_wsgi._parse_duration_attrs(
attributes, _StabilityMode.DEFAULT
)
self.duration_histogram_old.record(
max(round(duration_s * 1000), 0), duration_attrs
)
if self.duration_histogram_new:
duration_attrs = otel_wsgi._parse_duration_attrs(
attributes, _StabilityMode.HTTP
)
self.duration_histogram_new.record(
max(duration_s, 0), duration_attrs
)
duration = max(round((default_timer() - start) * 1000), 0)
self.duration_histogram.record(duration, duration_attrs)

self.active_requests_counter.add(-1, active_requests_count_attrs)
if exception is None:
activation.__exit__(None, None, None)
Expand All @@ -407,11 +449,13 @@ def __init__(
traced_request_attrs=None,
request_hook=None,
response_hook=None,
sem_conv_opt_in_mode: _StabilityMode = _StabilityMode.DEFAULT,
):
self.tracer = tracer
self._traced_request_attrs = traced_request_attrs
self._request_hook = request_hook
self._response_hook = response_hook
self._sem_conv_opt_in_mode = sem_conv_opt_in_mode

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

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

if not span or not span.is_recording():
if not span:
return

status = resp.status
reason = None
if resource is None:
status = "404"
reason = "NotFound"
status = falcon.HTTP_404
else:
exc_type, exc = None, None
if _ENVIRON_EXC in req.env:
exc = req.env[_ENVIRON_EXC]
exc_type = type(exc)
else:
exc_type, exc = None, None

if exc_type and not req_succeeded:
if "HTTPNotFound" in exc_type.__name__:
status = "404"
reason = "NotFound"
status = falcon.HTTP_404
elif isinstance(exc, (falcon.HTTPError, falcon.HTTPStatus)):
try:
if _falcon_version > 2:
status = falcon.code_to_http_status(exc.status)
else:
status = exc.status
except ValueError:
status = falcon.HTTP_500
else:
status = "500"
reason = f"{exc_type.__name__}: {exc}"
status = falcon.HTTP_500

# Falcon 1 does not support response headers. So
# send an empty dict.
response_headers = {}
if _falcon_version > 1:
response_headers = resp.headers

otel_wsgi.add_response_attributes(
span,
status,
response_headers,
req_attrs,
self._sem_conv_opt_in_mode,
)

status = status.split(" ")[0]
if (
_report_new(self._sem_conv_opt_in_mode)
and req.uri_template
and req_attrs is not None
):
req_attrs[HTTP_ROUTE] = req.uri_template
try:
status_code = int(status)
span.set_attribute(SpanAttributes.HTTP_STATUS_CODE, status_code)
otel_status_code = http_status_to_status_code(
status_code, server_span=True
)

# set the description only when the status code is ERROR
if otel_status_code is not StatusCode.ERROR:
reason = None

span.set_status(
Status(
status_code=otel_status_code,
description=reason,
)
)

# Falcon 1 does not support response headers. So
# send an empty dict.
response_headers = {}
if _falcon_version > 1:
response_headers = resp.headers

if span.is_recording() and span.kind == trace.SpanKind.SERVER:
# Check if low-cardinality route is available as per semantic-conventions
if req.uri_template:
span.update_name(f"{req.method} {req.uri_template}")
span.set_attribute(HTTP_ROUTE, req.uri_template)
else:
span.update_name(f"{req.method}")

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,5 @@
_instruments = ("falcon >= 1.4.1, < 5.0.0",)

_supports_metrics = True

_semconv_status = "migration"
Loading
Loading