Skip to content

Commit d329d55

Browse files
joshuahlangJoshua Lang
authored and
Joshua Lang
committed
ext-aiohttp-client implementation
This module is only supported on Python3.5, which is the oldest supported by aiohttp.
1 parent d7d9b15 commit d329d55

File tree

13 files changed

+743
-2
lines changed

13 files changed

+743
-2
lines changed

docs/conf.py

+1
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@
6565
"https://opentracing-python.readthedocs.io/en/latest/",
6666
None,
6767
),
68+
"aiohttp": ("https://aiohttp.readthedocs.io/en/stable/", None),
6869
}
6970

7071
# http://www.sphinx-doc.org/en/master/config.html#confval-nitpicky

docs/index.rst

+1
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ and integration packages.
3333
:maxdepth: 1
3434
:caption: OpenTelemetry Integrations:
3535

36+
opentelemetry.ext.aiohttp_client
3637
opentelemetry.ext.flask
3738
opentelemetry.ext.http_requests
3839
opentelemetry.ext.jaeger
+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
opentelemetry.ext.aiohttp_client package
2+
========================================
3+
4+
Module contents
5+
---------------
6+
7+
.. automodule:: opentelemetry.ext.aiohttp_client
8+
:members:
9+
:undoc-members:
10+
:show-inheritance:
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
OpenTelemetry aiohttp client Integration
2+
========================================
3+
4+
|pypi|
5+
6+
.. |pypi| image:: https://badge.fury.io/py/opentelemetry-ext-aiohttp-client.svg
7+
:target: https://pypi.org/project/opentelemetry-ext-aiohttp-client/
8+
9+
This library allows tracing HTTP requests made by the
10+
`aiohttp client<https://docs.aiohttp.org/en/stable/client.html>`_ library.
11+
12+
Installation
13+
------------
14+
15+
::
16+
17+
pip install opentelemetry-ext-aiohttp-client
18+
19+
Usage
20+
-----
21+
22+
.. code-block:: python
23+
24+
import aiohttp
25+
import asyncio
26+
27+
from opentelemetry.ext.aiohttp_client import create_trace_config
28+
29+
async def fetch(session, url):
30+
async with session.get(url) as response:
31+
return await response.text()
32+
33+
async def main():
34+
async with aiohttp.ClientSession(trace_configs=[create_trace_config()]) as session:
35+
html = await fetch(session, 'https://www.example.org')
36+
print(html)
37+
38+
if __name__ == '__main__':
39+
loop = asyncio.get_event_loop()
40+
loop.run_until_complete(main())
41+
42+
43+
References
44+
----------
45+
46+
* `OpenTelemetry Project <https://opentelemetry.io/>`_
47+
* `aiohttp client Tracing <https://docs.aiohttp.org/en/stable/tracing_reference.html>`_
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# Copyright 2020, 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+
[metadata]
16+
name = opentelemetry-ext-aiohttp-client
17+
description = OpenTelemetry aiohttp client integration
18+
long_description = file: README.rst
19+
long_description_content_type = text/x-rst
20+
author = OpenTelemetry Authors
21+
author_email = cncf-opentelemetry-contributors@lists.cncf.io
22+
url = https://github.com/open-telemetry/opentelemetry-python/ext/opentelemetry-ext-aiohttp-client
23+
platforms = any
24+
license = Apache-2.0
25+
classifiers =
26+
Development Status :: 3 - Alpha
27+
Intended Audience :: Developers
28+
License :: OSI Approved :: Apache Software License
29+
Programming Language :: Python
30+
Programming Language :: Python :: 3
31+
Programming Language :: Python :: 3.5
32+
Programming Language :: Python :: 3.6
33+
Programming Language :: Python :: 3.7
34+
35+
[options]
36+
python_requires = >=3.5.3
37+
package_dir=
38+
=src
39+
packages=find_namespace:
40+
install_requires =
41+
opentelemetry-api >= 0.5.dev0
42+
aiohttp ~= 3.0
43+
44+
[options.packages.find]
45+
where = src
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# Copyright 2020, 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+
import os
15+
16+
import setuptools
17+
18+
BASE_DIR = os.path.dirname(__file__)
19+
VERSION_FILENAME = os.path.join(
20+
BASE_DIR, "src", "opentelemetry", "ext", "aiohttp_client", "version.py"
21+
)
22+
PACKAGE_INFO = {}
23+
with open(VERSION_FILENAME) as f:
24+
exec(f.read(), PACKAGE_INFO)
25+
26+
setuptools.setup(version=PACKAGE_INFO["__version__"])
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
# Copyright 2020, 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+
"""
16+
The opentelemetry-ext-aiohttp-client package allows tracing HTTP requests
17+
made by the aiohttp client library.
18+
"""
19+
20+
import contextlib
21+
import socket
22+
import types
23+
import typing
24+
25+
import aiohttp
26+
27+
from opentelemetry import propagators, trace
28+
from opentelemetry.ext.aiohttp_client.version import __version__
29+
from opentelemetry.trace import SpanKind
30+
from opentelemetry.trace.status import Status, StatusCanonicalCode
31+
32+
33+
# TODO: refactor this code to some common utility
34+
def http_status_to_canonical_code(status: int) -> StatusCanonicalCode:
35+
# pylint:disable=too-many-branches,too-many-return-statements
36+
if status < 100:
37+
return StatusCanonicalCode.UNKNOWN
38+
if status <= 399:
39+
return StatusCanonicalCode.OK
40+
if status <= 499:
41+
if status == 401: # HTTPStatus.UNAUTHORIZED:
42+
return StatusCanonicalCode.UNAUTHENTICATED
43+
if status == 403: # HTTPStatus.FORBIDDEN:
44+
return StatusCanonicalCode.PERMISSION_DENIED
45+
if status == 404: # HTTPStatus.NOT_FOUND:
46+
return StatusCanonicalCode.NOT_FOUND
47+
if status == 429: # HTTPStatus.TOO_MANY_REQUESTS:
48+
return StatusCanonicalCode.RESOURCE_EXHAUSTED
49+
return StatusCanonicalCode.INVALID_ARGUMENT
50+
if status <= 599:
51+
if status == 501: # HTTPStatus.NOT_IMPLEMENTED:
52+
return StatusCanonicalCode.UNIMPLEMENTED
53+
if status == 503: # HTTPStatus.SERVICE_UNAVAILABLE:
54+
return StatusCanonicalCode.UNAVAILABLE
55+
if status == 504: # HTTPStatus.GATEWAY_TIMEOUT:
56+
return StatusCanonicalCode.DEADLINE_EXCEEDED
57+
return StatusCanonicalCode.INTERNAL
58+
return StatusCanonicalCode.UNKNOWN
59+
60+
61+
def url_path_span_name(params: aiohttp.TraceRequestStartParams) -> str:
62+
"""Extract a span name from the request URL path.
63+
64+
A simple callable to extract the path portion of the requested URL
65+
for use as the span name.
66+
67+
:param aiohttp.TraceRequestStartParams params: Parameters describing
68+
the traced request.
69+
70+
:return: The URL path.
71+
:rtype: str
72+
"""
73+
return params.url.path
74+
75+
76+
def create_trace_config(
77+
url_filter: typing.Optional[typing.Callable[[str], str]] = None,
78+
span_name: typing.Optional[
79+
typing.Union[
80+
typing.Callable[[aiohttp.TraceRequestStartParams], str], str
81+
]
82+
] = None,
83+
) -> aiohttp.TraceConfig:
84+
"""Create an aiohttp-compatible trace configuration.
85+
86+
One span is created for the entire HTTP request, including intial
87+
TCP/TLS setup if the connection doesn't exist.
88+
89+
By default the span name is set to the HTTP request method.
90+
91+
Example usage:
92+
93+
.. code-block:: python
94+
95+
import aiohttp
96+
from opentelemetry.ext.aiohttp_client import create_trace_config
97+
98+
async with aiohttp.ClientSession(trace_configs=[create_trace_config()]) as session:
99+
async with session.get(url) as response:
100+
await response.text()
101+
102+
103+
:param url_filter: A callback to process the requested URL prior to adding
104+
it as a span attribute. This can be useful to remove sensitive data
105+
such as API keys or user personal information.
106+
107+
:param str span_name: Override the default span name.
108+
109+
:return: An object suitable for use with :py:class:`aiohttp.ClientSession`.
110+
:rtype: :py:class:`aiohttp.TraceConfig`
111+
"""
112+
# `aiohttp.TraceRequestStartParams` resolves to `aiohttp.tracing.TraceRequestStartParams`
113+
# which doesn't exist in the aiottp intersphinx inventory.
114+
# Explicitly specify the type for the `span_name` param and rtype to work
115+
# around this issue.
116+
117+
tracer = trace.tracer_provider().get_tracer(__name__, __version__)
118+
119+
async def on_request_start(
120+
unused_session: aiohttp.ClientSession,
121+
trace_config_ctx: types.SimpleNamespace,
122+
params: aiohttp.TraceRequestStartParams,
123+
):
124+
http_method = params.method.upper()
125+
if trace_config_ctx.span_name is None:
126+
request_span_name = http_method
127+
elif callable(trace_config_ctx.span_name):
128+
request_span_name = str(trace_config_ctx.span_name(params))
129+
else:
130+
request_span_name = str(trace_config_ctx.span_name)
131+
132+
trace_config_ctx.span = trace_config_ctx.tracer.start_span(
133+
request_span_name,
134+
kind=SpanKind.CLIENT,
135+
attributes={
136+
"component": "http",
137+
"http.method": http_method,
138+
"http.url": trace_config_ctx.url_filter(params.url)
139+
if callable(trace_config_ctx.url_filter)
140+
else str(params.url),
141+
},
142+
)
143+
144+
# Set the span as active via the `Tracer.use_span` context.
145+
# TODO: would be nice to have an explicit API to set a context as active.
146+
span_manager = contextlib.ExitStack()
147+
span_manager.enter_context(
148+
trace_config_ctx.tracer.use_span(
149+
trace_config_ctx.span, end_on_exit=True
150+
)
151+
)
152+
trace_config_ctx.span_manager = span_manager
153+
154+
propagators.inject(
155+
tracer, type(params.headers).__setitem__, params.headers
156+
)
157+
158+
async def on_request_end(
159+
unused_session: aiohttp.ClientSession,
160+
trace_config_ctx: types.SimpleNamespace,
161+
params: aiohttp.TraceRequestEndParams,
162+
):
163+
trace_config_ctx.span.set_status(
164+
Status(http_status_to_canonical_code(int(params.response.status)))
165+
)
166+
trace_config_ctx.span.set_attribute(
167+
"http.status_code", params.response.status
168+
)
169+
trace_config_ctx.span.set_attribute(
170+
"http.status_text", params.response.reason
171+
)
172+
trace_config_ctx.span_manager.close()
173+
174+
async def on_request_exception(
175+
unused_session: aiohttp.ClientSession,
176+
trace_config_ctx: types.SimpleNamespace,
177+
params: aiohttp.TraceRequestExceptionParams,
178+
):
179+
if isinstance(
180+
params.exception,
181+
(aiohttp.ServerTimeoutError, aiohttp.TooManyRedirects),
182+
):
183+
status = StatusCanonicalCode.DEADLINE_EXCEEDED
184+
# Assume any getaddrinfo error is a DNS failure.
185+
elif isinstance(
186+
params.exception, aiohttp.ClientConnectorError
187+
) and isinstance(params.exception.os_error, socket.gaierror):
188+
# DNS resolution failed
189+
status = StatusCanonicalCode.UNKNOWN
190+
else:
191+
status = StatusCanonicalCode.UNAVAILABLE
192+
193+
trace_config_ctx.span.set_status(Status(status))
194+
trace_config_ctx.span_manager.close()
195+
196+
def _trace_config_ctx_factory(**kwargs):
197+
if kwargs.get("trace_request_ctx", None) is None:
198+
kwargs["trace_request_ctx"] = {}
199+
return types.SimpleNamespace(
200+
span_name=span_name, tracer=tracer, url_filter=url_filter, **kwargs
201+
)
202+
203+
trace_config = aiohttp.TraceConfig(
204+
trace_config_ctx_factory=_trace_config_ctx_factory
205+
)
206+
207+
trace_config.on_request_start.append(on_request_start)
208+
trace_config.on_request_end.append(on_request_end)
209+
trace_config.on_request_exception.append(on_request_exception)
210+
211+
return trace_config
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# Copyright 2020, 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+
__version__ = "0.5.dev0"

ext/opentelemetry-ext-aiohttp-client/tests/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)