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__)
|
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),
|
||||||
|
})
|
||||||
|
|
|
@ -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
|
||||||
},
|
},
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
from .adapters import ElixireAdapter, PingAdapter
|
from .adapters import HttpAdapter, PingAdapter
|
||||||
|
|
||||||
|
|
||||||
ADAPTERS = {
|
ADAPTERS = {
|
||||||
'elixire': ElixireAdapter,
|
'http': HttpAdapter,
|
||||||
'ping': PingAdapter,
|
'ping': PingAdapter,
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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}')
|
||||||
|
|
|
@ -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
11
run.py
|
@ -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,
|
||||||
|
|
Loading…
Reference in a new issue