Skip to content

Feature/logging system #112

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 21 commits into from
Jan 27, 2021
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
b9d44a9
first logging commit
Jan 19, 2021
87ff7bc
took main from develop logging commit
Jan 19, 2021
0299b10
Added docstring and annotations to logger, added logger tests
Jan 21, 2021
1408056
Added docstring and annotations to logger, added logger tests
Jan 21, 2021
3cd2e37
Removed merge conflicts
Jan 21, 2021
c644bc0
Fixed an issue with variable name
Jan 21, 2021
ea9c0f4
Fixed requirements.txt duplicate rows.
Jan 21, 2021
c1e0221
Fixed requirements.txt duplicate rows, again.
Jan 21, 2021
5f14fc6
Fixed merge suggestions in logger_customizer.py
Jan 22, 2021
41febf5
Fixed merge suggestions in logger_customizer.py
Jan 22, 2021
c07f18d
Fixed linting in test_logging, conftest and logger_customizer, still …
Jan 22, 2021
973b4fc
Took config from global config file,
Jan 22, 2021
8c07e19
Fixed tests, created new clientless logger fixture. Another fixture l…
Jan 23, 2021
551936d
Updated conftest and separated logger fixtures to new file, fix mergi…
Jan 23, 2021
310bd58
Fix requirements for merging from develop
Jan 23, 2021
e71328d
Finished logger_fixture, added logging config file to tests, added lo…
Jan 23, 2021
88713de
Added logger_fixture and config which werent added for some reason
Jan 23, 2021
91322d9
Changed logging config to be received by separate parameters for simp…
Jan 24, 2021
bca9c7c
removed logging_config file which is no longer needed
Jan 26, 2021
26f0a40
Fixing merge conflicts
Jan 27, 2021
1a7fd84
Fixing merge conflicts - missing whitespace on config.py.example
Jan 27, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"cSpell.ignoreWords": [
"smtpdfix"
]
}
18 changes: 16 additions & 2 deletions app/config.py.example
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ from fastapi_mail import ConnectionConfig

# flake8: noqa

# general
# GENERAL
DOMAIN = 'Our-Domain'

# DATABASE
Expand All @@ -18,7 +18,7 @@ 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//'

Expand All @@ -33,3 +33,17 @@ email_conf = ConnectionConfig(
MAIL_SSL=False,
USE_CREDENTIALS=True,
)

# LOGGER
LOGGER = {
"logger": {
"path": "./var/log",
"filename": "calendar.log",
"level": "error",
"rotation": "20 days",
"retention": "1 month",
"format": ("<level>{level: <8}</level> <green>{time:YYYY-MM-DD HH:mm:ss.SSS}</green>"
" - <cyan>{name}</cyan>:<cyan>{function}</cyan>"
" - <level>{message}</level>")
}
}
110 changes: 110 additions & 0 deletions app/internal/logger_customizer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import json
import sys
from typing import Union, Dict

from pathlib import Path
from loguru import logger, _Logger as Logger


class LoggerConfigError(Exception):
pass


class LoggerCustomizer:

@classmethod
def make_logger(cls, config_file_or_dict: Union[Path, Dict],
logger_name: str) -> Logger:
"""Creates a loguru logger from given configuration path or dict.

Args:
config_file_or_dict (Union[Path, Dict]): Path to logger
configuration file or dictionary of configuration
logger_name (str): Logger instance created from configuration

Raises:
LoggerConfigError: Error raised when the configuration is invalid

Returns:
Logger: Loguru logger instance
"""

config = cls.load_logging_config(config_file_or_dict)
try:
logging_config = config.get(logger_name)
logs_path = logging_config.get('path')
log_file_path = logging_config.get('filename')

logger = cls.customize_logging(
file_path=Path(logs_path) / Path(log_file_path),
level=logging_config.get('level'),
retention=logging_config.get('retention'),
rotation=logging_config.get('rotation'),
format=logging_config.get('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

@classmethod
def load_logging_config(cls, config: Union[Path, Dict]) -> Dict:
"""Loads logging configuration from file or dict

Args:
config (Union[Path, Dict]): Path to logging configuration file

Returns:
Dict: Configuration parsed as dictionary
"""
if isinstance(config, Path):
with open(config) as config_file:
used_config = json.load(config_file)
else:
used_config = config

return used_config
10 changes: 10 additions & 0 deletions app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,21 @@
MEDIA_PATH, STATIC_PATH, templates)
from app.routers import agenda, event, profile, email, invitation

from app.internal.logger_customizer import LoggerCustomizer

from app import config

models.Base.metadata.create_all(bind=engine)

app = FastAPI()
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.LOGGER, "logger")
app.logger = logger


app.include_router(profile.router)
app.include_router(event.router)
app.include_router(agenda.router)
Expand All @@ -21,6 +30,7 @@


@app.get("/")
@app.logger.catch()
async def home(request: Request):
return templates.TemplateResponse("home.html", {
"request": request,
Expand Down
3 changes: 2 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ idna==2.10
importlib-metadata==3.3.0
iniconfig==1.1.1
Jinja2==2.11.2
loguru==0.5.3
MarkupSafe==1.1.1
packaging==20.8
Pillow==8.1.0
Expand Down Expand Up @@ -49,4 +50,4 @@ uvicorn==0.13.3
watchgod==0.6
websockets==8.1
wsproto==1.0.0
zipp==3.4.0
zipp==3.4.0
1 change: 1 addition & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
'tests.invitation_fixture',
'tests.association_fixture',
'tests.client_fixture',
'tests.logger_fixture',
'smtpdfix',
]

Expand Down
36 changes: 36 additions & 0 deletions tests/logger_fixture.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
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.LOGGER, "logger")

return _logger


@pytest.fixture(scope='module')
def logger_external_configuration():
from pathlib import Path
config_path = Path(__file__).parent
config_path = config_path.absolute() / 'logging_config.json'
_logger = LoggerCustomizer.make_logger(config_path, "logger1")

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)
10 changes: 10 additions & 0 deletions tests/logging_config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"logger1": {
"path": "./var/log4",
"filename": "calendar3.log",
"level": "debug",
"rotation": "20 days",
"retention": "1 month",
"format": "<level>{level: <8}</level> <green>{time:YYYY-MM-DD HH:mm:ss.SSS}</green> - <cyan>{name}</cyan>:<cyan>{function}</cyan> - <level>{message}</level>"
}
}
55 changes: 55 additions & 0 deletions tests/test_logger.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import logging

import pytest

from app.internal.logger_customizer import LoggerCustomizer, LoggerConfigError


class TestLogger:
@staticmethod
def test_configuration_file(caplog, logger_external_configuration):
with caplog.at_level(logging.ERROR):
logger_external_configuration.error('Testing configuration ERROR')
assert 'Testing configuration' in caplog.text

@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():
bad_config = {
"logger": {
"path": "./var/log",
"filename": "calendar.log",
"level": "eror",
"rotation": "20 days",
"retention": "1 month",
"format": ("<level>{level: <8}</level> "
"<green>{time:YYYY-MM-DD HH:mm:ss.SSS}</green>"
"- <cyan>{name}</cyan>:<cyan>{function}</cyan>"
" - <level>{message}</level>")
}
}
with pytest.raises(LoggerConfigError):
LoggerCustomizer.make_logger(bad_config, 'logger')