mirror of
				https://github.com/XRPLF/clio.git
				synced 2025-11-04 11:55:51 +00:00 
			
		
		
		
	feat: Use spdlog logger (#2372)
This commit is contained in:
		@@ -58,6 +58,7 @@ include(deps/Threads)
 | 
			
		||||
include(deps/libfmt)
 | 
			
		||||
include(deps/cassandra)
 | 
			
		||||
include(deps/libbacktrace)
 | 
			
		||||
include(deps/spdlog)
 | 
			
		||||
 | 
			
		||||
add_subdirectory(src)
 | 
			
		||||
add_subdirectory(tests)
 | 
			
		||||
 
 | 
			
		||||
@@ -14,5 +14,5 @@ target_sources(
 | 
			
		||||
include(deps/gbench)
 | 
			
		||||
 | 
			
		||||
target_include_directories(clio_benchmark PRIVATE .)
 | 
			
		||||
target_link_libraries(clio_benchmark PUBLIC clio_util benchmark::benchmark_main)
 | 
			
		||||
target_link_libraries(clio_benchmark PUBLIC clio_util benchmark::benchmark_main spdlog::spdlog)
 | 
			
		||||
set_target_properties(clio_benchmark PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR})
 | 
			
		||||
 
 | 
			
		||||
@@ -22,26 +22,36 @@
 | 
			
		||||
#include "util/prometheus/Prometheus.hpp"
 | 
			
		||||
 | 
			
		||||
#include <benchmark/benchmark.h>
 | 
			
		||||
#include <boost/log/core/core.hpp>
 | 
			
		||||
#include <boost/log/utility/setup/common_attributes.hpp>
 | 
			
		||||
#include <fmt/format.h>
 | 
			
		||||
#include <spdlog/async.h>
 | 
			
		||||
#include <spdlog/async_logger.h>
 | 
			
		||||
#include <spdlog/spdlog.h>
 | 
			
		||||
 | 
			
		||||
#include <barrier>
 | 
			
		||||
#include <chrono>
 | 
			
		||||
#include <cstddef>
 | 
			
		||||
#include <filesystem>
 | 
			
		||||
#include <memory>
 | 
			
		||||
#include <string>
 | 
			
		||||
#include <thread>
 | 
			
		||||
#include <utility>
 | 
			
		||||
#include <vector>
 | 
			
		||||
 | 
			
		||||
using namespace util;
 | 
			
		||||
 | 
			
		||||
struct BenchmarkLoggingInitializer {
 | 
			
		||||
    static constexpr auto kLOG_FORMAT = "%TimeStamp% (%SourceLocation%) [%ThreadID%] %Channel%:%Severity% %Message%";
 | 
			
		||||
static constexpr auto kLOG_FORMAT = "%Y-%m-%d %H:%M:%S.%f %^%3!l:%n%$ - %v";
 | 
			
		||||
 | 
			
		||||
    static void
 | 
			
		||||
    initFileLogging(LogService::FileLoggingParams const& params)
 | 
			
		||||
struct BenchmarkLoggingInitializer {
 | 
			
		||||
    static std::shared_ptr<spdlog::sinks::sink>
 | 
			
		||||
    createFileSink(LogService::FileLoggingParams const& params)
 | 
			
		||||
    {
 | 
			
		||||
        LogService::initFileLogging(params, kLOG_FORMAT);
 | 
			
		||||
        return LogService::createFileSink(params);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    static Logger
 | 
			
		||||
    getLogger(std::shared_ptr<spdlog::logger> logger)
 | 
			
		||||
    {
 | 
			
		||||
        return Logger(std::move(logger));
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@@ -53,7 +63,7 @@ uniqueLogDir()
 | 
			
		||||
    auto const epochTime = std::chrono::high_resolution_clock::now().time_since_epoch();
 | 
			
		||||
    auto const tmpDir = std::filesystem::temp_directory_path();
 | 
			
		||||
    std::string const dirName =
 | 
			
		||||
        "logs_" + std::to_string(std::chrono::duration_cast<std::chrono::microseconds>(epochTime).count());
 | 
			
		||||
        fmt::format("logs_{}", std::chrono::duration_cast<std::chrono::microseconds>(epochTime).count());
 | 
			
		||||
    return tmpDir / "clio_benchmark" / dirName;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -62,7 +72,7 @@ uniqueLogDir()
 | 
			
		||||
static void
 | 
			
		||||
benchmarkConcurrentFileLogging(benchmark::State& state)
 | 
			
		||||
{
 | 
			
		||||
    boost::log::add_common_attributes();
 | 
			
		||||
    spdlog::drop_all();
 | 
			
		||||
 | 
			
		||||
    auto const numThreads = static_cast<size_t>(state.range(0));
 | 
			
		||||
    auto const messagesPerThread = static_cast<size_t>(state.range(1));
 | 
			
		||||
@@ -74,12 +84,12 @@ benchmarkConcurrentFileLogging(benchmark::State& state)
 | 
			
		||||
        state.PauseTiming();
 | 
			
		||||
 | 
			
		||||
        std::filesystem::create_directories(logDir);
 | 
			
		||||
        spdlog::init_thread_pool(8192, 1);
 | 
			
		||||
 | 
			
		||||
        BenchmarkLoggingInitializer::initFileLogging({
 | 
			
		||||
        auto fileSink = BenchmarkLoggingInitializer::createFileSink({
 | 
			
		||||
            .logDir = logDir,
 | 
			
		||||
            .rotationSizeMB = 5,
 | 
			
		||||
            .dirMaxSizeMB = 125,
 | 
			
		||||
            .rotationHours = 24,
 | 
			
		||||
            .dirMaxFiles = 25,
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        std::vector<std::thread> threads;
 | 
			
		||||
@@ -92,10 +102,16 @@ benchmarkConcurrentFileLogging(benchmark::State& state)
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        for (size_t threadNum = 0; threadNum < numThreads; ++threadNum) {
 | 
			
		||||
            threads.emplace_back([threadNum, messagesPerThread, &barrier]() {
 | 
			
		||||
                barrier.arrive_and_wait();
 | 
			
		||||
            threads.emplace_back([threadNum, messagesPerThread, fileSink, &barrier]() {
 | 
			
		||||
                std::string const channel = fmt::format("Thread_{}", threadNum);
 | 
			
		||||
                auto logger = std::make_shared<spdlog::async_logger>(
 | 
			
		||||
                    channel, fileSink, spdlog::thread_pool(), spdlog::async_overflow_policy::block
 | 
			
		||||
                );
 | 
			
		||||
                logger->set_pattern(kLOG_FORMAT);
 | 
			
		||||
                spdlog::register_logger(logger);
 | 
			
		||||
                Logger const threadLogger = BenchmarkLoggingInitializer::getLogger(std::move(logger));
 | 
			
		||||
 | 
			
		||||
                Logger const threadLogger("Thread_" + std::to_string(threadNum));
 | 
			
		||||
                barrier.arrive_and_wait();
 | 
			
		||||
 | 
			
		||||
                for (size_t messageNum = 0; messageNum < messagesPerThread; ++messageNum) {
 | 
			
		||||
                    LOG(threadLogger.info()) << "Test log message #" << messageNum;
 | 
			
		||||
@@ -106,10 +122,9 @@ benchmarkConcurrentFileLogging(benchmark::State& state)
 | 
			
		||||
        for (auto& thread : threads) {
 | 
			
		||||
            thread.join();
 | 
			
		||||
        }
 | 
			
		||||
        boost::log::core::get()->flush();
 | 
			
		||||
        spdlog::shutdown();
 | 
			
		||||
 | 
			
		||||
        auto const end = std::chrono::high_resolution_clock::now();
 | 
			
		||||
 | 
			
		||||
        state.SetIterationTime(std::chrono::duration_cast<std::chrono::duration<double>>(end - start).count());
 | 
			
		||||
 | 
			
		||||
        std::filesystem::remove_all(logDir);
 | 
			
		||||
@@ -129,7 +144,7 @@ BENCHMARK(benchmarkConcurrentFileLogging)
 | 
			
		||||
        // Number of threads
 | 
			
		||||
        {1, 2, 4, 8},
 | 
			
		||||
        // Messages per thread
 | 
			
		||||
        {10'000, 100'000, 500'000},
 | 
			
		||||
        {10'000, 100'000, 500'000, 1'000'000, 10'000'000},
 | 
			
		||||
    })
 | 
			
		||||
    ->UseManualTime()
 | 
			
		||||
    ->Unit(benchmark::kMillisecond);
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										5
									
								
								cmake/deps/spdlog.cmake
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								cmake/deps/spdlog.cmake
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,5 @@
 | 
			
		||||
find_package(spdlog REQUIRED)
 | 
			
		||||
 | 
			
		||||
if (NOT TARGET spdlog::spdlog)
 | 
			
		||||
  message(FATAL_ERROR "spdlog::spdlog target not found")
 | 
			
		||||
endif ()
 | 
			
		||||
@@ -5,6 +5,7 @@
 | 
			
		||||
        "xxhash/0.8.2#7856c968c985b2981b707ee8f2413b2b%1754325010.01",
 | 
			
		||||
        "xrpl/2.5.0@clio/boost-odr#f68e48da1490c0a583052e4f068ada55%1754325014.392",
 | 
			
		||||
        "sqlite3/3.47.0#7a0904fd061f5f8a2366c294f9387830%1754325009.708",
 | 
			
		||||
        "spdlog/1.15.3#3ca0e9e6b83af4d0151e26541d140c86%1754401846.61",
 | 
			
		||||
        "soci/4.0.3#a9f8d773cd33e356b5879a4b0564f287%1754412158.144",
 | 
			
		||||
        "re2/20230301#dfd6e2bf050eb90ddd8729cfb4c844a4%1754412148.014",
 | 
			
		||||
        "rapidjson/cci.20220822#1b9d8c2256876a154172dc5cfbe447c6%1754325007.656",
 | 
			
		||||
 
 | 
			
		||||
@@ -33,7 +33,8 @@ class ClioConan(ConanFile):
 | 
			
		||||
        'openssl/1.1.1v',
 | 
			
		||||
        'xrpl/2.5.0@clio/boost-odr',
 | 
			
		||||
        'zlib/1.3.1',
 | 
			
		||||
        'libbacktrace/cci.20210118'
 | 
			
		||||
        'libbacktrace/cci.20210118',
 | 
			
		||||
        'spdlog/1.15.3',
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    default_options = {
 | 
			
		||||
 
 | 
			
		||||
@@ -439,13 +439,21 @@ This document provides a list of all available Clio configuration properties in
 | 
			
		||||
- **Constraints**: The value must be one of the following: `trace`, `debug`, `info`, `warning`, `error`, `fatal`.
 | 
			
		||||
- **Description**: The general logging level of Clio. This level is applied to all log channels that do not have an explicitly defined logging level.
 | 
			
		||||
 | 
			
		||||
### log_format
 | 
			
		||||
### spdlog_format
 | 
			
		||||
 | 
			
		||||
- **Required**: True
 | 
			
		||||
- **Type**: string
 | 
			
		||||
- **Default value**: `%TimeStamp% (%SourceLocation%) [%ThreadID%] %Channel%:%Severity% %Message%`
 | 
			
		||||
- **Default value**: `%Y-%m-%d %H:%M:%S.%f %^%3!l:%n%$ - %v`
 | 
			
		||||
- **Constraints**: None
 | 
			
		||||
- **Description**: The format string for log messages. The format is described here: <https://www.boost.org/doc/libs/1_83_0/libs/log/doc/html/log/tutorial/formatters.html>.
 | 
			
		||||
- **Description**: The format string for log messages using spdlog format patterns. Documentation can be found at: <https://github.com/gabime/spdlog/wiki/Custom-formatting>.
 | 
			
		||||
 | 
			
		||||
### spdlog_async
 | 
			
		||||
 | 
			
		||||
- **Required**: True
 | 
			
		||||
- **Type**: boolean
 | 
			
		||||
- **Default value**: `True`
 | 
			
		||||
- **Constraints**: None
 | 
			
		||||
- **Description**: Whether spdlog is asynchronous or not.
 | 
			
		||||
 | 
			
		||||
### log_to_console
 | 
			
		||||
 | 
			
		||||
@@ -471,21 +479,13 @@ This document provides a list of all available Clio configuration properties in
 | 
			
		||||
- **Constraints**: The minimum value is `1`. The maximum value is `4294967295`.
 | 
			
		||||
- **Description**: The log rotation size in megabytes. When the log file reaches this particular size, a new log file starts.
 | 
			
		||||
 | 
			
		||||
### log_directory_max_size
 | 
			
		||||
### log_directory_max_files
 | 
			
		||||
 | 
			
		||||
- **Required**: True
 | 
			
		||||
- **Type**: int
 | 
			
		||||
- **Default value**: `51200`
 | 
			
		||||
- **Default value**: `25`
 | 
			
		||||
- **Constraints**: The minimum value is `1`. The maximum value is `4294967295`.
 | 
			
		||||
- **Description**: The maximum size of the log directory in megabytes.
 | 
			
		||||
 | 
			
		||||
### log_rotation_hour_interval
 | 
			
		||||
 | 
			
		||||
- **Required**: True
 | 
			
		||||
- **Type**: int
 | 
			
		||||
- **Default value**: `12`
 | 
			
		||||
- **Constraints**: The minimum value is `1`. The maximum value is `4294967295`.
 | 
			
		||||
- **Description**: Represents the interval (in hours) for log rotation. If the current log file reaches this value in logging, a new log file starts.
 | 
			
		||||
- **Description**: The maximum number of log files in the directory.
 | 
			
		||||
 | 
			
		||||
### log_tag_style
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -122,14 +122,15 @@
 | 
			
		||||
    "compress_reply": true
 | 
			
		||||
  },
 | 
			
		||||
  "log_level": "info",
 | 
			
		||||
  // Log format (this is the default format)
 | 
			
		||||
  "log_format": "%TimeStamp% (%SourceLocation%) [%ThreadID%] %Channel%:%Severity% %Message%",
 | 
			
		||||
  // Log format using spdlog format patterns (this is the default format)
 | 
			
		||||
  "spdlog_format": "%Y-%m-%d %H:%M:%S.%f %^%3!l:%n%$ - %v",
 | 
			
		||||
  // Whether spdlog is asynchronous or not.
 | 
			
		||||
  "spdlog_async": true,
 | 
			
		||||
  "log_to_console": true,
 | 
			
		||||
  // Clio logs to file in the specified directory only if "log_directory" is set
 | 
			
		||||
  // "log_directory": "./clio_log",
 | 
			
		||||
  "log_rotation_size": 2048,
 | 
			
		||||
  "log_directory_max_size": 51200,
 | 
			
		||||
  "log_rotation_hour_interval": 12,
 | 
			
		||||
  "log_directory_max_files": 25,
 | 
			
		||||
  "log_tag_style": "uint",
 | 
			
		||||
  "extractor_threads": 8,
 | 
			
		||||
  "read_only": false,
 | 
			
		||||
 
 | 
			
		||||
@@ -6,18 +6,29 @@ Clio provides several logging options, which all are configurable via the config
 | 
			
		||||
 | 
			
		||||
The minimum level of severity at which the log message will be outputted by default. Severity options are `trace`, `debug`, `info`, `warning`, `error`, `fatal`. Defaults to `info`.
 | 
			
		||||
 | 
			
		||||
## `log_format`
 | 
			
		||||
## `spdlog_format`
 | 
			
		||||
 | 
			
		||||
The format of log lines produced by Clio. Defaults to `"%TimeStamp% (%SourceLocation%) [%ThreadID%] %Channel%:%Severity% %Message%"`.
 | 
			
		||||
The format of log lines produced by Clio using spdlog format patterns. Defaults to `"%Y-%m-%d %H:%M:%S.%f %^%3!l:%n%$ - %v"`.
 | 
			
		||||
 | 
			
		||||
Each of the variables expands like so:
 | 
			
		||||
 | 
			
		||||
- `TimeStamp`: The full date and time of the log entry
 | 
			
		||||
- `SourceLocation`: A partial path to the c++ file and the line number in said file (`source/file/path:linenumber`)
 | 
			
		||||
- `ThreadID`: The ID of the thread the log entry is written from
 | 
			
		||||
- `Channel`: The channel that this log entry was sent to
 | 
			
		||||
- `Severity`: The severity (aka log level) the entry was sent at
 | 
			
		||||
- `Message`: The actual log message
 | 
			
		||||
- `%Y-%m-%d %H:%M:%S.%f`: The full date and time of the log entry with microsecond precision
 | 
			
		||||
- `%^`: Start color range
 | 
			
		||||
- `%3!l`: The severity (aka log level) the entry was sent at stripped to 3 characters
 | 
			
		||||
- `%n`: The logger name (channel) that this log entry was sent to
 | 
			
		||||
- `%$`: End color range
 | 
			
		||||
- `%v`: The actual log message
 | 
			
		||||
 | 
			
		||||
Some additional variables that might be useful:
 | 
			
		||||
 | 
			
		||||
- `%@`: A partial path to the C++ file and the line number in the said file (`src/file/path:linenumber`)
 | 
			
		||||
- `%t`: The ID of the thread the log entry is written from
 | 
			
		||||
 | 
			
		||||
For more information about spdlog format patterns, see: <https://github.com/gabime/spdlog/wiki/Custom-formatting>
 | 
			
		||||
 | 
			
		||||
## `spdlog_async`
 | 
			
		||||
 | 
			
		||||
Whether spdlog is asynchronous or not.
 | 
			
		||||
 | 
			
		||||
## `log_channels`
 | 
			
		||||
 | 
			
		||||
@@ -56,16 +67,9 @@ If the option is not specified, the logs are not written to a file.
 | 
			
		||||
 | 
			
		||||
The max size of the log file in **megabytes** before it will rotate into a smaller file. Defaults to 2GB.
 | 
			
		||||
 | 
			
		||||
## `log_directory_max_size`
 | 
			
		||||
## `log_directory_max_files`
 | 
			
		||||
 | 
			
		||||
The max size of the log directory in **megabytes** before old log files will be deleted to free up space. Defaults to 50GB.
 | 
			
		||||
 | 
			
		||||
## `log_rotation_hour_interval`
 | 
			
		||||
 | 
			
		||||
The time interval in **hours** after the last log rotation to automatically rotate the current log file. Defaults to 12 hours.
 | 
			
		||||
 | 
			
		||||
> [!NOTE]
 | 
			
		||||
> Log rotation based on time occurs in conjunction with size-based log rotation. For example, if a size-based log rotation occurs, the timer for the time-based rotation will reset.
 | 
			
		||||
The max number of log files in the directory before old log files will be deleted to free up space. Defaults to 25.
 | 
			
		||||
 | 
			
		||||
## `log_tag_style`
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -108,6 +108,8 @@ public:
 | 
			
		||||
 | 
			
		||||
            ioc.stop();
 | 
			
		||||
            LOG(util::LogService::info()) << "io_context stopped";
 | 
			
		||||
 | 
			
		||||
            util::LogService::shutdown();
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -22,6 +22,7 @@
 | 
			
		||||
#include "app/VerifyConfig.hpp"
 | 
			
		||||
#include "migration/MigrationApplication.hpp"
 | 
			
		||||
#include "rpc/common/impl/HandlerProvider.hpp"
 | 
			
		||||
#include "util/ScopeGuard.hpp"
 | 
			
		||||
#include "util/TerminationHandler.hpp"
 | 
			
		||||
#include "util/config/ConfigDefinition.hpp"
 | 
			
		||||
#include "util/log/Logger.hpp"
 | 
			
		||||
@@ -37,6 +38,7 @@ int
 | 
			
		||||
main(int argc, char const* argv[])
 | 
			
		||||
try {
 | 
			
		||||
    util::setTerminationHandler();
 | 
			
		||||
    util::ScopeGuard loggerShutdownGuard{[]() { util::LogService::shutdown(); }};
 | 
			
		||||
 | 
			
		||||
    auto const action = app::CliArgs::parse(argc, argv);
 | 
			
		||||
    return action.apply(
 | 
			
		||||
 
 | 
			
		||||
@@ -50,8 +50,6 @@ target_link_libraries(
 | 
			
		||||
  clio_util
 | 
			
		||||
  PUBLIC Boost::headers
 | 
			
		||||
         Boost::iostreams
 | 
			
		||||
         Boost::log
 | 
			
		||||
         Boost::log_setup
 | 
			
		||||
         fmt::fmt
 | 
			
		||||
         openssl::openssl
 | 
			
		||||
         xrpl::libxrpl
 | 
			
		||||
@@ -59,6 +57,7 @@ target_link_libraries(
 | 
			
		||||
         clio_options
 | 
			
		||||
         clio_rpc_center
 | 
			
		||||
         clio_build_version
 | 
			
		||||
  PRIVATE spdlog::spdlog
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
# FIXME: needed on gcc-12, clang-16 and AppleClang for now (known boost 1.82 issue for some compilers)
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										57
									
								
								src/util/ScopeGuard.hpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								src/util/ScopeGuard.hpp
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,57 @@
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
/*
 | 
			
		||||
    This file is part of clio: https://github.com/XRPLF/clio
 | 
			
		||||
    Copyright (c) 2025, the clio developers.
 | 
			
		||||
 | 
			
		||||
    Permission to use, copy, modify, and distribute this software for any
 | 
			
		||||
    purpose with or without fee is hereby granted, provided that the above
 | 
			
		||||
    copyright notice and this permission notice appear in all copies.
 | 
			
		||||
 | 
			
		||||
    THE  SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 | 
			
		||||
    WITH  REGARD  TO  THIS  SOFTWARE  INCLUDING  ALL  IMPLIED  WARRANTIES  OF
 | 
			
		||||
    MERCHANTABILITY  AND  FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 | 
			
		||||
    ANY  SPECIAL,  DIRECT,  INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 | 
			
		||||
    WHATSOEVER  RESULTING  FROM  LOSS  OF USE, DATA OR PROFITS, WHETHER IN AN
 | 
			
		||||
    ACTION  OF  CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 | 
			
		||||
    OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 | 
			
		||||
*/
 | 
			
		||||
//==============================================================================
 | 
			
		||||
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include <utility>
 | 
			
		||||
 | 
			
		||||
namespace util {
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @brief Run a function when the scope is exited
 | 
			
		||||
 */
 | 
			
		||||
template <typename Func>
 | 
			
		||||
class ScopeGuard {
 | 
			
		||||
public:
 | 
			
		||||
    ScopeGuard(ScopeGuard const&) = delete;
 | 
			
		||||
    ScopeGuard(ScopeGuard&&) = delete;
 | 
			
		||||
    ScopeGuard&
 | 
			
		||||
    operator=(ScopeGuard const&) = delete;
 | 
			
		||||
    ScopeGuard&
 | 
			
		||||
    operator=(ScopeGuard&&) = delete;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Create ScopeGuard object.
 | 
			
		||||
     *
 | 
			
		||||
     * @param func The function to run when the scope is exited.
 | 
			
		||||
     */
 | 
			
		||||
    ScopeGuard(Func func) : func_(std::move(func))
 | 
			
		||||
    {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    ~ScopeGuard()
 | 
			
		||||
    {
 | 
			
		||||
        func_();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
private:
 | 
			
		||||
    Func func_;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace util
 | 
			
		||||
@@ -44,6 +44,9 @@ terminationHandler()
 | 
			
		||||
#else
 | 
			
		||||
    LOG(LogService::fatal()) << "Exit on terminate. Stacktrace disabled.";
 | 
			
		||||
#endif  // CLIO_WITHOUT_STACKTRACE
 | 
			
		||||
 | 
			
		||||
    // We're calling std::abort later, so spdlog won't be shutdown automatically
 | 
			
		||||
    util::LogService::shutdown();
 | 
			
		||||
    std::abort();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -358,10 +358,9 @@ getClioConfig()
 | 
			
		||||
 | 
			
		||||
         {"log_level", ConfigValue{ConfigType::String}.defaultValue("info").withConstraint(gValidateLogLevelName)},
 | 
			
		||||
 | 
			
		||||
         {"log_format",
 | 
			
		||||
          ConfigValue{ConfigType::String}.defaultValue(
 | 
			
		||||
              R"(%TimeStamp% (%SourceLocation%) [%ThreadID%] %Channel%:%Severity% %Message%)"
 | 
			
		||||
          )},
 | 
			
		||||
         {"spdlog_format", ConfigValue{ConfigType::String}.defaultValue(R"(%Y-%m-%d %H:%M:%S.%f %^%3!l:%n%$ - %v)")},
 | 
			
		||||
 | 
			
		||||
         {"spdlog_async", ConfigValue{ConfigType::Boolean}.defaultValue(true)},
 | 
			
		||||
 | 
			
		||||
         {"log_to_console", ConfigValue{ConfigType::Boolean}.defaultValue(false)},
 | 
			
		||||
 | 
			
		||||
@@ -369,11 +368,7 @@ getClioConfig()
 | 
			
		||||
 | 
			
		||||
         {"log_rotation_size", ConfigValue{ConfigType::Integer}.defaultValue(2048).withConstraint(gValidateUint32)},
 | 
			
		||||
 | 
			
		||||
         {"log_directory_max_size",
 | 
			
		||||
          ConfigValue{ConfigType::Integer}.defaultValue(50 * 1024).withConstraint(gValidateUint32)},
 | 
			
		||||
 | 
			
		||||
         {"log_rotation_hour_interval",
 | 
			
		||||
          ConfigValue{ConfigType::Integer}.defaultValue(12).withConstraint(gValidateUint32)},
 | 
			
		||||
         {"log_directory_max_files", ConfigValue{ConfigType::Integer}.defaultValue(25).withConstraint(gValidateUint32)},
 | 
			
		||||
 | 
			
		||||
         {"log_tag_style", ConfigValue{ConfigType::String}.defaultValue("none").withConstraint(gValidateLogTag)},
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -263,18 +263,16 @@ private:
 | 
			
		||||
        KV{.key = "log_level",
 | 
			
		||||
           .value = "The general logging level of Clio. This level is applied to all log channels that do not have an "
 | 
			
		||||
                    "explicitly defined logging level."},
 | 
			
		||||
        KV{.key = "log_format",
 | 
			
		||||
           .value = "The format string for log messages. The format is described here: "
 | 
			
		||||
                    "<https://www.boost.org/doc/libs/1_83_0/libs/log/doc/html/log/tutorial/formatters.html>."},
 | 
			
		||||
        KV{.key = "spdlog_format",
 | 
			
		||||
           .value = "The format string for log messages using spdlog format patterns. Documentation can be found at: "
 | 
			
		||||
                    "<https://github.com/gabime/spdlog/wiki/Custom-formatting>."},
 | 
			
		||||
        KV{.key = "spdlog_async", .value = "Whether spdlog is asynchronous or not."},
 | 
			
		||||
        KV{.key = "log_to_console", .value = "Enables or disables logging to the console."},
 | 
			
		||||
        KV{.key = "log_directory", .value = "The directory path for the log files."},
 | 
			
		||||
        KV{.key = "log_rotation_size",
 | 
			
		||||
           .value = "The log rotation size in megabytes. When the log file reaches this particular size, a new log "
 | 
			
		||||
                    "file starts."},
 | 
			
		||||
        KV{.key = "log_directory_max_size", .value = "The maximum size of the log directory in megabytes."},
 | 
			
		||||
        KV{.key = "log_rotation_hour_interval",
 | 
			
		||||
           .value = "Represents the interval (in hours) for log rotation. If the current log file reaches this value "
 | 
			
		||||
                    "in logging, a new log file starts."},
 | 
			
		||||
        KV{.key = "log_directory_max_files", .value = "The maximum number of log files in the directory."},
 | 
			
		||||
        KV{.key = "log_tag_style",
 | 
			
		||||
           .value =
 | 
			
		||||
               "Log tags are unique identifiers for log messages. `uint`/`int` starts logging from 0 and increments, "
 | 
			
		||||
 
 | 
			
		||||
@@ -25,83 +25,62 @@
 | 
			
		||||
#include "util/config/ArrayView.hpp"
 | 
			
		||||
#include "util/config/ConfigDefinition.hpp"
 | 
			
		||||
#include "util/config/ObjectView.hpp"
 | 
			
		||||
#include "util/prometheus/Counter.hpp"
 | 
			
		||||
#include "util/prometheus/Label.hpp"
 | 
			
		||||
#include "util/prometheus/Prometheus.hpp"
 | 
			
		||||
 | 
			
		||||
#include <boost/algorithm/string/predicate.hpp>
 | 
			
		||||
#include <boost/date_time/posix_time/posix_time_duration.hpp>
 | 
			
		||||
#include <boost/log/attributes/attribute_value_set.hpp>
 | 
			
		||||
#include <boost/log/core/core.hpp>
 | 
			
		||||
#include <boost/log/expressions/filter.hpp>
 | 
			
		||||
#include <boost/log/keywords/auto_flush.hpp>
 | 
			
		||||
#include <boost/log/keywords/file_name.hpp>
 | 
			
		||||
#include <boost/log/keywords/filter.hpp>
 | 
			
		||||
#include <boost/log/keywords/format.hpp>
 | 
			
		||||
#include <boost/log/keywords/max_size.hpp>
 | 
			
		||||
#include <boost/log/keywords/open_mode.hpp>
 | 
			
		||||
#include <boost/log/keywords/rotation_size.hpp>
 | 
			
		||||
#include <boost/log/keywords/target.hpp>
 | 
			
		||||
#include <boost/log/keywords/target_file_name.hpp>
 | 
			
		||||
#include <boost/log/keywords/time_based_rotation.hpp>
 | 
			
		||||
#include <boost/log/sinks/text_file_backend.hpp>
 | 
			
		||||
#include <boost/log/utility/exception_handler.hpp>
 | 
			
		||||
#include <boost/log/utility/setup/common_attributes.hpp>
 | 
			
		||||
#include <boost/log/utility/setup/console.hpp>
 | 
			
		||||
#include <boost/log/utility/setup/file.hpp>
 | 
			
		||||
#include <boost/log/utility/setup/formatter_parser.hpp>
 | 
			
		||||
#include <fmt/format.h>
 | 
			
		||||
#include <spdlog/async.h>
 | 
			
		||||
#include <spdlog/async_logger.h>
 | 
			
		||||
#include <spdlog/common.h>
 | 
			
		||||
#include <spdlog/logger.h>
 | 
			
		||||
#include <spdlog/sinks/basic_file_sink.h>
 | 
			
		||||
#include <spdlog/sinks/rotating_file_sink.h>
 | 
			
		||||
#include <spdlog/sinks/stdout_color_sinks.h>
 | 
			
		||||
#include <spdlog/spdlog.h>
 | 
			
		||||
 | 
			
		||||
#include <algorithm>
 | 
			
		||||
#include <array>
 | 
			
		||||
#include <cstddef>
 | 
			
		||||
#include <cstdint>
 | 
			
		||||
#include <exception>
 | 
			
		||||
#include <filesystem>
 | 
			
		||||
#include <functional>
 | 
			
		||||
#include <ios>
 | 
			
		||||
#include <iostream>
 | 
			
		||||
#include <memory>
 | 
			
		||||
#include <optional>
 | 
			
		||||
#include <ostream>
 | 
			
		||||
#include <string>
 | 
			
		||||
#include <string_view>
 | 
			
		||||
#include <system_error>
 | 
			
		||||
#include <unordered_map>
 | 
			
		||||
#include <utility>
 | 
			
		||||
#include <vector>
 | 
			
		||||
 | 
			
		||||
namespace util {
 | 
			
		||||
 | 
			
		||||
LogService::Data LogService::data{};
 | 
			
		||||
 | 
			
		||||
namespace {
 | 
			
		||||
 | 
			
		||||
class LoggerExceptionHandler {
 | 
			
		||||
    std::reference_wrapper<util::prometheus::CounterInt> exceptionCounter_ =
 | 
			
		||||
        PrometheusService::counterInt("logger_exceptions_total_number", util::prometheus::Labels{});
 | 
			
		||||
 | 
			
		||||
public:
 | 
			
		||||
    using result_type = void;
 | 
			
		||||
 | 
			
		||||
    LoggerExceptionHandler()
 | 
			
		||||
spdlog::level::level_enum
 | 
			
		||||
toSpdlogLevel(Severity sev)
 | 
			
		||||
{
 | 
			
		||||
        ASSERT(PrometheusService::isInitialised(), "Prometheus should be initialised before Logger");
 | 
			
		||||
    switch (sev) {
 | 
			
		||||
        case Severity::TRC:
 | 
			
		||||
            return spdlog::level::trace;
 | 
			
		||||
        case Severity::DBG:
 | 
			
		||||
            return spdlog::level::debug;
 | 
			
		||||
        case Severity::NFO:
 | 
			
		||||
            return spdlog::level::info;
 | 
			
		||||
        case Severity::WRN:
 | 
			
		||||
            return spdlog::level::warn;
 | 
			
		||||
        case Severity::ERR:
 | 
			
		||||
            return spdlog::level::err;
 | 
			
		||||
        case Severity::FTL:
 | 
			
		||||
            return spdlog::level::critical;
 | 
			
		||||
    }
 | 
			
		||||
    return spdlog::level::info;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
    void
 | 
			
		||||
    operator()(std::exception const& e) const
 | 
			
		||||
std::string_view
 | 
			
		||||
toString(Severity sev)
 | 
			
		||||
{
 | 
			
		||||
        std::cerr << fmt::format("Exception in logger: {}\n", e.what());
 | 
			
		||||
        ++exceptionCounter_.get();
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace
 | 
			
		||||
 | 
			
		||||
Logger LogService::generalLog = Logger{"General"};
 | 
			
		||||
boost::log::filter LogService::filter{};
 | 
			
		||||
 | 
			
		||||
std::ostream&
 | 
			
		||||
operator<<(std::ostream& stream, Severity sev)
 | 
			
		||||
{
 | 
			
		||||
    static constexpr std::array<char const*, 6> kLABELS = {
 | 
			
		||||
    static constexpr std::array<std::string_view, 6> kLABELS = {
 | 
			
		||||
        "TRC",
 | 
			
		||||
        "DBG",
 | 
			
		||||
        "NFO",
 | 
			
		||||
@@ -110,9 +89,11 @@ operator<<(std::ostream& stream, Severity sev)
 | 
			
		||||
        "FTL",
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    return stream << kLABELS.at(static_cast<int>(sev));
 | 
			
		||||
    return kLABELS.at(static_cast<int>(sev));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @brief converts the loglevel to string to a corresponding Severity enum value.
 | 
			
		||||
 *
 | 
			
		||||
@@ -144,60 +125,47 @@ getSeverityLevel(std::string_view logLevel)
 | 
			
		||||
 * @brief Initializes console logging.
 | 
			
		||||
 *
 | 
			
		||||
 * @param logToConsole A boolean indicating whether to log to console.
 | 
			
		||||
 * @param format The log format string.
 | 
			
		||||
 * @return Vector of sinks for console logging.
 | 
			
		||||
 */
 | 
			
		||||
static void
 | 
			
		||||
initConsoleLogging(bool logToConsole, std::string const& format)
 | 
			
		||||
static std::vector<spdlog::sink_ptr>
 | 
			
		||||
createConsoleSinks(bool logToConsole)
 | 
			
		||||
{
 | 
			
		||||
    namespace keywords = boost::log::keywords;
 | 
			
		||||
    std::vector<spdlog::sink_ptr> sinks;
 | 
			
		||||
 | 
			
		||||
    if (logToConsole) {
 | 
			
		||||
        boost::log::add_console_log(
 | 
			
		||||
            std::cout, keywords::format = format, keywords::filter = LogSeverity < Severity::FTL
 | 
			
		||||
        );
 | 
			
		||||
        auto consoleSink = std::make_shared<spdlog::sinks::stdout_color_sink_mt>();
 | 
			
		||||
        consoleSink->set_level(spdlog::level::trace);
 | 
			
		||||
        sinks.push_back(std::move(consoleSink));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Always print fatal logs to cerr
 | 
			
		||||
    boost::log::add_console_log(std::cerr, keywords::format = format, keywords::filter = LogSeverity >= Severity::FTL);
 | 
			
		||||
    // Always add stderr sink for fatal logs
 | 
			
		||||
    auto stderrSink = std::make_shared<spdlog::sinks::stderr_color_sink_mt>();
 | 
			
		||||
    stderrSink->set_level(spdlog::level::critical);
 | 
			
		||||
    sinks.push_back(std::move(stderrSink));
 | 
			
		||||
 | 
			
		||||
    return sinks;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @brief Initializes file logging.
 | 
			
		||||
 *
 | 
			
		||||
 * @param config The configuration object containing log settings.
 | 
			
		||||
 * @param format The log format string.
 | 
			
		||||
 * @param dirPath The directory path where log files will be stored.
 | 
			
		||||
 * @return File sink for logging.
 | 
			
		||||
 */
 | 
			
		||||
void
 | 
			
		||||
LogService::initFileLogging(FileLoggingParams const& params, std::string const& format)
 | 
			
		||||
spdlog::sink_ptr
 | 
			
		||||
LogService::createFileSink(FileLoggingParams const& params)
 | 
			
		||||
{
 | 
			
		||||
    namespace keywords = boost::log::keywords;
 | 
			
		||||
    namespace sinks = boost::log::sinks;
 | 
			
		||||
 | 
			
		||||
    // the below are taken from user in MB, but boost::log::add_file_log needs it to be in bytes
 | 
			
		||||
    std::filesystem::path const dirPath(params.logDir);
 | 
			
		||||
    // the below are taken from user in MB, but spdlog needs it to be in bytes
 | 
			
		||||
    auto const rotationSize = mbToBytes(params.rotationSizeMB);
 | 
			
		||||
    auto const dirSize = mbToBytes(params.dirMaxSizeMB);
 | 
			
		||||
 | 
			
		||||
    auto const dirPath = std::filesystem::path{params.logDir};
 | 
			
		||||
    auto fileSink = std::make_shared<spdlog::sinks::rotating_file_sink_mt>(
 | 
			
		||||
        (dirPath / "clio.log").string(), rotationSize, params.dirMaxFiles
 | 
			
		||||
    );
 | 
			
		||||
    fileSink->set_level(spdlog::level::trace);
 | 
			
		||||
 | 
			
		||||
    auto fileSink = boost::log::add_file_log(
 | 
			
		||||
        keywords::file_name = dirPath / "clio.log",
 | 
			
		||||
        keywords::target_file_name = dirPath / "clio_%Y-%m-%d_%H-%M-%S.log",
 | 
			
		||||
        keywords::auto_flush = true,
 | 
			
		||||
        keywords::format = format,
 | 
			
		||||
        keywords::open_mode = std::ios_base::app,
 | 
			
		||||
        keywords::rotation_size = rotationSize,
 | 
			
		||||
        keywords::time_based_rotation =
 | 
			
		||||
            sinks::file::rotation_at_time_interval(boost::posix_time::hours(params.rotationHours))
 | 
			
		||||
    );
 | 
			
		||||
    fileSink->locked_backend()->set_file_collector(
 | 
			
		||||
        sinks::file::make_collector(keywords::target = dirPath, keywords::max_size = dirSize)
 | 
			
		||||
    );
 | 
			
		||||
    fileSink->locked_backend()->scan_for_files();
 | 
			
		||||
 | 
			
		||||
    boost::log::core::get()->set_exception_handler(
 | 
			
		||||
        boost::log::make_exception_handler<std::exception>(LoggerExceptionHandler())
 | 
			
		||||
    );
 | 
			
		||||
    return fileSink;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
@@ -229,41 +197,45 @@ getMinSeverity(config::ClioConfigDefinition const& config, Severity defaultSever
 | 
			
		||||
    return minSeverity;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @brief Creates a log filter based on the minimum severity levels for each channel.
 | 
			
		||||
 *
 | 
			
		||||
 * @param defaultSeverity The default severity level to use if not overridden.
 | 
			
		||||
 * @param minSeverity A map of channel names to their minimum severity levels.
 | 
			
		||||
 * @return A boost::log::filter that filters log records based on the severity level.
 | 
			
		||||
 */
 | 
			
		||||
static boost::log::filter
 | 
			
		||||
createLogFilter(Severity defaultSeverity, std::unordered_map<std::string, Severity> const& minSeverity)
 | 
			
		||||
std::shared_ptr<spdlog::logger>
 | 
			
		||||
LogService::registerLogger(std::string const& channel, Severity severity)
 | 
			
		||||
{
 | 
			
		||||
    auto logFilter = [minSeverity = minSeverity,
 | 
			
		||||
                      defaultSeverity](boost::log::attribute_value_set const& attributes) -> bool {
 | 
			
		||||
        auto const channel = attributes[LogChannel];
 | 
			
		||||
        auto const severity = attributes[LogSeverity];
 | 
			
		||||
        if (!channel || !severity)
 | 
			
		||||
            return false;
 | 
			
		||||
        if (auto const it = minSeverity.find(channel.get()); it != minSeverity.end())
 | 
			
		||||
            return severity.get() >= it->second;
 | 
			
		||||
        return severity.get() >= defaultSeverity;
 | 
			
		||||
    };
 | 
			
		||||
    std::shared_ptr<spdlog::logger> logger;
 | 
			
		||||
    if (data.isAsync) {
 | 
			
		||||
        logger = std::make_shared<spdlog::async_logger>(
 | 
			
		||||
            channel,
 | 
			
		||||
            data.allSinks.begin(),
 | 
			
		||||
            data.allSinks.end(),
 | 
			
		||||
            spdlog::thread_pool(),
 | 
			
		||||
            spdlog::async_overflow_policy::block
 | 
			
		||||
        );
 | 
			
		||||
    } else {
 | 
			
		||||
        logger = std::make_shared<spdlog::logger>(channel, data.allSinks.begin(), data.allSinks.end());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return boost::log::filter{std::move(logFilter)};
 | 
			
		||||
    logger->set_level(toSpdlogLevel(severity));
 | 
			
		||||
    logger->flush_on(spdlog::level::err);
 | 
			
		||||
 | 
			
		||||
    spdlog::register_logger(logger);
 | 
			
		||||
 | 
			
		||||
    return logger;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
std::expected<void, std::string>
 | 
			
		||||
LogService::init(config::ClioConfigDefinition const& config)
 | 
			
		||||
{
 | 
			
		||||
    boost::log::add_common_attributes();
 | 
			
		||||
    boost::log::register_simple_formatter_factory<Severity, char>("Severity");
 | 
			
		||||
    std::string const format = config.get<std::string>("log_format");
 | 
			
		||||
    // Drop existing loggers
 | 
			
		||||
    spdlog::drop_all();
 | 
			
		||||
 | 
			
		||||
    initConsoleLogging(config.get<bool>("log_to_console"), format);
 | 
			
		||||
    data.isAsync = config.get<bool>("spdlog_async");
 | 
			
		||||
 | 
			
		||||
    auto const logDir = config.maybeValue<std::string>("log_directory");
 | 
			
		||||
    if (logDir) {
 | 
			
		||||
    if (data.isAsync) {
 | 
			
		||||
        spdlog::init_thread_pool(8192, 1);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    data.allSinks = createConsoleSinks(config.get<bool>("log_to_console"));
 | 
			
		||||
 | 
			
		||||
    if (auto const logDir = config.maybeValue<std::string>("log_directory"); logDir.has_value()) {
 | 
			
		||||
        std::filesystem::path const dirPath{logDir.value()};
 | 
			
		||||
        if (not std::filesystem::exists(dirPath)) {
 | 
			
		||||
            if (std::error_code error; not std::filesystem::create_directories(dirPath, error)) {
 | 
			
		||||
@@ -272,13 +244,13 @@ LogService::init(config::ClioConfigDefinition const& config)
 | 
			
		||||
                };
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        FileLoggingParams const params{
 | 
			
		||||
            .logDir = logDir.value(),
 | 
			
		||||
            .rotationSizeMB = config.get<uint32_t>("log_rotation_size"),
 | 
			
		||||
            .dirMaxSizeMB = config.get<uint32_t>("log_directory_max_size"),
 | 
			
		||||
            .rotationHours = config.get<uint32_t>("log_rotation_hour_interval")
 | 
			
		||||
            .dirMaxFiles = config.get<uint32_t>("log_directory_max_files"),
 | 
			
		||||
        };
 | 
			
		||||
        initFileLogging(params, format);
 | 
			
		||||
        data.allSinks.push_back(createFileSink(params));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // get default severity, can be overridden per channel using the `log_channels` array
 | 
			
		||||
@@ -289,17 +261,92 @@ LogService::init(config::ClioConfigDefinition const& config)
 | 
			
		||||
    }
 | 
			
		||||
    auto const minSeverity = std::move(maybeMinSeverity).value();
 | 
			
		||||
 | 
			
		||||
    auto logFilter = createLogFilter(defaultSeverity, minSeverity);
 | 
			
		||||
    boost::log::core::get()->set_filter(logFilter);
 | 
			
		||||
    // Create loggers for each channel
 | 
			
		||||
    for (auto const& channel : Logger::kCHANNELS) {
 | 
			
		||||
        auto const it = minSeverity.find(channel);
 | 
			
		||||
        auto const severity = (it != minSeverity.end()) ? it->second : defaultSeverity;
 | 
			
		||||
        registerLogger(channel, severity);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    LOG(LogService::info()) << "Default log level = " << defaultSeverity;
 | 
			
		||||
    spdlog::set_default_logger(spdlog::get("General"));
 | 
			
		||||
 | 
			
		||||
    std::string const format = config.get<std::string>("spdlog_format");
 | 
			
		||||
    spdlog::set_pattern(format);
 | 
			
		||||
 | 
			
		||||
    LOG(LogService::info()) << "Default log level = " << toString(defaultSeverity);
 | 
			
		||||
    return {};
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void
 | 
			
		||||
LogService::shutdown()
 | 
			
		||||
{
 | 
			
		||||
    LOG(LogService::info()) << "Shutting down logger";
 | 
			
		||||
    spdlog::shutdown();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
Logger::Pump
 | 
			
		||||
LogService::trace(SourceLocationType const& loc)
 | 
			
		||||
{
 | 
			
		||||
    return Logger(spdlog::default_logger()).trace(loc);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
Logger::Pump
 | 
			
		||||
LogService::debug(SourceLocationType const& loc)
 | 
			
		||||
{
 | 
			
		||||
    return Logger(spdlog::default_logger()).debug(loc);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
Logger::Pump
 | 
			
		||||
LogService::info(SourceLocationType const& loc)
 | 
			
		||||
{
 | 
			
		||||
    return Logger(spdlog::default_logger()).info(loc);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
Logger::Pump
 | 
			
		||||
LogService::warn(SourceLocationType const& loc)
 | 
			
		||||
{
 | 
			
		||||
    return Logger(spdlog::default_logger()).warn(loc);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
Logger::Pump
 | 
			
		||||
LogService::error(SourceLocationType const& loc)
 | 
			
		||||
{
 | 
			
		||||
    return Logger(spdlog::default_logger()).error(loc);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
Logger::Pump
 | 
			
		||||
LogService::fatal(SourceLocationType const& loc)
 | 
			
		||||
{
 | 
			
		||||
    return Logger(spdlog::default_logger()).fatal(loc);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
bool
 | 
			
		||||
LogService::enabled()
 | 
			
		||||
{
 | 
			
		||||
    return boost::log::core::get()->get_logging_enabled();
 | 
			
		||||
    return spdlog::get_level() != spdlog::level::off;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
Logger::Logger(std::string channel) : logger_(spdlog::get(channel))
 | 
			
		||||
{
 | 
			
		||||
    if (!logger_) {
 | 
			
		||||
        logger_ = LogService::registerLogger(channel);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
Logger::Pump::Pump(std::shared_ptr<spdlog::logger> logger, Severity sev, SourceLocationType const& loc)
 | 
			
		||||
    : logger_(std::move(logger))
 | 
			
		||||
    , severity_(sev)
 | 
			
		||||
    , sourceLocation_(loc)
 | 
			
		||||
    , enabled_(logger_ != nullptr && logger_->should_log(toSpdlogLevel(sev)))
 | 
			
		||||
{
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
Logger::Pump::~Pump()
 | 
			
		||||
{
 | 
			
		||||
    if (enabled_) {
 | 
			
		||||
        spdlog::source_loc sourceLocation{prettyPath(sourceLocation_).cbegin(), sourceLocation_.line(), nullptr};
 | 
			
		||||
        logger_->log(sourceLocation, toSpdlogLevel(severity_), std::move(stream_).str());
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
Logger::Pump
 | 
			
		||||
@@ -333,17 +380,21 @@ Logger::fatal(SourceLocationType const& loc) const
 | 
			
		||||
    return {logger_, Severity::FTL, loc};
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
std::string
 | 
			
		||||
Logger::Logger(std::shared_ptr<spdlog::logger> logger) : logger_(std::move(logger))
 | 
			
		||||
{
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
std::string_view
 | 
			
		||||
Logger::Pump::prettyPath(SourceLocationType const& loc, size_t maxDepth)
 | 
			
		||||
{
 | 
			
		||||
    auto const filePath = std::string{loc.file_name()};
 | 
			
		||||
    std::string_view filePath{loc.file_name()};
 | 
			
		||||
    auto idx = filePath.size();
 | 
			
		||||
    while (maxDepth-- > 0) {
 | 
			
		||||
        idx = filePath.rfind('/', idx - 1);
 | 
			
		||||
        if (idx == std::string::npos || idx == 0)
 | 
			
		||||
            break;
 | 
			
		||||
    }
 | 
			
		||||
    return filePath.substr(idx == std::string::npos ? 0 : idx + 1) + ':' + std::to_string(loc.line());
 | 
			
		||||
    return filePath.substr(idx == std::string::npos ? 0 : idx + 1);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace util
 | 
			
		||||
 
 | 
			
		||||
@@ -21,35 +21,27 @@
 | 
			
		||||
 | 
			
		||||
#include "util/SourceLocation.hpp"
 | 
			
		||||
 | 
			
		||||
#include <boost/algorithm/string/predicate.hpp>
 | 
			
		||||
#include <boost/filesystem.hpp>
 | 
			
		||||
#include <boost/json.hpp>
 | 
			
		||||
#include <boost/json/conversion.hpp>
 | 
			
		||||
#include <boost/json/value.hpp>
 | 
			
		||||
#include <boost/log/core/core.hpp>
 | 
			
		||||
#include <boost/log/core/record.hpp>
 | 
			
		||||
#include <boost/log/expressions/filter.hpp>
 | 
			
		||||
#include <boost/log/expressions/keyword.hpp>
 | 
			
		||||
#include <boost/log/expressions/predicates/channel_severity_filter.hpp>
 | 
			
		||||
#include <boost/log/keywords/channel.hpp>
 | 
			
		||||
#include <boost/log/keywords/severity.hpp>
 | 
			
		||||
#include <boost/log/sinks/unlocked_frontend.hpp>
 | 
			
		||||
#include <boost/log/sources/record_ostream.hpp>
 | 
			
		||||
#include <boost/log/sources/severity_channel_logger.hpp>
 | 
			
		||||
#include <boost/log/sources/severity_feature.hpp>
 | 
			
		||||
#include <boost/log/sources/severity_logger.hpp>
 | 
			
		||||
#include <boost/log/utility/manipulators/add_value.hpp>
 | 
			
		||||
#include <boost/log/utility/setup/common_attributes.hpp>
 | 
			
		||||
#include <boost/log/utility/setup/console.hpp>
 | 
			
		||||
#include <boost/log/utility/setup/file.hpp>
 | 
			
		||||
#include <boost/log/utility/setup/formatter_parser.hpp>
 | 
			
		||||
 | 
			
		||||
#include <array>
 | 
			
		||||
#include <cstddef>
 | 
			
		||||
#include <cstdint>
 | 
			
		||||
#include <expected>
 | 
			
		||||
#include <optional>
 | 
			
		||||
#include <ostream>
 | 
			
		||||
#include <memory>
 | 
			
		||||
#include <sstream>
 | 
			
		||||
#include <string>
 | 
			
		||||
#include <string_view>
 | 
			
		||||
#include <vector>
 | 
			
		||||
 | 
			
		||||
// We forward declare spdlog::logger and spdlog::sinks::sink
 | 
			
		||||
// to avoid including the spdlog headers in this header file.
 | 
			
		||||
namespace spdlog {
 | 
			
		||||
 | 
			
		||||
class logger;
 | 
			
		||||
 | 
			
		||||
namespace sinks {
 | 
			
		||||
class sink;
 | 
			
		||||
}  // namespace sinks
 | 
			
		||||
 | 
			
		||||
}  // namespace spdlog
 | 
			
		||||
 | 
			
		||||
struct BenchmarkLoggingInitializer;
 | 
			
		||||
 | 
			
		||||
@@ -85,23 +77,6 @@ enum class Severity {
 | 
			
		||||
    FTL,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/** @cond */
 | 
			
		||||
// NOLINTBEGIN(readability-identifier-naming)
 | 
			
		||||
BOOST_LOG_ATTRIBUTE_KEYWORD(LogSeverity, "Severity", Severity);
 | 
			
		||||
BOOST_LOG_ATTRIBUTE_KEYWORD(LogChannel, "Channel", std::string);
 | 
			
		||||
// NOLINTEND(readability-identifier-naming)
 | 
			
		||||
/** @endcond */
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @brief Custom labels for @ref Severity in log output.
 | 
			
		||||
 *
 | 
			
		||||
 * @param stream std::ostream The output stream
 | 
			
		||||
 * @param sev Severity The severity to output to the ostream
 | 
			
		||||
 * @return The same ostream we were given
 | 
			
		||||
 */
 | 
			
		||||
std::ostream&
 | 
			
		||||
operator<<(std::ostream& stream, Severity sev);
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @brief A simple thread-safe logger for the channel specified
 | 
			
		||||
 * in the constructor.
 | 
			
		||||
@@ -111,31 +86,25 @@ operator<<(std::ostream& stream, Severity sev);
 | 
			
		||||
 * severity levels for each channel.
 | 
			
		||||
 */
 | 
			
		||||
class Logger final {
 | 
			
		||||
    using LoggerType = boost::log::sources::severity_channel_logger_mt<Severity, std::string>;
 | 
			
		||||
    mutable LoggerType logger_;
 | 
			
		||||
    std::shared_ptr<spdlog::logger> logger_;
 | 
			
		||||
 | 
			
		||||
    friend class LogService;  // to expose the Pump interface
 | 
			
		||||
    friend struct ::BenchmarkLoggingInitializer;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Helper that pumps data into a log record via `operator<<`.
 | 
			
		||||
     */
 | 
			
		||||
    class Pump final {
 | 
			
		||||
        using PumpOptType = std::optional<boost::log::aux::record_pump<LoggerType>>;
 | 
			
		||||
 | 
			
		||||
        boost::log::record rec_;
 | 
			
		||||
        PumpOptType pump_ = std::nullopt;
 | 
			
		||||
        std::shared_ptr<spdlog::logger> logger_;
 | 
			
		||||
        Severity const severity_;
 | 
			
		||||
        SourceLocationType const sourceLocation_;
 | 
			
		||||
        std::ostringstream stream_;
 | 
			
		||||
        bool const enabled_;
 | 
			
		||||
 | 
			
		||||
    public:
 | 
			
		||||
        ~Pump() = default;
 | 
			
		||||
        ~Pump();
 | 
			
		||||
 | 
			
		||||
        Pump(LoggerType& logger, Severity sev, SourceLocationType const& loc)
 | 
			
		||||
            : rec_{logger.open_record(boost::log::keywords::severity = sev)}
 | 
			
		||||
        {
 | 
			
		||||
            if (rec_) {
 | 
			
		||||
                pump_.emplace(boost::log::aux::make_record_pump(logger, rec_));
 | 
			
		||||
                pump_->stream() << boost::log::add_value("SourceLocation", prettyPath(loc));
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        Pump(std::shared_ptr<spdlog::logger> logger, Severity sev, SourceLocationType const& loc);
 | 
			
		||||
 | 
			
		||||
        Pump(Pump&&) = delete;
 | 
			
		||||
        Pump(Pump const&) = delete;
 | 
			
		||||
@@ -145,7 +114,7 @@ class Logger final {
 | 
			
		||||
        operator=(Pump&&) = delete;
 | 
			
		||||
 | 
			
		||||
        /**
 | 
			
		||||
         * @brief Perfectly forwards any incoming data into the underlying boost::log pump if the pump is available.
 | 
			
		||||
         * @brief Perfectly forwards any incoming data into the underlying stream if data should be logged.
 | 
			
		||||
         *
 | 
			
		||||
         * @tparam T Type of data to pump
 | 
			
		||||
         * @param data The data to pump
 | 
			
		||||
@@ -155,8 +124,8 @@ class Logger final {
 | 
			
		||||
        [[maybe_unused]] Pump&
 | 
			
		||||
        operator<<(T&& data)
 | 
			
		||||
        {
 | 
			
		||||
            if (pump_)
 | 
			
		||||
                pump_->stream() << std::forward<T>(data);
 | 
			
		||||
            if (enabled_)
 | 
			
		||||
                stream_ << std::forward<T>(data);
 | 
			
		||||
            return *this;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@@ -165,11 +134,11 @@ class Logger final {
 | 
			
		||||
         */
 | 
			
		||||
        operator bool() const
 | 
			
		||||
        {
 | 
			
		||||
            return pump_.has_value();
 | 
			
		||||
            return enabled_;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    private:
 | 
			
		||||
        [[nodiscard]] static std::string
 | 
			
		||||
        [[nodiscard]] static std::string_view
 | 
			
		||||
        prettyPath(SourceLocationType const& loc, size_t maxDepth = 3);
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
@@ -194,9 +163,7 @@ public:
 | 
			
		||||
     *
 | 
			
		||||
     * @param channel The channel this logger will report into.
 | 
			
		||||
     */
 | 
			
		||||
    Logger(std::string channel) : logger_{boost::log::keywords::channel = channel}
 | 
			
		||||
    {
 | 
			
		||||
    }
 | 
			
		||||
    Logger(std::string channel);
 | 
			
		||||
 | 
			
		||||
    Logger(Logger const&) = default;
 | 
			
		||||
    ~Logger() = default;
 | 
			
		||||
@@ -261,6 +228,9 @@ public:
 | 
			
		||||
     */
 | 
			
		||||
    [[nodiscard]] Pump
 | 
			
		||||
    fatal(SourceLocationType const& loc = CURRENT_SRC_LOCATION) const;
 | 
			
		||||
 | 
			
		||||
private:
 | 
			
		||||
    Logger(std::shared_ptr<spdlog::logger> logger);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
@@ -270,8 +240,19 @@ public:
 | 
			
		||||
 * entrypoint for logging into the `General` channel as well as raising alerts.
 | 
			
		||||
 */
 | 
			
		||||
class LogService {
 | 
			
		||||
    static Logger generalLog; /*< Global logger for General channel */
 | 
			
		||||
    static boost::log::filter filter;
 | 
			
		||||
    struct Data {
 | 
			
		||||
        bool isAsync;
 | 
			
		||||
        Severity severity;
 | 
			
		||||
        std::vector<std::shared_ptr<spdlog::sinks::sink>> allSinks;
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    friend class Logger;
 | 
			
		||||
 | 
			
		||||
private:
 | 
			
		||||
    static Data data;
 | 
			
		||||
 | 
			
		||||
    static std::shared_ptr<spdlog::logger>
 | 
			
		||||
    registerLogger(std::string const& channel, Severity severity = data.severity);
 | 
			
		||||
 | 
			
		||||
public:
 | 
			
		||||
    LogService() = delete;
 | 
			
		||||
@@ -285,6 +266,12 @@ public:
 | 
			
		||||
    [[nodiscard]] static std::expected<void, std::string>
 | 
			
		||||
    init(config::ClioConfigDefinition const& config);
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Shutdown spdlog to guarantee output is not lost
 | 
			
		||||
     */
 | 
			
		||||
    static void
 | 
			
		||||
    shutdown();
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Globally accessible General logger at Severity::TRC severity
 | 
			
		||||
     *
 | 
			
		||||
@@ -292,10 +279,7 @@ public:
 | 
			
		||||
     * @return The pump to use for logging
 | 
			
		||||
     */
 | 
			
		||||
    [[nodiscard]] static Logger::Pump
 | 
			
		||||
    trace(SourceLocationType const& loc = CURRENT_SRC_LOCATION)
 | 
			
		||||
    {
 | 
			
		||||
        return generalLog.trace(loc);
 | 
			
		||||
    }
 | 
			
		||||
    trace(SourceLocationType const& loc = CURRENT_SRC_LOCATION);
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Globally accessible General logger at Severity::DBG severity
 | 
			
		||||
@@ -304,10 +288,7 @@ public:
 | 
			
		||||
     * @return The pump to use for logging
 | 
			
		||||
     */
 | 
			
		||||
    [[nodiscard]] static Logger::Pump
 | 
			
		||||
    debug(SourceLocationType const& loc = CURRENT_SRC_LOCATION)
 | 
			
		||||
    {
 | 
			
		||||
        return generalLog.debug(loc);
 | 
			
		||||
    }
 | 
			
		||||
    debug(SourceLocationType const& loc = CURRENT_SRC_LOCATION);
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Globally accessible General logger at Severity::NFO severity
 | 
			
		||||
@@ -316,10 +297,7 @@ public:
 | 
			
		||||
     * @return The pump to use for logging
 | 
			
		||||
     */
 | 
			
		||||
    [[nodiscard]] static Logger::Pump
 | 
			
		||||
    info(SourceLocationType const& loc = CURRENT_SRC_LOCATION)
 | 
			
		||||
    {
 | 
			
		||||
        return generalLog.info(loc);
 | 
			
		||||
    }
 | 
			
		||||
    info(SourceLocationType const& loc = CURRENT_SRC_LOCATION);
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Globally accessible General logger at Severity::WRN severity
 | 
			
		||||
@@ -328,10 +306,7 @@ public:
 | 
			
		||||
     * @return The pump to use for logging
 | 
			
		||||
     */
 | 
			
		||||
    [[nodiscard]] static Logger::Pump
 | 
			
		||||
    warn(SourceLocationType const& loc = CURRENT_SRC_LOCATION)
 | 
			
		||||
    {
 | 
			
		||||
        return generalLog.warn(loc);
 | 
			
		||||
    }
 | 
			
		||||
    warn(SourceLocationType const& loc = CURRENT_SRC_LOCATION);
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Globally accessible General logger at Severity::ERR severity
 | 
			
		||||
@@ -340,10 +315,7 @@ public:
 | 
			
		||||
     * @return The pump to use for logging
 | 
			
		||||
     */
 | 
			
		||||
    [[nodiscard]] static Logger::Pump
 | 
			
		||||
    error(SourceLocationType const& loc = CURRENT_SRC_LOCATION)
 | 
			
		||||
    {
 | 
			
		||||
        return generalLog.error(loc);
 | 
			
		||||
    }
 | 
			
		||||
    error(SourceLocationType const& loc = CURRENT_SRC_LOCATION);
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Globally accessible General logger at Severity::FTL severity
 | 
			
		||||
@@ -352,10 +324,7 @@ public:
 | 
			
		||||
     * @return The pump to use for logging
 | 
			
		||||
     */
 | 
			
		||||
    [[nodiscard]] static Logger::Pump
 | 
			
		||||
    fatal(SourceLocationType const& loc = CURRENT_SRC_LOCATION)
 | 
			
		||||
    {
 | 
			
		||||
        return generalLog.fatal(loc);
 | 
			
		||||
    }
 | 
			
		||||
    fatal(SourceLocationType const& loc = CURRENT_SRC_LOCATION);
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief Whether the LogService is enabled or not
 | 
			
		||||
@@ -370,14 +339,14 @@ private:
 | 
			
		||||
        std::string logDir;
 | 
			
		||||
 | 
			
		||||
        uint32_t rotationSizeMB;
 | 
			
		||||
        uint32_t dirMaxSizeMB;
 | 
			
		||||
        uint32_t rotationHours;
 | 
			
		||||
        uint32_t dirMaxFiles;
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    friend struct ::BenchmarkLoggingInitializer;
 | 
			
		||||
 | 
			
		||||
    static void
 | 
			
		||||
    initFileLogging(FileLoggingParams const& params, std::string const& format);
 | 
			
		||||
    [[nodiscard]]
 | 
			
		||||
    static std::shared_ptr<spdlog::sinks::sink>
 | 
			
		||||
    createFileSink(FileLoggingParams const& params);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
};  // namespace util
 | 
			
		||||
 
 | 
			
		||||
@@ -18,4 +18,4 @@ target_sources(
 | 
			
		||||
include(deps/gtest)
 | 
			
		||||
 | 
			
		||||
target_include_directories(clio_testing_common PUBLIC .)
 | 
			
		||||
target_link_libraries(clio_testing_common PUBLIC clio_app gtest::gtest)
 | 
			
		||||
target_link_libraries(clio_testing_common PUBLIC clio_app gtest::gtest PRIVATE spdlog::spdlog)
 | 
			
		||||
 
 | 
			
		||||
@@ -21,44 +21,40 @@
 | 
			
		||||
 | 
			
		||||
#include "util/log/Logger.hpp"
 | 
			
		||||
 | 
			
		||||
#include <boost/log/core/core.hpp>
 | 
			
		||||
#include <boost/log/expressions/predicates/channel_severity_filter.hpp>
 | 
			
		||||
#include <boost/log/keywords/format.hpp>
 | 
			
		||||
#include <boost/log/utility/setup/common_attributes.hpp>
 | 
			
		||||
#include <boost/log/utility/setup/console.hpp>
 | 
			
		||||
#include <boost/log/utility/setup/formatter_parser.hpp>
 | 
			
		||||
#include <spdlog/common.h>
 | 
			
		||||
#include <spdlog/sinks/ostream_sink.h>
 | 
			
		||||
#include <spdlog/spdlog.h>
 | 
			
		||||
 | 
			
		||||
#include <algorithm>
 | 
			
		||||
#include <mutex>
 | 
			
		||||
#include <memory>
 | 
			
		||||
 | 
			
		||||
LoggerFixture::LoggerFixture()
 | 
			
		||||
{
 | 
			
		||||
    static std::once_flag kONCE;
 | 
			
		||||
    std::call_once(kONCE, [] {
 | 
			
		||||
        boost::log::add_common_attributes();
 | 
			
		||||
        boost::log::register_simple_formatter_factory<util::Severity, char>("Severity");
 | 
			
		||||
    // Clear any existing loggers
 | 
			
		||||
    spdlog::drop_all();
 | 
			
		||||
 | 
			
		||||
    // Create ostream sink for testing
 | 
			
		||||
    auto ostreamSink = std::make_shared<spdlog::sinks::ostream_sink_mt>(stream_);
 | 
			
		||||
 | 
			
		||||
    // Create loggers for each channel
 | 
			
		||||
    std::ranges::for_each(util::Logger::kCHANNELS, [&ostreamSink](char const* channel) {
 | 
			
		||||
        auto logger = std::make_shared<spdlog::logger>(channel, ostreamSink);
 | 
			
		||||
        logger->set_level(spdlog::level::trace);
 | 
			
		||||
        spdlog::register_logger(logger);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    namespace keywords = boost::log::keywords;
 | 
			
		||||
    namespace expr = boost::log::expressions;
 | 
			
		||||
    auto core = boost::log::core::get();
 | 
			
		||||
    spdlog::get("General")->set_level(spdlog::level::debug);
 | 
			
		||||
 | 
			
		||||
    core->remove_all_sinks();
 | 
			
		||||
    boost::log::add_console_log(stream_, keywords::format = "%Channel%:%Severity% %Message%");
 | 
			
		||||
    auto minSeverity = expr::channel_severity_filter(util::LogChannel, util::LogSeverity);
 | 
			
		||||
    auto traceLogger = std::make_shared<spdlog::logger>("Trace", ostreamSink);
 | 
			
		||||
    traceLogger->set_level(spdlog::level::trace);
 | 
			
		||||
    spdlog::register_logger(traceLogger);
 | 
			
		||||
 | 
			
		||||
    std::ranges::for_each(util::Logger::kCHANNELS, [&minSeverity](char const* channel) {
 | 
			
		||||
        minSeverity[channel] = util::Severity::TRC;
 | 
			
		||||
    });
 | 
			
		||||
    spdlog::set_default_logger(spdlog::get("General"));
 | 
			
		||||
 | 
			
		||||
    minSeverity["General"] = util::Severity::DBG;
 | 
			
		||||
    minSeverity["Trace"] = util::Severity::TRC;
 | 
			
		||||
 | 
			
		||||
    core->set_filter(minSeverity);
 | 
			
		||||
    core->set_logging_enabled(true);
 | 
			
		||||
    spdlog::set_pattern("%^%3!l:%n%$ - %v");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
NoLoggerFixture::NoLoggerFixture()
 | 
			
		||||
{
 | 
			
		||||
    boost::log::core::get()->set_logging_enabled(false);
 | 
			
		||||
    spdlog::set_level(spdlog::level::off);
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -19,56 +19,25 @@
 | 
			
		||||
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include "util/log/Logger.hpp"
 | 
			
		||||
#include "util/StringBuffer.hpp"
 | 
			
		||||
 | 
			
		||||
#include <gtest/gtest.h>
 | 
			
		||||
 | 
			
		||||
#include <algorithm>
 | 
			
		||||
#include <mutex>
 | 
			
		||||
#include <ostream>
 | 
			
		||||
#include <sstream>
 | 
			
		||||
#include <string>
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @brief Fixture with util::Logger support.
 | 
			
		||||
 * @brief A fixture for testing LogService and Logger.
 | 
			
		||||
 */
 | 
			
		||||
class LoggerFixture : virtual public ::testing::Test {
 | 
			
		||||
    /**
 | 
			
		||||
     * @brief A simple string buffer that can be used to mock std::cout for
 | 
			
		||||
     * console logging.
 | 
			
		||||
     */
 | 
			
		||||
    class FakeBuffer final : public std::stringbuf {
 | 
			
		||||
    public:
 | 
			
		||||
        std::string
 | 
			
		||||
        getStrAndReset()
 | 
			
		||||
        {
 | 
			
		||||
            auto value = str();
 | 
			
		||||
            str("");
 | 
			
		||||
            return value;
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    FakeBuffer buffer_;
 | 
			
		||||
    StringBuffer buffer_;
 | 
			
		||||
    std::ostream stream_ = std::ostream{&buffer_};
 | 
			
		||||
 | 
			
		||||
public:
 | 
			
		||||
    // Simulates the `util::Logger::init(config)` call
 | 
			
		||||
    // Simulates the `util::LogService::init(config)` call
 | 
			
		||||
    LoggerFixture();
 | 
			
		||||
 | 
			
		||||
protected:
 | 
			
		||||
    void
 | 
			
		||||
    checkEqual(std::string expected)
 | 
			
		||||
    {
 | 
			
		||||
        auto value = buffer_.getStrAndReset();
 | 
			
		||||
        ASSERT_EQ(value, expected + '\n') << "Got: " << value;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    void
 | 
			
		||||
    checkEmpty()
 | 
			
		||||
    {
 | 
			
		||||
        ASSERT_TRUE(buffer_.getStrAndReset().empty());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    std::string
 | 
			
		||||
    getLoggerString()
 | 
			
		||||
    {
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										37
									
								
								tests/common/util/StringBuffer.hpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								tests/common/util/StringBuffer.hpp
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,37 @@
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
/*
 | 
			
		||||
    This file is part of clio: https://github.com/XRPLF/clio
 | 
			
		||||
    Copyright (c) 2025, the clio developers.
 | 
			
		||||
 | 
			
		||||
    Permission to use, copy, modify, and distribute this software for any
 | 
			
		||||
    purpose with or without fee is hereby granted, provided that the above
 | 
			
		||||
    copyright notice and this permission notice appear in all copies.
 | 
			
		||||
 | 
			
		||||
    THE  SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 | 
			
		||||
    WITH  REGARD  TO  THIS  SOFTWARE  INCLUDING  ALL  IMPLIED  WARRANTIES  OF
 | 
			
		||||
    MERCHANTABILITY  AND  FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 | 
			
		||||
    ANY  SPECIAL,  DIRECT,  INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 | 
			
		||||
    WHATSOEVER  RESULTING  FROM  LOSS  OF USE, DATA OR PROFITS, WHETHER IN AN
 | 
			
		||||
    ACTION  OF  CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 | 
			
		||||
    OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 | 
			
		||||
*/
 | 
			
		||||
//==============================================================================
 | 
			
		||||
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include <sstream>
 | 
			
		||||
#include <string>
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @brief A simple string buffer that can be used to mock std::cout for console logging.
 | 
			
		||||
 */
 | 
			
		||||
class StringBuffer final : public std::stringbuf {
 | 
			
		||||
public:
 | 
			
		||||
    std::string
 | 
			
		||||
    getStrAndReset()
 | 
			
		||||
    {
 | 
			
		||||
        auto value = str();
 | 
			
		||||
        str("");
 | 
			
		||||
        return value;
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
@@ -69,6 +69,7 @@ target_sources(
 | 
			
		||||
          feed/TrackableSignalTests.cpp
 | 
			
		||||
          feed/TransactionFeedTests.cpp
 | 
			
		||||
          JsonUtilTests.cpp
 | 
			
		||||
          LogServiceInitTests.cpp
 | 
			
		||||
          LoggerTests.cpp
 | 
			
		||||
          Main.cpp
 | 
			
		||||
          Playground.cpp
 | 
			
		||||
@@ -173,6 +174,7 @@ target_sources(
 | 
			
		||||
          util/RepeatTests.cpp
 | 
			
		||||
          util/ResponseExpirationCacheTests.cpp
 | 
			
		||||
          util/RetryTests.cpp
 | 
			
		||||
          util/ScopeGuardTests.cpp
 | 
			
		||||
          util/SignalsHandlerTests.cpp
 | 
			
		||||
          util/SpawnTests.cpp
 | 
			
		||||
          util/StopHelperTests.cpp
 | 
			
		||||
@@ -216,7 +218,7 @@ gtest_discover_tests(clio_tests DISCOVERY_TIMEOUT 90)
 | 
			
		||||
target_compile_options(clio_options INTERFACE -gdwarf-4)
 | 
			
		||||
 | 
			
		||||
target_include_directories(clio_tests PRIVATE .)
 | 
			
		||||
target_link_libraries(clio_tests PUBLIC clio_testing_common)
 | 
			
		||||
target_link_libraries(clio_tests PUBLIC clio_testing_common PRIVATE spdlog::spdlog)
 | 
			
		||||
set_target_properties(clio_tests PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR})
 | 
			
		||||
 | 
			
		||||
# Generate `coverage_report` target if coverage is enabled
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										232
									
								
								tests/unit/LogServiceInitTests.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										232
									
								
								tests/unit/LogServiceInitTests.cpp
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,232 @@
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
/*
 | 
			
		||||
    This file is part of clio: https://github.com/XRPLF/clio
 | 
			
		||||
    Copyright (c) 2022, the clio developers.
 | 
			
		||||
 | 
			
		||||
    Permission to use, copy, modify, and distribute this software for any
 | 
			
		||||
    purpose with or without fee is hereby granted, provided that the above
 | 
			
		||||
    copyright notice and this permission notice appear in all copies.
 | 
			
		||||
 | 
			
		||||
    THE  SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 | 
			
		||||
    WITH  REGARD  TO  THIS  SOFTWARE  INCLUDING  ALL  IMPLIED  WARRANTIES  OF
 | 
			
		||||
    MERCHANTABILITY  AND  FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 | 
			
		||||
    ANY  SPECIAL,  DIRECT,  INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 | 
			
		||||
    WHATSOEVER  RESULTING  FROM  LOSS  OF USE, DATA OR PROFITS, WHETHER IN AN
 | 
			
		||||
    ACTION  OF  CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 | 
			
		||||
    OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 | 
			
		||||
*/
 | 
			
		||||
//==============================================================================
 | 
			
		||||
 | 
			
		||||
#include "util/StringBuffer.hpp"
 | 
			
		||||
#include "util/config/Array.hpp"
 | 
			
		||||
#include "util/config/ConfigConstraints.hpp"
 | 
			
		||||
#include "util/config/ConfigDefinition.hpp"
 | 
			
		||||
#include "util/config/ConfigFileJson.hpp"
 | 
			
		||||
#include "util/config/ConfigValue.hpp"
 | 
			
		||||
#include "util/config/Types.hpp"
 | 
			
		||||
#include "util/log/Logger.hpp"
 | 
			
		||||
 | 
			
		||||
#include <boost/json/array.hpp>
 | 
			
		||||
#include <boost/json/object.hpp>
 | 
			
		||||
#include <boost/json/parse.hpp>
 | 
			
		||||
#include <fmt/format.h>
 | 
			
		||||
#include <gmock/gmock.h>
 | 
			
		||||
#include <gtest/gtest.h>
 | 
			
		||||
#include <spdlog/sinks/ostream_sink.h>
 | 
			
		||||
#include <spdlog/spdlog.h>
 | 
			
		||||
 | 
			
		||||
#include <cstddef>
 | 
			
		||||
#include <cstdint>
 | 
			
		||||
#include <limits>
 | 
			
		||||
#include <memory>
 | 
			
		||||
#include <ostream>
 | 
			
		||||
#include <string>
 | 
			
		||||
#include <string_view>
 | 
			
		||||
#include <vector>
 | 
			
		||||
using namespace util;
 | 
			
		||||
 | 
			
		||||
using util::config::Array;
 | 
			
		||||
using util::config::ConfigFileJson;
 | 
			
		||||
using util::config::ConfigType;
 | 
			
		||||
using util::config::ConfigValue;
 | 
			
		||||
 | 
			
		||||
struct LogServiceInitTests : virtual public ::testing::Test {
 | 
			
		||||
protected:
 | 
			
		||||
    util::config::ClioConfigDefinition config_{
 | 
			
		||||
        {"log_channels.[].channel", Array{ConfigValue{ConfigType::String}}},
 | 
			
		||||
        {"log_channels.[].log_level", Array{ConfigValue{ConfigType::String}}},
 | 
			
		||||
 | 
			
		||||
        {"log_level", ConfigValue{ConfigType::String}.defaultValue("info")},
 | 
			
		||||
 | 
			
		||||
        {"spdlog_format", ConfigValue{ConfigType::String}.defaultValue(R"(%Y-%m-%d %H:%M:%S.%f %^%3!l:%n%$ - %v)")},
 | 
			
		||||
        {"spdlog_async", ConfigValue{ConfigType::Boolean}.defaultValue(false)},
 | 
			
		||||
 | 
			
		||||
        {"log_to_console", ConfigValue{ConfigType::Boolean}.defaultValue(false)},
 | 
			
		||||
 | 
			
		||||
        {"log_directory", ConfigValue{ConfigType::String}.optional()},
 | 
			
		||||
 | 
			
		||||
        {"log_rotation_size",
 | 
			
		||||
         ConfigValue{ConfigType::Integer}.defaultValue(2048).withConstraint(config::gValidateUint32)},
 | 
			
		||||
 | 
			
		||||
        {"log_directory_max_files",
 | 
			
		||||
         ConfigValue{ConfigType::Integer}.defaultValue(25).withConstraint(config::gValidateUint32)},
 | 
			
		||||
 | 
			
		||||
        {"log_tag_style", ConfigValue{ConfigType::String}.defaultValue("none")},
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    std::string
 | 
			
		||||
    getLoggerString()
 | 
			
		||||
    {
 | 
			
		||||
        return buffer_.getStrAndReset();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    void
 | 
			
		||||
    replaceSinks()
 | 
			
		||||
    {
 | 
			
		||||
        auto ostreamSink = std::make_shared<spdlog::sinks::ostream_sink_mt>(stream_);
 | 
			
		||||
 | 
			
		||||
        for (auto const& channel : Logger::kCHANNELS) {
 | 
			
		||||
            auto logger = spdlog::get(channel);
 | 
			
		||||
            ASSERT_TRUE(logger != nullptr);
 | 
			
		||||
            // It is expected that only stderrSink is present
 | 
			
		||||
            ASSERT_EQ(logger->sinks().size(), 1);
 | 
			
		||||
            logger->sinks().clear();
 | 
			
		||||
            logger->sinks().push_back(ostreamSink);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        spdlog::set_pattern("%^%3!l:%n%$ - %v");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
private:
 | 
			
		||||
    StringBuffer buffer_;
 | 
			
		||||
    std::ostream stream_ = std::ostream{&buffer_};
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
TEST_F(LogServiceInitTests, DefaultLogLevel)
 | 
			
		||||
{
 | 
			
		||||
    auto const parsingErrors = config_.parse(ConfigFileJson{boost::json::object{{"log_level", "warn"}}});
 | 
			
		||||
    ASSERT_FALSE(parsingErrors.has_value());
 | 
			
		||||
    std::string const logString = "some log";
 | 
			
		||||
 | 
			
		||||
    EXPECT_TRUE(LogService::init(config_));
 | 
			
		||||
    replaceSinks();
 | 
			
		||||
 | 
			
		||||
    for (auto const& channel : Logger::kCHANNELS) {
 | 
			
		||||
        Logger const log{channel};
 | 
			
		||||
        log.trace() << logString;
 | 
			
		||||
        ASSERT_TRUE(getLoggerString().empty());
 | 
			
		||||
 | 
			
		||||
        log.debug() << logString;
 | 
			
		||||
        ASSERT_TRUE(getLoggerString().empty());
 | 
			
		||||
 | 
			
		||||
        log.info() << logString;
 | 
			
		||||
        ASSERT_TRUE(getLoggerString().empty());
 | 
			
		||||
 | 
			
		||||
        log.warn() << logString;
 | 
			
		||||
        ASSERT_EQ(fmt::format("war:{} - {}\n", channel, logString), getLoggerString());
 | 
			
		||||
 | 
			
		||||
        log.error() << logString;
 | 
			
		||||
        ASSERT_EQ(fmt::format("err:{} - {}\n", channel, logString), getLoggerString());
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
TEST_F(LogServiceInitTests, ChannelLogLevel)
 | 
			
		||||
{
 | 
			
		||||
    std::string const configStr = R"JSON(
 | 
			
		||||
    {
 | 
			
		||||
        "log_level": "error",
 | 
			
		||||
        "log_channels": [
 | 
			
		||||
            {
 | 
			
		||||
                "channel": "Backend",
 | 
			
		||||
                "log_level": "warning"
 | 
			
		||||
            }
 | 
			
		||||
        ]
 | 
			
		||||
    }
 | 
			
		||||
    )JSON";
 | 
			
		||||
 | 
			
		||||
    auto const parsingErrors = config_.parse(ConfigFileJson{boost::json::parse(configStr).as_object()});
 | 
			
		||||
    ASSERT_FALSE(parsingErrors.has_value());
 | 
			
		||||
    std::string const logString = "some log";
 | 
			
		||||
 | 
			
		||||
    EXPECT_TRUE(LogService::init(config_));
 | 
			
		||||
    replaceSinks();
 | 
			
		||||
 | 
			
		||||
    for (auto const& channel : Logger::kCHANNELS) {
 | 
			
		||||
        Logger const log{channel};
 | 
			
		||||
        log.trace() << logString;
 | 
			
		||||
        ASSERT_TRUE(getLoggerString().empty());
 | 
			
		||||
 | 
			
		||||
        log.debug() << logString;
 | 
			
		||||
        ASSERT_TRUE(getLoggerString().empty());
 | 
			
		||||
 | 
			
		||||
        log.info() << logString;
 | 
			
		||||
        ASSERT_TRUE(getLoggerString().empty());
 | 
			
		||||
 | 
			
		||||
        log.warn() << logString;
 | 
			
		||||
        if (std::string_view{channel} == "Backend") {
 | 
			
		||||
            ASSERT_EQ(fmt::format("war:{} - {}\n", channel, logString), getLoggerString());
 | 
			
		||||
        } else {
 | 
			
		||||
            ASSERT_TRUE(getLoggerString().empty());
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        log.error() << logString;
 | 
			
		||||
        ASSERT_EQ(fmt::format("err:{} - {}\n", channel, logString), getLoggerString());
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
TEST_F(LogServiceInitTests, InitReturnsErrorIfCouldNotCreateLogDirectory)
 | 
			
		||||
{
 | 
			
		||||
    // "/proc" directory is read only on any unix OS
 | 
			
		||||
    auto const parsingErrors = config_.parse(ConfigFileJson{boost::json::object{{"log_directory", "/proc/logs"}}});
 | 
			
		||||
    ASSERT_FALSE(parsingErrors.has_value());
 | 
			
		||||
 | 
			
		||||
    auto const result = LogService::init(config_);
 | 
			
		||||
    EXPECT_FALSE(result);
 | 
			
		||||
    EXPECT_THAT(result.error(), testing::HasSubstr("Couldn't create logs directory"));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
TEST_F(LogServiceInitTests, InitReturnsErrorIfProvidedInvalidChannel)
 | 
			
		||||
{
 | 
			
		||||
    auto const jsonStr = R"JSON(
 | 
			
		||||
    {
 | 
			
		||||
        "log_channels": [
 | 
			
		||||
            {
 | 
			
		||||
                "channel": "SomeChannel",
 | 
			
		||||
                "log_level": "warn"
 | 
			
		||||
            }
 | 
			
		||||
        ]
 | 
			
		||||
    })JSON";
 | 
			
		||||
 | 
			
		||||
    auto const json = boost::json::parse(jsonStr).as_object();
 | 
			
		||||
    auto const parsingErrors = config_.parse(ConfigFileJson{json});
 | 
			
		||||
    ASSERT_FALSE(parsingErrors.has_value());
 | 
			
		||||
 | 
			
		||||
    auto const result = LogService::init(config_);
 | 
			
		||||
    EXPECT_FALSE(result);
 | 
			
		||||
    EXPECT_EQ(result.error(), "Can't override settings for log channel SomeChannel: invalid channel");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
TEST_F(LogServiceInitTests, LogSizeAndHourRotationCannotBeZero)
 | 
			
		||||
{
 | 
			
		||||
    std::vector<std::string_view> const keys{"log_directory_max_files", "log_rotation_size"};
 | 
			
		||||
 | 
			
		||||
    auto const jsonStr = fmt::format(
 | 
			
		||||
        R"JSON(
 | 
			
		||||
            {{
 | 
			
		||||
                "{}": 0,
 | 
			
		||||
                "{}": 0
 | 
			
		||||
            }}
 | 
			
		||||
        )JSON",
 | 
			
		||||
        keys[0],
 | 
			
		||||
        keys[1]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    auto const parsingErrors = config_.parse(ConfigFileJson{boost::json::parse(jsonStr).as_object()});
 | 
			
		||||
    ASSERT_EQ(parsingErrors->size(), 2);
 | 
			
		||||
    for (std::size_t i = 0; i < parsingErrors->size(); ++i) {
 | 
			
		||||
        EXPECT_EQ(
 | 
			
		||||
            (*parsingErrors)[i].error,
 | 
			
		||||
            fmt::format("{} Number must be between 1 and {}", keys[i], std::numeric_limits<uint32_t>::max())
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -18,27 +18,14 @@
 | 
			
		||||
//==============================================================================
 | 
			
		||||
 | 
			
		||||
#include "util/LoggerFixtures.hpp"
 | 
			
		||||
#include "util/config/Array.hpp"
 | 
			
		||||
#include "util/config/ConfigConstraints.hpp"
 | 
			
		||||
#include "util/config/ConfigDefinition.hpp"
 | 
			
		||||
#include "util/config/ConfigFileJson.hpp"
 | 
			
		||||
#include "util/config/ConfigValue.hpp"
 | 
			
		||||
#include "util/config/Types.hpp"
 | 
			
		||||
#include "util/log/Logger.hpp"
 | 
			
		||||
 | 
			
		||||
#include <boost/json/array.hpp>
 | 
			
		||||
#include <boost/json/object.hpp>
 | 
			
		||||
#include <boost/json/parse.hpp>
 | 
			
		||||
#include <fmt/format.h>
 | 
			
		||||
#include <gmock/gmock.h>
 | 
			
		||||
#include <gtest/gtest.h>
 | 
			
		||||
 | 
			
		||||
#include <cstddef>
 | 
			
		||||
#include <cstdint>
 | 
			
		||||
#include <limits>
 | 
			
		||||
#include <string>
 | 
			
		||||
#include <string_view>
 | 
			
		||||
#include <vector>
 | 
			
		||||
using namespace util;
 | 
			
		||||
 | 
			
		||||
// Used as a fixture for tests with enabled logging
 | 
			
		||||
@@ -51,189 +38,27 @@ TEST_F(LoggerTest, Basic)
 | 
			
		||||
{
 | 
			
		||||
    Logger const log{"General"};
 | 
			
		||||
    log.info() << "Info line logged";
 | 
			
		||||
    checkEqual("General:NFO Info line logged");
 | 
			
		||||
    ASSERT_EQ(getLoggerString(), "inf:General - Info line logged\n");
 | 
			
		||||
 | 
			
		||||
    LogService::debug() << "Debug line with numbers " << 12345;
 | 
			
		||||
    checkEqual("General:DBG Debug line with numbers 12345");
 | 
			
		||||
    ASSERT_EQ(getLoggerString(), "deb:General - Debug line with numbers 12345\n");
 | 
			
		||||
 | 
			
		||||
    LogService::warn() << "Warning is logged";
 | 
			
		||||
    checkEqual("General:WRN Warning is logged");
 | 
			
		||||
    ASSERT_EQ(getLoggerString(), "war:General - Warning is logged\n");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
TEST_F(LoggerTest, Filtering)
 | 
			
		||||
{
 | 
			
		||||
    Logger const log{"General"};
 | 
			
		||||
    log.trace() << "Should not be logged";
 | 
			
		||||
    checkEmpty();
 | 
			
		||||
    ASSERT_TRUE(getLoggerString().empty());
 | 
			
		||||
 | 
			
		||||
    log.warn() << "Warning is logged";
 | 
			
		||||
    checkEqual("General:WRN Warning is logged");
 | 
			
		||||
    ASSERT_EQ(getLoggerString(), "war:General - Warning is logged\n");
 | 
			
		||||
 | 
			
		||||
    Logger const tlog{"Trace"};
 | 
			
		||||
    tlog.trace() << "Trace line logged for 'Trace' component";
 | 
			
		||||
    checkEqual("Trace:TRC Trace line logged for 'Trace' component");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
using util::config::Array;
 | 
			
		||||
using util::config::ConfigFileJson;
 | 
			
		||||
using util::config::ConfigType;
 | 
			
		||||
using util::config::ConfigValue;
 | 
			
		||||
 | 
			
		||||
struct LoggerInitTest : LoggerTest {
 | 
			
		||||
protected:
 | 
			
		||||
    util::config::ClioConfigDefinition config_{
 | 
			
		||||
        {"log_channels.[].channel", Array{ConfigValue{ConfigType::String}}},
 | 
			
		||||
        {"log_channels.[].log_level", Array{ConfigValue{ConfigType::String}}},
 | 
			
		||||
 | 
			
		||||
        {"log_level", ConfigValue{ConfigType::String}.defaultValue("info")},
 | 
			
		||||
 | 
			
		||||
        {"log_format",
 | 
			
		||||
         ConfigValue{ConfigType::String}.defaultValue(
 | 
			
		||||
             R"(%TimeStamp% (%SourceLocation%) [%ThreadID%] %Channel%:%Severity% %Message%)"
 | 
			
		||||
         )},
 | 
			
		||||
 | 
			
		||||
        {"log_to_console", ConfigValue{ConfigType::Boolean}.defaultValue(false)},
 | 
			
		||||
 | 
			
		||||
        {"log_directory", ConfigValue{ConfigType::String}.optional()},
 | 
			
		||||
 | 
			
		||||
        {"log_rotation_size",
 | 
			
		||||
         ConfigValue{ConfigType::Integer}.defaultValue(2048).withConstraint(config::gValidateUint32)},
 | 
			
		||||
 | 
			
		||||
        {"log_directory_max_size",
 | 
			
		||||
         ConfigValue{ConfigType::Integer}.defaultValue(50 * 1024).withConstraint(config::gValidateUint32)},
 | 
			
		||||
 | 
			
		||||
        {"log_rotation_hour_interval",
 | 
			
		||||
         ConfigValue{ConfigType::Integer}.defaultValue(12).withConstraint(config::gValidateUint32)},
 | 
			
		||||
 | 
			
		||||
        {"log_tag_style", ConfigValue{ConfigType::String}.defaultValue("none")},
 | 
			
		||||
    };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
TEST_F(LoggerInitTest, DefaultLogLevel)
 | 
			
		||||
{
 | 
			
		||||
    auto const parsingErrors = config_.parse(ConfigFileJson{boost::json::object{{"log_level", "warn"}}});
 | 
			
		||||
    ASSERT_FALSE(parsingErrors.has_value());
 | 
			
		||||
    std::string const logString = "some log";
 | 
			
		||||
 | 
			
		||||
    EXPECT_TRUE(LogService::init(config_));
 | 
			
		||||
    for (auto const& channel : Logger::kCHANNELS) {
 | 
			
		||||
        Logger const log{channel};
 | 
			
		||||
        log.trace() << logString;
 | 
			
		||||
        checkEmpty();
 | 
			
		||||
 | 
			
		||||
        log.debug() << logString;
 | 
			
		||||
        checkEmpty();
 | 
			
		||||
 | 
			
		||||
        log.info() << logString;
 | 
			
		||||
        checkEmpty();
 | 
			
		||||
 | 
			
		||||
        log.warn() << logString;
 | 
			
		||||
        checkEqual(fmt::format("{}:WRN {}", channel, logString));
 | 
			
		||||
 | 
			
		||||
        log.error() << logString;
 | 
			
		||||
        checkEqual(fmt::format("{}:ERR {}", channel, logString));
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
TEST_F(LoggerInitTest, ChannelLogLevel)
 | 
			
		||||
{
 | 
			
		||||
    std::string const configStr = R"JSON(
 | 
			
		||||
    {
 | 
			
		||||
        "log_level": "error",
 | 
			
		||||
        "log_channels": [
 | 
			
		||||
            {
 | 
			
		||||
                "channel": "Backend",
 | 
			
		||||
                "log_level": "warning"
 | 
			
		||||
            }
 | 
			
		||||
        ]
 | 
			
		||||
    }
 | 
			
		||||
    )JSON";
 | 
			
		||||
 | 
			
		||||
    auto const parsingErrors = config_.parse(ConfigFileJson{boost::json::parse(configStr).as_object()});
 | 
			
		||||
    ASSERT_FALSE(parsingErrors.has_value());
 | 
			
		||||
    std::string const logString = "some log";
 | 
			
		||||
 | 
			
		||||
    EXPECT_TRUE(LogService::init(config_));
 | 
			
		||||
    for (auto const& channel : Logger::kCHANNELS) {
 | 
			
		||||
        Logger const log{channel};
 | 
			
		||||
        log.trace() << logString;
 | 
			
		||||
        checkEmpty();
 | 
			
		||||
 | 
			
		||||
        log.debug() << logString;
 | 
			
		||||
        checkEmpty();
 | 
			
		||||
 | 
			
		||||
        log.info() << logString;
 | 
			
		||||
        checkEmpty();
 | 
			
		||||
 | 
			
		||||
        log.warn() << logString;
 | 
			
		||||
        if (std::string_view{channel} == "Backend") {
 | 
			
		||||
            checkEqual(fmt::format("{}:WRN {}", channel, logString));
 | 
			
		||||
        } else {
 | 
			
		||||
            checkEmpty();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        log.error() << "some log";
 | 
			
		||||
        checkEqual(fmt::format("{}:ERR {}", channel, logString));
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
TEST_F(LoggerInitTest, InitReturnsErrorIfCouldNotCreateLogDirectory)
 | 
			
		||||
{
 | 
			
		||||
    // "/proc" directory is read only on any unix OS
 | 
			
		||||
    auto const parsingErrors = config_.parse(ConfigFileJson{boost::json::object{{"log_directory", "/proc/logs"}}});
 | 
			
		||||
    ASSERT_FALSE(parsingErrors.has_value());
 | 
			
		||||
 | 
			
		||||
    auto const result = LogService::init(config_);
 | 
			
		||||
    EXPECT_FALSE(result);
 | 
			
		||||
    EXPECT_THAT(result.error(), testing::HasSubstr("Couldn't create logs directory"));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
TEST_F(LoggerInitTest, InitReturnsErrorIfProvidedInvalidChannel)
 | 
			
		||||
{
 | 
			
		||||
    auto const jsonStr = R"JSON(
 | 
			
		||||
    {
 | 
			
		||||
        "log_channels": [
 | 
			
		||||
            {
 | 
			
		||||
                "channel": "SomeChannel",
 | 
			
		||||
                "log_level": "warn"
 | 
			
		||||
            }
 | 
			
		||||
        ]
 | 
			
		||||
    })JSON";
 | 
			
		||||
 | 
			
		||||
    auto const json = boost::json::parse(jsonStr).as_object();
 | 
			
		||||
    auto const parsingErrors = config_.parse(ConfigFileJson{json});
 | 
			
		||||
    ASSERT_FALSE(parsingErrors.has_value());
 | 
			
		||||
 | 
			
		||||
    auto const result = LogService::init(config_);
 | 
			
		||||
    EXPECT_FALSE(result);
 | 
			
		||||
    EXPECT_EQ(result.error(), "Can't override settings for log channel SomeChannel: invalid channel");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
TEST_F(LoggerInitTest, LogSizeAndHourRotationCannotBeZero)
 | 
			
		||||
{
 | 
			
		||||
    std::vector<std::string_view> const keys{
 | 
			
		||||
        "log_rotation_hour_interval", "log_directory_max_size", "log_rotation_size"
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    auto const jsonStr = fmt::format(
 | 
			
		||||
        R"JSON({{
 | 
			
		||||
        "{}": 0,
 | 
			
		||||
        "{}": 0,
 | 
			
		||||
        "{}": 0
 | 
			
		||||
    }})JSON",
 | 
			
		||||
        keys[0],
 | 
			
		||||
        keys[1],
 | 
			
		||||
        keys[2]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    auto const parsingErrors = config_.parse(ConfigFileJson{boost::json::parse(jsonStr).as_object()});
 | 
			
		||||
    ASSERT_EQ(parsingErrors->size(), 3);
 | 
			
		||||
    for (std::size_t i = 0; i < parsingErrors->size(); ++i) {
 | 
			
		||||
        EXPECT_EQ(
 | 
			
		||||
            (*parsingErrors)[i].error,
 | 
			
		||||
            fmt::format("{} Number must be between 1 and {}", keys[i], std::numeric_limits<uint32_t>::max())
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
    ASSERT_EQ(getLoggerString(), "tra:Trace - Trace line logged for 'Trace' component\n");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#ifndef COVERAGE_ENABLED
 | 
			
		||||
@@ -259,8 +84,8 @@ TEST_F(NoLoggerTest, Basic)
 | 
			
		||||
{
 | 
			
		||||
    Logger const log{"Trace"};
 | 
			
		||||
    log.trace() << "Nothing";
 | 
			
		||||
    checkEmpty();
 | 
			
		||||
    ASSERT_TRUE(getLoggerString().empty());
 | 
			
		||||
 | 
			
		||||
    LogService::fatal() << "Still nothing";
 | 
			
		||||
    checkEmpty();
 | 
			
		||||
    ASSERT_TRUE(getLoggerString().empty());
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -54,5 +54,5 @@ TEST_F(DefaultAmendmentBlockActionTest, Call)
 | 
			
		||||
{
 | 
			
		||||
    AmendmentBlockHandler::kDEFAULT_AMENDMENT_BLOCK_ACTION();
 | 
			
		||||
    auto const loggerString = getLoggerString();
 | 
			
		||||
    EXPECT_TRUE(loggerString.starts_with("ETL:FTL Can't process new ledgers")) << "LoggerString " << loggerString;
 | 
			
		||||
    EXPECT_TRUE(loggerString.starts_with("cri:ETL - Can't process new ledgers")) << "LoggerString " << loggerString;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -65,5 +65,5 @@ TEST_F(DefaultAmendmentBlockActionNgTest, Call)
 | 
			
		||||
{
 | 
			
		||||
    AmendmentBlockHandler::kDEFAULT_AMENDMENT_BLOCK_ACTION();
 | 
			
		||||
    auto const loggerString = getLoggerString();
 | 
			
		||||
    EXPECT_TRUE(loggerString.starts_with("ETL:FTL Can't process new ledgers")) << "LoggerString " << loggerString;
 | 
			
		||||
    EXPECT_TRUE(loggerString.starts_with("cri:ETL - Can't process new ledgers")) << "LoggerString " << loggerString;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1333,14 +1333,14 @@ INSTANTIATE_TEST_SUITE_P(
 | 
			
		||||
    RPCHelpersLogDurationTests,
 | 
			
		||||
    RPCHelpersLogDurationTest,
 | 
			
		||||
    testing::Values(
 | 
			
		||||
        RPCHelpersLogDurationTestBundle{"ShortDurationLogsAsInfo", std::chrono::milliseconds(500), "RPC:NFO", true},
 | 
			
		||||
        RPCHelpersLogDurationTestBundle{"ShortDurationLogsAsInfo", std::chrono::milliseconds(500), "inf:RPC ", true},
 | 
			
		||||
        RPCHelpersLogDurationTestBundle{
 | 
			
		||||
            "MediumDurationLogsAsWarning",
 | 
			
		||||
            std::chrono::milliseconds(5000),
 | 
			
		||||
            "RPC:WRN",
 | 
			
		||||
            "war:RPC ",
 | 
			
		||||
            true
 | 
			
		||||
        },
 | 
			
		||||
        RPCHelpersLogDurationTestBundle{"LongDurationLogsAsError", std::chrono::milliseconds(15000), "RPC:ERR", true}
 | 
			
		||||
        RPCHelpersLogDurationTestBundle{"LongDurationLogsAsError", std::chrono::milliseconds(15000), "err:RPC ", true}
 | 
			
		||||
    ),
 | 
			
		||||
    tests::util::kNAME_GENERATOR
 | 
			
		||||
);
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										32
									
								
								tests/unit/util/ScopeGuardTests.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								tests/unit/util/ScopeGuardTests.cpp
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,32 @@
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
/*
 | 
			
		||||
    This file is part of clio: https://github.com/XRPLF/clio
 | 
			
		||||
    Copyright (c) 2025, the clio developers.
 | 
			
		||||
 | 
			
		||||
    Permission to use, copy, modify, and distribute this software for any
 | 
			
		||||
    purpose with or without fee is hereby granted, provided that the above
 | 
			
		||||
    copyright notice and this permission notice appear in all copies.
 | 
			
		||||
 | 
			
		||||
    THE  SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 | 
			
		||||
    WITH  REGARD  TO  THIS  SOFTWARE  INCLUDING  ALL  IMPLIED  WARRANTIES  OF
 | 
			
		||||
    MERCHANTABILITY  AND  FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 | 
			
		||||
    ANY  SPECIAL,  DIRECT,  INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 | 
			
		||||
    WHATSOEVER  RESULTING  FROM  LOSS  OF USE, DATA OR PROFITS, WHETHER IN AN
 | 
			
		||||
    ACTION  OF  CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 | 
			
		||||
    OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 | 
			
		||||
*/
 | 
			
		||||
//==============================================================================
 | 
			
		||||
 | 
			
		||||
#include "util/ScopeGuard.hpp"
 | 
			
		||||
 | 
			
		||||
#include <gmock/gmock.h>
 | 
			
		||||
#include <gtest/gtest.h>
 | 
			
		||||
 | 
			
		||||
TEST(ScopeGuardTest, IsCalled)
 | 
			
		||||
{
 | 
			
		||||
    testing::StrictMock<testing::MockFunction<void()>> mockFunction;
 | 
			
		||||
    EXPECT_CALL(mockFunction, Call());
 | 
			
		||||
    {
 | 
			
		||||
        util::ScopeGuard guard([&mockFunction] { mockFunction.Call(); });
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user