mirror of
https://framagit.org/framasoft/framaspace/argos.git
synced 2025-04-29 18:32:38 +02:00
Merge branch 'fix-15' into 'main'
✨ — Implement mail and gotify alerts. Fix #15 and #16 Closes #16 et #15 See merge request framasoft/framaspace/argos!13
This commit is contained in:
commit
a4e6799fd0
8 changed files with 184 additions and 12 deletions
|
@ -441,7 +441,9 @@ disable=raw-checker-failed,
|
||||||
singleton-comparison,
|
singleton-comparison,
|
||||||
missing-module-docstring,
|
missing-module-docstring,
|
||||||
missing-class-docstring,
|
missing-class-docstring,
|
||||||
missing-function-docstring
|
missing-function-docstring,
|
||||||
|
too-many-arguments,
|
||||||
|
too-many-locals,
|
||||||
|
|
||||||
# Enable the message, report, category or checker with the given id(s). You can
|
# Enable the message, report, category or checker with the given id(s). You can
|
||||||
# either give multiple identifier separated by comma (,) or put this option
|
# either give multiple identifier separated by comma (,) or put this option
|
||||||
|
|
|
@ -22,13 +22,13 @@ Internally, a HTTP API is exposed, and a job queue is used to distribute the che
|
||||||
- [X] donner un aperçu rapide de l’état de la supervision.
|
- [X] donner un aperçu rapide de l’état de la supervision.
|
||||||
- [ ] Use background tasks for alerting (#23)
|
- [ ] Use background tasks for alerting (#23)
|
||||||
- [ ] Delete outdated tasks from config (#19, !25)
|
- [ ] Delete outdated tasks from config (#19, !25)
|
||||||
- [ ] Implement alerting tasks (#15, 16, !13)
|
- [X] Implement alerting tasks (#15, 16, !13)
|
||||||
- [ ] Handles multiple alerting backends (email, sms, gotify) (!13)
|
- [X] Handles multiple alerting backends (email, sms, gotify) (!13)
|
||||||
- [ ] add an "unknown" severity for check errors (!17)
|
- [ ] add an "unknown" severity for check errors (!17)
|
||||||
- [ ] Add a command to generate new authentication token (#22)
|
- [ ] Add a command to generate new authentication token (#22)
|
||||||
- [ ] Add a way to specify the severity of the alerts in the config
|
- [ ] Add a way to specify the severity of the alerts in the config
|
||||||
- [ ] Allow passing a dict to check
|
- [ ] Allow passing a dict to check
|
||||||
- [ ] Un flag de configuration permet d’ajouter automatiquement un job de vérification de redirection 301 de la version HTTP vers HTTPS
|
- [ ] A configuration flag can automatically add a check of 301 redirection from HTTP to HTTPS
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,15 @@ For database models, see argos.server.models.
|
||||||
"""
|
"""
|
||||||
from typing import Dict, List, Literal, Optional, Tuple
|
from typing import Dict, List, Literal, Optional, Tuple
|
||||||
|
|
||||||
from pydantic import BaseModel, ConfigDict, HttpUrl, field_validator
|
from pydantic import (
|
||||||
|
BaseModel,
|
||||||
|
ConfigDict,
|
||||||
|
HttpUrl,
|
||||||
|
StrictBool,
|
||||||
|
EmailStr,
|
||||||
|
PositiveInt,
|
||||||
|
field_validator,
|
||||||
|
)
|
||||||
from pydantic.functional_validators import BeforeValidator
|
from pydantic.functional_validators import BeforeValidator
|
||||||
from typing_extensions import Annotated
|
from typing_extensions import Annotated
|
||||||
|
|
||||||
|
@ -93,6 +101,23 @@ class Service(BaseModel):
|
||||||
secrets: List[str]
|
secrets: List[str]
|
||||||
|
|
||||||
|
|
||||||
|
class MailAuth(BaseModel):
|
||||||
|
"""Mail authentication configuration"""
|
||||||
|
login: str
|
||||||
|
password: str
|
||||||
|
|
||||||
|
|
||||||
|
class Mail(BaseModel):
|
||||||
|
"""Mail configuration"""
|
||||||
|
mailfrom: EmailStr
|
||||||
|
host: str = '127.0.0.1'
|
||||||
|
port: PositiveInt = 25
|
||||||
|
ssl: StrictBool = False
|
||||||
|
starttls: StrictBool = False
|
||||||
|
auth: Optional[MailAuth] = None
|
||||||
|
addresses: List[EmailStr]
|
||||||
|
|
||||||
|
|
||||||
class Alert(BaseModel):
|
class Alert(BaseModel):
|
||||||
"""List of way to handle alerts, by severity"""
|
"""List of way to handle alerts, by severity"""
|
||||||
|
|
||||||
|
@ -102,11 +127,18 @@ class Alert(BaseModel):
|
||||||
unknown: List[str]
|
unknown: List[str]
|
||||||
|
|
||||||
|
|
||||||
|
class GotifyUrl(BaseModel):
|
||||||
|
url: HttpUrl
|
||||||
|
tokens: List[str]
|
||||||
|
|
||||||
|
|
||||||
class General(BaseModel):
|
class General(BaseModel):
|
||||||
"""Frequency for the checks and alerts"""
|
"""Frequency for the checks and alerts"""
|
||||||
|
|
||||||
frequency: int
|
frequency: int
|
||||||
alerts: Alert
|
alerts: Alert
|
||||||
|
mail: Optional[Mail] = None
|
||||||
|
gotify: Optional[List[GotifyUrl]] = None
|
||||||
|
|
||||||
@field_validator("frequency", mode="before")
|
@field_validator("frequency", mode="before")
|
||||||
def parse_frequency(cls, value):
|
def parse_frequency(cls, value):
|
||||||
|
|
|
@ -1,9 +1,125 @@
|
||||||
|
import ssl
|
||||||
|
import smtplib
|
||||||
|
|
||||||
|
from typing import List
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
|
||||||
|
from argos.checks.base import Severity
|
||||||
from argos.logging import logger
|
from argos.logging import logger
|
||||||
|
from argos.schemas.config import Config, Mail, GotifyUrl
|
||||||
|
|
||||||
# XXX Implement mail alerts https://framagit.org/framasoft/framaspace/argos/-/issues/15
|
# XXX Implement mail alerts https://framagit.org/framasoft/framaspace/argos/-/issues/15
|
||||||
# XXX Implement gotify alerts https://framagit.org/framasoft/framaspace/argos/-/issues/16
|
# XXX Implement gotify alerts https://framagit.org/framasoft/framaspace/argos/-/issues/16
|
||||||
|
|
||||||
def handle_alert(config, result, task, severity):
|
def handle_alert(config: Config, result, task, severity, old_severity, request):
|
||||||
"""Dispatch alert through configured alert channels"""
|
"""Dispatch alert through configured alert channels"""
|
||||||
msg = f"task={task.id}, status={result.status}, {severity=}"
|
|
||||||
logger.error("Alerting stub: %s", msg)
|
if 'local' in getattr(config.general.alerts, severity):
|
||||||
|
logger.error("Alerting stub: task=%i, status=%s, severity=%s",
|
||||||
|
task.id,
|
||||||
|
result.status,
|
||||||
|
severity)
|
||||||
|
|
||||||
|
if config.general.mail is not None and \
|
||||||
|
'mail' in getattr(config.general.alerts, severity):
|
||||||
|
notify_by_mail(result, task, severity, old_severity, config.general.mail, request)
|
||||||
|
|
||||||
|
if config.general.gotify is not None and \
|
||||||
|
'gotify' in getattr(config.general.alerts, severity):
|
||||||
|
notify_with_gotify(result, task, severity, old_severity, config.general.gotify, request)
|
||||||
|
|
||||||
|
|
||||||
|
def notify_by_mail(result, task, severity: str, old_severity: str, config: Mail, request) -> None:
|
||||||
|
logger.debug('Will send mail notification')
|
||||||
|
|
||||||
|
msg = f"""\
|
||||||
|
URL: {task.url}
|
||||||
|
Check: {task.check}
|
||||||
|
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)}
|
||||||
|
"""
|
||||||
|
|
||||||
|
mail = f"""\
|
||||||
|
Subject: [Argos] {urlparse(task.url).netloc}: status {severity}
|
||||||
|
|
||||||
|
{msg}"""
|
||||||
|
|
||||||
|
if config.ssl:
|
||||||
|
logger.debug('Mail notification: SSL')
|
||||||
|
context = ssl.create_default_context()
|
||||||
|
smtp = smtplib.SMTP_SSL(host=config.host,
|
||||||
|
port=config.port,
|
||||||
|
context=context)
|
||||||
|
else:
|
||||||
|
smtp = smtplib.SMTP(host=config.host, # type: ignore
|
||||||
|
port=config.port)
|
||||||
|
if config.starttls:
|
||||||
|
logger.debug('Mail notification: STARTTLS')
|
||||||
|
context = ssl.create_default_context()
|
||||||
|
smtp.starttls(context=context)
|
||||||
|
|
||||||
|
if config.auth is not None:
|
||||||
|
logger.debug('Mail notification: authentification')
|
||||||
|
smtp.login(config.auth.login,
|
||||||
|
config.auth.password)
|
||||||
|
|
||||||
|
for address in config.addresses:
|
||||||
|
logger.debug('Sending mail to %s', address)
|
||||||
|
logger.debug(msg)
|
||||||
|
smtp.sendmail(config.mailfrom, address, mail)
|
||||||
|
|
||||||
|
|
||||||
|
def notify_with_gotify(
|
||||||
|
result,
|
||||||
|
task,
|
||||||
|
severity: str,
|
||||||
|
old_severity: str,
|
||||||
|
config: List[GotifyUrl],
|
||||||
|
request
|
||||||
|
) -> None:
|
||||||
|
logger.debug('Will send gotify notification')
|
||||||
|
headers = {'accept': 'application/json',
|
||||||
|
'content-type': 'application/json'}
|
||||||
|
|
||||||
|
priority = 9
|
||||||
|
icon = '❌'
|
||||||
|
if severity == Severity.OK:
|
||||||
|
priority = 1
|
||||||
|
icon = '✅'
|
||||||
|
elif severity == Severity.WARNING:
|
||||||
|
priority = 5
|
||||||
|
icon = '⚠️'
|
||||||
|
|
||||||
|
subject = f"{icon} {urlparse(task.url).netloc}: status {severity}"
|
||||||
|
msg = f"""\
|
||||||
|
URL: {task.url}
|
||||||
|
Check: {task.check}
|
||||||
|
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)}
|
||||||
|
"""
|
||||||
|
|
||||||
|
payload = {'title': subject,
|
||||||
|
'message': msg,
|
||||||
|
'priority': priority}
|
||||||
|
|
||||||
|
for url in config:
|
||||||
|
logger.debug('Sending gotify message(s) to %s', url)
|
||||||
|
for token in url.tokens:
|
||||||
|
try:
|
||||||
|
res = httpx.post(f"{url.url}message",
|
||||||
|
params={'token': token},
|
||||||
|
headers=headers,
|
||||||
|
json=payload)
|
||||||
|
res.raise_for_status()
|
||||||
|
except httpx.RequestError as err:
|
||||||
|
logger.error('An error occurred while sending a message to %s with token %s',
|
||||||
|
err.request.url,
|
||||||
|
token)
|
||||||
|
|
|
@ -44,19 +44,23 @@ async def create_results(
|
||||||
agent_id = agent_id or request.client.host
|
agent_id = agent_id or request.client.host
|
||||||
db_results = []
|
db_results = []
|
||||||
for agent_result in results:
|
for agent_result in results:
|
||||||
result = await queries.create_result(db, agent_result, agent_id)
|
|
||||||
# XXX Maybe offload this to a queue.
|
# XXX Maybe offload this to a queue.
|
||||||
# XXX Get all the tasks at once, to limit the queries on the db
|
# XXX Get all the tasks at once, to limit the queries on the db
|
||||||
task = await queries.get_task(db, agent_result.task_id)
|
task = await queries.get_task(db, agent_result.task_id)
|
||||||
if not task:
|
if not task:
|
||||||
logger.error("Unable to find task %i", agent_result.task_id)
|
logger.error("Unable to find task %i", agent_result.task_id)
|
||||||
else:
|
else:
|
||||||
|
last_severity = task.severity
|
||||||
|
result = await queries.create_result(db, agent_result, agent_id)
|
||||||
check = task.get_check()
|
check = task.get_check()
|
||||||
status, severity = await check.finalize(config, result, **result.context)
|
status, severity = await check.finalize(config, result, **result.context)
|
||||||
result.set_status(status, severity)
|
result.set_status(status, severity)
|
||||||
task.set_times_and_deselect()
|
task.set_times_and_deselect()
|
||||||
|
|
||||||
handle_alert(config, result, task, severity)
|
# Don’t create an alert if the severity has not changed
|
||||||
|
if last_severity != severity:
|
||||||
|
# XXX Use a job queue or make it async
|
||||||
|
handle_alert(config, result, task, severity, last_severity, request)
|
||||||
|
|
||||||
db_results.append(result)
|
db_results.append(result)
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|
|
@ -12,7 +12,7 @@
|
||||||
{{ counts_dict['ok'] }}
|
{{ counts_dict['ok'] }}
|
||||||
</article>
|
</article>
|
||||||
<article>
|
<article>
|
||||||
<header> ⚠️ Warning</header>
|
<header>⚠️ Warning</header>
|
||||||
{{ counts_dict['warning'] }}
|
{{ counts_dict['warning'] }}
|
||||||
</article>
|
</article>
|
||||||
<article>
|
<article>
|
||||||
|
|
|
@ -10,6 +10,24 @@ general:
|
||||||
- local
|
- local
|
||||||
unknown:
|
unknown:
|
||||||
- local
|
- local
|
||||||
|
# mail:
|
||||||
|
# mailfrom: no-reply@example.org
|
||||||
|
# host: 127.0.0.1
|
||||||
|
# port: 25
|
||||||
|
# ssl: False
|
||||||
|
# starttls: False
|
||||||
|
# auth:
|
||||||
|
# login: foo
|
||||||
|
# password: bar
|
||||||
|
# addresses:
|
||||||
|
# - foo@admin.example.org
|
||||||
|
# - bar@admin.example.org
|
||||||
|
# gotify:
|
||||||
|
# - url: https://example.org
|
||||||
|
# tokens:
|
||||||
|
# - foo
|
||||||
|
# - bar
|
||||||
|
|
||||||
service:
|
service:
|
||||||
secrets:
|
secrets:
|
||||||
# Secrets can be generated using `openssl rand -base64 32`.
|
# Secrets can be generated using `openssl rand -base64 32`.
|
||||||
|
|
|
@ -20,7 +20,7 @@ dependencies = [
|
||||||
"click>=8.1,<9",
|
"click>=8.1,<9",
|
||||||
"fastapi>=0.103,<0.104",
|
"fastapi>=0.103,<0.104",
|
||||||
"httpx>=0.25,<1",
|
"httpx>=0.25,<1",
|
||||||
"pydantic>=2.4,<3",
|
"pydantic[email]>=2.4,<3",
|
||||||
"pyyaml>=6.0,<7",
|
"pyyaml>=6.0,<7",
|
||||||
"pyyaml-include>=1.3,<2",
|
"pyyaml-include>=1.3,<2",
|
||||||
"sqlalchemy[asyncio]>=2.0,<3",
|
"sqlalchemy[asyncio]>=2.0,<3",
|
||||||
|
|
Loading…
Reference in a new issue