Created
June 11, 2025 12:22
-
-
Save eksiscloud/9f476538366b02ed8d1c19f9d1fc299f to your computer and use it in GitHub Desktop.
Flask-script to send notification to Discourse when error 502/503 happens
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
## Comments are in Finnish, sorry | |
#--- Rate-limiter muistiin --- | |
from datetime import datetime, timedelta | |
from zoneinfo import ZoneInfo | |
ALLOWED_ERROR_CODES = [502, 503, 504] | |
last_reported = {} # esim. {503: datetime_object} | |
RATE_LIMIT_HOURS = 4 | |
def should_send_report(code: int) -> bool: | |
now = datetime.now() | |
last_time = last_reported.get(code) | |
if not last_time or (now - last_time) > timedelta(hours=RATE_LIMIT_HOURS): | |
last_reported[code] = now | |
return True | |
return False | |
from flask import Flask, request, render_template_string, Response | |
import requests | |
import logging | |
from datetime import datetime | |
app = Flask(name) | |
Lokin perusasetukset | |
logging.basicConfig( | |
filename="/var/log/error_reporter.log", | |
level=logging.INFO, | |
format="%(asctime)s %(levelname)s: %(message)s", | |
) | |
Aseta oma Discoursen API-konfiguraatio | |
DISCOURSE_API_KEY = "really-long-one" | |
DISCOURSE_API_USERNAME = "system-monitor" | |
DISCOURSE_CATEGORY_ID = 18 # kategoria ID | |
DISCOURSE_URL = "https://foorumi.katiska.eu" | |
DISCOURSE_API_URL = "https://foorumi.katiska.eu/posts.json" | |
DISCOURSE_HEADERS = { | |
"Api-Key": DISCOURSE_API_KEY, | |
"Api-Username": DISCOURSE_API_USERNAME | |
} | |
Tarkistetaan että token saadaan vain Nginxiltä | |
EXPECTED_TOKEN = "something" # lisättävä Nginxissä esim. X-Error-Token -headerina | |
Mallipohja virhesivuille | |
Ensimmäinen näyttö | |
HTML_TEMPLATE = """ | |
<!DOCTYPE html> | |
<html lang="fi"> | |
<head> | |
<meta charset="UTF-8"> | |
<title>Virhe / Error</title> | |
</head> | |
<body> | |
<h1>Palvelinvirhe / Server Error</h1> | |
<p>Yhteys epäonnistui. / The connection failed.</p> | |
<p><strong>IP-osoitteesi / Client IP:</strong> {{ ip }}</p> | |
<p><strong>Selain / User Agent:</strong> {{ ua }}</p> | |
<p><strong>XID:</strong> {{ xid }}</p> | |
<hr> | |
<p>Ylläidolle on lähtenyt tieto virheestä.</p> | |
<p><em>Ilmoita silti virheestä foorumilla:</em> <a href="https://foorumi.katiska.eu">foorumi.katiska.eu</a></p> | |
</body> | |
</html> | |
""" | |
Muut näytöt | |
ALREADY_REPORTED_TEMPLATE = """ | |
<!DOCTYPE html> | |
<html lang="fi"> | |
<head> | |
<meta charset="UTF-8"> | |
<title>Virhe | Error</title> | |
<style> | |
body {{ font-family: sans-serif; margin: 2em; background: #f8f8f8; }} | |
h1 {{ color: #c00; }} | |
ul {{ line-height: 1.6; }} | |
</style> | |
</head> | |
<body> | |
<h1>Sivustolla on virhe / There is an error in the site</h1> | |
<p>Osoite, jota yritit, palautti virhekoodin.</p> | |
<p>Ongelma on tunnistettu ja siitä on tehty ilmoitus ylläpidolle.</p> | |
<p>Katkokset kestävät tyypillisesti muutamasta minuutista varttiin, mutta joskus voi mennä pidempään.</p> | |
<p>Kokeile myöhemmin uudelleen.</p> | |
<p></p> | |
<p>Katiskan foorumilla voi olla lisätietoa: <a href="https://foorumi.katiska.eu">https://foorumi.katiska.eu</a></p> | |
<hr> | |
<h2>Suosituimmat sivut</h2> | |
<p>Eniten käydyt sivut saattavat silti olla luettavissa välimuistin kautta: | |
<p></p> | |
{links} | |
<hr> | |
<p><small>IP: {ip} – XID: {xid}<br /> | |
User Agent: {ua}</small></p> | |
</body> | |
</html> | |
""" | |
def post_to_discourse(code, ip, ua, xid): | |
url = f"{DISCOURSE_URL}/posts.json" | |
#timestamp = datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S") | |
local_time = datetime.now(ZoneInfo("Europe/Helsinki")) | |
payload = { | |
"title": f"Virhe {code} havaittu - {local_time}", | |
"raw": f"**Virhe {code} havaittu**\n\n- IP: {ip}\n- User-Agent: {ua}\n- XID: {xid}", | |
"category": DISCOURSE_CATEGORY_ID | |
} | |
headers = { | |
"Api-Key": DISCOURSE_API_KEY, | |
"Api-Username": DISCOURSE_API_USERNAME | |
} | |
# Testiloki | |
logging.error("DISCOURSE PAYLOAD:\n%s", payload) | |
try: | |
response = requests.post(url, headers=headers, json=payload, timeout=5) | |
response.raise_for_status() | |
logging.info("Discourse post successful: %s", response.text) | |
except requests.exceptions.HTTPError as http_err: | |
logging.error("HTTP error occurred: %s", http_err) | |
logging.error("Status code: %s", response.status_code) | |
logging.error("Response text: %s", response.text) | |
except Exception as e: | |
logging.error("Unexpected error: %s", str(e)) | |
@app.route("/report/<int:code>", methods=["GET", "POST"]) | |
def report_error(code): | |
if code not in ALLOWED_ERROR_CODES: | |
return Response("Unknown error", status=400) | |
ip = request.headers.get("X-Real-IP", request.remote_addr) | |
ua = request.headers.get("User-Agent", "-") | |
xid = request.headers.get("X-Varnish-XID", "-") | |
if not should_send_report(code): | |
html = ALREADY_REPORTED_TEMPLATE.format(code=code, ip=ip, ua=ua, xid=xid, links="") | |
return Response(html, status=503, mimetype="text/html") | |
post_to_discourse(code, ip, ua, xid) | |
if request.method == "POST": | |
# Tarkista token vain POSTille | |
token = request.headers.get("X-Error-Token", "") | |
if token != EXPECTED_TOKEN: | |
return Response("Invalid token", status=403) | |
# Tarkista rate limiter | |
if not should_send_report(code): | |
return Response("Already reported recently", status=200) | |
post_to_discourse(code, ip, ua, xid) | |
return Response("Reported", status=200) | |
# GET vastaa aina HTML:llä | |
html = HTML_TEMPLATE.format(code=code, ip=ip, ua=ua, xid=xid) | |
return Response(html, status=code, mimetype="text/html") | |
@app.route('/health', methods=["GET"]) | |
def health_check(): | |
return jsonify({"status": "ok"}), 200 | |
if name == "main": | |
app.run(host="127.0.0.1", port=5060) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
This is for Nginx - Varnish - Apache2. The gist is only Flask reporting part to Discourse. I don’t think you can use this as is because you must setup Nginx hosts too.