Skip to content

Commit eaacb51

Browse files
committed
feat(asgi,fastapi,starlette)!: provide both send and receive hooks with scope and message
Currently, a «receive» hook is only provided with `scope`, but not `message`. On contrary, a «send» hook is only provided with `message`, but not `scope`. This change unifies the both hooks and provides them with additional request info. This is a breaking change: - The hooks will now be provided with one more positional argument - `client_request_hook` will be called _after_ `receive()` instead of _before_ `receive()`
1 parent eb8e456 commit eaacb51

File tree

8 files changed

+118
-67
lines changed

8 files changed

+118
-67
lines changed

Diff for: CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1919
([#2425](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2425))
2020
- `opentelemetry-instrumentation-flask` Add `http.method` to `span.name`
2121
([#2454](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2454))
22+
- ASGI, FastAPI, Starlette: provide both send and receive hooks with `scope` and `message` ([#2546](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2546))
2223

2324
### Added
2425

Diff for: instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/__init__.py

+19-18
Original file line numberDiff line numberDiff line change
@@ -81,15 +81,15 @@ async def hello():
8181
8282
.. code-block:: python
8383
84-
def server_request_hook(span: Span, scope: dict):
84+
def server_request_hook(span: Span, scope: dict[str, Any]):
8585
if span and span.is_recording():
8686
span.set_attribute("custom_user_attribute_from_request_hook", "some-value")
8787
88-
def client_request_hook(span: Span, scope: dict):
88+
def client_request_hook(span: Span, scope: dict[str, Any], message: dict[str, Any]):
8989
if span and span.is_recording():
9090
span.set_attribute("custom_user_attribute_from_client_request_hook", "some-value")
9191
92-
def client_response_hook(span: Span, message: dict):
92+
def client_response_hook(span: Span, scope: dict[str, Any], message: dict[str, Any]):
9393
if span and span.is_recording():
9494
span.set_attribute("custom_user_attribute_from_response_hook", "some-value")
9595
@@ -200,6 +200,11 @@ def client_response_hook(span: Span, message: dict):
200200
from asgiref.compatibility import guarantee_single_callable
201201

202202
from opentelemetry import context, trace
203+
from opentelemetry.instrumentation.asgi.types import (
204+
ClientRequestHook,
205+
ClientResponseHook,
206+
ServerRequestHook,
207+
)
203208
from opentelemetry.instrumentation.asgi.version import __version__ # noqa
204209
from opentelemetry.instrumentation.propagators import (
205210
get_global_response_propagator,
@@ -212,7 +217,7 @@ def client_response_hook(span: Span, message: dict):
212217
from opentelemetry.propagators.textmap import Getter, Setter
213218
from opentelemetry.semconv.metrics import MetricInstruments
214219
from opentelemetry.semconv.trace import SpanAttributes
215-
from opentelemetry.trace import Span, set_span_in_context
220+
from opentelemetry.trace import set_span_in_context
216221
from opentelemetry.trace.status import Status, StatusCode
217222
from opentelemetry.util.http import (
218223
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS,
@@ -227,10 +232,6 @@ def client_response_hook(span: Span, message: dict):
227232
remove_url_credentials,
228233
)
229234

230-
_ServerRequestHookT = typing.Optional[typing.Callable[[Span, dict], None]]
231-
_ClientRequestHookT = typing.Optional[typing.Callable[[Span, dict], None]]
232-
_ClientResponseHookT = typing.Optional[typing.Callable[[Span, dict], None]]
233-
234235

235236
class ASGIGetter(Getter[dict]):
236237
def get(
@@ -454,10 +455,10 @@ class OpenTelemetryMiddleware:
454455
Optional: Defaults to get_default_span_details.
455456
server_request_hook: Optional callback which is called with the server span and ASGI
456457
scope object for every incoming request.
457-
client_request_hook: Optional callback which is called with the internal span and an ASGI
458-
scope which is sent as a dictionary for when the method receive is called.
459-
client_response_hook: Optional callback which is called with the internal span and an ASGI
460-
event which is sent as a dictionary for when the method send is called.
458+
client_request_hook: Optional callback which is called with the internal span, and ASGI
459+
scope and event which are sent as dictionaries for when the method receive is called.
460+
client_response_hook: Optional callback which is called with the internal span, and ASGI
461+
scope and event which are sent as dictionaries for when the method send is called.
461462
tracer_provider: The optional tracer provider to use. If omitted
462463
the current globally configured one is used.
463464
"""
@@ -468,9 +469,9 @@ def __init__(
468469
app,
469470
excluded_urls=None,
470471
default_span_details=None,
471-
server_request_hook: _ServerRequestHookT = None,
472-
client_request_hook: _ClientRequestHookT = None,
473-
client_response_hook: _ClientResponseHookT = None,
472+
server_request_hook: ServerRequestHook = None,
473+
client_request_hook: ClientRequestHook = None,
474+
client_response_hook: ClientResponseHook = None,
474475
tracer_provider=None,
475476
meter_provider=None,
476477
meter=None,
@@ -666,9 +667,9 @@ async def otel_receive():
666667
with self.tracer.start_as_current_span(
667668
" ".join((server_span_name, scope["type"], "receive"))
668669
) as receive_span:
669-
if callable(self.client_request_hook):
670-
self.client_request_hook(receive_span, scope)
671670
message = await receive()
671+
if callable(self.client_request_hook):
672+
self.client_request_hook(receive_span, scope, message)
672673
if receive_span.is_recording():
673674
if message["type"] == "websocket.receive":
674675
set_status_code(receive_span, 200)
@@ -691,7 +692,7 @@ async def otel_send(message: dict[str, Any]):
691692
" ".join((server_span_name, scope["type"], "send"))
692693
) as send_span:
693694
if callable(self.client_response_hook):
694-
self.client_response_hook(send_span, message)
695+
self.client_response_hook(send_span, scope, message)
695696
if send_span.is_recording():
696697
if message["type"] == "http.response.start":
697698
status_code = message["status"]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
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+
from typing import Any, Callable, Dict, Optional
16+
17+
from opentelemetry.trace import Span
18+
19+
_Scope = Dict[str, Any]
20+
_Message = Dict[str, Any]
21+
22+
ServerRequestHook = Optional[Callable[[Span, _Scope], None]]
23+
"""
24+
Incoming request callback type.
25+
26+
Args:
27+
- Server span
28+
- ASGI scope as a mapping
29+
"""
30+
31+
ClientRequestHook = Optional[Callable[[Span, _Scope, _Message], None]]
32+
"""
33+
Receive callback type.
34+
35+
Args:
36+
- Internal span
37+
- ASGI scope as a mapping
38+
- ASGI event as a mapping
39+
"""
40+
41+
ClientResponseHook = Optional[Callable[[Span, _Scope, _Message], None]]
42+
"""
43+
Send callback type.
44+
45+
Args:
46+
- Internal span
47+
- ASGI scope as a mapping
48+
- ASGI event as a mapping
49+
"""

Diff for: instrumentation/opentelemetry-instrumentation-asgi/tests/test_asgi_middleware.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -683,10 +683,10 @@ def test_hooks(self):
683683
def server_request_hook(span, scope):
684684
span.update_name("name from server hook")
685685

686-
def client_request_hook(recieve_span, request):
687-
recieve_span.update_name("name from client request hook")
686+
def client_request_hook(receive_span, scope, message):
687+
receive_span.update_name("name from client request hook")
688688

689-
def client_response_hook(send_span, response):
689+
def client_response_hook(send_span, scope, message):
690690
send_span.set_attribute("attr-from-hook", "value")
691691

692692
def update_expected_hook_results(expected):

Diff for: instrumentation/opentelemetry-instrumentation-fastapi/src/opentelemetry/instrumentation/fastapi/__init__.py

+16-17
Original file line numberDiff line numberDiff line change
@@ -59,20 +59,20 @@ async def foobar():
5959
right after a span is created for a request and right before the span is finished for the response.
6060
6161
- The server request hook is passed a server span and ASGI scope object for every incoming request.
62-
- The client request hook is called with the internal span and an ASGI scope when the method ``receive`` is called.
63-
- The client response hook is called with the internal span and an ASGI event when the method ``send`` is called.
62+
- The client request hook is called with the internal span, and ASGI scope and event when the method ``receive`` is called.
63+
- The client response hook is called with the internal span, and ASGI scope and event when the method ``send`` is called.
6464
6565
.. code-block:: python
6666
67-
def server_request_hook(span: Span, scope: dict):
67+
def server_request_hook(span: Span, scope: dict[str, Any]):
6868
if span and span.is_recording():
6969
span.set_attribute("custom_user_attribute_from_request_hook", "some-value")
7070
71-
def client_request_hook(span: Span, scope: dict):
71+
def client_request_hook(span: Span, scope: dict[str, Any], message: dict[str, Any]):
7272
if span and span.is_recording():
7373
span.set_attribute("custom_user_attribute_from_client_request_hook", "some-value")
7474
75-
def client_response_hook(span: Span, message: dict):
75+
def client_response_hook(span: Span, scope: dict[str, Any], message: dict[str, Any]):
7676
if span and span.is_recording():
7777
span.set_attribute("custom_user_attribute_from_response_hook", "some-value")
7878
@@ -172,28 +172,27 @@ def client_response_hook(span: Span, message: dict):
172172
---
173173
"""
174174
import logging
175-
import typing
176175
from typing import Collection
177176

178177
import fastapi
179178
from starlette.routing import Match
180179

181180
from opentelemetry.instrumentation.asgi import OpenTelemetryMiddleware
181+
from opentelemetry.instrumentation.asgi.types import (
182+
ClientRequestHook,
183+
ClientResponseHook,
184+
ServerRequestHook,
185+
)
182186
from opentelemetry.instrumentation.fastapi.package import _instruments
183187
from opentelemetry.instrumentation.fastapi.version import __version__
184188
from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
185189
from opentelemetry.metrics import get_meter
186190
from opentelemetry.semconv.trace import SpanAttributes
187-
from opentelemetry.trace import Span
188191
from opentelemetry.util.http import get_excluded_urls, parse_excluded_urls
189192

190193
_excluded_urls_from_env = get_excluded_urls("FASTAPI")
191194
_logger = logging.getLogger(__name__)
192195

193-
_ServerRequestHookT = typing.Optional[typing.Callable[[Span, dict], None]]
194-
_ClientRequestHookT = typing.Optional[typing.Callable[[Span, dict], None]]
195-
_ClientResponseHookT = typing.Optional[typing.Callable[[Span, dict], None]]
196-
197196

198197
class FastAPIInstrumentor(BaseInstrumentor):
199198
"""An instrumentor for FastAPI
@@ -206,9 +205,9 @@ class FastAPIInstrumentor(BaseInstrumentor):
206205
@staticmethod
207206
def instrument_app(
208207
app: fastapi.FastAPI,
209-
server_request_hook: _ServerRequestHookT = None,
210-
client_request_hook: _ClientRequestHookT = None,
211-
client_response_hook: _ClientResponseHookT = None,
208+
server_request_hook: ServerRequestHook = None,
209+
client_request_hook: ClientRequestHook = None,
210+
client_response_hook: ClientResponseHook = None,
212211
tracer_provider=None,
213212
meter_provider=None,
214213
excluded_urls=None,
@@ -292,9 +291,9 @@ class _InstrumentedFastAPI(fastapi.FastAPI):
292291
_tracer_provider = None
293292
_meter_provider = None
294293
_excluded_urls = None
295-
_server_request_hook: _ServerRequestHookT = None
296-
_client_request_hook: _ClientRequestHookT = None
297-
_client_response_hook: _ClientResponseHookT = None
294+
_server_request_hook: ServerRequestHook = None
295+
_client_request_hook: ClientRequestHook = None
296+
_client_response_hook: ClientResponseHook = None
298297
_instrumented_fastapi_apps = set()
299298

300299
def __init__(self, *args, **kwargs):

Diff for: instrumentation/opentelemetry-instrumentation-fastapi/tests/test_fastapi_instrumentation.py

+6-6
Original file line numberDiff line numberDiff line change
@@ -342,23 +342,23 @@ def server_request_hook(self, span, scope):
342342
if self._server_request_hook is not None:
343343
self._server_request_hook(span, scope)
344344

345-
def client_request_hook(self, receive_span, request):
345+
def client_request_hook(self, receive_span, scope, message):
346346
if self._client_request_hook is not None:
347-
self._client_request_hook(receive_span, request)
347+
self._client_request_hook(receive_span, scope, message)
348348

349-
def client_response_hook(self, send_span, response):
349+
def client_response_hook(self, send_span, scope, message):
350350
if self._client_response_hook is not None:
351-
self._client_response_hook(send_span, response)
351+
self._client_response_hook(send_span, scope, message)
352352

353353
def test_hooks(self):
354354
def server_request_hook(span, scope):
355355
span.update_name("name from server hook")
356356

357-
def client_request_hook(receive_span, request):
357+
def client_request_hook(receive_span, scope, message):
358358
receive_span.update_name("name from client hook")
359359
receive_span.set_attribute("attr-from-request-hook", "set")
360360

361-
def client_response_hook(send_span, response):
361+
def client_response_hook(send_span, scope, message):
362362
send_span.update_name("name from response hook")
363363
send_span.set_attribute("attr-from-response-hook", "value")
364364

Diff for: instrumentation/opentelemetry-instrumentation-starlette/src/opentelemetry/instrumentation/starlette/__init__.py

+18-17
Original file line numberDiff line numberDiff line change
@@ -55,20 +55,22 @@ def home(request):
5555
right after a span is created for a request and right before the span is finished for the response.
5656
5757
- The server request hook is passed a server span and ASGI scope object for every incoming request.
58-
- The client request hook is called with the internal span and an ASGI scope when the method ``receive`` is called.
59-
- The client response hook is called with the internal span and an ASGI event when the method ``send`` is called.
58+
- The client request hook is called with the internal span, and ASGI scope and event when the method ``receive`` is called.
59+
- The client response hook is called with the internal span, and ASGI scope and event when the method ``send`` is called.
6060
6161
For example,
6262
6363
.. code-block:: python
6464
65-
def server_request_hook(span: Span, scope: dict):
65+
def server_request_hook(span: Span, scope: dict[str, Any]):
6666
if span and span.is_recording():
6767
span.set_attribute("custom_user_attribute_from_request_hook", "some-value")
68-
def client_request_hook(span: Span, scope: dict):
68+
69+
def client_request_hook(span: Span, scope: dict[str, Any], message: dict[str, Any]):
6970
if span and span.is_recording():
7071
span.set_attribute("custom_user_attribute_from_client_request_hook", "some-value")
71-
def client_response_hook(span: Span, message: dict):
72+
73+
def client_response_hook(span: Span, scope: dict[str, Any], message: dict[str, Any]):
7274
if span and span.is_recording():
7375
span.set_attribute("custom_user_attribute_from_response_hook", "some-value")
7476
@@ -167,27 +169,26 @@ def client_response_hook(span: Span, message: dict):
167169
API
168170
---
169171
"""
170-
import typing
171172
from typing import Collection
172173

173174
from starlette import applications
174175
from starlette.routing import Match
175176

176177
from opentelemetry.instrumentation.asgi import OpenTelemetryMiddleware
178+
from opentelemetry.instrumentation.asgi.types import (
179+
ClientRequestHook,
180+
ClientResponseHook,
181+
ServerRequestHook,
182+
)
177183
from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
178184
from opentelemetry.instrumentation.starlette.package import _instruments
179185
from opentelemetry.instrumentation.starlette.version import __version__
180186
from opentelemetry.metrics import get_meter
181187
from opentelemetry.semconv.trace import SpanAttributes
182-
from opentelemetry.trace import Span
183188
from opentelemetry.util.http import get_excluded_urls
184189

185190
_excluded_urls = get_excluded_urls("STARLETTE")
186191

187-
_ServerRequestHookT = typing.Optional[typing.Callable[[Span, dict], None]]
188-
_ClientRequestHookT = typing.Optional[typing.Callable[[Span, dict], None]]
189-
_ClientResponseHookT = typing.Optional[typing.Callable[[Span, dict], None]]
190-
191192

192193
class StarletteInstrumentor(BaseInstrumentor):
193194
"""An instrumentor for starlette
@@ -200,9 +201,9 @@ class StarletteInstrumentor(BaseInstrumentor):
200201
@staticmethod
201202
def instrument_app(
202203
app: applications.Starlette,
203-
server_request_hook: _ServerRequestHookT = None,
204-
client_request_hook: _ClientRequestHookT = None,
205-
client_response_hook: _ClientResponseHookT = None,
204+
server_request_hook: ServerRequestHook = None,
205+
client_request_hook: ClientRequestHook = None,
206+
client_response_hook: ClientResponseHook = None,
206207
meter_provider=None,
207208
tracer_provider=None,
208209
):
@@ -270,9 +271,9 @@ def _uninstrument(self, **kwargs):
270271
class _InstrumentedStarlette(applications.Starlette):
271272
_tracer_provider = None
272273
_meter_provider = None
273-
_server_request_hook: _ServerRequestHookT = None
274-
_client_request_hook: _ClientRequestHookT = None
275-
_client_response_hook: _ClientResponseHookT = None
274+
_server_request_hook: ServerRequestHook = None
275+
_client_request_hook: ClientRequestHook = None
276+
_client_response_hook: ClientResponseHook = None
276277
_instrumented_starlette_apps = set()
277278

278279
def __init__(self, *args, **kwargs):

0 commit comments

Comments
 (0)