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 models import service, log
from typing import Any, Optional, cast from typing import Any, Optional, cast
import json 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]]]: # Prepares log data for chart.js chart
if len(logs) <= 0: 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 ([], []) return ([], [])
x = [logs[0].dateCreated.isoformat()] x = [logs[0].dateCreatedUTC().isoformat()]
y = [logs[0].ping] y = [logs[0].ping]
for i in range(1, len(logs)): for i in range(1, len(logs)):
log1 = logs[i] log1 = logs[i]
log2 = logs[i - 1] 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) y.append(None)
x.append(log1.dateCreated.isoformat()) x.append(log1.dateCreatedUTC().isoformat())
y.append(log1.ping) y.append(log1.ping)
return (x, y) return (x, y)
@@ -74,12 +81,17 @@ def chart(id: int):
) )
else: else:
return abort(code=403) 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( return render_template(
"chart.html", "chart.html",
dates=x, dates=x,
values=json.dumps(y), 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 = { const data = {
labels: chartDates, labels: chartDates,
datasets: [{ datasets: [{
label: 'Example Data', label: 'Ping',
data: {{ values }}, data: {{ values }},
}] }]
}; };
const ctx = document.getElementById('myChart').getContext('2d'); 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, { new Chart(ctx, {
type: 'line', type: 'line',
data: data, data: data,
@@ -36,8 +43,14 @@
x: { x: {
type: 'time', // Important for datetime axis type: 'time', // Important for datetime axis
time: { 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, "log_id": self.id,
"service_id": self.service_id, "service_id": self.service_id,
"ping": self.ping, "ping": self.ping,
"dateCreated": self.dateCreated, "dateCreated": self.dateCreatedUTC(),
"timeout": self.timeout, "timeout": self.timeout,
} }
def dateCreatedUTC(self) -> datetime:
return self.dateCreated.replace(tzinfo=timezone.utc)
class service(db.Model): class service(db.Model):
id: int = db.Column(db.Integer, primary_key=True) # TODO: Switch to UUID id: int = db.Column(db.Integer, primary_key=True) # TODO: Switch to UUID

View File

@@ -4,21 +4,31 @@ import asyncio
import time import time
from models import log, service from models import log, service
from sqlalchemy.orm import sessionmaker 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: match s.ping_method:
case 0: case 0:
r = await client.head( r = await client.head(
url=s.url, url=s.url,
ssl=True if s.public_access else False, ssl=True if s.public_access else False,
allow_redirects=True, allow_redirects=True,
trace_request_ctx=ctx, # type: ignore
) )
case 1: case 1:
r = await client.get( r = await client.get(
url=s.url, url=s.url,
ssl=True if s.public_access else False, ssl=True if s.public_access else False,
allow_redirects=True, allow_redirects=True,
trace_request_ctx=ctx, # type: ignore
) )
case _: case _:
raise Exception("UNKNOWN PING METHOD") 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: async def check_service(client: aiohttp.ClientSession, s: service) -> log:
try: try:
# TODO: Use aiohttp latency timing rather than timing it manually ctx = SimpleNamespace()
before = time.perf_counter() status = await ping(client=client, s=s, ctx=ctx)
status = await ping(client=client, s=s)
after = time.perf_counter()
if status == 200: 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: else:
return log(service_id=s.id, ping=None) return log(service_id=s.id, ping=None)
except aiohttp.ConnectionTimeoutError: except aiohttp.ConnectionTimeoutError:
@@ -48,16 +56,48 @@ def start_async_loop():
loop.run_forever() 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): async def update_services(loop: asyncio.AbstractEventLoop):
print("Starting service updates...") print("Starting service updates...")
# Create new session # Create new session
with app.app_context(): with app.app_context():
WorkerSession = sessionmaker(bind=db.engine) 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: while True:
session = WorkerSession() session = WorkerSession()
sleeptask = asyncio.create_task(asyncio.sleep(5)) sleeptask = asyncio.create_task(asyncio.sleep(timeout_ / 1000 + 1))
tasks = [ tasks = [
check_service(client=client, s=s) check_service(client=client, s=s)
for s in session.query(service).all() for s in session.query(service).all()