Skip to content

Commit 0384670

Browse files
committed
ext-aiohttp-client implementation
This module is only supported on Python3.5, which is the oldest supported by aiohttp.
1 parent a756492 commit 0384670

File tree

13 files changed

+668
-2
lines changed

13 files changed

+668
-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,207 @@
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 types
22+
import typing
23+
24+
import aiohttp
25+
26+
from opentelemetry import propagators, trace
27+
from opentelemetry.ext.aiohttp_client.version import __version__
28+
from opentelemetry.trace import SpanKind
29+
from opentelemetry.trace.status import Status, StatusCanonicalCode
30+
31+
32+
# TODO: refactor this code to some common utility
33+
def http_status_to_canonical_code(status: int, allow_redirect: bool = True):
34+
# pylint:disable=too-many-branches,too-many-return-statements
35+
if status < 100:
36+
return StatusCanonicalCode.UNKNOWN
37+
if status <= 299:
38+
return StatusCanonicalCode.OK
39+
if status <= 399:
40+
if allow_redirect:
41+
return StatusCanonicalCode.OK
42+
return StatusCanonicalCode.DEADLINE_EXCEEDED
43+
if status <= 499:
44+
if status == 401: # HTTPStatus.UNAUTHORIZED:
45+
return StatusCanonicalCode.UNAUTHENTICATED
46+
if status == 403: # HTTPStatus.FORBIDDEN:
47+
return StatusCanonicalCode.PERMISSION_DENIED
48+
if status == 404: # HTTPStatus.NOT_FOUND:
49+
return StatusCanonicalCode.NOT_FOUND
50+
if status == 429: # HTTPStatus.TOO_MANY_REQUESTS:
51+
return StatusCanonicalCode.RESOURCE_EXHAUSTED
52+
return StatusCanonicalCode.INVALID_ARGUMENT
53+
if status <= 599:
54+
if status == 501: # HTTPStatus.NOT_IMPLEMENTED:
55+
return StatusCanonicalCode.UNIMPLEMENTED
56+
if status == 503: # HTTPStatus.SERVICE_UNAVAILABLE:
57+
return StatusCanonicalCode.UNAVAILABLE
58+
if status == 504: # HTTPStatus.GATEWAY_TIMEOUT:
59+
return StatusCanonicalCode.DEADLINE_EXCEEDED
60+
return StatusCanonicalCode.INTERNAL
61+
return StatusCanonicalCode.UNKNOWN
62+
63+
64+
def url_path_span_name(params: aiohttp.TraceRequestStartParams):
65+
return params.url.path
66+
67+
68+
def create_trace_config(
69+
url_filter: typing.Optional[typing.Callable[[str], str]] = None,
70+
span_name: typing.Optional[
71+
typing.Union[
72+
typing.Callable[[aiohttp.TraceRequestStartParams], str], str
73+
]
74+
] = None,
75+
) -> aiohttp.TraceConfig:
76+
"""Create an aiohttp-compatible trace configuration.
77+
78+
One span is created for the entire HTTP request, including intial
79+
TCP/TLS setup if the connection doesn't exist.
80+
81+
By default the span name is set to the HTTP request method.
82+
83+
Example usage:
84+
85+
.. code-block:: python
86+
87+
import aiohttp
88+
from opentelemetry.ext.aiohttp_client import create_trace_config
89+
90+
async with aiohttp.ClientSession(trace_configs=[create_trace_config()]) as session:
91+
async with session.get(url) as response:
92+
await response.text()
93+
94+
95+
:param url_filter: A callback to process the requested URL prior to adding
96+
it as a span attribute. This can be useful to remove sensitive data
97+
such as API keys or user personal information.
98+
99+
:param str span_name: Override the default span name.
100+
101+
:return: An object suitable for use with :py:class:`aiohttp.ClientSession`.
102+
:rtype: :py:class:`aiohttp.TraceConfig`
103+
"""
104+
# `aiohttp.TraceRequestStartParams` resolves to `aiohttp.tracing.TraceRequestStartParams`
105+
# which doesn't exist in the aiottp intersphinx inventory.
106+
# Explicitly specify the type for the `span_name` param and rtype to work
107+
# around this issue.
108+
109+
tracer = trace.tracer_provider().get_tracer(__name__, __version__)
110+
111+
async def on_request_start(
112+
unused_session: aiohttp.ClientSession,
113+
trace_config_ctx: types.SimpleNamespace,
114+
params: aiohttp.TraceRequestStartParams,
115+
):
116+
http_method = params.method.upper()
117+
if trace_config_ctx.span_name is None:
118+
request_span_name = http_method
119+
elif callable(trace_config_ctx.span_name):
120+
request_span_name = trace_config_ctx.span_name(params)
121+
else:
122+
request_span_name = str(trace_config_ctx.span_name)
123+
124+
trace_config_ctx.span = trace_config_ctx.tracer.start_span(
125+
request_span_name,
126+
kind=SpanKind.CLIENT,
127+
attributes={
128+
"component": "http",
129+
"http.method": http_method,
130+
"http.url": trace_config_ctx.url_filter(params.url)
131+
if callable(trace_config_ctx.url_filter)
132+
else str(params.url),
133+
},
134+
)
135+
136+
# Set the span as active via the `Tracer.use_span` context.
137+
# TODO: would be nice to have an explicit API to set a context as active.
138+
span_manager = contextlib.ExitStack()
139+
span_manager.enter_context(
140+
trace_config_ctx.tracer.use_span(
141+
trace_config_ctx.span, end_on_exit=True
142+
)
143+
)
144+
trace_config_ctx.span_manager = span_manager
145+
146+
propagators.inject(
147+
tracer, type(params.headers).__setitem__, params.headers
148+
)
149+
150+
async def on_request_end(
151+
unused_session: aiohttp.ClientSession,
152+
trace_config_ctx: types.SimpleNamespace,
153+
params: aiohttp.TraceRequestEndParams,
154+
):
155+
trace_config_ctx.span.set_status(
156+
# Assume redirects were allowed if any occurred (i.e. there is a history).
157+
# If redirects weren't allowed, there shouldn't be any history of them.
158+
Status(
159+
http_status_to_canonical_code(
160+
int(params.response.status), bool(params.response.history)
161+
)
162+
)
163+
)
164+
trace_config_ctx.span.set_attribute(
165+
"http.status_code", params.response.status
166+
)
167+
trace_config_ctx.span.set_attribute(
168+
"http.status_text", params.response.reason
169+
)
170+
trace_config_ctx.span_manager.close()
171+
172+
async def on_request_exception(
173+
unused_session: aiohttp.ClientSession,
174+
trace_config_ctx: types.SimpleNamespace,
175+
params: aiohttp.TraceRequestExceptionParams,
176+
):
177+
if isinstance(params.exception, aiohttp.ServerTimeoutError):
178+
status = StatusCanonicalCode.DEADLINE_EXCEEDED
179+
elif (
180+
# TODO: does this work on windows?
181+
isinstance(params.exception, aiohttp.ClientConnectorError)
182+
and params.exception.errno == -2
183+
):
184+
# DNS resolution failed
185+
status = StatusCanonicalCode.UNKNOWN
186+
else:
187+
status = StatusCanonicalCode.UNAVAILABLE
188+
189+
trace_config_ctx.span.set_status(Status(status))
190+
trace_config_ctx.span_manager.close()
191+
192+
def _trace_config_ctx_factory(**kwargs):
193+
if kwargs.get("trace_request_ctx", None) is None:
194+
kwargs["trace_request_ctx"] = {}
195+
return types.SimpleNamespace(
196+
span_name=span_name, tracer=tracer, url_filter=url_filter, **kwargs
197+
)
198+
199+
trace_config = aiohttp.TraceConfig(
200+
trace_config_ctx_factory=_trace_config_ctx_factory
201+
)
202+
203+
trace_config.on_request_start.append(on_request_start)
204+
trace_config.on_request_end.append(on_request_end)
205+
trace_config.on_request_exception.append(on_request_exception)
206+
207+
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)