Skip to content

Commit

Permalink
feat: add option to disable internal send and receive spans
Browse files Browse the repository at this point in the history
Fixes #831
  • Loading branch information
omgitsaheadcrab committed Aug 16, 2024
1 parent af9e841 commit 85648ee
Show file tree
Hide file tree
Showing 2 changed files with 90 additions and 46 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -191,9 +191,11 @@ def client_response_hook(span: Span, scope: dict[str, Any], message: dict[str, A

from __future__ import annotations

import os
import typing
import urllib
from collections import defaultdict
from distutils.util import strtobool
from functools import wraps
from timeit import default_timer
from typing import Any, Awaitable, Callable, DefaultDict, Tuple
Expand Down Expand Up @@ -226,6 +228,10 @@ def client_response_hook(span: Span, scope: dict[str, Any], message: dict[str, A
_set_http_user_agent,
_set_status,
)
from opentelemetry.instrumentation.asgi.environment_variables import (
OTEL_PYTHON_ASGI_EXCLUDE_RECEIVE_SPAN,
OTEL_PYTHON_ASGI_EXCLUDE_SEND_SPAN,
)
from opentelemetry.instrumentation.asgi.types import (
ClientRequestHook,
ClientResponseHook,
Expand Down Expand Up @@ -527,6 +533,8 @@ class OpenTelemetryMiddleware:
the current globally configured one is used.
meter_provider: The optional meter provider to use. If omitted
the current globally configured one is used.
exclude_receive_span: Optional flag to exclude the http receive span from the trace.
exclude_send_span: Optional flag to exclude the http send span from the trace.
"""

# pylint: disable=too-many-branches
Expand All @@ -545,6 +553,8 @@ def __init__(
http_capture_headers_server_request: list[str] | None = None,
http_capture_headers_server_response: list[str] | None = None,
http_capture_headers_sanitize_fields: list[str] | None = None,
exclude_receive_span: bool = False,
exclude_send_span: bool = False,
):
# initialize semantic conventions opt-in if needed
_OpenTelemetrySemanticConventionStability._initialize()
Expand Down Expand Up @@ -651,6 +661,12 @@ def __init__(
)
or []
)
self.exclude_receive_span = exclude_receive_span or strtobool(
os.getenv(OTEL_PYTHON_ASGI_EXCLUDE_RECEIVE_SPAN, "false")
)
self.exclude_send_span = exclude_send_span or strtobool(
os.getenv(OTEL_PYTHON_ASGI_EXCLUDE_SEND_SPAN, "false")
)

# pylint: disable=too-many-statements
async def __call__(
Expand Down Expand Up @@ -796,6 +812,8 @@ async def __call__(
# pylint: enable=too-many-branches

def _get_otel_receive(self, server_span_name, scope, receive):
if self.exclude_receive_span:
return receive
@wraps(receive)
async def otel_receive():
with self.tracer.start_as_current_span(
Expand Down Expand Up @@ -832,41 +850,48 @@ def _get_otel_send(
@wraps(send)
async def otel_send(message: dict[str, Any]):
nonlocal expecting_trailers
with self.tracer.start_as_current_span(
" ".join((server_span_name, scope["type"], "send"))
) as send_span:
if callable(self.client_response_hook):
self.client_response_hook(send_span, scope, message)

status_code = None
if message["type"] == "http.response.start":
status_code = message["status"]
elif message["type"] == "websocket.send":
status_code = 200

if send_span.is_recording():
if message["type"] == "http.response.start":
expecting_trailers = message.get("trailers", False)
send_span.set_attribute("asgi.event.type", message["type"])
if (
server_span.is_recording()
and server_span.kind == trace.SpanKind.SERVER
and "headers" in message
):
custom_response_attributes = (
collect_custom_headers_attributes(
message,
self.http_capture_headers_sanitize_fields,
self.http_capture_headers_server_response,
normalise_response_header_name,

status_code = None
if message["type"] == "http.response.start":
status_code = message["status"]
expecting_trailers = message.get("trailers", False)
elif message["type"] == "websocket.send":
status_code = 200

# Conditional send_span creation
if not self.exclude_send_span:
with self.tracer.start_as_current_span(
" ".join((server_span_name, scope["type"], "send"))
) as send_span:
if callable(self.client_response_hook):
self.client_response_hook(send_span, scope, message)

if send_span.is_recording():
send_span.set_attribute("asgi.event.type", message["type"])
if status_code:
set_status_code(
send_span,
status_code,
None,
self._sem_conv_opt_in_mode,
)
if self.http_capture_headers_server_response
else {}

# Server span logic always applied
if server_span.is_recording() and "headers" in message:
if server_span.kind == trace.SpanKind.SERVER:
custom_response_attributes = (
collect_custom_headers_attributes(
message,
self.http_capture_headers_sanitize_fields,
self.http_capture_headers_server_response,
normalise_response_header_name,
)
if len(custom_response_attributes) > 0:
server_span.set_attributes(
custom_response_attributes
)
if self.http_capture_headers_server_response
else {}
)
if len(custom_response_attributes) > 0:
server_span.set_attributes(custom_response_attributes)

if status_code:
# We record metrics only once
set_status_code(
Expand All @@ -875,12 +900,6 @@ async def otel_send(message: dict[str, Any]):
duration_attrs,
self._sem_conv_opt_in_mode,
)
set_status_code(
send_span,
status_code,
None,
self._sem_conv_opt_in_mode,
)

propagator = get_global_response_propagator()
if propagator:
Expand All @@ -892,14 +911,15 @@ async def otel_send(message: dict[str, Any]):
setter=asgi_setter,
)

content_length = asgi_getter.get(message, "content-length")
if content_length:
try:
self.content_length_header = int(content_length[0])
except ValueError:
pass
content_length = asgi_getter.get(message, "content-length")
if content_length:
try:
self.content_length_header = int(content_length[0])
except ValueError:
pass

await send(message)

await send(message)
# pylint: disable=too-many-boolean-expressions
if (
not expecting_trailers
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Copyright The OpenTelemetry Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""
Exclude the http send span from the tracing feature in Python.
"""

OTEL_PYTHON_ASGI_EXCLUDE_SEND_SPAN = "OTEL_PYTHON_ASGI_EXCLUDE_SEND_SPAN"

"""
Exclude the http receive span from the tracing feature in Python.
"""
OTEL_PYTHON_ASGI_EXCLUDE_RECEIVE_SPAN = "OTEL_PYTHON_ASGI_EXCLUDE_RECEIVE_SPAN"

0 comments on commit 85648ee

Please sign in to comment.