diff --git a/.gitignore b/.gitignore index 7cdd6cde..f34de73e 100644 --- a/.gitignore +++ b/.gitignore @@ -138,7 +138,6 @@ dmypy.json # Pyre type checker .pyre/ - # VScode .vscode/ app/.vscode/ diff --git a/app/config.py.example b/app/config.py.example index b165b5f1..50b524b0 100644 --- a/app/config.py.example +++ b/app/config.py.example @@ -13,7 +13,7 @@ class Settings(BaseSettings): env_file = ".env" -# general +# GENERAL DOMAIN = 'Our-Domain' # DATABASE @@ -29,11 +29,11 @@ AVATAR_SIZE = (120, 120) # API-KEYS WEATHER_API_KEY = os.getenv('WEATHER_API_KEY') -# export +# EXPORT ICAL_VERSION = '2.0' PRODUCT_ID = '-//Our product id//' -# email +# EMAIL email_conf = ConnectionConfig( MAIL_USERNAME=os.getenv("MAIL_USERNAME") or "user", MAIL_PASSWORD=os.getenv("MAIL_PASSWORD") or "password", @@ -47,3 +47,14 @@ email_conf = ConnectionConfig( # PATHS STATIC_ABS_PATH = os.path.abspath("static") + +# LOGGER +LOG_PATH = "./var/log" +LOG_FILENAME = "calendar.log" +LOG_LEVEL = "error" +LOG_ROTATION_INTERVAL = "20 days" +LOG_RETENTION_INTERVAL = "1 month" +LOG_FORMAT = ("{level: <8}" + " {time:YYYY-MM-DD HH:mm:ss.SSS}" + " - {name}:{function}" + " - {message}") diff --git a/app/internal/logger_customizer.py b/app/internal/logger_customizer.py new file mode 100644 index 00000000..274f6c74 --- /dev/null +++ b/app/internal/logger_customizer.py @@ -0,0 +1,94 @@ +import sys + +from pathlib import Path +from loguru import logger, _Logger as Logger + + +class LoggerConfigError(Exception): + pass + + +class LoggerCustomizer: + + @classmethod + def make_logger(cls, log_path: Path, + log_filename: str, + log_level: str, + log_rotation_interval: str, + log_retention_interval: str, + log_format: str) -> Logger: + """Creates a logger from given configurations + + Args: + log_path (Path): Path where the log file is located + log_filename (str): + + log_level (str): The level we want to start logging from + log_rotation_interval (str): Every how long the logs + would be rotated + log_retention_interval (str): Amount of time in words defining + how long the log will be kept + log_format (str): The logging format + + Raises: + LoggerConfigError: Error raised when the configuration is invalid + + Returns: + Logger: Loguru logger instance + """ + try: + logger = cls.customize_logging( + file_path=Path(log_path) / Path(log_filename), + level=log_level, + retention=log_retention_interval, + rotation=log_rotation_interval, + format=log_format + ) + except (TypeError, ValueError) as err: + raise LoggerConfigError( + f"You have an issue with the logger configuration: {err!r}, " + "fix it please") + + return logger + + @classmethod + def customize_logging(cls, + file_path: Path, + level: str, + rotation: str, + retention: str, + format: str + ) -> Logger: + """Used to customize the logger instance + + Args: + file_path (Path): Path where the log file is located + level (str): The level wanted to start logging from + rotation (str): Every how long the logs would be + rotated(creation of new file) + retention (str): Amount of time in words defining how + long a log is kept + format (str): The logging format + + Returns: + Logger: Instance of a logger mechanism + """ + logger.remove() + logger.add( + sys.stdout, + enqueue=True, + backtrace=True, + level=level.upper(), + format=format + ) + logger.add( + str(file_path), + rotation=rotation, + retention=retention, + enqueue=True, + backtrace=True, + level=level.upper(), + format=format + ) + + return logger diff --git a/app/main.py b/app/main.py index 9f562d34..c72c418e 100644 --- a/app/main.py +++ b/app/main.py @@ -9,6 +9,8 @@ from app.routers import ( agenda, dayview, email, event, invitation, profile, search, telegram) from app.telegram.bot import telegram_bot +from app.internal.logger_customizer import LoggerCustomizer +from app import config def create_tables(engine, psql_environment): @@ -27,6 +29,16 @@ def create_tables(engine, psql_environment): app.mount("/static", StaticFiles(directory=STATIC_PATH), name="static") app.mount("/media", StaticFiles(directory=MEDIA_PATH), name="media") +# Configure logger +logger = LoggerCustomizer.make_logger(config.LOG_PATH, + config.LOG_FILENAME, + config.LOG_LEVEL, + config.LOG_ROTATION_INTERVAL, + config.LOG_RETENTION_INTERVAL, + config.LOG_FORMAT) +app.logger = logger + + app.include_router(profile.router) app.include_router(event.router) app.include_router(agenda.router) @@ -40,6 +52,7 @@ def create_tables(engine, psql_environment): @app.get("/") +@app.logger.catch() async def home(request: Request): return templates.TemplateResponse("home.html", { "request": request, diff --git a/requirements.txt b/requirements.txt index d1cf56d5..0159b2af 100644 --- a/requirements.txt +++ b/requirements.txt @@ -39,6 +39,7 @@ iniconfig==1.1.1 Jinja2==2.11.2 joblib==1.0.0 lazy-object-proxy==1.5.2 +loguru==0.5.3 mypy==0.790 mypy-extensions==0.4.3 MarkupSafe==1.1.1 @@ -86,4 +87,4 @@ watchgod==0.6 websockets==8.1 word-forms==2.1.0 wsproto==1.0.0 -zipp==3.4.0 +zipp==3.4.0 \ No newline at end of file diff --git a/tests/conftest.py b/tests/conftest.py index 1100fc70..33ff6148 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -12,6 +12,7 @@ 'tests.association_fixture', 'tests.client_fixture', 'tests.asyncio_fixture', + 'tests.logger_fixture', 'smtpdfix', ] diff --git a/tests/logger_fixture.py b/tests/logger_fixture.py new file mode 100644 index 00000000..2684d336 --- /dev/null +++ b/tests/logger_fixture.py @@ -0,0 +1,31 @@ +import logging + +import pytest +from _pytest.logging import caplog as _caplog # noqa: F401 +from loguru import logger + +from app import config +from app.internal.logger_customizer import LoggerCustomizer + + +@pytest.fixture(scope='module') +def logger_instance(): + _logger = LoggerCustomizer.make_logger(config.LOG_PATH, + config.LOG_FILENAME, + config.LOG_LEVEL, + config.LOG_ROTATION_INTERVAL, + config.LOG_RETENTION_INTERVAL, + config.LOG_FORMAT) + + return _logger + + +@pytest.fixture +def caplog(_caplog): # noqa: F811 + class PropagateHandler(logging.Handler): + def emit(self, record): + logging.getLogger(record.name).handle(record) + + handler_id = logger.add(PropagateHandler(), format="{message} {extra}") + yield _caplog + logger.remove(handler_id) diff --git a/tests/test_logger.py b/tests/test_logger.py new file mode 100644 index 00000000..0c4cd97e --- /dev/null +++ b/tests/test_logger.py @@ -0,0 +1,42 @@ +import logging + +import pytest + +from app.internal.logger_customizer import LoggerCustomizer, LoggerConfigError +from app import config + + +class TestLogger: + @staticmethod + def test_log_debug(caplog, logger_instance): + with caplog.at_level(logging.DEBUG): + logger_instance.debug('Is it debugging now?') + assert 'Is it debugging now?' in caplog.text + + @staticmethod + def test_log_info(caplog, logger_instance): + with caplog.at_level(logging.INFO): + logger_instance.info('App started') + assert 'App started' in caplog.text + + @staticmethod + def test_log_error(caplog, logger_instance): + with caplog.at_level(logging.ERROR): + logger_instance.error('Something bad happened!') + assert 'Something bad happened!' in caplog.text + + @staticmethod + def test_log_critical(caplog, logger_instance): + with caplog.at_level(logging.CRITICAL): + logger_instance.critical("WE'RE DOOMED!") + assert "WE'RE DOOMED!" in caplog.text + + @staticmethod + def test_bad_configuration(): + with pytest.raises(LoggerConfigError): + LoggerCustomizer.make_logger(config.LOG_PATH, + config.LOG_FILENAME, + 'eror', + config.LOG_ROTATION_INTERVAL, + config.LOG_RETENTION_INTERVAL, + config.LOG_FORMAT)