blob: c4a66295aa8a8289f6a361e5ea6c527e07776d57 [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
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +020048LOG_CTX_VAR = '_log_ctx_'
49
50def dbg(*messages, _origin=None, _category=None, _src=None, **named_items):
51 '''Log on debug level. See also log()'''
52 _log(messages, named_items, origin=_origin, category=_category, level=L_DBG, src=_src)
53
54def log(*messages, _origin=None, _category=None, _level=L_LOG, _src=None, **named_items):
55 '''Log a message. The origin, an Origin class instance, is normally
56 determined by stack magic, only pass _origin to override. The category is
57 taken from the origin. _src is normally an integer indicating how many
58 levels up the stack sits the interesting source file to log about, can also
59 be a string. The log message is composed of all *messages and
60 **named_items, for example:
61 log('frobnicate:', thing, key=current_key, prop=erty)
62 '''
63 _log(messages, named_items, origin=_origin, category=_category, level=_level, src=_src)
64
65def err(*messages, _origin=None, _category=None, _src=None, **named_items):
66 '''Log on error level. See also log()'''
67 _log(messages, named_items, origin=_origin, category=_category, level=L_ERR, src=_src)
68
69def _log(messages=[], named_items={}, origin=None, category=None, level=L_LOG, src=None):
70 if origin is None:
71 origin = Origin.find_on_stack()
72 if category is None and isinstance(origin, Origin):
73 category = origin._log_category
74 if src is None:
75 # two levels up
76 src = 2
77 if isinstance(src, int):
78 src = get_src_from_caller(src + 1)
79 for target in LogTarget.all_targets:
80 target.log(origin, category, level, src, messages, named_items)
81
82
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +020083LONG_DATEFMT = '%Y-%m-%d_%H:%M:%S'
84DATEFMT = '%H:%M:%S'
85
Neels Hofmeyr3531a192017-03-28 14:30:28 +020086# may be overridden by regression tests
87get_process_id = lambda: '%d-%d' % (os.getpid(), time.time())
88
Neels Hofmeyr85eb3242017-04-09 22:01:16 +020089class Error(Exception):
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +020090 def __init__(self, *messages, **named_items):
91 super().__init__(compose_message(messages, named_items))
Neels Hofmeyr85eb3242017-04-09 22:01:16 +020092
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +020093class LogTarget:
Neels Hofmeyrf8166882017-05-05 19:48:35 +020094 all_targets = []
95
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +020096 do_log_time = None
97 do_log_category = None
98 do_log_level = None
99 do_log_origin = None
Neels Hofmeyr9576f5f2017-05-24 18:31:01 +0200100 do_log_all_origins_on_levels = None
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200101 do_log_traceback = None
102 do_log_src = None
103 origin_width = None
104 origin_fmt = None
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200105 all_levels = None
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200106
107 # redirected by logging test
108 get_time_str = lambda self: time.strftime(self.log_time_fmt)
109
110 # sink that gets each complete logging line
Neels Hofmeyrf8166882017-05-05 19:48:35 +0200111 log_write_func = None
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200112
113 category_levels = None
114
Neels Hofmeyrf8166882017-05-05 19:48:35 +0200115 def __init__(self, log_write_func=None):
116 if log_write_func is None:
117 log_write_func = sys.__stdout__.write
118 self.log_write_func = log_write_func
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200119 self.category_levels = {}
120 self.style()
Neels Hofmeyrf8166882017-05-05 19:48:35 +0200121 LogTarget.all_targets.append(self)
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200122
Neels Hofmeyrfd7b9d02017-05-05 19:51:40 +0200123 def remove(self):
124 LogTarget.all_targets.remove(self)
125
Neels Hofmeyr9576f5f2017-05-24 18:31:01 +0200126 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 +0200127 '''
128 set all logging format aspects, to defaults if not passed:
129 time: log timestamps;
130 time_fmt: format of timestamps;
131 category: print the logging category (three letters);
132 level: print the logging level, unless it is L_LOG;
133 origin: print which object(s) the message originated from;
134 origin_width: fill up the origin string with whitespace to this witdh;
135 src: log the source file and line number the log comes from;
136 trace: on exceptions, log the full stack trace;
Neels Hofmeyr9576f5f2017-05-24 18:31:01 +0200137 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 +0200138 '''
139 self.log_time_fmt = time_fmt
140 self.do_log_time = bool(time)
141 if not self.log_time_fmt:
142 self.do_log_time = False
143 self.do_log_category = bool(category)
144 self.do_log_level = bool(level)
145 self.do_log_origin = bool(origin)
146 self.origin_width = int(origin_width)
147 self.origin_fmt = '{:>%ds}' % self.origin_width
148 self.do_log_src = src
149 self.do_log_traceback = trace
Neels Hofmeyr9576f5f2017-05-24 18:31:01 +0200150 self.do_log_all_origins_on_levels = tuple(all_origins_on_levels or [])
Neels Hofmeyr1a2177c2017-05-06 23:58:46 +0200151 return self
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200152
Neels Hofmeyr9576f5f2017-05-24 18:31:01 +0200153 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 +0200154 'modify only the given aspects of the logging format'
155 self.style(
156 time=(time if time is not None else self.do_log_time),
157 time_fmt=(time_fmt if time_fmt is not None else self.log_time_fmt),
158 category=(category if category is not None else self.do_log_category),
159 level=(level if level is not None else self.do_log_level),
160 origin=(origin if origin is not None else self.do_log_origin),
161 origin_width=(origin_width if origin_width is not None else self.origin_width),
162 src=(src if src is not None else self.do_log_src),
163 trace=(trace if trace is not None else self.do_log_traceback),
Neels Hofmeyr9576f5f2017-05-24 18:31:01 +0200164 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 +0200165 )
Neels Hofmeyr1a2177c2017-05-06 23:58:46 +0200166 return self
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200167
168 def set_level(self, category, level):
169 'set global logging log.L_* level for a given log.C_* category'
170 self.category_levels[category] = level
Neels Hofmeyr1a2177c2017-05-06 23:58:46 +0200171 return self
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200172
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200173 def set_all_levels(self, level):
174 self.all_levels = level
Neels Hofmeyr1a2177c2017-05-06 23:58:46 +0200175 return self
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200176
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200177 def is_enabled(self, category, level):
178 if level == L_TRACEBACK:
179 return self.do_log_traceback
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200180 if self.all_levels is not None:
181 is_level = self.all_levels
182 else:
183 is_level = self.category_levels.get(category)
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200184 if is_level is None:
185 is_level = L_LOG
186 if level < is_level:
187 return False
188 return True
189
190 def log(self, origin, category, level, src, messages, named_items):
191 if category and len(category) != 3:
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200192 self.log_write_func('WARNING: INVALID LOGGING CATEGORY %r\n' % category)
Neels Hofmeyrf8166882017-05-05 19:48:35 +0200193 self.log_write_func('origin=%r category=%r level=%r\n' % (origin, category, level));
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200194
195 if not category:
196 category = C_DEFAULT
197 if not self.is_enabled(category, level):
198 return
199
200 log_pre = []
201 if self.do_log_time:
202 log_pre.append(self.get_time_str())
203
204 if self.do_log_category:
205 log_pre.append(category)
206
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200207 deeper_origins = ''
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200208 if self.do_log_origin:
209 if origin is None:
210 name = '-'
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200211 elif isinstance(origin, Origin):
212 name = origin.name()
213 # only log ancestry when there is more than one
214 if origin._parent is not None:
215 deeper_origins = origin.ancestry_str()
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200216 elif isinstance(origin, str):
217 name = origin or None
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200218 if not name:
219 name = str(origin.__class__.__name__)
220 log_pre.append(self.origin_fmt.format(name))
221
222 if self.do_log_level and level != L_LOG:
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200223 loglevel = '%s: ' % (level_str(level) or ('loglevel=' + str(level)))
224 else:
225 loglevel = ''
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200226
Neels Hofmeyr85eb3242017-04-09 22:01:16 +0200227 log_line = [compose_message(messages, named_items)]
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200228
Neels Hofmeyr9576f5f2017-05-24 18:31:01 +0200229 if deeper_origins and (level in self.do_log_all_origins_on_levels):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200230 log_line.append(' [%s]' % deeper_origins)
231
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200232 if self.do_log_src and src:
233 log_line.append(' [%s]' % str(src))
234
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200235 log_str = '%s%s%s%s' % (' '.join(log_pre),
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200236 ': ' if log_pre else '',
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200237 loglevel,
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200238 ' '.join(log_line))
239
Neels Hofmeyr5356d0a2017-04-10 03:45:30 +0200240 if not log_str.endswith('\n'):
241 log_str = log_str + '\n'
Neels Hofmeyrf8166882017-05-05 19:48:35 +0200242 self.log_write_func(log_str)
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200243
Neels Hofmeyr6ccda112017-06-06 19:41:17 +0200244 def large_separator(self, *msgs, sublevel=1, space_above=True):
245 sublevel = max(1, min(3, sublevel))
Your Name44af3412017-04-13 03:11:59 +0200246 msg = ' '.join(msgs)
Neels Hofmeyr6ccda112017-06-06 19:41:17 +0200247 sep = '-' * int(23 * (5 - sublevel))
Your Name44af3412017-04-13 03:11:59 +0200248 if not msg:
Neels Hofmeyr6ccda112017-06-06 19:41:17 +0200249 msg = sep
250 lines = [sep, msg, sep, '']
251 if space_above:
252 lines.insert(0, '')
253 self.log_write_func('\n'.join(lines))
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200254
255def level_str(level):
256 if level == L_TRACEBACK:
257 return L_TRACEBACK
258 if level <= L_DBG:
259 return 'DBG'
260 if level <= L_LOG:
261 return 'LOG'
262 return 'ERR'
263
264def _log_all_targets(origin, category, level, src, messages, named_items=None):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200265 if origin is None:
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200266 origin = Origin.find_on_stack()
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200267 if isinstance(src, int):
268 src = get_src_from_caller(src + 1)
Neels Hofmeyrf8166882017-05-05 19:48:35 +0200269 for target in LogTarget.all_targets:
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200270 target.log(origin, category, level, src, messages, named_items)
271
Neels Hofmeyr6ccda112017-06-06 19:41:17 +0200272def large_separator(*msgs, sublevel=1, space_above=True):
Neels Hofmeyrf8166882017-05-05 19:48:35 +0200273 for target in LogTarget.all_targets:
Neels Hofmeyr6ccda112017-06-06 19:41:17 +0200274 target.large_separator(*msgs, sublevel=sublevel, space_above=space_above)
Your Name44af3412017-04-13 03:11:59 +0200275
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200276def get_src_from_caller(levels_up=1):
277 caller = getframeinfo(stack()[levels_up][0])
278 return '%s:%d' % (os.path.basename(caller.filename), caller.lineno)
279
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200280def get_src_from_exc_info(exc_info=None, levels_up=1):
281 if exc_info is None:
282 exc_info = sys.exc_info()
283 ftb = traceback.extract_tb(exc_info[2])
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200284 f,l,m,c = ftb[-levels_up]
285 f = os.path.basename(f)
286 return '%s:%s: %s' % (f, l, c)
287
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200288def get_line_for_src(src_path):
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200289 '''find a given source file on the stack and return the line number for
290 that file. (Used to indicate the position in a test script.)'''
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200291 etype, exception, tb = sys.exc_info()
292 if tb:
293 ftb = traceback.extract_tb(tb)
294 for f,l,m,c in ftb:
295 if f.endswith(src_path):
296 return l
297
298 for frame in stack():
299 caller = getframeinfo(frame[0])
300 if caller.filename.endswith(src_path):
301 return caller.lineno
302 return None
303
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200304def ctx(*name_items, **detail_items):
305 '''Store log context in the current frame. This string will appear as
306 origin information for exceptions thrown within the calling scope.'''
307 if not name_items and not detail_items:
308 ctx_obj(None)
309 if not detail_items and len(name_items) == 1 and isinstance(name_items[0], Origin):
310 ctx_obj(name_items[0])
311 else:
312 ctx_obj(compose_message(name_items, detail_items))
313
314def ctx_obj(origin_or_str):
315 f = sys._getframe(2)
316 if origin_or_str is None:
317 f.f_locals.pop(LOG_CTX_VAR, None)
318 else:
319 f.f_locals[LOG_CTX_VAR] = origin_or_str
320
321class OriginLoopError(Error):
322 pass
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200323
324class Origin:
325 '''
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200326 Base class for all classes that want to appear in the log.
327 It is a simple named marker to find in the stack frames.
328 This depends on the object instance named 'self' in each member class.
329
330 In addition, it provides a logging category and a globally unique ID for
331 each instance.
332
333 Each child class *must* call super().__init__(category, name), to allow
334 noting its parent origins.
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200335 '''
336
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200337 _global_id = None
338
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200339 _name = None
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200340 _origin_id = None
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200341 _log_category = None
342 _parent = None
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200343
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200344 @staticmethod
345 def find_on_stack(except_obj=None, f=None):
346 if f is None:
347 f = sys._getframe(2)
348 log_ctx_obj = None
349 origin = None
350 while f is not None:
351 l = f.f_locals
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200352
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200353 # if there is a log_ctx in the scope, add it, pointing to the next
354 # actual Origin class in the stack
355 log_ctx = l.get(LOG_CTX_VAR)
356 if log_ctx:
357 if isinstance(log_ctx, Origin):
358 new_log_ctx_obj = log_ctx
359 else:
360 new_log_ctx_obj = Origin(None, log_ctx, find_parent=False)
361 if log_ctx_obj is None:
362 log_ctx_obj = new_log_ctx_obj
363 else:
364 log_ctx_obj.highest_ancestor()._set_parent(new_log_ctx_obj)
365
366 obj = l.get('self')
367 if obj and isinstance(obj, Origin) and (except_obj is not obj):
368 origin = obj
369 break
370 f = f.f_back
371
372 if (origin is not None) and (log_ctx_obj is not None):
373 log_ctx_obj.highest_ancestor()._set_parent(origin)
374 p = log_ctx_obj
375 while p:
376 p._set_log_category(origin._log_category)
377 p = p._parent
378 if log_ctx_obj is not None:
379 return log_ctx_obj
380 # may return None
381 return origin
382
383 @staticmethod
384 def find_in_exc_info(exc_info):
385 tb = exc_info[2]
386 # get last tb ... I hope that's right
387 while tb.tb_next:
388 tb = tb.tb_next
389 return Origin.find_on_stack(f=tb.tb_frame)
390
391 def __init__(self, category, *name_items, find_parent=True, **detail_items):
392 self._set_log_category(category)
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200393 self.set_name(*name_items, **detail_items)
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200394 if find_parent:
395 self._set_parent(Origin.find_on_stack(except_obj=self))
396
397 def _set_parent(self, parent):
398 # make sure to avoid loops
399 p = parent
400 while p:
401 if p is self:
402 raise OriginLoopError('Origin parent loop')
403 p = p._parent
404 self._parent = parent
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200405
406 def set_name(self, *name_items, **detail_items):
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200407 '''Change the origin's name for log output; rather use the constructor.
408 This function can be used to change the name in case naming info
409 becomes available only after class creation (like a pid)'''
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200410 if name_items:
411 name = '-'.join([str(i) for i in name_items])
412 elif not detail_items:
413 name = self.__class__.__name__
414 else:
415 name = ''
416 if detail_items:
417 details = '(%s)' % (', '.join([("%s=%r" % (k,v))
418 for k,v in sorted(detail_items.items())]))
419 else:
420 details = ''
421 self._name = name + details
422
423 def name(self):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200424 return self._name or self.__class__.__name__
425
426 __str__ = name
427 __repr__ = name
428
429 def origin_id(self):
430 if not self._origin_id:
431 if not Origin._global_id:
432 Origin._global_id = get_process_id()
433 self._origin_id = '%s-%s' % (self.name(), Origin._global_id)
434 return self._origin_id
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200435
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200436 def _set_log_category(self, category):
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200437 self._log_category = category
438
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200439 def redirect_stdout(self):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200440 return contextlib.redirect_stdout(SafeRedirectStdout(self))
441
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200442 def ancestry(self):
443 origins = []
444 n = 10
445 origin = self
446 while origin:
447 origins.insert(0, origin)
448 origin = origin._parent
449 n -= 1
450 if n < 0:
451 break
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200452 return origins
453
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200454 def ancestry_str(self):
455 return '↪'.join([o.name() for o in self.ancestry()])
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200456
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200457 def highest_ancestor(self):
458 if self._parent:
459 return self._parent.highest_ancestor()
460 return self
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200461
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200462 def log(self, *messages, _src=3, **named_items):
463 '''same as log.log() but passes this object to skip looking up an origin'''
464 log(*messages, _origin=self, _src=_src, **named_items)
465
466 def dbg(self, *messages, _src=3, **named_items):
467 '''same as log.dbg() but passes this object to skip looking up an origin'''
468 dbg(*messages, _origin=self, _src=_src, **named_items)
469
470 def err(self, *messages, _src=3, **named_items):
471 '''same as log.err() but passes this object to skip looking up an origin'''
472 err(*messages, _origin=self, _src=_src, **named_items)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200473
474class SafeRedirectStdout:
475 '''
476 To be able to use 'print' in test scripts, this is used to redirect stdout
477 to a test class' log() function. However, it turns out doing that breaks
478 python debugger sessions -- it uses extended features of stdout, and will
479 fail dismally if it finds this wrapper in sys.stdout. Luckily, overriding
480 __getattr__() to return the original sys.__stdout__ attributes for anything
481 else than write() makes the debugger session work nicely again!
482 '''
483 _log_line_buf = None
484
485 def __init__(self, origin):
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200486 self.origin = origin
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200487
488 def write(self, message):
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200489 lines = message.splitlines()
490 if not lines:
491 return
492 if self._log_line_buf:
493 lines[0] = self._log_line_buf + lines[0]
494 self._log_line_buf = None
495 if not message.endswith('\n'):
496 self._log_line_buf = lines[-1]
497 lines = lines[:-1]
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200498 for line in lines:
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200499 _log(messages=(line,),
500 origin=self.origin, level=L_LOG, src=2)
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200501
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200502 def __getattr__(self, name):
503 return sys.__stdout__.__getattribute__(name)
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200504
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200505def trace(exc_info=None, origin=None):
506 if exc_info is None:
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200507 exc_info = sys.exc_info()
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200508 if origin is None:
509 origin = Origin.find_in_exc_info(exc_info)
510 _log(messages=traceback.format_exception(*exc_info),
511 origin=origin, level=L_TRACEBACK)
512
513def log_exn():
514 exc_info = sys.exc_info()
515 origin = Origin.find_in_exc_info(exc_info)
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200516
517 etype, exception, tb = exc_info
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200518 if hasattr(exception, 'msg'):
519 msg = exception.msg
520 else:
521 msg = str(exception)
522
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200523 trace(exc_info, origin=origin)
524 _log(messages=('%s:' % str(etype.__name__), msg),
525 origin=origin, level=L_ERR, src=get_src_from_exc_info(exc_info))
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200526
527
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200528def set_all_levels(level):
Neels Hofmeyrf8166882017-05-05 19:48:35 +0200529 for target in LogTarget.all_targets:
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200530 target.set_all_levels(level)
531
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200532def set_level(category, level):
Neels Hofmeyrf8166882017-05-05 19:48:35 +0200533 for target in LogTarget.all_targets:
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200534 target.set_level(category, level)
535
536def style(**kwargs):
Neels Hofmeyrf8166882017-05-05 19:48:35 +0200537 for target in LogTarget.all_targets:
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200538 target.style(**kwargs)
539
540def style_change(**kwargs):
Neels Hofmeyrf8166882017-05-05 19:48:35 +0200541 for target in LogTarget.all_targets:
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200542 target.style_change(**kwargs)
543
544class TestsTarget(LogTarget):
545 'LogTarget producing deterministic results for regression tests'
Neels Hofmeyrf8166882017-05-05 19:48:35 +0200546 def __init__(self, log_write_func=None):
547 super().__init__(log_write_func)
Neels Hofmeyr532126a2017-05-05 19:51:40 +0200548 self.style(time=False, src=False, origin_width=0)
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200549
Neels Hofmeyr8f4f1742017-05-07 00:00:14 +0200550class FileLogTarget(LogTarget):
551 'LogTarget to log to a file system path'
552 log_file = None
553
554 def __init__(self, log_path):
555 atexit.register(self.at_exit)
556 self.path = log_path
557 self.log_file = open(log_path, 'a')
558 super().__init__(self.write_to_log_and_flush)
559
560 def remove(self):
561 super().remove()
562 self.log_file.close()
563 self.log_file = None
564
565 def write_to_log_and_flush(self, msg):
566 self.log_file.write(msg)
567 self.log_file.flush()
568
569 def at_exit(self):
570 if self.log_file is not None:
571 self.log_file.flush()
572 self.log_file.close()
573
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200574def run_logging_exceptions(func, *func_args, return_on_failure=None, **func_kwargs):
575 try:
576 return func(*func_args, **func_kwargs)
577 except:
578 log_exn()
579 return return_on_failure
580
Neels Hofmeyr2694a9d2017-04-27 19:48:09 +0200581def _compose_named_items(item):
582 'make sure dicts are output sorted, for test expectations'
583 if is_dict(item):
584 return '{%s}' % (', '.join(
585 ['%s=%s' % (k, _compose_named_items(v))
586 for k,v in sorted(item.items())]))
587 return repr(item)
588
Neels Hofmeyr85eb3242017-04-09 22:01:16 +0200589def compose_message(messages, named_items):
590 msgs = [str(m) for m in messages]
591
592 if named_items:
593 # unfortunately needs to be sorted to get deterministic results
Neels Hofmeyr2694a9d2017-04-27 19:48:09 +0200594 msgs.append(_compose_named_items(named_items))
Neels Hofmeyr85eb3242017-04-09 22:01:16 +0200595
596 return ' '.join(msgs)
597
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200598# vim: expandtab tabstop=4 shiftwidth=4