Compare commits

...

4 Commits

5 changed files with 90 additions and 21 deletions

28
app.py
View File

@@ -8,25 +8,32 @@ from pathlib import Path
from models import service, log
from typing import Any, Optional, cast
import json
from datetime import timedelta
from datetime import datetime, timezone, timedelta
from config import timeout
def split_graph(logs: list[log]) -> tuple[list[str], list[Optional[int]]]:
if len(logs) <= 0:
# Prepares log data for chart.js chart
def prepare_chart_data(
logs: list[log],
) -> tuple[list[str], list[Optional[int]]]:
if len(logs) <= 0: # Return empty if there are no logs
return ([], [])
x = [logs[0].dateCreated.isoformat()]
x = [logs[0].dateCreatedUTC().isoformat()]
y = [logs[0].ping]
for i in range(1, len(logs)):
log1 = logs[i]
log2 = logs[i - 1]
if (abs(log1.dateCreated - log2.dateCreated)) > timedelta(seconds=6):
x.append(log2.dateCreated.isoformat())
# Check if the gap in points exceeds a threshold
if (abs(log1.dateCreatedUTC() - log2.dateCreatedUTC())) > timedelta(
milliseconds=1.5 * (timeout + 1000)
):
x.append(log2.dateCreatedUTC().isoformat())
y.append(None)
x.append(log1.dateCreated.isoformat())
x.append(log1.dateCreatedUTC().isoformat())
y.append(log1.ping)
return (x, y)
@@ -74,12 +81,17 @@ def chart(id: int):
)
else:
return abort(code=403)
x, y = split_graph(logs=logs)
x, y = prepare_chart_data(logs=logs)
now = datetime.now(timezone.utc)
max_ = now
min_ = now - timedelta(hours=1)
return render_template(
"chart.html",
dates=x,
values=json.dumps(y),
min=min_.isoformat(),
max=max_.isoformat(),
)

1
config.py Normal file
View File

@@ -0,0 +1 @@
timeout: int = 4000

View File

@@ -22,12 +22,19 @@
const data = {
labels: chartDates,
datasets: [{
label: 'Example Data',
label: 'Ping',
data: {{ values }},
}]
};
const ctx = document.getElementById('myChart').getContext('2d');
// Current time in UTC
const nowUTC = new Date();
// One hour ago in UTC
const oneDayAgoUTC = new Date(nowUTC.getTime() - 24 * 60 * 60 * 1000);
const min = "{{ min }}"
const max = "{{ max }}"
new Chart(ctx, {
type: 'line',
data: data,
@@ -36,8 +43,14 @@
x: {
type: 'time', // Important for datetime axis
time: {
unit: 'day'
}
unit: 'hour',
tooltipFormat: 'HH:mm:ss',
displayFormats: {
hour: 'HH:mm'
}
},
min: min,
max: max
}
}
}

View File

@@ -30,10 +30,13 @@ class log(db.Model):
"log_id": self.id,
"service_id": self.service_id,
"ping": self.ping,
"dateCreated": self.dateCreated,
"dateCreated": self.dateCreatedUTC(),
"timeout": self.timeout,
}
def dateCreatedUTC(self) -> datetime:
return self.dateCreated.replace(tzinfo=timezone.utc)
class service(db.Model):
id: int = db.Column(db.Integer, primary_key=True) # TODO: Switch to UUID

View File

@@ -4,21 +4,31 @@ import asyncio
import time
from models import log, service
from sqlalchemy.orm import sessionmaker
from config import timeout as timeout_
from typing import Optional
from types import SimpleNamespace
async def ping(client: aiohttp.ClientSession, s: service) -> int:
async def ping(
client: aiohttp.ClientSession,
s: service,
ctx: Optional[SimpleNamespace] = None,
) -> int:
ctx = ctx or SimpleNamespace()
match s.ping_method:
case 0:
r = await client.head(
url=s.url,
ssl=True if s.public_access else False,
allow_redirects=True,
trace_request_ctx=ctx, # type: ignore
)
case 1:
r = await client.get(
url=s.url,
ssl=True if s.public_access else False,
allow_redirects=True,
trace_request_ctx=ctx, # type: ignore
)
case _:
raise Exception("UNKNOWN PING METHOD")
@@ -27,12 +37,10 @@ async def ping(client: aiohttp.ClientSession, s: service) -> int:
async def check_service(client: aiohttp.ClientSession, s: service) -> log:
try:
# TODO: Use aiohttp latency timing rather than timing it manually
before = time.perf_counter()
status = await ping(client=client, s=s)
after = time.perf_counter()
ctx = SimpleNamespace()
status = await ping(client=client, s=s, ctx=ctx)
if status == 200:
return log(service_id=s.id, ping=int((after - before) * 1000))
return log(service_id=s.id, ping=int(ctx.duration_ms))
else:
return log(service_id=s.id, ping=None)
except aiohttp.ConnectionTimeoutError:
@@ -48,16 +56,48 @@ def start_async_loop():
loop.run_forever()
def setup_client() -> aiohttp.ClientSession:
timeout = aiohttp.client.ClientTimeout(total=timeout_ / 1000)
# Each request will get its own context
trace_config = aiohttp.TraceConfig()
async def on_start(
session: aiohttp.ClientSession,
context: SimpleNamespace,
params: aiohttp.TraceRequestStartParams,
):
ctx = context.trace_request_ctx
ctx.start = time.perf_counter() # store per-request
async def on_end(
session: aiohttp.ClientSession,
context: SimpleNamespace,
params: aiohttp.TraceRequestEndParams,
):
ctx = context.trace_request_ctx
ctx.end = time.perf_counter()
ctx.duration_ms = int((ctx.end - ctx.start) * 1000)
trace_config.on_request_start.append(on_start)
trace_config.on_request_end.append(on_end)
client = aiohttp.ClientSession(
timeout=timeout, auto_decompress=False, trace_configs=[trace_config]
)
return client
async def update_services(loop: asyncio.AbstractEventLoop):
print("Starting service updates...")
# Create new session
with app.app_context():
WorkerSession = sessionmaker(bind=db.engine)
timeout = aiohttp.client.ClientTimeout(total=4)
client = aiohttp.ClientSession(timeout=timeout, auto_decompress=False)
client = setup_client()
# Actual update loop
while True:
session = WorkerSession()
sleeptask = asyncio.create_task(asyncio.sleep(5))
sleeptask = asyncio.create_task(asyncio.sleep(timeout_ / 1000 + 1))
tasks = [
check_service(client=client, s=s)
for s in session.query(service).all()