blob: 2cf1af0b4571de7dda1432ecad9cab7be77e3623 [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
Pau Espin Pedrol0d8deec2017-06-23 11:43:38 +020026from datetime import datetime # we need this for strftime as the one from time doesn't carry microsecond info
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +020027from inspect import getframeinfo, stack
28
Neels Hofmeyr2694a9d2017-04-27 19:48:09 +020029from .util import is_dict
30
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +020031L_ERR = 30
32L_LOG = 20
33L_DBG = 10
34L_TRACEBACK = 'TRACEBACK'
35
Neels Hofmeyr3531a192017-03-28 14:30:28 +020036LEVEL_STRS = {
37 'err': L_ERR,
38 'log': L_LOG,
39 'dbg': L_DBG,
40 }
41
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +020042C_NET = 'net'
43C_RUN = 'run'
44C_TST = 'tst'
45C_CNF = 'cnf'
Neels Hofmeyr3531a192017-03-28 14:30:28 +020046C_BUS = 'bus'
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +020047C_DEFAULT = '---'
48
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +020049LOG_CTX_VAR = '_log_ctx_'
50
51def dbg(*messages, _origin=None, _category=None, _src=None, **named_items):
52 '''Log on debug level. See also log()'''
53 _log(messages, named_items, origin=_origin, category=_category, level=L_DBG, src=_src)
54
55def log(*messages, _origin=None, _category=None, _level=L_LOG, _src=None, **named_items):
56 '''Log a message. The origin, an Origin class instance, is normally
57 determined by stack magic, only pass _origin to override. The category is
58 taken from the origin. _src is normally an integer indicating how many
59 levels up the stack sits the interesting source file to log about, can also
60 be a string. The log message is composed of all *messages and
61 **named_items, for example:
62 log('frobnicate:', thing, key=current_key, prop=erty)
63 '''
64 _log(messages, named_items, origin=_origin, category=_category, level=_level, src=_src)
65
66def err(*messages, _origin=None, _category=None, _src=None, **named_items):
67 '''Log on error level. See also log()'''
68 _log(messages, named_items, origin=_origin, category=_category, level=L_ERR, src=_src)
69
70def _log(messages=[], named_items={}, origin=None, category=None, level=L_LOG, src=None):
71 if origin is None:
72 origin = Origin.find_on_stack()
73 if category is None and isinstance(origin, Origin):
74 category = origin._log_category
75 if src is None:
76 # two levels up
77 src = 2
78 if isinstance(src, int):
79 src = get_src_from_caller(src + 1)
80 for target in LogTarget.all_targets:
81 target.log(origin, category, level, src, messages, named_items)
82
83
Pau Espin Pedrol0d8deec2017-06-23 11:43:38 +020084LONG_DATEFMT = '%Y-%m-%d_%H:%M:%S.%f'
85DATEFMT = '%H:%M:%S.%f'
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +020086
Neels Hofmeyr3531a192017-03-28 14:30:28 +020087# may be overridden by regression tests
88get_process_id = lambda: '%d-%d' % (os.getpid(), time.time())
89
Neels Hofmeyr85eb3242017-04-09 22:01:16 +020090class Error(Exception):
Pau Espin Pedrolcf6bec72020-06-04 16:13:37 +020091 def __init__(self, *messages, origin=None, **named_items):
92 msg = ''
93 if origin is None:
94 origin = Origin.find_on_stack(f=sys._getframe(1))
95 if origin:
96 msg += origin.name() + ': '
97 msg += compose_message(messages, named_items)
98 if origin and origin._parent is not None:
99 deeper_origins = origin.ancestry_str()
100 msg += ' [%s]' % deeper_origins
101 super().__init__(msg)
Neels Hofmeyr85eb3242017-04-09 22:01:16 +0200102
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200103class LogTarget:
Neels Hofmeyrf8166882017-05-05 19:48:35 +0200104 all_targets = []
105
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200106 do_log_time = None
107 do_log_category = None
108 do_log_level = None
109 do_log_origin = None
Neels Hofmeyr9576f5f2017-05-24 18:31:01 +0200110 do_log_all_origins_on_levels = None
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200111 do_log_traceback = None
112 do_log_src = None
113 origin_width = None
114 origin_fmt = None
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200115 all_levels = None
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200116
117 # redirected by logging test
Pau Espin Pedrol0d8deec2017-06-23 11:43:38 +0200118 get_time_str = lambda self: datetime.now().strftime(self.log_time_fmt)
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200119
120 # sink that gets each complete logging line
Neels Hofmeyrf8166882017-05-05 19:48:35 +0200121 log_write_func = None
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200122
123 category_levels = None
124
Neels Hofmeyrf8166882017-05-05 19:48:35 +0200125 def __init__(self, log_write_func=None):
126 if log_write_func is None:
127 log_write_func = sys.__stdout__.write
128 self.log_write_func = log_write_func
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200129 self.category_levels = {}
130 self.style()
Neels Hofmeyrf8166882017-05-05 19:48:35 +0200131 LogTarget.all_targets.append(self)
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200132
Neels Hofmeyrfd7b9d02017-05-05 19:51:40 +0200133 def remove(self):
134 LogTarget.all_targets.remove(self)
135
Neels Hofmeyr9576f5f2017-05-24 18:31:01 +0200136 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 +0200137 '''
138 set all logging format aspects, to defaults if not passed:
139 time: log timestamps;
140 time_fmt: format of timestamps;
141 category: print the logging category (three letters);
142 level: print the logging level, unless it is L_LOG;
143 origin: print which object(s) the message originated from;
144 origin_width: fill up the origin string with whitespace to this witdh;
145 src: log the source file and line number the log comes from;
146 trace: on exceptions, log the full stack trace;
Neels Hofmeyr9576f5f2017-05-24 18:31:01 +0200147 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 +0200148 '''
149 self.log_time_fmt = time_fmt
150 self.do_log_time = bool(time)
151 if not self.log_time_fmt:
152 self.do_log_time = False
153 self.do_log_category = bool(category)
154 self.do_log_level = bool(level)
155 self.do_log_origin = bool(origin)
156 self.origin_width = int(origin_width)
157 self.origin_fmt = '{:>%ds}' % self.origin_width
158 self.do_log_src = src
159 self.do_log_traceback = trace
Neels Hofmeyr9576f5f2017-05-24 18:31:01 +0200160 self.do_log_all_origins_on_levels = tuple(all_origins_on_levels or [])
Neels Hofmeyr1a2177c2017-05-06 23:58:46 +0200161 return self
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200162
Neels Hofmeyr9576f5f2017-05-24 18:31:01 +0200163 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 +0200164 'modify only the given aspects of the logging format'
165 self.style(
166 time=(time if time is not None else self.do_log_time),
167 time_fmt=(time_fmt if time_fmt is not None else self.log_time_fmt),
168 category=(category if category is not None else self.do_log_category),
169 level=(level if level is not None else self.do_log_level),
170 origin=(origin if origin is not None else self.do_log_origin),
171 origin_width=(origin_width if origin_width is not None else self.origin_width),
172 src=(src if src is not None else self.do_log_src),
173 trace=(trace if trace is not None else self.do_log_traceback),
Neels Hofmeyr9576f5f2017-05-24 18:31:01 +0200174 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 +0200175 )
Neels Hofmeyr1a2177c2017-05-06 23:58:46 +0200176 return self
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200177
178 def set_level(self, category, level):
179 'set global logging log.L_* level for a given log.C_* category'
180 self.category_levels[category] = level
Neels Hofmeyr1a2177c2017-05-06 23:58:46 +0200181 return self
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200182
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200183 def set_all_levels(self, level):
184 self.all_levels = level
Neels Hofmeyr1a2177c2017-05-06 23:58:46 +0200185 return self
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200186
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200187 def is_enabled(self, category, level):
188 if level == L_TRACEBACK:
189 return self.do_log_traceback
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200190 if self.all_levels is not None:
191 is_level = self.all_levels
192 else:
193 is_level = self.category_levels.get(category)
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200194 if is_level is None:
195 is_level = L_LOG
196 if level < is_level:
197 return False
198 return True
199
200 def log(self, origin, category, level, src, messages, named_items):
201 if category and len(category) != 3:
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200202 self.log_write_func('WARNING: INVALID LOGGING CATEGORY %r\n' % category)
Neels Hofmeyrf8166882017-05-05 19:48:35 +0200203 self.log_write_func('origin=%r category=%r level=%r\n' % (origin, category, level));
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200204
205 if not category:
206 category = C_DEFAULT
207 if not self.is_enabled(category, level):
208 return
209
210 log_pre = []
211 if self.do_log_time:
212 log_pre.append(self.get_time_str())
213
214 if self.do_log_category:
215 log_pre.append(category)
216
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200217 deeper_origins = ''
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200218 if self.do_log_origin:
219 if origin is None:
220 name = '-'
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200221 elif isinstance(origin, Origin):
222 name = origin.name()
223 # only log ancestry when there is more than one
224 if origin._parent is not None:
225 deeper_origins = origin.ancestry_str()
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200226 elif isinstance(origin, str):
227 name = origin or None
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200228 if not name:
229 name = str(origin.__class__.__name__)
230 log_pre.append(self.origin_fmt.format(name))
231
232 if self.do_log_level and level != L_LOG:
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200233 loglevel = '%s: ' % (level_str(level) or ('loglevel=' + str(level)))
234 else:
235 loglevel = ''
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200236
Neels Hofmeyr85eb3242017-04-09 22:01:16 +0200237 log_line = [compose_message(messages, named_items)]
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200238
Neels Hofmeyr9576f5f2017-05-24 18:31:01 +0200239 if deeper_origins and (level in self.do_log_all_origins_on_levels):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200240 log_line.append(' [%s]' % deeper_origins)
241
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200242 if self.do_log_src and src:
243 log_line.append(' [%s]' % str(src))
244
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200245 log_str = '%s%s%s%s' % (' '.join(log_pre),
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200246 ': ' if log_pre else '',
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200247 loglevel,
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200248 ' '.join(log_line))
249
Neels Hofmeyr5356d0a2017-04-10 03:45:30 +0200250 if not log_str.endswith('\n'):
251 log_str = log_str + '\n'
Neels Hofmeyrf8166882017-05-05 19:48:35 +0200252 self.log_write_func(log_str)
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200253
Neels Hofmeyr6ccda112017-06-06 19:41:17 +0200254 def large_separator(self, *msgs, sublevel=1, space_above=True):
255 sublevel = max(1, min(3, sublevel))
Your Name44af3412017-04-13 03:11:59 +0200256 msg = ' '.join(msgs)
Neels Hofmeyr6ccda112017-06-06 19:41:17 +0200257 sep = '-' * int(23 * (5 - sublevel))
Your Name44af3412017-04-13 03:11:59 +0200258 if not msg:
Neels Hofmeyr6ccda112017-06-06 19:41:17 +0200259 msg = sep
260 lines = [sep, msg, sep, '']
261 if space_above:
262 lines.insert(0, '')
263 self.log_write_func('\n'.join(lines))
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200264
265def level_str(level):
266 if level == L_TRACEBACK:
267 return L_TRACEBACK
268 if level <= L_DBG:
269 return 'DBG'
270 if level <= L_LOG:
271 return 'LOG'
272 return 'ERR'
273
274def _log_all_targets(origin, category, level, src, messages, named_items=None):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200275 if origin is None:
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200276 origin = Origin.find_on_stack()
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200277 if isinstance(src, int):
278 src = get_src_from_caller(src + 1)
Neels Hofmeyrf8166882017-05-05 19:48:35 +0200279 for target in LogTarget.all_targets:
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200280 target.log(origin, category, level, src, messages, named_items)
281
Neels Hofmeyr6ccda112017-06-06 19:41:17 +0200282def large_separator(*msgs, sublevel=1, space_above=True):
Neels Hofmeyrf8166882017-05-05 19:48:35 +0200283 for target in LogTarget.all_targets:
Neels Hofmeyr6ccda112017-06-06 19:41:17 +0200284 target.large_separator(*msgs, sublevel=sublevel, space_above=space_above)
Your Name44af3412017-04-13 03:11:59 +0200285
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200286def get_src_from_caller(levels_up=1):
Holger Hans Peter Freyther257ee102018-02-12 09:07:05 +0000287 # Poke into internal to avoid hitting the linecache which will make one or
288 # more calls to stat(2).
289 frame = sys._getframe(levels_up)
290 return '%s:%d' % (os.path.basename(frame.f_code.co_filename), frame.f_lineno)
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200291
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200292def get_src_from_exc_info(exc_info=None, levels_up=1):
293 if exc_info is None:
294 exc_info = sys.exc_info()
295 ftb = traceback.extract_tb(exc_info[2])
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200296 f,l,m,c = ftb[-levels_up]
297 f = os.path.basename(f)
298 return '%s:%s: %s' % (f, l, c)
299
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200300def get_line_for_src(src_path):
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200301 '''find a given source file on the stack and return the line number for
302 that file. (Used to indicate the position in a test script.)'''
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200303 etype, exception, tb = sys.exc_info()
304 if tb:
305 ftb = traceback.extract_tb(tb)
306 for f,l,m,c in ftb:
307 if f.endswith(src_path):
308 return l
309
310 for frame in stack():
311 caller = getframeinfo(frame[0])
312 if caller.filename.endswith(src_path):
313 return caller.lineno
314 return None
315
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200316def ctx(*name_items, **detail_items):
317 '''Store log context in the current frame. This string will appear as
318 origin information for exceptions thrown within the calling scope.'''
319 if not name_items and not detail_items:
320 ctx_obj(None)
321 if not detail_items and len(name_items) == 1 and isinstance(name_items[0], Origin):
322 ctx_obj(name_items[0])
323 else:
324 ctx_obj(compose_message(name_items, detail_items))
325
326def ctx_obj(origin_or_str):
327 f = sys._getframe(2)
328 if origin_or_str is None:
329 f.f_locals.pop(LOG_CTX_VAR, None)
Pau Espin Pedrol19fe3a12020-06-04 16:11:37 +0200330 return
331 if isinstance(origin_or_str, Origin) and origin_or_str is f.f_locals.get('self'):
332 # Avoid adding log ctx in stack frame where Origin it is already "self",
333 # it is not needed and will make find_on_stack() to malfunction
334 raise Error('Don\'t use log.ctx(self), it\'s not needed!')
335 f.f_locals[LOG_CTX_VAR] = origin_or_str
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200336
337class OriginLoopError(Error):
338 pass
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200339
340class Origin:
341 '''
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200342 Base class for all classes that want to appear in the log.
343 It is a simple named marker to find in the stack frames.
344 This depends on the object instance named 'self' in each member class.
345
346 In addition, it provides a logging category and a globally unique ID for
347 each instance.
348
349 Each child class *must* call super().__init__(category, name), to allow
350 noting its parent origins.
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200351 '''
352
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200353 _global_id = None
354
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200355 _name = None
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200356 _origin_id = None
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200357 _log_category = None
358 _parent = None
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200359
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200360 @staticmethod
361 def find_on_stack(except_obj=None, f=None):
362 if f is None:
363 f = sys._getframe(2)
364 log_ctx_obj = None
365 origin = None
366 while f is not None:
367 l = f.f_locals
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200368
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200369 # if there is a log_ctx in the scope, add it, pointing to the next
370 # actual Origin class in the stack
371 log_ctx = l.get(LOG_CTX_VAR)
372 if log_ctx:
373 if isinstance(log_ctx, Origin):
374 new_log_ctx_obj = log_ctx
375 else:
376 new_log_ctx_obj = Origin(None, log_ctx, find_parent=False)
377 if log_ctx_obj is None:
378 log_ctx_obj = new_log_ctx_obj
379 else:
380 log_ctx_obj.highest_ancestor()._set_parent(new_log_ctx_obj)
381
382 obj = l.get('self')
383 if obj and isinstance(obj, Origin) and (except_obj is not obj):
384 origin = obj
385 break
386 f = f.f_back
387
388 if (origin is not None) and (log_ctx_obj is not None):
389 log_ctx_obj.highest_ancestor()._set_parent(origin)
390 p = log_ctx_obj
391 while p:
392 p._set_log_category(origin._log_category)
393 p = p._parent
394 if log_ctx_obj is not None:
395 return log_ctx_obj
396 # may return None
397 return origin
398
399 @staticmethod
400 def find_in_exc_info(exc_info):
401 tb = exc_info[2]
402 # get last tb ... I hope that's right
403 while tb.tb_next:
404 tb = tb.tb_next
405 return Origin.find_on_stack(f=tb.tb_frame)
406
407 def __init__(self, category, *name_items, find_parent=True, **detail_items):
408 self._set_log_category(category)
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200409 self.set_name(*name_items, **detail_items)
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200410 if find_parent:
411 self._set_parent(Origin.find_on_stack(except_obj=self))
412
413 def _set_parent(self, parent):
414 # make sure to avoid loops
415 p = parent
416 while p:
417 if p is self:
418 raise OriginLoopError('Origin parent loop')
419 p = p._parent
420 self._parent = parent
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200421
422 def set_name(self, *name_items, **detail_items):
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200423 '''Change the origin's name for log output; rather use the constructor.
424 This function can be used to change the name in case naming info
425 becomes available only after class creation (like a pid)'''
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200426 if name_items:
427 name = '-'.join([str(i) for i in name_items])
428 elif not detail_items:
429 name = self.__class__.__name__
430 else:
431 name = ''
432 if detail_items:
433 details = '(%s)' % (', '.join([("%s=%r" % (k,v))
434 for k,v in sorted(detail_items.items())]))
435 else:
436 details = ''
437 self._name = name + details
438
439 def name(self):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200440 return self._name or self.__class__.__name__
441
442 __str__ = name
443 __repr__ = name
444
445 def origin_id(self):
446 if not self._origin_id:
447 if not Origin._global_id:
448 Origin._global_id = get_process_id()
449 self._origin_id = '%s-%s' % (self.name(), Origin._global_id)
450 return self._origin_id
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200451
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200452 def _set_log_category(self, category):
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200453 self._log_category = category
454
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200455 def redirect_stdout(self):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200456 return contextlib.redirect_stdout(SafeRedirectStdout(self))
457
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200458 def ancestry(self):
459 origins = []
460 n = 10
461 origin = self
462 while origin:
463 origins.insert(0, origin)
464 origin = origin._parent
465 n -= 1
466 if n < 0:
467 break
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200468 return origins
469
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200470 def ancestry_str(self):
471 return '↪'.join([o.name() for o in self.ancestry()])
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200472
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200473 def highest_ancestor(self):
474 if self._parent:
475 return self._parent.highest_ancestor()
476 return self
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200477
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200478 def log(self, *messages, _src=3, **named_items):
479 '''same as log.log() but passes this object to skip looking up an origin'''
480 log(*messages, _origin=self, _src=_src, **named_items)
481
482 def dbg(self, *messages, _src=3, **named_items):
483 '''same as log.dbg() but passes this object to skip looking up an origin'''
484 dbg(*messages, _origin=self, _src=_src, **named_items)
485
486 def err(self, *messages, _src=3, **named_items):
487 '''same as log.err() but passes this object to skip looking up an origin'''
488 err(*messages, _origin=self, _src=_src, **named_items)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200489
490class SafeRedirectStdout:
491 '''
492 To be able to use 'print' in test scripts, this is used to redirect stdout
493 to a test class' log() function. However, it turns out doing that breaks
494 python debugger sessions -- it uses extended features of stdout, and will
495 fail dismally if it finds this wrapper in sys.stdout. Luckily, overriding
496 __getattr__() to return the original sys.__stdout__ attributes for anything
497 else than write() makes the debugger session work nicely again!
498 '''
499 _log_line_buf = None
500
501 def __init__(self, origin):
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200502 self.origin = origin
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200503
504 def write(self, message):
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200505 lines = message.splitlines()
506 if not lines:
507 return
508 if self._log_line_buf:
509 lines[0] = self._log_line_buf + lines[0]
510 self._log_line_buf = None
511 if not message.endswith('\n'):
512 self._log_line_buf = lines[-1]
513 lines = lines[:-1]
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200514 for line in lines:
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200515 _log(messages=(line,),
516 origin=self.origin, level=L_LOG, src=2)
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200517
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200518 def __getattr__(self, name):
519 return sys.__stdout__.__getattribute__(name)
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200520
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200521def trace(exc_info=None, origin=None):
522 if exc_info is None:
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200523 exc_info = sys.exc_info()
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200524 if origin is None:
525 origin = Origin.find_in_exc_info(exc_info)
526 _log(messages=traceback.format_exception(*exc_info),
527 origin=origin, level=L_TRACEBACK)
528
529def log_exn():
530 exc_info = sys.exc_info()
531 origin = Origin.find_in_exc_info(exc_info)
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200532
533 etype, exception, tb = exc_info
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200534 if hasattr(exception, 'msg'):
535 msg = exception.msg
536 else:
537 msg = str(exception)
538
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200539 trace(exc_info, origin=origin)
540 _log(messages=('%s:' % str(etype.__name__), msg),
541 origin=origin, level=L_ERR, src=get_src_from_exc_info(exc_info))
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200542
543
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200544def set_all_levels(level):
Neels Hofmeyrf8166882017-05-05 19:48:35 +0200545 for target in LogTarget.all_targets:
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200546 target.set_all_levels(level)
547
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200548def set_level(category, level):
Neels Hofmeyrf8166882017-05-05 19:48:35 +0200549 for target in LogTarget.all_targets:
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200550 target.set_level(category, level)
551
552def style(**kwargs):
Neels Hofmeyrf8166882017-05-05 19:48:35 +0200553 for target in LogTarget.all_targets:
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200554 target.style(**kwargs)
555
556def style_change(**kwargs):
Neels Hofmeyrf8166882017-05-05 19:48:35 +0200557 for target in LogTarget.all_targets:
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200558 target.style_change(**kwargs)
559
560class TestsTarget(LogTarget):
561 'LogTarget producing deterministic results for regression tests'
Neels Hofmeyrf8166882017-05-05 19:48:35 +0200562 def __init__(self, log_write_func=None):
563 super().__init__(log_write_func)
Neels Hofmeyr532126a2017-05-05 19:51:40 +0200564 self.style(time=False, src=False, origin_width=0)
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200565
Neels Hofmeyr8f4f1742017-05-07 00:00:14 +0200566class FileLogTarget(LogTarget):
567 'LogTarget to log to a file system path'
568 log_file = None
569
570 def __init__(self, log_path):
571 atexit.register(self.at_exit)
572 self.path = log_path
573 self.log_file = open(log_path, 'a')
574 super().__init__(self.write_to_log_and_flush)
575
576 def remove(self):
577 super().remove()
578 self.log_file.close()
579 self.log_file = None
580
581 def write_to_log_and_flush(self, msg):
582 self.log_file.write(msg)
583 self.log_file.flush()
584
585 def at_exit(self):
586 if self.log_file is not None:
587 self.log_file.flush()
588 self.log_file.close()
589
Pau Espin Pedrol5bbdab82020-02-24 18:19:10 +0100590 def log_file_path(self):
591 return self.path
592
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200593def run_logging_exceptions(func, *func_args, return_on_failure=None, **func_kwargs):
594 try:
595 return func(*func_args, **func_kwargs)
596 except:
597 log_exn()
598 return return_on_failure
599
Neels Hofmeyr2694a9d2017-04-27 19:48:09 +0200600def _compose_named_items(item):
601 'make sure dicts are output sorted, for test expectations'
602 if is_dict(item):
603 return '{%s}' % (', '.join(
604 ['%s=%s' % (k, _compose_named_items(v))
605 for k,v in sorted(item.items())]))
606 return repr(item)
607
Neels Hofmeyr85eb3242017-04-09 22:01:16 +0200608def compose_message(messages, named_items):
609 msgs = [str(m) for m in messages]
610
611 if named_items:
612 # unfortunately needs to be sorted to get deterministic results
Neels Hofmeyr2694a9d2017-04-27 19:48:09 +0200613 msgs.append(_compose_named_items(named_items))
Neels Hofmeyr85eb3242017-04-09 22:01:16 +0200614
615 return ' '.join(msgs)
616
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200617# vim: expandtab tabstop=4 shiftwidth=4