diff --git a/.github/workflows/build-test-release.yml b/.github/workflows/build-test-release.yml index 81cbe2c5..5d5bf276 100644 --- a/.github/workflows/build-test-release.yml +++ b/.github/workflows/build-test-release.yml @@ -18,7 +18,7 @@ jobs: steps: - uses: actions/checkout@v4 - id: matrix - uses: splunk/addonfactory-test-matrix-action@v1 + uses: splunk/addonfactory-test-matrix-action@v2 fossa-scan: continue-on-error: true @@ -47,7 +47,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: apache/skywalking-eyes@v0.5.0 + - uses: apache/skywalking-eyes@v0.6.0 pre-commit: runs-on: ubuntu-latest diff --git a/poetry.lock b/poetry.lock index fed5519c..01f2014a 100644 --- a/poetry.lock +++ b/poetry.lock @@ -30,13 +30,13 @@ files = [ [[package]] name = "certifi" -version = "2024.2.2" +version = "2024.6.2" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.6" files = [ - {file = "certifi-2024.2.2-py3-none-any.whl", hash = "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1"}, - {file = "certifi-2024.2.2.tar.gz", hash = "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f"}, + {file = "certifi-2024.6.2-py3-none-any.whl", hash = "sha256:ddc6c8ce995e6987e7faf5e3f1b02b302836a0e5d98ece18392cb1a36c72ad56"}, + {file = "certifi-2024.6.2.tar.gz", hash = "sha256:3cd43f1c6fa7dedc5899d69d3ad0398fd018ad1a17fba83ddaf78aa46c747516"}, ] [[package]] @@ -175,15 +175,29 @@ files = [ {file = "defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69"}, ] +[[package]] +name = "deprecation" +version = "2.1.0" +description = "A library to handle automated deprecations" +optional = false +python-versions = "*" +files = [ + {file = "deprecation-2.1.0-py2.py3-none-any.whl", hash = "sha256:a10811591210e1fb0e768a8c25517cabeabcba6f0bf96564f8ff45189f90b14a"}, + {file = "deprecation-2.1.0.tar.gz", hash = "sha256:72b3bde64e5d778694b0cf68178aed03d15e15477116add3fb773e581f9518ff"}, +] + +[package.dependencies] +packaging = "*" + [[package]] name = "exceptiongroup" -version = "1.2.0" +version = "1.2.1" description = "Backport of PEP 654 (exception groups)" optional = false python-versions = ">=3.7" files = [ - {file = "exceptiongroup-1.2.0-py3-none-any.whl", hash = "sha256:4bfd3996ac73b41e9b9628b04e079f193850720ea5945fc96a08633c66912f14"}, - {file = "exceptiongroup-1.2.0.tar.gz", hash = "sha256:91f5c769735f051a4290d52edd0858999b57e5876e9f85937691bd4c9fa3ed68"}, + {file = "exceptiongroup-1.2.1-py3-none-any.whl", hash = "sha256:5258b9ed329c5bbdd31a309f53cbfb0b155341807f6ff7606a1e801a891b29ad"}, + {file = "exceptiongroup-1.2.1.tar.gz", hash = "sha256:a4785e48b045528f5bfe627b6ad554ff32def154f42372786903b7abcfe1aa16"}, ] [package.extras] @@ -223,13 +237,13 @@ colorama = ">=0.4" [[package]] name = "idna" -version = "3.6" +version = "3.7" description = "Internationalized Domain Names in Applications (IDNA)" optional = false python-versions = ">=3.5" files = [ - {file = "idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f"}, - {file = "idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca"}, + {file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"}, + {file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"}, ] [[package]] @@ -265,13 +279,13 @@ files = [ [[package]] name = "jinja2" -version = "3.1.3" +version = "3.1.4" description = "A very fast and expressive template engine." optional = false python-versions = ">=3.7" files = [ - {file = "Jinja2-3.1.3-py3-none-any.whl", hash = "sha256:7d6d50dd97d52cbc355597bd845fabfbac3f551e1f99619e39a35ce8c370b5fa"}, - {file = "Jinja2-3.1.3.tar.gz", hash = "sha256:ac8bd6544d4bb2c9792bf3a159e80bba8fda7f07e81bc3aed565432d5925ba90"}, + {file = "jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d"}, + {file = "jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369"}, ] [package.dependencies] @@ -867,14 +881,17 @@ files = [ [[package]] name = "splunk-sdk" -version = "1.7.4" +version = "2.0.1" description = "The Splunk Software Development Kit for Python." optional = false python-versions = "*" files = [ - {file = "splunk-sdk-1.7.4.tar.gz", hash = "sha256:8f3f149e3a0daf7526ed36882c109e4ec8080e417efe25d23f4578e86d38b9f2"}, + {file = "splunk-sdk-2.0.1.tar.gz", hash = "sha256:a1cc9b24e0c9c79ef8e2845fedcca066638219eef0018163f97795dbfa367c67"}, ] +[package.dependencies] +deprecation = "*" + [[package]] name = "tomli" version = "2.0.1" diff --git a/pyproject.toml b/pyproject.toml index 6252affc..790d59f8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,7 +16,7 @@ [tool.poetry] name = "solnlib" -version = "4.13.0-beta.2" +version = "4.14.1" description = "The Splunk Software Development Kit for Splunk Solutions" authors = ["Splunk "] license = "Apache-2.0" diff --git a/solnlib/__init__.py b/solnlib/__init__.py index 620cfeda..753ae50f 100644 --- a/solnlib/__init__.py +++ b/solnlib/__init__.py @@ -54,4 +54,4 @@ "utils", ] -__version__ = "4.13.0-beta.2" +__version__ = "4.14.1" diff --git a/solnlib/conf_manager.py b/solnlib/conf_manager.py index b10f00c4..141e871e 100644 --- a/solnlib/conf_manager.py +++ b/solnlib/conf_manager.py @@ -506,6 +506,7 @@ def get_log_level( session_key: str, app_name: str, conf_name: str, + log_stanza: str = "logging", log_level_field: str = "loglevel", default_log_level: str = "INFO", ) -> str: @@ -517,6 +518,7 @@ def get_log_level( session_key: Splunk access token. app_name: Add-on name. conf_name: Configuration file name where logging stanza is. + log_stanza: Logging stanza to define `log_level_field` and its value. log_level_field: Logging level field name under logging stanza. default_log_level: Default log level to return in case of errors. @@ -547,7 +549,7 @@ def get_log_level( ) return default_log_level try: - logging_details = conf.get("logging") + logging_details = conf.get(log_stanza) return logging_details.get(log_level_field, default_log_level) except ConfStanzaNotExistException: logger.error( diff --git a/solnlib/log.py b/solnlib/log.py index 0d624c9b..138ea328 100644 --- a/solnlib/log.py +++ b/solnlib/log.py @@ -252,18 +252,52 @@ def modular_input_end(logger: logging.Logger, modular_input_name: str): def events_ingested( - logger: logging.Logger, modular_input_name: str, sourcetype: str, n_events: int + logger: logging.Logger, + modular_input_name: str, + sourcetype: str, + n_events: int, + index: str, + account: str = None, + host: str = None, ): - """Specific function to log the number of events ingested.""" - log_event( - logger, - { - "action": "events_ingested", - "modular_input_name": modular_input_name, - "sourcetype_ingested": sourcetype, - "n_events": n_events, - }, - ) + """Specific function to log the basic information of events ingested for + the monitoring dashboard. + + Arguments: + logger: Add-on logger. + modular_input_name: Full name of the modular input. It needs to be in a format ://. + In case of invalid format ValueError is raised. + sourcetype: Source type used to write event. + n_events: Number of ingested events. + index: Index used to write event. + account: Account used to write event. (optional) + host: Host used to write event. (optional) + """ + + if "://" in modular_input_name: + input_name = modular_input_name.split("/")[-1] + else: + raise ValueError( + f"Invalid modular input name: {modular_input_name}. " + f"It should be in format ://" + ) + + result = { + "action": "events_ingested", + "modular_input_name": modular_input_name, + "sourcetype_ingested": sourcetype, + "n_events": n_events, + "event_input": input_name, + "event_index": index, + } + + if account: + result["event_account"] = account + + if host: + result["event_host"] = host + + log_event(logger, result) def log_exception( diff --git a/tests/unit/test_conf_manager.py b/tests/unit/test_conf_manager.py index 15f5533d..bb06ca9c 100644 --- a/tests/unit/test_conf_manager.py +++ b/tests/unit/test_conf_manager.py @@ -32,3 +32,60 @@ def test_get_log_level_when_error_getting_conf(mock_conf_manager_class): ) assert expected_log_level == log_level + + +@mock.patch.object(conf_manager, "ConfManager") +def test_get_log_level_with_custom_values(mock_conf_manager_class): + mock_conf_manager = mock_conf_manager_class.return_value + mock_conf_manager.get_conf.return_value = {"my_logger": {"my_field": "DEBUG"}} + expected_log_level = "DEBUG" + + log_level = conf_manager.get_log_level( + logger=mock.MagicMock(), + session_key="session_key", + app_name="app_name", + conf_name="conf_name", + log_stanza="my_logger", + log_level_field="my_field", + ) + + assert log_level == expected_log_level + + +@mock.patch.object(conf_manager, "ConfManager") +def test_get_log_level_with_no_logging_stanza(mock_conf_manager_class): + mock_conf_manager = mock_conf_manager_class.return_value + mock_conf_manager.get_conf.return_value = mock.MagicMock() + mock_conf_manager.get_conf.return_value.get.side_effect = ( + conf_manager.ConfStanzaNotExistException + ) + logger = mock.MagicMock() + expected_log_level = "INFO" + + log_level = conf_manager.get_log_level( + logger=logger, + session_key="session_key", + app_name="app_name", + conf_name="conf_name", + log_stanza="my_logger", + log_level_field="my_field", + ) + + assert log_level == expected_log_level + assert logger.error.call_count == 1 + + +@mock.patch.object(conf_manager, "ConfManager") +def test_get_log_level_with_default_fields(mock_conf_manager_class): + mock_conf_manager = mock_conf_manager_class.return_value + mock_conf_manager.get_conf.return_value = {"logging": {"loglevel": "WARN"}} + expected_log_level = "WARN" + + log_level = conf_manager.get_log_level( + logger=mock.MagicMock(), + session_key="session_key", + app_name="app_name", + conf_name="conf_name", + ) + + assert log_level == expected_log_level diff --git a/tests/unit/test_log.py b/tests/unit/test_log.py index d19a14f0..2dbc5bc4 100644 --- a/tests/unit/test_log.py +++ b/tests/unit/test_log.py @@ -22,6 +22,7 @@ import threading import traceback import time +import pytest from unittest import mock from solnlib import log @@ -193,14 +194,52 @@ def test_modular_input_end(): def test_events_ingested(): with mock.patch("logging.Logger") as mock_logger: - log.events_ingested(mock_logger, "modular_input_name", "sourcetype", 5) + log.events_ingested( + mock_logger, "input_type://input_name", "sourcetype", 5, "default" + ) + + mock_logger.log.assert_called_once_with( + logging.INFO, + "action=events_ingested modular_input_name=input_type://input_name sourcetype_ingested=sourcetype " + "n_events=5 event_input=input_name event_index=default", + ) + + with mock.patch("logging.Logger") as mock_logger: + log.events_ingested( + mock_logger, + "demo://modular_input_name", + "sourcetype", + 5, + "default", + host="abcd", + account="test_acc", + ) mock_logger.log.assert_called_once_with( logging.INFO, - "action=events_ingested modular_input_name=modular_input_name sourcetype_ingested=sourcetype n_events=5", + "action=events_ingested modular_input_name=demo://modular_input_name sourcetype_ingested=sourcetype n_" + "events=5 event_input=modular_input_name event_index=default event_account=test_acc event_host=abcd", ) +def test_events_ingested_invalid_input(): + exp_msg = "Invalid modular input name: modular_input_name. It should be in format ://" + + with pytest.raises(ValueError) as excinfo: + with mock.patch("logging.Logger") as mock_logger: + log.events_ingested( + mock_logger, + "modular_input_name", + "sourcetype", + 5, + "default", + host="abcd", + account="test_acc", + ) + + assert exp_msg == str(excinfo.value) + + def test_log_exceptions_full_msg(): start_msg = "some msg before exception" with mock.patch("logging.Logger") as mock_logger: