diff --git a/backend/.env.example b/backend/.env.example index bc5e2948..735a29a1 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -13,6 +13,10 @@ AWS_SES_REGION_NAME=region_name AWS_SES_REGION_ENDPOINT=region_endpoint DEFAULT_FROM_EMAIL=email +# Discord Webhook URL for error logging. Leave empty for development. +# To set up: https://support.discord.com/hc/en-us/articles/228383668-Intro-to-Webhooks +DISCORD_WEBHOOK_URL=webhook-url + # The URL of the main site, used for generating links (no trailing slash) BASE_URL=https://example.com @@ -26,9 +30,6 @@ COOKIE_DOMAIN=.example.com # The folder where logs will be stored LOG_DIR=filepath -# Comma-separated list of admin email addresses in case anything goes wrong -ADMIN_EMAILS=email,email - # Secret key for Django data signing, can stay a placeholder for development SECRET_KEY=django-secret-key diff --git a/backend/api/settings.py b/backend/api/settings.py index 56105b46..2ad06d01 100644 --- a/backend/api/settings.py +++ b/backend/api/settings.py @@ -6,6 +6,7 @@ from urllib.parse import urlparse import environ +import requests from celery.schedules import crontab from django.core.mail import send_mail from rest_framework.response import Response @@ -155,9 +156,11 @@ class ThrottleScopes: AWS_SES_REGION_NAME = env("AWS_SES_REGION_NAME") AWS_SES_REGION_ENDPOINT = env("AWS_SES_REGION_ENDPOINT") DEFAULT_FROM_EMAIL = env("DEFAULT_FROM_EMAIL") -ADMIN_EMAILS = env.list("ADMIN_EMAILS", default=[]) SEND_EMAILS = env.bool("SEND_EMAILS", default=False) -CRITICAL_EMAIL_INTERVAL_SECONDS = 1800 # 30 minutes + +# Discord Webhook URL +# Errors will only be sent if this is set. +DISCORD_WEBHOOK_URL = env("DISCORD_WEBHOOK_URL", default=None) # Automated tasks CELERY_BEAT_SCHEDULE = { @@ -218,30 +221,43 @@ class ThrottleScopes: # Custom logger just to add some of my own custom logging functions # We love DRY!!! class PlancakeLogger(logging.Logger): - _last_email_time = 0 + def execute_webhook(self, msg, color): + if DISCORD_WEBHOOK_URL: + try: + requests.post( + DISCORD_WEBHOOK_URL, + json={ + "embeds": [ + { + "title": "Error", + "description": msg, + "color": color, + "timestamp": time.strftime( + "%Y-%m-%dT%H:%M:%SZ", time.gmtime() + ), + } + ] + }, + timeout=5, + ) + except Exception as e: + self.warning( + "Failed to execute Discord Webhook for error logging: %s\nIf you don't have Webhooks set up, leave the environment variable empty.", + e, + ) def db_error(self, msg, *args, **kwargs): self.error("Database error: %s", msg, *args, **kwargs) + def error(self, msg, *args, **kwargs): + super().error(msg, *args, **kwargs) + formatted_msg = str(msg) % args if args else str(msg) + self.execute_webhook(formatted_msg, 0xFF0000) + def critical(self, msg, *args, **kwargs): super().critical(msg, *args, **kwargs) - - # Send an email to admins - if SEND_EMAILS: - now = time.time() - if now - self._last_email_time > CRITICAL_EMAIL_INTERVAL_SECONDS: - stack_trace = "".join(traceback.format_stack()) - try: - send_mail( - subject=f"Plancake - Critical Error", - message=f"A critical error occurred in the application: {msg}\n\nStack Trace:\n{stack_trace}", - from_email=DEFAULT_FROM_EMAIL, - recipient_list=ADMIN_EMAILS, - fail_silently=False, - ) - self._last_email_time = now - except Exception as e: - self.error("Failed to send critical error email: %s", e) + formatted_msg = str(msg) % args if args else str(msg) + self.execute_webhook(formatted_msg, 0xFF00FF) # Now any logger in the project will have access to this class diff --git a/backend/requirements.txt b/backend/requirements.txt index 1c686fa9..7e75db99 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -5,6 +5,8 @@ billiard==4.2.1 boto3==1.39.14 botocore==1.39.14 celery==5.5.3 +certifi==2026.6.17 +charset-normalizer==3.4.9 click==8.2.1 click-didyoumean==0.3.1 click-plugins==1.1.1.2 @@ -16,6 +18,7 @@ django-environ==0.12.0 django-ses==4.4.0 djangorestframework==3.16.0 gunicorn==23.0.0 +idna==3.18 isort==8.0.1 jmespath==1.0.1 kombu==5.5.4 @@ -25,6 +28,7 @@ psycopg==3.2.7 psycopg2-binary==2.9.10 python-dateutil==2.9.0.post0 redis==6.2.0 +requests==2.34.2 s3transfer==0.13.1 six==1.17.0 sqlparse==0.5.3