Skip to content

Commit c5c6977

Browse files
author
Ryo Kather
authored
Add hooks for aiohttp, asgi, starlette, fastAPI, urllib, urllib3 (#576)
1 parent 1157eb2 commit c5c6977

File tree

15 files changed

+556
-222
lines changed

15 files changed

+556
-222
lines changed

Diff for: CHANGELOG.md

+5
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

88
## [Unreleased](https://github.com/open-telemetry/opentelemetry-python/compare/v1.4.0-0.23b0...HEAD)
9+
10+
### Added
911
- `opentelemetry-sdk-extension-aws` Add AWS resource detectors to extension package
1012
([#586](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/586))
13+
- `opentelemetry-instrumentation-asgi`, `opentelemetry-instrumentation-aiohttp-client`, `openetelemetry-instrumentation-fastapi`,
14+
`opentelemetry-instrumentation-starlette`, `opentelemetry-instrumentation-urllib`, `opentelemetry-instrumentation-urllib3` Added `request_hook` and `response_hook` callbacks
15+
([#576](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/576))
1116

1217
## [1.4.0-0.23b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.4.0-0.23b0) - 2021-07-21
1318

Diff for: instrumentation/opentelemetry-instrumentation-aiohttp-client/src/opentelemetry/instrumentation/aiohttp_client/__init__.py

+40-18
Original file line numberDiff line numberDiff line change
@@ -81,13 +81,25 @@ def strip_query_params(url: yarl.URL) -> str:
8181
)
8282
from opentelemetry.propagate import inject
8383
from opentelemetry.semconv.trace import SpanAttributes
84-
from opentelemetry.trace import SpanKind, TracerProvider, get_tracer
84+
from opentelemetry.trace import Span, SpanKind, TracerProvider, get_tracer
8585
from opentelemetry.trace.status import Status, StatusCode
8686
from opentelemetry.util.http import remove_url_credentials
8787

8888
_UrlFilterT = typing.Optional[typing.Callable[[str], str]]
89-
_SpanNameT = typing.Optional[
90-
typing.Union[typing.Callable[[aiohttp.TraceRequestStartParams], str], str]
89+
_RequestHookT = typing.Optional[
90+
typing.Callable[[Span, aiohttp.TraceRequestStartParams], None]
91+
]
92+
_ResponseHookT = typing.Optional[
93+
typing.Callable[
94+
[
95+
Span,
96+
typing.Union[
97+
aiohttp.TraceRequestEndParams,
98+
aiohttp.TraceRequestExceptionParams,
99+
],
100+
],
101+
None,
102+
]
91103
]
92104

93105

@@ -108,7 +120,8 @@ def url_path_span_name(params: aiohttp.TraceRequestStartParams) -> str:
108120

109121
def create_trace_config(
110122
url_filter: _UrlFilterT = None,
111-
span_name: _SpanNameT = None,
123+
request_hook: _RequestHookT = None,
124+
response_hook: _ResponseHookT = None,
112125
tracer_provider: TracerProvider = None,
113126
) -> aiohttp.TraceConfig:
114127
"""Create an aiohttp-compatible trace configuration.
@@ -134,15 +147,16 @@ def create_trace_config(
134147
it as a span attribute. This can be useful to remove sensitive data
135148
such as API keys or user personal information.
136149
137-
:param str span_name: Override the default span name.
150+
:param Callable request_hook: Optional callback that can modify span name and request params.
151+
:param Callable response_hook: Optional callback that can modify span name and response params.
138152
:param tracer_provider: optional TracerProvider from which to get a Tracer
139153
140154
:return: An object suitable for use with :py:class:`aiohttp.ClientSession`.
141155
:rtype: :py:class:`aiohttp.TraceConfig`
142156
"""
143157
# `aiohttp.TraceRequestStartParams` resolves to `aiohttp.tracing.TraceRequestStartParams`
144-
# which doesn't exist in the aiottp intersphinx inventory.
145-
# Explicitly specify the type for the `span_name` param and rtype to work
158+
# which doesn't exist in the aiohttp intersphinx inventory.
159+
# Explicitly specify the type for the `request_hook` and `response_hook` param and rtype to work
146160
# around this issue.
147161

148162
tracer = get_tracer(__name__, __version__, tracer_provider)
@@ -161,17 +175,15 @@ async def on_request_start(
161175
return
162176

163177
http_method = params.method.upper()
164-
if trace_config_ctx.span_name is None:
165-
request_span_name = "HTTP {}".format(http_method)
166-
elif callable(trace_config_ctx.span_name):
167-
request_span_name = str(trace_config_ctx.span_name(params))
168-
else:
169-
request_span_name = str(trace_config_ctx.span_name)
178+
request_span_name = "HTTP {}".format(http_method)
170179

171180
trace_config_ctx.span = trace_config_ctx.tracer.start_span(
172181
request_span_name, kind=SpanKind.CLIENT,
173182
)
174183

184+
if callable(request_hook):
185+
request_hook(trace_config_ctx.span, params)
186+
175187
if trace_config_ctx.span.is_recording():
176188
attributes = {
177189
SpanAttributes.HTTP_METHOD: http_method,
@@ -198,6 +210,9 @@ async def on_request_end(
198210
if trace_config_ctx.span is None:
199211
return
200212

213+
if callable(response_hook):
214+
response_hook(trace_config_ctx.span, params)
215+
201216
if trace_config_ctx.span.is_recording():
202217
trace_config_ctx.span.set_status(
203218
Status(http_status_to_status_code(int(params.response.status)))
@@ -215,6 +230,9 @@ async def on_request_exception(
215230
if trace_config_ctx.span is None:
216231
return
217232

233+
if callable(response_hook):
234+
response_hook(trace_config_ctx.span, params)
235+
218236
if trace_config_ctx.span.is_recording() and params.exception:
219237
trace_config_ctx.span.set_status(Status(StatusCode.ERROR))
220238
trace_config_ctx.span.record_exception(params.exception)
@@ -223,7 +241,7 @@ async def on_request_exception(
223241
def _trace_config_ctx_factory(**kwargs):
224242
kwargs.setdefault("trace_request_ctx", {})
225243
return types.SimpleNamespace(
226-
span_name=span_name, tracer=tracer, url_filter=url_filter, **kwargs
244+
tracer=tracer, url_filter=url_filter, **kwargs
227245
)
228246

229247
trace_config = aiohttp.TraceConfig(
@@ -240,7 +258,8 @@ def _trace_config_ctx_factory(**kwargs):
240258
def _instrument(
241259
tracer_provider: TracerProvider = None,
242260
url_filter: _UrlFilterT = None,
243-
span_name: _SpanNameT = None,
261+
request_hook: _RequestHookT = None,
262+
response_hook: _ResponseHookT = None,
244263
):
245264
"""Enables tracing of all ClientSessions
246265
@@ -256,7 +275,8 @@ def instrumented_init(wrapped, instance, args, kwargs):
256275

257276
trace_config = create_trace_config(
258277
url_filter=url_filter,
259-
span_name=span_name,
278+
request_hook=request_hook,
279+
response_hook=response_hook,
260280
tracer_provider=tracer_provider,
261281
)
262282
trace_config._is_instrumented_by_opentelemetry = True
@@ -304,12 +324,14 @@ def _instrument(self, **kwargs):
304324
``url_filter``: A callback to process the requested URL prior to adding
305325
it as a span attribute. This can be useful to remove sensitive data
306326
such as API keys or user personal information.
307-
``span_name``: Override the default span name.
327+
``request_hook``: An optional callback that is invoked right after a span is created.
328+
``response_hook``: An optional callback which is invoked right before the span is finished processing a response.
308329
"""
309330
_instrument(
310331
tracer_provider=kwargs.get("tracer_provider"),
311332
url_filter=kwargs.get("url_filter"),
312-
span_name=kwargs.get("span_name"),
333+
request_hook=kwargs.get("request_hook"),
334+
response_hook=kwargs.get("response_hook"),
313335
)
314336

315337
def _uninstrument(self, **kwargs):

Diff for: instrumentation/opentelemetry-instrumentation-aiohttp-client/tests/test_aiohttp_client_integration.py

+61-43
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
from opentelemetry.instrumentation.utils import _SUPPRESS_INSTRUMENTATION_KEY
3434
from opentelemetry.semconv.trace import SpanAttributes
3535
from opentelemetry.test.test_base import TestBase
36-
from opentelemetry.trace import StatusCode
36+
from opentelemetry.trace import Span, StatusCode
3737

3838

3939
def run_with_test_server(
@@ -161,46 +161,51 @@ def test_not_recording(self):
161161
self.assertFalse(mock_span.set_attribute.called)
162162
self.assertFalse(mock_span.set_status.called)
163163

164-
def test_span_name_option(self):
165-
for span_name, method, path, expected in (
166-
("static", "POST", "/static-span-name", "static"),
167-
(
168-
lambda params: "{} - {}".format(
169-
params.method, params.url.path
170-
),
171-
"PATCH",
172-
"/some/path",
173-
"PATCH - /some/path",
174-
),
164+
def test_hooks(self):
165+
method = "PATCH"
166+
path = "/some/path"
167+
expected = "PATCH - /some/path"
168+
169+
def request_hook(span: Span, params: aiohttp.TraceRequestStartParams):
170+
span.update_name("{} - {}".format(params.method, params.url.path))
171+
172+
def response_hook(
173+
span: Span,
174+
params: typing.Union[
175+
aiohttp.TraceRequestEndParams,
176+
aiohttp.TraceRequestExceptionParams,
177+
],
175178
):
176-
with self.subTest(span_name=span_name, method=method, path=path):
177-
host, port = self._http_request(
178-
trace_config=aiohttp_client.create_trace_config(
179-
span_name=span_name
180-
),
181-
method=method,
182-
url=path,
183-
status_code=HTTPStatus.OK,
184-
)
179+
span.set_attribute("response_hook_attr", "value")
185180

186-
self.assert_spans(
187-
[
188-
(
189-
expected,
190-
(StatusCode.UNSET, None),
191-
{
192-
SpanAttributes.HTTP_METHOD: method,
193-
SpanAttributes.HTTP_URL: "http://{}:{}{}".format(
194-
host, port, path
195-
),
196-
SpanAttributes.HTTP_STATUS_CODE: int(
197-
HTTPStatus.OK
198-
),
199-
},
200-
)
201-
]
202-
)
203-
self.memory_exporter.clear()
181+
host, port = self._http_request(
182+
trace_config=aiohttp_client.create_trace_config(
183+
request_hook=request_hook, response_hook=response_hook,
184+
),
185+
method=method,
186+
url=path,
187+
status_code=HTTPStatus.OK,
188+
)
189+
190+
for span in self.memory_exporter.get_finished_spans():
191+
self.assertEqual(span.name, expected)
192+
self.assertEqual(
193+
(span.status.status_code, span.status.description),
194+
(StatusCode.UNSET, None),
195+
)
196+
self.assertEqual(
197+
span.attributes[SpanAttributes.HTTP_METHOD], method
198+
)
199+
self.assertEqual(
200+
span.attributes[SpanAttributes.HTTP_URL],
201+
"http://{}:{}{}".format(host, port, path),
202+
)
203+
self.assertEqual(
204+
span.attributes[SpanAttributes.HTTP_STATUS_CODE], HTTPStatus.OK
205+
)
206+
self.assertIn("response_hook_attr", span.attributes)
207+
self.assertEqual(span.attributes["response_hook_attr"], "value")
208+
self.memory_exporter.clear()
204209

205210
def test_url_filter_option(self):
206211
# Strips all query params from URL before adding as a span attribute.
@@ -501,19 +506,32 @@ def strip_query_params(url: yarl.URL) -> str:
501506
span.attributes[SpanAttributes.HTTP_URL],
502507
)
503508

504-
def test_span_name(self):
505-
def span_name_callback(params: aiohttp.TraceRequestStartParams) -> str:
506-
return "{} - {}".format(params.method, params.url.path)
509+
def test_hooks(self):
510+
def request_hook(span: Span, params: aiohttp.TraceRequestStartParams):
511+
span.update_name("{} - {}".format(params.method, params.url.path))
512+
513+
def response_hook(
514+
span: Span,
515+
params: typing.Union[
516+
aiohttp.TraceRequestEndParams,
517+
aiohttp.TraceRequestExceptionParams,
518+
],
519+
):
520+
span.set_attribute("response_hook_attr", "value")
507521

508522
AioHttpClientInstrumentor().uninstrument()
509-
AioHttpClientInstrumentor().instrument(span_name=span_name_callback)
523+
AioHttpClientInstrumentor().instrument(
524+
request_hook=request_hook, response_hook=response_hook
525+
)
510526

511527
url = "/test-path"
512528
run_with_test_server(
513529
self.get_default_request(url), url, self.default_handler
514530
)
515531
span = self.assert_spans(1)
516532
self.assertEqual("GET - /test-path", span.name)
533+
self.assertIn("response_hook_attr", span.attributes)
534+
self.assertEqual(span.attributes["response_hook_attr"], "value")
517535

518536

519537
class TestLoadingAioHttpInstrumentor(unittest.TestCase):

0 commit comments

Comments
 (0)