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 # 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 poll_services import start_async_loop
from mem import services, app, db from mem import services, app, db
import threading import threading
@@ -7,6 +7,29 @@ from flask_migrate import upgrade, stamp
from pathlib import Path 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
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 # Init and upgrade
with app.app_context(): with app.app_context():
@@ -34,7 +57,30 @@ with app.app_context():
@app.route("/") @app.route("/")
def homepage(): 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") @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, nullable=False,
) )
ping: Optional[int] = db.Column(db.Integer, nullable=True) 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__() super().__init__()
self.service_id = service_id self.service_id = service_id
self.ping = ping self.ping = ping
self.dateCreated = datetime.now(timezone.utc) self.dateCreated = datetime.now(timezone.utc)
self.timeout = timeout
def to_dict(self) -> dict[str, Any]: def to_dict(self) -> dict[str, Any]:
return { return {
@@ -27,6 +31,7 @@ class log(db.Model):
"service_id": self.service_id, "service_id": self.service_id,
"ping": self.ping, "ping": self.ping,
"dateCreated": self.dateCreated, "dateCreated": self.dateCreated,
"timeout": self.timeout,
} }

View File

@@ -6,31 +6,32 @@ from models import log, service
from sqlalchemy.orm import sessionmaker 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: async def check_service(client: aiohttp.ClientSession, s: service) -> log:
try: try:
timeout = aiohttp.client.ClientTimeout(total=4) # TODO: Use aiohttp latency timing rather than timing it manually
before = time.perf_counter() before = time.perf_counter()
match s.ping_method: status = await ping(client=client, s=s)
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")
after = time.perf_counter() after = time.perf_counter()
if r.status == 200: if status == 200:
return log(service_id=s.id, ping=int((after - before) * 1000)) return log(service_id=s.id, ping=int((after - before) * 1000))
else: else:
return log(service_id=s.id, ping=None) return log(service_id=s.id, ping=None)
@@ -49,9 +50,11 @@ def start_async_loop():
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
with app.app_context(): with app.app_context():
WorkerSession = sessionmaker(bind=db.engine) WorkerSession = sessionmaker(bind=db.engine)
client = aiohttp.ClientSession() timeout = aiohttp.client.ClientTimeout(total=4)
client = aiohttp.ClientSession(timeout=timeout, auto_decompress=False)
while True: while True:
session = WorkerSession() session = WorkerSession()
sleeptask = asyncio.create_task(asyncio.sleep(5)) sleeptask = asyncio.create_task(asyncio.sleep(5))