-
Notifications
You must be signed in to change notification settings - Fork 46
/
log_handlers.py
149 lines (117 loc) · 5.01 KB
/
log_handlers.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
import logging
import os
import sys
from logging.handlers import RotatingFileHandler
from types import TracebackType
from typing import Any, ClassVar
import click
from click.globals import resolve_color_default
from .conf import get_app_state_dir
__all__ = [
"initialise_logging",
"color_option",
"verbose_option",
"uncaught_exception_logging_handler",
"EXTRA_EXCLUDE_FROM_CONSOLE",
"EXTRA_EXCLUDE_FROM_LOGFILE",
]
class ClickHandler(logging.Handler):
"""Handle console output with click.echo(...)
Slightly special in that this class acts as both a sink and an additional formatter,
but they're kind of intertwined for our use case of actually displaying things to the user.
"""
styles: ClassVar[dict[str, dict[str, Any]]] = {
"critical": {"fg": "red", "bold": True},
"error": {"fg": "red"},
"warning": {"fg": "yellow"},
"debug": {"fg": "cyan"},
}
def emit(self, record: logging.LogRecord) -> None:
try:
msg = self.format(record)
level = record.levelname.lower()
if level in self.styles:
# if user hasn't disabled colors/styling, just use that
if resolve_color_default() is not False:
level_style = self.styles[level]
msg = click.style(msg, **level_style)
# otherwise, prefix the level name
else:
msg = f"{level.upper()}: {msg}"
click.echo(msg)
except Exception:
self.handleError(record)
class NoExceptionFormatter(logging.Formatter):
"""Prevent automatically displaying exception/traceback info.
(without interfering with other formatters that might later want to add such information)
"""
def formatException(self, *_args: Any) -> str: # noqa: N802
return ""
def formatStack(self, *_args: Any) -> str: # noqa: N802
return ""
CONSOLE_LOG_HANDLER_NAME = "console_log_handler"
EXCLUDE_FROM_KEY = "exclude_from"
EXCLUDE_FROM_CONSOLE_VALUE = "console"
EXCLUDE_FROM_LOGFILE_VALUE = "logfile"
EXTRA_EXCLUDE_FROM_CONSOLE = {EXCLUDE_FROM_KEY: EXCLUDE_FROM_CONSOLE_VALUE}
EXTRA_EXCLUDE_FROM_LOGFILE = {EXCLUDE_FROM_KEY: EXCLUDE_FROM_LOGFILE_VALUE}
class ManualExclusionFilter(logging.Filter):
def __init__(self, exclude_value: str):
super().__init__()
self.exclude_value = exclude_value
def filter(self, record: logging.LogRecord) -> bool:
return getattr(record, EXCLUDE_FROM_KEY, None) != self.exclude_value
def initialise_logging() -> None:
console_log_handler = ClickHandler()
# default to INFO, this case be upgraded later based on -v flag
console_log_handler.setLevel(logging.INFO)
console_log_handler.name = CONSOLE_LOG_HANDLER_NAME
console_log_handler.formatter = NoExceptionFormatter()
console_log_handler.addFilter(ManualExclusionFilter(exclude_value=EXCLUDE_FROM_CONSOLE_VALUE))
file_log_handler = RotatingFileHandler(
filename=get_app_state_dir() / "cli.log",
maxBytes=1 * 1024 * 1024,
backupCount=5,
encoding="utf-8",
)
file_log_handler.setLevel(logging.DEBUG)
file_log_handler.formatter = logging.Formatter(
"%(asctime)s.%(msecs)03d %(name)s %(levelname)s %(message)s", datefmt="%Y-%m-%dT%H:%M:%S"
)
file_log_handler.addFilter(ManualExclusionFilter(exclude_value=EXCLUDE_FROM_LOGFILE_VALUE))
logging.basicConfig(level=logging.DEBUG, handlers=[console_log_handler, file_log_handler], force=True)
def uncaught_exception_logging_handler(
exc_type: type[BaseException], exc_value: BaseException, exc_traceback: TracebackType | None
) -> None:
"""Function to be used as sys.excepthook, which logs uncaught exceptions."""
if issubclass(exc_type, KeyboardInterrupt):
# don't log ctrl-c or equivalents
sys.__excepthook__(exc_type, exc_value, exc_traceback)
else:
logging.critical(f"Unhandled {exc_type.__name__}: {exc_value}", exc_info=(exc_type, exc_value, exc_traceback))
def _set_verbose(_ctx: click.Context, _param: click.Option, value: bool) -> None: # noqa: FBT001
if value:
for handler in logging.getLogger().handlers:
if handler.name == CONSOLE_LOG_HANDLER_NAME:
handler.setLevel(logging.DEBUG)
return
raise RuntimeError(f"Couldn't locate required logger named {CONSOLE_LOG_HANDLER_NAME}")
def _set_force_styles_to(ctx: click.Context, _param: click.Option, value: bool | None) -> None:
if value is not None:
ctx.color = value
verbose_option = click.option(
"--verbose",
"-v",
is_flag=True,
callback=_set_verbose,
expose_value=False,
help="Enable logging of DEBUG messages to the console.",
)
color_option = click.option(
"--color/--no-color",
# support NO_COLOR (ref: https://no-color.org) env var as default value,
default=lambda: False if os.getenv("NO_COLOR") else None,
callback=_set_force_styles_to,
expose_value=False,
help="Force enable or disable of console output styling.",
)