Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[feat] Allow users define custom log handlers and attach them to the framework #3274

Merged
merged 9 commits into from
Oct 18, 2024
57 changes: 57 additions & 0 deletions docs/howto.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1162,3 +1162,60 @@ 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
---------------------------------

dmargala marked this conversation as resolved.
Show resolved Hide resolved
Here's an example implementation of a custom log handler defined in a Python-based configuration file.
dmargala marked this conversation as resolved.
Show resolved Hide resolved

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)

Apply the :func:`~reframe.core.logging.register_log_handler` decorator to a function returns an instance of the custom log handler:
dmargala marked this conversation as resolved.
Show resolved Hide resolved

.. 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 registered name for the custom log handler to the site config:

.. code-block:: python

site_configuration = {
'logging': [
{
'handlers': [
{
'type': 'mylogger',
'key': 'abc',
},
...
]
}
],
...
}
48 changes: 26 additions & 22 deletions reframe/core/logging.py
Original file line number Diff line number Diff line change
Expand Up @@ -412,6 +412,19 @@ 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
dmargala marked this conversation as resolved.
Show resolved Hide resolved
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:
Expand All @@ -431,6 +444,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'),
Expand All @@ -447,6 +461,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')

Expand Down Expand Up @@ -485,6 +500,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':
Expand All @@ -496,6 +512,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
Expand Down Expand Up @@ -528,6 +545,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')
Expand Down Expand Up @@ -678,20 +696,6 @@ def emit(self, record):
raise LoggingError('logging failed') from e


_create_handler_registry = {
'file': _create_file_handler,
'filelog': _create_filelog_handler,
'syslog': _create_syslog_handler,
'stream': _create_stream_handler,
'graylog': _create_graylog_handler,
'httpjson': _create_httpjson_handler,
}


def register_plugin_handler(create_plugin_handler):
_create_handler_registry["plugin"] = create_plugin_handler


def _extract_handlers(site_config, handlers_group):
handler_prefix = f'logging/0/{handlers_group}'
handlers_list = site_config.get(handler_prefix)
Expand All @@ -700,15 +704,15 @@ def _extract_handlers(site_config, handlers_group):
handler_type = handler_config['type']

try:
create_handler = _create_handler_registry[handler_type]
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
create_handler = _create_handlers[handler_type]
except KeyError:
# Should not enter here
raise AssertionError(f'unknown handler type: {handler_type}')
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')
Expand Down
5 changes: 1 addition & 4 deletions reframe/schemas/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,7 @@
"handler_common": {
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": ["file", "filelog", "graylog", "stream", "syslog", "httpjson", "plugin"]
},
"type": {"type": "string"},
"level": {"$ref": "#/defs/loglevel"},
"format": {"type": "string"},
"format_perfvars": {"type": "string"},
Expand Down
Loading