Skip to content

Instantly share code, notes, and snippets.

@eksiscloud
Created June 11, 2025 12:22
Show Gist options
  • Save eksiscloud/9f476538366b02ed8d1c19f9d1fc299f to your computer and use it in GitHub Desktop.
Save eksiscloud/9f476538366b02ed8d1c19f9d1fc299f to your computer and use it in GitHub Desktop.
Flask-script to send notification to Discourse when error 502/503 happens
## 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} &ndash; 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)
@eksiscloud
Copy link
Author

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment