Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 28 additions & 2 deletions docs/config_reference.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -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'
Expand Down Expand Up @@ -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
Expand Down
9 changes: 7 additions & 2 deletions examples/tutorial/config/cluster_perflogs_httpjson.py
Original file line number Diff line number Diff line change
Expand Up @@ -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():
Expand Down Expand Up @@ -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()
}
]
}
Expand Down
24 changes: 20 additions & 4 deletions reframe/core/logging.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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
Expand All @@ -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:
Expand All @@ -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:
Expand Down
2 changes: 2 additions & 0 deletions reframe/schemas/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,7 @@
"items": {"type": "string"}
},
"json_formatter": {},
"authorization_header": {},
"extra_headers": {"type": "object"},
"debug": {"type": "boolean"},
"backoff_intervals": {
Expand Down Expand Up @@ -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],
Expand Down
2 changes: 1 addition & 1 deletion reframe/utility/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
45 changes: 45 additions & 0 deletions unittests/test_logging.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'
3 changes: 1 addition & 2 deletions unittests/test_utility.py
Original file line number Diff line number Diff line change
Expand Up @@ -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():
Expand Down
Loading