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
177 changes: 177 additions & 0 deletions ASWLog/ASWLog_CategoryLog.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,12 @@ limitations under the License.
#include "ASWLog_CategoryLog.h"
//---------------------------------------------------------------------------
// System includes here
#include <algorithm>
#include <format>
#include <mutex>
#include <vector>
//---------------------------------------------------------------------------
#include "ASWLog_Utils.h"
#include "ASWLog_Version.h"
//---------------------------------------------------------------------------

Expand All @@ -36,10 +40,71 @@ namespace ASWLog
namespace
{

// A level TASWCategoryLog::ApplyLevels() gives the categories its name covers
struct TLevelRule
{
std::string Name; // "*", or a category's name
Level MinimumLevel = Level::Info;
};

// The categories that exist, and the rules TASWCategoryLog::ApplyLevels() set last
struct TCategoryRegistry
{
std::mutex Mutex;
std::vector<TASWCategoryLog*> Categories;
std::vector<TLevelRule> Rules;
};

//---------------------------------------------------------------------------

const TLevelRule* FindLevelRule(const std::vector<TLevelRule>& rules, std::string_view name) noexcept;
TCategoryRegistry& GetCategoryRegistry();
std::string MakeCategoryName(std::string_view name, const IASWLog& log);
bool ParseLevelRules(std::string_view spec, std::vector<TLevelRule>& rules, std::string& error);

//---------------------------------------------------------------------------

/*
FindLevelRule

The rule of 'rules' with the longest name that covers the category 'name' (its own name, the name of a category
above it, or "*"), or null if none covers it
*/
const TLevelRule* FindLevelRule(const std::vector<TLevelRule>& rules, std::string_view name) noexcept
{
const TLevelRule* found = nullptr;
std::size_t foundLength = 0;

for (const auto& rule : rules)
{
const bool isAll = rule.Name == "*";
const std::size_t length = isAll ? 0 : rule.Name.size();
const bool covers = isAll || (name.size() >= length && Detail::EqualsIgnoringCase(name.substr(0, length), rule.Name) &&
(name.size() == length || name[length] == '.'));

if (covers && (found == nullptr || length > foundLength))
{
found = &rule;
foundLength = length;
}
}

return found;
}

/*
GetCategoryRegistry

The process's category registry. Never destroyed, so categories destroyed at exit (e.g. static ones in other files)
can still leave it.
*/
TCategoryRegistry& GetCategoryRegistry()
{
static auto* const registry = new TCategoryRegistry();

return *registry;
}

/*
MakeCategoryName

Expand All @@ -57,6 +122,69 @@ std::string MakeCategoryName(std::string_view name, const IASWLog& log)
return fullName;
}

/*
ParseLevelRules

Adds the "name=level" items of 'spec' to 'rules' (see TASWCategoryLog::ApplyLevels()), a name given again replacing
its earlier level. Returns false at the first invalid item, describing it in 'error'.
*/
bool ParseLevelRules(std::string_view spec, std::vector<TLevelRule>& rules, std::string& error)
{
std::size_t start = 0;

while (start <= spec.size())
{
const auto comma = spec.find(',', start);
const auto end = comma == std::string_view::npos ? spec.size() : comma;
const auto item = Detail::TrimSpaces(spec.substr(start, end - start));
start = end + 1;

if (item.empty())
continue;

const auto equals = item.find('=');
if (equals == std::string_view::npos)
{
error = std::format("'{}' has no '=' (expected name=level)", item);
return false;
}

const auto name = Detail::TrimSpaces(item.substr(0, equals));
const auto levelText = Detail::TrimSpaces(item.substr(equals + 1));

if (name.empty())
{
error = std::format("'{}' has no category name", item);
return false;
}

if (name != "*" && name.find('*') != std::string_view::npos)
{
error = std::format("'{}': a name can't hold '*' (a name covers the categories under it; \"*\" covers all)", item);
return false;
}

const auto level = Level_FromString(levelText);
if (!level)
{
error = std::format("'{}': '{}' isn't a level (Trace, Debug, Info, Warn, Error, Critical or Off)", item, levelText);
return false;
}

const auto existing = std::find_if(rules.begin(), rules.end(), [name](const TLevelRule& rule)
{
return Detail::EqualsIgnoringCase(rule.Name, name);
});

if (existing != rules.end())
existing->MinimumLevel = *level;
else
rules.push_back(TLevelRule{ std::string(name), *level });
}

return true;
}

} // namespace

//---------------------------------------------------------------------------
Expand All @@ -70,6 +198,55 @@ TASWCategoryLog::TASWCategoryLog(std::string_view name, IASWLog& log)
: m_Log(log),
m_Name(MakeCategoryName(name, log))
{
auto& registry = GetCategoryRegistry();
std::lock_guard<std::mutex> lock(registry.Mutex);

registry.Categories.push_back(this);

if (const auto* rule = FindLevelRule(registry.Rules, m_Name))
m_Level.store(static_cast<std::uint8_t>(rule->MinimumLevel), std::memory_order_relaxed);
}

//---------------------------------------------------------------------------
TASWCategoryLog::~TASWCategoryLog()
{
auto& registry = GetCategoryRegistry();
std::lock_guard<std::mutex> lock(registry.Mutex);

std::erase(registry.Categories, this);
}

//---------------------------------------------------------------------------
bool TASWCategoryLog::ApplyLevels(std::string_view spec)
{
std::string error;

return ApplyLevels(spec, error);
}

//---------------------------------------------------------------------------
bool TASWCategoryLog::ApplyLevels(std::string_view spec, std::string& error)
{
error.clear();

std::vector<TLevelRule> rules;
if (!ParseLevelRules(spec, rules, error))
return false;

auto& registry = GetCategoryRegistry();
std::lock_guard<std::mutex> lock(registry.Mutex);

registry.Rules = std::move(rules);

for (auto* category : registry.Categories)
{
// Not through SetMinimumLevel(), a virtual call: a category of a derived class may be in its destructor,
// waiting for the lock to leave the registry
if (const auto* rule = FindLevelRule(registry.Rules, category->m_Name))
category->m_Level.store(static_cast<std::uint8_t>(rule->MinimumLevel), std::memory_order_relaxed);
}

return true;
}

//---------------------------------------------------------------------------
Expand Down
28 changes: 22 additions & 6 deletions ASWLog/ASWLog_CategoryLog.h
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,12 @@ namespace ASWLog
// a log the rest of the application uses. SetEnabled(false) silences only this category (and the categories wrapping
// it); the wrapped logger must be enabled too.
//
// Lock-free and thread-safe. The wrapped logger must outlive the category, and must not be a multi-log that holds the
// category. A logger that keeps an entry beyond the call (e.g. in its backtrace) copies the category's name, so the
// category may be destroyed meanwhile.
// The levels of several categories can also be set by name, e.g. from an environment variable, with ApplyLevels() (see
// TASWLogConfig::ApplyEnvironment()).
//
// Lock-free and thread-safe (making or destroying a category takes a process-wide lock, for ApplyLevels()). The wrapped
// logger must outlive the category, and must not be a multi-log that holds the category. A logger that keeps an entry
// beyond the call (e.g. in its backtrace) copies the category's name, so the category may be destroyed meanwhile.
/////////////////////////////////////////////////////////////////////////////
class TASWCategoryLog : public IASWLog
{
Expand All @@ -86,10 +89,23 @@ class TASWCategoryLog : public IASWLog
TASWLogRecord WithCategory(const TASWLogRecord& record) const noexcept;

public:
// Shows 'name' as is, or, if 'log' is a category, after that category's name and a '.'. Throws std::bad_alloc if
// the name can't be copied.
// Sets the minimum level of the categories 'spec' covers, process-wide: of those that exist now, and of those made
// until the next call. 'spec' is a list of "name=level" items separated by commas, e.g. "*=Info,Net=Debug,
// Net.Http=Trace": a name covers the category of that name and those under it (Net covers Net and Net.Http, but
// not Network), "*" covers every category, and the longest name that covers a category gives its level. Names and
// levels ignore case and the spaces around them; the levels are those of Level_FromString(). The categories it
// doesn't cover keep their level, and a later SetMinimumLevel() or ResetMinimumLevel() changes a category's level
// as usual. An empty spec covers none. Returns false and changes nothing if an item is invalid (no '=', no name, a
// '*' in a name other than "*", or an unknown level), describing it in 'error'. Throws std::bad_alloc if out of
// memory.
static bool ApplyLevels(std::string_view spec);
static bool ApplyLevels(std::string_view spec, std::string& error);

public:
// Shows 'name' as is, or, if 'log' is a category, after that category's name and a '.'. Takes its level from the
// levels ApplyLevels() set last, if they cover it. Throws std::bad_alloc if out of memory.
TASWCategoryLog(std::string_view name, IASWLog& log);
~TASWCategoryLog() override = default;
~TASWCategoryLog() override;

TASWCategoryLog(const TASWCategoryLog&) = delete;
TASWCategoryLog& operator=(const TASWCategoryLog&) = delete;
Expand Down
Loading
Loading