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__)
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),
})

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

11
run.py
View File

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