I am creating a logging library in C++, and I wanted your opinion on code quality and performance within some methods.
You may ask why yet another logger - the answer is simple. This logging library that I am developing is not exclusively said to be released, it's purely for my own experience and fun. But if it turns out to be atleast decent, why not.
Here is the code
lwlog.h
#pragma once
#include <iostream>
#include <string>
#include <cctype>
#include "core.h"
#include "detail.h"
#include "datetime.h"
#include "registry.h"
namespace lwlog
{
enum class log_level
{
all = (1 << 0),
info = (1 << 1),
warning = (1 << 2),
error = (1 << 3),
critical = (1 << 4),
debug = (1 << 5),
none = (1 << 6)
};
static log_level operator |(log_level lhs, log_level rhs)
{
return static_cast<log_level> (
static_cast<std::underlying_type<log_level>::type>(lhs)
| static_cast<std::underlying_type<log_level>::type>(rhs)
);
}
static log_level operator &(log_level lhs, log_level rhs)
{
return static_cast<log_level> (
static_cast<std::underlying_type<log_level>::type>(lhs)
& static_cast<std::underlying_type<log_level>::type>(rhs)
);
}
class LWLOG logger
{
private:
std::string m_message;
std::string m_loggerName;
std::string m_pattern;
std::string m_logLevel;
log_level m_logLevel_visibility = log_level::all;
std::unordered_map<std::string, std::string> m_patterns_data;
private:
void print_formatted(const std::string& message, std::string pattern);
public:
explicit logger(const std::string& name);
~logger();
void set_name(const std::string& loggerName);
void set_logLevel_visibility(log_level logLevel);
void set_pattern(const std::string& pattern);
void info(const std::string& message);
void warning(const std::string& message);
void error(const std::string& message);
void critical(const std::string& message);
void debug(const std::string& message);
inline std::string get_name() const { return m_loggerName; }
inline std::string get_logLevel() const { return m_logLevel; }
};
template<typename... Args>
void print(std::string format_str, Args&&... args)
{
std::vector<std::string> format_string_tokens_vec;
std::vector<int> format_numeric_tokens_vec;
std::vector<std::string> variadic_arguments_vec;
std::regex reg("(\\{\\d\\})");
(detail::populate_vec_with_variadic_params(variadic_arguments_vec, std::forward<Args>(args)), ...);
detail::populate_vec_with_regex_matches_from_str(format_string_tokens_vec, reg, format_str);
detail::remove_duplicates_in_vec(variadic_arguments_vec);
detail::string_to_numeric_vec(format_string_tokens_vec, format_numeric_tokens_vec, "{}");
for (int i = 0; i < format_string_tokens_vec.size(); ++i)
{
detail::replace_in_string(format_str, format_string_tokens_vec[i], variadic_arguments_vec[format_numeric_tokens_vec[i]]);
}
std::cout << format_str;
}
}
lwlog.cpp
#include "lwlog.h"
namespace lwlog
{
logger::logger(const std::string& logger_name)
: m_loggerName(logger_name), m_pattern("[%d, %x] [%l] [%n]: %v")
{
if (registry::instance().is_registry_automatic() == true)
{
registry::instance().register_logger(this);
}
}
logger::~logger()
{
}
void logger::set_name(const std::string& logger_name)
{
m_loggerName = logger_name;
}
void logger::set_logLevel_visibility(log_level logLevel)
{
if (logLevel == log_level::all)
{
m_logLevel_visibility = log_level::info | log_level::warning | log_level::error
| log_level::critical | log_level::debug;
}
else if (logLevel == log_level::none)
{
m_logLevel_visibility = log_level::none;
}
else
{
m_logLevel_visibility = logLevel;
}
}
void logger::set_pattern(const std::string& pattern)
{
m_pattern = pattern;
}
void logger::info(const std::string& message)
{
m_message = message;
m_logLevel = "info";
if (static_cast<std::underlying_type_t<log_level>>(m_logLevel_visibility)
& static_cast<std::underlying_type_t<log_level>>(log_level::info))
{
print_formatted(message, m_pattern);
}
}
void logger::warning(const std::string& message)
{
m_message = message;
m_logLevel = "warning";
if (static_cast<std::underlying_type_t<log_level>>(m_logLevel_visibility)
& static_cast<std::underlying_type_t<log_level>>(log_level::warning))
{
print_formatted(message, m_pattern);
}
}
void logger::error(const std::string& message)
{
m_message = message;
m_logLevel = "error";
if (static_cast<std::underlying_type_t<log_level>>(m_logLevel_visibility)
& static_cast<std::underlying_type_t<log_level>>(log_level::error))
{
print_formatted(message, m_pattern);
}
}
void logger::critical(const std::string& message)
{
m_message = message;
m_logLevel = "critical";
if (static_cast<std::underlying_type_t<log_level>>(m_logLevel_visibility)
& static_cast<std::underlying_type_t<log_level>>(log_level::critical))
{
print_formatted(message, m_pattern);
}
}
void logger::debug(const std::string& message)
{
m_message = message;
m_logLevel = "debug";
if (static_cast<std::underlying_type_t<log_level>>(m_logLevel_visibility)
& static_cast<std::underlying_type_t<log_level>>(log_level::debug))
{
print_formatted(message, m_pattern);
}
}
void logger::print_formatted(const std::string& message, std::string pattern)
{
m_patterns_data.emplace("%d", datetime::get_current_date());
m_patterns_data.emplace("%L", std::string(1, toupper(m_logLevel[0])));
m_patterns_data.emplace("%l", m_logLevel);
m_patterns_data.emplace("%n", m_loggerName);
m_patterns_data.emplace("%v", message);
m_patterns_data.emplace("%x", datetime::get_current_time());
std::vector<std::string> pattern_string_tokens_vec;
std::regex rg("(\\%[DdLlnvx]{1})");
detail::populate_vec_with_regex_matches_from_str(pattern_string_tokens_vec, rg, pattern);
for (int i = 0; i < pattern_string_tokens_vec.size(); ++i)
{
for (auto it = m_patterns_data.begin(); it != m_patterns_data.end(); ++it)
{
if (pattern_string_tokens_vec[i] == it->first)
{
detail::replace_in_string(pattern, pattern_string_tokens_vec[i], it->second);
}
}
}
print("{0} \n", pattern);
m_patterns_data.clear();
}
}
registry.h
#pragma once
#include <vector>
#include <memory>
#include <string>
#include <unordered_map>
#include "core.h"
namespace lwlog
{
class logger;
class LWLOG registry
{
private:
registry() {}
std::unordered_map<std::string, logger*> m_loggersMap;
bool m_automatic_registry = true;
public:
registry(const registry&) = delete;
registry& operator=(const registry&) = delete;
void register_logger(logger* new_logger);
void drop(const std::string& logger_name);
void drop_all();
void set_automatic_registry(bool automatic);
void display_all_loggers();
inline bool is_registry_automatic() { return m_automatic_registry ? true : false; }
static registry& instance()
{
static registry s_instance;
return s_instance;
}
};
}
registry.cpp
#include "registry.h"
#include "lwlog.h"
namespace lwlog
{
void registry::register_logger(logger* new_logger)
{
m_loggersMap.emplace(new_logger->get_name(), new_logger);
}
void registry::drop(const std::string& logger_name)
{
for (auto it = m_loggersMap.begin(); it != m_loggersMap.end(); ++it)
{
if (it->first == logger_name)
{
m_loggersMap.erase(logger_name);
break;
}
}
}
void registry::drop_all()
{
m_loggersMap.clear();
}
void registry::set_automatic_registry(bool automatic)
{
m_automatic_registry = automatic;
}
void registry::display_all_loggers()
{
for (auto it = m_loggersMap.begin(); it != m_loggersMap.end(); it++)
{
print("{0} \n", it->first);
}
}
}
detail.h
#pragma once
#include <regex>
#include <vector>
#include <map>
#include <unordered_set>
namespace detail
{
static void populate_vec_with_regex_matches_from_str(std::vector<std::string>& v, std::regex rg, std::string& s)
{
std::smatch matches;
std::string temp = s;
while (std::regex_search(temp, matches, rg))
{
v.push_back(matches.str(1));
temp = matches.suffix().str();
}
}
template<typename... Args>
void populate_vec_with_variadic_params(std::vector<std::string>& v, Args&&... args)
{
std::vector<std::string> vec =
{
[](auto && arg)
{
if constexpr (std::is_same_v<std::remove_reference_t<decltype(arg)>, char>)
{
return std::string(1, arg);
}
else if constexpr (std::is_arithmetic_v<std::remove_reference_t<decltype(arg)>>)
{
return std::to_string(std::forward<decltype(arg)>(arg));
}
else
{
return arg;
}
}(std::forward<Args>(args))...
};
for (const auto& i : vec)
{
v.push_back(i);
}
}
template<typename T, typename T1>
void string_to_numeric_vec(std::vector<T>& sv, std::vector<T1>& nv, const char* chars_to_remove)
{
for (int i = 0; i < sv.size(); ++i)
{
std::string temp = sv[i];
for (int i = 0; i < strlen(chars_to_remove); ++i)
{
temp.erase(std::remove(temp.begin(), temp.end(), chars_to_remove[i]), temp.end());
}
nv.push_back(std::stoi(temp));
}
}
template<typename T>
void remove_duplicates_in_vec(std::vector<T>& v)
{
std::unordered_set<T> s;
for (const auto& i : v)
{
s.insert(i);
}
v.assign(s.begin(), s.end());
}
static void replace_in_string(std::string& s, const std::string& to_replace, const std::string& replace_with)
{
size_t index = 0;
while (true)
{
index = s.find(to_replace, index);
if (index == std::string::npos)
{
break;
}
s.replace(index, to_replace.length(), replace_with);
index += to_replace.length();
}
}
}
datetime.h
#pragma once
#include <iomanip>
#include <sstream>
#include <chrono>
#include <ctime>
namespace lwlog
{
namespace datetime
{
static std::string get_current_time_and_date(const char* format)
{
auto now = std::chrono::system_clock::now();
auto in_time_t = std::chrono::system_clock::to_time_t(now);
std::stringstream ss;
ss << std::put_time(std::localtime(&in_time_t), format);
return ss.str();
}
static std::string get_current_time()
{
return get_current_time_and_date("%X");
}
static std::string get_current_date()
{
return get_current_time_and_date("%Y-%m-%d");
}
}
}
core.h
#pragma once
#ifdef _WIN32
#define LWLOG_PLATFORM_WINDOWS
#endif
#ifdef __linux__
#define LWLOG_PLATFORM_LINUX
#endif
#ifdef __APPLE__
#define LWLOG_PLATFORM_MAC
#endif
#ifndef LWLOG
#ifdef LWLOG_PLATFORM_WINDOWS
#if defined(LWLOG_BUILD_DLL)
#define LWLOG __declspec(dllexport)
#elif !defined(LWLOG_BUILD_STATIC)
#define LWLOG __declspec(dllimport)
#else
#define LWLOG
#endif
#else
#if __GNUC__ >= 4
#define LWLOG __attribute__((visibility("default")))
#else
#define LWLOG
#endif
#endif
#endif
And here is the sandbox (example usage) for you to play with the logger
Sandbox.cpp
#include "lwlog/lwlog.h"
int main()
{
std::string str1 = "test";
std::string str2 = "for";
lwlog::print("That's a {0} message {1} you. \n\n", str1, str2);
std::shared_ptr<lwlog::logger> core_logger = std::make_shared<lwlog::logger>("LOGGER");
std::shared_ptr<lwlog::logger> core_ldsdogger = std::make_shared<lwlog::logger>("LOGGER2");
std::shared_ptr<lwlog::logger> core_logaager = std::make_shared<lwlog::logger>("LOGGER3");
lwlog::registry::instance().display_all_loggers();
lwlog::registry::instance().drop("LOGGER2");
lwlog::print("\n");
lwlog::registry::instance().display_all_loggers();
core_logger->set_logLevel_visibility(lwlog::log_level::error | lwlog::log_level::critical);
core_logger->set_pattern("[%x] [%n]: %v");
core_logger->critical("A very critical message!");
return 0;
}
Still, there is no documentation, but I guess it will be simple to navigate since there is a Sandbox project for you to play with the library's features.
Any suggestions, opinions on code quality and performance are welcome, as well as feature suggestions. I suggest taking a closer look in terms of performance into the print method within lwlog.h, and maybe the detail.h file's contents.
P.S. Be aware that I started it a couple of days ago, so It's not so rich in features, so again, any suggestions for features are welcome. When it comes to multithreaded loggers, thread-safety, async and logging in files - I have these features on my mind, will try my best to implement them. Also, colored logs are something I'm working on using the ANSI escape codes.
m_message
a field? Unless I missed it, nothing ever reads it. Why ism_logLevel
a field instead of being passed into the print method likemessage
? If this were ever used from multiple threads, bad things would happen. I guess the intent is that every thread would use its own logger? \$\endgroup\$