diff --git a/django_logging/apps.py b/django_logging/apps.py index 6634da2..9970470 100644 --- a/django_logging/apps.py +++ b/django_logging/apps.py @@ -18,6 +18,12 @@ def ready(self): "LOG_FILE_LEVELS", ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] ) log_dir = log_settings.get("LOG_DIR", os.path.join(os.getcwd(), "logs")) + log_email_notifier = log_settings.get("LOG_EMAIL_NOTIFIER", {}) + log_email_notifier_enable = log_email_notifier.get("ENABLE", False) + log_email_notifier_log_levels = [ + "ERROR" if log_email_notifier.get("NOTIFY_ERROR", False) else None, + "CRITICAL" if log_email_notifier.get("NOTIFY_CRITICAL", False) else None, + ] # Set the logging configuration - set_logging(log_levels, log_dir) + set_logging(log_levels, log_dir, log_email_notifier_enable, log_email_notifier_log_levels) diff --git a/django_logging/handlers/__init__.py b/django_logging/handlers/__init__.py new file mode 100644 index 0000000..dd5e3a9 --- /dev/null +++ b/django_logging/handlers/__init__.py @@ -0,0 +1 @@ +from .email_handler import EmailHandler diff --git a/django_logging/handlers/email_handler.py b/django_logging/handlers/email_handler.py new file mode 100644 index 0000000..25399fa --- /dev/null +++ b/django_logging/handlers/email_handler.py @@ -0,0 +1,47 @@ +import logging + +from django.conf import settings +from django.template import engines +from django.utils.timezone import now +from django_logging.utils.email.notifier import send_email_async +from django_logging.middleware import RequestLogMiddleware + + +class EmailHandler(logging.Handler): + def __init__(self, include_html=False, *args, **kwargs): + super().__init__(*args, **kwargs) + self.include_html = include_html + + def emit(self, record): + try: + request = getattr(record, "request", None) + log_entry = self.format(record) + + if self.include_html: + email_body = self.render_template(log_entry, request) + else: + email_body = log_entry # Use plain text format if HTML is not included + + subject = f"New Log Record: {record.levelname}" + send_email_async(subject, email_body, [settings.ADMIN_EMAIL]) + + except Exception as e: + self.handleError(record) + + @staticmethod + def render_template(log_entry, request=None, template_path="email_notifier_template.html"): + django_engine = engines["django"] + template = django_engine.get_template(template_path) + + # Fetch IP address and user agent using middleware methods + ip_address = RequestLogMiddleware.get_ip_address(request) if request else "Unknown" + user_agent = RequestLogMiddleware.get_user_agent(request) if request else "Unknown" + + context = { + "message": log_entry, + "time": now(), + "browser_type": user_agent, + "ip_address": ip_address, + } + + return template.render(context) diff --git a/django_logging/settings/conf.py b/django_logging/settings/conf.py index 60ff671..f2eaa97 100644 --- a/django_logging/settings/conf.py +++ b/django_logging/settings/conf.py @@ -19,9 +19,14 @@ def __init__( self, log_levels: List[str], log_dir: str, + log_email_notifier_enable: bool, + log_email_notifier_log_levels: List[str], ) -> None: + self.log_levels = log_levels self.log_dir = log_dir + self.email_notifier_enable = log_email_notifier_enable + self.email_notifier_log_levels = log_email_notifier_log_levels class LogManager: @@ -79,6 +84,19 @@ def set_conf(self) -> None: "level": "DEBUG", } + email_handler = { + f"email_{level.lower()}": { + "class": "django_logging.handlers.EmailHandler", + "level": level, + "filters": [level.lower()], + } + for level in self.log_config.email_notifier_log_levels + if level + } + + if self.log_config.email_notifier_enable: + handlers.update(email_handler) + filters = { level.lower(): { "()": LoggingLevelFilter, diff --git a/django_logging/utils/email/__init__.py b/django_logging/utils/email/__init__.py new file mode 100644 index 0000000..3e9b62e --- /dev/null +++ b/django_logging/utils/email/__init__.py @@ -0,0 +1 @@ +from .notifier import send_email_async diff --git a/django_logging/utils/email/notifier.py b/django_logging/utils/email/notifier.py new file mode 100644 index 0000000..9fb9eed --- /dev/null +++ b/django_logging/utils/email/notifier.py @@ -0,0 +1,36 @@ +import logging +import threading +import smtplib +from email.mime.text import MIMEText +from email.mime.multipart import MIMEMultipart + +from django.conf import settings + +logger = logging.getLogger(__name__) + + +def send_email_async(subject, body, recipient_list): + def send_email(): + msg = MIMEMultipart() + msg["From"] = settings.DEFAULT_FROM_EMAIL + msg["To"] = ", ".join(recipient_list) + msg["Subject"] = subject + + msg.attach(MIMEText(body, "html")) + + try: + server = smtplib.SMTP(settings.EMAIL_HOST, settings.EMAIL_PORT) + server.starttls() + server.login(settings.EMAIL_HOST_USER, settings.EMAIL_HOST_PASSWORD) + server.sendmail( + settings.DEFAULT_FROM_EMAIL, recipient_list, msg.as_string() + ) + server.quit() + logger.info(f"Log Record has been sent to ADMIN EMAIL successfully.") + + except Exception as e: + logger.warning(f"Email Notifier failed to send Log Record: {e}") + + # Start a new thread to send the email asynchronously + email_thread = threading.Thread(target=send_email) + email_thread.start() diff --git a/django_logging/utils/log_and_notify.py b/django_logging/utils/log_and_notify.py new file mode 100644 index 0000000..c432a81 --- /dev/null +++ b/django_logging/utils/log_and_notify.py @@ -0,0 +1,42 @@ +import logging +import inspect +from typing import Optional, Dict + +from django.conf import settings + +from django_logging.utils.email import send_email_async +from django_logging.handlers import EmailHandler + + +def log_and_notify(logger, level: int, message: str, extra: Optional[Dict] = None): + # Get the caller's frame to capture the correct module, file, and line number + frame = inspect.currentframe().f_back + + try: + # create a LogRecord + log_record = logger.makeRecord( + name=logger.name, + level=level, + fn=frame.f_code.co_filename, + lno=frame.f_lineno, + msg=message, + args=None, + exc_info=None, + func=frame.f_code.co_name, + extra=extra, + ) + + # Pass the LogRecord to the logger's handlers + logger.handle(log_record) + except TypeError as e: + raise ValueError( + f"Failed to log message due to invalid param. Original error: {e}" + ) + + request = extra.get("request") if extra else None + + # Render the email template with the formatted message + email_body = EmailHandler.render_template(log_record, request) + + subject = f"New Log Record: {logging.getLevelName(level)}" + send_email_async(subject, email_body, [settings.ADMIN_EMAIL]) diff --git a/django_logging/utils/setup_conf.py b/django_logging/utils/setup_conf.py index c377afa..2ab38cf 100644 --- a/django_logging/utils/setup_conf.py +++ b/django_logging/utils/setup_conf.py @@ -3,7 +3,10 @@ from typing import List -def set_logging(log_levels: List[str], log_dir: str): +def set_logging(log_levels: List[str], + log_dir: str, + log_email_notifier_enable: bool, + log_email_notifier_log_levels: List[str]): """ Sets up the logging configuration. @@ -11,7 +14,7 @@ def set_logging(log_levels: List[str], log_dir: str): log_levels (List[str]): A list of log levels to configure. log_dir (str): The directory where log files will be stored. """ - log_config = LogConfig(log_levels, log_dir) + log_config = LogConfig(log_levels, log_dir, log_email_notifier_enable, log_email_notifier_log_levels) log_manager = LogManager(log_config) log_manager.create_log_files() log_manager.set_conf()