blob: f098f2b8fda63a0c4547e17efbf7fb2f067ba6fb [file] [log] [blame]
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +02001# osmo_gsm_tester: global logging
2#
3# Copyright (C) 2016-2017 by sysmocom - s.f.m.c. GmbH
4#
5# Author: Neels Hofmeyr <neels@hofmeyr.de>
6#
7# This program is free software: you can redistribute it and/or modify
Harald Welte27205342017-06-03 09:51:45 +02008# it under the terms of the GNU General Public License as
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +02009# published by the Free Software Foundation, either version 3 of the
10# License, or (at your option) any later version.
11#
12# This program is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
Harald Welte27205342017-06-03 09:51:45 +020015# GNU General Public License for more details.
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +020016#
Harald Welte27205342017-06-03 09:51:45 +020017# You should have received a copy of the GNU General Public License
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +020018# along with this program. If not, see <http://www.gnu.org/licenses/>.
19
20import os
21import sys
22import time
23import traceback
24import contextlib
Neels Hofmeyr8f4f1742017-05-07 00:00:14 +020025import atexit
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +020026from inspect import getframeinfo, stack
27
Neels Hofmeyr2694a9d2017-04-27 19:48:09 +020028from .util import is_dict
29
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +020030L_ERR = 30
31L_LOG = 20
32L_DBG = 10
33L_TRACEBACK = 'TRACEBACK'
34
Neels Hofmeyr3531a192017-03-28 14:30:28 +020035LEVEL_STRS = {
36 'err': L_ERR,
37 'log': L_LOG,
38 'dbg': L_DBG,
39 }
40
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +020041C_NET = 'net'
42C_RUN = 'run'
43C_TST = 'tst'
44C_CNF = 'cnf'
Neels Hofmeyr3531a192017-03-28 14:30:28 +020045C_BUS = 'bus'
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +020046C_DEFAULT = '---'
47
48LONG_DATEFMT = '%Y-%m-%d_%H:%M:%S'
49DATEFMT = '%H:%M:%S'
50
Neels Hofmeyr3531a192017-03-28 14:30:28 +020051# may be overridden by regression tests
52get_process_id = lambda: '%d-%d' % (os.getpid(), time.time())
53
Neels Hofmeyr85eb3242017-04-09 22:01:16 +020054class Error(Exception):
55 pass
56
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +020057class LogTarget:
Neels Hofmeyrf8166882017-05-05 19:48:35 +020058 all_targets = []
59
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +020060 do_log_time = None
61 do_log_category = None
62 do_log_level = None
63 do_log_origin = None
Neels Hofmeyr9576f5f2017-05-24 18:31:01 +020064 do_log_all_origins_on_levels = None
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +020065 do_log_traceback = None
66 do_log_src = None
67 origin_width = None
68 origin_fmt = None
Neels Hofmeyr3531a192017-03-28 14:30:28 +020069 all_levels = None
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +020070
71 # redirected by logging test
72 get_time_str = lambda self: time.strftime(self.log_time_fmt)
73
74 # sink that gets each complete logging line
Neels Hofmeyrf8166882017-05-05 19:48:35 +020075 log_write_func = None
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +020076
77 category_levels = None
78
Neels Hofmeyrf8166882017-05-05 19:48:35 +020079 def __init__(self, log_write_func=None):
80 if log_write_func is None:
81 log_write_func = sys.__stdout__.write
82 self.log_write_func = log_write_func
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +020083 self.category_levels = {}
84 self.style()
Neels Hofmeyrf8166882017-05-05 19:48:35 +020085 LogTarget.all_targets.append(self)
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +020086
Neels Hofmeyrfd7b9d02017-05-05 19:51:40 +020087 def remove(self):
88 LogTarget.all_targets.remove(self)
89
Neels Hofmeyr9576f5f2017-05-24 18:31:01 +020090 def style(self, time=True, time_fmt=DATEFMT, category=True, level=True, origin=True, origin_width=32, src=True, trace=False, all_origins_on_levels=(L_ERR, L_LOG, L_DBG, L_TRACEBACK)):
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +020091 '''
92 set all logging format aspects, to defaults if not passed:
93 time: log timestamps;
94 time_fmt: format of timestamps;
95 category: print the logging category (three letters);
96 level: print the logging level, unless it is L_LOG;
97 origin: print which object(s) the message originated from;
98 origin_width: fill up the origin string with whitespace to this witdh;
99 src: log the source file and line number the log comes from;
100 trace: on exceptions, log the full stack trace;
Neels Hofmeyr9576f5f2017-05-24 18:31:01 +0200101 all_origins_on_levels: pass a tuple of logging levels that should have a full trace of origins
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200102 '''
103 self.log_time_fmt = time_fmt
104 self.do_log_time = bool(time)
105 if not self.log_time_fmt:
106 self.do_log_time = False
107 self.do_log_category = bool(category)
108 self.do_log_level = bool(level)
109 self.do_log_origin = bool(origin)
110 self.origin_width = int(origin_width)
111 self.origin_fmt = '{:>%ds}' % self.origin_width
112 self.do_log_src = src
113 self.do_log_traceback = trace
Neels Hofmeyr9576f5f2017-05-24 18:31:01 +0200114 self.do_log_all_origins_on_levels = tuple(all_origins_on_levels or [])
Neels Hofmeyr1a2177c2017-05-06 23:58:46 +0200115 return self
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200116
Neels Hofmeyr9576f5f2017-05-24 18:31:01 +0200117 def style_change(self, time=None, time_fmt=None, category=None, level=None, origin=None, origin_width=None, src=None, trace=None, all_origins_on_levels=None):
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200118 'modify only the given aspects of the logging format'
119 self.style(
120 time=(time if time is not None else self.do_log_time),
121 time_fmt=(time_fmt if time_fmt is not None else self.log_time_fmt),
122 category=(category if category is not None else self.do_log_category),
123 level=(level if level is not None else self.do_log_level),
124 origin=(origin if origin is not None else self.do_log_origin),
125 origin_width=(origin_width if origin_width is not None else self.origin_width),
126 src=(src if src is not None else self.do_log_src),
127 trace=(trace if trace is not None else self.do_log_traceback),
Neels Hofmeyr9576f5f2017-05-24 18:31:01 +0200128 all_origins_on_levels=(all_origins_on_levels if all_origins_on_levels is not None else self.do_log_all_origins_on_levels),
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200129 )
Neels Hofmeyr1a2177c2017-05-06 23:58:46 +0200130 return self
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200131
132 def set_level(self, category, level):
133 'set global logging log.L_* level for a given log.C_* category'
134 self.category_levels[category] = level
Neels Hofmeyr1a2177c2017-05-06 23:58:46 +0200135 return self
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200136
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200137 def set_all_levels(self, level):
138 self.all_levels = level
Neels Hofmeyr1a2177c2017-05-06 23:58:46 +0200139 return self
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200140
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200141 def is_enabled(self, category, level):
142 if level == L_TRACEBACK:
143 return self.do_log_traceback
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200144 if self.all_levels is not None:
145 is_level = self.all_levels
146 else:
147 is_level = self.category_levels.get(category)
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200148 if is_level is None:
149 is_level = L_LOG
150 if level < is_level:
151 return False
152 return True
153
154 def log(self, origin, category, level, src, messages, named_items):
155 if category and len(category) != 3:
Neels Hofmeyrf8166882017-05-05 19:48:35 +0200156 self.log_write_func('WARNING: INVALID LOG SUBSYSTEM %r\n' % category)
157 self.log_write_func('origin=%r category=%r level=%r\n' % (origin, category, level));
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200158
159 if not category:
160 category = C_DEFAULT
161 if not self.is_enabled(category, level):
162 return
163
164 log_pre = []
165 if self.do_log_time:
166 log_pre.append(self.get_time_str())
167
168 if self.do_log_category:
169 log_pre.append(category)
170
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200171 deeper_origins = ''
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200172 if self.do_log_origin:
173 if origin is None:
174 name = '-'
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200175 elif isinstance(origin, Origins):
176 name = origin[-1]
177 if len(origin) > 1:
178 deeper_origins = str(origin)
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200179 elif isinstance(origin, str):
180 name = origin or None
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200181 elif hasattr(origin, 'name'):
182 name = origin.name()
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200183 if not name:
184 name = str(origin.__class__.__name__)
185 log_pre.append(self.origin_fmt.format(name))
186
187 if self.do_log_level and level != L_LOG:
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200188 loglevel = '%s: ' % (level_str(level) or ('loglevel=' + str(level)))
189 else:
190 loglevel = ''
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200191
Neels Hofmeyr85eb3242017-04-09 22:01:16 +0200192 log_line = [compose_message(messages, named_items)]
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200193
Neels Hofmeyr9576f5f2017-05-24 18:31:01 +0200194 if deeper_origins and (level in self.do_log_all_origins_on_levels):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200195 log_line.append(' [%s]' % deeper_origins)
196
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200197 if self.do_log_src and src:
198 log_line.append(' [%s]' % str(src))
199
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200200 log_str = '%s%s%s%s' % (' '.join(log_pre),
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200201 ': ' if log_pre else '',
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200202 loglevel,
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200203 ' '.join(log_line))
204
Neels Hofmeyr5356d0a2017-04-10 03:45:30 +0200205 if not log_str.endswith('\n'):
206 log_str = log_str + '\n'
Neels Hofmeyrf8166882017-05-05 19:48:35 +0200207 self.log_write_func(log_str)
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200208
Neels Hofmeyr6ccda112017-06-06 19:41:17 +0200209 def large_separator(self, *msgs, sublevel=1, space_above=True):
210 sublevel = max(1, min(3, sublevel))
Your Name44af3412017-04-13 03:11:59 +0200211 msg = ' '.join(msgs)
Neels Hofmeyr6ccda112017-06-06 19:41:17 +0200212 sep = '-' * int(23 * (5 - sublevel))
Your Name44af3412017-04-13 03:11:59 +0200213 if not msg:
Neels Hofmeyr6ccda112017-06-06 19:41:17 +0200214 msg = sep
215 lines = [sep, msg, sep, '']
216 if space_above:
217 lines.insert(0, '')
218 self.log_write_func('\n'.join(lines))
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200219
220def level_str(level):
221 if level == L_TRACEBACK:
222 return L_TRACEBACK
223 if level <= L_DBG:
224 return 'DBG'
225 if level <= L_LOG:
226 return 'LOG'
227 return 'ERR'
228
229def _log_all_targets(origin, category, level, src, messages, named_items=None):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200230 if origin is None:
231 origin = Origin._global_current_origin
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200232 if isinstance(src, int):
233 src = get_src_from_caller(src + 1)
Neels Hofmeyrf8166882017-05-05 19:48:35 +0200234 for target in LogTarget.all_targets:
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200235 target.log(origin, category, level, src, messages, named_items)
236
Neels Hofmeyr6ccda112017-06-06 19:41:17 +0200237def large_separator(*msgs, sublevel=1, space_above=True):
Neels Hofmeyrf8166882017-05-05 19:48:35 +0200238 for target in LogTarget.all_targets:
Neels Hofmeyr6ccda112017-06-06 19:41:17 +0200239 target.large_separator(*msgs, sublevel=sublevel, space_above=space_above)
Your Name44af3412017-04-13 03:11:59 +0200240
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200241def get_src_from_caller(levels_up=1):
242 caller = getframeinfo(stack()[levels_up][0])
243 return '%s:%d' % (os.path.basename(caller.filename), caller.lineno)
244
245def get_src_from_tb(tb, levels_up=1):
246 ftb = traceback.extract_tb(tb)
247 f,l,m,c = ftb[-levels_up]
248 f = os.path.basename(f)
249 return '%s:%s: %s' % (f, l, c)
250
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200251def get_line_for_src(src_path):
252 etype, exception, tb = sys.exc_info()
253 if tb:
254 ftb = traceback.extract_tb(tb)
255 for f,l,m,c in ftb:
256 if f.endswith(src_path):
257 return l
258
259 for frame in stack():
260 caller = getframeinfo(frame[0])
261 if caller.filename.endswith(src_path):
262 return caller.lineno
263 return None
264
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200265
266class Origin:
267 '''
268 Base class for all classes that want to log,
269 and to add an origin string to a code path:
270 with log.Origin('my name'):
271 raise Problem()
272 This will log 'my name' as an origin for the Problem.
273 '''
274
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200275 _global_current_origin = None
276 _global_id = None
277
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200278 _log_category = None
279 _src = None
280 _name = None
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200281 _origin_id = None
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200282
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200283 _parent_origin = None
284
285 def __init__(self, *name_items, category=None, **detail_items):
286 self.set_log_category(category)
287 self.set_name(*name_items, **detail_items)
288
289 def set_name(self, *name_items, **detail_items):
290 if name_items:
291 name = '-'.join([str(i) for i in name_items])
292 elif not detail_items:
293 name = self.__class__.__name__
294 else:
295 name = ''
296 if detail_items:
297 details = '(%s)' % (', '.join([("%s=%r" % (k,v))
298 for k,v in sorted(detail_items.items())]))
299 else:
300 details = ''
301 self._name = name + details
302
303 def name(self):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200304 return self._name or self.__class__.__name__
305
306 __str__ = name
307 __repr__ = name
308
309 def origin_id(self):
310 if not self._origin_id:
311 if not Origin._global_id:
312 Origin._global_id = get_process_id()
313 self._origin_id = '%s-%s' % (self.name(), Origin._global_id)
314 return self._origin_id
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200315
316 def set_log_category(self, category):
317 self._log_category = category
318
319 def _log(self, level, messages, named_items=None, src_levels_up=3, origins=None):
320 src = self._src or src_levels_up
321 origin = origins or self.gather_origins()
322 _log_all_targets(origin, self._log_category, level, src, messages, named_items)
323
324 def dbg(self, *messages, **named_items):
325 self._log(L_DBG, messages, named_items)
326
327 def log(self, *messages, **named_items):
328 self._log(L_LOG, messages, named_items)
329
330 def err(self, *messages, **named_items):
331 self._log(L_ERR, messages, named_items)
332
Neels Hofmeyr6ccda112017-06-06 19:41:17 +0200333 def trace(self, *messages, **named_items):
334 self._log(L_TRACEBACK, messages, named_items)
335
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200336 def log_exn(self, exc_info=None):
337 log_exn(self, self._log_category, exc_info)
338
339 def __enter__(self):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200340 if not self.set_child_of(Origin._global_current_origin):
Neels Hofmeyr0cc53ef2017-05-29 01:35:00 +0200341 return self
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200342 Origin._global_current_origin = self
Neels Hofmeyr0cc53ef2017-05-29 01:35:00 +0200343 return self
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200344
345 def __exit__(self, *exc_info):
346 rc = None
347 if exc_info[0] is not None:
348 rc = exn_add_info(exc_info, self)
349 Origin._global_current_origin, self._parent_origin = self._parent_origin, None
350 return rc
351
Neels Hofmeyr85eb3242017-04-09 22:01:16 +0200352 def raise_exn(self, *messages, exn_class=Error, **named_items):
353 with self:
354 raise exn_class(compose_message(messages, named_items))
355
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200356 def redirect_stdout(self):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200357 return contextlib.redirect_stdout(SafeRedirectStdout(self))
358
359 def gather_origins(self):
360 origins = Origins()
Neels Hofmeyr31e83202017-06-06 19:44:32 +0200361 # this object shall always be seen as the immediate origin of the log message.
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200362 origins.add(self)
Neels Hofmeyr31e83202017-06-06 19:44:32 +0200363 # now go through the parents of this object.
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200364 origin = self._parent_origin
Neels Hofmeyr31e83202017-06-06 19:44:32 +0200365 # but if this object is "loose" and not set up with cascaded 'with' statements,
366 # take the last seen 'with' statement's object as next parent:
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200367 if origin is None and Origin._global_current_origin is not None:
368 origin = Origin._global_current_origin
Neels Hofmeyr31e83202017-06-06 19:44:32 +0200369 # if this object is currently the _global_current_origin, we don't
370 # need to add it twice.
371 if origin is self:
372 origin = origin._parent_origin
373 # whichever we determined to be the parent above, go up through all its
374 # ancestors.
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200375 while origin is not None:
376 origins.add(origin)
377 origin = origin._parent_origin
378 return origins
379
380 def set_child_of(self, parent_origin):
381 # avoid loops
Neels Hofmeyr6ccda112017-06-06 19:41:17 +0200382 assert self._parent_origin is None
383 assert parent_origin is not self
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200384 self._parent_origin = parent_origin
385 return True
386
387class LineInfo(Origin):
388 def __init__(self, src_file, *name_items, **detail_items):
389 self.src_file = src_file
390 self.set_name(*name_items, **detail_items)
391
392 def name(self):
393 l = get_line_for_src(self.src_file)
394 if l is not None:
395 return '%s:%s' % (self._name, l)
396 return super().name()
397
398class SafeRedirectStdout:
399 '''
400 To be able to use 'print' in test scripts, this is used to redirect stdout
401 to a test class' log() function. However, it turns out doing that breaks
402 python debugger sessions -- it uses extended features of stdout, and will
403 fail dismally if it finds this wrapper in sys.stdout. Luckily, overriding
404 __getattr__() to return the original sys.__stdout__ attributes for anything
405 else than write() makes the debugger session work nicely again!
406 '''
407 _log_line_buf = None
408
409 def __init__(self, origin):
410 self._origin = origin
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200411
412 def write(self, message):
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200413 lines = message.splitlines()
414 if not lines:
415 return
416 if self._log_line_buf:
417 lines[0] = self._log_line_buf + lines[0]
418 self._log_line_buf = None
419 if not message.endswith('\n'):
420 self._log_line_buf = lines[-1]
421 lines = lines[:-1]
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200422 origins = self._origin.gather_origins()
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200423 for line in lines:
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200424 self._origin._log(L_LOG, (line,), origins=origins)
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200425
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200426 def __getattr__(self, name):
427 return sys.__stdout__.__getattribute__(name)
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200428
429
430def dbg(origin, category, *messages, **named_items):
431 _log_all_targets(origin, category, L_DBG, 2, messages, named_items)
432
433def log(origin, category, *messages, **named_items):
434 _log_all_targets(origin, category, L_LOG, 2, messages, named_items)
435
436def err(origin, category, *messages, **named_items):
437 _log_all_targets(origin, category, L_ERR, 2, messages, named_items)
438
439def trace(origin, category, exc_info):
440 _log_all_targets(origin, category, L_TRACEBACK, None,
441 traceback.format_exception(*exc_info))
442
443def resolve_category(origin, category):
444 if category is not None:
445 return category
446 if not hasattr(origin, '_log_category'):
447 return None
448 return origin._log_category
449
450def exn_add_info(exc_info, origin, category=None):
451 etype, exception, tb = exc_info
452 if not hasattr(exception, 'origins'):
453 exception.origins = Origins()
454 if not hasattr(exception, 'category'):
455 # only remember the deepest category
456 exception.category = resolve_category(origin, category)
457 if not hasattr(exception, 'src'):
458 exception.src = get_src_from_tb(tb)
459 exception.origins.add(origin)
460 return False
461
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200462def log_exn(origin=None, category=None, exc_info=None):
463 if not (exc_info is not None and len(exc_info) == 3):
464 exc_info = sys.exc_info()
465 if not (exc_info is not None and len(exc_info) == 3):
466 raise RuntimeError('invalid call to log_exn() -- no valid exception info')
467
468 etype, exception, tb = exc_info
469
470 # if there are origins recorded with the Exception, prefer that
471 if hasattr(exception, 'origins'):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200472 origin = exception.origins
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200473
474 # if there is a category recorded with the Exception, prefer that
475 if hasattr(exception, 'category'):
476 category = exception.category
477
478 if hasattr(exception, 'msg'):
479 msg = exception.msg
480 else:
481 msg = str(exception)
482
483 if hasattr(exception, 'src'):
484 src = exception.src
485 else:
486 src = 2
487
488 trace(origin, category, exc_info)
489 _log_all_targets(origin, category, L_ERR, src,
490 ('%s:' % str(etype.__name__), msg))
491
492
493class Origins(list):
494 def __init__(self, origin=None):
495 if origin is not None:
496 self.add(origin)
Neels Hofmeyrd3a33e32017-06-06 19:45:16 +0200497
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200498 def add(self, origin):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200499 if hasattr(origin, 'name'):
500 origin_str = origin.name()
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200501 else:
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200502 origin_str = repr(origin)
503 if origin_str is None:
504 raise RuntimeError('origin_str is None for %r' % origin)
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200505 self.insert(0, origin_str)
Neels Hofmeyrd3a33e32017-06-06 19:45:16 +0200506
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200507 def __str__(self):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200508 return '↪'.join(self)
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200509
510
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200511def set_all_levels(level):
Neels Hofmeyrf8166882017-05-05 19:48:35 +0200512 for target in LogTarget.all_targets:
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200513 target.set_all_levels(level)
514
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200515def set_level(category, level):
Neels Hofmeyrf8166882017-05-05 19:48:35 +0200516 for target in LogTarget.all_targets:
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200517 target.set_level(category, level)
518
519def style(**kwargs):
Neels Hofmeyrf8166882017-05-05 19:48:35 +0200520 for target in LogTarget.all_targets:
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200521 target.style(**kwargs)
522
523def style_change(**kwargs):
Neels Hofmeyrf8166882017-05-05 19:48:35 +0200524 for target in LogTarget.all_targets:
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200525 target.style_change(**kwargs)
526
527class TestsTarget(LogTarget):
528 'LogTarget producing deterministic results for regression tests'
Neels Hofmeyrf8166882017-05-05 19:48:35 +0200529 def __init__(self, log_write_func=None):
530 super().__init__(log_write_func)
Neels Hofmeyr532126a2017-05-05 19:51:40 +0200531 self.style(time=False, src=False, origin_width=0)
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200532
Neels Hofmeyr8f4f1742017-05-07 00:00:14 +0200533class FileLogTarget(LogTarget):
534 'LogTarget to log to a file system path'
535 log_file = None
536
537 def __init__(self, log_path):
538 atexit.register(self.at_exit)
539 self.path = log_path
540 self.log_file = open(log_path, 'a')
541 super().__init__(self.write_to_log_and_flush)
542
543 def remove(self):
544 super().remove()
545 self.log_file.close()
546 self.log_file = None
547
548 def write_to_log_and_flush(self, msg):
549 self.log_file.write(msg)
550 self.log_file.flush()
551
552 def at_exit(self):
553 if self.log_file is not None:
554 self.log_file.flush()
555 self.log_file.close()
556
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200557def run_logging_exceptions(func, *func_args, return_on_failure=None, **func_kwargs):
558 try:
559 return func(*func_args, **func_kwargs)
560 except:
561 log_exn()
562 return return_on_failure
563
Neels Hofmeyr2694a9d2017-04-27 19:48:09 +0200564def _compose_named_items(item):
565 'make sure dicts are output sorted, for test expectations'
566 if is_dict(item):
567 return '{%s}' % (', '.join(
568 ['%s=%s' % (k, _compose_named_items(v))
569 for k,v in sorted(item.items())]))
570 return repr(item)
571
Neels Hofmeyr85eb3242017-04-09 22:01:16 +0200572def compose_message(messages, named_items):
573 msgs = [str(m) for m in messages]
574
575 if named_items:
576 # unfortunately needs to be sorted to get deterministic results
Neels Hofmeyr2694a9d2017-04-27 19:48:09 +0200577 msgs.append(_compose_named_items(named_items))
Neels Hofmeyr85eb3242017-04-09 22:01:16 +0200578
579 return ' '.join(msgs)
580
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200581# vim: expandtab tabstop=4 shiftwidth=4