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

HTTP semantic convention stability migration for django #2714

Merged
merged 46 commits into from
Jul 22, 2024
Merged
Show file tree
Hide file tree
Changes from 44 commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
27e4c2c
Update .pylintrc
lzchen Apr 3, 2024
c6b4c05
Merge branch 'main' of https://github.com/open-telemetry/opentelemetr…
lzchen Apr 5, 2024
f690fc8
Merge branch 'main' of https://github.com/open-telemetry/opentelemetr…
lzchen Apr 9, 2024
008c36a
Merge branch 'main' of https://github.com/open-telemetry/opentelemetr…
lzchen Apr 16, 2024
b36c916
Merge branch 'main' of https://github.com/open-telemetry/opentelemetr…
lzchen Apr 22, 2024
2a30d60
Merge branch 'main' of https://github.com/open-telemetry/opentelemetr…
lzchen Apr 24, 2024
4cd5349
Merge branch 'main' of https://github.com/open-telemetry/opentelemetr…
lzchen May 13, 2024
5a7b9ad
Merge branch 'main' of https://github.com/open-telemetry/opentelemetr…
lzchen May 22, 2024
b721434
Merge branch 'main' of https://github.com/open-telemetry/opentelemetr…
lzchen May 23, 2024
6ce38bd
Merge branch 'main' of https://github.com/open-telemetry/opentelemetr…
lzchen May 28, 2024
df3275c
Merge branch 'main' of https://github.com/open-telemetry/opentelemetr…
lzchen May 31, 2024
cffc12f
Merge branch 'main' of https://github.com/open-telemetry/opentelemetr…
lzchen Jun 4, 2024
df80e8a
Merge branch 'main' of https://github.com/open-telemetry/opentelemetr…
lzchen Jun 5, 2024
71df253
Merge branch 'main' of https://github.com/open-telemetry/opentelemetr…
lzchen Jun 7, 2024
4c6d52a
Merge branch 'main' of https://github.com/open-telemetry/opentelemetr…
lzchen Jun 12, 2024
0730c81
Merge branch 'main' of https://github.com/open-telemetry/opentelemetr…
lzchen Jun 14, 2024
91b16eb
Merge branch 'main' of https://github.com/open-telemetry/opentelemetr…
lzchen Jun 17, 2024
2b6f107
Merge branch 'main' of https://github.com/open-telemetry/opentelemetr…
lzchen Jul 3, 2024
d3aee65
Merge branch 'main' of https://github.com/open-telemetry/opentelemetr…
lzchen Jul 8, 2024
b4f600d
Merge branch 'main' of https://github.com/open-telemetry/opentelemetr…
lzchen Jul 11, 2024
1796b7c
Merge branch 'main' of https://github.com/open-telemetry/opentelemetr…
lzchen Jul 15, 2024
c9731e9
Merge branch 'main' of https://github.com/open-telemetry/opentelemetr…
lzchen Jul 15, 2024
9be1808
Merge branch 'main' of https://github.com/open-telemetry/opentelemetr…
lzchen Jul 16, 2024
45a8808
django
lzchen Jul 16, 2024
bfa3292
Merge branch 'main' of https://github.com/open-telemetry/opentelemetr…
lzchen Jul 16, 2024
cd714f9
wsgi
lzchen Jul 18, 2024
240e994
chagelog
lzchen Jul 18, 2024
a454db0
asgi
lzchen Jul 18, 2024
b94396d
Merge branch 'main' of https://github.com/open-telemetry/opentelemetr…
lzchen Jul 18, 2024
80e1ce3
lint
lzchen Jul 18, 2024
e76d724
Merge branch 'main' of https://github.com/open-telemetry/opentelemetr…
lzchen Jul 18, 2024
83964d9
lint
lzchen Jul 18, 2024
1ba98d5
Update CHANGELOG.md
lzchen Jul 19, 2024
6b9acaf
Update CHANGELOG.md
lzchen Jul 19, 2024
c719502
Update CHANGELOG.md
lzchen Jul 19, 2024
ae17c7c
lint
lzchen Jul 19, 2024
31fe38f
Update test_middleware.py
lzchen Jul 19, 2024
1a38232
lint
lzchen Jul 22, 2024
9adb588
lint
lzchen Jul 22, 2024
985b28e
Merge branch 'main' into django
lzchen Jul 22, 2024
3af7e5d
fixes
lzchen Jul 22, 2024
4622d74
Merge branch 'django' of https://github.com/lzchen/opentelemetry-pyth…
lzchen Jul 22, 2024
cf1ec9e
test
lzchen Jul 22, 2024
9f0ae9a
Update test_wsgi_middleware.py
lzchen Jul 22, 2024
fef7677
tests
lzchen Jul 22, 2024
c4c9d31
Update test_middleware.py
lzchen Jul 22, 2024
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
6 changes: 5 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,15 +39,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
([#2673](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2673))
- `opentelemetry-instrumentation-django` Add `http.target` to Django duration metric attributes
([#2624](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2624))
- `opentelemetry-instrumentation-django` Implement new semantic convention opt-in with stable http semantic conventions
([#2714](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2714))

### Breaking changes

- `opentelemetry-instrumentation-asgi`, `opentelemetry-instrumentation-fastapi`, `opentelemetry-instrumentation-starlette` Use `tracer` and `meter` of originating components instead of one from `asgi` middleware
([#2580](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2580))
- Populate `{method}` as `HTTP` on `_OTHER` methods from scope for `asgi` middleware
([#2610](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2610))
- Populate `{method}` as `HTTP` on `_OTHER` methods from scope for `fastapi` middleware
- Populate `{method}` as `HTTP` on `_OTHER` methods from scope for `fastapi` instrumentation
([#2682](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2682))
- Populate `{method}` as `HTTP` on `_OTHER` methods from scope for `django` middleware
([#2714](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2714))

### Fixed
- Handle `redis.exceptions.WatchError` as a non-error event in redis instrumentation
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@ async def middleware(request, handler):
duration_histogram = meter.create_histogram(
name=MetricInstruments.HTTP_SERVER_DURATION,
unit="ms",
description="Duration of HTTP client requests.",
description="Duration of HTTP server requests.",
)

active_requests_counter = meter.create_up_down_counter(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,13 @@ def response_hook(span, request, response):
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured

from opentelemetry.instrumentation._semconv import (
_get_schema_url,
_OpenTelemetrySemanticConventionStability,
_OpenTelemetryStabilitySignalType,
_report_new,
_report_old,
)
from opentelemetry.instrumentation.django.environment_variables import (
OTEL_PYTHON_DJANGO_INSTRUMENT,
)
Expand All @@ -253,7 +260,13 @@ def response_hook(span, request, response):
from opentelemetry.instrumentation.django.version import __version__
from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
from opentelemetry.metrics import get_meter
from opentelemetry.semconv._incubating.metrics.http_metrics import (
create_http_server_active_requests,
)
from opentelemetry.semconv.metrics import MetricInstruments
from opentelemetry.semconv.metrics.http_metrics import (
HTTP_SERVER_REQUEST_DURATION,
)
from opentelemetry.trace import get_tracer
from opentelemetry.util.http import get_excluded_urls, parse_excluded_urls

Expand Down Expand Up @@ -293,21 +306,28 @@ def _instrument(self, **kwargs):
if environ.get(OTEL_PYTHON_DJANGO_INSTRUMENT) == "False":
return

# initialize semantic conventions opt-in if needed
_OpenTelemetrySemanticConventionStability._initialize()
sem_conv_opt_in_mode = _OpenTelemetrySemanticConventionStability._get_opentelemetry_stability_opt_in_mode(
_OpenTelemetryStabilitySignalType.HTTP,
)

tracer_provider = kwargs.get("tracer_provider")
meter_provider = kwargs.get("meter_provider")
_excluded_urls = kwargs.get("excluded_urls")
tracer = get_tracer(
__name__,
__version__,
tracer_provider=tracer_provider,
schema_url="https://opentelemetry.io/schemas/1.11.0",
schema_url=_get_schema_url(sem_conv_opt_in_mode),
)
meter = get_meter(
__name__,
__version__,
meter_provider=meter_provider,
schema_url="https://opentelemetry.io/schemas/1.11.0",
schema_url=_get_schema_url(sem_conv_opt_in_mode),
)
_DjangoMiddleware._sem_conv_opt_in_mode = sem_conv_opt_in_mode
_DjangoMiddleware._tracer = tracer
_DjangoMiddleware._meter = meter
_DjangoMiddleware._excluded_urls = (
Expand All @@ -319,15 +339,22 @@ def _instrument(self, **kwargs):
_DjangoMiddleware._otel_response_hook = kwargs.pop(
"response_hook", None
)
_DjangoMiddleware._duration_histogram = meter.create_histogram(
name=MetricInstruments.HTTP_SERVER_DURATION,
unit="ms",
description="Duration of HTTP client requests.",
)
_DjangoMiddleware._active_request_counter = meter.create_up_down_counter(
name=MetricInstruments.HTTP_SERVER_ACTIVE_REQUESTS,
unit="requests",
description="measures the number of concurrent HTTP requests those are currently in flight",
_DjangoMiddleware._duration_histogram_old = None
if _report_old(sem_conv_opt_in_mode):
_DjangoMiddleware._duration_histogram_old = meter.create_histogram(
name=MetricInstruments.HTTP_SERVER_DURATION,
unit="ms",
description="Duration of HTTP server requests.",
)
_DjangoMiddleware._duration_histogram_new = None
if _report_new(sem_conv_opt_in_mode):
_DjangoMiddleware._duration_histogram_new = meter.create_histogram(
name=HTTP_SERVER_REQUEST_DURATION,
description="Duration of HTTP server requests.",
unit="s",
)
_DjangoMiddleware._active_request_counter = (
create_http_server_active_requests(meter)
)
# This can not be solved, but is an inherent problem of this approach:
# the order of middleware entries matters, and here you have no control
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,17 @@
from django.http import HttpRequest, HttpResponse

from opentelemetry.context import detach
from opentelemetry.instrumentation._semconv import (
_filter_semconv_active_request_count_attr,
_filter_semconv_duration_attrs,
_HTTPStabilityMode,
_report_new,
_report_old,
_server_active_requests_count_attrs_new,
_server_active_requests_count_attrs_old,
_server_duration_attrs_new,
_server_duration_attrs_old,
)
from opentelemetry.instrumentation.propagators import (
get_global_response_propagator,
)
Expand All @@ -40,20 +51,20 @@
collect_request_attributes as wsgi_collect_request_attributes,
)
from opentelemetry.instrumentation.wsgi import wsgi_getter
from opentelemetry.semconv.attributes.http_attributes import HTTP_ROUTE
from opentelemetry.semconv.trace import SpanAttributes
from opentelemetry.trace import Span, SpanKind, use_span
from opentelemetry.util.http import (
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS,
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST,
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE,
SanitizeValue,
_parse_active_request_count_attrs,
_parse_duration_attrs,
get_custom_headers,
get_excluded_urls,
get_traced_request_attrs,
normalise_request_header_name,
normalise_response_header_name,
sanitize_method,
)

try:
Expand Down Expand Up @@ -113,26 +124,6 @@ def __call__(self, request):
_is_asgi_supported = False

_logger = getLogger(__name__)
_attributes_by_preference = [
[
SpanAttributes.HTTP_SCHEME,
SpanAttributes.HTTP_HOST,
SpanAttributes.HTTP_TARGET,
],
[
SpanAttributes.HTTP_SCHEME,
SpanAttributes.HTTP_SERVER_NAME,
SpanAttributes.NET_HOST_PORT,
SpanAttributes.HTTP_TARGET,
],
[
SpanAttributes.HTTP_SCHEME,
SpanAttributes.NET_HOST_NAME,
SpanAttributes.NET_HOST_PORT,
SpanAttributes.HTTP_TARGET,
],
[SpanAttributes.HTTP_URL],
]


def _is_asgi_request(request: HttpRequest) -> bool:
Expand All @@ -159,8 +150,10 @@ class _DjangoMiddleware(MiddlewareMixin):
_excluded_urls = get_excluded_urls("DJANGO")
_tracer = None
_meter = None
_duration_histogram = None
_duration_histogram_old = None
_duration_histogram_new = None
_active_request_counter = None
_sem_conv_opt_in_mode = _HTTPStabilityMode.DEFAULT

_otel_request_hook: Callable[[Span, HttpRequest], None] = None
_otel_response_hook: Callable[[Span, HttpRequest, HttpResponse], None] = (
Expand All @@ -169,17 +162,20 @@ class _DjangoMiddleware(MiddlewareMixin):

@staticmethod
def _get_span_name(request):
method = sanitize_method(request.method.strip())
if method == "_OTHER":
return "HTTP"
try:
if getattr(request, "resolver_match"):
match = request.resolver_match
else:
match = resolve(request.path)

if hasattr(match, "route") and match.route:
return f"{request.method} {match.route}"
return f"{method} {match.route}"

if hasattr(match, "url_name") and match.url_name:
return f"{request.method} {match.url_name}"
return f"{method} {match.url_name}"

return request.method

Expand Down Expand Up @@ -213,7 +209,10 @@ def process_request(self, request):
carrier_getter = wsgi_getter
collect_request_attributes = wsgi_collect_request_attributes

attributes = collect_request_attributes(carrier)
attributes = collect_request_attributes(
carrier,
self._sem_conv_opt_in_mode,
)
span, token = _start_internal_or_server_span(
tracer=self._tracer,
span_name=self._get_span_name(request),
Expand All @@ -226,14 +225,15 @@ def process_request(self, request):
)

active_requests_count_attrs = _parse_active_request_count_attrs(
attributes
attributes,
self._sem_conv_opt_in_mode,
)
duration_attrs = _parse_duration_attrs(attributes)

request.META[self._environ_active_request_attr_key] = (
active_requests_count_attrs
)
request.META[self._environ_duration_attr_key] = duration_attrs
# Pass all of attributes to duration key because we will filter during response
request.META[self._environ_duration_attr_key] = attributes
self._active_request_counter.add(1, active_requests_count_attrs)
if span.is_recording():
attributes = extract_attributes_from_object(
Expand Down Expand Up @@ -309,18 +309,20 @@ def process_view(self, request, view_func, *args, **kwargs):
):
span = request.META[self._environ_span_key]

if span.is_recording():
match = getattr(request, "resolver_match", None)
if match:
route = getattr(match, "route", None)
if route:
match = getattr(request, "resolver_match", None)
if match:
route = getattr(match, "route", None)
if route:
if span.is_recording():
# http.route is present for both old and new semconv
span.set_attribute(SpanAttributes.HTTP_ROUTE, route)
duration_attrs = request.META[
self._environ_duration_attr_key
]
# Metrics currently use the 1.11.0 schema, which puts the route in `http.target`.
# TODO: use `http.route` when the user sets `OTEL_SEMCONV_STABILITY_OPT_IN`.
duration_attrs = request.META[
self._environ_duration_attr_key
]
if _report_old(self._sem_conv_opt_in_mode):
duration_attrs[SpanAttributes.HTTP_TARGET] = route
if _report_new(self._sem_conv_opt_in_mode):
duration_attrs[HTTP_ROUTE] = route

def process_exception(self, request, exception):
if self._excluded_urls.url_disabled(request.build_absolute_uri("?")):
Expand All @@ -347,15 +349,16 @@ def process_response(self, request, response):
duration_attrs = request.META.pop(
self._environ_duration_attr_key, None
)
if duration_attrs:
duration_attrs[SpanAttributes.HTTP_STATUS_CODE] = (
response.status_code
)
request_start_time = request.META.pop(self._environ_timer_key, None)

if activation and span:
if is_asgi_request:
set_status_code(span, response.status_code)
set_status_code(
span,
response.status_code,
metric_attributes=duration_attrs,
sem_conv_opt_in_mode=self._sem_conv_opt_in_mode,
)

if span.is_recording() and span.kind == SpanKind.SERVER:
custom_headers = {}
Expand All @@ -381,6 +384,8 @@ def process_response(self, request, response):
span,
f"{response.status_code} {response.reason_phrase}",
response.items(),
duration_attrs=duration_attrs,
sem_conv_opt_in_mode=self._sem_conv_opt_in_mode,
)
if span.is_recording() and span.kind == SpanKind.SERVER:
custom_attributes = (
Expand Down Expand Up @@ -416,13 +421,46 @@ def process_response(self, request, response):
activation.__exit__(None, None, None)

if request_start_time is not None:
duration = max(
round((default_timer() - request_start_time) * 1000), 0
)
self._duration_histogram.record(duration, duration_attrs)
duration_s = default_timer() - request_start_time
if self._duration_histogram_old:
duration_attrs_old = _parse_duration_attrs(
duration_attrs, _HTTPStabilityMode.DEFAULT
)
self._duration_histogram_old.record(
max(round(duration_s * 1000), 0), duration_attrs_old
)
if self._duration_histogram_new:
duration_attrs_new = _parse_duration_attrs(
duration_attrs, _HTTPStabilityMode.HTTP
)
self._duration_histogram_new.record(
max(duration_s, 0), duration_attrs_new
)
self._active_request_counter.add(-1, active_requests_count_attrs)
if request.META.get(self._environ_token, None) is not None:
detach(request.META.get(self._environ_token))
request.META.pop(self._environ_token)

return response


def _parse_duration_attrs(
req_attrs, sem_conv_opt_in_mode=_HTTPStabilityMode.DEFAULT
):
return _filter_semconv_duration_attrs(
req_attrs,
_server_duration_attrs_old,
_server_duration_attrs_new,
sem_conv_opt_in_mode,
)


def _parse_active_request_count_attrs(
req_attrs, sem_conv_opt_in_mode=_HTTPStabilityMode.DEFAULT
):
return _filter_semconv_active_request_count_attr(
req_attrs,
_server_active_requests_count_attrs_old,
_server_active_requests_count_attrs_new,
sem_conv_opt_in_mode,
)
Loading