diff --git a/docs/howto.rst b/docs/howto.rst index cdc4c3d08..97c72bf55 100644 --- a/docs/howto.rst +++ b/docs/howto.rst @@ -1162,3 +1162,63 @@ If you use a Python-based configuration file, you can define your custom launche .. note:: In versions prior to 4.0, launchers could only be implemented inside the source code tree of ReFrame. + + +.. _custom-loggers: + +Implementing a custom log handler +--------------------------------- + +.. versionadded:: 4.7 + +ReFrame allows you to define custom log handlers and attach them to the framework. +Here's an example implementation of a custom log handler and how it can be used in a Python-based configuration file. + +Define a custom log handler class based on :class:`~logging.Handler` which uses a custom logging API: + +.. code-block:: python + + import logging + import mylogger + + class MyLoggerHandler(logging.Handler): + def __init__(self, key): + super().__init__() + self.key = key + + def emit(self, record): + myrecord = { + 'value': record.check_perf_value, + } + mylogger.log(self.key, myrecord) + +Applying the :func:`@register_log_handler ` decorator to a function returns an instance of the custom log handler: + +.. code-block:: python + + from reframe.core.logging import register_log_handler + + @register_log_handler("mylogger") + def _create_mylogger_handler(site_config, config_prefix): + key = site_config.get(f'{config_prefix}/key') + return MyLoggerHandler(key) + + +Finally, add a handler entry with type matching the registered name for the custom log handler to the site config: + +.. code-block:: python + + site_configuration = { + 'logging': [ + { + 'handlers': [ + { + 'type': 'mylogger', + 'key': 'abc', + }, + ... + ] + } + ], + ... + } diff --git a/reframe/core/logging.py b/reframe/core/logging.py index d91b067fb..804bd103b 100644 --- a/reframe/core/logging.py +++ b/reframe/core/logging.py @@ -412,6 +412,20 @@ def stream_handler_kind(handler): return logger +# Registry for log handler creation functions +_create_handlers = {} + + +def register_log_handler(name): + '''Register the decorated log handler creation function''' + def _create_handler_wrapper(fn): + _create_handlers[name] = fn + return fn + + return _create_handler_wrapper + + +@register_log_handler('file') def _create_file_handler(site_config, config_prefix): filename = os.path.expandvars(site_config.get(f'{config_prefix}/name')) if not filename: @@ -431,6 +445,7 @@ def _create_file_handler(site_config, config_prefix): mode='a+' if append else 'w+') +@register_log_handler('filelog') def _create_filelog_handler(site_config, config_prefix): basedir = os.path.abspath(os.path.join( site_config.get('systems/0/prefix'), @@ -447,6 +462,7 @@ def _create_filelog_handler(site_config, config_prefix): ignore_keys=ignore_keys) +@register_log_handler('syslog') def _create_syslog_handler(site_config, config_prefix): address = site_config.get(f'{config_prefix}/address') @@ -485,6 +501,7 @@ def _create_syslog_handler(site_config, config_prefix): return logging.handlers.SysLogHandler(address, facility_type, socket_type) +@register_log_handler('stream') def _create_stream_handler(site_config, config_prefix): stream = site_config.get(f'{config_prefix}/name') if stream == 'stdout': @@ -496,6 +513,7 @@ def _create_stream_handler(site_config, config_prefix): raise AssertionError(f'unknown stream: {stream}') +@register_log_handler('graylog') def _create_graylog_handler(site_config, config_prefix): try: import pygelf @@ -528,6 +546,7 @@ def _create_graylog_handler(site_config, config_prefix): json_default=jsonext.encode) +@register_log_handler('httpjson') def _create_httpjson_handler(site_config, config_prefix): url = site_config.get(f'{config_prefix}/url') extras = site_config.get(f'{config_prefix}/extras') @@ -684,35 +703,18 @@ def _extract_handlers(site_config, handlers_group): handlers = [] for i, handler_config in enumerate(handlers_list): handler_type = handler_config['type'] - if handler_type == 'file': - hdlr = _create_file_handler(site_config, f'{handler_prefix}/{i}') - elif handler_type == 'filelog': - hdlr = _create_filelog_handler( - site_config, f'{handler_prefix}/{i}' - ) - elif handler_type == 'syslog': - hdlr = _create_syslog_handler(site_config, f'{handler_prefix}/{i}') - elif handler_type == 'stream': - hdlr = _create_stream_handler(site_config, f'{handler_prefix}/{i}') - elif handler_type == 'graylog': - hdlr = _create_graylog_handler( - site_config, f'{handler_prefix}/{i}' - ) - if hdlr is None: - getlogger().warning('could not initialize the ' - 'graylog handler; ignoring ...') - continue - elif handler_type == 'httpjson': - hdlr = _create_httpjson_handler( - site_config, f'{handler_prefix}/{i}' - ) - if hdlr is None: - getlogger().warning('could not initialize the ' - 'httpjson handler; ignoring ...') - continue - else: - # Should not enter here - raise AssertionError(f'unknown handler type: {handler_type}') + + try: + create_handler = _create_handlers[handler_type] + except KeyError: + raise ConfigError( + f'unknown handler type: {handler_type}') from None + + hdlr = create_handler(site_config, f'{handler_prefix}/{i}') + if hdlr is None: + getlogger().warning('could not initialize the ' + f'{handler_type} handler; ignoring ...') + continue level = site_config.get(f'{handler_prefix}/{i}/level') fmt = site_config.get(f'{handler_prefix}/{i}/format') diff --git a/reframe/schemas/config.json b/reframe/schemas/config.json index a3b9eccd1..4e8b868be 100644 --- a/reframe/schemas/config.json +++ b/reframe/schemas/config.json @@ -49,10 +49,7 @@ "handler_common": { "type": "object", "properties": { - "type": { - "type": "string", - "enum": ["file", "filelog", "graylog", "stream", "syslog", "httpjson"] - }, + "type": {"type": "string"}, "level": {"$ref": "#/defs/loglevel"}, "format": {"type": "string"}, "format_perfvars": {"type": "string"},