Skip to content
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

Aiohttp-server Instrumentation #1800

Merged
merged 44 commits into from
Oct 30, 2023
Merged
Show file tree
Hide file tree
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 Feb 26, 2022
5a14c7d
aiohttp server
dmanchon Mar 2, 2022
57f59b0
aiohttp server tests
dmanchon Mar 3, 2022
ceb0d18
PR comments
dmanchon Feb 19, 2023
ea76c6d
Change the project to use pyproject conventions and bump version
decko Apr 12, 2023
1239fc5
Fixes the resource name in pyproject.
decko Apr 12, 2023
3c00ed7
Reverts to the previous usage of set_attributes method.
decko Apr 12, 2023
767adcc
Add pytest-asyncio and pytest-aiohttp as test dependencies.
decko Apr 14, 2023
e312bec
Convert the unittest tests to pytest test style.
decko Apr 14, 2023
8699f9c
Deal with an exception not setting the span's http status code.
decko Apr 14, 2023
c29be30
Remove Python 3.6 as supported version.
decko Apr 14, 2023
ad6b87f
Add the new aiohttp-server package as project dependency.
decko Apr 14, 2023
b006305
Adds Metric support.
decko May 10, 2023
be9ccfc
Refactor some tests.
decko May 10, 2023
04d5450
Merge branch 'main' into aiohttp-server-instrumentation
decko Jun 20, 2023
38204c0
Add some typing to the `keys` function.
decko Jun 21, 2023
e235e7e
Add Python 3.11 as a supported version by the library.
decko Jun 21, 2023
9a551b1
Fixes some typos and add more typing to the code.
decko Jun 21, 2023
82d3b8d
Add better docstrings and also some types.
decko Jun 21, 2023
4f922a2
Merge branch 'main' into aiohttp-server-instrumentation
decko Jun 21, 2023
74fece8
Inject context into span to avoid it being orphaned.
decko Jun 23, 2023
adee7a2
Merge branch 'main' into aiohttp-server-instrumentation
decko Jun 23, 2023
653d923
Merge branch 'main' into aiohttp-server-instrumentation
decko Jun 26, 2023
ce28b31
Merge branch 'main' into aiohttp-server-instrumentation
ocelotl Jul 6, 2023
836a738
Bump dependencies and lib versions.
decko Aug 21, 2023
40c6e3d
Correctly import `extract`.
decko Aug 21, 2023
5160ca6
Merge branch 'main' into aiohttp-server-instrumentation
decko Aug 31, 2023
cd569b1
Revert the bootstrap.py file changes
decko Aug 31, 2023
98c0f42
Remove the of iter_entry_points from pkg_resources.
decko Aug 31, 2023
3c1a0b2
Sync the package version with the one on pyproject.toml.
decko Aug 31, 2023
a76f1df
Fixes the library version and bumps OpenTelemetry dependency versions.
decko Sep 4, 2023
dec87c6
Merge branch 'main' into aiohttp-server-instrumentation
decko Sep 6, 2023
7b4e50c
Bump opentelemetry dependencies and this lib itself to 0.42b0.
decko Sep 6, 2023
bde869c
Remove the usage of `round` to allow a more precise value.
decko Sep 8, 2023
4397a46
Add a CHANGELOG entry.
decko Sep 8, 2023
d5caef0
Merge branch 'main' into aiohttp-server-instrumentation
ocelotl Sep 29, 2023
75f2aad
Adding updates from tox -e generate
ocelotl Sep 29, 2023
26393c6
Remove a skiped test.
decko Oct 12, 2023
96300f5
Add LICENSE header.
decko Oct 12, 2023
9809d79
Add LICENSE header and fixes the use of a constant.
decko Oct 12, 2023
fbbe270
Fix linter issues.
decko Oct 12, 2023
58936f5
Merge branch 'main' into aiohttp-server-instrumentation
decko Oct 12, 2023
782e4e8
Change the HTTPMethod Enum.
decko Oct 16, 2023
c22e300
Fixes some tests.
decko Oct 16, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## Unreleased

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

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


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

### Fixed
Expand Down
1 change: 1 addition & 0 deletions instrumentation/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
| --------------- | ------------------ | --------------- |
| [opentelemetry-instrumentation-aio-pika](./opentelemetry-instrumentation-aio-pika) | aio_pika >= 7.2.0, < 10.0.0 | No
| [opentelemetry-instrumentation-aiohttp-client](./opentelemetry-instrumentation-aiohttp-client) | aiohttp ~= 3.0 | No
| [opentelemetry-instrumentation-aiohttp-server](./opentelemetry-instrumentation-aiohttp-server) | aiohttp ~= 3.0 | No
| [opentelemetry-instrumentation-aiopg](./opentelemetry-instrumentation-aiopg) | aiopg >= 0.13.0, < 2.0.0 | No
| [opentelemetry-instrumentation-asgi](./opentelemetry-instrumentation-asgi) | asgiref ~= 3.0 | No
| [opentelemetry-instrumentation-asyncpg](./opentelemetry-instrumentation-asyncpg) | asyncpg >= 0.12.0 | No
Expand Down
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>`_
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 = "cncf-opentelemetry-contributors@lists.cncf.io"}
]
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"]
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
decko marked this conversation as resolved.
Show resolved Hide resolved
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
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",)
Loading