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:
parent
359c62efd6
commit
d92c09772e
9 changed files with 95 additions and 29 deletions
|
@ -1,4 +0,0 @@
|
|||
# Used by "mix format"
|
||||
[
|
||||
inputs: ["mix.exs", "{config,lib,test}/**/*.{ex,exs}"]
|
||||
]
|
|
@ -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),
|
||||
})
|
||||
|
|
|
@ -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
|
||||
},
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
from .adapters import ElixireAdapter, PingAdapter
|
||||
from .adapters import HttpAdapter, PingAdapter
|
||||
|
||||
|
||||
ADAPTERS = {
|
||||
'elixire': ElixireAdapter,
|
||||
'http': HttpAdapter,
|
||||
'ping': PingAdapter,
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}')
|
||||
|
|
|
@ -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
11
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,
|
||||
|
|
Loading…
Reference in a new issue