-
-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #11 from MEHRSHAD-MIRSHEKARY/feature/send-logs-com…
…mand Feature/send logs command
- Loading branch information
Showing
17 changed files
with
854 additions
and
3 deletions.
There are no files selected for viewing
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
# To get started with Dependabot version updates, you'll need to specify which | ||
# package ecosystems to update and where the package manifests are located. | ||
# Please see the documentation for all configuration options: | ||
# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file | ||
|
||
version: 2 | ||
updates: | ||
- package-ecosystem: "pip" | ||
directory: "/packages" # Location of the requirements.txt file | ||
schedule: | ||
interval: "weekly" | ||
# If you want to target the specific file | ||
target-branch: "main" # Replace with your default branch if different | ||
|
||
- package-ecosystem: "pip" | ||
directory: "/packages" # Location of the requirements-dev.txt file | ||
schedule: | ||
interval: "weekly" | ||
# If you want to target the specific file | ||
target-branch: "main" # Replace with your default branch if different |
This file contains 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
Empty file.
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
import logging | ||
|
||
|
||
class LoggingLevelFilter(logging.Filter): | ||
""" | ||
Filters log records based on their logging level. | ||
This filter is used to prevent log records from being written to log files | ||
intended for lower log levels. For example, if we have separate log | ||
files for DEBUG, INFO, WARNING, and ERROR levels, this filter ensures that | ||
a log record with level ERROR is only written to the ERROR log file, and not | ||
to the DEBUG, INFO or WARNING log files. | ||
""" | ||
|
||
def __init__(self, logging_level: int): | ||
""" | ||
Initializes a LoggingLevelFilter instance. | ||
Args: | ||
logging_level: The logging level to filter on (e.g. logging.DEBUG, logging.INFO, etc.). | ||
Returns: | ||
None | ||
""" | ||
super().__init__() | ||
self.logging_level = logging_level | ||
|
||
def filter(self, record: logging.LogRecord) -> bool: | ||
""" | ||
Filters a log record based on its level. | ||
Args: | ||
record: The log record to filter. | ||
Returns: | ||
True if the log record's level matches the specified logging level, False otherwise. | ||
""" | ||
return record.levelno == self.logging_level |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
from .email_handler import EmailHandler |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
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 | ||
|
||
try: | ||
# Attempt to retrieve the logging settings from the Django settings | ||
logging_settings = settings.DJANGO_LOGGING | ||
|
||
self.include_html = logging_settings.get("LOG_EMAIL_NOTIFIER", {}).get("USE_TEMPLATE", include_html) | ||
|
||
except AttributeError: | ||
logging.warning( | ||
f"DJANGO_LOGGING settings not found. Using default include_html value: {self.include_html}.") | ||
except Exception as e: | ||
logging.error(f"An unexpected error occurred while initializing EmailHandler: {e}") | ||
|
||
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 | ||
|
||
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) |
Empty file.
Empty file.
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,107 @@ | ||
import os | ||
import shutil | ||
import tempfile | ||
import logging | ||
from django.core.mail import EmailMessage | ||
from django.core.management.base import BaseCommand | ||
from django.conf import settings | ||
|
||
logger = logging.getLogger(__name__) | ||
|
||
|
||
class Command(BaseCommand): | ||
""" | ||
A Django management command that zips the log directory and sends it to | ||
the specified email address. | ||
This command is used to send the log files to a specified email address. | ||
It zips the log directory, creates an email with the zipped file as an attachment, | ||
and sends it to the specified email address. | ||
""" | ||
|
||
help = "Send log folder to the specified email address" | ||
|
||
def add_arguments(self, parser): | ||
""" | ||
Add custom command arguments. | ||
Parameters: | ||
parser (ArgumentParser): The argument parser to add arguments to. | ||
""" | ||
parser.add_argument( | ||
"email", type=str, help="The email address to send the logs to" | ||
) | ||
|
||
def handle(self, *args, **kwargs): | ||
""" | ||
The main entry point for the command. | ||
Parameters: | ||
args (tuple): Positional arguments. | ||
kwargs (dict): Keyword arguments. | ||
""" | ||
email = kwargs["email"] | ||
|
||
log_dir = settings.DJANGO_LOGGING.get("LOG_DIR", os.path.join(os.getcwd(), "logs")) | ||
|
||
if not os.path.exists(log_dir): | ||
self.stdout.write( | ||
self.style.ERROR(f'Log directory "{log_dir}" does not exist.') | ||
) | ||
logger.error(f'Log directory "{log_dir}" does not exist.') | ||
return | ||
|
||
self.check_email_settings() | ||
|
||
# Create a temporary file to store the zipped logs | ||
with tempfile.NamedTemporaryFile(delete=False) as tmp_file: | ||
zip_path = f"{tmp_file.name}.zip" | ||
tmp_file.close() | ||
|
||
# Zip the log directory | ||
shutil.make_archive(tmp_file.name, "zip", log_dir) | ||
|
||
# Send the email with the zipped logs | ||
email_subject = "Log Files" | ||
email_body = "Please find the attached log files." | ||
email_message = EmailMessage( | ||
subject=email_subject, | ||
body=email_body, | ||
from_email=settings.DEFAULT_FROM_EMAIL, | ||
to=[email], | ||
) | ||
email_message.attach_file(zip_path) | ||
|
||
try: | ||
email_message.send() | ||
self.stdout.write(self.style.SUCCESS(f"Logs sent successfully to {email}.")) | ||
logger.info(f"Logs sent successfully to {email}.") | ||
except Exception as e: | ||
self.stdout.write(self.style.ERROR(f"Failed to send logs: {e}")) | ||
logger.error(f"Failed to send logs: {e}") | ||
finally: | ||
# Clean up the temporary file | ||
os.remove(zip_path) | ||
logger.info("Temporary zip file cleaned up successfully.") | ||
|
||
def check_email_settings(self): | ||
""" | ||
Check if all required email settings are present in the settings file. | ||
Raises an exception if any of the required email settings are missing. | ||
""" | ||
required_settings = [ | ||
"EMAIL_HOST", | ||
"EMAIL_PORT", | ||
"EMAIL_HOST_USER", | ||
"EMAIL_HOST_PASSWORD", | ||
"EMAIL_USE_TLS", | ||
"DEFAULT_FROM_EMAIL", | ||
] | ||
|
||
for setting in required_settings: | ||
if not getattr(settings, setting, None): | ||
error_message = f"Missing required email setting: {setting}" | ||
self.stdout.write(self.style.ERROR(error_message)) | ||
logger.error(f"Missing required email setting: {setting}") | ||
raise Exception(error_message) |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
from .request_middleware import RequestLogMiddleware |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,84 @@ | ||
import logging | ||
from django.contrib.auth import get_user_model | ||
from django.http import HttpResponse, HttpRequest | ||
from typing import Callable | ||
|
||
logger = logging.getLogger(__name__) | ||
|
||
|
||
class RequestLogMiddleware: | ||
""" | ||
Middleware to log information about each incoming request. | ||
This middleware logs the request path, the user making the request (if authenticated), | ||
and the user's IP address. | ||
""" | ||
|
||
def __init__(self, get_response: Callable[[HttpRequest], HttpResponse]) -> None: | ||
""" | ||
Initializes the RequestLogMiddleware instance. | ||
Args: | ||
get_response: A callable that returns an HttpResponse object. | ||
""" | ||
self.get_response = get_response | ||
user_model = get_user_model() | ||
self.username_field = user_model.USERNAME_FIELD | ||
|
||
def __call__(self, request: HttpRequest) -> HttpResponse: | ||
""" | ||
Processes an incoming request and logs relevant information. | ||
Args: | ||
request: The incoming request object. | ||
Returns: | ||
The response object returned by the view function. | ||
""" | ||
# Before view (and later middleware) are called. | ||
response = self.get_response(request) | ||
|
||
# After view is called. | ||
if hasattr(request, "user") and request.user.is_authenticated: | ||
user = getattr(request.user, self.username_field, "Anonymous") | ||
else: | ||
user = "Anonymous" | ||
|
||
# Get the user's IP address | ||
ip_address = self.get_ip_address(request) | ||
|
||
# Get the user agent | ||
user_agent = self.get_user_agent(request) | ||
|
||
# Attach IP and user agent to the request | ||
request.ip_address = ip_address | ||
request.browser_type = user_agent | ||
|
||
logger.info( | ||
f"Request Info: (request_path: {request.path}, user: {user}," | ||
f"\nIP: {ip_address}, user_agent: {user_agent})" | ||
) | ||
|
||
return response | ||
|
||
@staticmethod | ||
def get_ip_address(request: HttpRequest) -> str: | ||
""" | ||
Retrieves the client's IP address from the request object. | ||
""" | ||
ip_address = request.META.get("HTTP_X_FORWARDED_FOR") | ||
if ip_address: | ||
ip_address = ip_address.split(",")[0] | ||
else: | ||
ip_address = request.META.get("LIMITED_ACCESS") | ||
if not ip_address: | ||
ip_address = request.META.get("REMOTE_ADDR") | ||
|
||
return ip_address | ||
|
||
@staticmethod | ||
def get_user_agent(request: HttpRequest) -> str: | ||
""" | ||
Retrieves the client's user agent from the request object. | ||
""" | ||
return request.META.get("HTTP_USER_AGENT", "Unknown User Agent") |
This file contains 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
Oops, something went wrong.