Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add basic re-routeable logger #9

Merged
merged 7 commits into from
Apr 16, 2024
Merged
Show file tree
Hide file tree
Changes from 6 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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@
# JetBrains IDE
.idea/

# VSCode IDE
.vscode/

# build directories
build/
cmake-*/
4 changes: 2 additions & 2 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
set(CMAKE_BUILD_TYPE "Release" CACHE STRING "Choose the type of build." FORCE)
endif()

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD 20)

# set the module path, used for includes
set(CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake")
Expand All @@ -31,7 +31,7 @@ else()
endif()

# glob src
file(GLOB_RECURSE DD_TARGET_FILES src/*.cpp)
file(GLOB_RECURSE DD_TARGET_FILES src/*.h src/*.cpp)
FrogTheFrog marked this conversation as resolved.
Show resolved Hide resolved

# tests
if(BUILD_TESTS)
Expand Down
100 changes: 100 additions & 0 deletions src/logging.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
// class header include
#include "logging.h"

// system includes
#include <chrono>
#include <format>
#include <iostream>
#include <mutex>

namespace display_device {
logger_t &
logger_t::get() {
static logger_t instance;
return instance;
}

void
logger_t::set_log_level(const log_level_e log_level) {
m_enabled_log_level = log_level;
}

bool
logger_t::is_log_level_enabled(const log_level_e log_level) const {
const auto log_level_v { static_cast<std::underlying_type_t<log_level_e>>(log_level) };
const auto enabled_log_level_v { static_cast<std::underlying_type_t<log_level_e>>(m_enabled_log_level) };
return log_level_v >= enabled_log_level_v;
}

void
logger_t::set_custom_callback(callback_t callback) {
m_custom_callback = std::move(callback);
}

void
logger_t::write(const log_level_e log_level, std::string value) {
if (!is_log_level_enabled(log_level)) {
return;
}

if (m_custom_callback) {
m_custom_callback(log_level, std::move(value));
return;
}

std::stringstream stream;
{
// Time
const auto now { std::chrono::current_zone()->to_local(std::chrono::system_clock::now()) };
const auto now_ms { std::chrono::duration_cast<std::chrono::milliseconds>(now.time_since_epoch()) };
const auto now_s { std::chrono::duration_cast<std::chrono::seconds>(now_ms) };

// we need to ensure that the time formatter does not print decimal numbers for seconds as
// it currently has inconsistent formatting logic between platforms...
// Instead we will do it manually.
const auto now_local_seconds { std::chrono::local_seconds(now_s) };
const auto now_decimal_part { now_ms - now_s };

stream << std::format("[{:%Y-%m-%d %H:%M:%S}.{:0>3%Q}] ", now_local_seconds, now_decimal_part);

// Log level
switch (log_level) {
case log_level_e::verbose:
stream << "VERBOSE: ";
break;
case log_level_e::debug:
stream << "DEBUG: ";
break;
case log_level_e::info:
stream << "INFO: ";
break;
case log_level_e::warning:
stream << "WARNING: ";
break;
case log_level_e::error:
stream << "ERROR: ";
break;
default:
break;
}

// Value
stream << value;
}

static std::mutex log_mutex;
std::lock_guard lock { log_mutex };
std::cout << stream.rdbuf() << std::endl;
}

logger_t::logger_t():
m_enabled_log_level { log_level_e::info } {
}

Check warning on line 92 in src/logging.cpp

View check run for this annotation

Codecov / codecov/patch

src/logging.cpp#L92

Added line #L92 was not covered by tests

log_writer_t::log_writer_t(const logger_t::log_level_e log_level):
m_log_level { log_level } {}

log_writer_t::~log_writer_t() {
logger_t::get().write(m_log_level, m_buffer.str());
}
} // namespace display_device
169 changes: 169 additions & 0 deletions src/logging.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
#pragma once

// system includes
#include <functional>
#include <sstream>
#include <string>

namespace display_device {
/**
* @brief A singleton class for logging or re-routing logs.
*
* This class is not meant to be used directly (only for configuration).
* Instead, the MACRO below should be used throughout the code for logging.
*
* @note A lazy-evaluated, correctly-destroyed, thread-safe singleton pattern is used here (https://stackoverflow.com/a/1008289).
*/
class logger_t {
public:
/**
* @brief Defines the possible log levels.
* @note Level implicitly includes all other levels below it.
* @note All levels are in lower-case on purpose to fit the "BOOST_LOG(info)" style.
*/
enum class log_level_e {
verbose = 0,
debug,
info,
warning,
error
};

/**
* @brief Defines the callback type for log data re-routing.
*/
using callback_t = std::function<void(const log_level_e, std::string)>;

/**
* @brief Get the singleton instance.
* @returns Singleton instance for the class.
*
* EXAMPLES:
* ```cpp
* logger_t& logger { logger_t::get() };
* ```
*/
static logger_t &
get();

/**
* @brief Set the log level for the logger.
* @param log_level New level to be used.
*
* EXAMPLES:
* ```cpp
* logger_t::get().set_log_level(logger_t::log_level_e::Info);
* ```
*/
void
set_log_level(const log_level_e log_level);

/**
* @brief Check if log level is currently enabled.
* @param log_level Log level to check.
* @returns True if log level is enabled.
*
* EXAMPLES:
* ```cpp
* const bool is_enabled { logger_t::get().is_log_level_enabled(logger_t::log_level_e::Info) };
* ```
*/
[[nodiscard]] bool
is_log_level_enabled(const log_level_e log_level) const;

/**
* @brief Set custom callback for writing the logs.
* @param callback New callback to be used or nullptr to reset to the default.
*
* EXAMPLES:
* ```cpp
* logger_t::get().set_custom_callback([](const log_level_e level, std::string value){
* // write to file or something
* });
* ```
*/
void
set_custom_callback(callback_t callback);

/**
* @brief Write the string to the output (via callback) if the log level is enabled.
* @param log_level Log level to be checked and (probably) written.
* @param value A copy of the string to be written.
*
* EXAMPLES:
* ```cpp
* logger_t::get().write(logger_t::log_level_e::Info, "Hello World!");
* ```
*/
void
write(const log_level_e log_level, std::string value);

/**
* @brief A deleted copy constructor for singleton pattern.
* @note Public to ensure better compiler error message.
*/
logger_t(logger_t const &) = delete;

/**
* @brief A deleted assignment operator for singleton pattern.
* @note Public to ensure better compiler error message.
*/
void
operator=(logger_t const &) = delete;

private:
/**
* @brief A private constructor to ensure the singleton pattern.
*/
explicit logger_t();

log_level_e m_enabled_log_level; /**< The currently enabled log level. */
callback_t m_custom_callback; /**< Custom callback to pass log data to. */
};

/**
* @brief A helper class for accumulating output via the stream operator and then writing it out at once.
*/
class log_writer_t {
public:
/**
* @brief Constructor scoped writer utility.
* @param log_level Level to be used when writing out the output.
*/
explicit log_writer_t(const logger_t::log_level_e log_level);

/**
* @brief Write out the accumulated output.
*/
virtual ~log_writer_t();

/**
* @brief Stream value to the buffer.
* @param value Arbitrary value to be written to the buffer.
* @returns Reference to the writer utility for chaining the operators.
*/
template <class T>
log_writer_t &
operator<<(T &&value) {
m_buffer << std::forward<T>(value);
return *this;
}

private:
logger_t::log_level_e m_log_level; /**< Log level to be used. */
std::ostringstream m_buffer; /**< Buffer to hold all the output. */
};
} // namespace display_device

/**
* @brief Helper MACRO that disables output string computation if log level is not enabled.
*
* EXAMPLES:
* ```cpp
* DD_LOG(info) << "Hello World!" << " " << 123;
* DD_LOG(error) << "OH MY GAWD!";
* ```
*/
#define DD_LOG(level) \
for (bool is_enabled { display_device::logger_t::get().is_log_level_enabled(display_device::logger_t::log_level_e::level) }; is_enabled; is_enabled = false) \
display_device::log_writer_t(display_device::logger_t::log_level_e::level)
11 changes: 0 additions & 11 deletions src/main.cpp

This file was deleted.

7 changes: 2 additions & 5 deletions tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -33,19 +33,16 @@ file(GLOB_RECURSE TEST_SOURCES
set(DD_SOURCES
${DD_TARGET_FILES})

# remove main.cpp from the list of sources
list(REMOVE_ITEM DD_SOURCES ${CMAKE_SOURCE_DIR}/src/main.cpp)

add_executable(${PROJECT_NAME}
${TEST_SOURCES}
${DD_SOURCES})
set_target_properties(${PROJECT_NAME} PROPERTIES CXX_STANDARD 17)
set_target_properties(${PROJECT_NAME} PROPERTIES CXX_STANDARD 20)
target_link_libraries(${PROJECT_NAME}
${DD_EXTERNAL_LIBRARIES}
gtest
gtest_main) # if we use this we don't need our own main function
target_compile_definitions(${PROJECT_NAME} PUBLIC ${DD_DEFINITIONS} ${TEST_DEFINITIONS})
target_compile_options(${PROJECT_NAME} PRIVATE $<$<COMPILE_LANGUAGE:CXX>:${DD_COMPILE_OPTIONS}> -std=c++17)
target_compile_options(${PROJECT_NAME} PRIVATE $<$<COMPILE_LANGUAGE:CXX>:${DD_COMPILE_OPTIONS}> -std=c++20)
target_link_options(${PROJECT_NAME} PRIVATE)

add_test(NAME ${PROJECT_NAME} COMMAND libdisplaydevice_test)
12 changes: 8 additions & 4 deletions tests/conftest.cpp
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
// system includes
#include <filesystem>
#include <gtest/gtest.h>

#include <tests/utils.h>
// local includes
#include "src/logging.h"
#include "tests/utils.h"

// Undefine the original TEST macro
#undef TEST
Expand Down Expand Up @@ -56,10 +59,14 @@ class BaseTest: public ::testing::Test {

sbuf = std::cout.rdbuf(); // save cout buffer (std::cout)
std::cout.rdbuf(cout_buffer.rdbuf()); // redirect cout to buffer (std::cout)

// Default to the verbose level in case some test fails
display_device::logger_t::get().set_log_level(display_device::logger_t::log_level_e::verbose);
}

void
TearDown() override {
display_device::logger_t::get().set_custom_callback(nullptr); // restore the default callback to avoid potential leaks
std::cout.rdbuf(sbuf); // restore cout buffer

// get test info
Expand All @@ -69,8 +76,6 @@ class BaseTest: public ::testing::Test {
std::cout << std::endl
<< "Test failed: " << test_info->name() << std::endl
<< std::endl
<< "Captured boost log:" << std::endl
<< boost_log_buffer.str() << std::endl
<< "Captured cout:" << std::endl
<< cout_buffer.str() << std::endl
<< "Captured stdout:" << std::endl
Expand All @@ -94,7 +99,6 @@ class BaseTest: public ::testing::Test {
std::vector<std::string> testArgs; // CLI arguments used
std::filesystem::path testBinary; // full path of this binary
std::filesystem::path testBinaryDir; // full directory of this binary
std::stringstream boost_log_buffer; // declare boost_log_buffer
std::stringstream cout_buffer; // declare cout_buffer
std::stringstream stdout_buffer; // declare stdout_buffer
std::stringstream stderr_buffer; // declare stderr_buffer
Expand Down
Loading
Loading