elstat.adapters: add latency to db spec

Since this is a change to the table schema for all services using the
ping adapter, you will have to delete the tables relating to them.

 - elstat.blueprints: add incidents blueprint (WIP)
 - elstat.blueprints: add decorators.py
 - elstat.blueprints: add errors.py
 - elstat.manager: add incidents & incident_stages tables
 - run: add handler for ApiError
This commit is contained in:
Luna Mendes 2018-07-13 23:20:09 -03:00
parent d4e280a563
commit ecf6234cfe
10 changed files with 186 additions and 14 deletions

View File

@ -11,6 +11,10 @@ python3.6 -m pip install -Ur requirements.txt
# edit config.py as you wish
cp config.example.py config.py
# build frontend
cd priv/frontend
# check instructions on README.md
```
## Run

View File

@ -1,5 +1,7 @@
PORT = 8069
PASSWORD = '123456'
SERVICES = {
'elixire': {
'description': "elixi.re's backend",

View File

@ -3,6 +3,7 @@ import time
import re
PING_RGX = re.compile(r'(.+)( 0% packet loss)(.+)', re.I | re.M)
PING_LATENCY_RGX = re.compile('time\=(\d+(\.\d+)?) ms', re.M)
class Adapter:
@ -20,7 +21,7 @@ class PingAdapter(Adapter):
"""Ping the given address and report if
any packet loss happened."""
spec = {
'db': ('timestamp', 'status')
'db': ('timestamp', 'status', 'latency')
}
@classmethod
@ -34,10 +35,25 @@ class PingAdapter(Adapter):
out, err = map(lambda s: s.decode('utf-8'),
await process.communicate())
alive = bool(re.search(PING_RGX, out + err))
worker.log.info(f'{worker.name}: alive? {alive}')
out += err
return (alive,)
alive = bool(re.search(PING_RGX, out))
latency = PING_LATENCY_RGX.search(out)
if latency is not None:
num = latency.group(1)
try:
latency = int(num)
except ValueError:
try:
latency = max(float(num), 1)
except ValueError:
latency = 0
else:
latency = 0
worker.log.info(f'{worker.name}: alive? {alive} latency? {latency}ms')
return (alive, latency)
class HttpAdapter(Adapter):

View File

@ -1,2 +1,3 @@
from .api import bp as api
from .incidents import bp as incidents
from .streaming import bp as streaming

View File

@ -0,0 +1,22 @@
import hashlib
from .errors import ApiError
def auth_route(handler):
"""Declare an authenticated route."""
async def _handler(request, *args, **kwargs):
try:
pwhash = request.headers['Authorization']
except KeyError:
raise ApiError('no password provided', 403)
correct = request.app.cfg.PASSWORD
hashed = hashlib.sha256(correct.encode()).hexdigest()
if pwhash != hashed:
raise ApiError('invalid password', 403)
return await handler(request, *args, **kwargs)
return _handler

View File

@ -0,0 +1,8 @@
class ApiError(Exception):
@property
def message(self):
return self.args[0]
@property
def status_code(self):
return self.args[1]

View File

@ -0,0 +1,73 @@
import datetime
from sanic import Blueprint, response
from .decorators import auth_route
bp = Blueprint(__name__)
# TODO: pages
@bp.get('/api/incidents')
async def get_incidents(request):
manager = request.app.manager
cur = manager.conn.cursor()
cur.execute("""
SELECT id, incident_type, title, content, ongoing,
start_timestamp, end_timestamp
FROM incidents
ORDER BY id DESC
""")
rows = cur.fetchall()
res = []
for row in rows:
cur = manager.conn.cursor()
cur.execute("""
SELECT title, content
FROM incident_stages
WHERE parent_id = ?
ORDER BY timestamp ASC
""", (row[0],))
stage_rows = cur.fetchall()
def stage_obj(stage_row):
return {
'title': stage_row[0],
'content': stage_row[1],
}
stages = list(map(stage_obj, stage_rows))
start_timestamp = datetime.datetime.fromtimestamp(row[5])
end_timestamp = datetime.datetime.fromtimestamp(row[6])
res.append({
'id': str(row[0]),
'type': row[1],
'title': row[2],
'content': row[3],
'ongoing': row[4],
'start_timestamp': start_timestamp.isoformat(),
'end_timestamp': end_timestamp.isoformat(),
'stages': stages
})
try:
first = next(iter(res))
except StopIteration:
first = {'ongoing': False}
return response.json({
'all_good': not first['ongoing'],
'incidents': res,
})
@bp.put('/api/incidents')
@auth_route
async def create_incident(request):
return response.text('im gay')

View File

@ -44,6 +44,26 @@ class ServiceManager:
);
""")
self.conn.executescript("""
CREATE TABLE IF NOT EXISTS incidents (
id bigint PRIMARY KEY,
incident_type text,
title text,
content text,
ongoing bool,
start_timestamp bigint,
end_timestamp bigint
);
CREATE TABLE IF NOT EXISTS incident_stages (
parent_id bigint REFERENCES incidents (id) NOT NULL,
timestamp bigint,
title text,
content text,
PRIMARY KEY (parent_id)
);
""")
def _check(self, columns: tuple, field: str, worker_name: str):
chan_name = f'{field}:{worker_name}'

View File

@ -4125,11 +4125,13 @@
},
"balanced-match": {
"version": "1.0.0",
"bundled": true
"bundled": true,
"optional": true
},
"brace-expansion": {
"version": "1.1.11",
"bundled": true,
"optional": true,
"requires": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
@ -4142,15 +4144,18 @@
},
"code-point-at": {
"version": "1.1.0",
"bundled": true
"bundled": true,
"optional": true
},
"concat-map": {
"version": "0.0.1",
"bundled": true
"bundled": true,
"optional": true
},
"console-control-strings": {
"version": "1.1.0",
"bundled": true
"bundled": true,
"optional": true
},
"core-util-is": {
"version": "1.0.2",
@ -4253,7 +4258,8 @@
},
"inherits": {
"version": "2.0.3",
"bundled": true
"bundled": true,
"optional": true
},
"ini": {
"version": "1.3.5",
@ -4263,6 +4269,7 @@
"is-fullwidth-code-point": {
"version": "1.0.0",
"bundled": true,
"optional": true,
"requires": {
"number-is-nan": "^1.0.0"
}
@ -4275,17 +4282,20 @@
"minimatch": {
"version": "3.0.4",
"bundled": true,
"optional": true,
"requires": {
"brace-expansion": "^1.1.7"
}
},
"minimist": {
"version": "0.0.8",
"bundled": true
"bundled": true,
"optional": true
},
"minipass": {
"version": "2.2.4",
"bundled": true,
"optional": true,
"requires": {
"safe-buffer": "^5.1.1",
"yallist": "^3.0.0"
@ -4302,6 +4312,7 @@
"mkdirp": {
"version": "0.5.1",
"bundled": true,
"optional": true,
"requires": {
"minimist": "0.0.8"
}
@ -4374,7 +4385,8 @@
},
"number-is-nan": {
"version": "1.0.1",
"bundled": true
"bundled": true,
"optional": true
},
"object-assign": {
"version": "4.1.1",
@ -4384,6 +4396,7 @@
"once": {
"version": "1.4.0",
"bundled": true,
"optional": true,
"requires": {
"wrappy": "1"
}
@ -4489,6 +4502,7 @@
"string-width": {
"version": "1.0.2",
"bundled": true,
"optional": true,
"requires": {
"code-point-at": "^1.0.0",
"is-fullwidth-code-point": "^1.0.0",

18
run.py
View File

@ -7,8 +7,10 @@ from sanic_cors import CORS
from sanic.exceptions import NotFound, FileNotFound
import config
from elstat import manager
from elstat.blueprints import api, streaming
from elstat.manager import ServiceManager
from elstat.blueprints import api, streaming, incidents
from elstat.blueprints.errors import ApiError
logging.basicConfig(level=logging.INFO)
log = logging.getLogger(__name__)
@ -18,6 +20,7 @@ app.cfg = config
CORS(app, automatic_options=True)
app.blueprint(api)
app.blueprint(incidents)
app.blueprint(streaming)
@ -25,7 +28,7 @@ app.blueprint(streaming)
async def _app_start(refapp, loop):
refapp.session = aiohttp.ClientSession(loop=loop)
refapp.conn = sqlite3.connect('elstat.db')
refapp.manager = manager.ServiceManager(app)
refapp.manager = ServiceManager(app)
@app.listener('after_server_stop')
@ -34,6 +37,15 @@ async def _app_stop(refapp, _loop):
refapp.conn.close()
@app.exception(ApiError)
async def _handle_api_err(request, exception):
return response.json({
'error': True,
'code': exception.status_code,
'message': exception.message
}, status=exception.status_code)
@app.exception(Exception)
async def _handle_exc(request, exception):
log.exception('oopsie woopsie')