-
Notifications
You must be signed in to change notification settings - Fork 0
/
log_init.py
191 lines (157 loc) · 7.09 KB
/
log_init.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
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
"""
Sets up the logging for the TAS.
The default setup is to created colored logs for the console, and
generate a log to file under Logs/ folder.
The logging can be configured in config.yaml with the following params:
```yaml
logging:
color_log : True # Colors the console output. May not work on some systems.
verbosity : DEBUG # Verbosity of log messages in the console.
# Valid levels: DEBUG, INFO, WARNING, ERROR, CRITICAL
# Full log will always be available in a file.
```
"""
import datetime
import logging
import time
from logging import LogRecord
from typing import Self
def reset_logging_time_reference() -> None:
"""Reset time reference at start of run."""
logging._startTime = time.time()
# Python has a logging library that will do what we want.
# Basic documentation here: https://docs.python.org/3/howto/logging.html
# Advanced documentation here: https://docs.python.org/3/howto/logging-cookbook.html
class DeltaTimeFormatter(logging.Formatter):
"""
Create a custom formatter for the timestamp.
Adds the delta and color/color_reset properties to the record.
This will hold the time from the start of the program.
"""
white = "\x1b[0m"
blue = "\x1b[36;20m"
yellow = "\x1b[33;20m"
red = "\x1b[31;20m"
bold_red = "\x1b[31;1m"
reset = "\x1b[0m"
COLOR = {
logging.DEBUG: blue,
logging.INFO: white,
logging.WARNING: yellow,
logging.ERROR: red,
logging.CRITICAL: bold_red,
}
def format(self: Self, record: LogRecord) -> str:
"""Add colors and a timestamp to the logging output."""
# Create a timestamp we can use to parse,
# using the millisecond timestamp since start of program / 1000
duration = datetime.datetime.utcfromtimestamp(record.relativeCreated / 1000)
# Create the delta property, with the format 'HH:MM:SS.sss'
record.delta_ms = (
f"{duration.strftime('%H:%M:%S')}.{int(duration.strftime('%f')) // 1000:03d}"
)
# Latter part may be removed if we are not interested in milliseconds,
# or replaced with %f if we want microseconds.
record.delta = f"{duration.strftime('%H:%M:%S')}.{int(duration.strftime('%f')) // 1000:03d}"
record.color = self.COLOR.get(record.levelno)
record.color_reset = self.reset
return super().format(record)
def initialize_logging(config_data: dict) -> None:
"""Call once in main, before any calls to the logging library."""
# Defines the format of the colored logs
color_log_fmt = "%(color)s[%(delta)s] %(name)-16s %(levelname)-8s %(message)s%(color_reset)s"
color_log_formatter = DeltaTimeFormatter(fmt=color_log_fmt)
# Same format, but without the coloring
bw_log_fmt = "[%(delta)s] %(name)-16s %(levelname)-8s %(message)s"
bw_log_formatter = DeltaTimeFormatter(fmt=bw_log_fmt)
# Log file specific format
file_log_fmt = "[%(delta_ms)s] %(name)-16s %(levelname)-8s %(message)s"
file_log_formatter = DeltaTimeFormatter(fmt=file_log_fmt)
# Get current time in order to create log file name
time_now = datetime.datetime.now()
time_str = (
f"{time_now.year}{time_now.month:02d}{time_now.day:02d}_"
+ f"{time_now.hour:02d}_{time_now.minute:02d}_{time_now.second:02d}"
)
# Set up logging to file
logging.basicConfig(
filename=f"Logs/SeaOfStars_Log_{time_str}.txt",
filemode="w", # Log everything in the file
level=logging.DEBUG,
)
# Apply non-colored, file-specific formatter to file output
logging.getLogger("").root.handlers[0].setFormatter(file_log_formatter)
# Get the visible log level for the console logger from config.yaml
config_logging = config_data.get("logging", {})
console_log_level = config_logging.get("verbosity", "DEBUG")
color_log = config_logging.get("color_log", False)
# Set up the console logger
console = logging.StreamHandler()
console.setLevel(console_log_level) # Log the appropriate information to console
# Apply black&white formatter by default
formatter_to_use = bw_log_formatter
if color_log:
formatter_to_use = color_log_formatter # Apply color formatter
console.setFormatter(formatter_to_use)
# Add the handlers to the root logger
logging.getLogger("").addHandler(console)
# Now the logging to file/console is configured!
# Examples of using logging:
#
# import logging
#
# logging.debug('Log level: DEBUG')
# logging.info('Log level: INFO')
# logging.warning('Log level: WARNING')
# logging.error('Log level: ERROR')
# logging.critical('Log level: CRITICAL')
#
# logging.info(f"Some variable {some_var}")
# Examples with defining specific sources of log messages:
#
# import logging
#
# #setting up named logger
# logger = logging.getLogger(__name__)
#
# logger.info('In the submodule')
# Method for adding a custom log level. Adapted from:
# https://stackoverflow.com/questions/2183233/how-to-add-a-custom-loglevel-to-pythons-logging-facility/35804945#35804945
def _add_log_level(level_name: str, level_num: int, method_name: str | None = None) -> None:
"""
Add a new logging level to the `logging` module and the currently configured logging class.
`level_name` becomes an attribute of the `logging` module with the value `level_num`.
`method_name` becomes a convenience method for both `logging` itself and the class returned
by `logging.getLoggerClass()` (usually just `logging.Logger`). If `method_name` is
not specified, `level_name.lower()` is used.
To avoid accidental clobbering of existing attributes, this method will raise
an `AttributeError` if the level name is already an attribute of the `logging` module
or if the method name is already present
Example:
-------
>>> _add_log_level('TRACE', logging.DEBUG - 5)
>>> logging.getLogger(__name__).setLevel("TRACE")
>>> logging.getLogger(__name__).trace('that worked')
>>> logging.trace('so did this')
>>> logging.TRACE
5
"""
method_name = method_name or level_name.lower()
if hasattr(logging, level_name):
raise AttributeError(f"{level_name} already defined in logging module")
if hasattr(logging, method_name):
raise AttributeError(f"{method_name} already defined in logging module")
if hasattr(logging.getLoggerClass(), method_name):
raise AttributeError(f"{method_name} already defined in logger class")
# This method was inspired by the answers to Stack Overflow post
# http://stackoverflow.com/q/2183233/2988730, especially
# http://stackoverflow.com/a/13638084/2988730
def log_for_level(self: Self, message: str, *args, **kwargs) -> None:
if self.isEnabledFor(level_num):
self._log(level_num, message, args, **kwargs)
def log_to_root(message: str, *args, **kwargs) -> None:
logging.log(level_num, message, *args, **kwargs)
logging.addLevelName(level_num, level_name)
setattr(logging, level_name, level_num)
setattr(logging.getLoggerClass(), method_name, log_for_level)
setattr(logging, method_name, log_to_root)