Skip to content

Commit 4aa8ac9

Browse files
author
Vasileios Karakasis
authored
Merge pull request #1600 from rsarm/feat/logging
[feat] Allow any test attribute to be logged
2 parents dc7e2d8 + 46dec6a commit 4aa8ac9

File tree

3 files changed

+121
-34
lines changed

3 files changed

+121
-34
lines changed

docs/config_reference.rst

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -695,12 +695,8 @@ All logging handlers share the following set of common attributes:
695695
If specific formatting is desired, the ``check_job_completion_time`` should be used instead.
696696
- ``%(check_name)s``: The name of the regression test on behalf of which ReFrame is currently executing.
697697
If ReFrame is not executing in the context of a regression test, ``reframe`` will be printed instead.
698-
- ``%(check_num_tasks)s``: The number of tasks assigned to the regression test.
699-
- ``%(check_outputdir)s``: The output directory associated with the currently executing test.
700698
- ``%(check_partition)s``: The system partition where this test is currently executing.
701-
- ``%(check_stagedir)s``: The stage directory associated with the currently executing test.
702699
- ``%(check_system)s``: The system where this test is currently executing.
703-
- ``%(check_tags)s``: The tags associated with this test.
704700
- ``%(check_perf_lower_thres)s``: The lower threshold of the performance difference from the reference value expressed as a fractional value.
705701
See the :attr:`reframe.core.pipeline.RegressionTest.reference` attribute of regression tests for more details.
706702
- ``%(check_perf_ref)s``: The reference performance value of a certain performance variable.
@@ -709,11 +705,20 @@ All logging handlers share the following set of common attributes:
709705
See the :attr:`reframe.core.pipeline.RegressionTest.reference` attribute of regression tests for more details.
710706
- ``%(check_perf_value)s``: The performance value obtained for a certain performance variable.
711707
- ``%(check_perf_var)s``: The name of the `performance variable <tutorial_basic.html#writing-a-performance-test>`__ being logged.
708+
- ``%(check_ATTR)s``: This will log the value of the attribute ``ATTR`` of the currently executing regression test.
709+
Mappings will be logged as ``k1=v1,k2=v2,..`` and all other iterables, except strings, will be logged as comma-separated lists.
710+
If ``ATTR`` is not an attribute of the test, ``%(check_ATTR)s`` will be logged as ``<undefined>``.
711+
This allows users to log arbitrary attributes of their tests.
712+
For the complete list of test attributes, please refer to :doc:`regression_test_api`.
712713
- ``%(osuser)s``: The name of the OS user running ReFrame.
713714
- ``%(osgroup)s``: The name of the OS group running ReFrame.
714715
- ``%(version)s``: The ReFrame version.
715716

716717

718+
.. versionadded:: 3.3
719+
The ability to log arbitrary test attributes was added.
720+
721+
717722
.. js:attribute:: .logging[].handlers[].datefmt
718723

719724
.. object:: .logging[].handlers_perflog[].datefmt

reframe/core/logging.py

Lines changed: 79 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
import logging.handlers
1010
import numbers
1111
import os
12-
import pprint
1312
import re
1413
import shutil
1514
import sys
@@ -145,7 +144,7 @@ def emit(self, record):
145144
except OSError as e:
146145
raise LoggingError('logging failed') from e
147146

148-
self.baseFilename = os.path.join(dirname, record.check_name + '.log')
147+
self.baseFilename = os.path.join(dirname, record.check.name + '.log')
149148
self.stream = self._streams.get(self.baseFilename, None)
150149
super().emit(record)
151150
self._streams[self.baseFilename] = self.stream
@@ -165,7 +164,65 @@ def _format_time_rfc3339(timestamp, datefmt):
165164
return re.sub(r'(%)?\:z', tz_rfc3339, time.strftime(datefmt, timestamp))
166165

167166

168-
class RFC3339Formatter(logging.Formatter):
167+
def _xfmt(val):
168+
from reframe.core.deferrable import _DeferredExpression
169+
170+
if val is None:
171+
return '<undefined>'
172+
173+
if isinstance(val, _DeferredExpression):
174+
try:
175+
return val.evaluate()
176+
except BaseException:
177+
return '<error>'
178+
179+
if isinstance(val, str):
180+
return val
181+
182+
if isinstance(val, collections.abc.Mapping):
183+
return ','.join(f'{k}={v}' for k, v in val.items())
184+
185+
if isinstance(val, collections.abc.Iterable):
186+
return ','.join(val)
187+
188+
return val
189+
190+
191+
class CheckFieldFormatter(logging.Formatter):
192+
'''Log formatter that dynamically looks up format specifiers inside a
193+
regression test.'''
194+
195+
def __init__(self, fmt=None, datefmt=None, style='%'):
196+
super().__init__(fmt, datefmt, style)
197+
198+
# NOTE: This will work only when style='%'
199+
self.__extras = {
200+
spec: None for spec in re.findall(r'\%\((check_\S+?)\)s', fmt)
201+
}
202+
203+
# Set the default value for 'check_name'
204+
if 'check_name' in self.__extras:
205+
self.__extras['check_name'] = 'reframe'
206+
207+
def format(self, record):
208+
# Fill in the check-specific record attributes
209+
if record.check:
210+
for spec in self.__extras:
211+
if hasattr(record, spec):
212+
# Attribute set elsewhere
213+
continue
214+
215+
attr = spec.split('_', maxsplit=1)[1]
216+
val = getattr(record.check, attr, None)
217+
record.__dict__[spec] = _xfmt(val)
218+
else:
219+
# Update record with the dynamic extras even if check is not set
220+
record.__dict__.update(self.__extras)
221+
222+
return super().format(record)
223+
224+
225+
class RFC3339Formatter(CheckFieldFormatter):
169226
def formatTime(self, record, datefmt=None):
170227
datefmt = datefmt or self.default_time_format
171228
if '%:z' not in datefmt:
@@ -176,7 +233,7 @@ def formatTime(self, record, datefmt=None):
176233

177234
def format(self, record):
178235
datefmt = self.datefmt or self.default_time_format
179-
if record.check_job_completion_time_unix is not None:
236+
if record.check_job_completion_time_unix != _xfmt(None):
180237
ct = self.converter(record.check_job_completion_time_unix)
181238
record.check_job_completion_time = _format_time_rfc3339(
182239
ct, datefmt
@@ -388,26 +445,24 @@ def __init__(self, logger=None, check=None):
388445
super().__init__(
389446
logger,
390447
{
391-
'check_name': 'reframe',
392-
'check_jobid': '-1',
393-
'check_job_completion_time': None,
394-
'check_job_completion_time_unix': None,
448+
# Here we only set the format specifiers that do not
449+
# correspond directly to check attributes
450+
'check': check,
451+
'check_jobid': _xfmt(None),
452+
'check_job_completion_time': _xfmt(None),
453+
'check_job_completion_time_unix': _xfmt(None),
395454
'check_info': 'reframe',
396-
'check_system': None,
397-
'check_partition': None,
398-
'check_environ': None,
399-
'check_outputdir': None,
400-
'check_stagedir': None,
401-
'check_num_tasks': None,
402-
'check_perf_var': None,
403-
'check_perf_value': None,
404-
'check_perf_ref': None,
405-
'check_perf_lower_thres': None,
406-
'check_perf_upper_thres': None,
407-
'check_perf_unit': None,
408-
'osuser': osext.osuser() or '<unknown>',
409-
'osgroup': osext.osgroup() or '<unknown>',
410-
'check_tags': None,
455+
'check_system': _xfmt(None),
456+
'check_partition': _xfmt(None),
457+
'check_environ': _xfmt(None),
458+
'check_perf_var': _xfmt(None),
459+
'check_perf_value': _xfmt(None),
460+
'check_perf_ref': _xfmt(None),
461+
'check_perf_lower_thres': _xfmt(None),
462+
'check_perf_upper_thres': _xfmt(None),
463+
'check_perf_unit': _xfmt(None),
464+
'osuser': _xfmt(osext.osuser()),
465+
'osgroup': _xfmt(osext.osgroup()),
411466
'version': osext.reframe_version(),
412467
}
413468
)
@@ -428,15 +483,11 @@ def std_stream_handlers(self):
428483

429484
def _update_check_extras(self):
430485
'''Return a dictionary with all the check-specific information.'''
486+
431487
if self.check is None:
432488
return
433489

434-
self.extra['check_name'] = self.check.name
435490
self.extra['check_info'] = self.check.info()
436-
self.extra['check_outputdir'] = self.check.outputdir
437-
self.extra['check_stagedir'] = self.check.stagedir
438-
self.extra['check_num_tasks'] = self.check.num_tasks
439-
self.extra['check_tags'] = ','.join(self.check.tags)
440491
if self.check.current_system:
441492
self.extra['check_system'] = self.check.current_system.name
442493

unittests/test_logging.py

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import reframe.core.runtime as rt
2020
import reframe.core.settings as settings
2121
import reframe.utility as util
22+
import reframe.utility.sanity as sn
2223
from reframe.core.exceptions import ConfigError, ReframeError
2324
from reframe.core.backends import (getlauncher, getscheduler)
2425
from reframe.core.schedulers import Job
@@ -29,20 +30,30 @@ def fake_check():
2930
class _FakeCheck(rfm.RegressionTest):
3031
pass
3132

33+
@sn.sanity_function
34+
def error():
35+
raise BaseException
36+
3237
# A bit hacky, but we don't want to run a full test every time
3338
test = _FakeCheck()
3439
test._job = Job.create(getscheduler('local')(),
3540
getlauncher('local')(),
3641
'fakejob')
3742
test.job._completion_time = time.time()
43+
test.custom = 'hello extras'
44+
test.custom_list = ['custom', 'attr']
45+
test.custom_dict = {'a': 1, 'b': 2}
46+
test.deferred = sn.defer('hello')
47+
test.deferred_error = error()
3848
return test
3949

4050

4151
@pytest.fixture
4252
def rfc3339formatter():
4353
return rlog.RFC3339Formatter(
4454
fmt='[%(asctime)s] %(levelname)s: %(check_name)s: %(message)s',
45-
datefmt='%FT%T')
55+
datefmt='%FT%T'
56+
)
4657

4758

4859
@pytest.fixture
@@ -146,12 +157,32 @@ def test_logger_levels(logfile, logger_with_check):
146157
assert _pattern_in_logfile('foo', logfile)
147158

148159

160+
def test_logger_dynamic_attributes(logfile, logger_with_check):
161+
formatter = rlog.RFC3339Formatter('%(check_custom)s|%(check_custom_list)s|'
162+
'%(check_foo)s|%(check_custom_dict)s')
163+
logger_with_check.logger.handlers[0].setFormatter(formatter)
164+
logger_with_check.info('xxx')
165+
assert _pattern_in_logfile(
166+
r'hello extras\|custom,attr\|<undefined>\|a=1,b=2', logfile
167+
)
168+
169+
170+
def test_logger_dynamic_attributes_deferrables(logfile, logger_with_check):
171+
formatter = rlog.RFC3339Formatter(
172+
'%(check_deferred)s|%(check_deferred_error)s'
173+
)
174+
logger_with_check.logger.handlers[0].setFormatter(formatter)
175+
logger_with_check.info('xxx')
176+
assert _pattern_in_logfile(r'hello\|<error>', logfile)
177+
178+
149179
def test_rfc3339_timezone_extension(logfile, logger_with_check,
150180
logger_without_check):
151181
formatter = rlog.RFC3339Formatter(
152182
fmt=('[%(asctime)s] %(levelname)s: %(check_name)s: '
153183
'ct:%(check_job_completion_time)s: %(message)s'),
154-
datefmt='%FT%T%:z')
184+
datefmt='%FT%T%:z'
185+
)
155186
logger_with_check.logger.handlers[0].setFormatter(formatter)
156187
logger_with_check.info('foo')
157188
logger_without_check.info('foo')

0 commit comments

Comments
 (0)