-
Notifications
You must be signed in to change notification settings - Fork 713
Aiohttp-server Instrumentation #1800
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
ocelotl
merged 44 commits into
open-telemetry:main
from
decko:aiohttp-server-instrumentation
Oct 30, 2023
Merged
Changes from all commits
Commits
Show all changes
44 commits
Select commit
Hold shift + click to select a range
8d0c504
draft instrumentation for aiohttp server / guillotina
dmanchon 5a14c7d
aiohttp server
dmanchon 57f59b0
aiohttp server tests
dmanchon ceb0d18
PR comments
dmanchon ea76c6d
Change the project to use pyproject conventions and bump version
decko 1239fc5
Fixes the resource name in pyproject.
decko 3c00ed7
Reverts to the previous usage of set_attributes method.
decko 767adcc
Add pytest-asyncio and pytest-aiohttp as test dependencies.
decko e312bec
Convert the unittest tests to pytest test style.
decko 8699f9c
Deal with an exception not setting the span's http status code.
decko c29be30
Remove Python 3.6 as supported version.
decko ad6b87f
Add the new aiohttp-server package as project dependency.
decko b006305
Adds Metric support.
decko be9ccfc
Refactor some tests.
decko 04d5450
Merge branch 'main' into aiohttp-server-instrumentation
decko 38204c0
Add some typing to the `keys` function.
decko e235e7e
Add Python 3.11 as a supported version by the library.
decko 9a551b1
Fixes some typos and add more typing to the code.
decko 82d3b8d
Add better docstrings and also some types.
decko 4f922a2
Merge branch 'main' into aiohttp-server-instrumentation
decko 74fece8
Inject context into span to avoid it being orphaned.
decko adee7a2
Merge branch 'main' into aiohttp-server-instrumentation
decko 653d923
Merge branch 'main' into aiohttp-server-instrumentation
decko ce28b31
Merge branch 'main' into aiohttp-server-instrumentation
ocelotl 836a738
Bump dependencies and lib versions.
decko 40c6e3d
Correctly import `extract`.
decko 5160ca6
Merge branch 'main' into aiohttp-server-instrumentation
decko cd569b1
Revert the bootstrap.py file changes
decko 98c0f42
Remove the of iter_entry_points from pkg_resources.
decko 3c1a0b2
Sync the package version with the one on pyproject.toml.
decko a76f1df
Fixes the library version and bumps OpenTelemetry dependency versions.
decko dec87c6
Merge branch 'main' into aiohttp-server-instrumentation
decko 7b4e50c
Bump opentelemetry dependencies and this lib itself to 0.42b0.
decko bde869c
Remove the usage of `round` to allow a more precise value.
decko 4397a46
Add a CHANGELOG entry.
decko d5caef0
Merge branch 'main' into aiohttp-server-instrumentation
ocelotl 75f2aad
Adding updates from tox -e generate
ocelotl 26393c6
Remove a skiped test.
decko 96300f5
Add LICENSE header.
decko 9809d79
Add LICENSE header and fixes the use of a constant.
decko fbbe270
Fix linter issues.
decko 58936f5
Merge branch 'main' into aiohttp-server-instrumentation
decko 782e4e8
Change the HTTPMethod Enum.
decko c22e300
Fixes some tests.
decko File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
24 changes: 24 additions & 0 deletions
24
instrumentation/opentelemetry-instrumentation-aiohttp-server/README.rst
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
OpenTelemetry aiohttp server Integration | ||
======================================== | ||
|
||
|pypi| | ||
|
||
.. |pypi| image:: https://badge.fury.io/py/opentelemetry-instrumentation-aiohttp-client.svg | ||
:target: https://pypi.org/project/opentelemetry-instrumentation-aiohttp-client/ | ||
|
||
This library allows tracing HTTP requests made by the | ||
`aiohttp server <https://docs.aiohttp.org/en/stable/server.html>`_ library. | ||
|
||
Installation | ||
------------ | ||
|
||
:: | ||
|
||
pip install opentelemetry-instrumentation-aiohttp-server | ||
|
||
References | ||
---------- | ||
|
||
* `OpenTelemetry Project <https://opentelemetry.io/>`_ | ||
* `aiohttp client Tracing <https://docs.aiohttp.org/en/stable/tracing_reference.html>`_ | ||
* `OpenTelemetry Python Examples <https://github.com/open-telemetry/opentelemetry-python/tree/main/docs/examples>`_ |
61 changes: 61 additions & 0 deletions
61
instrumentation/opentelemetry-instrumentation-aiohttp-server/pyproject.toml
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
[build-system] | ||
requires = ["hatchling"] | ||
build-backend = "hatchling.build" | ||
|
||
[project] | ||
name = "opentelemetry-instrumentation-aiohttp-server" | ||
dynamic = ["version"] | ||
description = "Aiohttp server instrumentation for OpenTelemetry" | ||
readme = "README.rst" | ||
license = "Apache-2.0" | ||
requires-python = ">=3.7" | ||
authors = [ | ||
{ name = "OpenTelemetry Authors", email = "[email protected]"} | ||
] | ||
classifiers = [ | ||
"Development Status :: 4 - Beta", | ||
"Intended Audience :: Developers", | ||
"License :: OSI Approved :: Apache Software License", | ||
"Programming Language :: Python", | ||
"Programming Language :: Python :: 3", | ||
"Programming Language :: Python :: 3.7", | ||
"Programming Language :: Python :: 3.8", | ||
"Programming Language :: Python :: 3.9", | ||
"Programming Language :: Python :: 3.10", | ||
"Programming Language :: Python :: 3.11" | ||
] | ||
dependencies = [ | ||
"opentelemetry-api ~= 1.12", | ||
"opentelemetry-instrumentation == 0.42b0.dev", | ||
"opentelemetry-semantic-conventions == 0.42b0.dev", | ||
"opentelemetry-util-http == 0.42b0.dev", | ||
"wrapt >= 1.0.0, < 2.0.0", | ||
] | ||
|
||
[project.optional-dependencies] | ||
instruments = [ | ||
"aiohttp ~= 3.0", | ||
] | ||
test = [ | ||
"opentelemetry-instrumentation-aiohttp-server[instruments]", | ||
"pytest-asyncio", | ||
"pytest-aiohttp", | ||
] | ||
|
||
[project.entry-points.opentelemetry_instrumentor] | ||
aiohttp-server = "opentelemetry.instrumentation.aiohttp_server:AioHttpServerInstrumentor" | ||
|
||
[project.urls] | ||
Homepage = "https://github.com/open-telemetry/opentelemetry-python-contrib/tree/main/instrumentation/opentelemetry-instrumentation-aiohttp-server" | ||
|
||
[tool.hatch.version] | ||
path = "src/opentelemetry/instrumentation/aiohttp_server/version.py" | ||
|
||
[tool.hatch.build.targets.sdist] | ||
include = [ | ||
"/src", | ||
"/tests", | ||
] | ||
|
||
[tool.hatch.build.targets.wheel] | ||
packages = ["src/opentelemetry"] |
267 changes: 267 additions & 0 deletions
267
...strumentation-aiohttp-server/src/opentelemetry/instrumentation/aiohttp_server/__init__.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,267 @@ | ||
# Copyright 2020, OpenTelemetry Authors | ||
# | ||
# Licensed under the Apache License, Version 2.0 (the "License"); | ||
# you may not use this file except in compliance with the License. | ||
# You may obtain a copy of the License at | ||
# | ||
# http://www.apache.org/licenses/LICENSE-2.0 | ||
# | ||
# Unless required by applicable law or agreed to in writing, software | ||
# distributed under the License is distributed on an "AS IS" BASIS, | ||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
# See the License for the specific language governing permissions and | ||
# limitations under the License. | ||
|
||
import urllib | ||
from aiohttp import web | ||
from multidict import CIMultiDictProxy | ||
from timeit import default_timer | ||
from typing import Tuple, Dict, List, Union | ||
|
||
from opentelemetry import context, trace, metrics | ||
from opentelemetry.context import _SUPPRESS_HTTP_INSTRUMENTATION_KEY | ||
from opentelemetry.instrumentation.aiohttp_server.package import _instruments | ||
from opentelemetry.instrumentation.aiohttp_server.version import __version__ | ||
from opentelemetry.instrumentation.instrumentor import BaseInstrumentor | ||
from opentelemetry.instrumentation.utils import http_status_to_status_code | ||
from opentelemetry.propagators.textmap import Getter | ||
from opentelemetry.propagate import extract | ||
from opentelemetry.semconv.trace import SpanAttributes | ||
from opentelemetry.semconv.metrics import MetricInstruments | ||
from opentelemetry.trace.status import Status, StatusCode | ||
from opentelemetry.util.http import get_excluded_urls | ||
from opentelemetry.util.http import remove_url_credentials | ||
|
||
_duration_attrs = [ | ||
SpanAttributes.HTTP_METHOD, | ||
SpanAttributes.HTTP_HOST, | ||
SpanAttributes.HTTP_SCHEME, | ||
SpanAttributes.HTTP_STATUS_CODE, | ||
SpanAttributes.HTTP_FLAVOR, | ||
SpanAttributes.HTTP_SERVER_NAME, | ||
SpanAttributes.NET_HOST_NAME, | ||
SpanAttributes.NET_HOST_PORT, | ||
SpanAttributes.HTTP_ROUTE, | ||
] | ||
|
||
_active_requests_count_attrs = [ | ||
SpanAttributes.HTTP_METHOD, | ||
SpanAttributes.HTTP_HOST, | ||
SpanAttributes.HTTP_SCHEME, | ||
SpanAttributes.HTTP_FLAVOR, | ||
SpanAttributes.HTTP_SERVER_NAME, | ||
] | ||
|
||
tracer = trace.get_tracer(__name__) | ||
meter = metrics.get_meter(__name__, __version__) | ||
_excluded_urls = get_excluded_urls("AIOHTTP_SERVER") | ||
|
||
|
||
def _parse_duration_attrs(req_attrs): | ||
duration_attrs = {} | ||
for attr_key in _duration_attrs: | ||
if req_attrs.get(attr_key) is not None: | ||
duration_attrs[attr_key] = req_attrs[attr_key] | ||
return duration_attrs | ||
|
||
|
||
def _parse_active_request_count_attrs(req_attrs): | ||
active_requests_count_attrs = {} | ||
for attr_key in _active_requests_count_attrs: | ||
if req_attrs.get(attr_key) is not None: | ||
active_requests_count_attrs[attr_key] = req_attrs[attr_key] | ||
return active_requests_count_attrs | ||
|
||
|
||
def get_default_span_details(request: web.Request) -> Tuple[str, dict]: | ||
"""Default implementation for get_default_span_details | ||
Args: | ||
request: the request object itself. | ||
Returns: | ||
a tuple of the span name, and any attributes to attach to the span. | ||
""" | ||
span_name = request.path.strip() or f"HTTP {request.method}" | ||
return span_name, {} | ||
|
||
|
||
def _get_view_func(request: web.Request) -> str: | ||
"""Returns the name of the request handler. | ||
Args: | ||
request: the request object itself. | ||
Returns: | ||
a string containing the name of the handler function | ||
""" | ||
try: | ||
return request.match_info.handler.__name__ | ||
except AttributeError: | ||
return "unknown" | ||
|
||
|
||
def collect_request_attributes(request: web.Request) -> Dict: | ||
"""Collects HTTP request attributes from the ASGI scope and returns a | ||
dictionary to be used as span creation attributes.""" | ||
|
||
server_host, port, http_url = ( | ||
request.url.host, | ||
request.url.port, | ||
str(request.url), | ||
) | ||
query_string = request.query_string | ||
if query_string and http_url: | ||
if isinstance(query_string, bytes): | ||
query_string = query_string.decode("utf8") | ||
http_url += "?" + urllib.parse.unquote(query_string) | ||
|
||
result = { | ||
SpanAttributes.HTTP_SCHEME: request.scheme, | ||
SpanAttributes.HTTP_HOST: server_host, | ||
SpanAttributes.NET_HOST_PORT: port, | ||
SpanAttributes.HTTP_ROUTE: _get_view_func(request), | ||
SpanAttributes.HTTP_FLAVOR: f"{request.version.major}.{request.version.minor}", | ||
SpanAttributes.HTTP_TARGET: request.path, | ||
SpanAttributes.HTTP_URL: remove_url_credentials(http_url), | ||
} | ||
|
||
http_method = request.method | ||
if http_method: | ||
result[SpanAttributes.HTTP_METHOD] = http_method | ||
|
||
http_host_value_list = ( | ||
[request.host] if type(request.host) != list else request.host | ||
) | ||
if http_host_value_list: | ||
result[SpanAttributes.HTTP_SERVER_NAME] = ",".join( | ||
http_host_value_list | ||
) | ||
http_user_agent = request.headers.get("user-agent") | ||
if http_user_agent: | ||
result[SpanAttributes.HTTP_USER_AGENT] = http_user_agent | ||
|
||
# remove None values | ||
result = {k: v for k, v in result.items() if v is not None} | ||
|
||
return result | ||
|
||
|
||
def set_status_code(span, status_code: int) -> None: | ||
"""Adds HTTP response attributes to span using the status_code argument.""" | ||
|
||
try: | ||
status_code = int(status_code) | ||
except ValueError: | ||
span.set_status( | ||
Status( | ||
StatusCode.ERROR, | ||
"Non-integer HTTP status: " + repr(status_code), | ||
) | ||
) | ||
else: | ||
span.set_attribute(SpanAttributes.HTTP_STATUS_CODE, status_code) | ||
span.set_status( | ||
Status(http_status_to_status_code(status_code, server_span=True)) | ||
) | ||
|
||
|
||
class AiohttpGetter(Getter): | ||
"""Extract current trace from headers""" | ||
|
||
def get(self, carrier, key: str) -> Union[List, None]: | ||
"""Getter implementation to retrieve an HTTP header value from the ASGI | ||
scope. | ||
|
||
Args: | ||
carrier: ASGI scope object | ||
key: header name in scope | ||
Returns: | ||
A list of all header values matching the key, or None if the key | ||
does not match any header. | ||
""" | ||
headers: CIMultiDictProxy = carrier.headers | ||
if not headers: | ||
return None | ||
return headers.getall(key, None) | ||
|
||
def keys(self, carrier: Dict) -> List: | ||
return list(carrier.keys()) | ||
|
||
|
||
getter = AiohttpGetter() | ||
|
||
|
||
@web.middleware | ||
async def middleware(request, handler): | ||
"""Middleware for aiohttp implementing tracing logic""" | ||
if ( | ||
context.get_value("suppress_instrumentation") | ||
or context.get_value(_SUPPRESS_HTTP_INSTRUMENTATION_KEY) | ||
or _excluded_urls.url_disabled(request.url.path) | ||
): | ||
return await handler(request) | ||
|
||
span_name, additional_attributes = get_default_span_details(request) | ||
|
||
req_attrs = collect_request_attributes(request) | ||
duration_attrs = _parse_duration_attrs(req_attrs) | ||
active_requests_count_attrs = _parse_active_request_count_attrs(req_attrs) | ||
|
||
duration_histogram = meter.create_histogram( | ||
name=MetricInstruments.HTTP_SERVER_DURATION, | ||
unit="ms", | ||
description="measures the duration of the inbound HTTP request", | ||
) | ||
|
||
active_requests_counter = meter.create_up_down_counter( | ||
name=MetricInstruments.HTTP_SERVER_ACTIVE_REQUESTS, | ||
unit="requests", | ||
description="measures the number of concurrent HTTP requests those are currently in flight", | ||
) | ||
|
||
with tracer.start_as_current_span( | ||
span_name, | ||
context=extract(request, getter=getter), | ||
decko marked this conversation as resolved.
Show resolved
Hide resolved
|
||
kind=trace.SpanKind.SERVER, | ||
) as span: | ||
attributes = collect_request_attributes(request) | ||
attributes.update(additional_attributes) | ||
span.set_attributes(attributes) | ||
start = default_timer() | ||
active_requests_counter.add(1, active_requests_count_attrs) | ||
try: | ||
resp = await handler(request) | ||
set_status_code(span, resp.status) | ||
except web.HTTPException as ex: | ||
set_status_code(span, ex.status_code) | ||
raise | ||
finally: | ||
duration = max((default_timer() - start) * 1000, 0) | ||
duration_histogram.record(duration, duration_attrs) | ||
active_requests_counter.add(-1, active_requests_count_attrs) | ||
return resp | ||
|
||
|
||
class _InstrumentedApplication(web.Application): | ||
"""Insert tracing middleware""" | ||
|
||
def __init__(self, *args, **kwargs): | ||
middlewares = kwargs.pop("middlewares", []) | ||
middlewares.insert(0, middleware) | ||
kwargs["middlewares"] = middlewares | ||
super().__init__(*args, **kwargs) | ||
|
||
|
||
class AioHttpServerInstrumentor(BaseInstrumentor): | ||
# pylint: disable=protected-access,attribute-defined-outside-init | ||
"""An instrumentor for aiohttp.web.Application | ||
|
||
See `BaseInstrumentor` | ||
""" | ||
|
||
def _instrument(self, **kwargs): | ||
self._original_app = web.Application | ||
setattr(web, "Application", _InstrumentedApplication) | ||
|
||
def _uninstrument(self, **kwargs): | ||
setattr(web, "Application", self._original_app) | ||
|
||
def instrumentation_dependencies(self): | ||
return _instruments |
16 changes: 16 additions & 0 deletions
16
...nstrumentation-aiohttp-server/src/opentelemetry/instrumentation/aiohttp_server/package.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
# Copyright The OpenTelemetry Authors | ||
# | ||
# Licensed under the Apache License, Version 2.0 (the "License"); | ||
# you may not use this file except in compliance with the License. | ||
# You may obtain a copy of the License at | ||
# | ||
# http://www.apache.org/licenses/LICENSE-2.0 | ||
# | ||
# Unless required by applicable law or agreed to in writing, software | ||
# distributed under the License is distributed on an "AS IS" BASIS, | ||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
# See the License for the specific language governing permissions and | ||
# limitations under the License. | ||
|
||
|
||
_instruments = ("aiohttp ~= 3.0",) |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.