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
60 changes: 60 additions & 0 deletions docs/howto.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
---------------------------------

dmargala marked this conversation as resolved.
Show resolved Hide resolved
.. 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 <reframe.core.logging.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',
},
...
]
}
],
...
}
59 changes: 30 additions & 29 deletions reframe/core/logging.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
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 +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'),
Expand All @@ -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')

Expand Down Expand Up @@ -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':
Expand All @@ -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
Expand Down Expand Up @@ -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')
Expand Down Expand Up @@ -684,35 +703,17 @@ 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')
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"]
},
"type": {"type": "string"},
"level": {"$ref": "#/defs/loglevel"},
"format": {"type": "string"},
"format_perfvars": {"type": "string"},
Expand Down
Loading