├── docs ├── authors.rst ├── history.rst ├── readme.rst ├── contributing.rst ├── usage.rst ├── index.rst ├── Makefile ├── make.bat ├── installation.rst └── conf.py ├── .pyup.yml ├── tests ├── __init__.py ├── test_middleware.py └── test_trace.py ├── aiohttp_prometheus_exporter ├── __init__.py ├── handler.py ├── middleware.py └── trace.py ├── AUTHORS.rst ├── .github ├── dependabot.yml └── ISSUE_TEMPLATE.md ├── HISTORY.rst ├── .gitignore ├── MANIFEST.in ├── requirements_dev.txt ├── .editorconfig ├── setup.cfg ├── tox.ini ├── .travis.yml ├── LICENSE ├── setup.py ├── Makefile ├── README.rst └── CONTRIBUTING.rst /docs/authors.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../AUTHORS.rst 2 | -------------------------------------------------------------------------------- /docs/history.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../HISTORY.rst 2 | -------------------------------------------------------------------------------- /docs/readme.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../README.rst 2 | -------------------------------------------------------------------------------- /docs/contributing.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../CONTRIBUTING.rst 2 | -------------------------------------------------------------------------------- /.pyup.yml: -------------------------------------------------------------------------------- 1 | schedule: "every day" 2 | 3 | assignees: 4 | - adriankrupa 5 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | """Unit test package for aiohttp_prometheus_exporter.""" 2 | -------------------------------------------------------------------------------- /docs/usage.rst: -------------------------------------------------------------------------------- 1 | ===== 2 | Usage 3 | ===== 4 | 5 | To use aiohttp prometheus exporter in a project:: 6 | 7 | import aiohttp_prometheus_exporter 8 | -------------------------------------------------------------------------------- /aiohttp_prometheus_exporter/__init__.py: -------------------------------------------------------------------------------- 1 | """Top-level package for aiohttp prometheus exporter.""" 2 | 3 | __author__ = """Adrian Krupa""" 4 | __email__ = "adrian.krupa91@gmail.com" 5 | __version__ = "__version__ = '0.2.4'" 6 | -------------------------------------------------------------------------------- /AUTHORS.rst: -------------------------------------------------------------------------------- 1 | ======= 2 | Credits 3 | ======= 4 | 5 | Development Lead 6 | ---------------- 7 | 8 | * Adrian Krupa 9 | 10 | Contributors 11 | ------------ 12 | 13 | None yet. Why not be the first? 14 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: pip 4 | directory: "/" 5 | schedule: 6 | interval: weekly 7 | time: "09:00" 8 | open-pull-requests-limit: 10 9 | assignees: 10 | - adriankrupa 11 | -------------------------------------------------------------------------------- /HISTORY.rst: -------------------------------------------------------------------------------- 1 | History 2 | ======= 3 | 4 | 0.2.4 (2020-04-07) 5 | ------------------ 6 | 7 | * Fixed building problems 8 | * Configured black linter 9 | 10 | 0.1.0 (2020-04-07) 11 | ------------------ 12 | 13 | * First release on PyPI. 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .ipynb_checkpoints 3 | .mypy_cache 4 | .vscode 5 | __pycache__ 6 | .pytest_cache 7 | htmlcov 8 | venv/ 9 | dist 10 | site 11 | .coverage 12 | coverage.xml 13 | .netlify 14 | test.db 15 | log.txt 16 | Pipfile.lock 17 | env3.* 18 | docs_build 19 | .tox/ 20 | *.egg-info 21 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include AUTHORS.rst 2 | include CONTRIBUTING.rst 3 | include HISTORY.rst 4 | include LICENSE 5 | include README.rst 6 | 7 | recursive-include tests * 8 | recursive-exclude * __pycache__ 9 | recursive-exclude * *.py[co] 10 | 11 | recursive-include docs *.rst conf.py Makefile make.bat *.jpg *.png *.gif 12 | -------------------------------------------------------------------------------- /requirements_dev.txt: -------------------------------------------------------------------------------- 1 | pip==21.1.3 2 | bump2version==1.0.1 3 | wheel==0.36.2 4 | watchdog==2.1.3 5 | flake8==3.9.2 6 | tox==3.23.1 7 | coverage==5.5 8 | Sphinx==3.5.4 9 | twine==3.4.1 10 | black==21.9b0 11 | 12 | pytest==6.2.4 13 | pytest-runner==5.3.0 14 | pytest-cov==2.12.0 15 | pytest-aiohttp==0.3.0 16 | uvloop==0.15.2 17 | 18 | aiohttp==3.7.4 19 | prometheus_client==0.10.1 20 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | indent_style = space 7 | indent_size = 4 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | charset = utf-8 11 | end_of_line = lf 12 | 13 | [*.bat] 14 | indent_style = tab 15 | end_of_line = crlf 16 | 17 | [LICENSE] 18 | insert_final_newline = false 19 | 20 | [Makefile] 21 | indent_style = tab 22 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | * aiohttp prometheus exporter version: 2 | * Python version: 3 | * Operating System: 4 | 5 | ### Description 6 | 7 | Describe what you were trying to get done. 8 | Tell us what happened, what went wrong, and what you expected to happen. 9 | 10 | ### What I Did 11 | 12 | ``` 13 | Paste the command(s) you ran and the output. 14 | If there was a crash, please include the traceback here. 15 | ``` 16 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | Welcome to aiohttp prometheus exporter's documentation! 2 | ====================================== 3 | 4 | .. toctree:: 5 | :maxdepth: 2 6 | :caption: Contents: 7 | 8 | readme 9 | installation 10 | usage 11 | modules 12 | contributing 13 | authors 14 | history 15 | 16 | Indices and tables 17 | ================== 18 | * :ref:`genindex` 19 | * :ref:`modindex` 20 | * :ref:`search` 21 | -------------------------------------------------------------------------------- /aiohttp_prometheus_exporter/handler.py: -------------------------------------------------------------------------------- 1 | import prometheus_client 2 | from aiohttp import web 3 | 4 | 5 | def metrics(registry: prometheus_client.CollectorRegistry = None): 6 | async def handler(_): 7 | prom_registry = registry if registry else prometheus_client.REGISTRY 8 | 9 | response = web.Response(body=prometheus_client.generate_latest(prom_registry)) 10 | response.content_type = prometheus_client.CONTENT_TYPE_LATEST 11 | return response 12 | 13 | return handler 14 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bumpversion] 2 | current_version = 0.2.4 3 | commit = True 4 | tag = True 5 | 6 | [bumpversion:file:setup.py] 7 | search = version='{current_version}' 8 | replace = version='{new_version}' 9 | 10 | [bumpversion:file:aiohttp_prometheus_exporter/__init__.py] 11 | search = __version__ = '{current_version}' 12 | replace = __version__ = '{new_version}' 13 | 14 | [bdist_wheel] 15 | universal = 1 16 | 17 | [flake8] 18 | exclude = docs 19 | 20 | [aliases] 21 | test = pytest 22 | 23 | [tool:pytest] 24 | collect_ignore = ['setup.py'] 25 | addopts = --cov=aiohttp_prometheus_exporter --cov-branch --cov-report term-missing --cov-report xml --cov-fail-under=100 --aiohttp-loop=all 26 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = python -msphinx 7 | SPHINXPROJ = aiohttp_prometheus_exporter 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py37, py38, py39, docs, lint 3 | 4 | [travis] 5 | python = 6 | 3.9: py39, docs, lint 7 | 3.8: py38, docs, lint 8 | 3.7: py37, docs, lint 9 | 10 | [testenv:docs] 11 | setenv = 12 | PYTHONPATH = {toxinidir} 13 | deps = 14 | -r{toxinidir}/requirements_dev.txt 15 | commands = 16 | python setup.py sdist 17 | twine check dist/* 18 | 19 | [testenv:lint] 20 | setenv = 21 | PYTHONPATH = {toxinidir} 22 | deps = 23 | -r{toxinidir}/requirements_dev.txt 24 | commands = 25 | black --check aiohttp_prometheus_exporter tests 26 | 27 | [testenv] 28 | setenv = 29 | PYTHONPATH = {toxinidir} 30 | deps = 31 | -r{toxinidir}/requirements_dev.txt 32 | commands = 33 | pip install -U pip 34 | pytest --basetemp={envtmpdir} 35 | 36 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=python -msphinx 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | set SPHINXPROJ=aiohttp_prometheus_exporter 13 | 14 | if "%1" == "" goto help 15 | 16 | %SPHINXBUILD% >NUL 2>NUL 17 | if errorlevel 9009 ( 18 | echo. 19 | echo.The Sphinx module was not found. Make sure you have Sphinx installed, 20 | echo.then set the SPHINXBUILD environment variable to point to the full 21 | echo.path of the 'sphinx-build' executable. Alternatively you may add the 22 | echo.Sphinx directory to PATH. 23 | echo. 24 | echo.If you don't have Sphinx installed, grab it from 25 | echo.http://sphinx-doc.org/ 26 | exit /b 1 27 | ) 28 | 29 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 30 | goto end 31 | 32 | :help 33 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 34 | 35 | :end 36 | popd 37 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - 3.9 4 | - 3.8 5 | - 3.7 6 | install: pip install -U tox-travis codecov 7 | script: tox 8 | after_success: 9 | - codecov 10 | deploy: 11 | provider: pypi 12 | distributions: sdist bdist_wheel 13 | user: __token__ 14 | password: 15 | secure: q1ikIL4YgcCaXdlzzbXtq2tMYQgBJVIoaC3rwKaDyziT5OhyvSjXOUP5VtEnGufXo5jZKdHrKcDgx2y0FTAEb5NWmb4zzqwDiG0yAUCwiASK4JKBrGgcuSOlTRSwT6HlT+lhi1yFyj96Qu0HfDhUOIsC6G+UiLvFAf0ysCHP6FFacul/pkahJsfyBqQi1er6M1sLRxEwLCiSnc7SY6MC8oJyRfkFxV7gvoSCxXVFs7QwYVGHICqFZbkd/+kJvRSz5heO5OdM0Pb7P/2iQbU4agojwUJlrzSrOvr+ck05ra9b2xOMZxWVHQtr1A2mjfDgT9wieVW082xTV3XnNRWCp7i7fWUEOTQypQyxBQIK0ajaEG5eXbAkVCVQA/tU7InFNAoNeg1HgwBsWwZQU56yBcL0VI16uH3tMOGI+JWIax2Z6HtTvHBGbV4noicLPbqU8QcT5dIBVmnBBh+A6SxoS2kh9ojRowdCYyV5Ac5FwaJTXi4nxVuOIf1Pgb7J/7H7jV9s+LoWkbAnt2m/ZV5FnEL8Uho0CNmgeIMI8Q+fF8HJlfVAIaQae6l3LKT37nCkZ/hXPcOp9X3HFk+8+V4kItm3KE6rmuvpeWkgUVQlpurLvTvHlz5zolOYOYnUzzA7omYlFbZeLkJC8ZYr8U9HOYfmt13T9xljYCicwnD1IpE= 16 | on: 17 | tags: true 18 | repo: adriankrupa/aiohttp_prometheus_exporter 19 | python: 3.9 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020, Adrian Krupa 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /docs/installation.rst: -------------------------------------------------------------------------------- 1 | .. highlight:: shell 2 | 3 | ============ 4 | Installation 5 | ============ 6 | 7 | 8 | Stable release 9 | -------------- 10 | 11 | To install aiohttp prometheus exporter, run this command in your terminal: 12 | 13 | .. code-block:: console 14 | 15 | $ pip install aiohttp_prometheus_exporter 16 | 17 | This is the preferred method to install aiohttp prometheus exporter, as it will always install the most recent stable release. 18 | 19 | If you don't have `pip`_ installed, this `Python installation guide`_ can guide 20 | you through the process. 21 | 22 | .. _pip: https://pip.pypa.io 23 | .. _Python installation guide: http://docs.python-guide.org/en/latest/starting/installation/ 24 | 25 | 26 | From sources 27 | ------------ 28 | 29 | The sources for aiohttp prometheus exporter can be downloaded from the `Github repo`_. 30 | 31 | You can either clone the public repository: 32 | 33 | .. code-block:: console 34 | 35 | $ git clone git://github.com/adriankrupa/aiohttp_prometheus_exporter 36 | 37 | Or download the `tarball`_: 38 | 39 | .. code-block:: console 40 | 41 | $ curl -OJL https://github.com/adriankrupa/aiohttp_prometheus_exporter/tarball/master 42 | 43 | Once you have a copy of the source, you can install it with: 44 | 45 | .. code-block:: console 46 | 47 | $ python setup.py install 48 | 49 | 50 | .. _Github repo: https://github.com/adriankrupa/aiohttp_prometheus_exporter 51 | .. _tarball: https://github.com/adriankrupa/aiohttp_prometheus_exporter/tarball/master 52 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """The setup script.""" 4 | 5 | from setuptools import setup, find_packages 6 | 7 | with open('README.rst') as readme_file: 8 | readme = readme_file.read() 9 | 10 | with open('HISTORY.rst') as history_file: 11 | history = history_file.read() 12 | 13 | requirements = ['aiohttp>=3', 'prometheus_client>=0.6', ] 14 | 15 | setup_requirements = ['pytest-runner', ] 16 | 17 | test_requirements = ['pytest>=3', ] 18 | 19 | setup( 20 | author="Adrian Krupa", 21 | author_email='adrian.krupa91@gmail.com', 22 | python_requires='>=3.7', 23 | classifiers=[ 24 | 'Development Status :: 2 - Pre-Alpha', 25 | 'Intended Audience :: Developers', 26 | 'License :: OSI Approved :: MIT License', 27 | 'Natural Language :: English', 28 | 'Programming Language :: Python :: 3', 29 | 'Programming Language :: Python :: 3.7', 30 | 'Programming Language :: Python :: 3.8', 31 | 'Programming Language :: Python :: 3.9', 32 | ], 33 | description="Prometheus exporter for aiohttp server and client.", 34 | install_requires=requirements, 35 | license="MIT license", 36 | long_description=readme + '\n\n' + history, 37 | long_description_content_type='text/x-rst', 38 | include_package_data=True, 39 | keywords='aiohttp_prometheus_exporter', 40 | name='aiohttp_prometheus_exporter', 41 | packages=find_packages(include=['aiohttp_prometheus_exporter', 'aiohttp_prometheus_exporter.*']), 42 | setup_requires=setup_requirements, 43 | test_suite='tests', 44 | tests_require=test_requirements, 45 | url='https://github.com/adriankrupa/aiohttp_prometheus_exporter', 46 | version='0.2.4', 47 | zip_safe=False, 48 | ) 49 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: clean clean-test clean-pyc clean-build docs help 2 | .DEFAULT_GOAL := help 3 | 4 | define BROWSER_PYSCRIPT 5 | import os, webbrowser, sys 6 | 7 | from urllib.request import pathname2url 8 | 9 | webbrowser.open("file://" + pathname2url(os.path.abspath(sys.argv[1]))) 10 | endef 11 | export BROWSER_PYSCRIPT 12 | 13 | define PRINT_HELP_PYSCRIPT 14 | import re, sys 15 | 16 | for line in sys.stdin: 17 | match = re.match(r'^([a-zA-Z_-]+):.*?## (.*)$$', line) 18 | if match: 19 | target, help = match.groups() 20 | print("%-20s %s" % (target, help)) 21 | endef 22 | export PRINT_HELP_PYSCRIPT 23 | 24 | BROWSER := python -c "$$BROWSER_PYSCRIPT" 25 | 26 | help: 27 | @python -c "$$PRINT_HELP_PYSCRIPT" < $(MAKEFILE_LIST) 28 | 29 | clean: clean-build clean-pyc clean-test ## remove all build, test, coverage and Python artifacts 30 | 31 | clean-build: ## remove build artifacts 32 | rm -fr build/ 33 | rm -fr dist/ 34 | rm -fr .eggs/ 35 | find . -name '*.egg-info' -exec rm -fr {} + 36 | find . -name '*.egg' -exec rm -f {} + 37 | 38 | clean-pyc: ## remove Python file artifacts 39 | find . -name '*.pyc' -exec rm -f {} + 40 | find . -name '*.pyo' -exec rm -f {} + 41 | find . -name '*~' -exec rm -f {} + 42 | find . -name '__pycache__' -exec rm -fr {} + 43 | 44 | clean-test: ## remove test and coverage artifacts 45 | rm -fr .tox/ 46 | rm -f .coverage 47 | rm -fr htmlcov/ 48 | rm -fr .pytest_cache 49 | 50 | lint: ## check style with flake8 51 | flake8 aiohttp_prometheus_exporter tests 52 | 53 | test: ## run tests quickly with the default Python 54 | pytest 55 | 56 | test-all: ## run tests on every Python version with tox 57 | tox 58 | 59 | coverage: ## check code coverage quickly with the default Python 60 | coverage run --source aiohttp_prometheus_exporter -m pytest 61 | coverage report -m 62 | coverage html 63 | $(BROWSER) htmlcov/index.html 64 | 65 | docs: ## generate Sphinx HTML documentation, including API docs 66 | rm -f docs/aiohttp_prometheus_exporter.rst 67 | rm -f docs/modules.rst 68 | sphinx-apidoc -o docs/ aiohttp_prometheus_exporter 69 | $(MAKE) -C docs clean 70 | $(MAKE) -C docs html 71 | $(BROWSER) docs/_build/html/index.html 72 | 73 | servedocs: docs ## compile the docs watching for changes 74 | watchmedo shell-command -p '*.rst' -c '$(MAKE) -C docs html' -R -D . 75 | 76 | release: dist ## package and upload a release 77 | twine upload dist/* 78 | 79 | dist: clean ## builds source and wheel package 80 | python setup.py sdist 81 | python setup.py bdist_wheel 82 | ls -l dist 83 | 84 | install: clean ## install the package to the active Python's site-packages 85 | python setup.py install 86 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | =========================== 2 | aiohttp prometheus exporter 3 | =========================== 4 | 5 | 6 | .. image:: https://img.shields.io/pypi/v/aiohttp_prometheus_exporter.svg 7 | :target: https://pypi.python.org/pypi/aiohttp_prometheus_exporter 8 | 9 | .. image:: https://img.shields.io/travis/adriankrupa/aiohttp_prometheus_exporter.svg 10 | :target: https://travis-ci.org/adriankrupa/aiohttp_prometheus_exporter 11 | 12 | .. image:: https://img.shields.io/pypi/pyversions/aiohttp_prometheus_exporter.svg 13 | :target: https://pypi.python.org/pypi/aiohttp_prometheus_exporter 14 | 15 | .. image:: https://img.shields.io/pypi/dm/aiohttp_prometheus_exporter.svg 16 | :target: https://pypi.python.org/pypi/aiohttp_prometheus_exporter 17 | 18 | .. image:: https://codecov.io/gh/adriankrupa/aiohttp_prometheus_exporter/branch/master/graph/badge.svg 19 | :target: https://codecov.io/gh/adriankrupa/aiohttp_prometheus_exporter 20 | 21 | .. image:: https://img.shields.io/badge/code%20style-black-000000.svg 22 | :target: https://github.com/psf/black 23 | :alt: Black 24 | 25 | .. image:: https://readthedocs.org/projects/aiohttp-prometheus-exporter/badge/?version=latest 26 | :target: https://aiohttp-prometheus-exporter.readthedocs.io/en/latest/?badge=latest 27 | :alt: Documentation Status 28 | 29 | .. image:: https://pyup.io/repos/github/adriankrupa/aiohttp_prometheus_exporter/shield.svg 30 | :target: https://pyup.io/repos/github/adriankrupa/aiohttp_prometheus_exporter/ 31 | :alt: Updates 32 | 33 | Export aiohttp metrics for Prometheus.io 34 | 35 | Usage 36 | ***** 37 | 38 | Requirements 39 | ------------ 40 | 41 | * aiohttp >= 3 42 | 43 | Installation 44 | ------------ 45 | 46 | Install with: 47 | 48 | .. code-block:: shell 49 | 50 | pip install aiohttp-prometheus-exporter 51 | 52 | Server quickstart 53 | ----------------- 54 | 55 | .. code-block:: python 56 | 57 | from aiohttp import web 58 | from aiohttp_prometheus_exporter.handler import metrics 59 | from aiohttp_prometheus_exporter.middleware import prometheus_middleware_factory 60 | 61 | async def hello(request): 62 | return web.Response(text="Hello, world") 63 | 64 | app = web.Application() 65 | app.add_routes([web.get('/', hello)]) 66 | 67 | app.middlewares.append(prometheus_middleware_factory()) 68 | app.router.add_get("/metrics", metrics()) 69 | 70 | web.run_app(app) 71 | 72 | Client quickstart 73 | ----------------- 74 | 75 | .. code-block:: python 76 | 77 | import aiohttp 78 | from aiohttp_prometheus_exporter.trace import PrometheusTraceConfig 79 | 80 | async with aiohttp.ClientSession(trace_configs=[PrometheusTraceConfig()) as session: 81 | async with session.get('http://httpbin.org/get') as resp: 82 | print(resp.status) 83 | print(await resp.text()) 84 | 85 | Now, client metrics are attached to metrics exposed by your web server. 86 | 87 | Credits 88 | ------- 89 | 90 | This package was created with Cookiecutter_ and the `audreyr/cookiecutter-pypackage`_ project template. 91 | 92 | .. _Cookiecutter: https://github.com/audreyr/cookiecutter 93 | .. _`audreyr/cookiecutter-pypackage`: https://github.com/audreyr/cookiecutter-pypackage 94 | -------------------------------------------------------------------------------- /CONTRIBUTING.rst: -------------------------------------------------------------------------------- 1 | .. highlight:: shell 2 | 3 | ============ 4 | Contributing 5 | ============ 6 | 7 | Contributions are welcome, and they are greatly appreciated! Every little bit 8 | helps, and credit will always be given. 9 | 10 | You can contribute in many ways: 11 | 12 | Types of Contributions 13 | ---------------------- 14 | 15 | Report Bugs 16 | ~~~~~~~~~~~ 17 | 18 | Report bugs at https://github.com/adriankrupa/aiohttp_prometheus_exporter/issues. 19 | 20 | If you are reporting a bug, please include: 21 | 22 | * Your operating system name and version. 23 | * Any details about your local setup that might be helpful in troubleshooting. 24 | * Detailed steps to reproduce the bug. 25 | 26 | Fix Bugs 27 | ~~~~~~~~ 28 | 29 | Look through the GitHub issues for bugs. Anything tagged with "bug" and "help 30 | wanted" is open to whoever wants to implement it. 31 | 32 | Implement Features 33 | ~~~~~~~~~~~~~~~~~~ 34 | 35 | Look through the GitHub issues for features. Anything tagged with "enhancement" 36 | and "help wanted" is open to whoever wants to implement it. 37 | 38 | Write Documentation 39 | ~~~~~~~~~~~~~~~~~~~ 40 | 41 | aiohttp prometheus exporter could always use more documentation, whether as part of the 42 | official aiohttp prometheus exporter docs, in docstrings, or even on the web in blog posts, 43 | articles, and such. 44 | 45 | Submit Feedback 46 | ~~~~~~~~~~~~~~~ 47 | 48 | The best way to send feedback is to file an issue at https://github.com/adriankrupa/aiohttp_prometheus_exporter/issues. 49 | 50 | If you are proposing a feature: 51 | 52 | * Explain in detail how it would work. 53 | * Keep the scope as narrow as possible, to make it easier to implement. 54 | * Remember that this is a volunteer-driven project, and that contributions 55 | are welcome :) 56 | 57 | Get Started! 58 | ------------ 59 | 60 | Ready to contribute? Here's how to set up `aiohttp_prometheus_exporter` for local development. 61 | 62 | 1. Fork the `aiohttp_prometheus_exporter` repo on GitHub. 63 | 2. Clone your fork locally:: 64 | 65 | $ git clone git@github.com:your_name_here/aiohttp_prometheus_exporter.git 66 | 67 | 3. Install your local copy into a virtualenv. Assuming you have virtualenvwrapper installed, this is how you set up your fork for local development:: 68 | 69 | $ mkvirtualenv aiohttp_prometheus_exporter 70 | $ cd aiohttp_prometheus_exporter/ 71 | $ python setup.py develop 72 | 73 | 4. Create a branch for local development:: 74 | 75 | $ git checkout -b name-of-your-bugfix-or-feature 76 | 77 | Now you can make your changes locally. 78 | 79 | 5. When you're done making changes, check that your changes pass flake8 and the 80 | tests, including testing other Python versions with tox:: 81 | 82 | $ flake8 aiohttp_prometheus_exporter tests 83 | $ python setup.py test or pytest 84 | $ tox 85 | 86 | To get flake8 and tox, just pip install them into your virtualenv. 87 | 88 | 6. Commit your changes and push your branch to GitHub:: 89 | 90 | $ git add . 91 | $ git commit -m "Your detailed description of your changes." 92 | $ git push origin name-of-your-bugfix-or-feature 93 | 94 | 7. Submit a pull request through the GitHub website. 95 | 96 | Pull Request Guidelines 97 | ----------------------- 98 | 99 | Before you submit a pull request, check that it meets these guidelines: 100 | 101 | 1. The pull request should include tests. 102 | 2. If the pull request adds functionality, the docs should be updated. Put 103 | your new functionality into a function with a docstring, and add the 104 | feature to the list in README.rst. 105 | 3. The pull request should work for Python 3.7, 3.8 and 3.9 and for PyPy. Check 106 | https://travis-ci.com/adriankrupa/aiohttp_prometheus_exporter/pull_requests 107 | and make sure that the tests pass for all supported Python versions. 108 | 109 | Tips 110 | ---- 111 | 112 | To run a subset of tests:: 113 | 114 | $ pytest tests.test_aiohttp_prometheus_exporter 115 | 116 | 117 | Deploying 118 | --------- 119 | 120 | A reminder for the maintainers on how to deploy. 121 | Make sure all your changes are committed (including an entry in HISTORY.rst). 122 | Then run:: 123 | 124 | $ bump2version patch # possible: major / minor / patch 125 | $ git push 126 | $ git push --tags 127 | 128 | Travis will then deploy to PyPI if tests pass. 129 | -------------------------------------------------------------------------------- /aiohttp_prometheus_exporter/middleware.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from typing import Callable, Awaitable 3 | 4 | import prometheus_client 5 | from aiohttp.web_exceptions import HTTPException 6 | from aiohttp.web_middlewares import middleware 7 | from aiohttp.web_request import Request 8 | from aiohttp.web_response import Response 9 | 10 | 11 | def get_loop(): 12 | return getattr(asyncio, "get_running_loop", asyncio.get_event_loop)() 13 | 14 | 15 | def prometheus_middleware_factory( 16 | metrics_prefix="aiohttp", registry: prometheus_client.CollectorRegistry = None 17 | ): 18 | used_registry = registry if registry else prometheus_client.REGISTRY 19 | 20 | requests_metrics = prometheus_client.Counter( 21 | name=f"{metrics_prefix}_requests", 22 | documentation="Total requests by method, scheme, remote and path template.", 23 | labelnames=["method", "scheme", "remote", "path_template"], 24 | registry=used_registry, 25 | ) 26 | 27 | responses_metrics = prometheus_client.Counter( 28 | name=f"{metrics_prefix}_responses", 29 | documentation="Total responses by method, scheme, remote, path template and status code.", 30 | labelnames=["method", "scheme", "remote", "path_template", "status_code"], 31 | registry=used_registry, 32 | ) 33 | 34 | requests_processing_time_metrics = prometheus_client.Histogram( 35 | name=f"{metrics_prefix}_request_duration", 36 | documentation="Histogram of requests processing time by method, " 37 | "scheme, remote, path template and status code (in seconds)", 38 | labelnames=["method", "scheme", "remote", "path_template", "status_code"], 39 | unit="seconds", 40 | registry=used_registry, 41 | ) 42 | 43 | requests_in_progress_metrics = prometheus_client.Gauge( 44 | name=f"{metrics_prefix}_requests_in_progress", 45 | documentation="Gauge of requests by method, scheme, remote and path template currently being processed.", 46 | labelnames=["method", "scheme", "remote", "path_template"], 47 | registry=used_registry, 48 | ) 49 | 50 | exceptions_metrics = prometheus_client.Counter( 51 | name=f"{metrics_prefix}_exceptions", 52 | documentation="Total exceptions raised by path, scheme, remote, path template and exception type.", 53 | labelnames=["method", "scheme", "remote", "path_template", "exception_type"], 54 | registry=used_registry, 55 | ) 56 | 57 | @middleware 58 | async def prometheus_middleware( 59 | request: Request, handler: Callable[[Request], Awaitable[Response]] 60 | ): 61 | loop = get_loop() 62 | 63 | try: 64 | path_template = request.match_info.route.resource.canonical 65 | except AttributeError: 66 | path_template = "__not_matched__" 67 | 68 | requests_metrics.labels( 69 | method=request.method, 70 | scheme=request.scheme, 71 | remote=request.remote, 72 | path_template=path_template, 73 | ).inc() 74 | requests_in_progress_metrics.labels( 75 | method=request.method, 76 | scheme=request.scheme, 77 | remote=request.remote, 78 | path_template=path_template, 79 | ).inc() 80 | 81 | request_start_time = loop.time() 82 | try: 83 | response = await handler(request) 84 | request_end_time = loop.time() 85 | 86 | except Exception as e: 87 | request_end_time = loop.time() 88 | status = e.status if isinstance(e, HTTPException) else 500 89 | 90 | responses_metrics.labels( 91 | method=request.method, 92 | scheme=request.scheme, 93 | remote=request.remote, 94 | path_template=path_template, 95 | status_code=status, 96 | ).inc() 97 | exceptions_metrics.labels( 98 | method=request.method, 99 | scheme=request.scheme, 100 | remote=request.remote, 101 | path_template=path_template, 102 | exception_type=type(e).__name__, 103 | ).inc() 104 | requests_processing_time_metrics.labels( 105 | method=request.method, 106 | scheme=request.scheme, 107 | remote=request.remote, 108 | path_template=path_template, 109 | status_code=status, 110 | ).observe(request_end_time - request_start_time) 111 | raise e from None 112 | else: 113 | responses_metrics.labels( 114 | method=request.method, 115 | scheme=request.scheme, 116 | remote=request.remote, 117 | path_template=path_template, 118 | status_code=response.status, 119 | ).inc() 120 | requests_processing_time_metrics.labels( 121 | method=request.method, 122 | scheme=request.scheme, 123 | remote=request.remote, 124 | path_template=path_template, 125 | status_code=response.status, 126 | ).observe(request_end_time - request_start_time) 127 | finally: 128 | requests_in_progress_metrics.labels( 129 | method=request.method, 130 | scheme=request.scheme, 131 | remote=request.remote, 132 | path_template=path_template, 133 | ).dec() 134 | return response 135 | 136 | return prometheus_middleware 137 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # aiohttp_prometheus_exporter documentation build configuration file, created by 4 | # sphinx-quickstart on Fri Jun 9 13:47:02 2017. 5 | # 6 | # This file is execfile()d with the current directory set to its 7 | # containing dir. 8 | # 9 | # Note that not all possible configuration values are present in this 10 | # autogenerated file. 11 | # 12 | # All configuration values have a default; values that are commented out 13 | # serve to show the default. 14 | 15 | # If extensions (or modules to document with autodoc) are in another 16 | # directory, add these directories to sys.path here. If the directory is 17 | # relative to the documentation root, use os.path.abspath to make it 18 | # absolute, like shown here. 19 | # 20 | import os 21 | import sys 22 | sys.path.insert(0, os.path.abspath('..')) 23 | 24 | import aiohttp_prometheus_exporter 25 | 26 | # -- General configuration --------------------------------------------- 27 | 28 | # If your documentation needs a minimal Sphinx version, state it here. 29 | # 30 | # needs_sphinx = '1.0' 31 | 32 | # Add any Sphinx extension module names here, as strings. They can be 33 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 34 | extensions = ['sphinx.ext.autodoc', 'sphinx.ext.viewcode'] 35 | 36 | # Add any paths that contain templates here, relative to this directory. 37 | templates_path = ['_templates'] 38 | 39 | # The suffix(es) of source filenames. 40 | # You can specify multiple suffix as a list of string: 41 | # 42 | # source_suffix = ['.rst', '.md'] 43 | source_suffix = '.rst' 44 | 45 | # The master toctree document. 46 | master_doc = 'index' 47 | 48 | # General information about the project. 49 | project = 'aiohttp prometheus exporter' 50 | copyright = "2020, Adrian Krupa" 51 | author = "Adrian Krupa" 52 | 53 | # The version info for the project you're documenting, acts as replacement 54 | # for |version| and |release|, also used in various other places throughout 55 | # the built documents. 56 | # 57 | # The short X.Y version. 58 | version = aiohttp_prometheus_exporter.__version__ 59 | # The full version, including alpha/beta/rc tags. 60 | release = aiohttp_prometheus_exporter.__version__ 61 | 62 | # The language for content autogenerated by Sphinx. Refer to documentation 63 | # for a list of supported languages. 64 | # 65 | # This is also used if you do content translation via gettext catalogs. 66 | # Usually you set "language" from the command line for these cases. 67 | language = None 68 | 69 | # List of patterns, relative to source directory, that match files and 70 | # directories to ignore when looking for source files. 71 | # This patterns also effect to html_static_path and html_extra_path 72 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 73 | 74 | # The name of the Pygments (syntax highlighting) style to use. 75 | pygments_style = 'sphinx' 76 | 77 | # If true, `todo` and `todoList` produce output, else they produce nothing. 78 | todo_include_todos = False 79 | 80 | 81 | # -- Options for HTML output ------------------------------------------- 82 | 83 | # The theme to use for HTML and HTML Help pages. See the documentation for 84 | # a list of builtin themes. 85 | # 86 | html_theme = 'alabaster' 87 | 88 | # Theme options are theme-specific and customize the look and feel of a 89 | # theme further. For a list of options available for each theme, see the 90 | # documentation. 91 | # 92 | # html_theme_options = {} 93 | 94 | # Add any paths that contain custom static files (such as style sheets) here, 95 | # relative to this directory. They are copied after the builtin static files, 96 | # so a file named "default.css" will overwrite the builtin "default.css". 97 | html_static_path = ['_static'] 98 | 99 | 100 | # -- Options for HTMLHelp output --------------------------------------- 101 | 102 | # Output file base name for HTML help builder. 103 | htmlhelp_basename = 'aiohttp_prometheus_exporterdoc' 104 | 105 | 106 | # -- Options for LaTeX output ------------------------------------------ 107 | 108 | latex_elements = { 109 | # The paper size ('letterpaper' or 'a4paper'). 110 | # 111 | # 'papersize': 'letterpaper', 112 | 113 | # The font size ('10pt', '11pt' or '12pt'). 114 | # 115 | # 'pointsize': '10pt', 116 | 117 | # Additional stuff for the LaTeX preamble. 118 | # 119 | # 'preamble': '', 120 | 121 | # Latex figure (float) alignment 122 | # 123 | # 'figure_align': 'htbp', 124 | } 125 | 126 | # Grouping the document tree into LaTeX files. List of tuples 127 | # (source start file, target name, title, author, documentclass 128 | # [howto, manual, or own class]). 129 | latex_documents = [ 130 | (master_doc, 'aiohttp_prometheus_exporter.tex', 131 | 'aiohttp prometheus exporter Documentation', 132 | 'Adrian Krupa', 'manual'), 133 | ] 134 | 135 | 136 | # -- Options for manual page output ------------------------------------ 137 | 138 | # One entry per manual page. List of tuples 139 | # (source start file, name, description, authors, manual section). 140 | man_pages = [ 141 | (master_doc, 'aiohttp_prometheus_exporter', 142 | 'aiohttp prometheus exporter Documentation', 143 | [author], 1) 144 | ] 145 | 146 | 147 | # -- Options for Texinfo output ---------------------------------------- 148 | 149 | # Grouping the document tree into Texinfo files. List of tuples 150 | # (source start file, target name, title, author, 151 | # dir menu entry, description, category) 152 | texinfo_documents = [ 153 | (master_doc, 'aiohttp_prometheus_exporter', 154 | 'aiohttp prometheus exporter Documentation', 155 | author, 156 | 'aiohttp_prometheus_exporter', 157 | 'One line description of project.', 158 | 'Miscellaneous'), 159 | ] 160 | 161 | 162 | 163 | -------------------------------------------------------------------------------- /tests/test_middleware.py: -------------------------------------------------------------------------------- 1 | import typing 2 | 3 | import prometheus_client 4 | import pytest 5 | from aiohttp import web 6 | from aiohttp.test_utils import TestClient 7 | from aiohttp.web_response import json_response 8 | from prometheus_client import parser 9 | 10 | from aiohttp_prometheus_exporter.handler import metrics 11 | from aiohttp_prometheus_exporter.middleware import prometheus_middleware_factory 12 | 13 | 14 | @pytest.fixture 15 | def app(): 16 | """ create a test app with various endpoints for the test scenarios """ 17 | app = web.Application() 18 | routes = web.RouteTableDef() 19 | 20 | registry = prometheus_client.CollectorRegistry() 21 | 22 | app.middlewares.append(prometheus_middleware_factory(registry=registry)) 23 | app.router.add_get("/metrics", metrics(registry=registry)) 24 | 25 | @routes.get("/200") 26 | async def response_200(_): 27 | return json_response({"message": "Hello World"}) 28 | 29 | @routes.get("/exception") 30 | async def response_exception(_): 31 | raise ValueError("Error") 32 | 33 | @routes.get("/path/{value}") 34 | async def response_detail(request): 35 | return json_response({"message": f"Hello {request.match_info['value']}"}) 36 | 37 | app.router.add_routes(routes) 38 | 39 | # 40 | # @app.route("/500") 41 | # async def error(request): 42 | # raise HTTPException(status_code=500, detail="this is a test error") 43 | # 44 | # @app.route("/unhandled") 45 | # async def unhandled(request): 46 | # test_dict = {"yup": 123} 47 | # return JSONResponse({"message": test_dict["value_error"]}) 48 | 49 | yield app 50 | 51 | 52 | class TestMiddleware: 53 | @pytest.fixture() 54 | async def client(self, aiohttp_client, app) -> TestClient: 55 | return await aiohttp_client(app) 56 | 57 | @pytest.mark.parametrize( 58 | "path,path_template,server_response", 59 | [ 60 | ("/200", "/200", 200), 61 | ("/path/123", "/path/{value}", 200), 62 | ("/random_path", "__not_matched__", 404), 63 | ], 64 | ) 65 | async def test_ok( 66 | self, client: TestClient, path: str, path_template: str, server_response: int 67 | ): 68 | resp = await client.get(path) 69 | assert resp.status == server_response 70 | 71 | metrics_response = await client.get("/metrics") 72 | assert metrics_response.status == 200 73 | 74 | metrics_text = await metrics_response.text() 75 | 76 | families = text_string_to_metric_families_map(metrics_text) 77 | 78 | assert_entry_exist( 79 | families, 80 | "aiohttp_requests", 81 | { 82 | "method": "GET", 83 | "path_template": path_template, 84 | "remote": "127.0.0.1", 85 | "scheme": "http", 86 | }, 87 | 1.0, 88 | ) 89 | 90 | assert_entry_exist( 91 | families, 92 | "aiohttp_responses", 93 | { 94 | "method": "GET", 95 | "path_template": path_template, 96 | "remote": "127.0.0.1", 97 | "scheme": "http", 98 | "status_code": f"{server_response}", 99 | }, 100 | 1.0, 101 | ) 102 | 103 | assert_entry_exist( 104 | families, 105 | "aiohttp_requests_in_progress", 106 | { 107 | "method": "GET", 108 | "path_template": path_template, 109 | "remote": "127.0.0.1", 110 | "scheme": "http", 111 | }, 112 | 0.0, 113 | ) 114 | 115 | assert_entry_exist( 116 | families, 117 | "aiohttp_requests_in_progress", 118 | { 119 | "method": "GET", 120 | "path_template": path_template, 121 | "remote": "127.0.0.1", 122 | "scheme": "http", 123 | }, 124 | 0.0, 125 | ) 126 | 127 | assert_entry_exist( 128 | families, 129 | "aiohttp_request_duration_seconds", 130 | { 131 | "method": "GET", 132 | "path_template": path_template, 133 | "remote": "127.0.0.1", 134 | "scheme": "http", 135 | }, 136 | ) 137 | 138 | @pytest.mark.parametrize( 139 | "path,path_template,server_response", [("/exception", "/exception", 500)] 140 | ) 141 | async def test_exception( 142 | self, client: TestClient, path: str, path_template: str, server_response: int 143 | ): 144 | resp = await client.get("/exception") 145 | assert resp.status == server_response 146 | 147 | metrics_response = await client.get("/metrics") 148 | assert metrics_response.status == 200 149 | 150 | metrics_text = await metrics_response.text() 151 | 152 | families = text_string_to_metric_families_map(metrics_text) 153 | 154 | assert_entry_exist( 155 | families, 156 | "aiohttp_requests", 157 | { 158 | "method": "GET", 159 | "path_template": "/exception", 160 | "remote": "127.0.0.1", 161 | "scheme": "http", 162 | }, 163 | 1.0, 164 | ) 165 | 166 | assert_entry_exist( 167 | families, 168 | "aiohttp_requests_in_progress", 169 | { 170 | "method": "GET", 171 | "path_template": "/exception", 172 | "remote": "127.0.0.1", 173 | "scheme": "http", 174 | }, 175 | 0.0, 176 | ) 177 | 178 | assert_entry_exist( 179 | families, 180 | "aiohttp_exceptions", 181 | { 182 | "method": "GET", 183 | "path_template": "/exception", 184 | "remote": "127.0.0.1", 185 | "scheme": "http", 186 | "exception_type": "ValueError", 187 | }, 188 | 1.0, 189 | ) 190 | 191 | 192 | def assert_entry_exist( 193 | families: typing.Mapping[str, prometheus_client.Metric], 194 | metric_name: str, 195 | labels: typing.Mapping[str, str], 196 | value: float = None, 197 | ): 198 | metric = families[metric_name] 199 | 200 | samples = [ 201 | s 202 | for s in metric.samples 203 | if all( 204 | (label in s.labels and s.labels[label] == label_value) 205 | for label, label_value in labels.items() 206 | ) 207 | ] 208 | 209 | if value is not None: 210 | assert any(sample.value == value for sample in samples) 211 | 212 | assert samples 213 | 214 | 215 | def text_string_to_metric_families_map( 216 | text, 217 | ) -> typing.Mapping[str, prometheus_client.Metric]: 218 | families: typing.Generator[ 219 | prometheus_client.Metric 220 | ] = parser.text_string_to_metric_families(text) 221 | return {f.name: f for f in families} 222 | -------------------------------------------------------------------------------- /tests/test_trace.py: -------------------------------------------------------------------------------- 1 | from importlib import reload 2 | 3 | import aiohttp 4 | import asyncio 5 | import prometheus_client 6 | import pytest 7 | from aiohttp import web 8 | from aiohttp.test_utils import TestClient 9 | from aiohttp.web_response import json_response 10 | from prometheus_client.samples import Sample 11 | from typing import Optional, List, Dict 12 | 13 | import aiohttp_prometheus_exporter 14 | from aiohttp_prometheus_exporter.trace import PrometheusTraceConfig 15 | 16 | 17 | @pytest.fixture(autouse=True) 18 | def clear_registry(): 19 | yield 20 | 21 | reload(aiohttp_prometheus_exporter.trace.prometheus_client.registry) 22 | reload(aiohttp_prometheus_exporter.trace.prometheus_client.metrics) 23 | reload(aiohttp_prometheus_exporter.trace.prometheus_client) 24 | reload(aiohttp_prometheus_exporter.trace) 25 | 26 | 27 | @pytest.fixture 28 | def app(): 29 | """ create a test app with various endpoints for the test scenarios """ 30 | app = web.Application() 31 | routes = web.RouteTableDef() 32 | 33 | @routes.get("/200") 34 | async def response_200(_): 35 | return json_response({"message": "Hello World"}) 36 | 37 | @routes.get("/redirect") 38 | async def redirect(_): 39 | raise web.HTTPFound("/200") 40 | 41 | app.router.add_routes(routes) 42 | 43 | return app 44 | 45 | 46 | def registry_generator(): 47 | return prometheus_client.CollectorRegistry(auto_describe=True) 48 | 49 | 50 | @pytest.mark.parametrize( 51 | "namespace", [None, "namespace"], ids=["no_namespace", "custom_namespace"] 52 | ) 53 | @pytest.mark.parametrize( 54 | "registry_gen", 55 | [None, registry_generator], 56 | ids=["default_registry", "custom_registry"], 57 | ) 58 | @pytest.mark.parametrize( 59 | "client_name", 60 | ["aiohttp_client", "custom_client"], 61 | ids=["default_client_name", "custom_client_name"], 62 | ) 63 | class TestTrace: 64 | @pytest.fixture 65 | def registry(self, registry_gen) -> Optional[prometheus_client.CollectorRegistry]: 66 | if registry_gen is None: 67 | return registry_gen 68 | return registry_gen() 69 | 70 | @pytest.fixture 71 | def current_registry( 72 | self, registry: prometheus_client.CollectorRegistry 73 | ) -> prometheus_client.CollectorRegistry: 74 | if registry: 75 | return registry 76 | return prometheus_client.REGISTRY 77 | 78 | @pytest.fixture 79 | def namespace_prefix(self, namespace): 80 | if namespace is None: 81 | return "" 82 | return f"{namespace}_" 83 | 84 | @pytest.fixture() 85 | async def client( 86 | self, 87 | aiohttp_client, 88 | app: web.Application, 89 | registry: prometheus_client.CollectorRegistry, 90 | namespace: Optional[str], 91 | client_name: Optional[str], 92 | ) -> TestClient: 93 | params = {} 94 | if registry: 95 | params["registry"] = registry 96 | if namespace is not None: 97 | params["namespace"] = namespace 98 | if client_name is not None: 99 | params["client_name"] = client_name 100 | return await aiohttp_client( 101 | app, 102 | trace_configs=[PrometheusTraceConfig(**params)], 103 | connector=aiohttp.TCPConnector(limit=1), 104 | timeout=aiohttp.ClientTimeout(total=0.05), 105 | ) 106 | 107 | async def test_ok( 108 | self, 109 | client: TestClient, 110 | client_name: str, 111 | namespace_prefix: str, 112 | current_registry: prometheus_client.CollectorRegistry, 113 | ): 114 | response = await client.get("/200") 115 | await response.json() 116 | 117 | current_frozen_registry: List[prometheus_client.Metric] = list( 118 | current_registry.collect() 119 | ) 120 | 121 | assert_metric_value( 122 | current_frozen_registry, 123 | f"{namespace_prefix}aiohttp_client_requests", 124 | f"{namespace_prefix}aiohttp_client_requests_total", 125 | 1.0, 126 | labels={ 127 | "client_name": client_name, 128 | "method": "GET", 129 | "scheme": "http", 130 | "remote": "127.0.0.1", 131 | "status_code": "200", 132 | }, 133 | ) 134 | 135 | assert_metric_value( 136 | current_frozen_registry, 137 | f"{namespace_prefix}aiohttp_client_requests_in_progress", 138 | f"{namespace_prefix}aiohttp_client_requests_in_progress", 139 | 0.0, 140 | labels={ 141 | "client_name": client_name, 142 | "method": "GET", 143 | "scheme": "http", 144 | "remote": "127.0.0.1", 145 | }, 146 | ) 147 | 148 | assert_metric_exists( 149 | current_frozen_registry, 150 | f"{namespace_prefix}aiohttp_client_request_duration_seconds", 151 | f"{namespace_prefix}aiohttp_client_request_duration_seconds_bucket", 152 | labels={ 153 | "client_name": client_name, 154 | "method": "GET", 155 | "scheme": "http", 156 | "remote": "127.0.0.1", 157 | }, 158 | ) 159 | 160 | assert_metric_value( 161 | current_frozen_registry, 162 | f"{namespace_prefix}aiohttp_client_chunks_sent_bytes", 163 | f"{namespace_prefix}aiohttp_client_chunks_sent_bytes_total", 164 | 0.0, 165 | labels={"client_name": client_name,}, 166 | ) 167 | 168 | assert_metric_value( 169 | current_frozen_registry, 170 | f"{namespace_prefix}aiohttp_client_chunks_received_bytes", 171 | f"{namespace_prefix}aiohttp_client_chunks_received_bytes_total", 172 | 26.0, 173 | labels={"client_name": client_name,}, 174 | ) 175 | 176 | assert_metric_exists( 177 | current_frozen_registry, 178 | f"{namespace_prefix}aiohttp_client_connection_create_seconds", 179 | f"{namespace_prefix}aiohttp_client_connection_create_seconds_bucket", 180 | labels={"client_name": client_name,}, 181 | ) 182 | 183 | async def test_parallel_connection( 184 | self, 185 | client: TestClient, 186 | client_name: str, 187 | namespace_prefix: str, 188 | current_registry: prometheus_client.CollectorRegistry, 189 | ): 190 | results = await asyncio.gather(client.get("/200"), client.get("/200")) 191 | await asyncio.gather(*(r.json() for r in results)) 192 | 193 | current_frozen_registry: List[prometheus_client.Metric] = list( 194 | current_registry.collect() 195 | ) 196 | 197 | assert_metric_exists( 198 | current_frozen_registry, 199 | f"{namespace_prefix}aiohttp_client_connection_create_seconds", 200 | f"{namespace_prefix}aiohttp_client_connection_create_seconds_bucket", 201 | labels={"client_name": client_name,}, 202 | ) 203 | 204 | assert_metric_value( 205 | current_frozen_registry, 206 | f"{namespace_prefix}aiohttp_client_connection_reuseconn", 207 | f"{namespace_prefix}aiohttp_client_connection_reuseconn_total", 208 | 1.0, 209 | labels={"client_name": client_name,}, 210 | ) 211 | 212 | async def test_redirect( 213 | self, 214 | client: TestClient, 215 | client_name: str, 216 | namespace_prefix: str, 217 | current_registry: prometheus_client.CollectorRegistry, 218 | ): 219 | response = await client.get("/redirect") 220 | await response.json() 221 | current_frozen_registry: List[prometheus_client.Metric] = list( 222 | current_registry.collect() 223 | ) 224 | 225 | assert_metric_value( 226 | current_frozen_registry, 227 | f"{namespace_prefix}aiohttp_client_requests_redirect", 228 | f"{namespace_prefix}aiohttp_client_requests_redirect_total", 229 | 1.0, 230 | labels={ 231 | "client_name": client_name, 232 | "method": "GET", 233 | "scheme": "http", 234 | "remote": "127.0.0.1", 235 | "status_code": "302", 236 | }, 237 | ) 238 | 239 | assert_metric_value( 240 | current_frozen_registry, 241 | f"{namespace_prefix}aiohttp_client_requests", 242 | f"{namespace_prefix}aiohttp_client_requests_total", 243 | 1.0, 244 | labels={ 245 | "client_name": client_name, 246 | "method": "GET", 247 | "scheme": "http", 248 | "remote": "127.0.0.1", 249 | "status_code": "200", 250 | }, 251 | ) 252 | 253 | assert_metric_value( 254 | current_frozen_registry, 255 | f"{namespace_prefix}aiohttp_client_requests", 256 | f"{namespace_prefix}aiohttp_client_requests_total", 257 | 1.0, 258 | labels={ 259 | "client_name": client_name, 260 | "method": "GET", 261 | "scheme": "http", 262 | "remote": "127.0.0.1", 263 | "status_code": "302", 264 | }, 265 | ) 266 | 267 | async def test_exception( 268 | self, 269 | client: TestClient, 270 | client_name: str, 271 | namespace_prefix: str, 272 | current_registry: prometheus_client.CollectorRegistry, 273 | ): 274 | with pytest.raises(TypeError): 275 | response = await client.post("/200", data=TestClient) 276 | await response.json() 277 | current_frozen_registry: List[prometheus_client.Metric] = list( 278 | current_registry.collect() 279 | ) 280 | 281 | assert_metric_value( 282 | current_frozen_registry, 283 | f"{namespace_prefix}aiohttp_client_requests_exceptions", 284 | f"{namespace_prefix}aiohttp_client_requests_exceptions_total", 285 | 1.0, 286 | labels={ 287 | "client_name": client_name, 288 | "method": "POST", 289 | "scheme": "http", 290 | "remote": "127.0.0.1", 291 | "exception_name": "TypeError", 292 | }, 293 | ) 294 | 295 | async def test_google( 296 | self, 297 | registry, 298 | namespace: str, 299 | namespace_prefix: str, 300 | client_name: str, 301 | current_registry: prometheus_client.CollectorRegistry, 302 | ): 303 | params = {} 304 | if registry: 305 | params["registry"] = registry 306 | if namespace is not None: 307 | params["namespace"] = namespace 308 | if client_name is not None: 309 | params["client_name"] = client_name 310 | 311 | connector = aiohttp.TCPConnector(ttl_dns_cache=300, force_close=True) 312 | 313 | async with aiohttp.ClientSession( 314 | trace_configs=[PrometheusTraceConfig(**params)], connector=connector 315 | ) as session: 316 | async with session.get("http://www.google.com/") as resp: 317 | assert resp.status 318 | 319 | async with session.get("http://www.google.com/") as resp: 320 | assert resp.status 321 | 322 | async with aiohttp.ClientSession( 323 | trace_configs=[PrometheusTraceConfig(**params)] 324 | ) as session: 325 | async with session.get("http://www.google.com/") as resp: 326 | assert resp.status 327 | 328 | current_frozen_registry: List[prometheus_client.Metric] = list( 329 | current_registry.collect() 330 | ) 331 | 332 | assert_metric_exists( 333 | current_frozen_registry, 334 | f"{namespace_prefix}aiohttp_client_dns_resolvehost_seconds", 335 | f"{namespace_prefix}aiohttp_client_dns_resolvehost_seconds_bucket", 336 | labels={"client_name": client_name, "host": "www.google.com",}, 337 | ) 338 | 339 | assert_metric_exists( 340 | current_frozen_registry, 341 | f"{namespace_prefix}aiohttp_client_dns_cache_miss", 342 | f"{namespace_prefix}aiohttp_client_dns_cache_miss_total", 343 | labels={"client_name": client_name, "host": "www.google.com",}, 344 | ) 345 | 346 | assert_metric_exists( 347 | current_frozen_registry, 348 | f"{namespace_prefix}aiohttp_client_dns_cache_hit", 349 | f"{namespace_prefix}aiohttp_client_dns_cache_hit_total", 350 | labels={"client_name": client_name, "host": "www.google.com",}, 351 | ) 352 | 353 | 354 | def get_metric_value( 355 | frozen_registry: List[prometheus_client.Metric], 356 | metric_label: str, 357 | sample_label: str, 358 | labels: Dict[str, str], 359 | ): 360 | for metric in frozen_registry: 361 | if metric.name != metric_label: 362 | continue 363 | for sample in metric.samples: # type: Sample 364 | if sample.name != sample_label: 365 | continue 366 | if all( 367 | label in sample.labels and label_value == sample.labels[label] 368 | for label, label_value in labels.items() 369 | ): 370 | return sample.value 371 | 372 | 373 | def assert_metric_value( 374 | frozen_registry: List[prometheus_client.Metric], 375 | metric_label: str, 376 | sample_label: str, 377 | expected_value: float, 378 | labels: Dict[str, str], 379 | ): 380 | value = get_metric_value(frozen_registry, metric_label, sample_label, labels) 381 | 382 | assert expected_value == value 383 | 384 | 385 | def assert_metric_exists( 386 | frozen_registry: List[prometheus_client.Metric], 387 | metric_label: str, 388 | sample_label: str, 389 | labels: Dict[str, str], 390 | ): 391 | value = get_metric_value(frozen_registry, metric_label, sample_label, labels) 392 | 393 | assert value is not None 394 | -------------------------------------------------------------------------------- /aiohttp_prometheus_exporter/trace.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from types import SimpleNamespace 3 | from typing import Type, Optional, Dict, Tuple 4 | 5 | import aiohttp 6 | import prometheus_client 7 | from prometheus_client.registry import CollectorRegistry 8 | from yarl import URL 9 | 10 | 11 | class MetricsStore: 12 | def __init__(self, registry, namespace: str = None): 13 | self.requests_metrics = prometheus_client.Counter( 14 | name=f"aiohttp_client_requests", 15 | documentation="Total client requests by client name, method, scheme, remote and status code.", 16 | labelnames=["client_name", "method", "scheme", "remote", "status_code"], 17 | namespace=namespace, 18 | registry=registry, 19 | ) 20 | 21 | self.requests_in_progress_metrics = prometheus_client.Gauge( 22 | name=f"aiohttp_client_requests_in_progress", 23 | documentation="Gauge of client requests by client name, method, scheme and remote currently being processed.", 24 | labelnames=["client_name", "method", "scheme", "remote"], 25 | namespace=namespace, 26 | registry=registry, 27 | ) 28 | 29 | self.requests_processing_time_metrics = prometheus_client.Histogram( 30 | name=f"aiohttp_client_request_duration", 31 | documentation="Histogram of requests processing time by client name, method, scheme, remote and status code (in seconds).", 32 | labelnames=["client_name", "method", "scheme", "remote", "status_code"], 33 | unit="seconds", 34 | namespace=namespace, 35 | registry=registry, 36 | ) 37 | 38 | self.requests_chunks_sent_metrics = prometheus_client.Counter( 39 | name=f"aiohttp_client_chunks_sent", 40 | documentation="Total bytes sent by client name (in bytes).", 41 | labelnames=["client_name"], 42 | unit="bytes", 43 | namespace=namespace, 44 | registry=registry, 45 | ) 46 | 47 | self.requests_chunks_received_metrics = prometheus_client.Counter( 48 | name=f"aiohttp_client_chunks_received", 49 | documentation="Total bytes received by client name (in bytes).", 50 | labelnames=["client_name"], 51 | unit="bytes", 52 | namespace=namespace, 53 | registry=registry, 54 | ) 55 | 56 | self.requests_exceptions_metrics = prometheus_client.Counter( 57 | name=f"aiohttp_client_requests_exceptions", 58 | documentation="Total client exceptions by client name, method, scheme, remote and exception name.", 59 | labelnames=["client_name", "method", "scheme", "remote", "exception_name"], 60 | namespace=namespace, 61 | registry=registry, 62 | ) 63 | 64 | self.requests_redirect_metrics = prometheus_client.Counter( 65 | name=f"aiohttp_client_requests_redirect", 66 | documentation="Total client exceptions by client name, method, scheme, remote and exception name.", 67 | labelnames=["client_name", "method", "scheme", "remote", "status_code"], 68 | namespace=namespace, 69 | registry=registry, 70 | ) 71 | 72 | self.connection_queued_time_metrics = prometheus_client.Histogram( 73 | name=f"aiohttp_client_connection_queued", 74 | documentation="Gauge of connection queue time by client name (in seconds).", 75 | labelnames=["client_name"], 76 | unit="seconds", 77 | namespace=namespace, 78 | registry=registry, 79 | ) 80 | 81 | self.connection_create_time_metrics = prometheus_client.Histogram( 82 | name=f"aiohttp_client_connection_create", 83 | documentation="Gauge of connection create time by client name (in seconds).", 84 | labelnames=["client_name"], 85 | unit="seconds", 86 | namespace=namespace, 87 | registry=registry, 88 | ) 89 | 90 | self.connection_reuseconn_metrics = prometheus_client.Counter( 91 | name=f"aiohttp_client_connection_reuseconn", 92 | documentation="Total reused connections.", 93 | labelnames=["client_name"], 94 | namespace=namespace, 95 | registry=registry, 96 | ) 97 | 98 | self.dns_resolvehost_metrics = prometheus_client.Histogram( 99 | name=f"aiohttp_client_dns_resolvehost", 100 | documentation="Gauge of dsn resolving time by client name and host (in seconds).", 101 | labelnames=["client_name", "host"], 102 | unit="seconds", 103 | namespace=namespace, 104 | registry=registry, 105 | ) 106 | 107 | self.dns_cache_hit_metrics = prometheus_client.Counter( 108 | name=f"aiohttp_client_dns_cache_hit", 109 | documentation="Total dns cache hits.", 110 | labelnames=["client_name", "host"], 111 | namespace=namespace, 112 | registry=registry, 113 | ) 114 | 115 | self.dns_cache_miss_metrics = prometheus_client.Counter( 116 | name=f"aiohttp_client_dns_cache_miss", 117 | documentation="Total dns cache misses.", 118 | labelnames=["client_name", "host"], 119 | namespace=namespace, 120 | registry=registry, 121 | ) 122 | 123 | 124 | _metrics = {} # type: Dict[Tuple[Optional[str], CollectorRegistry], MetricsStore] 125 | 126 | 127 | def get_loop(): 128 | return getattr(asyncio, "get_running_loop", asyncio.get_event_loop)() 129 | 130 | 131 | class PrometheusTraceConfig(aiohttp.TraceConfig): 132 | def __init__( 133 | self, 134 | client_name="aiohttp_client", 135 | namespace=None, 136 | registry=None, 137 | trace_config_ctx_factory: Type[SimpleNamespace] = SimpleNamespace, 138 | ) -> None: 139 | super().__init__(trace_config_ctx_factory) 140 | 141 | if registry is None: 142 | registry = prometheus_client.REGISTRY 143 | 144 | if (namespace, registry) in _metrics: 145 | self.metrics = _metrics[(namespace, registry)] 146 | else: 147 | self.metrics = MetricsStore(registry=registry, namespace=namespace) 148 | _metrics[(namespace, registry)] = self.metrics 149 | 150 | self.client_name = client_name 151 | 152 | self.on_request_start.append(self.__on_request_start) 153 | self.on_request_end.append(self.__on_request_end) 154 | self.on_request_chunk_sent.append(self.__on_request_chunk_sent) 155 | self.on_response_chunk_received.append(self.__on_response_chunk_received) 156 | self.on_request_exception.append(self.__on_request_exception) 157 | self.on_request_redirect.append(self.__on_request_redirect) 158 | self.on_connection_queued_start.append(self.__on_connection_queued_start) 159 | self.on_connection_queued_end.append(self.__on_connection_queued_end) 160 | self.on_connection_create_start.append(self.__on_connection_create_start) 161 | self.on_connection_create_end.append(self.__on_connection_create_end) 162 | self.on_connection_reuseconn.append(self.__on_connection_reuseconn) 163 | self.on_dns_resolvehost_start.append(self.__on_dns_resolvehost_start) 164 | self.on_dns_resolvehost_end.append(self.__on_dns_resolvehost_end) 165 | self.on_dns_cache_hit.append(self.__on_dns_cache_hit) 166 | self.on_dns_cache_miss.append(self.__on_dns_cache_miss) 167 | 168 | async def __on_request_start( 169 | self, 170 | session: aiohttp.ClientSession, 171 | trace_config_ctx: SimpleNamespace, 172 | params: aiohttp.TraceRequestStartParams, 173 | ) -> None: 174 | loop = get_loop() 175 | 176 | trace_config_ctx._request_start_time = loop.time() 177 | 178 | self.metrics.requests_in_progress_metrics.labels( 179 | client_name=self.client_name, 180 | method=params.method, 181 | scheme=params.url.scheme, 182 | remote=params.url.host, 183 | ).inc() 184 | 185 | async def __on_request_end( 186 | self, 187 | session: aiohttp.ClientSession, 188 | trace_config_ctx: SimpleNamespace, 189 | params: aiohttp.TraceRequestEndParams, 190 | ) -> None: 191 | loop = get_loop() 192 | request_end_time = loop.time() 193 | 194 | request_start_time = getattr( 195 | trace_config_ctx, "_request_start_time", request_end_time 196 | ) 197 | 198 | self.metrics.requests_in_progress_metrics.labels( 199 | client_name=self.client_name, 200 | method=params.method, 201 | scheme=params.url.scheme, 202 | remote=params.url.host, 203 | ).dec() 204 | 205 | self.metrics.requests_metrics.labels( 206 | client_name=self.client_name, 207 | method=params.response.method, 208 | scheme=params.response.url.scheme, 209 | remote=params.response.url.host, 210 | status_code=params.response.status, 211 | ).inc() 212 | 213 | self.metrics.requests_processing_time_metrics.labels( 214 | client_name=self.client_name, 215 | method=params.response.method, 216 | scheme=params.response.url.scheme, 217 | remote=params.response.url.host, 218 | status_code=params.response.status, 219 | ).observe(request_end_time - request_start_time) 220 | 221 | async def __on_request_chunk_sent( 222 | self, 223 | session: aiohttp.ClientSession, 224 | trace_config_ctx: SimpleNamespace, 225 | params: aiohttp.TraceRequestChunkSentParams, 226 | ) -> None: 227 | self.metrics.requests_chunks_sent_metrics.labels( 228 | client_name=self.client_name 229 | ).inc(len(params.chunk)) 230 | 231 | async def __on_response_chunk_received( 232 | self, 233 | session: aiohttp.ClientSession, 234 | trace_config_ctx: SimpleNamespace, 235 | params: aiohttp.TraceResponseChunkReceivedParams, 236 | ) -> None: 237 | self.metrics.requests_chunks_received_metrics.labels( 238 | client_name=self.client_name 239 | ).inc(len(params.chunk)) 240 | 241 | async def __on_request_exception( 242 | self, 243 | session: aiohttp.ClientSession, 244 | trace_config_ctx: SimpleNamespace, 245 | params: aiohttp.TraceRequestExceptionParams, 246 | ) -> None: 247 | self.metrics.requests_exceptions_metrics.labels( 248 | client_name=self.client_name, 249 | method=params.method, 250 | scheme=params.url.scheme, 251 | remote=params.url.host, 252 | exception_name=type(params.exception).__name__, 253 | ).inc() 254 | 255 | async def __on_request_redirect( 256 | self, 257 | session: aiohttp.ClientSession, 258 | trace_config_ctx: SimpleNamespace, 259 | params: aiohttp.TraceRequestRedirectParams, 260 | ) -> None: 261 | location = params.response.headers["Location"] 262 | new_url = URL(location) 263 | 264 | self.metrics.requests_in_progress_metrics.labels( 265 | client_name=self.client_name, 266 | method=params.method, 267 | scheme=params.url.scheme, 268 | remote=params.url.host, 269 | ).dec() 270 | 271 | self.metrics.requests_in_progress_metrics.labels( 272 | client_name=self.client_name, 273 | method=params.method, 274 | scheme=new_url.scheme, 275 | remote=new_url.host, 276 | ).inc() 277 | 278 | self.metrics.requests_metrics.labels( 279 | client_name=self.client_name, 280 | method=params.response.method, 281 | scheme=params.url.scheme, 282 | remote=params.url.host, 283 | status_code=params.response.status, 284 | ).inc() 285 | 286 | self.metrics.requests_redirect_metrics.labels( 287 | client_name=self.client_name, 288 | method=params.response.method, 289 | scheme=params.response.url.scheme, 290 | remote=params.response.url.host, 291 | status_code=params.response.status, 292 | ).inc() 293 | 294 | @staticmethod 295 | async def __on_connection_queued_start( 296 | session: aiohttp.ClientSession, 297 | trace_config_ctx: SimpleNamespace, 298 | params: aiohttp.TraceConnectionQueuedStartParams, 299 | ) -> None: 300 | loop = get_loop() 301 | trace_config_ctx._connection_queued_start_time = loop.time() 302 | 303 | async def __on_connection_queued_end( 304 | self, 305 | session: aiohttp.ClientSession, 306 | trace_config_ctx: SimpleNamespace, 307 | params: aiohttp.TraceConnectionQueuedEndParams, 308 | ) -> None: 309 | loop = get_loop() 310 | connection_queued_end_time = loop.time() 311 | connection_queued_start_time = getattr( 312 | trace_config_ctx, 313 | "_connection_queued_start_time", 314 | connection_queued_end_time, 315 | ) 316 | self.metrics.connection_queued_time_metrics.labels( 317 | client_name=self.client_name 318 | ).observe(connection_queued_end_time - connection_queued_start_time) 319 | 320 | @staticmethod 321 | async def __on_connection_create_start( 322 | session: aiohttp.ClientSession, 323 | trace_config_ctx: SimpleNamespace, 324 | params: aiohttp.TraceConnectionCreateStartParams, 325 | ) -> None: 326 | loop = get_loop() 327 | trace_config_ctx._connection_create_start_time = loop.time() 328 | 329 | async def __on_connection_create_end( 330 | self, 331 | session: aiohttp.ClientSession, 332 | trace_config_ctx: SimpleNamespace, 333 | params: aiohttp.TraceConnectionCreateStartParams, 334 | ) -> None: 335 | loop = get_loop() 336 | connection_create_end_time = loop.time() 337 | connection_create_start_time = getattr( 338 | trace_config_ctx, 339 | "_connection_create_start_time", 340 | connection_create_end_time, 341 | ) 342 | self.metrics.connection_create_time_metrics.labels( 343 | client_name=self.client_name 344 | ).observe(connection_create_end_time - connection_create_start_time) 345 | 346 | async def __on_connection_reuseconn( 347 | self, 348 | session: aiohttp.ClientSession, 349 | trace_config_ctx: SimpleNamespace, 350 | params: aiohttp.TraceConnectionReuseconnParams, 351 | ) -> None: 352 | self.metrics.connection_reuseconn_metrics.labels( 353 | client_name=self.client_name 354 | ).inc() 355 | 356 | @staticmethod 357 | async def __on_dns_resolvehost_start( 358 | session: aiohttp.ClientSession, 359 | trace_config_ctx: SimpleNamespace, 360 | params: aiohttp.TraceDnsResolveHostStartParams, 361 | ) -> None: 362 | loop = get_loop() 363 | trace_config_ctx._dns_resolvehost_start_time = loop.time() 364 | 365 | async def __on_dns_resolvehost_end( 366 | self, 367 | session: aiohttp.ClientSession, 368 | trace_config_ctx: SimpleNamespace, 369 | params: aiohttp.TraceDnsResolveHostEndParams, 370 | ) -> None: 371 | loop = get_loop() 372 | dns_resolvehost_end_time = loop.time() 373 | dns_resolvehost_start_time = getattr( 374 | trace_config_ctx, "_dns_resolvehost_start_time", dns_resolvehost_end_time 375 | ) 376 | self.metrics.dns_resolvehost_metrics.labels( 377 | client_name=self.client_name, host=params.host 378 | ).observe(dns_resolvehost_end_time - dns_resolvehost_start_time) 379 | 380 | async def __on_dns_cache_hit( 381 | self, 382 | session: aiohttp.ClientSession, 383 | trace_config_ctx: SimpleNamespace, 384 | params: aiohttp.TraceDnsCacheHitParams, 385 | ) -> None: 386 | self.metrics.dns_cache_hit_metrics.labels( 387 | client_name=self.client_name, host=params.host 388 | ).inc() 389 | 390 | async def __on_dns_cache_miss( 391 | self, 392 | session: aiohttp.ClientSession, 393 | trace_config_ctx: SimpleNamespace, 394 | params: aiohttp.TraceDnsCacheMissParams, 395 | ) -> None: 396 | self.metrics.dns_cache_miss_metrics.labels( 397 | client_name=self.client_name, host=params.host 398 | ).inc() 399 | --------------------------------------------------------------------------------