Compare commits

...

5 Commits

5 changed files with 163 additions and 25 deletions

50
app.py
View File

@@ -1,5 +1,5 @@
# import requests as r
from flask import jsonify, render_template, send_file
from flask import jsonify, render_template, send_file, abort
from poll_services import start_async_loop
from mem import services, app, db
import threading
@@ -7,6 +7,29 @@ from flask_migrate import upgrade, stamp
from pathlib import Path
from models import service, log
from typing import Any, Optional, cast
import json
from datetime import timedelta
def split_graph(logs: list[log]) -> tuple[list[str], list[Optional[int]]]:
if len(logs) <= 0:
return ([], [])
x = [logs[0].dateCreated.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())
y.append(None)
x.append(log1.dateCreated.isoformat())
y.append(log1.ping)
return (x, y)
# Init and upgrade
with app.app_context():
@@ -34,7 +57,30 @@ with app.app_context():
@app.route("/")
def homepage():
return render_template("home.html", services=services)
return render_template("home.html")
@app.route("/chart/<int:id>")
def chart(id: int):
with app.app_context():
logs = []
s = db.session.query(service).filter_by(id=id).first()
if s:
logs = cast(
list[log],
s.logs.order_by(log.dateCreated.desc()) # type: ignore
.limit(300)
.all(),
)
else:
return abort(code=403)
x, y = split_graph(logs=logs)
return render_template(
"chart.html",
dates=x,
values=json.dumps(y),
)
@app.route("/api/status")

47
mem/templates/chart.html Normal file
View File

@@ -0,0 +1,47 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Simple Line Chart</title>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-date-fns"></script>
<script src="https://www.chartjs.org/docs/latest/samples/utils.js"></script>
</head>
<body>
<canvas id="myChart"></canvas>
</body>
<script>
const chartDates = ({{ dates | safe }}).map(dt => new Date(dt));
const data = {
labels: chartDates,
datasets: [{
label: 'Example Data',
data: {{ values }},
}]
};
const ctx = document.getElementById('myChart').getContext('2d');
new Chart(ctx, {
type: 'line',
data: data,
options: {
scales: {
x: {
type: 'time', // Important for datetime axis
time: {
unit: 'day'
}
}
}
}
});
</script>
</html>

View File

@@ -0,0 +1,37 @@
"""empty message
Revision ID: 3c05315d5b9b
Revises: f87909a4293b
Create Date: 2025-09-05 09:48:08.561045
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = "3c05315d5b9b"
down_revision = "f87909a4293b"
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table("log", schema=None) as batch_op:
batch_op.add_column(
sa.Column(
"timeout", sa.Boolean(), nullable=False, server_default="false"
)
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table("log", schema=None) as batch_op:
batch_op.drop_column("timeout")
# ### end Alembic commands ###

View File

@@ -13,13 +13,17 @@ class log(db.Model):
nullable=False,
)
ping: Optional[int] = db.Column(db.Integer, nullable=True)
timeout: bool = db.Column(db.Boolean, nullable=False)
def __init__(self, service_id: int, ping: Optional[int]):
def __init__(
self, service_id: int, ping: Optional[int], timeout: bool = False
):
super().__init__()
self.service_id = service_id
self.ping = ping
self.dateCreated = datetime.now(timezone.utc)
self.timeout = timeout
def to_dict(self) -> dict[str, Any]:
return {
@@ -27,6 +31,7 @@ class log(db.Model):
"service_id": self.service_id,
"ping": self.ping,
"dateCreated": self.dateCreated,
"timeout": self.timeout,
}

View File

@@ -6,31 +6,32 @@ from models import log, service
from sqlalchemy.orm import sessionmaker
async def ping(client: aiohttp.ClientSession, s: service) -> int:
match s.ping_method:
case 0:
r = await client.head(
url=s.url,
ssl=True if s.public_access else False,
allow_redirects=True,
)
case 1:
r = await client.get(
url=s.url,
ssl=True if s.public_access else False,
allow_redirects=True,
)
case _:
raise Exception("UNKNOWN PING METHOD")
return r.status
async def check_service(client: aiohttp.ClientSession, s: service) -> log:
try:
timeout = aiohttp.client.ClientTimeout(total=4)
# TODO: Use aiohttp latency timing rather than timing it manually
before = time.perf_counter()
match s.ping_method:
case 0:
r = await client.head(
url=s.url,
ssl=True if s.public_access else False,
allow_redirects=True,
timeout=timeout,
auto_decompress=False,
)
case 1:
r = await client.get(
url=s.url,
ssl=True if s.public_access else False,
allow_redirects=True,
timeout=timeout,
auto_decompress=False,
)
case _:
raise Exception("UNKNOWN PING TYPE")
status = await ping(client=client, s=s)
after = time.perf_counter()
if r.status == 200:
if status == 200:
return log(service_id=s.id, ping=int((after - before) * 1000))
else:
return log(service_id=s.id, ping=None)
@@ -49,9 +50,11 @@ def start_async_loop():
async def update_services(loop: asyncio.AbstractEventLoop):
print("Starting service updates...")
# Create new session
with app.app_context():
WorkerSession = sessionmaker(bind=db.engine)
client = aiohttp.ClientSession()
timeout = aiohttp.client.ClientTimeout(total=4)
client = aiohttp.ClientSession(timeout=timeout, auto_decompress=False)
while True:
session = WorkerSession()
sleeptask = asyncio.create_task(asyncio.sleep(5))