Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions .changelog/5255.added
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
`opentelemetry-instrumentation-asgi`: add `OTEL_PYTHON_ASGI_EXCLUDE_SPANS` environment variable to exclude internal `send`/`receive` spans
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,20 @@ def client_response_hook(span: Span, scope: Scope, message: dict[str, Any]):
client_response_hook=client_response_hook,
)

Exclude internal send/receive spans
***********************************
To skip the internal ``http send`` and ``http receive`` spans, set the environment variable
``OTEL_PYTHON_ASGI_EXCLUDE_SPANS`` to a comma delimited list of ``send`` and/or ``receive``,
or pass the ``exclude_spans`` keyword argument to ``OpenTelemetryMiddleware``.

For example,
::

export OTEL_PYTHON_ASGI_EXCLUDE_SPANS="send,receive"

The ``exclude_spans`` argument takes precedence over the environment variable. The setting also applies
to instrumentations built on this middleware, such as FastAPI and Starlette.

Capture HTTP request and response headers
*****************************************
You can configure the agent to capture specified HTTP headers as span attributes, according to the
Expand Down Expand Up @@ -302,6 +316,8 @@ def client_response_hook(span: Span, scope: Scope, message: dict[str, Any]):
sanitize_method,
)

OTEL_PYTHON_ASGI_EXCLUDE_SPANS = "OTEL_PYTHON_ASGI_EXCLUDE_SPANS"


class ASGIGetter(Getter[dict]):
def get(self, carrier: dict, key: str) -> list[str] | None:
Expand Down Expand Up @@ -575,6 +591,7 @@ class OpenTelemetryMiddleware:
meter_provider: The optional meter provider to use. If omitted
the current globally configured one is used.
exclude_spans: Optionally exclude HTTP `send` and/or `receive` spans from the trace.
Defaults to the value of ``OTEL_PYTHON_ASGI_EXCLUDE_SPANS``.
"""

# pylint: disable=too-many-branches,too-many-positional-arguments
Expand Down Expand Up @@ -697,8 +714,10 @@ def wrapper(span: Span, *args, **kwargs):
or (get_custom_headers(OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS))
or []
)
self.exclude_receive_span = "receive" in exclude_spans if exclude_spans else False
self.exclude_send_span = "send" in exclude_spans if exclude_spans else False
if exclude_spans is None:
exclude_spans = get_custom_headers(OTEL_PYTHON_ASGI_EXCLUDE_SPANS)
self.exclude_receive_span = "receive" in exclude_spans
self.exclude_send_span = "send" in exclude_spans

def _collect_custom_request_headers_attributes(self, scope) -> dict[str, list[str]]:
if not self.http_capture_headers_server_request:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -662,6 +662,38 @@ async def test_exclude_internal_spans(self):
for excluded_span in excluded_spans:
self.assertNotEqual(span.name, excluded_span)

async def test_exclude_internal_spans_from_env(self):
cases = [
("receive,send", {"GET /"}),
(" send ", {"GET /", "GET / http receive"}),
("receive", {"GET /", "GET / http send"}),
("", {"GET /", "GET / http receive", "GET / http send"}),
]
for env_value, expected_span_names in cases:
with self.subTest(env_value=env_value):
with mock.patch.dict(
"os.environ",
{otel_asgi.OTEL_PYTHON_ASGI_EXCLUDE_SPANS: env_value},
):
app = otel_asgi.OpenTelemetryMiddleware(simple_asgi)
self.seed_app(app)
await self.send_default_request()
await self.get_all_output()
span_names = {span.name for span in self.get_finished_spans()}
self.assertEqual(span_names, expected_span_names)

async def test_exclude_spans_argument_overrides_env(self):
with mock.patch.dict(
"os.environ",
{otel_asgi.OTEL_PYTHON_ASGI_EXCLUDE_SPANS: "receive,send"},
):
app = otel_asgi.OpenTelemetryMiddleware(simple_asgi, exclude_spans=[])
self.seed_app(app)
await self.send_default_request()
await self.get_all_output()
span_names = {span.name for span in self.get_finished_spans()}
self.assertEqual(span_names, {"GET /", "GET / http receive", "GET / http send"})

async def test_trailers(self):
"""Test that trailers are emitted as expected and that the server span is ended
BEFORE the background task is finished."""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,19 @@ async def foobar():

FastAPIInstrumentor.instrument_app(app, excluded_urls="client/.*/info,healthcheck")

To skip the internal ``http send`` and ``http receive`` spans, set the environment variable
``OTEL_PYTHON_ASGI_EXCLUDE_SPANS`` to a comma delimited list of ``send`` and/or ``receive``:

::

export OTEL_PYTHON_ASGI_EXCLUDE_SPANS="send,receive"

or pass the ``exclude_spans`` keyword argument, which takes precedence over the environment variable:

.. code-block:: python

FastAPIInstrumentor.instrument_app(app, exclude_spans=["send", "receive"])

Request/Response hooks
**********************

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,10 @@
_server_duration_attrs_new,
_server_duration_attrs_old,
)
from opentelemetry.instrumentation.asgi import OpenTelemetryMiddleware
from opentelemetry.instrumentation.asgi import (
OTEL_PYTHON_ASGI_EXCLUDE_SPANS,
OpenTelemetryMiddleware,
)
from opentelemetry.instrumentation.auto_instrumentation._load import (
_load_instrumentors,
)
Expand Down Expand Up @@ -1255,6 +1258,12 @@ def _create_app_explicit_excluded_urls(self):
)
return self._create_fastapi_app()

def test_exclude_spans_from_env(self):
with patch.dict("os.environ", {OTEL_PYTHON_ASGI_EXCLUDE_SPANS: "receive,send"}):
TestClient(self._create_fastapi_app()).get("/foobar")
spans = self.memory_exporter.get_finished_spans()
self.assertEqual([span.name for span in spans], ["GET /foobar"])

def test_request(self):
self._client.get("/foobar")
spans = self.memory_exporter.get_finished_spans()
Expand Down
Loading