Skip to content

Commit 85648ee

Browse files
feat: add option to disable internal send and receive spans
Fixes open-telemetry#831
1 parent af9e841 commit 85648ee

File tree

2 files changed

+90
-46
lines changed

2 files changed

+90
-46
lines changed

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

+66-46
Original file line numberDiff line numberDiff line change
@@ -191,9 +191,11 @@ def client_response_hook(span: Span, scope: dict[str, Any], message: dict[str, A
191191

192192
from __future__ import annotations
193193

194+
import os
194195
import typing
195196
import urllib
196197
from collections import defaultdict
198+
from distutils.util import strtobool
197199
from functools import wraps
198200
from timeit import default_timer
199201
from typing import Any, Awaitable, Callable, DefaultDict, Tuple
@@ -226,6 +228,10 @@ def client_response_hook(span: Span, scope: dict[str, Any], message: dict[str, A
226228
_set_http_user_agent,
227229
_set_status,
228230
)
231+
from opentelemetry.instrumentation.asgi.environment_variables import (
232+
OTEL_PYTHON_ASGI_EXCLUDE_RECEIVE_SPAN,
233+
OTEL_PYTHON_ASGI_EXCLUDE_SEND_SPAN,
234+
)
229235
from opentelemetry.instrumentation.asgi.types import (
230236
ClientRequestHook,
231237
ClientResponseHook,
@@ -527,6 +533,8 @@ class OpenTelemetryMiddleware:
527533
the current globally configured one is used.
528534
meter_provider: The optional meter provider to use. If omitted
529535
the current globally configured one is used.
536+
exclude_receive_span: Optional flag to exclude the http receive span from the trace.
537+
exclude_send_span: Optional flag to exclude the http send span from the trace.
530538
"""
531539

532540
# pylint: disable=too-many-branches
@@ -545,6 +553,8 @@ def __init__(
545553
http_capture_headers_server_request: list[str] | None = None,
546554
http_capture_headers_server_response: list[str] | None = None,
547555
http_capture_headers_sanitize_fields: list[str] | None = None,
556+
exclude_receive_span: bool = False,
557+
exclude_send_span: bool = False,
548558
):
549559
# initialize semantic conventions opt-in if needed
550560
_OpenTelemetrySemanticConventionStability._initialize()
@@ -651,6 +661,12 @@ def __init__(
651661
)
652662
or []
653663
)
664+
self.exclude_receive_span = exclude_receive_span or strtobool(
665+
os.getenv(OTEL_PYTHON_ASGI_EXCLUDE_RECEIVE_SPAN, "false")
666+
)
667+
self.exclude_send_span = exclude_send_span or strtobool(
668+
os.getenv(OTEL_PYTHON_ASGI_EXCLUDE_SEND_SPAN, "false")
669+
)
654670

655671
# pylint: disable=too-many-statements
656672
async def __call__(
@@ -796,6 +812,8 @@ async def __call__(
796812
# pylint: enable=too-many-branches
797813

798814
def _get_otel_receive(self, server_span_name, scope, receive):
815+
if self.exclude_receive_span:
816+
return receive
799817
@wraps(receive)
800818
async def otel_receive():
801819
with self.tracer.start_as_current_span(
@@ -832,41 +850,48 @@ def _get_otel_send(
832850
@wraps(send)
833851
async def otel_send(message: dict[str, Any]):
834852
nonlocal expecting_trailers
835-
with self.tracer.start_as_current_span(
836-
" ".join((server_span_name, scope["type"], "send"))
837-
) as send_span:
838-
if callable(self.client_response_hook):
839-
self.client_response_hook(send_span, scope, message)
840-
841-
status_code = None
842-
if message["type"] == "http.response.start":
843-
status_code = message["status"]
844-
elif message["type"] == "websocket.send":
845-
status_code = 200
846-
847-
if send_span.is_recording():
848-
if message["type"] == "http.response.start":
849-
expecting_trailers = message.get("trailers", False)
850-
send_span.set_attribute("asgi.event.type", message["type"])
851-
if (
852-
server_span.is_recording()
853-
and server_span.kind == trace.SpanKind.SERVER
854-
and "headers" in message
855-
):
856-
custom_response_attributes = (
857-
collect_custom_headers_attributes(
858-
message,
859-
self.http_capture_headers_sanitize_fields,
860-
self.http_capture_headers_server_response,
861-
normalise_response_header_name,
853+
854+
status_code = None
855+
if message["type"] == "http.response.start":
856+
status_code = message["status"]
857+
expecting_trailers = message.get("trailers", False)
858+
elif message["type"] == "websocket.send":
859+
status_code = 200
860+
861+
# Conditional send_span creation
862+
if not self.exclude_send_span:
863+
with self.tracer.start_as_current_span(
864+
" ".join((server_span_name, scope["type"], "send"))
865+
) as send_span:
866+
if callable(self.client_response_hook):
867+
self.client_response_hook(send_span, scope, message)
868+
869+
if send_span.is_recording():
870+
send_span.set_attribute("asgi.event.type", message["type"])
871+
if status_code:
872+
set_status_code(
873+
send_span,
874+
status_code,
875+
None,
876+
self._sem_conv_opt_in_mode,
862877
)
863-
if self.http_capture_headers_server_response
864-
else {}
878+
879+
# Server span logic always applied
880+
if server_span.is_recording() and "headers" in message:
881+
if server_span.kind == trace.SpanKind.SERVER:
882+
custom_response_attributes = (
883+
collect_custom_headers_attributes(
884+
message,
885+
self.http_capture_headers_sanitize_fields,
886+
self.http_capture_headers_server_response,
887+
normalise_response_header_name,
865888
)
866-
if len(custom_response_attributes) > 0:
867-
server_span.set_attributes(
868-
custom_response_attributes
869-
)
889+
if self.http_capture_headers_server_response
890+
else {}
891+
)
892+
if len(custom_response_attributes) > 0:
893+
server_span.set_attributes(custom_response_attributes)
894+
870895
if status_code:
871896
# We record metrics only once
872897
set_status_code(
@@ -875,12 +900,6 @@ async def otel_send(message: dict[str, Any]):
875900
duration_attrs,
876901
self._sem_conv_opt_in_mode,
877902
)
878-
set_status_code(
879-
send_span,
880-
status_code,
881-
None,
882-
self._sem_conv_opt_in_mode,
883-
)
884903

885904
propagator = get_global_response_propagator()
886905
if propagator:
@@ -892,14 +911,15 @@ async def otel_send(message: dict[str, Any]):
892911
setter=asgi_setter,
893912
)
894913

895-
content_length = asgi_getter.get(message, "content-length")
896-
if content_length:
897-
try:
898-
self.content_length_header = int(content_length[0])
899-
except ValueError:
900-
pass
914+
content_length = asgi_getter.get(message, "content-length")
915+
if content_length:
916+
try:
917+
self.content_length_header = int(content_length[0])
918+
except ValueError:
919+
pass
920+
921+
await send(message)
901922

902-
await send(message)
903923
# pylint: disable=too-many-boolean-expressions
904924
if (
905925
not expecting_trailers
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# Copyright The OpenTelemetry Authors
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""
16+
Exclude the http send span from the tracing feature in Python.
17+
"""
18+
19+
OTEL_PYTHON_ASGI_EXCLUDE_SEND_SPAN = "OTEL_PYTHON_ASGI_EXCLUDE_SEND_SPAN"
20+
21+
"""
22+
Exclude the http receive span from the tracing feature in Python.
23+
"""
24+
OTEL_PYTHON_ASGI_EXCLUDE_RECEIVE_SPAN = "OTEL_PYTHON_ASGI_EXCLUDE_RECEIVE_SPAN"

0 commit comments

Comments
 (0)