From 4ee37b51927d2a22888b2d1d1d20d8bcfaa83886 Mon Sep 17 00:00:00 2001 From: ZayanKhan-12 <108294002+ZayanKhan-12@users.noreply.github.com> Date: Sat, 25 Jul 2026 05:38:11 -0400 Subject: [PATCH] Add support for negative logging categories in the log gateway Categories prefixed with '!' in the logging_categories setting specify messages that should not be written to the error log. A negative match takes precedence over any positive match, and when only negative categories are specified, all other messages remain pertinent, so e.g. ["!access"] logs everything except access messages. Behaviour is unchanged when logging_categories is omitted (everything is logged), empty (nothing is logged), or contains only positive categories. Closes #338 Co-Authored-By: Claude Fable 5 --- Development/cmake/NmosCppTest.cmake | 1 + Development/nmos/log_gate.h | 26 +++++++++- Development/nmos/settings.h | 2 + Development/nmos/test/log_gate_test.cpp | 69 +++++++++++++++++++++++++ 4 files changed, 96 insertions(+), 2 deletions(-) create mode 100644 Development/nmos/test/log_gate_test.cpp diff --git a/Development/cmake/NmosCppTest.cmake b/Development/cmake/NmosCppTest.cmake index 4a589c51f..b0d02b9ad 100644 --- a/Development/cmake/NmosCppTest.cmake +++ b/Development/cmake/NmosCppTest.cmake @@ -56,6 +56,7 @@ set(NMOS_CPP_TEST_NMOS_TEST_SOURCES nmos/test/json_validator_test.cpp nmos/test/jwt_generator_test.cpp nmos/test/jwt_validation_test.cpp + nmos/test/log_gate_test.cpp nmos/test/mdns_test.cpp nmos/test/node_interfaces_test.cpp nmos/test/paging_utils_test.cpp diff --git a/Development/nmos/log_gate.h b/Development/nmos/log_gate.h index 08a3669b5..bd9ac9f32 100644 --- a/Development/nmos/log_gate.h +++ b/Development/nmos/log_gate.h @@ -63,14 +63,36 @@ namespace nmos const auto& pertinent_categories = nmos::fields::logging_categories(model.settings); + // categories prefixed with '!' specify messages that should not be logged; + // a negative match takes precedence over any positive match, and when only + // negative categories are specified, all other messages are pertinent + const auto is_negative = [](const web::json::value& category) + { + const auto& s = category.as_string(); + return !s.empty() && U('!') == s.front(); + }; + const bool default_pertinent = 0 != pertinent_categories.size() + && pertinent_categories.end() == boost::range::find_if(pertinent_categories, [&](const web::json::value& category) + { + return !is_negative(category); + }); + if (categories.empty()) { static const auto no_category = web::json::value::string(utility::string_t()); - return pertinent_categories.end() != boost::range::find(pertinent_categories, no_category); + return default_pertinent || pertinent_categories.end() != boost::range::find(pertinent_categories, no_category); } // this could be made more efficient if there may be many pertinent categories - return categories.end() != boost::range::find_if(categories, [&](const nmos::category& c) + if (categories.end() != boost::range::find_if(categories, [&](const nmos::category& c) + { + return pertinent_categories.end() != boost::range::find(pertinent_categories, web::json::value::string(utility::s2us("!" + c))); + })) + { + return false; + } + + return default_pertinent || categories.end() != boost::range::find_if(categories, [&](const nmos::category& c) { return pertinent_categories.end() != boost::range::find(pertinent_categories, web::json::value::string(utility::s2us(c))); }); diff --git a/Development/nmos/settings.h b/Development/nmos/settings.h index 008461129..e4dd8d25b 100644 --- a/Development/nmos/settings.h +++ b/Development/nmos/settings.h @@ -100,6 +100,8 @@ namespace nmos const web::json::field_as_integer_or logging_level{ U("logging_level"), 0 }; // 0, rather than slog::severities::info or slog::nil_severity, just to avoid a #include // logging_categories [registry, node]: array of logging categories to be included in the error log + // categories prefixed with '!' are excluded, even if another category matches positively; + // when only excluded categories are specified, all other log messages are included const web::json::field_as_array logging_categories{ U("logging_categories") }; // when omitted, all log messages are included // Configuration settings and defaults for the NMOS APIs diff --git a/Development/nmos/test/log_gate_test.cpp b/Development/nmos/test/log_gate_test.cpp new file mode 100644 index 000000000..004ab844a --- /dev/null +++ b/Development/nmos/test/log_gate_test.cpp @@ -0,0 +1,69 @@ +// The first "test" is of course whether the header compiles standalone +#include "nmos/log_gate.h" + +#include +#include "bst/test/test.h" + +namespace +{ + struct test_gate : nmos::experimental::log_gate + { + test_gate(std::ostream& error_log, std::ostream& access_log, nmos::experimental::log_model& model) + : nmos::experimental::log_gate(error_log, access_log, model) {} + using nmos::experimental::log_gate::pertinent; + }; +} + +//////////////////////////////////////////////////////////////////////////////////////////// +BST_TEST_CASE(testLogGatePertinentCategories) +{ + using web::json::value_of; + + std::ostringstream error_log; + std::ostringstream access_log; + nmos::experimental::log_model model; + test_gate gate(error_log, access_log, model); + + const std::list no_categories; + const std::list access{ "access" }; + const std::list send_query_ws_events{ "send_query_ws_events" }; + const std::list both{ "send_query_ws_events", "access" }; + + // when logging_categories is omitted, all messages are pertinent + BST_REQUIRE(gate.pertinent(no_categories)); + BST_REQUIRE(gate.pertinent(access)); + BST_REQUIRE(gate.pertinent(both)); + + // when logging_categories is empty, no messages are pertinent + model.settings[nmos::fields::logging_categories] = web::json::value::array(); + BST_REQUIRE(!gate.pertinent(no_categories)); + BST_REQUIRE(!gate.pertinent(access)); + BST_REQUIRE(!gate.pertinent(both)); + + // positive categories select the messages to be logged + model.settings[nmos::fields::logging_categories] = value_of({ U("send_query_ws_events") }); + BST_REQUIRE(!gate.pertinent(no_categories)); + BST_REQUIRE(gate.pertinent(send_query_ws_events)); + BST_REQUIRE(!gate.pertinent(access)); + BST_REQUIRE(gate.pertinent(both)); + + // the empty string selects messages with no category + model.settings[nmos::fields::logging_categories] = value_of({ U("") }); + BST_REQUIRE(gate.pertinent(no_categories)); + BST_REQUIRE(!gate.pertinent(access)); + + // a category prefixed with '!' excludes matching messages, even if another + // category matches positively + model.settings[nmos::fields::logging_categories] = value_of({ U("send_query_ws_events"), U("!access") }); + BST_REQUIRE(gate.pertinent(send_query_ws_events)); + BST_REQUIRE(!gate.pertinent(access)); + BST_REQUIRE(!gate.pertinent(both)); + BST_REQUIRE(!gate.pertinent(no_categories)); + + // when only excluded categories are specified, all other messages are pertinent + model.settings[nmos::fields::logging_categories] = value_of({ U("!access") }); + BST_REQUIRE(gate.pertinent(no_categories)); + BST_REQUIRE(gate.pertinent(send_query_ws_events)); + BST_REQUIRE(!gate.pertinent(access)); + BST_REQUIRE(!gate.pertinent(both)); +}