diff --git a/django_logging/apps.py b/django_logging/apps.py index b054097..7640e7f 100644 --- a/django_logging/apps.py +++ b/django_logging/apps.py @@ -9,9 +9,10 @@ class DjangoLoggingConfig(AppConfig): def ready(self) -> None: from django_logging.settings import checks - from django_logging.utils.set_conf import set_config from django_logging.utils.get_conf import get_config + from django_logging.utils.set_conf import set_config + conf = get_config() # Set the logging configuration - set_config(*conf) + set_config(**conf) diff --git a/django_logging/constants/__init__.py b/django_logging/constants/__init__.py index 56dcc41..8ea139c 100644 --- a/django_logging/constants/__init__.py +++ b/django_logging/constants/__init__.py @@ -1,3 +1,3 @@ +from .default_settings import DefaultConsoleSettings, DefaultLoggingSettings from .log_format_options import FORMAT_OPTIONS -from .default_settings import DefaultLoggingSettings from .log_format_specifiers import LOG_FORMAT_SPECIFIERS diff --git a/django_logging/constants/config_types.py b/django_logging/constants/config_types.py index 436a1c7..f5b135c 100644 --- a/django_logging/constants/config_types.py +++ b/django_logging/constants/config_types.py @@ -1,4 +1,4 @@ -from typing import TypedDict, Union, List, Literal +from typing import List, Literal, TypedDict, Union FormatOption = Union[int, str] diff --git a/django_logging/constants/default_settings.py b/django_logging/constants/default_settings.py index 725e4a1..06e3f2d 100644 --- a/django_logging/constants/default_settings.py +++ b/django_logging/constants/default_settings.py @@ -1,15 +1,15 @@ import os -from typing import cast from dataclasses import dataclass, field +from typing import cast from django_logging.constants.config_types import ( - LogFileFormatsType, - LogDir, - LogLevel, - LogLevels, FormatOption, LogDateFormat, + LogDir, LogEmailNotifierType, + LogFileFormatsType, + LogLevel, + LogLevels, ) @@ -34,9 +34,7 @@ class DefaultLoggingSettings: }, ) ) - log_console_level: LogLevel = "DEBUG" - log_console_format: FormatOption = 1 - log_console_colorize: bool = True + log_email_notifier: LogEmailNotifierType = field( default_factory=lambda: cast( LogEmailNotifierType, @@ -49,3 +47,10 @@ class DefaultLoggingSettings: }, ) ) + + +@dataclass +class DefaultConsoleSettings: + log_console_level: LogLevel = "DEBUG" + log_console_format: FormatOption = 1 + log_console_colorize: bool = True diff --git a/django_logging/formatters/colored_formatter.py b/django_logging/formatters/colored_formatter.py index f14d228..95f43f2 100644 --- a/django_logging/formatters/colored_formatter.py +++ b/django_logging/formatters/colored_formatter.py @@ -1,4 +1,5 @@ -from logging import LogRecord, Formatter +from logging import Formatter, LogRecord + from django_logging.settings.conf import LogConfig from django_logging.utils.console_colorizer import colorize_log_format diff --git a/django_logging/handlers/email_handler.py b/django_logging/handlers/email_handler.py index 8011f5b..a46043f 100644 --- a/django_logging/handlers/email_handler.py +++ b/django_logging/handlers/email_handler.py @@ -5,9 +5,10 @@ from django.http import HttpRequest from django.template import engines from django.utils.timezone import now -from django_logging.utils.log_email_notifier.notifier import send_email_async -from django_logging.utils.get_conf import use_email_notifier_template + from django_logging.middleware import RequestLogMiddleware +from django_logging.utils.get_conf import use_email_notifier_template +from django_logging.utils.log_email_notifier.notifier import send_email_async class EmailHandler(Handler): @@ -24,12 +25,14 @@ def emit(self, record: LogRecord) -> None: subject = f"New Log Record: {record.levelname}" send_email_async(subject, email_body, [settings.ADMIN_EMAIL]) - except Exception as e: + except Exception: # pylint: disable=W0718 self.handleError(record) @staticmethod def render_template( - log_entry: str, request: Optional[HttpRequest] = None, template_path: str = "email_notifier_template.html" + log_entry: str, + request: Optional[HttpRequest] = None, + template_path: str = "email_notifier_template.html", ) -> str: django_engine = engines["django"] template = django_engine.get_template(template_path) diff --git a/django_logging/management/commands/send_logs.py b/django_logging/management/commands/send_logs.py index 3f92e9a..fb768be 100644 --- a/django_logging/management/commands/send_logs.py +++ b/django_logging/management/commands/send_logs.py @@ -1,18 +1,18 @@ +import logging import os import shutil import tempfile -import logging from argparse import ArgumentParser from typing import Dict, Tuple +from django.conf import settings from django.core.exceptions import ImproperlyConfigured from django.core.mail import EmailMessage from django.core.management.base import BaseCommand -from django.conf import settings +from django_logging.constants import DefaultLoggingSettings from django_logging.constants.config_types import LogDir from django_logging.validators.email_settings_validator import check_email_settings -from django_logging.constants import DefaultLoggingSettings logger = logging.getLogger(__name__) @@ -60,7 +60,7 @@ def handle(self, *args: Tuple, **kwargs: Dict) -> None: self.stdout.write( self.style.ERROR(f'Log directory "{log_dir}" does not exist.') ) - logger.error(f'Log directory "{log_dir}" does not exist.') + logger.error("Log directory '%s' does not exist.", log_dir) return self.validate_email_settings() @@ -87,10 +87,10 @@ def handle(self, *args: Tuple, **kwargs: Dict) -> None: 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: + logger.info("Logs sent successfully to %s.", email) + except Exception as e: # pylint: disable=W0718 self.stdout.write(self.style.ERROR(f"Failed to send logs: {e}")) - logger.error(f"Failed to send logs: {e}") + logger.error("Failed to send logs: %s", e) finally: # Clean up the temporary file if exists if os.path.exists(zip_path): diff --git a/django_logging/middleware/request_middleware.py b/django_logging/middleware/request_middleware.py index b523f2f..ba3168a 100644 --- a/django_logging/middleware/request_middleware.py +++ b/django_logging/middleware/request_middleware.py @@ -1,8 +1,9 @@ import logging -from django.contrib.auth import get_user_model -from django.http import HttpResponse, HttpRequest from typing import Callable +from django.contrib.auth import get_user_model +from django.http import HttpRequest, HttpResponse + logger = logging.getLogger(__name__) @@ -55,8 +56,11 @@ def __call__(self, request: HttpRequest) -> HttpResponse: request.browser_type = user_agent logger.info( - f"Request Info: (request_path: {request.path}, user: {user}," - f"\nIP: {ip_address}, user_agent: {user_agent})" + "Request Info: (request_path: %s, user: %s," "\nIP: %s, user_agent: %s)", + request.path, + user, + ip_address, + user_agent, ) return response diff --git a/django_logging/settings/checks.py b/django_logging/settings/checks.py index 40d2e43..9a70ccb 100644 --- a/django_logging/settings/checks.py +++ b/django_logging/settings/checks.py @@ -1,16 +1,20 @@ -from django.conf import settings +from typing import Any, Dict, List + from django.core.checks import Error, register -from typing import Dict, Any, List from django_logging.constants import DefaultLoggingSettings - +from django_logging.utils.get_conf import ( + get_config, + is_auto_initialization_enabled, + is_initialization_message_enabled, +) from django_logging.validators.config_validators import ( - validate_directory, - validate_log_levels, + validate_boolean_setting, validate_date_format, - validate_format_option, + validate_directory, validate_email_notifier, - validate_boolean_setting, + validate_format_option, + validate_log_levels, ) from django_logging.validators.email_settings_validator import check_email_settings @@ -19,30 +23,31 @@ def check_logging_settings(app_configs: Dict[str, Any], **kwargs: Any) -> List[Error]: errors: List[Error] = [] - log_settings = getattr(settings, "DJANGO_LOGGING", {}) - defaults = DefaultLoggingSettings() + log_settings = get_config(extra_info=True) + logging_defaults = DefaultLoggingSettings() # Validate LOG_DIR - log_dir = log_settings.get("LOG_DIR", defaults.log_dir) - errors.extend(validate_directory(log_dir, "LOG_DIR")) + errors.extend(validate_directory(log_settings.get("log_dir"), "LOG_DIR")) # type: ignore # Validate LOG_FILE_LEVELS - log_file_levels = log_settings.get("LOG_FILE_LEVELS", defaults.log_levels) + log_file_levels = log_settings.get("log_levels") errors.extend( validate_log_levels( - log_file_levels, "LOG_FILE_LEVELS", defaults.log_levels + log_file_levels, "LOG_FILE_LEVELS", logging_defaults.log_levels # type: ignore ) ) # Validate LOG_FILE_FORMATS - log_file_formats = log_settings.get("LOG_FILE_FORMATS", defaults.log_file_formats) + log_file_formats = log_settings.get( + "log_file_formats", logging_defaults.log_file_formats + ) if isinstance(log_file_formats, dict): for level, format_option in log_file_formats.items(): - if level not in defaults.log_levels: + if level not in logging_defaults.log_levels: errors.append( Error( f"Invalid log level '{level}' in LOG_FILE_FORMATS.", - hint=f"Valid log levels are: {defaults.log_levels}.", + hint=f"Valid log levels are: {logging_defaults.log_levels}.", id="django_logging.E019_LOG_FILE_FORMATS", ) ) @@ -59,60 +64,46 @@ def check_logging_settings(app_configs: Dict[str, Any], **kwargs: Any) -> List[E ) # Validate LOG_CONSOLE_FORMAT - log_console_format = log_settings.get( - "LOG_CONSOLE_FORMAT", defaults.log_console_format - ) - errors.extend(validate_format_option(log_console_format, "LOG_CONSOLE_FORMAT")) + log_console_format = log_settings.get("console_format") + errors.extend(validate_format_option(log_console_format, "LOG_CONSOLE_FORMAT")) # type: ignore # Validate LOG_CONSOLE_LEVEL - log_console_level = log_settings.get( - "LOG_CONSOLE_LEVEL", defaults.log_console_level - ) + log_console_level = log_settings.get("console_level") errors.extend( validate_log_levels( - [log_console_level], "LOG_CONSOLE_LEVEL", defaults.log_levels + [log_console_level], "LOG_CONSOLE_LEVEL", logging_defaults.log_levels # type: ignore ) ) # Validate LOG_CONSOLE_COLORIZE - log_console_colorize = log_settings.get( - "LOG_CONSOLE_COLORIZE", defaults.log_console_colorize - ) + log_console_colorize = log_settings.get("colorize_console") errors.extend( - validate_boolean_setting(log_console_colorize, "LOG_CONSOLE_COLORIZE") + validate_boolean_setting(log_console_colorize, "LOG_CONSOLE_COLORIZE") # type: ignore ) # Validate LOG_DATE_FORMAT - log_date_format = log_settings.get("LOG_DATE_FORMAT", defaults.log_date_format) - errors.extend(validate_date_format(log_date_format, "LOG_DATE_FORMAT")) + log_date_format = log_settings.get("log_date_format") + errors.extend(validate_date_format(log_date_format, "LOG_DATE_FORMAT")) # type: ignore # Validate AUTO_INITIALIZATION_ENABLE - auto_initialization_enable = log_settings.get( - "AUTO_INITIALIZATION_ENABLE", defaults.auto_initialization_enable - ) errors.extend( validate_boolean_setting( - auto_initialization_enable, "AUTO_INITIALIZATION_ENABLE" + is_auto_initialization_enabled(), "AUTO_INITIALIZATION_ENABLE" ) ) # Validate INITIALIZATION_MESSAGE_ENABLE - initialization_message_enable = log_settings.get( - "INITIALIZATION_MESSAGE_ENABLE", defaults.initialization_message_enable - ) errors.extend( validate_boolean_setting( - initialization_message_enable, "INITIALIZATION_MESSAGE_ENABLE" + is_initialization_message_enabled(), "INITIALIZATION_MESSAGE_ENABLE" ) ) # Validate LOG_EMAIL_NOTIFIER - log_email_notifier = log_settings.get( - "LOG_EMAIL_NOTIFIER", defaults.log_email_notifier - ) - errors.extend(validate_email_notifier(log_email_notifier)) + log_email_notifier = log_settings.get("log_email_notifier") + errors.extend(validate_email_notifier(log_email_notifier)) # type: ignore - if log_email_notifier.get("ENABLE", False): + if log_email_notifier.get("ENABLE", False): # type: ignore errors.extend(check_email_settings()) return errors diff --git a/django_logging/settings/conf.py b/django_logging/settings/conf.py index 449720a..38344e2 100644 --- a/django_logging/settings/conf.py +++ b/django_logging/settings/conf.py @@ -1,21 +1,22 @@ import logging import logging.config import os -from typing import List, Dict, Optional +from typing import Dict, List, Optional from django_logging.constants import FORMAT_OPTIONS, DefaultLoggingSettings from django_logging.constants.config_types import ( - LogLevels, + FormatOption, + LogDateFormat, LogDir, LogFileFormatsType, - LogDateFormat, - FormatOption, LogLevel, - NotifierLogLevels + LogLevels, + NotifierLogLevels, ) from django_logging.filters.log_level_filter import LoggingLevelFilter +# pylint: disable=too-many-instance-attributes, too-many-arguments class LogConfig: """ Configuration class for django_logging. @@ -85,11 +86,13 @@ def remove_ansi_escape_sequences(log_message: str) -> str: @staticmethod def resolve_format(_format: FormatOption, use_colors: bool = False) -> str: + resolved_format: str = "" if _format: if isinstance(_format, int): resolved_format = FORMAT_OPTIONS.get(_format, FORMAT_OPTIONS[1]) - else: + elif isinstance(_format, str): resolved_format = _format + else: resolved_format = FORMAT_OPTIONS[1] @@ -122,7 +125,8 @@ def create_log_files(self) -> None: ) os.makedirs(os.path.dirname(log_file_path), exist_ok=True) if not os.path.exists(log_file_path): - open(log_file_path, "w").close() + with open(log_file_path, "w", encoding="utf-8"): + pass self.log_files[log_level] = log_file_path def get_log_file(self, log_level: LogLevel) -> Optional[str]: diff --git a/django_logging/utils/context_manager.py b/django_logging/utils/context_manager.py index 9a7a055..278dc16 100644 --- a/django_logging/utils/context_manager.py +++ b/django_logging/utils/context_manager.py @@ -1,7 +1,6 @@ from contextlib import contextmanager -from logging import getLogger, Logger, PlaceHolder +from logging import Logger, PlaceHolder, getLogger from typing import Dict, Iterator, Union -from django.conf import settings from django_logging.settings.conf import LogConfig, LogManager from django_logging.utils.get_conf import get_config, is_auto_initialization_enabled @@ -30,7 +29,7 @@ def config_setup() -> Iterator[LogManager]: try: conf = get_config() - log_config = LogConfig(*conf) + log_config = LogConfig(**conf) log_manager = LogManager(log_config) log_manager.create_log_files() log_manager.set_conf() diff --git a/django_logging/utils/get_conf.py b/django_logging/utils/get_conf.py index 8a4d300..754944c 100644 --- a/django_logging/utils/get_conf.py +++ b/django_logging/utils/get_conf.py @@ -1,32 +1,45 @@ import os +from typing import Dict + from django.conf import settings -from typing import List -from django_logging.constants import DefaultLoggingSettings +from django_logging.constants import DefaultConsoleSettings, DefaultLoggingSettings -def get_config() -> List: +# pylint: disable=too-many-locals +def get_config(extra_info: bool = False) -> Dict: """ Retrieve logging configuration from Django settings. Returns: - A tuple containing all necessary configurations for logging. + A Dict containing all necessary configurations for logging. """ log_settings = getattr(settings, "DJANGO_LOGGING", {}) - defaults = DefaultLoggingSettings() + logging_defaults = DefaultLoggingSettings() + console_defaults = DefaultConsoleSettings() - log_levels = log_settings.get("LOG_FILE_LEVELS", defaults.log_levels) - log_dir = log_settings.get("LOG_DIR", os.path.join(os.getcwd(), defaults.log_dir)) - log_file_formats = log_settings.get("LOG_FILE_FORMATS", defaults.log_file_formats) - console_level = log_settings.get("LOG_CONSOLE_LEVEL", defaults.log_console_level) - console_format = log_settings.get("LOG_CONSOLE_FORMAT", defaults.log_console_format) + log_levels = log_settings.get("LOG_FILE_LEVELS", logging_defaults.log_levels) + log_dir = log_settings.get( + "LOG_DIR", os.path.join(os.getcwd(), logging_defaults.log_dir) + ) + log_file_formats = log_settings.get( + "LOG_FILE_FORMATS", logging_defaults.log_file_formats + ) + console_level = log_settings.get( + "LOG_CONSOLE_LEVEL", console_defaults.log_console_level + ) + console_format = log_settings.get( + "LOG_CONSOLE_FORMAT", console_defaults.log_console_format + ) colorize_console = log_settings.get( - "LOG_CONSOLE_COLORIZE", defaults.log_console_colorize + "LOG_CONSOLE_COLORIZE", console_defaults.log_console_colorize + ) + log_date_format = log_settings.get( + "LOG_DATE_FORMAT", logging_defaults.log_date_format ) - log_date_format = log_settings.get("LOG_DATE_FORMAT", defaults.log_date_format) log_email_notifier = log_settings.get( - "LOG_EMAIL_NOTIFIER", defaults.log_email_notifier + "LOG_EMAIL_NOTIFIER", logging_defaults.log_email_notifier ) log_email_notifier_enable = log_email_notifier.get("ENABLE") log_email_notifier_log_levels = [ @@ -35,19 +48,22 @@ def get_config() -> List: ] log_email_notifier_log_format = log_email_notifier.get("LOG_FORMAT") - configs = [ - log_levels, - log_dir, - log_file_formats, - console_level, - console_format, - colorize_console, - log_date_format, - log_email_notifier_enable, - log_email_notifier_log_levels, - log_email_notifier_log_format, - ] - return configs + config = { + "log_levels": log_levels, + "log_dir": log_dir, + "log_file_formats": log_file_formats, + "console_level": console_level, + "console_format": console_format, + "colorize_console": colorize_console, + "log_date_format": log_date_format, + "log_email_notifier_enable": log_email_notifier_enable, + "log_email_notifier_log_levels": log_email_notifier_log_levels, + "log_email_notifier_log_format": log_email_notifier_log_format, + } + if extra_info: + config.update({"log_email_notifier": log_email_notifier}) + + return config def use_email_notifier_template() -> bool: diff --git a/django_logging/utils/log_email_notifier/log_and_notify.py b/django_logging/utils/log_email_notifier/log_and_notify.py index fb26557..9d0b8fe 100644 --- a/django_logging/utils/log_email_notifier/log_and_notify.py +++ b/django_logging/utils/log_email_notifier/log_and_notify.py @@ -1,25 +1,25 @@ -import logging import inspect -from typing import Optional, Dict, Any +import logging +from typing import Any, Dict, Optional from django.conf import settings from django_logging.constants.log_format_options import FORMAT_OPTIONS -from django_logging.utils.log_email_notifier.notifier import send_email_async -from django_logging.utils.get_conf import get_config from django_logging.handlers import EmailHandler from django_logging.settings.conf import LogConfig +from django_logging.utils.get_conf import get_config +from django_logging.utils.log_email_notifier.notifier import send_email_async def log_and_notify_admin( - logger: logging.Logger, level: int, message: str, extra: Optional[Dict[str, Any]] = None + logger: logging.Logger, + level: int, + message: str, + extra: Optional[Dict[str, Any]] = None, ) -> None: # Get the caller's frame to capture the correct module, file, and line number - frame = inspect.currentframe().f_back - logging_settings = get_config() - email_notifier_enable = getattr( - logging_settings, "log_email_notifier_enable", False - ) + frame = inspect.currentframe().f_back # type: ignore + email_notifier_enable = get_config().get("log_email_notifier_enable", False) if not email_notifier_enable: raise ValueError( @@ -27,30 +27,28 @@ def log_and_notify_admin( " in DJANGO_LOGGING in your settings to activate email notifications." ) - _format = getattr( - logging_settings, "log_email_notifier_log_format", FORMAT_OPTIONS[1] - ) + _format = get_config().get("log_email_notifier_log_format", FORMAT_OPTIONS[1]) try: # create a LogRecord log_record = logger.makeRecord( name=logger.name, level=level, - fn=frame.f_code.co_filename, - lno=frame.f_lineno, + fn=frame.f_code.co_filename, # type: ignore + lno=frame.f_lineno, # type: ignore msg=message, args=(), exc_info=None, - func=frame.f_code.co_name, + func=frame.f_code.co_name, # type: ignore extra=extra, ) # Pass the LogRecord to the logger's handlers logger.handle(log_record) - except TypeError as e: + except (TypeError, AttributeError) as e: raise ValueError( f"Failed to log message due to invalid param. Original error: {e}" - ) + ) from e # Create a formatter instance and pass the email_notifier format formatter = logging.Formatter(LogConfig.resolve_format(_format)) diff --git a/django_logging/utils/log_email_notifier/notifier.py b/django_logging/utils/log_email_notifier/notifier.py index c721814..d0322ce 100644 --- a/django_logging/utils/log_email_notifier/notifier.py +++ b/django_logging/utils/log_email_notifier/notifier.py @@ -1,9 +1,9 @@ import logging import threading -from typing import List, Optional -from smtplib import SMTP -from email.mime.text import MIMEText from email.mime.multipart import MIMEMultipart +from email.mime.text import MIMEText +from smtplib import SMTP +from typing import List, Optional from django.conf import settings @@ -32,10 +32,10 @@ def send_email() -> None: settings.DEFAULT_FROM_EMAIL, recipient_list, msg.as_string() ) server.quit() - logger.info(f"Log Record has been sent to ADMIN EMAIL successfully.") + logger.info("Log Record has been sent to ADMIN EMAIL successfully.") - except Exception as e: - logger.warning(f"Email Notifier failed to send Log Record: {e}") + except Exception as e: # pylint: disable=broad-exception-caught + logger.warning("Email Notifier failed to send Log Record: %s", e) finally: if event: diff --git a/django_logging/utils/set_conf.py b/django_logging/utils/set_conf.py index 95ae4bd..34c6d06 100644 --- a/django_logging/utils/set_conf.py +++ b/django_logging/utils/set_conf.py @@ -2,18 +2,24 @@ from django.core.exceptions import ImproperlyConfigured +from django_logging.constants.ansi_colors import AnsiColors from django_logging.constants.config_types import ( - LogLevels, LogDir, LogFileFormatsType, - LogLevel, LogDateFormat, FormatOption, - NotifierLogLevels + FormatOption, + LogDateFormat, + LogDir, + LogFileFormatsType, + LogLevel, + LogLevels, + NotifierLogLevels, ) from django_logging.settings.conf import LogConfig, LogManager - -from typing import List -from django_logging.constants.ansi_colors import AnsiColors -from django_logging.utils.get_conf import is_auto_initialization_enabled, is_initialization_message_enabled +from django_logging.utils.get_conf import ( + is_auto_initialization_enabled, + is_initialization_message_enabled, +) +# pylint: disable=too-many-arguments def set_config( log_levels: LogLevels, log_dir: LogDir, @@ -27,11 +33,47 @@ def set_config( log_email_notifier_log_format: FormatOption, ) -> None: """ - Sets up the logging configuration. + Sets up the logging configuration based on the provided parameters. + + This function initializes and configures logging for the application, + including file-based logging, console output, and optional email notifications. + It will skip configuration if automatic initialization is disabled. Args: - log_levels (List[str]): A list of log levels to configure. - log_dir (str): The directory where log files will be stored. + log_levels (LogLevels): A list specifying the log levels for different handlers. + log_dir (LogDir): The directory where log files will be stored. + log_file_formats (LogFileFormatsType): The format of the log files. + console_level (LogLevel): The log level for console output. + console_format (FormatOption): The format for console log messages. + colorize_console (bool): Whether to colorize console output. + log_date_format (LogDateFormat): The date format used in log entries. + log_email_notifier_enable (bool): If True, enables email notifications for logs. + log_email_notifier_log_levels (NotifierLogLevels): The log levels for the email notifier. + log_email_notifier_log_format (FormatOption): The format for log messages in emails. + + Raises: + ValueError: If invalid log configuration parameters are provided. + ImproperlyConfigured: If the configuration is not set up correctly. + AttributeError: If an attribute-related error occurs during setup. + + + Example: + >>> set_config( + ... log_levels=['DEBUG', 'INFO'], + ... log_dir='/var/log/myapp/', + ... log_file_formats={'INFO': '%(levelname)s %(asctime)s %(message)s'}, + ... console_level='DEBUG', + ... console_format='{message}', + ... colorize_console=True, + ... log_date_format='%Y-%m-%d %H:%M:%S', + ... log_email_notifier_enable=False, + ... log_email_notifier_log_levels=['ERROR'], + ... log_email_notifier_log_format='%(module)s %(message)s' + ... ) + + Notes: + - The function performs system checks and logs warnings if configuration issues are detected. + - It also logs the current logging setup upon successful initialization. """ if not is_auto_initialization_enabled(): return @@ -53,17 +95,21 @@ def set_config( log_manager = LogManager(log_config) log_manager.create_log_files() log_manager.set_conf() - except (ValueError, ImproperlyConfigured, AttributeError): + except (ValueError, ImproperlyConfigured, AttributeError, FileNotFoundError): import logging logging.warning( "\n" - f"========================{AnsiColors.RED_BACKGROUND}DJANGO LOGGING{AnsiColors.RESET}" - f"========================\n" - f"{AnsiColors.RED}[CONFIGURATION ERROR]{AnsiColors.RESET}" - f" A configuration issue has been detected.\n" + "========================%sDJANGO LOGGING%s" + "========================\n" + "%s[CONFIGURATION ERROR]%s" + " A configuration issue has been detected.\n" "System checks will be run to provide more detailed information.\n" - "==============================================================\n" + "==============================================================\n", + AnsiColors.RED_BACKGROUND, + AnsiColors.RESET, + AnsiColors.RED, + AnsiColors.RESET, ) return diff --git a/django_logging/validators/config_validators.py b/django_logging/validators/config_validators.py index 64f0657..e899e2b 100644 --- a/django_logging/validators/config_validators.py +++ b/django_logging/validators/config_validators.py @@ -1,16 +1,15 @@ import os import re from datetime import datetime - from typing import List from django.core.checks import Error -from django_logging.constants import LOG_FORMAT_SPECIFIERS, FORMAT_OPTIONS +from django_logging.constants import FORMAT_OPTIONS, LOG_FORMAT_SPECIFIERS from django_logging.constants.config_types import ( FormatOption, - LogLevels, LogEmailNotifierType, + LogLevels, ) @@ -27,7 +26,7 @@ def validate_directory(path: str, config_name: str) -> List[Error]: elif not os.path.exists(path): try: os.makedirs(os.path.dirname(path), exist_ok=True) - except Exception as e: + except Exception: # pylint: disable=broad-exception-caught errors.append( Error( f"The path specified in {config_name} is not a valid path.", @@ -222,7 +221,7 @@ def validate_email_notifier(notifier_config: LogEmailNotifierType) -> List[Error errors.append( Error( f"Unknown type '{expected_type}' for {config_name}.", - hint=f"Check the expected types in LogEmailNotifierType.", + hint="Check the expected types in LogEmailNotifierType.", id=f"django_logging.E018_{config_name}", ) ) diff --git a/django_logging/validators/email_settings_validator.py b/django_logging/validators/email_settings_validator.py index d988368..c3d671f 100644 --- a/django_logging/validators/email_settings_validator.py +++ b/django_logging/validators/email_settings_validator.py @@ -1,6 +1,8 @@ from typing import List + from django.conf import settings from django.core.checks import Error + from django_logging.constants.required_email_settings import ( EMAIL_REQUIRED_SETTINGS, NOTIFIER_EXTRA_REQUIRED_SETTING, diff --git a/tests/commands/test_send_logs.py b/tests/commands/test_send_logs.py index 7b54d10..f4ed85e 100644 --- a/tests/commands/test_send_logs.py +++ b/tests/commands/test_send_logs.py @@ -1,15 +1,13 @@ import os -import tempfile import shutil - -from unittest.mock import patch, ANY, Mock +import tempfile +from unittest.mock import ANY, Mock, patch import pytest from django.core.exceptions import ImproperlyConfigured from django.core.management import call_command from django.test import TestCase - pytestmark = [pytest.mark.commands, pytest.mark.commands_send_logs] @@ -125,7 +123,7 @@ def test_handle_missing_log_dir(self, mock_validate_email_settings: Mock) -> Non call_command("send_logs", "test@example.com") self.assertIn( - f'ERROR:django_logging.management.commands.send_logs:Log directory "{non_existent_dir}" does not exist.', + f"ERROR:django_logging.management.commands.send_logs:Log directory '{non_existent_dir}' does not exist.", cm.output, ) mock_validate_email_settings.assert_not_called() diff --git a/tests/conftest.py b/tests/conftest.py index bcc541d..308db44 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,20 +1,20 @@ from tests.fixtures import ( - debug_log_record, - error_log_record, + admin_email_mock_settings, colored_formatter, + debug_log_record, email_handler, - request_middleware, - request_factory, + email_mock_settings, + error_log_record, get_response, - mock_settings, - reset_settings, log_config, log_manager, + magic_mock_logger, + mock_email_settings, mock_logger, - notifier_mock_logger, - email_mock_settings, + mock_settings, mock_smtp, - admin_email_mock_settings, - magic_mock_logger, - mock_email_settings + notifier_mock_logger, + request_factory, + request_middleware, + reset_settings, ) diff --git a/tests/filters/test_log_level_filter.py b/tests/filters/test_log_level_filter.py index 71542b7..ef389fe 100644 --- a/tests/filters/test_log_level_filter.py +++ b/tests/filters/test_log_level_filter.py @@ -1,11 +1,10 @@ import logging + import pytest + from django_logging.filters import LoggingLevelFilter -pytestmark = [ - pytest.mark.filters, - pytest.mark.filters_level_filter -] +pytestmark = [pytest.mark.filters, pytest.mark.filters_level_filter] class TestLoggingLevelFilter: diff --git a/tests/fixtures/__init__.py b/tests/fixtures/__init__.py index cb0d5e6..5b8601d 100644 --- a/tests/fixtures/__init__.py +++ b/tests/fixtures/__init__.py @@ -1,10 +1,14 @@ -from .log_record_fixture import debug_log_record, error_log_record from .colored_formatter_fixture import colored_formatter -from .email_handler_fixture import email_handler -from .request_middleware_fixture import request_middleware, request_factory, get_response -from .settings_fixture import mock_settings, reset_settings from .conf_fixture import log_config, log_manager -from .logger_fixture import mock_logger -from .email_notifier_fixture import mock_smtp, email_mock_settings, notifier_mock_logger -from .log_and_notify_fixture import admin_email_mock_settings, magic_mock_logger +from .email_handler_fixture import email_handler +from .email_notifier_fixture import email_mock_settings, mock_smtp, notifier_mock_logger from .email_settings_fixture import mock_email_settings +from .log_and_notify_fixture import admin_email_mock_settings, magic_mock_logger +from .log_record_fixture import debug_log_record, error_log_record +from .logger_fixture import mock_logger +from .request_middleware_fixture import ( + get_response, + request_factory, + request_middleware, +) +from .settings_fixture import mock_settings, reset_settings diff --git a/tests/fixtures/log_and_notify_fixture.py b/tests/fixtures/log_and_notify_fixture.py index f1cd2db..8565b48 100644 --- a/tests/fixtures/log_and_notify_fixture.py +++ b/tests/fixtures/log_and_notify_fixture.py @@ -2,7 +2,6 @@ from unittest.mock import MagicMock import pytest - from django.conf import settings diff --git a/tests/fixtures/log_record_fixture.py b/tests/fixtures/log_record_fixture.py index 35f571b..cab0438 100644 --- a/tests/fixtures/log_record_fixture.py +++ b/tests/fixtures/log_record_fixture.py @@ -1,4 +1,5 @@ -from logging import LogRecord, DEBUG, ERROR +from logging import DEBUG, ERROR, LogRecord + import pytest @@ -39,4 +40,4 @@ def error_log_record() -> LogRecord: msg="Test message", args=(), exc_info=None, - ) \ No newline at end of file + ) diff --git a/tests/fixtures/logger_fixture.py b/tests/fixtures/logger_fixture.py index 0a9ef67..cf021fb 100644 --- a/tests/fixtures/logger_fixture.py +++ b/tests/fixtures/logger_fixture.py @@ -1,6 +1,6 @@ from logging import Logger, getLogger from typing import Generator -from unittest.mock import patch, Mock +from unittest.mock import Mock, patch import pytest diff --git a/tests/fixtures/settings_fixture.py b/tests/fixtures/settings_fixture.py index 6ba31a8..a26642c 100644 --- a/tests/fixtures/settings_fixture.py +++ b/tests/fixtures/settings_fixture.py @@ -1,8 +1,7 @@ -from typing import Generator, Dict +from typing import Dict, Generator from unittest.mock import patch import pytest - from django.conf import settings diff --git a/tests/formatters/test_colored_formatter.py b/tests/formatters/test_colored_formatter.py index bec9446..fe4682a 100644 --- a/tests/formatters/test_colored_formatter.py +++ b/tests/formatters/test_colored_formatter.py @@ -1,15 +1,11 @@ import logging - -from unittest.mock import patch, MagicMock +from unittest.mock import MagicMock, patch import pytest from django_logging.formatters import ColoredFormatter -pytestmark = [ - pytest.mark.formatters, - pytest.mark.colored_formatter - ] +pytestmark = [pytest.mark.formatters, pytest.mark.colored_formatter] class TestColoredFormatter: diff --git a/tests/handlers/test_email_handler.py b/tests/handlers/test_email_handler.py index 5e47404..45c37ea 100644 --- a/tests/handlers/test_email_handler.py +++ b/tests/handlers/test_email_handler.py @@ -1,7 +1,7 @@ import logging -import pytest -from unittest.mock import patch, MagicMock, ANY +from unittest.mock import ANY, MagicMock, patch +import pytest from django.conf import settings from django_logging.handlers.email_handler import EmailHandler diff --git a/tests/middleware/test_request_middleware.py b/tests/middleware/test_request_middleware.py index 60f7a84..eecb50f 100644 --- a/tests/middleware/test_request_middleware.py +++ b/tests/middleware/test_request_middleware.py @@ -5,8 +5,8 @@ from django.contrib.auth import get_user_model from django.contrib.auth.models import AnonymousUser from django.test import RequestFactory -from django_logging.middleware import RequestLogMiddleware +from django_logging.middleware import RequestLogMiddleware pytestmark = [pytest.mark.middleware, pytest.mark.request_middleware] diff --git a/tests/settings/test_checks.py b/tests/settings/test_checks.py index b674f74..8668bbf 100644 --- a/tests/settings/test_checks.py +++ b/tests/settings/test_checks.py @@ -2,8 +2,9 @@ from unittest.mock import patch import pytest -from django.core.checks import Error from django.conf import settings +from django.core.checks import Error + from django_logging.settings.checks import check_logging_settings pytestmark = [pytest.mark.settings, pytest.mark.settings_checks] diff --git a/tests/settings/test_conf.py b/tests/settings/test_conf.py index 19e0f19..14b1e2f 100644 --- a/tests/settings/test_conf.py +++ b/tests/settings/test_conf.py @@ -1,10 +1,11 @@ -import pytest -from unittest import mock import os +from shutil import rmtree +from unittest import mock -from django_logging.settings.conf import LogConfig, LogManager -from django_logging.constants import FORMAT_OPTIONS +import pytest +from django_logging.constants import FORMAT_OPTIONS +from django_logging.settings.conf import LogConfig, LogManager pytestmark = [pytest.mark.settings, pytest.mark.settings_conf] @@ -92,7 +93,7 @@ def test_create_log_files(self, log_manager: LogManager) -> None: expected_file_path = os.path.join( "/tmp/logs", f"{log_level.lower()}.log" ) - open_mock.assert_any_call(expected_file_path, "w") + open_mock.assert_any_call(expected_file_path, "w", encoding="utf-8") log_manager.log_files[log_level] = expected_file_path @@ -143,6 +144,9 @@ def test_set_conf(self, log_manager: LogManager) -> None: assert "handlers" in config["root"] assert "disable_existing_loggers" in config + # Remove the log dir created by test + rmtree("/tmp", ignore_errors=True) + def test_log_manager_get_log_file(self, log_manager: LogManager) -> None: """ Test retrieval of log file paths. diff --git a/tests/utils/test_context_manager.py b/tests/utils/test_context_manager.py index 29a440b..a758130 100644 --- a/tests/utils/test_context_manager.py +++ b/tests/utils/test_context_manager.py @@ -1,11 +1,9 @@ import logging from unittest import mock + import pytest -from django_logging.utils.context_manager import ( - config_setup, - _restore_logging_config, -) +from django_logging.utils.context_manager import _restore_logging_config, config_setup pytestmark = [pytest.mark.utils, pytest.mark.utils_context_manager] @@ -29,7 +27,9 @@ def test_config_setup_auto_initialization_enabled(self) -> None: return_value=True, ): with pytest.raises(ValueError) as excinfo: + # fmt: off with config_setup(): "" + # fmt: on assert ( str(excinfo.value) @@ -64,18 +64,18 @@ def test_config_setup_applies_custom_config( ): with mock.patch( "django_logging.utils.context_manager.get_config", - return_value=( - ["INFO"], - "/tmp/logs", - {"INFO": 1}, - "DEBUG", - 2, - False, - "", - False, - [], - 1, - ), + return_value={ + "log_levels": ["INFO"], + "log_dir": "/tmp/logs", + "log_file_formats": {"INFO": 1}, + "console_level": "DEBUG", + "console_format": 2, + "colorize_console": False, + "log_date_format": "", + "log_email_notifier_enable": False, + "log_email_notifier_log_levels": [], + "log_email_notifier_log_format": 1, + }, ): with mock.patch( "django_logging.utils.context_manager.LogManager" @@ -117,18 +117,18 @@ def test_config_context_restores_original_config( ): with mock.patch( "django_logging.utils.context_manager.get_config", - return_value=( - ["INFO"], - "/tmp/logs", - {"INFO": 1}, - "DEBUG", - 2, - False, - "", - False, - [], - 1, - ), + return_value={ + "log_levels": ["INFO"], + "log_dir": "/tmp/logs", + "log_file_formats": {"INFO": 1}, + "console_level": "DEBUG", + "console_format": 2, + "colorize_console": False, + "log_date_format": "", + "log_email_notifier_enable": False, + "log_email_notifier_log_levels": [], + "log_email_notifier_log_format": 1, + }, ): with mock.patch("django_logging.utils.context_manager.LogManager"): with config_setup(): diff --git a/tests/utils/test_email_notifier.py b/tests/utils/test_email_notifier.py index 7d9d7a4..c0997b1 100644 --- a/tests/utils/test_email_notifier.py +++ b/tests/utils/test_email_notifier.py @@ -1,6 +1,9 @@ -import pytest import threading -from unittest.mock import MagicMock +from smtplib import SMTPException +from unittest.mock import ANY, MagicMock + +import pytest + from django_logging.utils.log_email_notifier.notifier import send_email_async pytestmark = [pytest.mark.utils, pytest.mark.utils_email_notifier] @@ -101,7 +104,7 @@ def test_send_email_async_failure( - The success message was not logged. """ mock_info, mock_warning = notifier_mock_logger - mock_smtp.side_effect = Exception("SMTP failure") + mock_smtp.side_effect = SMTPException("SMTP failure") email_sent_event = threading.Event() @@ -112,6 +115,6 @@ def test_send_email_async_failure( email_sent_event.wait() mock_warning.assert_called_once_with( - "Email Notifier failed to send Log Record: SMTP failure" + "Email Notifier failed to send Log Record: %s", ANY ) mock_info.assert_not_called() diff --git a/tests/utils/test_get_conf.py b/tests/utils/test_get_conf.py index 8742393..cf00904 100644 --- a/tests/utils/test_get_conf.py +++ b/tests/utils/test_get_conf.py @@ -1,13 +1,14 @@ from typing import Dict +from unittest.mock import patch import pytest -from unittest.mock import patch from django.conf import settings + from django_logging.utils.get_conf import ( get_config, - use_email_notifier_template, is_auto_initialization_enabled, is_initialization_message_enabled, + use_email_notifier_template, ) pytestmark = [pytest.mark.utils, pytest.mark.utils_get_conf] @@ -31,23 +32,49 @@ def test_get_conf(self, mock_settings: Dict) -> None: - The returned configuration matches the expected values for logging levels, directory, file formats, console settings, email notifier settings, etc. """ - expected = [ - ["DEBUG", "INFO"], # log_levels - "/custom/log/dir", # log_dir - { + expected = { + "log_levels": ["DEBUG", "INFO"], + "log_dir": "/custom/log/dir", + "log_file_formats": { "DEBUG": "%(asctime)s - %(name)s - %(levelname)s - %(message)s" - }, # log_file_formats - "WARNING", # console_level - "%(levelname)s - %(message)s", # console_format - True, # colorize_console - "%Y-%m-%d", # log_date_format - True, # log_email_notifier_enable - ["ERROR", None], # log_email_notifier_log_levels - "custom_format", # log_email_notifier_log_format - ] + }, + "console_level": "WARNING", + "console_format": "%(levelname)s - %(message)s", + "colorize_console": True, + "log_date_format": "%Y-%m-%d", + "log_email_notifier_enable": True, + "log_email_notifier_log_levels": ["ERROR", None], + "log_email_notifier_log_format": "custom_format", + } + print(expected) result = get_config() assert result == expected + result = get_config(extra_info=True) + + expected_extra = { + "log_levels": ["DEBUG", "INFO"], + "log_dir": "/custom/log/dir", + "log_file_formats": { + "DEBUG": "%(asctime)s - %(name)s - %(levelname)s - %(message)s" + }, + "console_level": "WARNING", + "console_format": "%(levelname)s - %(message)s", + "colorize_console": True, + "log_date_format": "%Y-%m-%d", + "log_email_notifier_enable": True, + "log_email_notifier_log_levels": ["ERROR", None], + "log_email_notifier_log_format": "custom_format", + "log_email_notifier": { + "ENABLE": True, + "NOTIFY_ERROR": True, + "NOTIFY_CRITICAL": False, + "LOG_FORMAT": "custom_format", + }, + } + + assert result == expected_extra + def test_use_email_notifier_template(self, mock_settings: Dict) -> None: """ Test that the `use_email_notifier_template` function correctly reads the `USE_TEMPLATE` setting. diff --git a/tests/utils/test_log_and_notify.py b/tests/utils/test_log_and_notify.py index c584222..2bd2dc3 100644 --- a/tests/utils/test_log_and_notify.py +++ b/tests/utils/test_log_and_notify.py @@ -1,15 +1,17 @@ import logging +from typing import Dict +from unittest.mock import MagicMock, patch import pytest -from unittest.mock import patch, MagicMock from django.conf import settings + from django_logging.utils.log_email_notifier.log_and_notify import log_and_notify_admin pytestmark = [pytest.mark.utils, pytest.mark.utils_log_and_notify] class TestLogAndNotify: - def mock_log_config(self, email_notifier_enable: bool = True) -> MagicMock: + def mock_log_config(self, email_notifier_enable: bool = True) -> Dict: """ Helper function to create a mock LogConfig object. @@ -20,13 +22,13 @@ def mock_log_config(self, email_notifier_enable: bool = True) -> MagicMock: Returns: -------- - MagicMock - A mock object with specified settings. + Dict + A dictionary with the specified settings. """ - return MagicMock( - log_email_notifier_enable=email_notifier_enable, - log_email_notifier_log_format=1, - ) + return { + "log_email_notifier_enable": email_notifier_enable, + "log_email_notifier_log_format": 1, + } def test_log_and_notify_email_notifier_disabled( self, magic_mock_logger: MagicMock diff --git a/tests/utils/test_set_conf.py b/tests/utils/test_set_conf.py index c350296..e8d061e 100644 --- a/tests/utils/test_set_conf.py +++ b/tests/utils/test_set_conf.py @@ -1,10 +1,9 @@ -from unittest.mock import patch, MagicMock import os +from unittest.mock import MagicMock, patch import pytest from django_logging.constants.ansi_colors import AnsiColors - from django_logging.utils.set_conf import set_config pytestmark = [pytest.mark.utils, pytest.mark.utils_set_conf] @@ -207,10 +206,14 @@ def test_set_config_exception_handling( ) mock_warning.assert_called_once_with( "\n" - f"========================{AnsiColors.RED_BACKGROUND}DJANGO LOGGING{AnsiColors.RESET}" - f"========================\n" - f"{AnsiColors.RED}[CONFIGURATION ERROR]{AnsiColors.RESET}" - f" A configuration issue has been detected.\n" + "========================%sDJANGO LOGGING%s" + "========================\n" + "%s[CONFIGURATION ERROR]%s" + " A configuration issue has been detected.\n" "System checks will be run to provide more detailed information.\n" - "==============================================================\n" + "==============================================================\n", + AnsiColors.RED_BACKGROUND, + AnsiColors.RESET, + AnsiColors.RED, + AnsiColors.RESET, ) diff --git a/tests/validators/test_config_validators.py b/tests/validators/test_config_validators.py index c4adb05..570f57e 100644 --- a/tests/validators/test_config_validators.py +++ b/tests/validators/test_config_validators.py @@ -4,13 +4,13 @@ from django_logging.constants.config_types import LogLevels from django_logging.validators.config_validators import ( - validate_directory, - validate_log_levels, - validate_format_string, - validate_format_option, validate_boolean_setting, validate_date_format, + validate_directory, validate_email_notifier, + validate_format_option, + validate_format_string, + validate_log_levels, ) pytestmark = [pytest.mark.validators, pytest.mark.config_validator] diff --git a/tests/validators/test_email_settings_validator.py b/tests/validators/test_email_settings_validator.py index 3fefb85..30b9d5b 100644 --- a/tests/validators/test_email_settings_validator.py +++ b/tests/validators/test_email_settings_validator.py @@ -1,7 +1,7 @@ from typing import Dict +from unittest.mock import patch import pytest -from unittest.mock import patch from django.conf import settings from django_logging.validators.email_settings_validator import check_email_settings