blob: a573f051125ce046553d3af43534be1ca1ad2312 [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
Neels Hofmeyr8f4f1742017-05-07 00:00:14 +020024import atexit
Pau Espin Pedrol0d8deec2017-06-23 11:43:38 +020025from 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 +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
Pau Espin Pedrolec285722020-06-11 17:57:43 +020048FILE_LOG = 'log'
49FILE_LOG_BRIEF = 'log_brief'
50
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +020051LOG_CTX_VAR = '_log_ctx_'
52
53def dbg(*messages, _origin=None, _category=None, _src=None, **named_items):
54 '''Log on debug level. See also log()'''
55 _log(messages, named_items, origin=_origin, category=_category, level=L_DBG, src=_src)
56
57def log(*messages, _origin=None, _category=None, _level=L_LOG, _src=None, **named_items):
58 '''Log a message. The origin, an Origin class instance, is normally
59 determined by stack magic, only pass _origin to override. The category is
60 taken from the origin. _src is normally an integer indicating how many
61 levels up the stack sits the interesting source file to log about, can also
62 be a string. The log message is composed of all *messages and
63 **named_items, for example:
64 log('frobnicate:', thing, key=current_key, prop=erty)
65 '''
66 _log(messages, named_items, origin=_origin, category=_category, level=_level, src=_src)
67
68def err(*messages, _origin=None, _category=None, _src=None, **named_items):
69 '''Log on error level. See also log()'''
70 _log(messages, named_items, origin=_origin, category=_category, level=L_ERR, src=_src)
71
72def _log(messages=[], named_items={}, origin=None, category=None, level=L_LOG, src=None):
73 if origin is None:
74 origin = Origin.find_on_stack()
75 if category is None and isinstance(origin, Origin):
76 category = origin._log_category
77 if src is None:
78 # two levels up
79 src = 2
80 if isinstance(src, int):
81 src = get_src_from_caller(src + 1)
82 for target in LogTarget.all_targets:
83 target.log(origin, category, level, src, messages, named_items)
84
85
Pau Espin Pedrol0d8deec2017-06-23 11:43:38 +020086LONG_DATEFMT = '%Y-%m-%d_%H:%M:%S.%f'
87DATEFMT = '%H:%M:%S.%f'
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +020088
Neels Hofmeyr3531a192017-03-28 14:30:28 +020089# may be overridden by regression tests
90get_process_id = lambda: '%d-%d' % (os.getpid(), time.time())
91
Neels Hofmeyr85eb3242017-04-09 22:01:16 +020092class Error(Exception):
Pau Espin Pedrolcf6bec72020-06-04 16:13:37 +020093 def __init__(self, *messages, origin=None, **named_items):
94 msg = ''
95 if origin is None:
96 origin = Origin.find_on_stack(f=sys._getframe(1))
97 if origin:
98 msg += origin.name() + ': '
99 msg += compose_message(messages, named_items)
100 if origin and origin._parent is not None:
101 deeper_origins = origin.ancestry_str()
102 msg += ' [%s]' % deeper_origins
103 super().__init__(msg)
Neels Hofmeyr85eb3242017-04-09 22:01:16 +0200104
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200105class LogTarget:
Neels Hofmeyrf8166882017-05-05 19:48:35 +0200106 all_targets = []
107
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200108 do_log_time = None
109 do_log_category = None
110 do_log_level = None
111 do_log_origin = None
Neels Hofmeyr9576f5f2017-05-24 18:31:01 +0200112 do_log_all_origins_on_levels = None
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200113 do_log_traceback = None
114 do_log_src = None
115 origin_width = None
116 origin_fmt = None
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200117 all_levels = None
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200118
119 # redirected by logging test
Pau Espin Pedrol0d8deec2017-06-23 11:43:38 +0200120 get_time_str = lambda self: datetime.now().strftime(self.log_time_fmt)
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200121
122 # sink that gets each complete logging line
Neels Hofmeyrf8166882017-05-05 19:48:35 +0200123 log_write_func = None
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200124
125 category_levels = None
126
Neels Hofmeyrf8166882017-05-05 19:48:35 +0200127 def __init__(self, log_write_func=None):
128 if log_write_func is None:
129 log_write_func = sys.__stdout__.write
130 self.log_write_func = log_write_func
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200131 self.category_levels = {}
132 self.style()
Neels Hofmeyrf8166882017-05-05 19:48:35 +0200133 LogTarget.all_targets.append(self)
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200134
Neels Hofmeyrfd7b9d02017-05-05 19:51:40 +0200135 def remove(self):
136 LogTarget.all_targets.remove(self)
137
Neels Hofmeyr9576f5f2017-05-24 18:31:01 +0200138 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 +0200139 '''
140 set all logging format aspects, to defaults if not passed:
141 time: log timestamps;
142 time_fmt: format of timestamps;
143 category: print the logging category (three letters);
144 level: print the logging level, unless it is L_LOG;
145 origin: print which object(s) the message originated from;
146 origin_width: fill up the origin string with whitespace to this witdh;
147 src: log the source file and line number the log comes from;
148 trace: on exceptions, log the full stack trace;
Neels Hofmeyr9576f5f2017-05-24 18:31:01 +0200149 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 +0200150 '''
151 self.log_time_fmt = time_fmt
152 self.do_log_time = bool(time)
153 if not self.log_time_fmt:
154 self.do_log_time = False
155 self.do_log_category = bool(category)
156 self.do_log_level = bool(level)
157 self.do_log_origin = bool(origin)
158 self.origin_width = int(origin_width)
159 self.origin_fmt = '{:>%ds}' % self.origin_width
160 self.do_log_src = src
161 self.do_log_traceback = trace
Neels Hofmeyr9576f5f2017-05-24 18:31:01 +0200162 self.do_log_all_origins_on_levels = tuple(all_origins_on_levels or [])
Neels Hofmeyr1a2177c2017-05-06 23:58:46 +0200163 return self
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200164
Neels Hofmeyr9576f5f2017-05-24 18:31:01 +0200165 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 +0200166 'modify only the given aspects of the logging format'
167 self.style(
168 time=(time if time is not None else self.do_log_time),
169 time_fmt=(time_fmt if time_fmt is not None else self.log_time_fmt),
170 category=(category if category is not None else self.do_log_category),
171 level=(level if level is not None else self.do_log_level),
172 origin=(origin if origin is not None else self.do_log_origin),
173 origin_width=(origin_width if origin_width is not None else self.origin_width),
174 src=(src if src is not None else self.do_log_src),
175 trace=(trace if trace is not None else self.do_log_traceback),
Neels Hofmeyr9576f5f2017-05-24 18:31:01 +0200176 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 +0200177 )
Neels Hofmeyr1a2177c2017-05-06 23:58:46 +0200178 return self
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200179
180 def set_level(self, category, level):
181 'set global logging log.L_* level for a given log.C_* category'
182 self.category_levels[category] = level
Neels Hofmeyr1a2177c2017-05-06 23:58:46 +0200183 return self
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200184
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200185 def set_all_levels(self, level):
186 self.all_levels = level
Neels Hofmeyr1a2177c2017-05-06 23:58:46 +0200187 return self
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200188
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200189 def is_enabled(self, category, level):
190 if level == L_TRACEBACK:
191 return self.do_log_traceback
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200192 if self.all_levels is not None:
193 is_level = self.all_levels
194 else:
195 is_level = self.category_levels.get(category)
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200196 if is_level is None:
197 is_level = L_LOG
198 if level < is_level:
199 return False
200 return True
201
202 def log(self, origin, category, level, src, messages, named_items):
203 if category and len(category) != 3:
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200204 self.log_write_func('WARNING: INVALID LOGGING CATEGORY %r\n' % category)
Neels Hofmeyrf8166882017-05-05 19:48:35 +0200205 self.log_write_func('origin=%r category=%r level=%r\n' % (origin, category, level));
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200206
207 if not category:
208 category = C_DEFAULT
209 if not self.is_enabled(category, level):
210 return
211
212 log_pre = []
213 if self.do_log_time:
214 log_pre.append(self.get_time_str())
215
216 if self.do_log_category:
217 log_pre.append(category)
218
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200219 deeper_origins = ''
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200220 if self.do_log_origin:
221 if origin is None:
222 name = '-'
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200223 elif isinstance(origin, Origin):
Neels Hofmeyr081e89f2020-12-04 17:25:23 +0100224 name = origin.src()
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200225 # only log ancestry when there is more than one
226 if origin._parent is not None:
227 deeper_origins = origin.ancestry_str()
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200228 elif isinstance(origin, str):
229 name = origin or None
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200230 if not name:
231 name = str(origin.__class__.__name__)
232 log_pre.append(self.origin_fmt.format(name))
233
234 if self.do_log_level and level != L_LOG:
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200235 loglevel = '%s: ' % (level_str(level) or ('loglevel=' + str(level)))
236 else:
237 loglevel = ''
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200238
Neels Hofmeyr85eb3242017-04-09 22:01:16 +0200239 log_line = [compose_message(messages, named_items)]
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200240
Neels Hofmeyr9576f5f2017-05-24 18:31:01 +0200241 if deeper_origins and (level in self.do_log_all_origins_on_levels):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200242 log_line.append(' [%s]' % deeper_origins)
243
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200244 if self.do_log_src and src:
245 log_line.append(' [%s]' % str(src))
246
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200247 log_str = '%s%s%s%s' % (' '.join(log_pre),
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200248 ': ' if log_pre else '',
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200249 loglevel,
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200250 ' '.join(log_line))
251
Neels Hofmeyr5356d0a2017-04-10 03:45:30 +0200252 if not log_str.endswith('\n'):
253 log_str = log_str + '\n'
Neels Hofmeyrf8166882017-05-05 19:48:35 +0200254 self.log_write_func(log_str)
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200255
Neels Hofmeyr6ccda112017-06-06 19:41:17 +0200256 def large_separator(self, *msgs, sublevel=1, space_above=True):
257 sublevel = max(1, min(3, sublevel))
Your Name44af3412017-04-13 03:11:59 +0200258 msg = ' '.join(msgs)
Neels Hofmeyr6ccda112017-06-06 19:41:17 +0200259 sep = '-' * int(23 * (5 - sublevel))
Your Name44af3412017-04-13 03:11:59 +0200260 if not msg:
Neels Hofmeyr6ccda112017-06-06 19:41:17 +0200261 msg = sep
262 lines = [sep, msg, sep, '']
263 if space_above:
264 lines.insert(0, '')
265 self.log_write_func('\n'.join(lines))
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200266
Neels Hofmeyr12ed9962020-12-02 17:38:55 +0100267 def get_mark(self):
268 # implemented in FileLogTarget
269 return 0
270
271 def get_output(self, since_mark=0):
272 # implemented in FileLogTarget
273 return ''
274
275
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200276def level_str(level):
277 if level == L_TRACEBACK:
278 return L_TRACEBACK
279 if level <= L_DBG:
280 return 'DBG'
281 if level <= L_LOG:
282 return 'LOG'
283 return 'ERR'
284
285def _log_all_targets(origin, category, level, src, messages, named_items=None):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200286 if origin is None:
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200287 origin = Origin.find_on_stack()
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200288 if isinstance(src, int):
289 src = get_src_from_caller(src + 1)
Neels Hofmeyrf8166882017-05-05 19:48:35 +0200290 for target in LogTarget.all_targets:
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200291 target.log(origin, category, level, src, messages, named_items)
292
Neels Hofmeyr6ccda112017-06-06 19:41:17 +0200293def large_separator(*msgs, sublevel=1, space_above=True):
Neels Hofmeyrf8166882017-05-05 19:48:35 +0200294 for target in LogTarget.all_targets:
Neels Hofmeyr6ccda112017-06-06 19:41:17 +0200295 target.large_separator(*msgs, sublevel=sublevel, space_above=space_above)
Your Name44af3412017-04-13 03:11:59 +0200296
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200297def get_src_from_caller(levels_up=1):
Holger Hans Peter Freyther257ee102018-02-12 09:07:05 +0000298 # Poke into internal to avoid hitting the linecache which will make one or
299 # more calls to stat(2).
300 frame = sys._getframe(levels_up)
301 return '%s:%d' % (os.path.basename(frame.f_code.co_filename), frame.f_lineno)
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200302
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200303def get_src_from_exc_info(exc_info=None, levels_up=1):
304 if exc_info is None:
305 exc_info = sys.exc_info()
306 ftb = traceback.extract_tb(exc_info[2])
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200307 f,l,m,c = ftb[-levels_up]
308 f = os.path.basename(f)
309 return '%s:%s: %s' % (f, l, c)
310
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200311def get_line_for_src(src_path):
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200312 '''find a given source file on the stack and return the line number for
313 that file. (Used to indicate the position in a test script.)'''
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200314 etype, exception, tb = sys.exc_info()
315 if tb:
316 ftb = traceback.extract_tb(tb)
317 for f,l,m,c in ftb:
318 if f.endswith(src_path):
319 return l
320
321 for frame in stack():
322 caller = getframeinfo(frame[0])
323 if caller.filename.endswith(src_path):
324 return caller.lineno
325 return None
326
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200327def ctx(*name_items, **detail_items):
328 '''Store log context in the current frame. This string will appear as
329 origin information for exceptions thrown within the calling scope.'''
330 if not name_items and not detail_items:
331 ctx_obj(None)
332 if not detail_items and len(name_items) == 1 and isinstance(name_items[0], Origin):
333 ctx_obj(name_items[0])
334 else:
335 ctx_obj(compose_message(name_items, detail_items))
336
337def ctx_obj(origin_or_str):
338 f = sys._getframe(2)
339 if origin_or_str is None:
340 f.f_locals.pop(LOG_CTX_VAR, None)
Pau Espin Pedrol19fe3a12020-06-04 16:11:37 +0200341 return
342 if isinstance(origin_or_str, Origin) and origin_or_str is f.f_locals.get('self'):
343 # Avoid adding log ctx in stack frame where Origin it is already "self",
344 # it is not needed and will make find_on_stack() to malfunction
345 raise Error('Don\'t use log.ctx(self), it\'s not needed!')
346 f.f_locals[LOG_CTX_VAR] = origin_or_str
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200347
348class OriginLoopError(Error):
349 pass
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200350
351class Origin:
352 '''
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200353 Base class for all classes that want to appear in the log.
354 It is a simple named marker to find in the stack frames.
355 This depends on the object instance named 'self' in each member class.
356
357 In addition, it provides a logging category and a globally unique ID for
358 each instance.
359
360 Each child class *must* call super().__init__(category, name), to allow
361 noting its parent origins.
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200362 '''
363
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200364 _global_id = None
365
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200366 _name = None
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200367 _origin_id = None
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200368 _log_category = None
369 _parent = None
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200370
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200371 @staticmethod
372 def find_on_stack(except_obj=None, f=None):
373 if f is None:
374 f = sys._getframe(2)
375 log_ctx_obj = None
376 origin = None
377 while f is not None:
378 l = f.f_locals
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200379
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200380 # if there is a log_ctx in the scope, add it, pointing to the next
381 # actual Origin class in the stack
382 log_ctx = l.get(LOG_CTX_VAR)
383 if log_ctx:
384 if isinstance(log_ctx, Origin):
385 new_log_ctx_obj = log_ctx
386 else:
387 new_log_ctx_obj = Origin(None, log_ctx, find_parent=False)
388 if log_ctx_obj is None:
389 log_ctx_obj = new_log_ctx_obj
390 else:
391 log_ctx_obj.highest_ancestor()._set_parent(new_log_ctx_obj)
392
393 obj = l.get('self')
394 if obj and isinstance(obj, Origin) and (except_obj is not obj):
395 origin = obj
396 break
397 f = f.f_back
398
399 if (origin is not None) and (log_ctx_obj is not None):
Pau Espin Pedrol9e2e8d22020-06-11 11:24:41 +0200400 log_ctx_highest_ancestor = log_ctx_obj.highest_ancestor()
401 # If Both end up in same ancestor it means they are connected to the
402 # same tree, so no need to connect them, we'll use log_ctx_obj
403 # specific path in that case.
404 if log_ctx_highest_ancestor != origin.highest_ancestor():
405 log_ctx_highest_ancestor._set_parent(origin)
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200406 p = log_ctx_obj
407 while p:
408 p._set_log_category(origin._log_category)
409 p = p._parent
410 if log_ctx_obj is not None:
411 return log_ctx_obj
412 # may return None
413 return origin
414
415 @staticmethod
416 def find_in_exc_info(exc_info):
417 tb = exc_info[2]
418 # get last tb ... I hope that's right
419 while tb.tb_next:
420 tb = tb.tb_next
421 return Origin.find_on_stack(f=tb.tb_frame)
422
423 def __init__(self, category, *name_items, find_parent=True, **detail_items):
424 self._set_log_category(category)
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200425 self.set_name(*name_items, **detail_items)
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200426 if find_parent:
427 self._set_parent(Origin.find_on_stack(except_obj=self))
428
429 def _set_parent(self, parent):
430 # make sure to avoid loops
431 p = parent
432 while p:
433 if p is self:
434 raise OriginLoopError('Origin parent loop')
435 p = p._parent
436 self._parent = parent
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200437
438 def set_name(self, *name_items, **detail_items):
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200439 '''Change the origin's name for log output; rather use the constructor.
440 This function can be used to change the name in case naming info
441 becomes available only after class creation (like a pid)'''
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200442 if name_items:
443 name = '-'.join([str(i) for i in name_items])
444 elif not detail_items:
445 name = self.__class__.__name__
446 else:
447 name = ''
448 if detail_items:
449 details = '(%s)' % (', '.join([("%s=%r" % (k,v))
450 for k,v in sorted(detail_items.items())]))
451 else:
452 details = ''
453 self._name = name + details
454
455 def name(self):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200456 return self._name or self.__class__.__name__
457
Neels Hofmeyr081e89f2020-12-04 17:25:23 +0100458 def src(self):
459 '''subclasses may override this to provide more detailed source
460 information with the name, for a backtrace. For example, a line number
461 in a test script.'''
462 return self.name()
463
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200464 __str__ = name
465 __repr__ = name
466
467 def origin_id(self):
468 if not self._origin_id:
469 if not Origin._global_id:
470 Origin._global_id = get_process_id()
471 self._origin_id = '%s-%s' % (self.name(), Origin._global_id)
472 return self._origin_id
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200473
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200474 def _set_log_category(self, category):
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200475 self._log_category = category
476
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200477 def ancestry(self):
478 origins = []
479 n = 10
480 origin = self
481 while origin:
482 origins.insert(0, origin)
483 origin = origin._parent
484 n -= 1
485 if n < 0:
486 break
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200487 return origins
488
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200489 def ancestry_str(self):
Neels Hofmeyr081e89f2020-12-04 17:25:23 +0100490 return '↪'.join([o.src() for o in self.ancestry()])
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200491
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200492 def highest_ancestor(self):
493 if self._parent:
494 return self._parent.highest_ancestor()
495 return self
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200496
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200497 def log(self, *messages, _src=3, **named_items):
498 '''same as log.log() but passes this object to skip looking up an origin'''
499 log(*messages, _origin=self, _src=_src, **named_items)
500
501 def dbg(self, *messages, _src=3, **named_items):
502 '''same as log.dbg() but passes this object to skip looking up an origin'''
503 dbg(*messages, _origin=self, _src=_src, **named_items)
504
505 def err(self, *messages, _src=3, **named_items):
506 '''same as log.err() but passes this object to skip looking up an origin'''
507 err(*messages, _origin=self, _src=_src, **named_items)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200508
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200509def trace(exc_info=None, origin=None):
510 if exc_info is None:
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200511 exc_info = sys.exc_info()
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200512 if origin is None:
513 origin = Origin.find_in_exc_info(exc_info)
514 _log(messages=traceback.format_exception(*exc_info),
515 origin=origin, level=L_TRACEBACK)
516
517def log_exn():
518 exc_info = sys.exc_info()
519 origin = Origin.find_in_exc_info(exc_info)
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200520
521 etype, exception, tb = exc_info
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200522 if hasattr(exception, 'msg'):
523 msg = exception.msg
524 else:
525 msg = str(exception)
526
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200527 trace(exc_info, origin=origin)
528 _log(messages=('%s:' % str(etype.__name__), msg),
529 origin=origin, level=L_ERR, src=get_src_from_exc_info(exc_info))
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200530
531
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200532def set_all_levels(level):
Neels Hofmeyrf8166882017-05-05 19:48:35 +0200533 for target in LogTarget.all_targets:
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200534 target.set_all_levels(level)
535
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200536def set_level(category, level):
Neels Hofmeyrf8166882017-05-05 19:48:35 +0200537 for target in LogTarget.all_targets:
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200538 target.set_level(category, level)
539
540def style(**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(**kwargs)
543
544def style_change(**kwargs):
Neels Hofmeyrf8166882017-05-05 19:48:35 +0200545 for target in LogTarget.all_targets:
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200546 target.style_change(**kwargs)
547
548class TestsTarget(LogTarget):
549 'LogTarget producing deterministic results for regression tests'
Neels Hofmeyrf8166882017-05-05 19:48:35 +0200550 def __init__(self, log_write_func=None):
551 super().__init__(log_write_func)
Neels Hofmeyr532126a2017-05-05 19:51:40 +0200552 self.style(time=False, src=False, origin_width=0)
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200553
Neels Hofmeyr8f4f1742017-05-07 00:00:14 +0200554class FileLogTarget(LogTarget):
555 'LogTarget to log to a file system path'
556 log_file = None
557
558 def __init__(self, log_path):
559 atexit.register(self.at_exit)
560 self.path = log_path
561 self.log_file = open(log_path, 'a')
562 super().__init__(self.write_to_log_and_flush)
563
564 def remove(self):
565 super().remove()
566 self.log_file.close()
567 self.log_file = None
568
569 def write_to_log_and_flush(self, msg):
570 self.log_file.write(msg)
571 self.log_file.flush()
572
573 def at_exit(self):
574 if self.log_file is not None:
575 self.log_file.flush()
576 self.log_file.close()
577
Pau Espin Pedrol5bbdab82020-02-24 18:19:10 +0100578 def log_file_path(self):
579 return self.path
580
Neels Hofmeyr12ed9962020-12-02 17:38:55 +0100581 def get_mark(self):
582 if self.path is None:
583 return 0
584 # return current file length
585 with open(self.path, 'r') as logfile:
586 return logfile.seek(0, 2)
587
588 def get_output(self, since_mark=0):
589 if self.path is None:
590 return ''
591 with open(self.path, 'r') as logfile:
592 if since_mark:
593 logfile.seek(since_mark)
594 return logfile.read()
595
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200596def run_logging_exceptions(func, *func_args, return_on_failure=None, **func_kwargs):
597 try:
598 return func(*func_args, **func_kwargs)
599 except:
600 log_exn()
601 return return_on_failure
602
Neels Hofmeyr2694a9d2017-04-27 19:48:09 +0200603def _compose_named_items(item):
604 'make sure dicts are output sorted, for test expectations'
605 if is_dict(item):
606 return '{%s}' % (', '.join(
607 ['%s=%s' % (k, _compose_named_items(v))
608 for k,v in sorted(item.items())]))
609 return repr(item)
610
Neels Hofmeyr85eb3242017-04-09 22:01:16 +0200611def compose_message(messages, named_items):
612 msgs = [str(m) for m in messages]
613
614 if named_items:
615 # unfortunately needs to be sorted to get deterministic results
Neels Hofmeyr2694a9d2017-04-27 19:48:09 +0200616 msgs.append(_compose_named_items(named_items))
Neels Hofmeyr85eb3242017-04-09 22:01:16 +0200617
618 return ' '.join(msgs)
619
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200620# vim: expandtab tabstop=4 shiftwidth=4