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
|
# edit config.py as you wish
|
||||||
cp config.example.py config.py
|
cp config.example.py config.py
|
||||||
|
|
||||||
|
# build frontend
|
||||||
|
cd priv/frontend
|
||||||
|
# check instructions on README.md
|
||||||
```
|
```
|
||||||
|
|
||||||
## Run
|
## Run
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
PORT = 8069
|
PORT = 8069
|
||||||
|
|
||||||
|
PASSWORD = '123456'
|
||||||
|
|
||||||
SERVICES = {
|
SERVICES = {
|
||||||
'elixire': {
|
'elixire': {
|
||||||
'description': "elixi.re's backend",
|
'description': "elixi.re's backend",
|
||||||
|
|
|
@ -3,6 +3,7 @@ import time
|
||||||
import re
|
import re
|
||||||
|
|
||||||
PING_RGX = re.compile(r'(.+)( 0% packet loss)(.+)', re.I | re.M)
|
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:
|
class Adapter:
|
||||||
|
@ -20,7 +21,7 @@ class PingAdapter(Adapter):
|
||||||
"""Ping the given address and report if
|
"""Ping the given address and report if
|
||||||
any packet loss happened."""
|
any packet loss happened."""
|
||||||
spec = {
|
spec = {
|
||||||
'db': ('timestamp', 'status')
|
'db': ('timestamp', 'status', 'latency')
|
||||||
}
|
}
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
@ -34,10 +35,25 @@ class PingAdapter(Adapter):
|
||||||
out, err = map(lambda s: s.decode('utf-8'),
|
out, err = map(lambda s: s.decode('utf-8'),
|
||||||
await process.communicate())
|
await process.communicate())
|
||||||
|
|
||||||
alive = bool(re.search(PING_RGX, out + err))
|
out += err
|
||||||
worker.log.info(f'{worker.name}: alive? {alive}')
|
|
||||||
|
|
||||||
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):
|
class HttpAdapter(Adapter):
|
||||||
|
|
|
@ -1,2 +1,3 @@
|
||||||
from .api import bp as api
|
from .api import bp as api
|
||||||
|
from .incidents import bp as incidents
|
||||||
from .streaming import bp as streaming
|
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):
|
def _check(self, columns: tuple, field: str, worker_name: str):
|
||||||
chan_name = f'{field}:{worker_name}'
|
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": {
|
"balanced-match": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"bundled": true
|
"bundled": true,
|
||||||
|
"optional": true
|
||||||
},
|
},
|
||||||
"brace-expansion": {
|
"brace-expansion": {
|
||||||
"version": "1.1.11",
|
"version": "1.1.11",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
|
"optional": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"balanced-match": "^1.0.0",
|
"balanced-match": "^1.0.0",
|
||||||
"concat-map": "0.0.1"
|
"concat-map": "0.0.1"
|
||||||
|
@ -4142,15 +4144,18 @@
|
||||||
},
|
},
|
||||||
"code-point-at": {
|
"code-point-at": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"bundled": true
|
"bundled": true,
|
||||||
|
"optional": true
|
||||||
},
|
},
|
||||||
"concat-map": {
|
"concat-map": {
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"bundled": true
|
"bundled": true,
|
||||||
|
"optional": true
|
||||||
},
|
},
|
||||||
"console-control-strings": {
|
"console-control-strings": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"bundled": true
|
"bundled": true,
|
||||||
|
"optional": true
|
||||||
},
|
},
|
||||||
"core-util-is": {
|
"core-util-is": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
|
@ -4253,7 +4258,8 @@
|
||||||
},
|
},
|
||||||
"inherits": {
|
"inherits": {
|
||||||
"version": "2.0.3",
|
"version": "2.0.3",
|
||||||
"bundled": true
|
"bundled": true,
|
||||||
|
"optional": true
|
||||||
},
|
},
|
||||||
"ini": {
|
"ini": {
|
||||||
"version": "1.3.5",
|
"version": "1.3.5",
|
||||||
|
@ -4263,6 +4269,7 @@
|
||||||
"is-fullwidth-code-point": {
|
"is-fullwidth-code-point": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
|
"optional": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"number-is-nan": "^1.0.0"
|
"number-is-nan": "^1.0.0"
|
||||||
}
|
}
|
||||||
|
@ -4275,17 +4282,20 @@
|
||||||
"minimatch": {
|
"minimatch": {
|
||||||
"version": "3.0.4",
|
"version": "3.0.4",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
|
"optional": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"brace-expansion": "^1.1.7"
|
"brace-expansion": "^1.1.7"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"minimist": {
|
"minimist": {
|
||||||
"version": "0.0.8",
|
"version": "0.0.8",
|
||||||
"bundled": true
|
"bundled": true,
|
||||||
|
"optional": true
|
||||||
},
|
},
|
||||||
"minipass": {
|
"minipass": {
|
||||||
"version": "2.2.4",
|
"version": "2.2.4",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
|
"optional": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"safe-buffer": "^5.1.1",
|
"safe-buffer": "^5.1.1",
|
||||||
"yallist": "^3.0.0"
|
"yallist": "^3.0.0"
|
||||||
|
@ -4302,6 +4312,7 @@
|
||||||
"mkdirp": {
|
"mkdirp": {
|
||||||
"version": "0.5.1",
|
"version": "0.5.1",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
|
"optional": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"minimist": "0.0.8"
|
"minimist": "0.0.8"
|
||||||
}
|
}
|
||||||
|
@ -4374,7 +4385,8 @@
|
||||||
},
|
},
|
||||||
"number-is-nan": {
|
"number-is-nan": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"bundled": true
|
"bundled": true,
|
||||||
|
"optional": true
|
||||||
},
|
},
|
||||||
"object-assign": {
|
"object-assign": {
|
||||||
"version": "4.1.1",
|
"version": "4.1.1",
|
||||||
|
@ -4384,6 +4396,7 @@
|
||||||
"once": {
|
"once": {
|
||||||
"version": "1.4.0",
|
"version": "1.4.0",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
|
"optional": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"wrappy": "1"
|
"wrappy": "1"
|
||||||
}
|
}
|
||||||
|
@ -4489,6 +4502,7 @@
|
||||||
"string-width": {
|
"string-width": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
|
"optional": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"code-point-at": "^1.0.0",
|
"code-point-at": "^1.0.0",
|
||||||
"is-fullwidth-code-point": "^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
|
from sanic.exceptions import NotFound, FileNotFound
|
||||||
|
|
||||||
import config
|
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)
|
logging.basicConfig(level=logging.INFO)
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
@ -18,6 +20,7 @@ app.cfg = config
|
||||||
CORS(app, automatic_options=True)
|
CORS(app, automatic_options=True)
|
||||||
|
|
||||||
app.blueprint(api)
|
app.blueprint(api)
|
||||||
|
app.blueprint(incidents)
|
||||||
app.blueprint(streaming)
|
app.blueprint(streaming)
|
||||||
|
|
||||||
|
|
||||||
|
@ -25,7 +28,7 @@ app.blueprint(streaming)
|
||||||
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.manager = manager.ServiceManager(app)
|
refapp.manager = ServiceManager(app)
|
||||||
|
|
||||||
|
|
||||||
@app.listener('after_server_stop')
|
@app.listener('after_server_stop')
|
||||||
|
@ -34,6 +37,15 @@ async def _app_stop(refapp, _loop):
|
||||||
refapp.conn.close()
|
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)
|
@app.exception(Exception)
|
||||||
async def _handle_exc(request, exception):
|
async def _handle_exc(request, exception):
|
||||||
log.exception('oopsie woopsie')
|
log.exception('oopsie woopsie')
|
||||||
|
|
Loading…
Reference in a new issue