Skip to content

Commit 311ba67

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 7b1d866 commit 311ba67

File tree

12 files changed

+738
-2
lines changed

12 files changed

+738
-2
lines changed

docs/conf.py

+1
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@
6767
"https://opentracing-python.readthedocs.io/en/latest/",
6868
None,
6969
),
70+
"aiohttp": ("https://aiohttp.readthedocs.io/en/stable/", None),
7071
}
7172

7273
# http://www.sphinx-doc.org/en/master/config.html#confval-nitpicky
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
OpenTelemetry aiohttp client Integration
2+
========================================
3+
4+
.. automodule:: opentelemetry.ext.aiohttp_client
5+
:members:
6+
:undoc-members:
7+
:show-inheritance:
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
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+
20+
References
21+
----------
22+
23+
* `OpenTelemetry Project <https://opentelemetry.io/>`_
24+
* `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,236 @@
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+
Example usage:
20+
21+
.. code-block:: python
22+
23+
import aiohttp
24+
from opentelemetry.ext.aiohttp_client import (
25+
create_trace_config,
26+
url_path_span_name
27+
)
28+
import yarl
29+
30+
31+
def strip_query_params(url: yarl.URL) -> str:
32+
return str(url.with_query(None))
33+
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()
42+
43+
"""
44+
45+
import contextlib
46+
import socket
47+
import types
48+
import typing
49+
50+
import aiohttp
51+
52+
from opentelemetry import propagators, trace
53+
from opentelemetry.ext.aiohttp_client.version import __version__
54+
from opentelemetry.trace import SpanKind
55+
from opentelemetry.trace.status import Status, StatusCanonicalCode
56+
57+
58+
# TODO: refactor this code to some common utility
59+
def http_status_to_canonical_code(status: int) -> StatusCanonicalCode:
60+
# pylint:disable=too-many-branches,too-many-return-statements
61+
if status < 100:
62+
return StatusCanonicalCode.UNKNOWN
63+
if status <= 399:
64+
return StatusCanonicalCode.OK
65+
if status <= 499:
66+
if status == 401: # HTTPStatus.UNAUTHORIZED:
67+
return StatusCanonicalCode.UNAUTHENTICATED
68+
if status == 403: # HTTPStatus.FORBIDDEN:
69+
return StatusCanonicalCode.PERMISSION_DENIED
70+
if status == 404: # HTTPStatus.NOT_FOUND:
71+
return StatusCanonicalCode.NOT_FOUND
72+
if status == 429: # HTTPStatus.TOO_MANY_REQUESTS:
73+
return StatusCanonicalCode.RESOURCE_EXHAUSTED
74+
return StatusCanonicalCode.INVALID_ARGUMENT
75+
if status <= 599:
76+
if status == 501: # HTTPStatus.NOT_IMPLEMENTED:
77+
return StatusCanonicalCode.UNIMPLEMENTED
78+
if status == 503: # HTTPStatus.SERVICE_UNAVAILABLE:
79+
return StatusCanonicalCode.UNAVAILABLE
80+
if status == 504: # HTTPStatus.GATEWAY_TIMEOUT:
81+
return StatusCanonicalCode.DEADLINE_EXCEEDED
82+
return StatusCanonicalCode.INTERNAL
83+
return StatusCanonicalCode.UNKNOWN
84+
85+
86+
def url_path_span_name(params: aiohttp.TraceRequestStartParams) -> str:
87+
"""Extract a span name from the request URL path.
88+
89+
A simple callable to extract the path portion of the requested URL
90+
for use as the span name.
91+
92+
:param aiohttp.TraceRequestStartParams params: Parameters describing
93+
the traced request.
94+
95+
:return: The URL path.
96+
:rtype: str
97+
"""
98+
return params.url.path
99+
100+
101+
def create_trace_config(
102+
url_filter: typing.Optional[typing.Callable[[str], str]] = None,
103+
span_name: typing.Optional[
104+
typing.Union[
105+
typing.Callable[[aiohttp.TraceRequestStartParams], str], str
106+
]
107+
] = None,
108+
) -> aiohttp.TraceConfig:
109+
"""Create an aiohttp-compatible trace configuration.
110+
111+
One span is created for the entire HTTP request, including intial
112+
TCP/TLS setup if the connection doesn't exist.
113+
114+
By default the span name is set to the HTTP request method.
115+
116+
Example usage:
117+
118+
.. code-block:: python
119+
120+
import aiohttp
121+
from opentelemetry.ext.aiohttp_client import create_trace_config
122+
123+
async with aiohttp.ClientSession(trace_configs=[create_trace_config()]) as session:
124+
async with session.get(url) as response:
125+
await response.text()
126+
127+
128+
:param url_filter: A callback to process the requested URL prior to adding
129+
it as a span attribute. This can be useful to remove sensitive data
130+
such as API keys or user personal information.
131+
132+
:param str span_name: Override the default span name.
133+
134+
:return: An object suitable for use with :py:class:`aiohttp.ClientSession`.
135+
:rtype: :py:class:`aiohttp.TraceConfig`
136+
"""
137+
# `aiohttp.TraceRequestStartParams` resolves to `aiohttp.tracing.TraceRequestStartParams`
138+
# which doesn't exist in the aiottp intersphinx inventory.
139+
# Explicitly specify the type for the `span_name` param and rtype to work
140+
# around this issue.
141+
142+
tracer = trace.get_tracer_provider().get_tracer(__name__, __version__)
143+
144+
async def on_request_start(
145+
unused_session: aiohttp.ClientSession,
146+
trace_config_ctx: types.SimpleNamespace,
147+
params: aiohttp.TraceRequestStartParams,
148+
):
149+
http_method = params.method.upper()
150+
if trace_config_ctx.span_name is None:
151+
request_span_name = http_method
152+
elif callable(trace_config_ctx.span_name):
153+
request_span_name = str(trace_config_ctx.span_name(params))
154+
else:
155+
request_span_name = str(trace_config_ctx.span_name)
156+
157+
trace_config_ctx.span = trace_config_ctx.tracer.start_span(
158+
request_span_name,
159+
kind=SpanKind.CLIENT,
160+
attributes={
161+
"component": "http",
162+
"http.method": http_method,
163+
"http.url": trace_config_ctx.url_filter(params.url)
164+
if callable(trace_config_ctx.url_filter)
165+
else str(params.url),
166+
},
167+
)
168+
169+
# Set the span as active via the `Tracer.use_span` context.
170+
# TODO: would be nice to have an explicit API to set a context as active.
171+
span_manager = contextlib.ExitStack()
172+
span_manager.enter_context(
173+
trace_config_ctx.tracer.use_span(
174+
trace_config_ctx.span, end_on_exit=True
175+
)
176+
)
177+
trace_config_ctx.span_manager = span_manager
178+
179+
propagators.inject(
180+
tracer, type(params.headers).__setitem__, params.headers
181+
)
182+
183+
async def on_request_end(
184+
unused_session: aiohttp.ClientSession,
185+
trace_config_ctx: types.SimpleNamespace,
186+
params: aiohttp.TraceRequestEndParams,
187+
):
188+
trace_config_ctx.span.set_status(
189+
Status(http_status_to_canonical_code(int(params.response.status)))
190+
)
191+
trace_config_ctx.span.set_attribute(
192+
"http.status_code", params.response.status
193+
)
194+
trace_config_ctx.span.set_attribute(
195+
"http.status_text", params.response.reason
196+
)
197+
trace_config_ctx.span_manager.close()
198+
199+
async def on_request_exception(
200+
unused_session: aiohttp.ClientSession,
201+
trace_config_ctx: types.SimpleNamespace,
202+
params: aiohttp.TraceRequestExceptionParams,
203+
):
204+
if isinstance(
205+
params.exception,
206+
(aiohttp.ServerTimeoutError, aiohttp.TooManyRedirects),
207+
):
208+
status = StatusCanonicalCode.DEADLINE_EXCEEDED
209+
# Assume any getaddrinfo error is a DNS failure.
210+
elif isinstance(
211+
params.exception, aiohttp.ClientConnectorError
212+
) and isinstance(params.exception.os_error, socket.gaierror):
213+
# DNS resolution failed
214+
status = StatusCanonicalCode.UNKNOWN
215+
else:
216+
status = StatusCanonicalCode.UNAVAILABLE
217+
218+
trace_config_ctx.span.set_status(Status(status))
219+
trace_config_ctx.span_manager.close()
220+
221+
def _trace_config_ctx_factory(**kwargs):
222+
if kwargs.get("trace_request_ctx", None) is None:
223+
kwargs["trace_request_ctx"] = {}
224+
return types.SimpleNamespace(
225+
span_name=span_name, tracer=tracer, url_filter=url_filter, **kwargs
226+
)
227+
228+
trace_config = aiohttp.TraceConfig(
229+
trace_config_ctx_factory=_trace_config_ctx_factory
230+
)
231+
232+
trace_config.on_request_start.append(on_request_start)
233+
trace_config.on_request_end.append(on_request_end)
234+
trace_config.on_request_exception.append(on_request_exception)
235+
236+
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.6.dev0"

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

Whitespace-only changes.

0 commit comments

Comments
 (0)