Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 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
14 changes: 14 additions & 0 deletions app/config.py.example
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,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>")
}
}
111 changes: 111 additions & 0 deletions app/internal/logger_customizer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import json
import sys
from typing import Union

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
9 changes: 9 additions & 0 deletions app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,29 @@
MEDIA_PATH, STATIC_PATH, templates)
from app.routers import agenda, event, profile, email

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)
app.include_router(email.router)


@app.get("/")
@app.logger.catch()
async def home(request: Request):
return templates.TemplateResponse("home.html", {
"request": request,
Expand Down
7 changes: 4 additions & 3 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,6 @@ click==7.1.2
colorama==0.4.4
coverage==5.3.1
fastapi==0.63.0
fastapi_mail==0.3.3.1
faker==5.6.2
smtpdfix==0.2.6
h11==0.12.0
h2==4.0.0
hpack==4.0.0
Expand All @@ -19,6 +16,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 All @@ -45,3 +43,6 @@ watchgod==0.6
websockets==8.1
wsproto==1.0.0
zipp==3.4.0
fastapi_mail==0.3.3.1
faker==5.6.2
smtpdfix==0.2.6
31 changes: 30 additions & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,23 @@
import datetime
import logging

import pytest
from _pytest.logging import caplog as _caplog # noqa: F401
from loguru import logger
from faker import Faker

from app.database.database import Base, SessionLocal, engine
from app.database.models import Event, User
from app.internal.logger_customizer import LoggerCustomizer
from app.main import app
from app.routers import profile
from faker import Faker
from app import config

from fastapi.testclient import TestClient
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker


pytest_plugins = "smtpdfix"

SQLALCHEMY_TEST_DATABASE_URL = "sqlite:///./test.db"
Expand All @@ -29,6 +38,15 @@ def client():
return TestClient(app)


@pytest.fixture(scope='module')
def client_with_logger():
logger = LoggerCustomizer.make_logger(config.LOGGER, "logger")

client = TestClient(app)
client.logger = logger
return client


@pytest.fixture
def session():
Base.metadata.create_all(bind=engine)
Expand Down Expand Up @@ -82,3 +100,14 @@ def profile_test_client():
with TestClient(app) as client:
yield client
app.dependency_overrides = {}


@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)
49 changes: 49 additions & 0 deletions tests/test_logger.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import logging

import pytest

from app.internal.logger_customizer import LoggerCustomizer, LoggerConfigError


class TestLogger:
@staticmethod
def test_log_debug(caplog, client_with_logger):
with caplog.at_level(logging.DEBUG):
client_with_logger.logger.debug('Is it debugging now?')
assert 'Is it debugging now?' in caplog.text

@staticmethod
def test_log_info(caplog, client_with_logger):
with caplog.at_level(logging.INFO):
client_with_logger.logger.info('App started')
assert 'App started' in caplog.text

@staticmethod
def test_log_error(caplog, client_with_logger):
with caplog.at_level(logging.ERROR):
client_with_logger.logger.error('Something bad happened!')
assert 'Something bad happened!' in caplog.text

@staticmethod
def test_log_critical(caplog, client_with_logger):
with caplog.at_level(logging.CRITICAL):
client_with_logger.logger.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')