diff --git a/docs/config_reference.rst b/docs/config_reference.rst index 2aaa042242..4299ca2246 100644 --- a/docs/config_reference.rst +++ b/docs/config_reference.rst @@ -1643,9 +1643,13 @@ The additional properties for the ``httpjson`` handler are the following: :required: No :default: ``{}`` - A set of optional key/value pairs to be sent as HTTP message headers (e.g. API keys). + A set of optional key/value pairs to be sent as HTTP message headers (e.g. Static API keys). These may depend on the server configuration. + .. note:: + If you specify an authorization header here, it will be evaluated at the start of the test session and potentially expire. + Consider using the :attr:`~config.logging.handlers_perflog..httpjson..authorization_header` parameter instead for dynamic authorization headers. + .. versionadded:: 4.2 @@ -1675,7 +1679,7 @@ An example configuration of this handler for performance logging is shown here: 'type': 'httpjson', 'url': 'http://httpjson-server:12345/rfm', 'level': 'info', - 'extra_headers': {'Authorization': 'Token YOUR_API_TOKEN'}, + 'extra_headers': {'key': 'value'}, 'extras': { 'facility': 'reframe', 'data-version': '1.0' @@ -1719,6 +1723,28 @@ This handler transmits the whole log record, meaning that all the information wi .. versionadded:: 4.1 +.. py:attribute:: logging.handlers_perflog..httpjson..authorization_header + + :required: No + :default: :obj:`None` + + A callable to set the authorization header before sending the HTTP request. + + If not specified, no authorization header will be sent unless statically set via :attr:`~config.logging.handlers_perflog..httpjson..extra_headers`. + + .. py:function:: authorization_header() -> str + + :returns: The value of the authorization header, including the scheme. + + .. note:: + Setting the authorization header here will override any static authorization header set via :attr:`~config.logging.handlers_perflog..httpjson..extra_headers`. + + .. note:: + This configuration parameter can only be used in a Python configuration file. + + .. versionadded:: 4.10 + + .. py:attribute:: logging.handlers_perflog..httpjson..backoff_intervals :required: No diff --git a/examples/tutorial/config/cluster_perflogs_httpjson.py b/examples/tutorial/config/cluster_perflogs_httpjson.py index c0725f6cad..179335675f 100644 --- a/examples/tutorial/config/cluster_perflogs_httpjson.py +++ b/examples/tutorial/config/cluster_perflogs_httpjson.py @@ -7,6 +7,10 @@ import os +def _get_authorization_header(): + return 'Bearer YOUR_API_TOKEN' + + def _format_record(record, extras, ignore_keys): data = {} for attr, val in record.__dict__.items(): @@ -108,14 +112,15 @@ def _format_record(record, extras, ignore_keys): 'url': 'https://httpjson-server:12345/rfm', 'level': 'info', 'debug': True, - 'extra_headers': {'Authorization': 'Token YOUR_API_TOKEN'}, + 'extra_headers': {'Key0': 'Value0', 'Key1': 'Value1'}, 'extras': { 'facility': 'reframe', 'data-version': '1.0' }, 'ignore_keys': ['check_perfvalues'], 'json_formatter': (_format_record - if os.getenv('CUSTOM_JSON') else None) + if os.getenv('CUSTOM_JSON') else None), + 'authorization_header': _get_authorization_header() } ] } diff --git a/reframe/core/logging.py b/reframe/core/logging.py index bf1e1d6c48..adff012f5a 100644 --- a/reframe/core/logging.py +++ b/reframe/core/logging.py @@ -596,6 +596,9 @@ def _create_httpjson_handler(site_config, config_prefix): extras = site_config.get(f'{config_prefix}/extras') ignore_keys = site_config.get(f'{config_prefix}/ignore_keys') json_formatter = site_config.get(f'{config_prefix}/json_formatter') + authorization_header = site_config.get( + f'{config_prefix}/authorization_header' + ) extra_headers = site_config.get(f'{config_prefix}/extra_headers') debug = site_config.get(f'{config_prefix}/debug') backoff_intervals = site_config.get(f'{config_prefix}/backoff_intervals') @@ -641,8 +644,8 @@ def _create_httpjson_handler(site_config, config_prefix): 'no data will be sent to the server') return HTTPJSONHandler(url, extras, ignore_keys, json_formatter, - extra_headers, debug, backoff_intervals, - retry_timeout) + authorization_header, extra_headers, debug, + backoff_intervals, retry_timeout) def _record_to_json(record, extras, ignore_keys): @@ -692,7 +695,8 @@ class HTTPJSONHandler(logging.Handler): } def __init__(self, url, extras=None, ignore_keys=None, - json_formatter=None, extra_headers=None, + json_formatter=None, + authorization_header=None, extra_headers=None, debug=False, backoff_intervals=(1, 2, 3), retry_timeout=0): super().__init__() self._url = url @@ -707,10 +711,18 @@ def __init__(self, url, extras=None, ignore_keys=None, if not is_trivially_callable(self._json_format, non_def_args=3): raise ConfigError( - "httpjson: 'json_formatter' has not the right signature: " + "httpjson: 'json_formatter' has the wrong signature: " "it must be 'json_formatter(record, extras, ignore_keys)'" ) + if not is_trivially_callable(authorization_header): + raise ConfigError( + "httpjson: 'authorization_header' has the wrong signature: " + "it must be 'authorization_header()'" + ) + + self._authorization_header = authorization_header + self._headers = {'Content-type': 'application/json', 'Accept-Charset': 'UTF-8'} if extra_headers: @@ -737,7 +749,11 @@ def emit(self, record): return + if self._authorization_header is not None: + self._headers['Authorization'] = self._authorization_header() + timeout_time = time.time() + self._timeout + try: backoff_intervals = itertools.cycle(self._backoff_intervals) while True: diff --git a/reframe/schemas/config.json b/reframe/schemas/config.json index 255acf948a..fc774f8b69 100644 --- a/reframe/schemas/config.json +++ b/reframe/schemas/config.json @@ -175,6 +175,7 @@ "items": {"type": "string"} }, "json_formatter": {}, + "authorization_header": {}, "extra_headers": {"type": "object"}, "debug": {"type": "boolean"}, "backoff_intervals": { @@ -660,6 +661,7 @@ "logging/handlers_perflog/httpjson_extras": {}, "logging/handlers_perflog/httpjson_ignore_keys": [], "logging/handlers_perflog/httpjson_json_formatter": null, + "logging/handlers_perflog/httpjson_authorization_header": null, "logging/handlers_perflog/httpjson_extra_headers": {}, "logging/handlers_perflog/httpjson_debug": false, "logging/handlers_perflog/httpjson_backoff_intervals": [0.1, 0.2, 0.4, 0.8, 1.6, 3.2], diff --git a/reframe/utility/__init__.py b/reframe/utility/__init__.py index 13a1f52abb..59fc08bef7 100644 --- a/reframe/utility/__init__.py +++ b/reframe/utility/__init__.py @@ -456,7 +456,7 @@ def is_trivially_callable(fn, *, non_def_args=0): ''' if not callable(fn): - raise TypeError('argument is not a callable') + return False explicit_args = [p for p in inspect.signature(fn).parameters.values() if p.default is p.empty] diff --git a/unittests/test_logging.py b/unittests/test_logging.py index b67e4f2dd1..c217aa951f 100644 --- a/unittests/test_logging.py +++ b/unittests/test_logging.py @@ -556,3 +556,48 @@ def test_httpjson_handler_no_port(make_exec_ctx, config_file, }) ) rlog.configure_logging(rt.runtime().site_config) + + +def test_httpjson_auth_not_callable_error(): + with pytest.raises(ConfigError, + match=r'authorization_header.*has the wrong signature'): + rlog.HTTPJSONHandler(url='http://xyz/rfm', + authorization_header='NOT CALLABLE') + + +@pytest.fixture +def record_with_check_tags(): + record = logging.LogRecord('reframe', rlog.INFO, '', + 0, 'test message', None, None) + + record.check_tags = {} + return record + + +@pytest.fixture +def mock_requests_post_200(monkeypatch): + def mock_post(url, **kwargs): + return type('Response', (object,), {'status_code': 200, 'ok': True})() + + monkeypatch.setattr(rlog.requests, 'post', mock_post) + + +@pytest.fixture +def httpjson_handler(): + def mock_get_token(): + return 'Bearer mocked_token' + + handler = rlog.HTTPJSONHandler( + url='http://xyz/rfm', authorization_header=mock_get_token) + + yield handler + handler.close() + + +def test_httpjson_auth_header_set(httpjson_handler, + record_with_check_tags, + mock_requests_post_200): + + httpjson_handler.emit(record_with_check_tags) + assert httpjson_handler._authorization_header is not None + assert httpjson_handler._headers['Authorization'] == 'Bearer mocked_token' diff --git a/unittests/test_utility.py b/unittests/test_utility.py index 2ffc733810..a16b62f566 100644 --- a/unittests/test_utility.py +++ b/unittests/test_utility.py @@ -2016,8 +2016,7 @@ def bar(x, y): assert util.is_trivially_callable(foo) assert util.is_trivially_callable(bar, non_def_args=2) - with pytest.raises(TypeError): - util.is_trivially_callable(1) + assert not util.is_trivially_callable(1) def test_nodelist_utilities():