feat(asgi,fastapi,starlette)!: provide both send and receive hooks with scope and message (#2546)

This commit is contained in:
Pavel Perestoronin
2024-06-03 19:18:47 +02:00
committed by GitHub
parent 73d0fa46a9
commit ed51ebb312
8 changed files with 118 additions and 67 deletions

View File

@ -27,6 +27,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
([#2425](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2425)) ([#2425](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2425))
- `opentelemetry-instrumentation-flask` Add `http.method` to `span.name` - `opentelemetry-instrumentation-flask` Add `http.method` to `span.name`
([#2454](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2454)) ([#2454](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2454))
- ASGI, FastAPI, Starlette: provide both send and receive hooks with `scope` and `message` for internal spans ([#2546](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2546))
### Added ### Added

View File

@ -81,15 +81,15 @@ For example,
.. code-block:: python .. code-block:: python
def server_request_hook(span: Span, scope: dict): def server_request_hook(span: Span, scope: dict[str, Any]):
if span and span.is_recording(): if span and span.is_recording():
span.set_attribute("custom_user_attribute_from_request_hook", "some-value") span.set_attribute("custom_user_attribute_from_request_hook", "some-value")
def client_request_hook(span: Span, scope: dict): def client_request_hook(span: Span, scope: dict[str, Any], message: dict[str, Any]):
if span and span.is_recording(): if span and span.is_recording():
span.set_attribute("custom_user_attribute_from_client_request_hook", "some-value") span.set_attribute("custom_user_attribute_from_client_request_hook", "some-value")
def client_response_hook(span: Span, message: dict): def client_response_hook(span: Span, scope: dict[str, Any], message: dict[str, Any]):
if span and span.is_recording(): if span and span.is_recording():
span.set_attribute("custom_user_attribute_from_response_hook", "some-value") span.set_attribute("custom_user_attribute_from_response_hook", "some-value")
@ -200,6 +200,11 @@ from typing import Any, Awaitable, Callable, Tuple
from asgiref.compatibility import guarantee_single_callable from asgiref.compatibility import guarantee_single_callable
from opentelemetry import context, trace from opentelemetry import context, trace
from opentelemetry.instrumentation.asgi.types import (
ClientRequestHook,
ClientResponseHook,
ServerRequestHook,
)
from opentelemetry.instrumentation.asgi.version import __version__ # noqa from opentelemetry.instrumentation.asgi.version import __version__ # noqa
from opentelemetry.instrumentation.propagators import ( from opentelemetry.instrumentation.propagators import (
get_global_response_propagator, get_global_response_propagator,
@ -212,7 +217,7 @@ from opentelemetry.metrics import get_meter
from opentelemetry.propagators.textmap import Getter, Setter from opentelemetry.propagators.textmap import Getter, Setter
from opentelemetry.semconv.metrics import MetricInstruments from opentelemetry.semconv.metrics import MetricInstruments
from opentelemetry.semconv.trace import SpanAttributes from opentelemetry.semconv.trace import SpanAttributes
from opentelemetry.trace import Span, set_span_in_context from opentelemetry.trace import set_span_in_context
from opentelemetry.trace.status import Status, StatusCode from opentelemetry.trace.status import Status, StatusCode
from opentelemetry.util.http import ( from opentelemetry.util.http import (
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS, OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS,
@ -227,10 +232,6 @@ from opentelemetry.util.http import (
remove_url_credentials, remove_url_credentials,
) )
_ServerRequestHookT = typing.Optional[typing.Callable[[Span, dict], None]]
_ClientRequestHookT = typing.Optional[typing.Callable[[Span, dict], None]]
_ClientResponseHookT = typing.Optional[typing.Callable[[Span, dict], None]]
class ASGIGetter(Getter[dict]): class ASGIGetter(Getter[dict]):
def get( def get(
@ -454,10 +455,10 @@ class OpenTelemetryMiddleware:
Optional: Defaults to get_default_span_details. Optional: Defaults to get_default_span_details.
server_request_hook: Optional callback which is called with the server span and ASGI server_request_hook: Optional callback which is called with the server span and ASGI
scope object for every incoming request. scope object for every incoming request.
client_request_hook: Optional callback which is called with the internal span and an ASGI client_request_hook: Optional callback which is called with the internal span, and ASGI
scope which is sent as a dictionary for when the method receive is called. scope and event which are sent as dictionaries for when the method receive is called.
client_response_hook: Optional callback which is called with the internal span and an ASGI client_response_hook: Optional callback which is called with the internal span, and ASGI
event which is sent as a dictionary for when the method send is called. scope and event which are sent as dictionaries for when the method send is called.
tracer_provider: The optional tracer provider to use. If omitted tracer_provider: The optional tracer provider to use. If omitted
the current globally configured one is used. the current globally configured one is used.
""" """
@ -468,9 +469,9 @@ class OpenTelemetryMiddleware:
app, app,
excluded_urls=None, excluded_urls=None,
default_span_details=None, default_span_details=None,
server_request_hook: _ServerRequestHookT = None, server_request_hook: ServerRequestHook = None,
client_request_hook: _ClientRequestHookT = None, client_request_hook: ClientRequestHook = None,
client_response_hook: _ClientResponseHookT = None, client_response_hook: ClientResponseHook = None,
tracer_provider=None, tracer_provider=None,
meter_provider=None, meter_provider=None,
meter=None, meter=None,
@ -666,9 +667,9 @@ class OpenTelemetryMiddleware:
with self.tracer.start_as_current_span( with self.tracer.start_as_current_span(
" ".join((server_span_name, scope["type"], "receive")) " ".join((server_span_name, scope["type"], "receive"))
) as receive_span: ) as receive_span:
if callable(self.client_request_hook):
self.client_request_hook(receive_span, scope)
message = await receive() message = await receive()
if callable(self.client_request_hook):
self.client_request_hook(receive_span, scope, message)
if receive_span.is_recording(): if receive_span.is_recording():
if message["type"] == "websocket.receive": if message["type"] == "websocket.receive":
set_status_code(receive_span, 200) set_status_code(receive_span, 200)
@ -691,7 +692,7 @@ class OpenTelemetryMiddleware:
" ".join((server_span_name, scope["type"], "send")) " ".join((server_span_name, scope["type"], "send"))
) as send_span: ) as send_span:
if callable(self.client_response_hook): if callable(self.client_response_hook):
self.client_response_hook(send_span, message) self.client_response_hook(send_span, scope, message)
if send_span.is_recording(): if send_span.is_recording():
if message["type"] == "http.response.start": if message["type"] == "http.response.start":
status_code = message["status"] status_code = message["status"]

View File

@ -0,0 +1,49 @@
# 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.
from typing import Any, Callable, Dict, Optional
from opentelemetry.trace import Span
_Scope = Dict[str, Any]
_Message = Dict[str, Any]
ServerRequestHook = Optional[Callable[[Span, _Scope], None]]
"""
Incoming request callback type.
Args:
- Server span
- ASGI scope as a mapping
"""
ClientRequestHook = Optional[Callable[[Span, _Scope, _Message], None]]
"""
Receive callback type.
Args:
- Internal span
- ASGI scope as a mapping
- ASGI event as a mapping
"""
ClientResponseHook = Optional[Callable[[Span, _Scope, _Message], None]]
"""
Send callback type.
Args:
- Internal span
- ASGI scope as a mapping
- ASGI event as a mapping
"""

View File

@ -683,10 +683,10 @@ class TestAsgiApplication(AsgiTestBase):
def server_request_hook(span, scope): def server_request_hook(span, scope):
span.update_name("name from server hook") span.update_name("name from server hook")
def client_request_hook(recieve_span, request): def client_request_hook(receive_span, scope, message):
recieve_span.update_name("name from client request hook") receive_span.update_name("name from client request hook")
def client_response_hook(send_span, response): def client_response_hook(send_span, scope, message):
send_span.set_attribute("attr-from-hook", "value") send_span.set_attribute("attr-from-hook", "value")
def update_expected_hook_results(expected): def update_expected_hook_results(expected):

View File

@ -59,20 +59,20 @@ This instrumentation supports request and response hooks. These are functions th
right after a span is created for a request and right before the span is finished for the response. right after a span is created for a request and right before the span is finished for the response.
- The server request hook is passed a server span and ASGI scope object for every incoming request. - The server request hook is passed a server span and ASGI scope object for every incoming request.
- The client request hook is called with the internal span and an ASGI scope when the method ``receive`` is called. - The client request hook is called with the internal span, and ASGI scope and event when the method ``receive`` is called.
- The client response hook is called with the internal span and an ASGI event when the method ``send`` is called. - The client response hook is called with the internal span, and ASGI scope and event when the method ``send`` is called.
.. code-block:: python .. code-block:: python
def server_request_hook(span: Span, scope: dict): def server_request_hook(span: Span, scope: dict[str, Any]):
if span and span.is_recording(): if span and span.is_recording():
span.set_attribute("custom_user_attribute_from_request_hook", "some-value") span.set_attribute("custom_user_attribute_from_request_hook", "some-value")
def client_request_hook(span: Span, scope: dict): def client_request_hook(span: Span, scope: dict[str, Any], message: dict[str, Any]):
if span and span.is_recording(): if span and span.is_recording():
span.set_attribute("custom_user_attribute_from_client_request_hook", "some-value") span.set_attribute("custom_user_attribute_from_client_request_hook", "some-value")
def client_response_hook(span: Span, message: dict): def client_response_hook(span: Span, scope: dict[str, Any], message: dict[str, Any]):
if span and span.is_recording(): if span and span.is_recording():
span.set_attribute("custom_user_attribute_from_response_hook", "some-value") span.set_attribute("custom_user_attribute_from_response_hook", "some-value")
@ -172,28 +172,27 @@ API
--- ---
""" """
import logging import logging
import typing
from typing import Collection from typing import Collection
import fastapi import fastapi
from starlette.routing import Match from starlette.routing import Match
from opentelemetry.instrumentation.asgi import OpenTelemetryMiddleware from opentelemetry.instrumentation.asgi import OpenTelemetryMiddleware
from opentelemetry.instrumentation.asgi.types import (
ClientRequestHook,
ClientResponseHook,
ServerRequestHook,
)
from opentelemetry.instrumentation.fastapi.package import _instruments from opentelemetry.instrumentation.fastapi.package import _instruments
from opentelemetry.instrumentation.fastapi.version import __version__ from opentelemetry.instrumentation.fastapi.version import __version__
from opentelemetry.instrumentation.instrumentor import BaseInstrumentor from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
from opentelemetry.metrics import get_meter from opentelemetry.metrics import get_meter
from opentelemetry.semconv.trace import SpanAttributes from opentelemetry.semconv.trace import SpanAttributes
from opentelemetry.trace import Span
from opentelemetry.util.http import get_excluded_urls, parse_excluded_urls from opentelemetry.util.http import get_excluded_urls, parse_excluded_urls
_excluded_urls_from_env = get_excluded_urls("FASTAPI") _excluded_urls_from_env = get_excluded_urls("FASTAPI")
_logger = logging.getLogger(__name__) _logger = logging.getLogger(__name__)
_ServerRequestHookT = typing.Optional[typing.Callable[[Span, dict], None]]
_ClientRequestHookT = typing.Optional[typing.Callable[[Span, dict], None]]
_ClientResponseHookT = typing.Optional[typing.Callable[[Span, dict], None]]
class FastAPIInstrumentor(BaseInstrumentor): class FastAPIInstrumentor(BaseInstrumentor):
"""An instrumentor for FastAPI """An instrumentor for FastAPI
@ -206,9 +205,9 @@ class FastAPIInstrumentor(BaseInstrumentor):
@staticmethod @staticmethod
def instrument_app( def instrument_app(
app: fastapi.FastAPI, app: fastapi.FastAPI,
server_request_hook: _ServerRequestHookT = None, server_request_hook: ServerRequestHook = None,
client_request_hook: _ClientRequestHookT = None, client_request_hook: ClientRequestHook = None,
client_response_hook: _ClientResponseHookT = None, client_response_hook: ClientResponseHook = None,
tracer_provider=None, tracer_provider=None,
meter_provider=None, meter_provider=None,
excluded_urls=None, excluded_urls=None,
@ -292,9 +291,9 @@ class _InstrumentedFastAPI(fastapi.FastAPI):
_tracer_provider = None _tracer_provider = None
_meter_provider = None _meter_provider = None
_excluded_urls = None _excluded_urls = None
_server_request_hook: _ServerRequestHookT = None _server_request_hook: ServerRequestHook = None
_client_request_hook: _ClientRequestHookT = None _client_request_hook: ClientRequestHook = None
_client_response_hook: _ClientResponseHookT = None _client_response_hook: ClientResponseHook = None
_instrumented_fastapi_apps = set() _instrumented_fastapi_apps = set()
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):

View File

@ -342,23 +342,23 @@ class TestFastAPIManualInstrumentationHooks(TestFastAPIManualInstrumentation):
if self._server_request_hook is not None: if self._server_request_hook is not None:
self._server_request_hook(span, scope) self._server_request_hook(span, scope)
def client_request_hook(self, receive_span, request): def client_request_hook(self, receive_span, scope, message):
if self._client_request_hook is not None: if self._client_request_hook is not None:
self._client_request_hook(receive_span, request) self._client_request_hook(receive_span, scope, message)
def client_response_hook(self, send_span, response): def client_response_hook(self, send_span, scope, message):
if self._client_response_hook is not None: if self._client_response_hook is not None:
self._client_response_hook(send_span, response) self._client_response_hook(send_span, scope, message)
def test_hooks(self): def test_hooks(self):
def server_request_hook(span, scope): def server_request_hook(span, scope):
span.update_name("name from server hook") span.update_name("name from server hook")
def client_request_hook(receive_span, request): def client_request_hook(receive_span, scope, message):
receive_span.update_name("name from client hook") receive_span.update_name("name from client hook")
receive_span.set_attribute("attr-from-request-hook", "set") receive_span.set_attribute("attr-from-request-hook", "set")
def client_response_hook(send_span, response): def client_response_hook(send_span, scope, message):
send_span.update_name("name from response hook") send_span.update_name("name from response hook")
send_span.set_attribute("attr-from-response-hook", "value") send_span.set_attribute("attr-from-response-hook", "value")

View File

@ -55,20 +55,22 @@ This instrumentation supports request and response hooks. These are functions th
right after a span is created for a request and right before the span is finished for the response. right after a span is created for a request and right before the span is finished for the response.
- The server request hook is passed a server span and ASGI scope object for every incoming request. - The server request hook is passed a server span and ASGI scope object for every incoming request.
- The client request hook is called with the internal span and an ASGI scope when the method ``receive`` is called. - The client request hook is called with the internal span, and ASGI scope and event when the method ``receive`` is called.
- The client response hook is called with the internal span and an ASGI event when the method ``send`` is called. - The client response hook is called with the internal span, and ASGI scope and event when the method ``send`` is called.
For example, For example,
.. code-block:: python .. code-block:: python
def server_request_hook(span: Span, scope: dict): def server_request_hook(span: Span, scope: dict[str, Any]):
if span and span.is_recording(): if span and span.is_recording():
span.set_attribute("custom_user_attribute_from_request_hook", "some-value") span.set_attribute("custom_user_attribute_from_request_hook", "some-value")
def client_request_hook(span: Span, scope: dict):
def client_request_hook(span: Span, scope: dict[str, Any], message: dict[str, Any]):
if span and span.is_recording(): if span and span.is_recording():
span.set_attribute("custom_user_attribute_from_client_request_hook", "some-value") span.set_attribute("custom_user_attribute_from_client_request_hook", "some-value")
def client_response_hook(span: Span, message: dict):
def client_response_hook(span: Span, scope: dict[str, Any], message: dict[str, Any]):
if span and span.is_recording(): if span and span.is_recording():
span.set_attribute("custom_user_attribute_from_response_hook", "some-value") span.set_attribute("custom_user_attribute_from_response_hook", "some-value")
@ -167,27 +169,26 @@ Note:
API API
--- ---
""" """
import typing
from typing import Collection from typing import Collection
from starlette import applications from starlette import applications
from starlette.routing import Match from starlette.routing import Match
from opentelemetry.instrumentation.asgi import OpenTelemetryMiddleware from opentelemetry.instrumentation.asgi import OpenTelemetryMiddleware
from opentelemetry.instrumentation.asgi.types import (
ClientRequestHook,
ClientResponseHook,
ServerRequestHook,
)
from opentelemetry.instrumentation.instrumentor import BaseInstrumentor from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
from opentelemetry.instrumentation.starlette.package import _instruments from opentelemetry.instrumentation.starlette.package import _instruments
from opentelemetry.instrumentation.starlette.version import __version__ from opentelemetry.instrumentation.starlette.version import __version__
from opentelemetry.metrics import get_meter from opentelemetry.metrics import get_meter
from opentelemetry.semconv.trace import SpanAttributes from opentelemetry.semconv.trace import SpanAttributes
from opentelemetry.trace import Span
from opentelemetry.util.http import get_excluded_urls from opentelemetry.util.http import get_excluded_urls
_excluded_urls = get_excluded_urls("STARLETTE") _excluded_urls = get_excluded_urls("STARLETTE")
_ServerRequestHookT = typing.Optional[typing.Callable[[Span, dict], None]]
_ClientRequestHookT = typing.Optional[typing.Callable[[Span, dict], None]]
_ClientResponseHookT = typing.Optional[typing.Callable[[Span, dict], None]]
class StarletteInstrumentor(BaseInstrumentor): class StarletteInstrumentor(BaseInstrumentor):
"""An instrumentor for starlette """An instrumentor for starlette
@ -200,9 +201,9 @@ class StarletteInstrumentor(BaseInstrumentor):
@staticmethod @staticmethod
def instrument_app( def instrument_app(
app: applications.Starlette, app: applications.Starlette,
server_request_hook: _ServerRequestHookT = None, server_request_hook: ServerRequestHook = None,
client_request_hook: _ClientRequestHookT = None, client_request_hook: ClientRequestHook = None,
client_response_hook: _ClientResponseHookT = None, client_response_hook: ClientResponseHook = None,
meter_provider=None, meter_provider=None,
tracer_provider=None, tracer_provider=None,
): ):
@ -270,9 +271,9 @@ class StarletteInstrumentor(BaseInstrumentor):
class _InstrumentedStarlette(applications.Starlette): class _InstrumentedStarlette(applications.Starlette):
_tracer_provider = None _tracer_provider = None
_meter_provider = None _meter_provider = None
_server_request_hook: _ServerRequestHookT = None _server_request_hook: ServerRequestHook = None
_client_request_hook: _ClientRequestHookT = None _client_request_hook: ClientRequestHook = None
_client_response_hook: _ClientResponseHookT = None _client_response_hook: ClientResponseHook = None
_instrumented_starlette_apps = set() _instrumented_starlette_apps = set()
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):

View File

@ -263,23 +263,23 @@ class TestStarletteManualInstrumentationHooks(
if self._server_request_hook is not None: if self._server_request_hook is not None:
self._server_request_hook(span, scope) self._server_request_hook(span, scope)
def client_request_hook(self, receive_span, request): def client_request_hook(self, receive_span, scope, message):
if self._client_request_hook is not None: if self._client_request_hook is not None:
self._client_request_hook(receive_span, request) self._client_request_hook(receive_span, scope, message)
def client_response_hook(self, send_span, response): def client_response_hook(self, send_span, scope, message):
if self._client_response_hook is not None: if self._client_response_hook is not None:
self._client_response_hook(send_span, response) self._client_response_hook(send_span, scope, message)
def test_hooks(self): def test_hooks(self):
def server_request_hook(span, scope): def server_request_hook(span, scope):
span.update_name("name from server hook") span.update_name("name from server hook")
def client_request_hook(receive_span, request): def client_request_hook(receive_span, scope, message):
receive_span.update_name("name from client hook") receive_span.update_name("name from client hook")
receive_span.set_attribute("attr-from-request-hook", "set") receive_span.set_attribute("attr-from-request-hook", "set")
def client_response_hook(send_span, response): def client_response_hook(send_span, scope, message):
send_span.update_name("name from response hook") send_span.update_name("name from response hook")
send_span.set_attribute("attr-from-response-hook", "value") send_span.set_attribute("attr-from-response-hook", "value")