Skip to content

Commit df1ef8d

Browse files
Liad-nLiad Noam
andauthored
Feature/logging system (#112)
* feat: first logging commit Co-authored-by: Liad Noam <LiadN@Taldor.corp>
1 parent 6e317e8 commit df1ef8d

File tree

8 files changed

+197
-5
lines changed

8 files changed

+197
-5
lines changed

.gitignore

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,6 @@ dmypy.json
138138
# Pyre type checker
139139
.pyre/
140140

141-
142141
# VScode
143142
.vscode/
144143
app/.vscode/

app/config.py.example

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ class Settings(BaseSettings):
1313
env_file = ".env"
1414

1515

16-
# general
16+
# GENERAL
1717
DOMAIN = 'Our-Domain'
1818

1919
# DATABASE
@@ -29,11 +29,11 @@ AVATAR_SIZE = (120, 120)
2929
# API-KEYS
3030
WEATHER_API_KEY = os.getenv('WEATHER_API_KEY')
3131

32-
# export
32+
# EXPORT
3333
ICAL_VERSION = '2.0'
3434
PRODUCT_ID = '-//Our product id//'
3535

36-
# email
36+
# EMAIL
3737
email_conf = ConnectionConfig(
3838
MAIL_USERNAME=os.getenv("MAIL_USERNAME") or "user",
3939
MAIL_PASSWORD=os.getenv("MAIL_PASSWORD") or "password",
@@ -47,3 +47,14 @@ email_conf = ConnectionConfig(
4747

4848
# PATHS
4949
STATIC_ABS_PATH = os.path.abspath("static")
50+
51+
# LOGGER
52+
LOG_PATH = "./var/log"
53+
LOG_FILENAME = "calendar.log"
54+
LOG_LEVEL = "error"
55+
LOG_ROTATION_INTERVAL = "20 days"
56+
LOG_RETENTION_INTERVAL = "1 month"
57+
LOG_FORMAT = ("<level>{level: <8}</level>"
58+
" <green>{time:YYYY-MM-DD HH:mm:ss.SSS}</green>"
59+
" - <cyan>{name}</cyan>:<cyan>{function}</cyan>"
60+
" - <level>{message}</level>")

app/internal/logger_customizer.py

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import sys
2+
3+
from pathlib import Path
4+
from loguru import logger, _Logger as Logger
5+
6+
7+
class LoggerConfigError(Exception):
8+
pass
9+
10+
11+
class LoggerCustomizer:
12+
13+
@classmethod
14+
def make_logger(cls, log_path: Path,
15+
log_filename: str,
16+
log_level: str,
17+
log_rotation_interval: str,
18+
log_retention_interval: str,
19+
log_format: str) -> Logger:
20+
"""Creates a logger from given configurations
21+
22+
Args:
23+
log_path (Path): Path where the log file is located
24+
log_filename (str):
25+
26+
log_level (str): The level we want to start logging from
27+
log_rotation_interval (str): Every how long the logs
28+
would be rotated
29+
log_retention_interval (str): Amount of time in words defining
30+
how long the log will be kept
31+
log_format (str): The logging format
32+
33+
Raises:
34+
LoggerConfigError: Error raised when the configuration is invalid
35+
36+
Returns:
37+
Logger: Loguru logger instance
38+
"""
39+
try:
40+
logger = cls.customize_logging(
41+
file_path=Path(log_path) / Path(log_filename),
42+
level=log_level,
43+
retention=log_retention_interval,
44+
rotation=log_rotation_interval,
45+
format=log_format
46+
)
47+
except (TypeError, ValueError) as err:
48+
raise LoggerConfigError(
49+
f"You have an issue with the logger configuration: {err!r}, "
50+
"fix it please")
51+
52+
return logger
53+
54+
@classmethod
55+
def customize_logging(cls,
56+
file_path: Path,
57+
level: str,
58+
rotation: str,
59+
retention: str,
60+
format: str
61+
) -> Logger:
62+
"""Used to customize the logger instance
63+
64+
Args:
65+
file_path (Path): Path where the log file is located
66+
level (str): The level wanted to start logging from
67+
rotation (str): Every how long the logs would be
68+
rotated(creation of new file)
69+
retention (str): Amount of time in words defining how
70+
long a log is kept
71+
format (str): The logging format
72+
73+
Returns:
74+
Logger: Instance of a logger mechanism
75+
"""
76+
logger.remove()
77+
logger.add(
78+
sys.stdout,
79+
enqueue=True,
80+
backtrace=True,
81+
level=level.upper(),
82+
format=format
83+
)
84+
logger.add(
85+
str(file_path),
86+
rotation=rotation,
87+
retention=retention,
88+
enqueue=True,
89+
backtrace=True,
90+
level=level.upper(),
91+
format=format
92+
)
93+
94+
return logger

app/main.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
whatsapp
1212
)
1313
from app.telegram.bot import telegram_bot
14+
from app.internal.logger_customizer import LoggerCustomizer
15+
from app import config
1416

1517

1618
def create_tables(engine, psql_environment):
@@ -29,6 +31,16 @@ def create_tables(engine, psql_environment):
2931
app.mount("/static", StaticFiles(directory=STATIC_PATH), name="static")
3032
app.mount("/media", StaticFiles(directory=MEDIA_PATH), name="media")
3133

34+
# Configure logger
35+
logger = LoggerCustomizer.make_logger(config.LOG_PATH,
36+
config.LOG_FILENAME,
37+
config.LOG_LEVEL,
38+
config.LOG_ROTATION_INTERVAL,
39+
config.LOG_RETENTION_INTERVAL,
40+
config.LOG_FORMAT)
41+
app.logger = logger
42+
43+
3244
app.include_router(profile.router)
3345
app.include_router(event.router)
3446
app.include_router(agenda.router)
@@ -43,6 +55,7 @@ def create_tables(engine, psql_environment):
4355

4456

4557
@app.get("/")
58+
@app.logger.catch()
4659
async def home(request: Request):
4760
return templates.TemplateResponse("home.html", {
4861
"request": request,

requirements.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ iniconfig==1.1.1
3939
Jinja2==2.11.2
4040
joblib==1.0.0
4141
lazy-object-proxy==1.5.2
42+
loguru==0.5.3
4243
mypy==0.790
4344
mypy-extensions==0.4.3
4445
MarkupSafe==1.1.1
@@ -86,4 +87,4 @@ watchgod==0.6
8687
websockets==8.1
8788
word-forms==2.1.0
8889
wsproto==1.0.0
89-
zipp==3.4.0
90+
zipp==3.4.0

tests/conftest.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
'tests.association_fixture',
1313
'tests.client_fixture',
1414
'tests.asyncio_fixture',
15+
'tests.logger_fixture',
1516
'smtpdfix',
1617
]
1718

tests/logger_fixture.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import logging
2+
3+
import pytest
4+
from _pytest.logging import caplog as _caplog # noqa: F401
5+
from loguru import logger
6+
7+
from app import config
8+
from app.internal.logger_customizer import LoggerCustomizer
9+
10+
11+
@pytest.fixture(scope='module')
12+
def logger_instance():
13+
_logger = LoggerCustomizer.make_logger(config.LOG_PATH,
14+
config.LOG_FILENAME,
15+
config.LOG_LEVEL,
16+
config.LOG_ROTATION_INTERVAL,
17+
config.LOG_RETENTION_INTERVAL,
18+
config.LOG_FORMAT)
19+
20+
return _logger
21+
22+
23+
@pytest.fixture
24+
def caplog(_caplog): # noqa: F811
25+
class PropagateHandler(logging.Handler):
26+
def emit(self, record):
27+
logging.getLogger(record.name).handle(record)
28+
29+
handler_id = logger.add(PropagateHandler(), format="{message} {extra}")
30+
yield _caplog
31+
logger.remove(handler_id)

tests/test_logger.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import logging
2+
3+
import pytest
4+
5+
from app.internal.logger_customizer import LoggerCustomizer, LoggerConfigError
6+
from app import config
7+
8+
9+
class TestLogger:
10+
@staticmethod
11+
def test_log_debug(caplog, logger_instance):
12+
with caplog.at_level(logging.DEBUG):
13+
logger_instance.debug('Is it debugging now?')
14+
assert 'Is it debugging now?' in caplog.text
15+
16+
@staticmethod
17+
def test_log_info(caplog, logger_instance):
18+
with caplog.at_level(logging.INFO):
19+
logger_instance.info('App started')
20+
assert 'App started' in caplog.text
21+
22+
@staticmethod
23+
def test_log_error(caplog, logger_instance):
24+
with caplog.at_level(logging.ERROR):
25+
logger_instance.error('Something bad happened!')
26+
assert 'Something bad happened!' in caplog.text
27+
28+
@staticmethod
29+
def test_log_critical(caplog, logger_instance):
30+
with caplog.at_level(logging.CRITICAL):
31+
logger_instance.critical("WE'RE DOOMED!")
32+
assert "WE'RE DOOMED!" in caplog.text
33+
34+
@staticmethod
35+
def test_bad_configuration():
36+
with pytest.raises(LoggerConfigError):
37+
LoggerCustomizer.make_logger(config.LOG_PATH,
38+
config.LOG_FILENAME,
39+
'eror',
40+
config.LOG_ROTATION_INTERVAL,
41+
config.LOG_RETENTION_INTERVAL,
42+
config.LOG_FORMAT)

0 commit comments

Comments
 (0)