Skip to content

Commit

Permalink
HTTP semantic convention stability migration for django (#2714)
Browse files Browse the repository at this point in the history
  • Loading branch information
lzchen authored Jul 22, 2024
1 parent a322a0a commit 910d5ec
Show file tree
Hide file tree
Showing 9 changed files with 736 additions and 71 deletions.
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

0 comments on commit 910d5ec

Please sign in to comment.