Skip to content

Commit

Permalink
✨⚡💡feat: add Email-Notifier & log_and_notify
Browse files Browse the repository at this point in the history
Introduced an EmailHandler to send email notifications for specific log levels.
Configurable log levels for triggering email notifications, allowing granular control over email alerts.
Added the log_and_notify method to log a message and send an email notification simultaneously:
Uses the logger to create a LogRecord with the correct module, file, and line number.
Handles the log record with the logger's handlers and sends an email using the EmailHandler.
Supports customizable log messages with additional context data, such as request information.
Includes error handling to raise a ValueError if logging fails due to invalid parameters.
Asynchronously sends an email with the formatted log message to the administrator.
Updated:

Log Config:
Added log_email_notifier_enable to enable or disable the email notifier feature.
Added log_email_notifier_log_levels to specify which log levels should trigger email notifications.
Updated the LogConfig class initializer to include new parameters for email notifier settings.
apps.py:
Introduced logic to fetch and apply email notifier settings from LOG_EMAIL_NOTIFIER.
Set up conditions for enabling email notifications for ERROR and CRITICAL log levels based on settings.
Updated the set_logging method call to pass the email notifier settings to the logging config.
Docstrings: Included comprehensive docstrings for the email notifier feature, log_and_notify method, and configuration updates.
Closes #8
  • Loading branch information
MEHRSHAD-MIRSHEKARY committed Aug 18, 2024
1 parent d4b4d04 commit 897272a
Show file tree
Hide file tree
Showing 8 changed files with 157 additions and 3 deletions.
8 changes: 7 additions & 1 deletion django_logging/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
1 change: 1 addition & 0 deletions django_logging/handlers/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .email_handler import EmailHandler
47 changes: 47 additions & 0 deletions django_logging/handlers/email_handler.py
Original file line number Diff line number Diff line change
@@ -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)
18 changes: 18 additions & 0 deletions django_logging/settings/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions django_logging/utils/email/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .notifier import send_email_async
36 changes: 36 additions & 0 deletions django_logging/utils/email/notifier.py
Original file line number Diff line number Diff line change
@@ -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()
42 changes: 42 additions & 0 deletions django_logging/utils/log_and_notify.py
Original file line number Diff line number Diff line change
@@ -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])
7 changes: 5 additions & 2 deletions django_logging/utils/setup_conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,18 @@
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.
Args:
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()

0 comments on commit 897272a

Please sign in to comment.