Skip to content
Open
Show file tree
Hide file tree
Changes from 43 commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
4e189e1
Email notification initial commit
gabortodor Oct 22, 2025
6d70099
working on the issue creation
xXPinkmagicXx Oct 22, 2025
163aa25
working on sending issues
xXPinkmagicXx Oct 22, 2025
df51251
created notification content class
xXPinkmagicXx Oct 22, 2025
c578ad9
Merge pull request #6 from flask-dashboard/feature/notification-content
xXPinkmagicXx Oct 23, 2025
176541b
Merge remote-tracking branch 'origin' into feature/issue-notification
xXPinkmagicXx Oct 23, 2025
bd83210
Do not track environment file
xXPinkmagicXx Oct 23, 2025
2baf6aa
Now it can create issues
xXPinkmagicXx Oct 23, 2025
22c129c
Deleted test code in main.py
xXPinkmagicXx Oct 23, 2025
640db86
deleted commented out code
xXPinkmagicXx Oct 23, 2025
084a835
Added method to check if hash is in database
xXPinkmagicXx Oct 23, 2025
d4c06a5
added another throw method to test the exception notificaiton flow
xXPinkmagicXx Oct 23, 2025
98ffae8
Moved the notification method in the exception_collector class
xXPinkmagicXx Oct 23, 2025
e5c7232
Merge branch 'flask-dashboard:master' into master
gabortodor Oct 24, 2025
8741d34
better formatting for issues created
xXPinkmagicXx Oct 24, 2025
aae159f
Merge pull request #10 from flask-dashboard/feature/issue-notification
xXPinkmagicXx Oct 25, 2025
7bfd1f9
Merge pull request #12 from flask-dashboard/feature/issue-by-group
xXPinkmagicXx Oct 25, 2025
365b957
working on config instead of .env for issues creation
xXPinkmagicXx Oct 25, 2025
81adf79
Now using config instead of .env file
xXPinkmagicXx Oct 25, 2025
ea8fff4
Moved config to _notification and implemented config timezone in Noti…
xXPinkmagicXx Oct 25, 2025
29baf4c
parsing github config
xXPinkmagicXx Oct 25, 2025
40519eb
Merge branch 'master' into email_notification
gabortodor Oct 26, 2025
91a2d33
Merge pull request #16 from flask-dashboard/feature/config-for-github…
gabortodor Oct 26, 2025
cc7b7c9
Merge branch 'master' into email_notification
gabortodor Oct 26, 2025
fa5f20e
Implemented modifiable notification type
gabortodor Oct 26, 2025
2882e3f
Merge pull request #17 from flask-dashboard/email_notification
klnyzzz33 Oct 26, 2025
58ec5ca
Added chat integration
gabortodor Oct 28, 2025
627494b
Merge pull request #20 from flask-dashboard/feature/chat_integration
klnyzzz33 Oct 29, 2025
75b7d31
Refactoring
gabortodor Nov 21, 2025
5de732f
Merge branch 'flask-dashboard:master' into master
gabortodor Nov 22, 2025
fa43c82
Merge pull request #21 from flask-dashboard/master
gabortodor Nov 22, 2025
0156a05
Fixed controller name error
gabortodor Nov 22, 2025
153fb95
Fixed alert settings page
gabortodor Nov 22, 2025
ce89ca2
Fixed alert settings page
gabortodor Nov 22, 2025
307a92f
Refactored alert settings page + bugfixes
klnyzzz33 Nov 23, 2025
8cf16e7
Fixed email template rendering
gabortodor Nov 23, 2025
a2594d0
Change alert_enabled default value to false
klnyzzz33 Nov 23, 2025
2a9b063
Implemented alert character limiting
gabortodor Nov 23, 2025
8c442c4
Merge remote-tracking branch 'origin/feature/chat_integration' into f…
gabortodor Nov 23, 2025
4fbfb75
Switched teams stacktrace to codeblock.
klnyzzz33 Nov 23, 2025
e7a5d12
Changed default notification type
klnyzzz33 Nov 25, 2025
e117af2
Added alert config validation at application startup
klnyzzz33 Nov 25, 2025
fe1fba9
Merge pull request #22 from flask-dashboard/feature/chat_integration
gabortodor Nov 25, 2025
474fb7c
Merge branch 'flask-dashboard:master' into master
gabortodor Dec 4, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,9 @@ ENV/
# database file
*.db

# Environment variables file
.env

# Spyder project settings
.spyderproject

Expand Down
21 changes: 21 additions & 0 deletions config.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,24 @@ DATABASE=sqlite:////<path to your project>/dashboard.db
TIMEZONE=Europe/Amsterdam
COLORS={'main':'[0,97,255]',
'static':'[255,153,0]'}

[alerting]
ENABLED=False
TYPE=email
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we really need the TYPE? We can simply test in the code for the existence of the ENVVARS? Or is there a case where we want to have the TYPE defined independently from the enviers?

# TYPE=email,issue,chat

# email
SMTP_HOST=<example_host>
SMTP_PORT=<example_port>
SMTP_USER=<example_user>
SMTP_PASSWORD=<example_password>
SMTP_TO=<example_email1,example_email2,example_email3>

# issue
GITHUB_TOKEN=<example_github_token>
REPOSITORY_OWNER=<example_repo_owner>
REPOSITORY_NAME=<example_repo_name>

# chat
CHAT_PLATFORM=TEAMS
CHAT_WEBHOOK_URL=<example_url>
13 changes: 13 additions & 0 deletions flask_monitoringdashboard/core/alert/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Alerts

## Issue Creation

At the moment the secrets and variables for the issue are saved in a .env file

There is a template with file name `.env_template` that can be used to fill in the info needed for the issue creation alert to work.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see such a file. And also, I see that the file above -- config.cfg -- already definies secrets and variables. So is this still needed?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No it is not needed and should be deleted. Was a part of previous setup. Now the config.cfg is used







Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know why but alert. Seems like a strange name for this module. Either alerting or alerts would somehow be more understandable for me.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree

Empty file.
60 changes: 60 additions & 0 deletions flask_monitoringdashboard/core/alert/alert_content.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import importlib.resources
import os
import traceback
from datetime import datetime

from jinja2 import Environment, FileSystemLoader

import flask_monitoringdashboard
from flask_monitoringdashboard.core.config import Config

template_root = os.path.join(importlib.resources.files(flask_monitoringdashboard).__str__(), 'templates', 'alert')
template_env = Environment(loader=FileSystemLoader(template_root))
template = template_env.get_template('report.html')


class AlertContent:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A small comment explaining the use of this class would be good


def __init__(self, exception: BaseException, config: Config):
self._exception = exception

self.created_at = datetime.now(config.timezone)
self.created_at_str = self.created_at.strftime('%Y-%m-%d %H:%M:%S')
self.send_at = None # When was the alert sent, None if not sent yet

self.exception_type = exception.__class__.__name__

self.title = self._create_title()
self.stack_trace = ''.join(traceback.format_exception(type(exception), exception, exception.__traceback__))

def _create_title(self) -> str:
return f"[{self.exception_type}] Uncaught exception at {self.created_at_str}"

def get_limited_stack_trace(self, char_limit: int | None) -> str:
if not char_limit:
return self.stack_trace
return self.stack_trace[:char_limit] + ('...' if len(self.stack_trace) > char_limit else '')

def create_body_text(self, char_limit: int | None) -> str:
return f"An exception of type {self.exception_type} \n\nStack Trace: {self.get_limited_stack_trace(char_limit)}"

def create_body_markdown(self, char_limit: int | None) -> str:
return (
f"**Type:** `{self.exception_type}`\n"
f"**Timestamp:** `{self.created_at_str}`\n"
f"**Stack Trace:**\n```\n{self.get_limited_stack_trace(char_limit)}\n```"
)

def create_body_mrkdwn(self, char_limit: int | None) -> str:
return (
f"*Type:* `{self.exception_type}`\n"
f"*Timestamp:* `{self.created_at_str}`\n"
f"*Stack Trace:*\n```\n{self.get_limited_stack_trace(char_limit)}\n```"
)

def create_body_html(self, char_limit: int | None) -> str:
return template.render(
exc_type=self.exception_type,
timestamp=self.created_at_str,
stacktrace=self.get_limited_stack_trace(char_limit)
)
112 changes: 112 additions & 0 deletions flask_monitoringdashboard/core/alert/chat.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import requests

from flask_monitoringdashboard.core.alert.alert_content import AlertContent

SLACK_CHAR_LIMIT = 2750
ROCKET_CHAT_CHAR_LIMIT = 4500
TEAMS_CHAR_LIMIT = 5000


def send_message(alert_content: AlertContent):
from flask_monitoringdashboard import config

payload_creators = {
"SLACK": create_slack_payload,
"ROCKET_CHAT": create_rocket_chat_payload,
"TEAMS": create_teams_payload,
}

creator = payload_creators.get(config.chat_platform)
if not creator:
print("Invalid chat platform.")
return

payload = creator(alert_content)

try:
resp = requests.post(config.chat_webhook_url, json=payload, timeout=5)
resp.raise_for_status()
except Exception as e:
print("Alert delivery failed:", e)


def create_slack_payload(alert_content: AlertContent):
return {
"blocks": [
{
"type": "section",
"text": {"type": "mrkdwn", "text": alert_content.create_body_mrkdwn(SLACK_CHAR_LIMIT)}
}
]
}


def create_rocket_chat_payload(alert_content: AlertContent):
return {
"text": alert_content.create_body_markdown(ROCKET_CHAT_CHAR_LIMIT),
}


def create_teams_payload(alert_content: AlertContent):
return {
"type": "message",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

haha. Why am I not surprised that the Microsoft alternative is the most complicated one?

"attachments": [
{
"contentType": "application/vnd.microsoft.card.adaptive",
"content": {
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
"type": "AdaptiveCard",
"version": "1.4",
"body": [
{
"type": "TextBlock",
"text": "Exception Alert",
"weight": "Bolder",
"size": "Large",
"color": "Attention"
},
{
"type": "FactSet",
"facts": [
{
"title": "Type:",
"value": f"{alert_content.exception_type}"
},
{
"title": "Timestamp:",
"value": f"{alert_content.created_at_str}"
}
]
},
{
"type": "TextBlock",
"text": "Stack Trace:",
"weight": "Bolder",
"wrap": True
},
{
"type": "Container",
"id": "stackContainer",
"isVisible": True,
"items": [
{
"type": "CodeBlock",
"codeSnippet": f"{alert_content.get_limited_stack_trace(TEAMS_CHAR_LIMIT)}",
"language": "bash",
"wrap": True,
"fontType": "Monospace",
"targetWidth": "Standard",
"fallback": {
"type": "TextBlock",
"text": f"{alert_content.get_limited_stack_trace(TEAMS_CHAR_LIMIT)}",
"wrap": True,
"fontType": "Monospace"
}
}
]
}
]
}
}
]
}
29 changes: 29 additions & 0 deletions flask_monitoringdashboard/core/alert/email.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import smtplib
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText

from flask_monitoringdashboard.core.alert.alert_content import AlertContent


def send_email(alert_content: AlertContent):
from flask_monitoringdashboard import config

message = MIMEMultipart('alternative')
message.set_charset('utf-8')
message['Subject'] = alert_content.title
message['From'] = config.smtp_user
message['To'] = ', '.join(config.smtp_to)

message.attach(MIMEText(alert_content.create_body_text(None), 'plain', 'utf-8'))
message.attach(MIMEText(alert_content.create_body_html(None), 'html', 'utf-8'))

try:
with smtplib.SMTP(config.smtp_host, int(config.smtp_port)) as smtp:
smtp.starttls()

if config.smtp_password:
smtp.login(config.smtp_user, config.smtp_password)

smtp.sendmail(config.smtp_user, config.smtp_to, message.as_string())
except Exception as e:
print("Error sending email alert:", e)
16 changes: 16 additions & 0 deletions flask_monitoringdashboard/core/alert/github_request_info.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
class GitHubRequestInfo:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you really need this class?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess we could do it without. Is that preferred?

"""
RequestInfo is a class that represents the information of a request.
"""

def __init__(self, github_token: str, repo_owner: str, repo_name: str):
"""
Initializes the RequestInfo object with the given parameters.
:param github_token: the PAT token that has access to the repository
:param repo_owner: The owner/organisation that contains the repository
:param repo_name: The name of the repository
"""
self.repo_owner = repo_owner
self.repo_name = repo_name
self.github_token = github_token
53 changes: 53 additions & 0 deletions flask_monitoringdashboard/core/alert/issue.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import requests

from flask_monitoringdashboard.core.alert.alert_content import AlertContent
from .github_request_info import GitHubRequestInfo

GITHUB_CHAR_LIMIT = 60000


def get_base_repo_url(repo_owner: str, repo_name: str):
return f"https://api.github.com/repos/{repo_owner}/{repo_name}/"


def get_issue_url(repo_owner: str, repo_name: str):
return get_base_repo_url(repo_owner, repo_name) + "issues"


def get_endpoint_url(repo_owner: str, repo_name: str, endpoint: str):
return f"https://api.github.com/repos/{repo_owner}/{repo_name}/{endpoint}"


def make_post_request(request_info: GitHubRequestInfo, endpoint: str, data):
url = get_endpoint_url(request_info.repo_owner, request_info.repo_name, endpoint)
headers = _post_headers(request_info.github_token)

return requests.post(url, headers=headers, json=data)


def create_issue(
request_info: GitHubRequestInfo,
alert_content: AlertContent) -> requests.Response:
data = {
"title": alert_content.title,
"body": alert_content.create_body_markdown(GITHUB_CHAR_LIMIT),
"labels": ["automated-issue", "exception"],
}

return make_post_request(request_info, "issues", data)


def _post_headers(github_token: str):
headers = {
"Authorization": f"Bearer {github_token}",
"Accept": "application/vnd.github.v3+json"
}
return headers


def main():
print("This is a utility file with helper functions not to be run directly.")


if __name__ == "__main__":
main()
Loading