Skip to content

Commit 1305436

Browse files
deckokennytrytek-wfdmanchon
authored
Aiohttp-server Instrumentation (#1800)
Co-authored-by: Kenny Trytek <kenny.trytek@workiva.com> Co-authored-by: Daniel Manchon <dmanchon@gmail.com>
1 parent 3478831 commit 1305436

File tree

13 files changed

+536
-1
lines changed

13 files changed

+536
-1
lines changed

CHANGELOG.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## Unreleased
99

10+
- `opentelemetry-instrumentation-aiohttp-server` Add instrumentor and auto instrumentation support for aiohttp-server
11+
([#1800](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1800))
1012

1113
### Added
1214
- `opentelemetry-instrumentation-system-metrics` Add support for collecting process metrics
@@ -19,7 +21,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1921
- `opentelemetry-resource-detector-azure` Using new Cloud Resource ID attribute.
2022
([#1976](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1976))
2123

22-
2324
## Version 1.20.0/0.41b0 (2023-09-01)
2425

2526
### Fixed

instrumentation/README.md

+1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
| --------------- | ------------------ | --------------- |
44
| [opentelemetry-instrumentation-aio-pika](./opentelemetry-instrumentation-aio-pika) | aio_pika >= 7.2.0, < 10.0.0 | No
55
| [opentelemetry-instrumentation-aiohttp-client](./opentelemetry-instrumentation-aiohttp-client) | aiohttp ~= 3.0 | No
6+
| [opentelemetry-instrumentation-aiohttp-server](./opentelemetry-instrumentation-aiohttp-server) | aiohttp ~= 3.0 | No
67
| [opentelemetry-instrumentation-aiopg](./opentelemetry-instrumentation-aiopg) | aiopg >= 0.13.0, < 2.0.0 | No
78
| [opentelemetry-instrumentation-asgi](./opentelemetry-instrumentation-asgi) | asgiref ~= 3.0 | No
89
| [opentelemetry-instrumentation-asyncpg](./opentelemetry-instrumentation-asyncpg) | asyncpg >= 0.12.0 | No
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
OpenTelemetry aiohttp server Integration
2+
========================================
3+
4+
|pypi|
5+
6+
.. |pypi| image:: https://badge.fury.io/py/opentelemetry-instrumentation-aiohttp-client.svg
7+
:target: https://pypi.org/project/opentelemetry-instrumentation-aiohttp-client/
8+
9+
This library allows tracing HTTP requests made by the
10+
`aiohttp server <https://docs.aiohttp.org/en/stable/server.html>`_ library.
11+
12+
Installation
13+
------------
14+
15+
::
16+
17+
pip install opentelemetry-instrumentation-aiohttp-server
18+
19+
References
20+
----------
21+
22+
* `OpenTelemetry Project <https://opentelemetry.io/>`_
23+
* `aiohttp client Tracing <https://docs.aiohttp.org/en/stable/tracing_reference.html>`_
24+
* `OpenTelemetry Python Examples <https://github.com/open-telemetry/opentelemetry-python/tree/main/docs/examples>`_
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
[build-system]
2+
requires = ["hatchling"]
3+
build-backend = "hatchling.build"
4+
5+
[project]
6+
name = "opentelemetry-instrumentation-aiohttp-server"
7+
dynamic = ["version"]
8+
description = "Aiohttp server instrumentation for OpenTelemetry"
9+
readme = "README.rst"
10+
license = "Apache-2.0"
11+
requires-python = ">=3.7"
12+
authors = [
13+
{ name = "OpenTelemetry Authors", email = "cncf-opentelemetry-contributors@lists.cncf.io"}
14+
]
15+
classifiers = [
16+
"Development Status :: 4 - Beta",
17+
"Intended Audience :: Developers",
18+
"License :: OSI Approved :: Apache Software License",
19+
"Programming Language :: Python",
20+
"Programming Language :: Python :: 3",
21+
"Programming Language :: Python :: 3.7",
22+
"Programming Language :: Python :: 3.8",
23+
"Programming Language :: Python :: 3.9",
24+
"Programming Language :: Python :: 3.10",
25+
"Programming Language :: Python :: 3.11"
26+
]
27+
dependencies = [
28+
"opentelemetry-api ~= 1.12",
29+
"opentelemetry-instrumentation == 0.42b0.dev",
30+
"opentelemetry-semantic-conventions == 0.42b0.dev",
31+
"opentelemetry-util-http == 0.42b0.dev",
32+
"wrapt >= 1.0.0, < 2.0.0",
33+
]
34+
35+
[project.optional-dependencies]
36+
instruments = [
37+
"aiohttp ~= 3.0",
38+
]
39+
test = [
40+
"opentelemetry-instrumentation-aiohttp-server[instruments]",
41+
"pytest-asyncio",
42+
"pytest-aiohttp",
43+
]
44+
45+
[project.entry-points.opentelemetry_instrumentor]
46+
aiohttp-server = "opentelemetry.instrumentation.aiohttp_server:AioHttpServerInstrumentor"
47+
48+
[project.urls]
49+
Homepage = "https://github.com/open-telemetry/opentelemetry-python-contrib/tree/main/instrumentation/opentelemetry-instrumentation-aiohttp-server"
50+
51+
[tool.hatch.version]
52+
path = "src/opentelemetry/instrumentation/aiohttp_server/version.py"
53+
54+
[tool.hatch.build.targets.sdist]
55+
include = [
56+
"/src",
57+
"/tests",
58+
]
59+
60+
[tool.hatch.build.targets.wheel]
61+
packages = ["src/opentelemetry"]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,267 @@
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+
import urllib
16+
from aiohttp import web
17+
from multidict import CIMultiDictProxy
18+
from timeit import default_timer
19+
from typing import Tuple, Dict, List, Union
20+
21+
from opentelemetry import context, trace, metrics
22+
from opentelemetry.context import _SUPPRESS_HTTP_INSTRUMENTATION_KEY
23+
from opentelemetry.instrumentation.aiohttp_server.package import _instruments
24+
from opentelemetry.instrumentation.aiohttp_server.version import __version__
25+
from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
26+
from opentelemetry.instrumentation.utils import http_status_to_status_code
27+
from opentelemetry.propagators.textmap import Getter
28+
from opentelemetry.propagate import extract
29+
from opentelemetry.semconv.trace import SpanAttributes
30+
from opentelemetry.semconv.metrics import MetricInstruments
31+
from opentelemetry.trace.status import Status, StatusCode
32+
from opentelemetry.util.http import get_excluded_urls
33+
from opentelemetry.util.http import remove_url_credentials
34+
35+
_duration_attrs = [
36+
SpanAttributes.HTTP_METHOD,
37+
SpanAttributes.HTTP_HOST,
38+
SpanAttributes.HTTP_SCHEME,
39+
SpanAttributes.HTTP_STATUS_CODE,
40+
SpanAttributes.HTTP_FLAVOR,
41+
SpanAttributes.HTTP_SERVER_NAME,
42+
SpanAttributes.NET_HOST_NAME,
43+
SpanAttributes.NET_HOST_PORT,
44+
SpanAttributes.HTTP_ROUTE,
45+
]
46+
47+
_active_requests_count_attrs = [
48+
SpanAttributes.HTTP_METHOD,
49+
SpanAttributes.HTTP_HOST,
50+
SpanAttributes.HTTP_SCHEME,
51+
SpanAttributes.HTTP_FLAVOR,
52+
SpanAttributes.HTTP_SERVER_NAME,
53+
]
54+
55+
tracer = trace.get_tracer(__name__)
56+
meter = metrics.get_meter(__name__, __version__)
57+
_excluded_urls = get_excluded_urls("AIOHTTP_SERVER")
58+
59+
60+
def _parse_duration_attrs(req_attrs):
61+
duration_attrs = {}
62+
for attr_key in _duration_attrs:
63+
if req_attrs.get(attr_key) is not None:
64+
duration_attrs[attr_key] = req_attrs[attr_key]
65+
return duration_attrs
66+
67+
68+
def _parse_active_request_count_attrs(req_attrs):
69+
active_requests_count_attrs = {}
70+
for attr_key in _active_requests_count_attrs:
71+
if req_attrs.get(attr_key) is not None:
72+
active_requests_count_attrs[attr_key] = req_attrs[attr_key]
73+
return active_requests_count_attrs
74+
75+
76+
def get_default_span_details(request: web.Request) -> Tuple[str, dict]:
77+
"""Default implementation for get_default_span_details
78+
Args:
79+
request: the request object itself.
80+
Returns:
81+
a tuple of the span name, and any attributes to attach to the span.
82+
"""
83+
span_name = request.path.strip() or f"HTTP {request.method}"
84+
return span_name, {}
85+
86+
87+
def _get_view_func(request: web.Request) -> str:
88+
"""Returns the name of the request handler.
89+
Args:
90+
request: the request object itself.
91+
Returns:
92+
a string containing the name of the handler function
93+
"""
94+
try:
95+
return request.match_info.handler.__name__
96+
except AttributeError:
97+
return "unknown"
98+
99+
100+
def collect_request_attributes(request: web.Request) -> Dict:
101+
"""Collects HTTP request attributes from the ASGI scope and returns a
102+
dictionary to be used as span creation attributes."""
103+
104+
server_host, port, http_url = (
105+
request.url.host,
106+
request.url.port,
107+
str(request.url),
108+
)
109+
query_string = request.query_string
110+
if query_string and http_url:
111+
if isinstance(query_string, bytes):
112+
query_string = query_string.decode("utf8")
113+
http_url += "?" + urllib.parse.unquote(query_string)
114+
115+
result = {
116+
SpanAttributes.HTTP_SCHEME: request.scheme,
117+
SpanAttributes.HTTP_HOST: server_host,
118+
SpanAttributes.NET_HOST_PORT: port,
119+
SpanAttributes.HTTP_ROUTE: _get_view_func(request),
120+
SpanAttributes.HTTP_FLAVOR: f"{request.version.major}.{request.version.minor}",
121+
SpanAttributes.HTTP_TARGET: request.path,
122+
SpanAttributes.HTTP_URL: remove_url_credentials(http_url),
123+
}
124+
125+
http_method = request.method
126+
if http_method:
127+
result[SpanAttributes.HTTP_METHOD] = http_method
128+
129+
http_host_value_list = (
130+
[request.host] if type(request.host) != list else request.host
131+
)
132+
if http_host_value_list:
133+
result[SpanAttributes.HTTP_SERVER_NAME] = ",".join(
134+
http_host_value_list
135+
)
136+
http_user_agent = request.headers.get("user-agent")
137+
if http_user_agent:
138+
result[SpanAttributes.HTTP_USER_AGENT] = http_user_agent
139+
140+
# remove None values
141+
result = {k: v for k, v in result.items() if v is not None}
142+
143+
return result
144+
145+
146+
def set_status_code(span, status_code: int) -> None:
147+
"""Adds HTTP response attributes to span using the status_code argument."""
148+
149+
try:
150+
status_code = int(status_code)
151+
except ValueError:
152+
span.set_status(
153+
Status(
154+
StatusCode.ERROR,
155+
"Non-integer HTTP status: " + repr(status_code),
156+
)
157+
)
158+
else:
159+
span.set_attribute(SpanAttributes.HTTP_STATUS_CODE, status_code)
160+
span.set_status(
161+
Status(http_status_to_status_code(status_code, server_span=True))
162+
)
163+
164+
165+
class AiohttpGetter(Getter):
166+
"""Extract current trace from headers"""
167+
168+
def get(self, carrier, key: str) -> Union[List, None]:
169+
"""Getter implementation to retrieve an HTTP header value from the ASGI
170+
scope.
171+
172+
Args:
173+
carrier: ASGI scope object
174+
key: header name in scope
175+
Returns:
176+
A list of all header values matching the key, or None if the key
177+
does not match any header.
178+
"""
179+
headers: CIMultiDictProxy = carrier.headers
180+
if not headers:
181+
return None
182+
return headers.getall(key, None)
183+
184+
def keys(self, carrier: Dict) -> List:
185+
return list(carrier.keys())
186+
187+
188+
getter = AiohttpGetter()
189+
190+
191+
@web.middleware
192+
async def middleware(request, handler):
193+
"""Middleware for aiohttp implementing tracing logic"""
194+
if (
195+
context.get_value("suppress_instrumentation")
196+
or context.get_value(_SUPPRESS_HTTP_INSTRUMENTATION_KEY)
197+
or _excluded_urls.url_disabled(request.url.path)
198+
):
199+
return await handler(request)
200+
201+
span_name, additional_attributes = get_default_span_details(request)
202+
203+
req_attrs = collect_request_attributes(request)
204+
duration_attrs = _parse_duration_attrs(req_attrs)
205+
active_requests_count_attrs = _parse_active_request_count_attrs(req_attrs)
206+
207+
duration_histogram = meter.create_histogram(
208+
name=MetricInstruments.HTTP_SERVER_DURATION,
209+
unit="ms",
210+
description="measures the duration of the inbound HTTP request",
211+
)
212+
213+
active_requests_counter = meter.create_up_down_counter(
214+
name=MetricInstruments.HTTP_SERVER_ACTIVE_REQUESTS,
215+
unit="requests",
216+
description="measures the number of concurrent HTTP requests those are currently in flight",
217+
)
218+
219+
with tracer.start_as_current_span(
220+
span_name,
221+
context=extract(request, getter=getter),
222+
kind=trace.SpanKind.SERVER,
223+
) as span:
224+
attributes = collect_request_attributes(request)
225+
attributes.update(additional_attributes)
226+
span.set_attributes(attributes)
227+
start = default_timer()
228+
active_requests_counter.add(1, active_requests_count_attrs)
229+
try:
230+
resp = await handler(request)
231+
set_status_code(span, resp.status)
232+
except web.HTTPException as ex:
233+
set_status_code(span, ex.status_code)
234+
raise
235+
finally:
236+
duration = max((default_timer() - start) * 1000, 0)
237+
duration_histogram.record(duration, duration_attrs)
238+
active_requests_counter.add(-1, active_requests_count_attrs)
239+
return resp
240+
241+
242+
class _InstrumentedApplication(web.Application):
243+
"""Insert tracing middleware"""
244+
245+
def __init__(self, *args, **kwargs):
246+
middlewares = kwargs.pop("middlewares", [])
247+
middlewares.insert(0, middleware)
248+
kwargs["middlewares"] = middlewares
249+
super().__init__(*args, **kwargs)
250+
251+
252+
class AioHttpServerInstrumentor(BaseInstrumentor):
253+
# pylint: disable=protected-access,attribute-defined-outside-init
254+
"""An instrumentor for aiohttp.web.Application
255+
256+
See `BaseInstrumentor`
257+
"""
258+
259+
def _instrument(self, **kwargs):
260+
self._original_app = web.Application
261+
setattr(web, "Application", _InstrumentedApplication)
262+
263+
def _uninstrument(self, **kwargs):
264+
setattr(web, "Application", self._original_app)
265+
266+
def instrumentation_dependencies(self):
267+
return _instruments
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# Copyright The 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+
_instruments = ("aiohttp ~= 3.0",)

0 commit comments

Comments
 (0)