Skip to content

Commit c7cc4d9

Browse files
authored
Add instrumentor and auto instrumentation support for aiohttp (open-telemetry#1075)
1 parent affe911 commit c7cc4d9

File tree

4 files changed

+378
-70
lines changed

4 files changed

+378
-70
lines changed

instrumentation/opentelemetry-instrumentation-aiohttp-client/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ Released 2020-09-17
88

99
- Updating span name to match semantic conventions
1010
([#972](https://github.com/open-telemetry/opentelemetry-python/pull/972))
11+
- Add instrumentor and auto instrumentation support for aiohttp
12+
([#1075](https://github.com/open-telemetry/opentelemetry-python/pull/1075))
1113

1214
## Version 0.12b0
1315

instrumentation/opentelemetry-instrumentation-aiohttp-client/setup.cfg

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,12 +39,17 @@ package_dir=
3939
=src
4040
packages=find_namespace:
4141
install_requires =
42-
opentelemetry-api >= 0.12.dev0
42+
opentelemetry-api == 0.14.dev0
4343
opentelemetry-instrumentation == 0.14.dev0
4444
aiohttp ~= 3.0
45+
wrapt >= 1.0.0, < 2.0.0
4546

4647
[options.packages.find]
4748
where = src
4849

4950
[options.extras_require]
5051
test =
52+
53+
[options.entry_points]
54+
opentelemetry_instrumentor =
55+
aiohttp-client = opentelemetry.instrumentation.aiohttp_client:AioHttpClientInstrumentor

instrumentation/opentelemetry-instrumentation-aiohttp-client/src/opentelemetry/instrumentation/aiohttp_client/__init__.py

Lines changed: 146 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -18,44 +18,73 @@
1818
1919
Usage
2020
-----
21+
Explicitly instrumenting a single client session:
2122
22-
.. code:: python
23+
.. code:: python
2324
24-
import aiohttp
25-
from opentelemetry.instrumentation.aiohttp_client import (
26-
create_trace_config,
27-
url_path_span_name
28-
)
29-
import yarl
25+
import aiohttp
26+
from opentelemetry.instrumentation.aiohttp_client import (
27+
create_trace_config,
28+
url_path_span_name
29+
)
30+
import yarl
3031
31-
def strip_query_params(url: yarl.URL) -> str:
32-
return str(url.with_query(None))
32+
def strip_query_params(url: yarl.URL) -> str:
33+
return str(url.with_query(None))
3334
34-
async with aiohttp.ClientSession(trace_configs=[create_trace_config(
35-
# Remove all query params from the URL attribute on the span.
36-
url_filter=strip_query_params,
37-
# Use the URL's path as the span name.
38-
span_name=url_path_span_name
39-
)]) as session:
40-
async with session.get(url) as response:
41-
await response.text()
35+
async with aiohttp.ClientSession(trace_configs=[create_trace_config(
36+
# Remove all query params from the URL attribute on the span.
37+
url_filter=strip_query_params,
38+
# Use the URL's path as the span name.
39+
span_name=url_path_span_name
40+
)]) as session:
41+
async with session.get(url) as response:
42+
await response.text()
43+
44+
Instrumenting all client sessions:
45+
46+
.. code:: python
47+
48+
import aiohttp
49+
from opentelemetry.instrumentation.aiohttp_client import (
50+
AioHttpClientInstrumentor
51+
)
4252
53+
# Enable instrumentation
54+
AioHttpClientInstrumentor().instrument()
55+
56+
# Create a session and make an HTTP get request
57+
async with aiohttp.ClientSession() as session:
58+
async with session.get(url) as response:
59+
await response.text()
60+
61+
API
62+
---
4363
"""
4464

45-
import contextlib
4665
import socket
4766
import types
4867
import typing
4968

5069
import aiohttp
70+
import wrapt
5171

5272
from opentelemetry import context as context_api
5373
from opentelemetry import propagators, trace
5474
from opentelemetry.instrumentation.aiohttp_client.version import __version__
55-
from opentelemetry.instrumentation.utils import http_status_to_canonical_code
56-
from opentelemetry.trace import SpanKind
75+
from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
76+
from opentelemetry.instrumentation.utils import (
77+
http_status_to_canonical_code,
78+
unwrap,
79+
)
80+
from opentelemetry.trace import SpanKind, TracerProvider, get_tracer
5781
from opentelemetry.trace.status import Status, StatusCanonicalCode
5882

83+
_UrlFilterT = typing.Optional[typing.Callable[[str], str]]
84+
_SpanNameT = typing.Optional[
85+
typing.Union[typing.Callable[[aiohttp.TraceRequestStartParams], str], str]
86+
]
87+
5988

6089
def url_path_span_name(params: aiohttp.TraceRequestStartParams) -> str:
6190
"""Extract a span name from the request URL path.
@@ -73,12 +102,9 @@ def url_path_span_name(params: aiohttp.TraceRequestStartParams) -> str:
73102

74103

75104
def create_trace_config(
76-
url_filter: typing.Optional[typing.Callable[[str], str]] = None,
77-
span_name: typing.Optional[
78-
typing.Union[
79-
typing.Callable[[aiohttp.TraceRequestStartParams], str], str
80-
]
81-
] = None,
105+
url_filter: _UrlFilterT = None,
106+
span_name: _SpanNameT = None,
107+
tracer_provider: TracerProvider = None,
82108
) -> aiohttp.TraceConfig:
83109
"""Create an aiohttp-compatible trace configuration.
84110
@@ -104,6 +130,7 @@ def create_trace_config(
104130
such as API keys or user personal information.
105131
106132
:param str span_name: Override the default span name.
133+
:param tracer_provider: optional TracerProvider from which to get a Tracer
107134
108135
:return: An object suitable for use with :py:class:`aiohttp.ClientSession`.
109136
:rtype: :py:class:`aiohttp.TraceConfig`
@@ -113,7 +140,7 @@ def create_trace_config(
113140
# Explicitly specify the type for the `span_name` param and rtype to work
114141
# around this issue.
115142

116-
tracer = trace.get_tracer_provider().get_tracer(__name__, __version__)
143+
tracer = get_tracer(__name__, __version__, tracer_provider)
117144

118145
def _end_trace(trace_config_ctx: types.SimpleNamespace):
119146
context_api.detach(trace_config_ctx.token)
@@ -124,6 +151,10 @@ async def on_request_start(
124151
trace_config_ctx: types.SimpleNamespace,
125152
params: aiohttp.TraceRequestStartParams,
126153
):
154+
if context_api.get_value("suppress_instrumentation"):
155+
trace_config_ctx.span = None
156+
return
157+
127158
http_method = params.method.upper()
128159
if trace_config_ctx.span_name is None:
129160
request_span_name = "HTTP {}".format(http_method)
@@ -158,6 +189,9 @@ async def on_request_end(
158189
trace_config_ctx: types.SimpleNamespace,
159190
params: aiohttp.TraceRequestEndParams,
160191
):
192+
if trace_config_ctx.span is None:
193+
return
194+
161195
if trace_config_ctx.span.is_recording():
162196
trace_config_ctx.span.set_status(
163197
Status(
@@ -177,6 +211,9 @@ async def on_request_exception(
177211
trace_config_ctx: types.SimpleNamespace,
178212
params: aiohttp.TraceRequestExceptionParams,
179213
):
214+
if trace_config_ctx.span is None:
215+
return
216+
180217
if trace_config_ctx.span.is_recording():
181218
if isinstance(
182219
params.exception,
@@ -193,6 +230,7 @@ async def on_request_exception(
193230
status = StatusCanonicalCode.UNAVAILABLE
194231

195232
trace_config_ctx.span.set_status(Status(status))
233+
trace_config_ctx.span.record_exception(params.exception)
196234
_end_trace(trace_config_ctx)
197235

198236
def _trace_config_ctx_factory(**kwargs):
@@ -210,3 +248,84 @@ def _trace_config_ctx_factory(**kwargs):
210248
trace_config.on_request_exception.append(on_request_exception)
211249

212250
return trace_config
251+
252+
253+
def _instrument(
254+
tracer_provider: TracerProvider = None,
255+
url_filter: _UrlFilterT = None,
256+
span_name: _SpanNameT = None,
257+
):
258+
"""Enables tracing of all ClientSessions
259+
260+
When a ClientSession gets created a TraceConfig is automatically added to
261+
the session's trace_configs.
262+
"""
263+
# pylint:disable=unused-argument
264+
def instrumented_init(wrapped, instance, args, kwargs):
265+
if context_api.get_value("suppress_instrumentation"):
266+
return wrapped(*args, **kwargs)
267+
268+
trace_configs = list(kwargs.get("trace_configs") or ())
269+
270+
trace_config = create_trace_config(
271+
url_filter=url_filter,
272+
span_name=span_name,
273+
tracer_provider=tracer_provider,
274+
)
275+
trace_config.opentelemetry_aiohttp_instrumented = True
276+
trace_configs.append(trace_config)
277+
278+
kwargs["trace_configs"] = trace_configs
279+
return wrapped(*args, **kwargs)
280+
281+
wrapt.wrap_function_wrapper(
282+
aiohttp.ClientSession, "__init__", instrumented_init
283+
)
284+
285+
286+
def _uninstrument():
287+
"""Disables instrumenting for all newly created ClientSessions"""
288+
unwrap(aiohttp.ClientSession, "__init__")
289+
290+
291+
def _uninstrument_session(client_session: aiohttp.ClientSession):
292+
"""Disables instrumentation for the given ClientSession"""
293+
# pylint: disable=protected-access
294+
trace_configs = client_session._trace_configs
295+
client_session._trace_configs = [
296+
trace_config
297+
for trace_config in trace_configs
298+
if not hasattr(trace_config, "opentelemetry_aiohttp_instrumented")
299+
]
300+
301+
302+
class AioHttpClientInstrumentor(BaseInstrumentor):
303+
"""An instrumentor for aiohttp client sessions
304+
305+
See `BaseInstrumentor`
306+
"""
307+
308+
def _instrument(self, **kwargs):
309+
"""Instruments aiohttp ClientSession
310+
311+
Args:
312+
**kwargs: Optional arguments
313+
``tracer_provider``: a TracerProvider, defaults to global
314+
``url_filter``: A callback to process the requested URL prior to adding
315+
it as a span attribute. This can be useful to remove sensitive data
316+
such as API keys or user personal information.
317+
``span_name``: Override the default span name.
318+
"""
319+
_instrument(
320+
tracer_provider=kwargs.get("tracer_provider"),
321+
url_filter=kwargs.get("url_filter"),
322+
span_name=kwargs.get("span_name"),
323+
)
324+
325+
def _uninstrument(self, **kwargs):
326+
_uninstrument()
327+
328+
@staticmethod
329+
def uninstrument_session(client_session: aiohttp.ClientSession):
330+
"""Disables instrumentation for the given session"""
331+
_uninstrument_session(client_session)

0 commit comments

Comments
 (0)