-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathlogger.py
329 lines (247 loc) · 9.17 KB
/
logger.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
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
"""Logging facility.
It takes in many different types of input and directs them to the correct
output.
The logger has 4 major steps:
1. Inputs, such as a simple string or something more complicated like
TabularInput, are passed to the log() method of an instantiated Logger.
2. The Logger class checks for any outputs that have been added to it, and
calls the record() method of any outputs that accept the type of input.
3. The output (a subclass of LogOutput) receives the input via its record()
method and handles it in whatever way is expected.
4. (only in some cases) The dump method is used to dump the output to file.
It is necessary for some LogOutput subclasses, like TensorBoardOutput.
# Here's a demonstration of dowel:
from dowel import logger
+------+
|logger|
+------+
# Let's add an output to the logger. We want to log to the console, so we'll
# add a StdOutput.
from dowel import StdOutput
logger.add_output(StdOutput())
+------+ +---------+
|logger+------>StdOutput|
+------+ +---------+
# Great! Now we can start logging text.
logger.log('Hello dowel')
# This will go straight to the console as 'Hello dowel'
+------+ +---------+
|logger+---'Hello dowel'--->StdOutput|
+------+ +---------+
# Let's try adding another output.
from dowel import TextOutput
logger.add_output(TextOutput('log_folder/log.txt'))
+---------+
+------>StdOutput|
+------+ +---------+
|logger|
+------+ +----------+
+------>TextOutput|
+----------+
# And another output.
from dowel import CsvOutput
logger.add_output(CsvOutput('log_folder/table.csv'))
+---------+
+------>StdOutput|
| +---------+
|
+------+ +----------+
|logger+------>TextOutput|
+------+ +----------+
|
| +---------+
+------>CsvOutput|
+---------+
# The logger will record anything passed to logger.log to all outputs that
# accept its type.
logger.log('test')
+---------+
+---'test'--->StdOutput|
| +---------+
|
+------+ +----------+
|logger+---'test'--->TextOutput|
+------+ +----------+
|
| +---------+
+-----!!----->CsvOutput|
+---------+
# !! Note that the logger knows not to send CsvOutput the string 'test'
# Similarly, more complex objects like tf.tensor won't be sent to (for
# example) TextOutput.
# This behavior is defined in each output's types_accepted property
# Here's a more complex example.
# TabularInput, instantiated for you as the tabular, can log key/value pairs.
from dowel import tabular
tabular.record('key', 72)
tabular.record('foo', 'bar')
logger.log(tabular)
+---------+
+---tabular--->StdOutput|
| +---------+
|
+------+ +----------+
|logger+---tabular--->TextOutput|
+------+ +----------+
|
| +---------+
+---tabular--->CsvOutput|
+---------+
Note that LogOutputs which consume TabularInputs must call TabularInput.mark()
on each key they log. This helps the logger detect when tabular data is not
logged.
# Console Output:
--- ---
key 72
foo bar
--- ---
# Feel free to add your own inputs and outputs to the logger!
"""
import abc
import contextlib
import warnings
from dowel.utils import colorize
class LogOutput(abc.ABC):
"""Abstract class for Logger Outputs."""
@property
def types_accepted(self):
"""Pass these types to this logger output.
The types in this tuple will be accepted by this output.
:return: A tuple containing all valid input types.
"""
return ()
@abc.abstractmethod
def record(self, data, prefix=""):
"""Pass logger data to this output.
:param data: The data to be logged by the output.
:param prefix: A prefix placed before a log entry in text outputs.
"""
pass
def dump(self, step=None):
"""Dump the contents of this output.
:param step: The current run step.
"""
pass
def close(self):
"""Close any files used by the output."""
pass
def __del__(self):
"""Clean up object upon deletion."""
self.close()
class Logger:
"""This is the class that handles logging."""
def __init__(self):
self._outputs = []
self._prefixes = []
self._prefix_str = ""
self._warned_once = set()
self._disable_warnings = False
def log(self, data):
"""Magic method that takes in all different types of input.
This method is the main API for the logger. Any data to be logged goes
through this method.
Any data sent to this method is sent to all outputs that accept its
type (defined in the types_accepted property).
:param data: Data to be logged. This can be any type specified in the
types_accepted property of any of the logger outputs.
"""
if not self._outputs:
self._warn("No outputs have been added to the logger.")
at_least_one_logged = False
for output in self._outputs:
if isinstance(data, output.types_accepted):
output.record(data, prefix=self._prefix_str)
at_least_one_logged = True
if not at_least_one_logged:
warning = "Log data of type {} was not accepted by any output".format(
type(data).__name__
)
self._warn(warning)
def add_output(self, output):
"""Add a new output to the logger.
All data that is compatible with this output will be sent there.
:param output: An instantiation of a LogOutput subclass to be added.
"""
if isinstance(output, type):
msg = "Output object must be instantiated - don't pass a type."
raise ValueError(msg)
elif not isinstance(output, LogOutput):
raise ValueError("Output object must be a subclass of LogOutput")
self._outputs.append(output)
def remove_all(self):
"""Remove all outputs that have been added to this logger."""
self._outputs.clear()
def remove_output_type(self, output_type):
"""Remove all outputs of a given type.
:param output_type: A LogOutput subclass type to be removed.
"""
self._outputs = [
output for output in self._outputs if not isinstance(output, output_type)
]
def reset_output(self, output):
"""Removes, then re-adds a given output to the logger.
:param output: An instantiation of a LogOutput subclass to be added.
"""
self.remove_output_type(type(output))
self.add_output(output)
def has_output_type(self, output_type):
"""Check to see if a given logger output is attached to the logger.
:param output_type: A LogOutput subclass type to be checked for.
"""
for output in self._outputs:
if isinstance(output, output_type):
return True
return False
def dump_output_type(self, output_type, step=None):
"""Dump all outputs of the given type.
:param output_type: A LogOutput subclass type to be dumped.
:param step: The current run step.
"""
for output in self._outputs:
if isinstance(output, output_type):
output.dump(step=step)
def dump_all(self, step=None):
"""Dump all outputs connected to the logger.
:param step: The current run step.
"""
for output in self._outputs:
output.dump(step=step)
@contextlib.contextmanager
def prefix(self, prefix):
"""Add a prefix to the logger.
This allows text output to be prepended with a given stack of prefixes.
Example:
with logger.prefix('prefix: '):
logger.log('test_string') # this will have the prefix
logger.log('test_string2') # this will not have the prefix
:param prefix: The prefix string to be logged.
"""
self.push_prefix(prefix)
try:
yield
finally:
self.pop_prefix()
def push_prefix(self, prefix):
"""Add prefix to prefix stack.
:param prefix: The prefix string to be logged.
"""
self._prefixes.append(prefix)
self._prefix_str = "".join(self._prefixes)
def pop_prefix(self):
"""Pop prefix from prefix stack."""
del self._prefixes[-1]
self._prefix_str = "".join(self._prefixes)
def _warn(self, msg):
"""Warns the user using warnings.warn.
The stacklevel parameter needs to be 3 to ensure the call to logger.log
is the one printed.
"""
if not self._disable_warnings and msg not in self._warned_once:
warnings.warn(colorize(msg, "yellow"), LoggerWarning, stacklevel=3)
self._warned_once.add(msg)
return msg
def disable_warnings(self):
"""Disable logger warnings for testing."""
self._disable_warnings = True
class LoggerWarning(UserWarning):
"""Warning class for the Logger."""