mirror of
https://framagit.org/framasoft/framaspace/argos.git
synced 2025-04-28 09:52:38 +02:00
Merge branch 'simplify-schema' into 'main'
🗃💄🎨 — Lot of changes: DB, UI and some code improvements Closes #27 See merge request framasoft/framaspace/argos!17
This commit is contained in:
commit
017738ffd7
26 changed files with 430 additions and 2806 deletions
2
Makefile
2
Makefile
|
@ -21,7 +21,7 @@ cog: ## Run cog, to integrate the CLI options to the docs.
|
|||
tests: venv ## Run the tests
|
||||
venv/bin/pytest
|
||||
djlint: venv ## Format the templates
|
||||
venv/bin/djlint --ignore=H030,H031 --lint argos/server/templates/*html
|
||||
venv/bin/djlint --ignore=H030,H031,H006 --profile jinja --lint argos/server/templates/*html
|
||||
pylint: venv ## Runs pylint on the code
|
||||
venv/bin/pylint argos
|
||||
lint: djlint pylint
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
"""Add severity to Task and add severity level UNKNOWN
|
||||
|
||||
Revision ID: e99bc35702c9
|
||||
Revises: 7d480e6f1112
|
||||
Create Date: 2024-02-28 14:14:22.519918
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = 'e99bc35702c9'
|
||||
down_revision: Union[str, None] = '7d480e6f1112'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.execute("ALTER TYPE severity ADD VALUE 'unknown'")
|
||||
op.add_column('tasks',
|
||||
sa.Column('severity',
|
||||
sa.Enum(
|
||||
'ok',
|
||||
'warning',
|
||||
'critical',
|
||||
'unknown',
|
||||
name='severity'),
|
||||
nullable=False))
|
||||
op.add_column('tasks', sa.Column('last_severity_update', sa.DateTime(), nullable=True))
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_column('tasks', 'last_severity_update')
|
||||
op.drop_column('tasks', 'severity')
|
|
@ -24,6 +24,7 @@ class Severity:
|
|||
OK = "ok"
|
||||
WARNING = "warning"
|
||||
CRITICAL = "critical"
|
||||
UNKNOWN = "unknown"
|
||||
|
||||
|
||||
# XXX We could name this Result, but is it could overlap with schemas.Result.
|
||||
|
@ -115,13 +116,15 @@ class BaseCheck:
|
|||
|
||||
- All FAILUREs should be reported as CRITICAL
|
||||
- All SUCCESS should be reported as OK
|
||||
- All ERRORS should be reported as CRITICAL.
|
||||
- All ERRORS should be reported as UNKNOWN.
|
||||
|
||||
This behaviour can be changed in each check, by defining the `finalize` method.
|
||||
XXX Allow this to be tweaked by the config.
|
||||
"""
|
||||
if result.status in (Status.SUCCESS, Status.ERROR):
|
||||
if result.status == Status.SUCCESS:
|
||||
return result.status, Severity.OK
|
||||
if result.status == Status.ERROR:
|
||||
return result.status, Severity.UNKNOWN
|
||||
if result.status == Status.FAILURE:
|
||||
return result.status, Severity.CRITICAL
|
||||
if result.status == Status.ON_CHECK:
|
||||
|
|
|
@ -71,7 +71,7 @@ def agent(server_url, auth, max_tasks, wait_time, log_level):
|
|||
from argos.logging import logger
|
||||
|
||||
logger.setLevel(log_level)
|
||||
agent_ = ArgosAgent(server_url, auth, max_tasks, wait_time)
|
||||
agent_ = ArgosAgent(server, auth, max_tasks, wait_time)
|
||||
asyncio.run(agent_.run())
|
||||
|
||||
|
||||
|
|
|
@ -18,7 +18,7 @@ from typing_extensions import Annotated
|
|||
|
||||
from argos.schemas.utils import string_to_duration
|
||||
|
||||
Severity = Literal["warning", "error", "critical"]
|
||||
Severity = Literal["warning", "error", "critical", "unknown"]
|
||||
|
||||
|
||||
def parse_threshold(value):
|
||||
|
|
|
@ -41,7 +41,7 @@ Status: {severity}
|
|||
Time: {result.submitted_at}
|
||||
Previous status: {old_severity}
|
||||
|
||||
See results of task on {request.url_for('get_task_results', task_id=task.id)}
|
||||
See results of task on {request.url_for('get_task_results_view', task_id=task.id)}
|
||||
"""
|
||||
|
||||
mail = f"""\
|
||||
|
@ -103,7 +103,7 @@ Status: {severity}
|
|||
Time: {result.submitted_at}
|
||||
Previous status: {old_severity}
|
||||
|
||||
See results of task on {request.url_for('get_task_results', task_id=task.id)}
|
||||
See results of task on {request.url_for('get_task_results_view', task_id=task.id)}
|
||||
"""
|
||||
|
||||
payload = {'title': subject,
|
||||
|
|
|
@ -41,6 +41,12 @@ class Task(Base):
|
|||
completed_at: Mapped[datetime] = mapped_column(nullable=True)
|
||||
next_run: Mapped[datetime] = mapped_column(nullable=True)
|
||||
|
||||
severity: Mapped[Literal["ok", "warning", "critical", "unknown"]] = mapped_column(
|
||||
Enum("ok", "warning", "critical", "unknown", name="severity"),
|
||||
insert_default="unknown"
|
||||
)
|
||||
last_severity_update: Mapped[datetime] = mapped_column(nullable=True)
|
||||
|
||||
results: Mapped[List["Result"]] = relationship(back_populates="task")
|
||||
|
||||
def __str__(self):
|
||||
|
@ -50,8 +56,10 @@ class Task(Base):
|
|||
"""Returns a check instance for this specific task"""
|
||||
return get_registered_check(self.check)
|
||||
|
||||
def set_times_and_deselect(self):
|
||||
"""Removes the lock on task and set the time for the next run"""
|
||||
def set_times_severity_and_deselect(self, severity, submitted_at):
|
||||
"""Removes the lock on task, set its severity and set the time for the next run"""
|
||||
self.severity = severity
|
||||
self.last_severity_update = submitted_at
|
||||
self.selected_by = None
|
||||
self.selected_at = None
|
||||
|
||||
|
@ -73,13 +81,6 @@ class Task(Base):
|
|||
return None
|
||||
return self.last_result.status
|
||||
|
||||
@property
|
||||
def severity(self):
|
||||
"""Get severity of the task"""
|
||||
if not self.last_result:
|
||||
return None
|
||||
return self.last_result.severity
|
||||
|
||||
|
||||
class Result(Base):
|
||||
"""There is multiple results per tasks.
|
||||
|
@ -100,8 +101,8 @@ class Result(Base):
|
|||
status: Mapped[Literal["success", "failure", "error", "on-check"]] = mapped_column(
|
||||
Enum("success", "failure", "error", "on-check", name="status")
|
||||
)
|
||||
severity: Mapped[Literal["ok", "warning", "critical"]] = mapped_column(
|
||||
Enum("ok", "warning", "critical", name="severity")
|
||||
severity: Mapped[Literal["ok", "warning", "critical", "unknown"]] = mapped_column(
|
||||
Enum("ok", "warning", "critical", "unknown", name="severity")
|
||||
)
|
||||
context: Mapped[dict] = mapped_column()
|
||||
|
||||
|
|
|
@ -110,29 +110,28 @@ async def update_from_config(db: Session, config: schemas.Config):
|
|||
|
||||
async def get_severity_counts(db: Session) -> dict:
|
||||
"""Get the severities (ok, warning, critical…) and their count"""
|
||||
# Get the last result of each task
|
||||
subquery = (
|
||||
db.query(Result.task_id, func.max(Result.id).label("max_result_id")) # pylint: disable-msg=not-callable
|
||||
.group_by(Result.task_id)
|
||||
.subquery()
|
||||
)
|
||||
|
||||
# Join this back to get full result rows, and group by status
|
||||
query = (
|
||||
db.query(Result.severity, func.count(Result.id).label("count")) # pylint: disable-msg=not-callable
|
||||
.join(subquery, Result.id == subquery.columns.max_result_id)
|
||||
.group_by(Result.severity)
|
||||
db.query(Task.severity, func.count(Task.id).label("count")) # pylint: disable-msg=not-callable
|
||||
.group_by(Task.severity)
|
||||
)
|
||||
|
||||
# Execute the query and fetch the results
|
||||
task_counts_by_severity = query.all()
|
||||
|
||||
counts_dict = dict(task_counts_by_severity)
|
||||
for key in ("ok", "warning", "critical"):
|
||||
for key in ("ok", "warning", "critical", "unknown"):
|
||||
counts_dict.setdefault(key, 0)
|
||||
return counts_dict
|
||||
|
||||
|
||||
async def reschedule_all(db: Session):
|
||||
"""Reschedule checks of all non OK tasks ASAP"""
|
||||
db.query(Task) \
|
||||
.filter(Task.severity.in_(['warning', 'critical', 'unknown'])) \
|
||||
.update({Task.next_run: datetime.now() - timedelta(days=1)})
|
||||
db.commit()
|
||||
|
||||
|
||||
async def remove_old_results(db: Session, max_results: int):
|
||||
tasks = db.query(Task).all()
|
||||
deleted = 0
|
||||
|
|
|
@ -55,7 +55,7 @@ async def create_results(
|
|||
check = task.get_check()
|
||||
status, severity = await check.finalize(config, result, **result.context)
|
||||
result.set_status(status, severity)
|
||||
task.set_times_and_deselect()
|
||||
task.set_times_severity_and_deselect(severity, result.submitted_at)
|
||||
|
||||
# Don’t create an alert if the severity has not changed
|
||||
if last_severity != severity:
|
||||
|
@ -67,7 +67,40 @@ async def create_results(
|
|||
return {"result_ids": [r.id for r in db_results]}
|
||||
|
||||
|
||||
@route.get("/stats")
|
||||
@route.post("/reschedule/all",
|
||||
responses={
|
||||
200: {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"example": {
|
||||
"msg": "Non OK tasks reschuled"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
async def reschedule_all(request: Request, db: Session = Depends(get_db)):
|
||||
"""Reschedule checks of all non OK tasks ASAP"""
|
||||
await queries.reschedule_all(db)
|
||||
return {"msg": "Non OK tasks reschuled"}
|
||||
|
||||
|
||||
@route.get("/stats",
|
||||
responses={
|
||||
200: {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"example": {
|
||||
"upcoming_tasks_count":0,
|
||||
"results_count":1993085,
|
||||
"selected_tasks_count":1845
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
async def get_stats(db: Session = Depends(get_db)):
|
||||
"""Get tasks statistics"""
|
||||
return {
|
||||
|
@ -77,7 +110,19 @@ async def get_stats(db: Session = Depends(get_db)):
|
|||
}
|
||||
|
||||
|
||||
@route.get("/severities")
|
||||
@route.get("/severities",
|
||||
responses={
|
||||
200: {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"example": {
|
||||
"ok":1541,"warning":0,"critical":0,"unknown":0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
async def get_severity_counts(db: Session = Depends(get_db)):
|
||||
"""Returns the number of results per severity"""
|
||||
return await queries.get_severity_counts(db)
|
||||
|
|
|
@ -5,8 +5,8 @@ from urllib.parse import urlparse
|
|||
|
||||
from fastapi import APIRouter, Depends, Request
|
||||
from fastapi.templating import Jinja2Templates
|
||||
from sqlalchemy import desc
|
||||
from sqlalchemy.orm import Session, aliased
|
||||
from sqlalchemy import func
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from argos.schemas import Config
|
||||
from argos.server import queries
|
||||
|
@ -20,12 +20,12 @@ SEVERITY_LEVELS = {
|
|||
"ok": 1,
|
||||
"warning": 2,
|
||||
"critical": 3,
|
||||
"to-process": 4
|
||||
"unknown": 4
|
||||
}
|
||||
|
||||
|
||||
@route.get("/")
|
||||
async def get_severity_counts(
|
||||
async def get_severity_counts_view(
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
refresh: bool = False,
|
||||
|
@ -48,26 +48,21 @@ async def get_severity_counts(
|
|||
)
|
||||
|
||||
|
||||
@route.get("/details")
|
||||
async def read_tasks(request: Request, db: Session = Depends(get_db)):
|
||||
@route.get("/domains")
|
||||
async def get_domains_view(request: Request, db: Session = Depends(get_db)):
|
||||
"""Show all tasks and their current state"""
|
||||
tasks = db.query(Task).all()
|
||||
|
||||
results = (
|
||||
db.query(Task, Result)
|
||||
.join(Result)
|
||||
.distinct(Task.id)
|
||||
.order_by(Task.id, desc(Result.submitted_at))
|
||||
.all()
|
||||
)
|
||||
|
||||
domains_severities = defaultdict(list)
|
||||
domains_last_checks = defaultdict(list)
|
||||
for task, result in results:
|
||||
severity = result.severity or "to-process"
|
||||
|
||||
for task in tasks:
|
||||
domain = urlparse(task.url).netloc
|
||||
domains_severities[domain].append(severity)
|
||||
domains_last_checks[domain].append(result.submitted_at)
|
||||
domains_severities[domain].append(task.severity)
|
||||
if task.last_severity_update is not None:
|
||||
domains_last_checks[domain] = task.last_severity_update
|
||||
else:
|
||||
domains_last_checks[domain] = 'Waiting to be checked'
|
||||
|
||||
def _max_severity(severities):
|
||||
return max(severities, key=SEVERITY_LEVELS.get)
|
||||
|
@ -84,17 +79,16 @@ async def read_tasks(request: Request, db: Session = Depends(get_db)):
|
|||
return 0
|
||||
|
||||
domains = [(key, _max_severity(value)) for key, value in domains_severities.items()]
|
||||
last_checks = {key: max(value) for key, value in domains_last_checks.items()}
|
||||
domains.sort(key=cmp_to_key(_cmp_domains))
|
||||
|
||||
agents = db.query(Result.agent_id).distinct().all()
|
||||
|
||||
return templates.TemplateResponse(
|
||||
"details.html",
|
||||
"domains.html",
|
||||
{
|
||||
"request": request,
|
||||
"domains": domains,
|
||||
"last_checks": last_checks,
|
||||
"last_checks": domains_last_checks,
|
||||
"total_task_count": len(tasks),
|
||||
"agents": agents,
|
||||
},
|
||||
|
@ -102,18 +96,18 @@ async def read_tasks(request: Request, db: Session = Depends(get_db)):
|
|||
|
||||
|
||||
@route.get("/domain/{domain}")
|
||||
async def get_domain_tasks(
|
||||
async def get_domain_tasks_view(
|
||||
request: Request, domain: str, db: Session = Depends(get_db)
|
||||
):
|
||||
"""Show all tasks attached to a domain"""
|
||||
tasks = db.query(Task).filter(Task.domain.contains(domain)).all() # type: ignore[attr-defined]
|
||||
tasks = db.query(Task).filter(Task.domain.contains(f'//{domain}')).all()
|
||||
return templates.TemplateResponse(
|
||||
"domain.html", {"request": request, "domain": domain, "tasks": tasks}
|
||||
)
|
||||
|
||||
|
||||
@route.get("/result/{result_id}")
|
||||
async def get_result(request: Request, result_id: int, db: Session = Depends(get_db)):
|
||||
async def get_result_view(request: Request, result_id: int, db: Session = Depends(get_db)):
|
||||
"""Show the details of a result"""
|
||||
result = db.query(Result).get(result_id)
|
||||
return templates.TemplateResponse(
|
||||
|
@ -122,7 +116,7 @@ async def get_result(request: Request, result_id: int, db: Session = Depends(get
|
|||
|
||||
|
||||
@route.get("/task/{task_id}/results")
|
||||
async def get_task_results(
|
||||
async def get_task_results_view(
|
||||
request: Request,
|
||||
task_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
|
@ -149,17 +143,11 @@ async def get_task_results(
|
|||
|
||||
|
||||
@route.get("/agents")
|
||||
async def get_agents(request: Request, db: Session = Depends(get_db)):
|
||||
async def get_agents_view(request: Request, db: Session = Depends(get_db)):
|
||||
"""Show argos agents and the last time the server saw them"""
|
||||
t1 = aliased(Result, name="t1")
|
||||
t2 = aliased(Result, name="t2")
|
||||
|
||||
last_seen = (
|
||||
db.query(t1)
|
||||
.outerjoin(
|
||||
t2, (t1.agent_id == t2.agent_id) & (t1.submitted_at < t2.submitted_at)
|
||||
)
|
||||
.filter(t2.agent_id.is_(None))
|
||||
db.query(Result.agent_id, func.max(Result.submitted_at).label('submitted_at'))
|
||||
.group_by(Result.agent_id)
|
||||
.all()
|
||||
)
|
||||
|
||||
|
|
BIN
argos/server/static/logo-64.png
Normal file
BIN
argos/server/static/logo-64.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 9.5 KiB |
BIN
argos/server/static/logo.png
Normal file
BIN
argos/server/static/logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 160 KiB |
File diff suppressed because it is too large
Load diff
5
argos/server/static/pico.min.css
vendored
Normal file
5
argos/server/static/pico.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
argos/server/static/pico.min.css.map
Normal file
1
argos/server/static/pico.min.css.map
Normal file
File diff suppressed because one or more lines are too long
|
@ -1,9 +1,21 @@
|
|||
@import url("pico.css");
|
||||
@import url("pico.min.css");
|
||||
|
||||
code {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
body > header,
|
||||
body > main {
|
||||
padding: 0 !important;
|
||||
}
|
||||
#title {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin-bottom: calc(var(--typography-spacing-vertical) * 0.5);
|
||||
}
|
||||
|
||||
.grid-index {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
@ -14,6 +26,17 @@ code {
|
|||
.grid-index article {
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
padding-bottom: calc(var(--block-spacing-vertical) * 0.7);
|
||||
}
|
||||
.grid-index article > header {
|
||||
margin-bottom: calc(var(--block-spacing-vertical) * 0.7);
|
||||
}
|
||||
|
||||
label[for="select-status"] {
|
||||
display: inline-block;
|
||||
}
|
||||
#select-status {
|
||||
width: initial;
|
||||
}
|
||||
|
||||
.inline-label {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{% extends "base.html" %}
|
||||
{% block title %}<h2>Agents</h2>{% endblock %}
|
||||
{% block title %}<h2>Agents</h2>{% endblock title %}
|
||||
{% block content %}
|
||||
<table role="grid">
|
||||
<thead>
|
||||
|
@ -17,4 +17,4 @@
|
|||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endblock %}
|
||||
{% endblock content %}
|
||||
|
|
|
@ -3,25 +3,69 @@
|
|||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Argos</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=Edge">
|
||||
<link rel="shortcut icon"
|
||||
href="{{ url_for('static', path='/logo.png') }}">
|
||||
<meta name="viewport"
|
||||
content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="X-UA-Compatible"
|
||||
content="IE=Edge">
|
||||
{% if refresh %}
|
||||
<meta http-equiv="refresh"
|
||||
content="{{ delay }}">
|
||||
{% endif %}
|
||||
<link href="{{ url_for('static', path='/styles.css') }}"
|
||||
rel="stylesheet">
|
||||
<link rel="stylesheet"
|
||||
href="{{ url_for('static', path='/styles.css') }}">
|
||||
</head>
|
||||
<body>
|
||||
<header class="container">
|
||||
<nav>
|
||||
<ul>
|
||||
<li>
|
||||
<img src="{{ url_for('static', path='/logo-64.png') }}"
|
||||
width="64"
|
||||
height="64"
|
||||
alt="">
|
||||
</li>
|
||||
<li>
|
||||
<h1 id="title">Argos monitoring</h1>
|
||||
</li>
|
||||
</ul>
|
||||
<ul>
|
||||
<li>
|
||||
<a href="{{ url_for('get_severity_counts_view') }}"
|
||||
class="outline {{ 'contrast' if request.url == url_for('get_severity_counts_view') }}"
|
||||
role="button">
|
||||
Dashboard
|
||||
</a>
|
||||
<a href="{{ url_for('get_domains_view') }}"
|
||||
class="outline {{ 'contrast' if request.url == url_for('get_domains_view') }}"
|
||||
role="button">
|
||||
Domains
|
||||
</a>
|
||||
<a href="{{ url_for('get_agents_view') }}"
|
||||
class="outline {{ 'contrast' if request.url == url_for('get_agents_view') }}"
|
||||
role="button">
|
||||
Agents
|
||||
</a>
|
||||
<a href="#"
|
||||
id="reschedule-all"
|
||||
class="outline"
|
||||
title="Reschedule non-ok checks as soon as possible">
|
||||
🕐
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</header>
|
||||
<main class="container">
|
||||
<dialog id="msg"></dialog>
|
||||
<div id="header">
|
||||
{% block title %}
|
||||
<a href="/"><h1 id="title">Argos monitoring</h1></a>
|
||||
{% endblock %}
|
||||
{% endblock title %}
|
||||
</div>
|
||||
<div id="content">
|
||||
{% block content %}
|
||||
{% endblock %}
|
||||
{% endblock content %}
|
||||
</div>
|
||||
</main>
|
||||
<footer class="text-center">
|
||||
|
@ -34,5 +78,21 @@
|
|||
or
|
||||
<a href="{{ url_for('get_severity_counts_view') }}redoc">Redoc</a>
|
||||
</footer>
|
||||
<script>
|
||||
async function rescheduleAll() {
|
||||
const response = await fetch('{{ url_for("reschedule_all") }}', {method: 'POST'});
|
||||
const json = await response.json();
|
||||
const dialog = document.getElementById('msg');
|
||||
dialog.innerText = json.msg;
|
||||
dialog.setAttribute('open', '');
|
||||
setTimeout(() => {
|
||||
dialog.removeAttribute('open');
|
||||
}, 1500);
|
||||
}
|
||||
document.getElementById('reschedule-all').addEventListener('click', event => {
|
||||
event.preventDefault();
|
||||
rescheduleAll();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -1,33 +0,0 @@
|
|||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<div id="domains" class="frame">
|
||||
<p>
|
||||
{{ domains | length}} domains,
|
||||
{{ total_task_count }} tasks,
|
||||
<a href="/agents">{{ agents | length }} agent{% if agents | length > 1 %}s{% endif %}</a>
|
||||
</p>
|
||||
<table id="domains-list" role="grid">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Domain</th>
|
||||
<th class="today">Current status</th>
|
||||
<th>Last check</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody id="domains-body">
|
||||
{% for (domain, status) in domains %}
|
||||
<tr>
|
||||
<td>
|
||||
<a href="/domain/{{ domain }}">{{ domain }}</a>
|
||||
</td>
|
||||
<td class="status highlight">
|
||||
{% if status == "ok" %}✅ OK {% elif statuts == "warning"%}⚠ Warning{% elif status == "critical"%}❌ Critical{% elif status == "to-process" %}⏱︎ Waiting for the jobs{% endif %}
|
||||
</td>
|
||||
<td>{{ last_checks.get(domain) }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endblock %}
|
|
@ -1,5 +1,5 @@
|
|||
{% extends "base.html" %}
|
||||
{% block title %}<h2>{{ domain }}</h2>{% endblock %}
|
||||
{% block title %}<h2>{{ domain }}</h2>{% endblock title %}
|
||||
{% block content %}
|
||||
<div id="domains" class="frame">
|
||||
<table id="domains-list" role="grid">
|
||||
|
@ -20,12 +20,27 @@
|
|||
<td>{{ task.check }}</td>
|
||||
<td>{{ task.expected }}</td>
|
||||
<td class="status highlight">
|
||||
<a data-tooltip="Completed at {{ task.completed_at }}" href="/result/{{ task.last_result.id }}">{% if task.status == "success" %}✅ Success {% elif task.status == "error"%}⚠ Error{% elif task.status == "failure"%}❌ Failure{% endif %}</a>
|
||||
{% if task.status %}
|
||||
<a data-tooltip="Completed at {{ task.completed_at }}"
|
||||
href="{{ url_for('get_result_view', result_id=task.last_result.id) }}">
|
||||
{% if task.status == "success" %}
|
||||
✅ Success
|
||||
{% elif task.status == "error" %}
|
||||
⚠ Error
|
||||
{% elif task.status == "failure" %}
|
||||
❌ Failure
|
||||
{% elif task.status == "unknown" %}
|
||||
❔ Unknown
|
||||
{% endif %}
|
||||
</a>
|
||||
{% else %}
|
||||
Waiting to be checked
|
||||
{% endif %}
|
||||
</td>
|
||||
<td><a href="/task/{{task.id}}/results">view all</a></td>
|
||||
<td><a href="{{ url_for('get_task_results_view', task_id=task.id) }}">view all</a></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% endblock content %}
|
||||
|
|
79
argos/server/templates/domains.html
Normal file
79
argos/server/templates/domains.html
Normal file
|
@ -0,0 +1,79 @@
|
|||
{% extends "base.html" %}
|
||||
{% block title %}<h2>Tasks list</h2>{% endblock title %}
|
||||
{% block content %}
|
||||
<div id="domains" class="frame">
|
||||
<nav>
|
||||
<ul>
|
||||
<li>
|
||||
{{ domains | length }} domains,
|
||||
{{ total_task_count }} tasks,
|
||||
<a href="{{ url_for('get_agents_view') }}">
|
||||
{{ agents | length }} agent{{ 's' if agents | length > 1 }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<ul>
|
||||
<li>
|
||||
<label for="select-status">Show domains with status:</label>
|
||||
<select id="select-status">
|
||||
<option value="all">All</option>
|
||||
<option value="ok">✅ OK</option>
|
||||
<option value="warning">⚠️ Warning</option>
|
||||
<option value="critical">❌ Critical</option>
|
||||
<option value="unknown">❔ Unknown</option>
|
||||
</select>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
<table id="domains-list" role="grid">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Domain</th>
|
||||
<th class="today">Current status</th>
|
||||
<th>Last check</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody id="domains-body">
|
||||
{% for (domain, status) in domains %}
|
||||
<tr data-status={{ status }}>
|
||||
<td>
|
||||
<a href="{{ url_for('get_domain_tasks_view', domain=domain) }}">
|
||||
{{ domain }}
|
||||
</a>
|
||||
</td>
|
||||
<td class="status highlight">
|
||||
{% if status == "ok" %}
|
||||
✅ OK
|
||||
{% elif status == "warning" %}
|
||||
⚠️ Warning
|
||||
{% elif status == "critical" %}
|
||||
❌ Critical
|
||||
{% elif status == "unknown" %}
|
||||
❔ Unknown
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ last_checks.get(domain) }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<script>
|
||||
document.getElementById('select-status').addEventListener('change', (e) => {
|
||||
if (e.currentTarget.value === 'all') {
|
||||
document.querySelectorAll('[data-status]').forEach((item) => {
|
||||
item.style.display = null;
|
||||
})
|
||||
} else {
|
||||
document.querySelectorAll('[data-status]').forEach((item) => {
|
||||
if (item.dataset.status === e.currentTarget.value) {
|
||||
item.style.display = null;
|
||||
} else {
|
||||
item.style.display = 'none';
|
||||
}
|
||||
})
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock content %}
|
|
@ -1,5 +1,5 @@
|
|||
{% extends "base.html" %}
|
||||
{% block title %}<h2>Dashboard</h2>{% endblock %}
|
||||
{% block title %}<h2>Dashboard</h2>{% endblock title %}
|
||||
{% block content %}
|
||||
<div id="domains" class="frame">
|
||||
<nav>
|
||||
|
@ -44,19 +44,19 @@
|
|||
<div class="grid grid-index">
|
||||
<article>
|
||||
<header title="Unknown">❔</header>
|
||||
<span id="sev-unknown">{{ counts_dict['unknown'] }}</span>
|
||||
{{ counts_dict['unknown'] }}
|
||||
</article>
|
||||
<article>
|
||||
<header title="OK">✅</header>
|
||||
<span id="sev-ok">{{ counts_dict['ok'] }}</span>
|
||||
{{ counts_dict['ok'] }}
|
||||
</article>
|
||||
<article>
|
||||
<header title="Warning">⚠️</header>
|
||||
<span id="sev-warning">{{ counts_dict['warning'] }}</span>
|
||||
{{ counts_dict['warning'] }}
|
||||
</article>
|
||||
<article>
|
||||
<header title="Critical">❌</header>
|
||||
<span id="sev-critical">{{ counts_dict['critical'] }}</span>
|
||||
{{ counts_dict['critical'] }}
|
||||
</article>
|
||||
</div>
|
||||
<p class="text-center">
|
||||
|
@ -68,4 +68,4 @@
|
|||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% endblock content %}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{% extends "base.html" %}
|
||||
{% block title %}<h2>{{ result }}</h2>{% endblock %}
|
||||
{% block title %}<h2>{{ result }}</h2>{% endblock title %}
|
||||
{% block content %}
|
||||
<dl>
|
||||
<dt>Task</dt>
|
||||
|
@ -13,4 +13,4 @@
|
|||
<dt>Context</dt>
|
||||
<dd>{{ result.context }}</dd>
|
||||
</dl>
|
||||
{% endblock %}
|
||||
{% endblock content %}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{% extends "base.html" %}
|
||||
{% block title %}<h2>{{ task }}</h2>{% endblock %}
|
||||
{% block title %}<h2>{{ task }}</h2>{% endblock title %}
|
||||
{% block content %}
|
||||
<code>{{ description }}</code>
|
||||
<table role="grid">
|
||||
|
@ -23,4 +23,4 @@
|
|||
</tbody>
|
||||
</table>
|
||||
|
||||
{% endblock %}
|
||||
{% endblock content %}
|
||||
|
|
|
@ -2,13 +2,14 @@
|
|||
|
||||
## Python packages
|
||||
|
||||
- [Click](https://click.palletsprojects.com/) for the command-line interface ;
|
||||
- [FastAPI](https://fastapi.tiangolo.com/) is the framework that allows us to expose the HTTP API ;
|
||||
- [HTTPX](https://www.python-httpx.org/) is used to issue asynchronous requests in the agents ;
|
||||
- [Jinja](https://jinja.palletsprojects.com/) is handling the templating ;
|
||||
- [Pydantic](https://pydantic.dev/) is useful to ensure the data matches our expectactions ;
|
||||
- [SQLAlchemy](https://www.sqlalchemy.org/) is the ORM we use, to connect to our database and issue queries ;
|
||||
- [Tenacity](https://github.com/jd/tenacity) a small utility to retry a function in case an error occured ;
|
||||
- [Click](https://click.palletsprojects.com/) for the command-line interface;
|
||||
- [FastAPI](https://fastapi.tiangolo.com/) is the framework that allows us to expose the HTTP API;
|
||||
- [HTTPX](https://www.python-httpx.org/) is used to issue asynchronous requests in the agents;
|
||||
- [Jinja](https://jinja.palletsprojects.com/) is handling the templating;
|
||||
- [Pydantic](https://pydantic.dev/) is useful to ensure the data matches our expectactions;
|
||||
- [SQLAlchemy](https://www.sqlalchemy.org/) is the ORM we use, to connect to our database and issue queries;
|
||||
- [Alembic](https://alembic.sqlalchemy.org) is used for DB migrations;
|
||||
- [Tenacity](https://github.com/jd/tenacity) a small utility to retry a function in case an error occured;
|
||||
- [Uvicorn](https://www.uvicorn.org/) is the tool used to run our server.
|
||||
|
||||
## CSS framework
|
||||
|
|
|
@ -123,6 +123,21 @@ async def test_update_from_config_db_updates_existing_tasks(db, empty_config, ta
|
|||
assert db.query(Task).count() == 1
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_reschedule_all(db, ten_tasks, ten_warning_tasks, ten_critical_tasks, ten_ok_tasks):
|
||||
assert db.query(Task).count() == 40
|
||||
assert db.query(Task).filter(Task.severity == "unknown").count() == 10
|
||||
assert db.query(Task).filter(Task.severity == "warning").count() == 10
|
||||
assert db.query(Task).filter(Task.severity == "critical").count() == 10
|
||||
assert db.query(Task).filter(Task.severity == "ok").count() == 10
|
||||
|
||||
one_hour_ago = datetime.now() - timedelta(hours=1)
|
||||
assert db.query(Task).filter(Task.next_run <= one_hour_ago).count() == 0
|
||||
|
||||
await queries.reschedule_all(db)
|
||||
assert db.query(Task).filter(Task.next_run <= one_hour_ago).count() == 30
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def task(db):
|
||||
task = Task(
|
||||
|
@ -215,3 +230,63 @@ def ten_tasks(db):
|
|||
tasks.append(task)
|
||||
db.commit()
|
||||
return tasks
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def ten_warning_tasks(db):
|
||||
now = datetime.now()
|
||||
tasks = []
|
||||
for i in range(10):
|
||||
task = Task(
|
||||
url="https://www.example.com",
|
||||
domain="example.com",
|
||||
check="body-contains",
|
||||
expected="foo",
|
||||
frequency=1,
|
||||
next_run=now,
|
||||
severity="warning"
|
||||
)
|
||||
db.add(task)
|
||||
tasks.append(task)
|
||||
db.commit()
|
||||
return tasks
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def ten_critical_tasks(db):
|
||||
now = datetime.now()
|
||||
tasks = []
|
||||
for i in range(10):
|
||||
task = Task(
|
||||
url="https://www.example.com",
|
||||
domain="example.com",
|
||||
check="body-contains",
|
||||
expected="foo",
|
||||
frequency=1,
|
||||
next_run=now,
|
||||
severity="critical"
|
||||
)
|
||||
db.add(task)
|
||||
tasks.append(task)
|
||||
db.commit()
|
||||
return tasks
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def ten_ok_tasks(db):
|
||||
now = datetime.now()
|
||||
tasks = []
|
||||
for i in range(10):
|
||||
task = Task(
|
||||
url="https://www.example.com",
|
||||
domain="example.com",
|
||||
check="body-contains",
|
||||
expected="foo",
|
||||
frequency=1,
|
||||
next_run=now,
|
||||
severity="ok"
|
||||
)
|
||||
db.add(task)
|
||||
tasks.append(task)
|
||||
db.commit()
|
||||
return tasks
|
||||
|
|
Loading…
Reference in a new issue