Skip to content

Commit 8ba34ba

Browse files
logging: add config for trace loggers and logging directory (#118)
* logging: add config for trace loggers and logging directory - allow configuring loggers and their specified levels Some loggers can get spammy when debugging, this allows turning them down, or setting other loggers to lower levels than the root To use, set a MODMAIL_{level}_LOGGERS environment variable, delimiting the loggers with `,` Valid levels are all of the valid levels for logging, as follows trace, debug, info, notice, warning, error, critical - add support for configuring the file logging directory The directory for logging files was fully dependent on the current working directory This caused my environment to be littered with logging directories The solution to this was to continue to use the current working directory, unless the parent directory of the bot is also a parent of the cwd, in which case the bot parent directory is used for creating the logging directory. In addition, `MODMAIL_LOGGING_DIRECTORY` has been added as an override environment variable. Setting it will use that directory for logging. * dispatcher: import ModmailLogger from the correct module * logging: add changelog entry * deps: add python-dotenv as a top level dependency * fix: skip trace logging if the log level isn't enabled * fix: fully resolve the provided logging dir
1 parent a89f5d6 commit 8ba34ba

File tree

8 files changed

+127
-23
lines changed

8 files changed

+127
-23
lines changed

docs/changelog.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2424
### Changed
2525

2626
- Embedified the meta commands so they have a nicer UI (#78)
27+
- Improved the logging system to allow trace logging and a specific logging directory to be configured. (#118)
2728

2829
## [0.2.0] - 2021-09-29
2930

modmail/__init__.py

Lines changed: 10 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,10 @@
22
import logging
33
import logging.handlers
44
import os
5-
from pathlib import Path
65

76
import coloredlogs
87

9-
from modmail.log import ModmailLogger, get_log_level_from_name
8+
from modmail import log
109

1110

1211
try:
@@ -22,24 +21,17 @@
2221
if os.name == "nt":
2322
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
2423

25-
logging.TRACE = 5
26-
logging.NOTICE = 25
27-
logging.addLevelName(logging.TRACE, "TRACE")
28-
logging.addLevelName(logging.NOTICE, "NOTICE")
29-
30-
3124
LOG_FILE_SIZE = 8 * (2**10) ** 2 # 8MB, discord upload limit
3225

33-
# this logging level is set to logging.TRACE because if it is not set to the lowest level,
34-
# the child level will be limited to the lowest level this is set to.
35-
ROOT_LOG_LEVEL = get_log_level_from_name(os.environ.get("MODMAIL_LOG_LEVEL", logging.TRACE))
26+
27+
ROOT_LOG_LEVEL = log.get_logging_level()
3628
FMT = "%(asctime)s %(levelname)10s %(name)15s - [%(lineno)5d]: %(message)s"
3729
DATEFMT = "%Y/%m/%d %H:%M:%S"
3830

39-
logging.setLoggerClass(ModmailLogger)
31+
logging.setLoggerClass(log.ModmailLogger)
4032

41-
# Set up file logging
42-
log_file = Path("logs", "bot.log")
33+
# Set up file logging relative to the current path
34+
log_file = log.get_log_dir() / "bot.log"
4335
log_file.parent.mkdir(parents=True, exist_ok=True)
4436

4537
# file handler
@@ -64,7 +56,7 @@
6456
coloredlogs.install(level=logging.TRACE, fmt=FMT, datefmt=DATEFMT)
6557

6658
# Create root logger
67-
root: ModmailLogger = logging.getLogger()
59+
root: log.ModmailLogger = logging.getLogger()
6860
root.setLevel(ROOT_LOG_LEVEL)
6961
root.addHandler(file_handler)
7062

@@ -73,3 +65,6 @@
7365
logging.getLogger("websockets").setLevel(logging.ERROR)
7466
# Set asyncio logging back to the default of INFO even if asyncio's debug mode is enabled.
7567
logging.getLogger("asyncio").setLevel(logging.INFO)
68+
69+
# set up trace loggers
70+
log.set_logger_levels()

modmail/dispatcher.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import logging
55
from typing import Callable, Coroutine, Dict, List, Optional, Tuple, Union
66

7-
from modmail import ModmailLogger
7+
from modmail.log import ModmailLogger
88
from modmail.utils.general import module_function_disidenticality
99

1010

modmail/log.py

Lines changed: 106 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,22 @@
1+
import functools
12
import logging
2-
from typing import Any, Union
3+
import pathlib
4+
from typing import Any, Dict, Union
5+
6+
7+
__all__ = [
8+
"DEFAULT",
9+
"get_logging_level",
10+
"set_logger_levels",
11+
"ModmailLogger",
12+
]
13+
14+
logging.TRACE = 5
15+
logging.NOTICE = 25
16+
logging.addLevelName(logging.TRACE, "TRACE")
17+
logging.addLevelName(logging.NOTICE, "NOTICE")
18+
19+
DEFAULT = logging.INFO
320

421

522
def get_log_level_from_name(name: Union[str, int]) -> int:
@@ -13,6 +30,94 @@ def get_log_level_from_name(name: Union[str, int]) -> int:
1330
return value
1431

1532

33+
@functools.lru_cache(maxsize=32)
34+
def _get_env() -> Dict[str, str]:
35+
import os
36+
37+
try:
38+
from dotenv import dotenv_values
39+
except ModuleNotFoundError:
40+
dotenv_values = lambda *args, **kwargs: dict() # noqa: E731
41+
42+
return {**dotenv_values(), **os.environ}
43+
44+
45+
def get_logging_level() -> None:
46+
"""Get the configured logging level, defaulting to logging.INFO."""
47+
key = "MODMAIL_LOG_LEVEL"
48+
49+
level = _get_env().get(key, DEFAULT)
50+
51+
try:
52+
level = int(level)
53+
except TypeError:
54+
level = DEFAULT
55+
except ValueError:
56+
level = level.upper()
57+
if hasattr(logging, level) and isinstance(level := getattr(logging, level), int):
58+
return level
59+
print(
60+
f"Environment variable {key} must be able to be converted into an integer.\n"
61+
f"To resolve this issue, set {key} to an integer value, or remove it from the environment.\n"
62+
"It is also possible that it is sourced from an .env file."
63+
)
64+
exit(1)
65+
66+
return level
67+
68+
69+
def set_logger_levels() -> None:
70+
"""
71+
Set all loggers to the provided environment variables.
72+
73+
eg MODMAIL_LOGGERS_TRACE will be split by `,` and each logger will be set to the trace level
74+
This is applied for every logging level.
75+
"""
76+
env_vars = _get_env()
77+
fmt_key = "MODMAIL_LOGGERS_{level}"
78+
79+
for level in ["trace", "debug", "info", "notice", "warning", "error", "critical"]:
80+
level = level.upper()
81+
key = fmt_key.format(level=level)
82+
loggers: str = env_vars.get(key, None)
83+
if loggers is None:
84+
continue
85+
86+
for logger in loggers.split(","):
87+
logging.getLogger(logger.strip()).setLevel(level)
88+
89+
90+
def get_log_dir() -> pathlib.Path:
91+
"""
92+
Return a directory to be used for logging.
93+
94+
The log directory is made in the current directory
95+
unless the current directory shares a parent directory with the bot.
96+
97+
This is ignored if a environment variable provides the logging directory.
98+
"""
99+
env_vars = _get_env()
100+
key = "MODMAIL_LOGGING_DIRECTORY"
101+
if log_dir := env_vars.get(key, None):
102+
# return the log dir if its absolute, otherwise use the root/cwd trick
103+
path = pathlib.Path(log_dir).expanduser()
104+
if path.is_absolute():
105+
return path
106+
107+
log_dir = log_dir or "logs"
108+
109+
# Get the directory above the bot module directory
110+
path = pathlib.Path(__file__).parents[1]
111+
cwd = pathlib.Path.cwd()
112+
try:
113+
cwd.relative_to(path)
114+
except ValueError:
115+
log_path = path / log_dir
116+
else:
117+
log_path = cwd / log_dir
118+
return log_path.resolve()
119+
120+
16121
class ModmailLogger(logging.Logger):
17122
"""Custom logging class implementation."""
18123

poetry.lock

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,11 @@ arrow = "^1.1.1"
1919
colorama = "^0.4.3"
2020
coloredlogs = "^15.0"
2121
"discord.py" = { url = "https://github.com/Rapptz/discord.py/archive/45d498c1b76deaf3b394d17ccf56112fa691d160.zip" }
22+
python-dotenv = "^0.19.2"
2223
atoml = "^1.0.3"
2324
attrs = "^21.2.0"
2425
desert = "^2020.11.18"
2526
marshmallow = "~=3.13.0"
26-
python-dotenv = "^0.19.0"
2727
PyYAML = { version = "^5.4.1", optional = true }
2828
typing-extensions = "^3.10.0.2"
2929
marshmallow-enum = "^1.5.1"

requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ pycares==4.1.2
2525
pycparser==2.20 ; python_version >= "2.7" and python_version != "3.0" and python_version != "3.1" and python_version != "3.2" and python_version != "3.3"
2626
pyreadline3==3.3 ; sys_platform == "win32"
2727
python-dateutil==2.8.2 ; python_version != "3.0"
28-
python-dotenv==0.19.0 ; python_version >= "3.5"
28+
python-dotenv==0.19.2 ; python_version >= "3.5"
2929
pyyaml==5.4.1 ; python_version >= "2.7" and python_version != "3.0" and python_version != "3.1" and python_version != "3.2" and python_version != "3.3" and python_version != "3.4" and python_version != "3.5"
3030
six==1.16.0 ; python_version >= "2.7" and python_version != "3.0" and python_version != "3.1" and python_version != "3.2"
3131
typing-extensions==3.10.0.2

tests/modmail/test_logs.py renamed to tests/modmail/test_log.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,9 @@ def test_notice_level(log: ModmailLogger) -> None:
4747
@pytest.mark.dependency(depends=["create_logger"])
4848
def test_trace_level(log: ModmailLogger) -> None:
4949
"""Test trace logging level prints a trace response."""
50+
if not log.isEnabledFor(logging.TRACE):
51+
pytest.skip("Skipping because logging isn't enabled for the necessary level")
52+
5053
trace_test_phrase = "Getting in the weeds"
5154
stdout = io.StringIO()
5255

0 commit comments

Comments
 (0)