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
1 change: 1 addition & 0 deletions Development/cmake/NmosCppTest.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,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
Expand Down
2 changes: 2 additions & 0 deletions Development/nmos-cpp-node/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@
//"logging_level": 0,

// 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;
// "!" excludes messages with no category; when only excluded categories are specified, all other log messages are included
//"logging_categories": ["node_implementation"],

// Configuration settings and defaults for the NMOS APIs
Expand Down
2 changes: 2 additions & 0 deletions Development/nmos-cpp-registry/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
//"logging_level": 0,

// 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;
// "!" excludes messages with no category; when only excluded categories are specified, all other log messages are included
//"logging_categories": ["send_query_ws_events"],

// Configuration settings and defaults for the NMOS APIs
Expand Down
42 changes: 39 additions & 3 deletions Development/nmos/log_gate.h
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
#include <boost/algorithm/string/find_format.hpp>
#include <boost/algorithm/string/finder.hpp>
#include <boost/algorithm/string/formatter.hpp>
#include <boost/algorithm/string/predicate.hpp>
#include <boost/range/algorithm/find.hpp>
#include <boost/range/algorithm/find_if.hpp>
#include "nmos/log_model.h"
Expand Down Expand Up @@ -56,23 +57,58 @@ namespace nmos
protected:
virtual bool pertinent(const std::list<nmos::category>& categories) const
{
// logging_categories setting:
// - omitted: log everything
// - empty list: log nothing
// - only positives: allowlist (log just those; "" includes uncategorized)
// - only negatives ('!' prefixes): blocklist (log everything except those; "!" excludes uncategorized)
// - mix: allowlist of the positives, but a negative match still wins

if (!model.settings.has_field(nmos::fields::logging_categories))
{
return true;
}

const auto& pertinent_categories = nmos::fields::logging_categories(model.settings);

const auto is_negative = [](const web::json::value& category)
{
return boost::starts_with(category.as_string(), U("!"));
};
// true when the list is non-empty and every entry is negative (blocklist mode)
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())
{
// "!" is the negative of "", i.e. excludes messages with no category
static const auto no_category_negative = web::json::value::string(U("!"));
if (pertinent_categories.end() != boost::range::find(pertinent_categories, no_category_negative))
{
return false;
}

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)
{
const auto category_negative = web::json::value::string(utility::s2us("!" + c));
return pertinent_categories.end() != boost::range::find(pertinent_categories, category_negative);
}))
{
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)));
const auto category = web::json::value::string(utility::s2us(c));
return pertinent_categories.end() != boost::range::find(pertinent_categories, category);
});
}

Expand Down
2 changes: 2 additions & 0 deletions Development/nmos/settings.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
// "!" excludes messages with no category; 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
Expand Down
87 changes: 87 additions & 0 deletions Development/nmos/test/log_gate_test.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
// The first "test" is of course whether the header compiles standalone
#include "nmos/log_gate.h"

#include <sstream>
#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<nmos::category> no_categories;
const std::list<nmos::category> access{ "access" };
const std::list<nmos::category> send_query_ws_events{ "send_query_ws_events" };
const std::list<nmos::category> 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));

// a negative match takes precedence over the same category listed positively
model.settings[nmos::fields::logging_categories] = value_of({ U("access"), U("!access") });
BST_REQUIRE(!gate.pertinent(access));
BST_REQUIRE(!gate.pertinent(both));
BST_REQUIRE(!gate.pertinent(no_categories));
BST_REQUIRE(!gate.pertinent(send_query_ws_events));

// "!" excludes messages with no category (negation of "")
model.settings[nmos::fields::logging_categories] = value_of({ U("!") });
BST_REQUIRE(!gate.pertinent(no_categories));
BST_REQUIRE(gate.pertinent(access));
BST_REQUIRE(gate.pertinent(send_query_ws_events));

model.settings[nmos::fields::logging_categories] = value_of({ U("!"), U("!access") });
BST_REQUIRE(!gate.pertinent(no_categories));
BST_REQUIRE(!gate.pertinent(access));
BST_REQUIRE(gate.pertinent(send_query_ws_events));
}
Loading