merge
This commit is contained in:
commit
f43aeec290
10 changed files with 186 additions and 14 deletions
|
@ -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
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
PORT = 8069
|
||||
|
||||
PASSWORD = '123456'
|
||||
|
||||
SERVICES = {
|
||||
'elixire': {
|
||||
'description': "elixi.re's backend",
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
from .api import bp as api
|
||||
from .incidents import bp as incidents
|
||||
from .streaming import bp as streaming
|
||||
|
|
22
elstat/blueprints/decorators.py
Normal file
22
elstat/blueprints/decorators.py
Normal 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
|
8
elstat/blueprints/errors.py
Normal file
8
elstat/blueprints/errors.py
Normal 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]
|
73
elstat/blueprints/incidents.py
Normal file
73
elstat/blueprints/incidents.py
Normal 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')
|
|
@ -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}'
|
||||
|
||||
|
|
28
priv/frontend/package-lock.json
generated
28
priv/frontend/package-lock.json
generated
|
@ -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
18
run.py
|
@ -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')
|
||||
|
|
Loading…
Reference in a new issue