From d92c09772e840164b110d3dc034c911b9e878b97 Mon Sep 17 00:00:00 2001 From: Luna Mendes Date: Mon, 9 Jul 2018 02:48:44 -0300 Subject: [PATCH] add public api - generalize elixire adapter into http adapter - fix ping adapter not sending a tuple - add ServiceWorker.process_work to insert results into the database --- .formatter.exs | 4 ---- blueprints/api.py | 52 ++++++++++++++++++++++++++++++++++++++-- config.example.py | 4 ++-- elstat/adapters.py | 15 +++++------- elstat/consts.py | 4 ++-- elstat/manager.py | 4 +--- elstat/worker.py | 27 +++++++++++++++++---- priv/frontend/src/App.js | 3 ++- run.py | 11 +++++++-- 9 files changed, 95 insertions(+), 29 deletions(-) delete mode 100644 .formatter.exs diff --git a/.formatter.exs b/.formatter.exs deleted file mode 100644 index 525446d..0000000 --- a/.formatter.exs +++ /dev/null @@ -1,4 +0,0 @@ -# Used by "mix format" -[ - inputs: ["mix.exs", "{config,lib,test}/**/*.{ex,exs}"] -] diff --git a/blueprints/api.py b/blueprints/api.py index 3a0bcdc..31cdab5 100644 --- a/blueprints/api.py +++ b/blueprints/api.py @@ -2,12 +2,60 @@ from sanic import Blueprint, response bp = Blueprint(__name__) +def get_status(manager): + res = {} + + for name, state in manager.state.items(): + # ignore unitialized workers + if state is None: + continue + + # timestamp will always be the first + worker = manager.workers[name] + columns = worker.adapter.spec['db'][1:] + res[name] = {} + for key, val in zip(columns, state): + res[name][key] = val + + res[name]['description'] = worker.service['description'] + + return res + + +def get_graphs(manager): + res = {} + + for name, worker in manager.workers.items(): + # skip adapters without latency + if 'latency' not in worker.adapter.spec['db']: + continue + + cur = manager.conn.cursor() + + cur.execute(f""" + SELECT timestamp, latency FROM {name} + ORDER BY timestamp DESC + LIMIT 50 + """) + + qres = cur.fetchall() + + res[name] = qres + + return res + @bp.get('/api/current_status') async def get_cur_status(request): - pass + manager = request.app.manager + return response.json(get_status(manager)) @bp.get('/api/status') async def get_full_status(request): - pass + manager = request.app.manager + + return response.json({ + 'status': get_status(manager), + 'graph': get_graphs(manager), + }) diff --git a/config.example.py b/config.example.py index aaf17dd..69e5493 100644 --- a/config.example.py +++ b/config.example.py @@ -3,9 +3,9 @@ PORT = 8069 SERVICES = { 'elixire': { 'description': "elixi.re's backend", - 'adapter': 'elixire', + 'adapter': 'http', 'adapter_args': { - 'base_url': 'https://elixi.re' + 'url': 'https://elixi.re/api/hello' }, 'poll': 10 }, diff --git a/elstat/adapters.py b/elstat/adapters.py index 3575037..be4bc33 100644 --- a/elstat/adapters.py +++ b/elstat/adapters.py @@ -11,7 +11,7 @@ class Adapter: } @classmethod - async def query(cls, _worker, _adp_args): + async def query(cls, _worker, _adp_args) -> tuple: """Main query function.""" raise NotImplementedError @@ -37,15 +37,12 @@ class PingAdapter(Adapter): alive = bool(re.search(PING_RGX, out + err)) worker.log.info(f'{worker.name}: alive? {alive}') - return alive + return (alive,) -class ElixireAdapter(Adapter): +class HttpAdapter(Adapter): """Adapter to check if a certain - elixire instance is reporting well. - - Uses the /api/hello route to determine livelyhood. - """ + URL is giving 200.""" spec = { 'db': ('timestamp', 'status', 'latency') } @@ -56,7 +53,7 @@ class ElixireAdapter(Adapter): session = worker.manager.app.session t_start = time.monotonic() - resp = await session.get(f'{adp_args["base_url"]}/api/hello') + resp = await session.get(f'{adp_args["url"]}') t_end = time.monotonic() latency = round((t_end - t_start) * 1000) @@ -65,7 +62,7 @@ class ElixireAdapter(Adapter): f'latency={latency}ms') if resp.status == 200: - return 200, latency + return True, latency # use 0ms drops as failures return False, 0 diff --git a/elstat/consts.py b/elstat/consts.py index 5a30671..6bf276e 100644 --- a/elstat/consts.py +++ b/elstat/consts.py @@ -1,7 +1,7 @@ -from .adapters import ElixireAdapter, PingAdapter +from .adapters import HttpAdapter, PingAdapter ADAPTERS = { - 'elixire': ElixireAdapter, + 'http': HttpAdapter, 'ping': PingAdapter, } diff --git a/elstat/manager.py b/elstat/manager.py index da8b6c6..559aba6 100644 --- a/elstat/manager.py +++ b/elstat/manager.py @@ -38,8 +38,6 @@ class ServiceManager: """) def _start(self): - self.conn.executescript(""" - """) for name, service in self.cfg.SERVICES.items(): self._make_db_table(name, service) @@ -47,4 +45,4 @@ class ServiceManager: serv_worker = ServiceWorker(self, name, service) self.workers[name] = serv_worker - self.state[name] = False + self.state[name] = None diff --git a/elstat/worker.py b/elstat/worker.py index 3cdeae8..3cde172 100644 --- a/elstat/worker.py +++ b/elstat/worker.py @@ -17,18 +17,37 @@ class ServiceWorker: self._start() async def work(self): - res = await self.adapter.query(self, self.service['adapter_args']) - return res + return await self.adapter.query(self, self.service['adapter_args']) + + async def process_work(self, result): + columns = self.adapter.spec['db'] + conn = self.manager.conn + + timestamp = int(time.time() * 1000) + + args_str = ','.join(['?'] * (len(result) + 1)) + query = f""" + INSERT INTO {self.name} ({','.join(columns)}) + VALUES ({args_str}) + """ + + conn.execute(query, (timestamp,) + result) + conn.commit() async def _work_loop(self): try: while True: self.log.info(f'polling {self.name}') self.last_poll = time.monotonic() - await self.work() + res = await self.work() + + self.manager.state[self.name] = res + await self.process_work(res) + await asyncio.sleep(self.service['poll']) except Exception: - self.log.exception('fail on poll') + self.log.exception('fail on poll, retrying') + await self._work_loop() def _start(self): self.log.info(f'starting work loop for {self.name}') diff --git a/priv/frontend/src/App.js b/priv/frontend/src/App.js index 28afbc5..f5e4e6a 100644 --- a/priv/frontend/src/App.js +++ b/priv/frontend/src/App.js @@ -3,7 +3,8 @@ import React, { Component } from 'react'; import Service from './Service.js'; import './App.css'; -const ENDPOINT = 'https://elstatus.stayathomeserver.club/api/status' +// const ENDPOINT = 'https://elstatus.stayathomeserver.club/api/status' +const ENDPOINT = 'http://localhost:8069/api/status' export default class App extends Component { state = { diff --git a/run.py b/run.py index 1bf8bf7..6deb84f 100644 --- a/run.py +++ b/run.py @@ -8,18 +8,23 @@ from sanic.exceptions import NotFound, FileNotFound import config from elstat import manager +from blueprints import api logging.basicConfig(level=logging.INFO) +log = logging.getLogger(__name__) + app = Sanic() app.cfg = config CORS(app, automatic_options=True) +app.blueprint(api) + @app.listener('before_server_start') async def _app_start(refapp, loop): refapp.session = aiohttp.ClientSession(loop=loop) refapp.conn = sqlite3.connect('elstat.db') - refapp.serv = manager.ServiceManager(app) + refapp.manager = manager.ServiceManager(app) @app.listener('after_server_stop') @@ -29,8 +34,10 @@ async def _app_stop(refapp, _loop): @app.exception(Exception) async def _handle_exc(request, exception): + log.exception('oopsie woopsie') + status_code = 404 if isinstance(exception, (NotFound, FileNotFound)) \ - else 500 + else 500 return response.json({ 'error': True,