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
This commit is contained in:
Luna Mendes 2018-07-09 02:48:44 -03:00
parent 359c62efd6
commit d92c09772e
No known key found for this signature in database
GPG Key ID: 7D950EEE259CE92F
9 changed files with 95 additions and 29 deletions

View File

@ -1,4 +0,0 @@
# Used by "mix format"
[
inputs: ["mix.exs", "{config,lib,test}/**/*.{ex,exs}"]
]

View File

@ -2,12 +2,60 @@ from sanic import Blueprint, response
bp = Blueprint(__name__) 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') @bp.get('/api/current_status')
async def get_cur_status(request): async def get_cur_status(request):
pass manager = request.app.manager
return response.json(get_status(manager))
@bp.get('/api/status') @bp.get('/api/status')
async def get_full_status(request): async def get_full_status(request):
pass manager = request.app.manager
return response.json({
'status': get_status(manager),
'graph': get_graphs(manager),
})

View File

@ -3,9 +3,9 @@ PORT = 8069
SERVICES = { SERVICES = {
'elixire': { 'elixire': {
'description': "elixi.re's backend", 'description': "elixi.re's backend",
'adapter': 'elixire', 'adapter': 'http',
'adapter_args': { 'adapter_args': {
'base_url': 'https://elixi.re' 'url': 'https://elixi.re/api/hello'
}, },
'poll': 10 'poll': 10
}, },

View File

@ -11,7 +11,7 @@ class Adapter:
} }
@classmethod @classmethod
async def query(cls, _worker, _adp_args): async def query(cls, _worker, _adp_args) -> tuple:
"""Main query function.""" """Main query function."""
raise NotImplementedError raise NotImplementedError
@ -37,15 +37,12 @@ class PingAdapter(Adapter):
alive = bool(re.search(PING_RGX, out + err)) alive = bool(re.search(PING_RGX, out + err))
worker.log.info(f'{worker.name}: alive? {alive}') worker.log.info(f'{worker.name}: alive? {alive}')
return alive return (alive,)
class ElixireAdapter(Adapter): class HttpAdapter(Adapter):
"""Adapter to check if a certain """Adapter to check if a certain
elixire instance is reporting well. URL is giving 200."""
Uses the /api/hello route to determine livelyhood.
"""
spec = { spec = {
'db': ('timestamp', 'status', 'latency') 'db': ('timestamp', 'status', 'latency')
} }
@ -56,7 +53,7 @@ class ElixireAdapter(Adapter):
session = worker.manager.app.session session = worker.manager.app.session
t_start = time.monotonic() 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() t_end = time.monotonic()
latency = round((t_end - t_start) * 1000) latency = round((t_end - t_start) * 1000)
@ -65,7 +62,7 @@ class ElixireAdapter(Adapter):
f'latency={latency}ms') f'latency={latency}ms')
if resp.status == 200: if resp.status == 200:
return 200, latency return True, latency
# use 0ms drops as failures # use 0ms drops as failures
return False, 0 return False, 0

View File

@ -1,7 +1,7 @@
from .adapters import ElixireAdapter, PingAdapter from .adapters import HttpAdapter, PingAdapter
ADAPTERS = { ADAPTERS = {
'elixire': ElixireAdapter, 'http': HttpAdapter,
'ping': PingAdapter, 'ping': PingAdapter,
} }

View File

@ -38,8 +38,6 @@ class ServiceManager:
""") """)
def _start(self): def _start(self):
self.conn.executescript("""
""")
for name, service in self.cfg.SERVICES.items(): for name, service in self.cfg.SERVICES.items():
self._make_db_table(name, service) self._make_db_table(name, service)
@ -47,4 +45,4 @@ class ServiceManager:
serv_worker = ServiceWorker(self, name, service) serv_worker = ServiceWorker(self, name, service)
self.workers[name] = serv_worker self.workers[name] = serv_worker
self.state[name] = False self.state[name] = None

View File

@ -17,18 +17,37 @@ class ServiceWorker:
self._start() self._start()
async def work(self): async def work(self):
res = await self.adapter.query(self, self.service['adapter_args']) return await self.adapter.query(self, self.service['adapter_args'])
return res
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): async def _work_loop(self):
try: try:
while True: while True:
self.log.info(f'polling {self.name}') self.log.info(f'polling {self.name}')
self.last_poll = time.monotonic() 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']) await asyncio.sleep(self.service['poll'])
except Exception: except Exception:
self.log.exception('fail on poll') self.log.exception('fail on poll, retrying')
await self._work_loop()
def _start(self): def _start(self):
self.log.info(f'starting work loop for {self.name}') self.log.info(f'starting work loop for {self.name}')

View File

@ -3,7 +3,8 @@ import React, { Component } from 'react';
import Service from './Service.js'; import Service from './Service.js';
import './App.css'; 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 { export default class App extends Component {
state = { state = {

11
run.py
View File

@ -8,18 +8,23 @@ from sanic.exceptions import NotFound, FileNotFound
import config import config
from elstat import manager from elstat import manager
from blueprints import api
logging.basicConfig(level=logging.INFO) logging.basicConfig(level=logging.INFO)
log = logging.getLogger(__name__)
app = Sanic() app = Sanic()
app.cfg = config app.cfg = config
CORS(app, automatic_options=True) CORS(app, automatic_options=True)
app.blueprint(api)
@app.listener('before_server_start') @app.listener('before_server_start')
async def _app_start(refapp, loop): async def _app_start(refapp, loop):
refapp.session = aiohttp.ClientSession(loop=loop) refapp.session = aiohttp.ClientSession(loop=loop)
refapp.conn = sqlite3.connect('elstat.db') refapp.conn = sqlite3.connect('elstat.db')
refapp.serv = manager.ServiceManager(app) refapp.manager = manager.ServiceManager(app)
@app.listener('after_server_stop') @app.listener('after_server_stop')
@ -29,8 +34,10 @@ async def _app_stop(refapp, _loop):
@app.exception(Exception) @app.exception(Exception)
async def _handle_exc(request, exception): async def _handle_exc(request, exception):
log.exception('oopsie woopsie')
status_code = 404 if isinstance(exception, (NotFound, FileNotFound)) \ status_code = 404 if isinstance(exception, (NotFound, FileNotFound)) \
else 500 else 500
return response.json({ return response.json({
'error': True, 'error': True,