Add prototype

This commit is contained in:
Roman Mogylatov 2020-08-05 13:11:45 -04:00
parent dc9c1dde3f
commit e56b72e411
11 changed files with 224 additions and 0 deletions

View File

@ -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"]

View File

@ -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]

View File

@ -0,0 +1,9 @@
version: "3.7"
services:
monitor:
build: ./
image: monitoring-daemon
volumes:
- "./:/code"

View File

@ -0,0 +1 @@
"""Top-level package."""

View File

@ -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()

View File

@ -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,
),
)

View File

@ -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))

View File

@ -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

View File

@ -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)

View File

@ -0,0 +1,3 @@
dependency-injector
aiohttp
pyyaml