diff --git a/examples/miniapps/monitoring-daemon-asyncio/Dockerfile b/examples/miniapps/monitoring-daemon-asyncio/Dockerfile new file mode 100644 index 00000000..c4bc7012 --- /dev/null +++ b/examples/miniapps/monitoring-daemon-asyncio/Dockerfile @@ -0,0 +1,13 @@ +FROM python:3.8-buster + +ENV PYTHONUNBUFFERED=1 + +WORKDIR /code +COPY . /code/ + +RUN apt-get install openssl \ + && pip install --upgrade pip \ + && pip install -r requirements.txt \ + && rm -rf ~/.cache + +CMD ["python", "-m", "monitoringdaemon"] diff --git a/examples/miniapps/monitoring-daemon-asyncio/config.yml b/examples/miniapps/monitoring-daemon-asyncio/config.yml new file mode 100644 index 00000000..c7da7877 --- /dev/null +++ b/examples/miniapps/monitoring-daemon-asyncio/config.yml @@ -0,0 +1,17 @@ +log: + level: "INFO" + format: "[%(asctime)s] [%(levelname)s] [%(name)s]: %(message)s" + +monitors: + + example: + method: "GET" + url: "http://example.com" + check_every: 5 + expected_codes: [200] + + httpbin: + method: "GET" + url: "http://httpbin.org/get" + check_every: 5 + expected_codes: [200] diff --git a/examples/miniapps/monitoring-daemon-asyncio/docker-compose.yml b/examples/miniapps/monitoring-daemon-asyncio/docker-compose.yml new file mode 100644 index 00000000..2857d812 --- /dev/null +++ b/examples/miniapps/monitoring-daemon-asyncio/docker-compose.yml @@ -0,0 +1,9 @@ +version: "3.7" + +services: + + monitor: + build: ./ + image: monitoring-daemon + volumes: + - "./:/code" diff --git a/examples/miniapps/monitoring-daemon-asyncio/monitoringdaemon/__init__.py b/examples/miniapps/monitoring-daemon-asyncio/monitoringdaemon/__init__.py new file mode 100644 index 00000000..1c744ca5 --- /dev/null +++ b/examples/miniapps/monitoring-daemon-asyncio/monitoringdaemon/__init__.py @@ -0,0 +1 @@ +"""Top-level package.""" diff --git a/examples/miniapps/monitoring-daemon-asyncio/monitoringdaemon/__main__.py b/examples/miniapps/monitoring-daemon-asyncio/monitoringdaemon/__main__.py new file mode 100644 index 00000000..69b66dd7 --- /dev/null +++ b/examples/miniapps/monitoring-daemon-asyncio/monitoringdaemon/__main__.py @@ -0,0 +1,18 @@ +"""Main module.""" + +from .containers import ApplicationContainer + + +def main() -> None: + """Run the application.""" + container = ApplicationContainer() + + container.config.from_yaml('config.yml') + container.configure_logging() + + dispatcher = container.dispatcher() + dispatcher.run() + + +if __name__ == '__main__': + main() diff --git a/examples/miniapps/monitoring-daemon-asyncio/monitoringdaemon/containers.py b/examples/miniapps/monitoring-daemon-asyncio/monitoringdaemon/containers.py new file mode 100644 index 00000000..c66ec327 --- /dev/null +++ b/examples/miniapps/monitoring-daemon-asyncio/monitoringdaemon/containers.py @@ -0,0 +1,43 @@ +"""Application containers module.""" + +import logging +import sys + +from dependency_injector import containers, providers + +from . import http, monitors, dispatcher + + +class ApplicationContainer(containers.DeclarativeContainer): + """Application container.""" + + config = providers.Configuration() + + configure_logging = providers.Callable( + logging.basicConfig, + stream=sys.stdout, + level=config.log.level, + format=config.log.format, + ) + + http_client = providers.Factory(http.HttpClient) + + example_monitor = providers.Factory( + monitors.HttpMonitor, + http_client=http_client, + options=config.monitors.example, + ) + + httpbin_monitor = providers.Factory( + monitors.HttpMonitor, + http_client=http_client, + options=config.monitors.httpbin, + ) + + dispatcher = providers.Factory( + dispatcher.Dispatcher, + monitors=providers.List( + example_monitor, + httpbin_monitor, + ), + ) diff --git a/examples/miniapps/monitoring-daemon-asyncio/monitoringdaemon/dispatcher.py b/examples/miniapps/monitoring-daemon-asyncio/monitoringdaemon/dispatcher.py new file mode 100644 index 00000000..6bbc797e --- /dev/null +++ b/examples/miniapps/monitoring-daemon-asyncio/monitoringdaemon/dispatcher.py @@ -0,0 +1,67 @@ +"""Dispatcher module.""" + +import asyncio +import logging +import signal +import time +from typing import List + +from .monitors import Monitor + + +logger = logging.getLogger(__name__) + + +class Dispatcher: + + def __init__(self, monitors: List[Monitor]) -> None: + self._monitors = monitors + self._monitor_tasks: List[asyncio.Task] = [] # type: ignore + self._stopping = False + + def run(self) -> None: + asyncio.run(self._do_work()) + + async def _do_work(self) -> None: + logger.info('Dispatcher is starting up') + + for monitor in self._monitors: + self._monitor_tasks.append(asyncio.create_task(self._run_monitor(monitor))) + logger.info('Monitoring task has been started %s', monitor.full_name) + + asyncio.get_event_loop().add_signal_handler(signal.SIGTERM, self._stop) + asyncio.get_event_loop().add_signal_handler(signal.SIGINT, self._stop) + + await asyncio.gather(*self._monitor_tasks, return_exceptions=True) + + self._stop() + + def _stop(self) -> None: + if self._stopping: + return + + self._stopping = True + + logger.info('Dispatcher is shutting down') + for task, monitor in zip(self._monitor_tasks, self._monitors): + task.cancel() + logger.info('Monitoring task has been stopped %s', monitor.full_name) + logger.info('Dispatcher shutting down finished successfully') + + @staticmethod + async def _run_monitor(monitor: Monitor) -> None: + def _until_next(last: float) -> float: + time_took = time.time() - last + return monitor.check_every - time_took + + while True: + time_start = time.time() + + try: + await monitor.check() + except asyncio.CancelledError: + break + except Exception: + monitor.logger.exception('Error running monitoring check') + + await asyncio.sleep(_until_next(last=time_start)) diff --git a/examples/miniapps/monitoring-daemon-asyncio/monitoringdaemon/http.py b/examples/miniapps/monitoring-daemon-asyncio/monitoringdaemon/http.py new file mode 100644 index 00000000..5076e5b5 --- /dev/null +++ b/examples/miniapps/monitoring-daemon-asyncio/monitoringdaemon/http.py @@ -0,0 +1,11 @@ +"""Http client module.""" + +from aiohttp import ClientSession, ClientResponse + + +class HttpClient: + + async def request(self, method: str, url: str) -> ClientResponse: + async with ClientSession() as session: + async with session.request(method, url) as response: + return response diff --git a/examples/miniapps/monitoring-daemon-asyncio/monitoringdaemon/monitors.py b/examples/miniapps/monitoring-daemon-asyncio/monitoringdaemon/monitors.py new file mode 100644 index 00000000..a54bac21 --- /dev/null +++ b/examples/miniapps/monitoring-daemon-asyncio/monitoringdaemon/monitors.py @@ -0,0 +1,42 @@ +"""Monitors module.""" + +import logging +from typing import Dict, Any + +from .http import HttpClient + + +class Monitor: + + def __init__(self, check_every: int) -> None: + self.check_every = check_every + self.logger = logging.getLogger(self.full_name) + + @property + def full_name(self) -> str: + raise NotImplementedError() + + async def check(self) -> None: + raise NotImplementedError() + + +class HttpMonitor(Monitor): + + def __init__( + self, + http_client: HttpClient, + options: Dict[str, Any], + ) -> None: + self._client = http_client + self._method = options.pop('method') + self._url = options.pop('url') + self._expected_codes = options.pop('expected_codes') + super().__init__(check_every=options.pop('check_every')) + + @property + def full_name(self) -> str: + return '{0}.{1}(url="{2}")'.format(__name__, self.__class__.__name__, self._url) + + async def check(self) -> None: + response = await self._client.request(method=self._method, url=self._url) + self.logger.info('Return response code: %s', response.status) diff --git a/examples/miniapps/monitoring-daemon-asyncio/monitoringdaemon/tests.py b/examples/miniapps/monitoring-daemon-asyncio/monitoringdaemon/tests.py new file mode 100644 index 00000000..e69de29b diff --git a/examples/miniapps/monitoring-daemon-asyncio/requirements.txt b/examples/miniapps/monitoring-daemon-asyncio/requirements.txt new file mode 100644 index 00000000..03541e70 --- /dev/null +++ b/examples/miniapps/monitoring-daemon-asyncio/requirements.txt @@ -0,0 +1,3 @@ +dependency-injector +aiohttp +pyyaml