diff --git a/.ci/build-kit/docker/Dockerfile b/.ci/build-kit/docker/Dockerfile new file mode 100644 index 0000000..ca655b1 --- /dev/null +++ b/.ci/build-kit/docker/Dockerfile @@ -0,0 +1,3 @@ +# syntax=docker/dockerfile:1 +ARG BASE_IMAGE_TAG=latest +FROM ghcr.io/everest/everest-ci/build-kit-base:${BASE_IMAGE_TAG} diff --git a/.ci/build-kit/install_and_test.sh b/.ci/build-kit/install_and_test.sh deleted file mode 100755 index 26b42a2..0000000 --- a/.ci/build-kit/install_and_test.sh +++ /dev/null @@ -1,15 +0,0 @@ -#!/bin/sh - -set -e - -cmake \ - -B build \ - -S "$EXT_MOUNT/source" \ - -G Ninja \ - -DBUILD_TESTING=ON \ - -DCMAKE_BUILD_TYPE=Debug \ - -DCMAKE_INSTALL_PREFIX="$WORKSPACE_PATH/dist" - -ninja -j$(nproc) -C build install - -ninja -j$(nproc) -C build test diff --git a/.ci/build-kit/scripts/compile.sh b/.ci/build-kit/scripts/compile.sh new file mode 100755 index 0000000..1c7a914 --- /dev/null +++ b/.ci/build-kit/scripts/compile.sh @@ -0,0 +1,21 @@ +#!/bin/sh + +cmake \ + -B "$EXT_MOUNT/build" \ + -S "$EXT_MOUNT/source" \ + -G Ninja \ + -DBUILD_TESTING=ON \ + -DCMAKE_BUILD_TYPE=Debug \ + -DCMAKE_INSTALL_PREFIX="$EXT_MOUNT/dist" +retVal=$? +if [ $retVal -ne 0 ]; then + echo "Configuring failed with return code $retVal" + exit $retVal +fi + +ninja -C "$EXT_MOUNT/build" +retVal=$? +if [ $retVal -ne 0 ]; then + echo "Compiling failed with return code $retVal" + exit $retVal +fi diff --git a/.ci/build-kit/scripts/install.sh b/.ci/build-kit/scripts/install.sh new file mode 100755 index 0000000..174dbc7 --- /dev/null +++ b/.ci/build-kit/scripts/install.sh @@ -0,0 +1,9 @@ +#!/bin/sh + +ninja -C "$EXT_MOUNT/build" install +retVal=$? + +if [ $retVal -ne 0 ]; then + echo "Installation failed with return code $retVal" + exit $retVal +fi diff --git a/.ci/build-kit/scripts/run_coverage.sh b/.ci/build-kit/scripts/run_coverage.sh new file mode 100755 index 0000000..7fda31f --- /dev/null +++ b/.ci/build-kit/scripts/run_coverage.sh @@ -0,0 +1,12 @@ +#!/bin/sh + +ninja -C "$EXT_MOUNT/build" everest-log_gcovr_coverage +retVal=$? + +# Copy the generated coverage report to the mounted directory in any case +cp -R "$EXT_MOUNT/build/everest-log_gcovr_coverage" "$EXT_MOUNT/gcovr-coverage" + +if [ $retVal -ne 0 ]; then + echo "Coverage failed with return code $retVal" + exit $retVal +fi diff --git a/.ci/build-kit/scripts/run_unit_tests.sh b/.ci/build-kit/scripts/run_unit_tests.sh new file mode 100755 index 0000000..c1a29d1 --- /dev/null +++ b/.ci/build-kit/scripts/run_unit_tests.sh @@ -0,0 +1,12 @@ +#!/bin/sh + +ninja -C "$EXT_MOUNT/build" test +retVal=$? + +# Copy the LastTest.log file to the mounted directory in any case +cp "$EXT_MOUNT/build/Testing/Temporary/LastTest.log" "$EXT_MOUNT/ctest-report" + +if [ $retVal -ne 0 ]; then + echo "Unit tests failed with return code $retVal" + exit $retValUnitTests +fi diff --git a/.github/workflows/build_and_test.yaml b/.github/workflows/build_and_test.yaml index cc4457f..4bb71d8 100644 --- a/.github/workflows/build_and_test.yaml +++ b/.github/workflows/build_and_test.yaml @@ -1,7 +1,4 @@ -# Please reference work here https://github.com/EVerest/everest-core/tree/main/.github/workflows -# TODO: modify to reuse the above workflow to DRY up CI. - -name: Build and test liblog +name: Build, Lint and Test on: pull_request: workflow_dispatch: @@ -14,6 +11,13 @@ on: options: - 'ubuntu-22.04' - 'large-ubuntu-22.04-xxl' + schedule: + - cron: '33 13,1 * * *' + +env: + DOCKER_REGISTRY: ghcr.io + EVEREST_CI_VERSION: v1.3.1 + jobs: lint: name: Lint @@ -24,14 +28,53 @@ jobs: with: path: source - name: Run clang-format - uses: everest/everest-ci/github-actions/run-clang-format@v1.1.0 + uses: everest/everest-ci/github-actions/run-clang-format@v1.3.1 with: source-dir: source extensions: hpp,cpp exclude: cache - install_and_test: - name: Install and test + + # Since env variables can't be passed to reusable workflows, we need to pass them as outputs + setup-env: + name: Setup Environment + runs-on: ${{ inputs.runner || 'ubuntu-22.04' }} + outputs: + docker_registry: ${{ env.DOCKER_REGISTRY }} + everest_ci_version: ${{ env.EVEREST_CI_VERSION }} + steps: + - id: check + run: | + echo "Setting up environment" + + build-and-push-build-kit: + name: Build and Push Build Kit + uses: everest/everest-ci/.github/workflows/deploy-single-docker-image.yml@v1.3.1 + needs: setup-env + secrets: + SA_GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} + SA_GITHUB_USERNAME: ${{ github.actor }} + permissions: + contents: read + packages: write + with: + image_name: ${{ github.event.repository.name }}/build-kit-${{ github.event.repository.name }} + directory: .ci/build-kit/docker + docker_registry: ${{ needs.setup-env.outputs.docker_registry }} + github_ref_before: ${{ github.event.before }} + github_ref_after: ${{ github.event.after }} + platforms: linux/amd64 + depends_on_paths: | + .ci/build-kit + .github/workflows/build_and_test.yaml + build_args: | + BASE_IMAGE_TAG=${{ needs.setup-env.outputs.everest_ci_version }} + + build: + name: Build and Unit Tests + needs: build-and-push-build-kit runs-on: ${{ inputs.runner || 'ubuntu-22.04' }} + env: + BUILD_KIT_IMAGE: ${{ needs.build-and-push-build-kit.outputs.one_image_tag_long }} steps: - name: Checkout liblog uses: actions/checkout@v3 @@ -40,21 +83,54 @@ jobs: - name: Setup run scripts run: | mkdir scripts - rsync -a source/.ci/build-kit/ scripts + rsync -a source/.ci/build-kit/scripts/ scripts - name: Pull docker container run: | - docker pull --platform=linux/x86_64 --quiet ghcr.io/everest/build-kit-alpine:latest - docker image tag ghcr.io/everest/build-kit-alpine:latest build-kit - - name: Run install with tests + docker pull --platform=linux/x86_64 --quiet ${{ env.BUILD_KIT_IMAGE }} + docker image tag ${{ env.BUILD_KIT_IMAGE }} build-kit + - name: Compile + run: | + docker run \ + --volume "${{ github.workspace }}:/ext" \ + --name compile-container \ + build-kit run-script compile + docker commit compile-container build-image + - name: Run unit tests run: | docker run \ - --volume "$(pwd):/ext" \ - --name test-container \ - build-kit run-script install_and_test + --volume "${{ github.workspace }}:/ext" \ + --name unit-test-container \ + build-image run-script run_unit_tests + docker commit unit-test-container unit-test-image - name: Archive test results if: always() - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: ctest-report - path: /workspace/build/tests/Testing/Temporary/LastTest.log - + path: ${{ github.workspace }}/ctest-report + - name: Run coverage + run: | + docker run \ + --volume "${{ github.workspace }}:/ext" \ + --name coverage-container \ + unit-test-image run-script run_coverage + - name: Archive coverage report + if: always() + uses: actions/upload-artifact@v4 + with: + name: gcovr-coverage + path: ${{ github.workspace }}/gcovr-coverage + - name: Create dist + run: | + docker run \ + --volume "${{ github.workspace }}:/ext" \ + --name install-container \ + build-image run-script install + - name: Tar dist dir and keep permissions + run: | + tar -czf dist.tar.gz dist + - name: Upload dist artifact + uses: actions/upload-artifact@v4.3.3 + with: + path: dist.tar.gz + name: dist diff --git a/CMakeLists.txt b/CMakeLists.txt index 3946e0e..3cc877c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,7 +1,7 @@ cmake_minimum_required(VERSION 3.11) project(everest-log - VERSION 0.2.1 + VERSION 0.3 DESCRIPTION "EVerest logging library" LANGUAGES CXX C ) @@ -18,9 +18,27 @@ option(BUILD_EXAMPLES "Build liblog example binaries." OFF) option(LOG_INSTALL "Install the library (shared data might be installed anyway)" ${EVC_MAIN_PROJECT}) option(CMAKE_RUN_CLANG_TIDY "Run clang-tidy" OFF) option(LIBLOG_USE_BOOST_FILESYSTEM "Usage of boost/filesystem.hpp instead of std::filesystem" OFF) +option(LIBLOG_OMIT_FILE_AND_LINE_NUMBERS "Always omit file and line numbers in logs" OFF) -# library dependencies +if((${CMAKE_PROJECT_NAME} STREQUAL ${PROJECT_NAME} OR ${PROJECT_NAME}_BUILD_TESTING) AND BUILD_TESTING) + set(LIBLOG_BUILD_TESTING ON) + evc_include(CodeCoverage) + + append_coverage_compiler_flags() +endif() +if(NOT DISABLE_EDM) + evc_setup_edm() + + # In EDM mode, we can't install exports (because the dependencies usually do not install their exports) + set(LOG_INSTALL OFF) +endif() + +# library dependencies +# use cmakes FindBoost module until we require Boost 1.70+ which provides its own BoostConfig.cmake +if("${CMAKE_VERSION}" VERSION_GREATER_EQUAL "3.30") + cmake_policy(SET CMP0167 OLD) +endif() if (LIBLOG_USE_BOOST_FILESYSTEM) message(STATUS "Using boost/filesystem instead of std::filesystem") find_package(Boost COMPONENTS log_setup log filesystem REQUIRED) @@ -68,27 +86,11 @@ endif() # testing -if((${CMAKE_PROJECT_NAME} STREQUAL ${PROJECT_NAME} OR ${PROJECT_NAME}_BUILD_TESTING) AND BUILD_TESTING) - include(CTest) - add_subdirectory(tests) - +if(LIBLOG_BUILD_TESTING) set(CMAKE_BUILD_TYPE Debug CACHE STRING "Build type" FORCE) - evc_include(CodeCoverage) - - append_coverage_compiler_flags() - - setup_target_for_coverage_gcovr_html( - NAME gcovr_coverage_liblog - EXECUTABLE test_config - DEPENDENCIES test_config everest - ) - - setup_target_for_coverage_lcov( - NAME lcov_coverage_liblog - EXECUTABLE test_config - DEPENDENCIES test_config everest - ) + include(CTest) + add_subdirectory(tests) else() message("Not running unit tests") endif() diff --git a/dependencies.yaml b/dependencies.yaml new file mode 100644 index 0000000..70602ef --- /dev/null +++ b/dependencies.yaml @@ -0,0 +1,14 @@ +libfmt: + git: https://github.com/fmtlib/fmt.git + git_tag: 10.1.0 + options: + ["FMT_TEST OFF", "FMT_DOC OFF", "BUILD_SHARED_LIBS ON", "FMT_INSTALL ON"] +spdlog: + git: https://github.com/gabime/spdlog.git + git_tag: v1.14.1 + options: + ["SPDLOG_NO_TLS ON", "SPDLOG_FMT_EXTERNAL ON"] +gtest: + git: https://github.com/google/googletest.git + git_tag: release-1.12.1 + cmake_condition: "LIBLOG_BUILD_TESTING" diff --git a/examples/logging.ini b/examples/logging.ini index 988aebd..cc9a6b6 100644 --- a/examples/logging.ini +++ b/examples/logging.ini @@ -3,7 +3,7 @@ [Core] DisableLogging=false -Filter="%Severity% >= DEBG" +Filter="(%Severity% >= VERB and %Process% contains EVerest)" [Sinks.Console] Destination=Console @@ -16,3 +16,15 @@ SeverityStringColorInfo="\033[1;37m" SeverityStringColorWarning="\033[1;33m" SeverityStringColorError="\033[1;31m" SeverityStringColorCritical="\033[1;35m" + +[Sinks.TextFile] +Destination=TextFile +FileName="FileLog%5N.log" +Format = "%TimeStamp% [%Severity%] %Message%" +RotationSize = 500 # bytes +MaxFiles = 5 + +[Sinks.Syslog] +Destination=Syslog +Format="%TimeStamp% [%Severity%] %Message%" +EnableFormatting=false diff --git a/examples/main.cpp b/examples/main.cpp index a549fb2..1a5797d 100644 --- a/examples/main.cpp +++ b/examples/main.cpp @@ -1,14 +1,26 @@ // SPDX-License-Identifier: Apache-2.0 -// Copyright 2020 - 2021 Pionix GmbH and Contributors to EVerest +// Copyright Pionix GmbH and Contributors to EVerest +#include #include #include +#include #include #include namespace po = boost::program_options; +struct TestStruct { + std::string hello; + int integer; +}; + +std::ostream& operator<<(std::ostream& os, const TestStruct& test) { + os << "TestStruct: hello " << test.hello << " " << test.integer; + return os; +} + int main(int argc, char* argv[]) { po::options_description desc("EVerest::log example"); desc.add_options()("help,h", "produce help message"); @@ -28,7 +40,7 @@ int main(int argc, char* argv[]) { if (vm.count("logconf") != 0) { logging_config = vm["logconf"].as(); } - Everest::Logging::init(logging_config, "hello there"); + Everest::Logging::init(logging_config, "hello there. EVerest"); EVLOG_debug << "logging_config was set to " << logging_config; @@ -39,5 +51,33 @@ int main(int argc, char* argv[]) { EVLOG_error << "This is a ERROR message."; EVLOG_critical << "This is a CRITICAL message."; + TestStruct test_struct{"there", 42}; + EVLOG_info << "This logs a TestStruct using a operator<<: " << test_struct; + EVLOG_info << "Test logs with an additional std::endl at the end" << std::endl; + EVLOG_info << "Test logs with different types: " << 42 << " " << 12.34; + + EVLOG_critical << Everest::Logging::LogSource("file.name", 42, "function_with_file_name_and_line_nr()") + << "This is a CRITICAL message"; + EVLOG_critical << Everest::Logging::LogSource("function_without_file_name_or_line_nr()") + << "This is a CRITICAL message"; + + auto t = std::thread([]() { EVLOG_info << "From another thread"; }); + t.join(); + + try { + EVTHROW(std::runtime_error("hello there")); + + } catch (...) { + } + try { + EVLOG_AND_THROW(std::runtime_error("hello there")); + + } catch (...) { + } + try { + EVTHROW(EVEXCEPTION(Everest::EverestInternalError, "Something", " with", " multiple", " args")); + + } catch (...) { + } return 0; } diff --git a/include/everest/logging.hpp b/include/everest/logging.hpp index 200c208..f871917 100644 --- a/include/everest/logging.hpp +++ b/include/everest/logging.hpp @@ -1,19 +1,18 @@ // SPDX-License-Identifier: Apache-2.0 -// Copyright 2020 - 2022 Pionix GmbH and Contributors to EVerest +// Copyright Pionix GmbH and Contributors to EVerest #ifndef LOGGING_HPP #define LOGGING_HPP +#include #include #include -#include -#include #include -#include -#include #include -#include #include + #include +#include +#include #include namespace Everest { @@ -32,55 +31,108 @@ void init(const std::string& logconf); void init(const std::string& logconf, std::string process_name); void update_process_name(std::string process_name); std::string trace(); + +struct LogSource { + std::string file; + int line = 0; + std::string function; + + bool log_file = true; + bool log_line = true; + bool log_function = true; + + LogSource(const std::string& file, int line, const std::string& function) : + file(file), line(line), function(function) { + } + + explicit LogSource(const std::string& function) : function(function), log_file(false), log_line(false) { + } +}; + +class EverestLogger { +public: + EverestLogger(const std::string& file, int line, const std::string& function, spdlog::level::level_enum level) : + file(file), line(line), function(function), level(level), log_file(true), log_line(true), log_function(true) { + } + EverestLogger(const std::string& function, spdlog::level::level_enum level) : + function(function), level(level), log_file(false), log_line(false), log_function(true) { + } + explicit EverestLogger(spdlog::level::level_enum level) : + level(level), log_file(false), log_line(false), log_function(false) { + } + + EverestLogger(const EverestLogger&) = delete; + void operator=(const EverestLogger&) = delete; + EverestLogger(const EverestLogger&&) = delete; + void operator=(const EverestLogger&&) = delete; + + ~EverestLogger() { +#ifdef LIBLOG_OMIT_FILE_AND_LINE_NUMBERS + log_file = false; + log_line = false; +#endif + if (log_file and log_line and log_function) { + spdlog::default_logger_raw()->log(spdlog::source_loc{file.c_str(), line, function.c_str()}, level, + log.str()); + } else if ((not log_file) and (not log_line) and log_function) { + spdlog::default_logger_raw()->log(spdlog::source_loc{"", 0, function.c_str()}, level, log.str()); + } else { + spdlog::default_logger_raw()->log(level, log.str()); + } + } + + template EverestLogger& operator<<(T const& message) { + log << message; + return *this; + } + + EverestLogger& operator<<(std::ostream& (*message)(std::ostream&)) { + log << message; + return *this; + } + + EverestLogger& operator<<(const LogSource& source) { + file = source.file; + line = source.line; + function = source.function; + log_file = source.log_file; + log_line = source.log_line; + log_function = source.log_function; + return *this; + } + +private: + std::stringstream log; + std::string file; + int line = 0; + std::string function; + spdlog::level::level_enum level; + bool log_file; + bool log_line; + bool log_function; +}; + } // namespace Logging // clang-format off -#define EVLOG_verbose \ - BOOST_LOG_SEV(::global_logger::get(), ::Everest::Logging::verbose) \ - << boost::log::BOOST_LOG_VERSION_NAMESPACE::add_value("file", __FILE__) \ - << boost::log::BOOST_LOG_VERSION_NAMESPACE::add_value("line", __LINE__) \ - << boost::log::BOOST_LOG_VERSION_NAMESPACE::add_value("function", BOOST_CURRENT_FUNCTION) - -#define EVLOG_debug \ - BOOST_LOG_SEV(::global_logger::get(), ::Everest::Logging::debug) \ - << boost::log::BOOST_LOG_VERSION_NAMESPACE::add_value("function", BOOST_CURRENT_FUNCTION) - -#define EVLOG_info \ - BOOST_LOG_SEV(::global_logger::get(), ::Everest::Logging::info) - -#define EVLOG_warning \ - BOOST_LOG_SEV(::global_logger::get(), ::Everest::Logging::warning) \ - << boost::log::BOOST_LOG_VERSION_NAMESPACE::add_value("function", BOOST_CURRENT_FUNCTION) - -#define EVLOG_error \ - BOOST_LOG_SEV(::global_logger::get(), ::Everest::Logging::error) \ - << boost::log::BOOST_LOG_VERSION_NAMESPACE::add_value("function", BOOST_CURRENT_FUNCTION) - -#define EVLOG_critical \ - BOOST_LOG_SEV(::global_logger::get(), ::Everest::Logging::critical) \ - << boost::log::BOOST_LOG_VERSION_NAMESPACE::add_value("function", BOOST_CURRENT_FUNCTION) -// clang-format on - -#define EVLOG_AND_THROW(ex) \ - do { \ - try { \ - BOOST_THROW_EXCEPTION(boost::enable_error_info(ex) \ - << boost::log::BOOST_LOG_VERSION_NAMESPACE::current_scope()); \ - } catch (std::exception & e) { \ - EVLOG_error << e.what(); \ - throw; \ - } \ - } while (0) - -#define EVTHROW(ex) \ - do { \ - BOOST_THROW_EXCEPTION(boost::enable_error_info(ex) \ - << boost::log::BOOST_LOG_VERSION_NAMESPACE::current_scope()); \ - } while (0) +#define EVLOG_verbose (::Everest::Logging::EverestLogger(__FILE__, __LINE__, BOOST_CURRENT_FUNCTION, spdlog::level::trace)) +#define EVLOG_debug (::Everest::Logging::EverestLogger(BOOST_CURRENT_FUNCTION, spdlog::level::debug)) +#define EVLOG_info (::Everest::Logging::EverestLogger(spdlog::level::info)) +#define EVLOG_warning (::Everest::Logging::EverestLogger(BOOST_CURRENT_FUNCTION, spdlog::level::warn)) +#define EVLOG_error (::Everest::Logging::EverestLogger(BOOST_CURRENT_FUNCTION, spdlog::level::err)) +#define EVLOG_critical (::Everest::Logging::EverestLogger(BOOST_CURRENT_FUNCTION, spdlog::level::critical)) + } // namespace Everest -BOOST_LOG_INLINE_GLOBAL_LOGGER_DEFAULT( - global_logger, - boost::log::BOOST_LOG_VERSION_NAMESPACE::sources::severity_logger_mt) +template constexpr void EVLOG_AND_THROW(const T& ex) { + EVLOG_error << ex.what(); + BOOST_THROW_EXCEPTION(boost::enable_error_info(ex) + << boost::log::BOOST_LOG_VERSION_NAMESPACE::current_scope()); +} + +template constexpr void EVTHROW(const T& ex) { + BOOST_THROW_EXCEPTION(boost::enable_error_info(ex) + << boost::log::BOOST_LOG_VERSION_NAMESPACE::current_scope()); +} #endif // LOGGING_HPP diff --git a/include/everest/rotating_file_sink.hpp b/include/everest/rotating_file_sink.hpp new file mode 100644 index 0000000..bbdd615 --- /dev/null +++ b/include/everest/rotating_file_sink.hpp @@ -0,0 +1,146 @@ +// Copyright(c) 2015-present, Gabi Melman & spdlog contributors. +// Distributed under the MIT License (http://opensource.org/licenses/MIT) +// Based on +// https://github.com/gabime/spdlog/blob/5ebfc927306fd7ce551fa22244be801cf2b9fdd9/include/spdlog/sinks/rotating_file_sink.h + +#pragma once + +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace Everest { +namespace Logging { + +/// \brief Rotating file sink based on size +template class rotating_file_sink : public spdlog::sinks::base_sink { +public: + rotating_file_sink(spdlog::filename_t base_filename, std::size_t max_size, std::size_t max_files, + bool rotate_on_open) : + base_filename_(std::move(base_filename)), max_size_(max_size), max_files_(max_files) { + if (max_files_ > 0) { + max_files_ = max_files_ - 1; + } + file_helper_.open(calc_filename(base_filename_, 0)); + current_size_ = file_helper_.size(); // expensive. called only once + if (max_size != 0 and rotate_on_open && current_size_ > 0) { + rotate_(); + current_size_ = 0; + } + } + + static spdlog::filename_t calc_filename(const spdlog::filename_t& filename, std::size_t index) { + std::string format_str = filename; + std::string log_file_name = filename; + try { + // find any %5N like constructs in the filename + size_t pos_percent = 0; + while ((pos_percent = format_str.find('%', pos_percent)) != std::string::npos) { + size_t pos_n = pos_percent; + pos_n = format_str.find('N', pos_n); + if (pos_n != std::string::npos) { + format_str = format_str.replace(pos_n, 1, "d"); + format_str = format_str.replace(pos_percent, 1, "%0"); + } else { + break; + } + } + log_file_name = fmt::sprintf(format_str, index); + } catch (const fmt::format_error& e) { + } + + if (log_file_name == filename) { + spdlog::filename_t basename, ext; + std::tie(basename, ext) = spdlog::details::file_helper::split_by_extension(filename); + log_file_name = fmt::format("{}{:05}{}", basename, index, ext); + } + + return log_file_name; + } + spdlog::filename_t filename() { + std::lock_guard lock(spdlog::sinks::base_sink::mutex_); + return file_helper_.filename(); + } + +protected: + void sink_it_(const spdlog::details::log_msg& msg) override { + spdlog::memory_buf_t formatted; + spdlog::sinks::base_sink::formatter_->format(msg, formatted); + auto new_size = current_size_ + formatted.size(); + + // rotate if the new estimated file size exceeds max size. + // rotate only if the real size > 0 to better deal with full disk (see issue #2261). + // we only check the real size when new_size > max_size_ because it is relatively expensive. + if (max_size_ != 0 and new_size > max_size_) { + file_helper_.flush(); + if (file_helper_.size() > 0) { + rotate_(); + new_size = formatted.size(); + } + } + file_helper_.write(formatted); + current_size_ = new_size; + } + void flush_() override { + file_helper_.flush(); + } + +private: + // Rotate files: + // log.txt -> log.1.txt + // log.1.txt -> log.2.txt + // log.2.txt -> log.3.txt + // log.3.txt -> delete + void rotate_() { + using spdlog::details::os::filename_to_str; + using spdlog::details::os::path_exists; + + file_helper_.close(); + for (auto i = max_files_; i > 0; --i) { + spdlog::filename_t src = calc_filename(base_filename_, i - 1); + if (!path_exists(src)) { + continue; + } + spdlog::filename_t target = calc_filename(base_filename_, i); + + if (!rename_file_(src, target)) { + // if failed try again after a small delay. + // this is a workaround to a windows issue, where very high rotation + // rates can cause the rename to fail with permission denied (because of antivirus?). + spdlog::details::os::sleep_for_millis(100); + if (!rename_file_(src, target)) { + file_helper_.reopen(true); // truncate the log file anyway to prevent it to grow beyond its limit! + current_size_ = 0; + spdlog::throw_spdlog_ex("rotating_file_sink: failed renaming " + filename_to_str(src) + " to " + + filename_to_str(target), + errno); + } + } + } + file_helper_.reopen(true); + } + + // delete the target if exists, and rename the src file to target + // return true on success, false otherwise. + bool rename_file_(const spdlog::filename_t& src_filename, const spdlog::filename_t& target_filename) { + // try to delete the target file in case it already exists. + (void)spdlog::details::os::remove(target_filename); + return spdlog::details::os::rename(src_filename, target_filename) == 0; + } + spdlog::filename_t base_filename_; + std::size_t max_size_; + std::size_t max_files_; + std::size_t current_size_; + spdlog::details::file_helper file_helper_; +}; + +using rotating_file_sink_mt = rotating_file_sink; + +} // namespace Logging +} // namespace Everest diff --git a/lib/CMakeLists.txt b/lib/CMakeLists.txt index de24462..f4cd0d3 100644 --- a/lib/CMakeLists.txt +++ b/lib/CMakeLists.txt @@ -16,8 +16,10 @@ target_include_directories(log target_link_libraries(log PUBLIC Boost::log + spdlog::spdlog PRIVATE Boost::log_setup + fmt::fmt ) if (LIBLOG_USE_BOOST_FILESYSTEM) @@ -31,6 +33,14 @@ if (LIBLOG_USE_BOOST_FILESYSTEM) ) endif() +if (LIBLOG_OMIT_FILE_AND_LINE_NUMBERS) + message(STATUS "liblog: Omitting file names and line numbers in all log messages") + target_compile_definitions(log + PRIVATE + LIBLOG_OMIT_FILE_AND_LINE_NUMBERS + ) +endif() + # FIXME (aw): in case FindBoost.cmake was used we need to add things # this should be removed no support for Boost < 1.74 is needed if (NOT Boost_DIR) diff --git a/lib/logging.cpp b/lib/logging.cpp index ada2e51..fe5b27c 100644 --- a/lib/logging.cpp +++ b/lib/logging.cpp @@ -1,34 +1,38 @@ // SPDX-License-Identifier: Apache-2.0 -// Copyright 2020 - 2023 Pionix GmbH and Contributors to EVerest +// Copyright Pionix GmbH and Contributors to EVerest #ifdef LIBLOG_USE_BOOST_FILESYSTEM #include #else #include #endif -#include -#include -#include -#include -#include -#include + +#ifdef __linux__ +#include +#endif + +#include + +#include +#include +#include #include -#include -#include -#include -#include #include -#include + +#include +#include +#include + +// FIXME: move this into a detail header so we do not have to expose this to the world +#include #include #include // this will only be used while bootstrapping our logging (e.g. the logging settings aren't yet applied) -// NOLINTNEXTLINE(cppcoreguidelines-macro-usage) -#define EVEREST_INTERNAL_LOG_AND_THROW(exception) \ - do { \ - BOOST_LOG_TRIVIAL(fatal) << (exception).what(); \ - throw(exception); \ - } while (0); +template constexpr void EVEREST_INTERNAL_LOG_AND_THROW(const T& exception) { + spdlog::critical("{}", (exception).what()); + throw exception; +} #ifdef LIBLOG_USE_BOOST_FILESYSTEM namespace fs = boost::filesystem; @@ -40,47 +44,294 @@ namespace attrs = logging::attributes; namespace Everest { namespace Logging { -std::array severity_strings = { - "VERB", // - "DEBG", // - "INFO", // - "WARN", // - "ERRO", // - "CRIT", // + +inline constexpr auto level_verb = 0; +inline constexpr auto level_debg = 1; +inline constexpr auto level_info = 2; +inline constexpr auto level_warn = 3; +inline constexpr auto level_erro = 4; +inline constexpr auto level_crit = 5; + +const std::array severity_strings = { + std::string("VERB"), // + std::string("DEBG"), // + std::string("INFO"), // + std::string("WARN"), // + std::string("ERRO"), // + std::string("CRIT"), // }; -std::array severity_strings_colors = { - "", // - "", // - "", // - "", // - "", // - "", // +std::array severity_strings_colors = { + std::string(""), // + std::string(""), // + std::string(""), // + std::string(""), // + std::string(""), // + std::string(""), // }; -std::string process_name_padding(const std::string& process_name) { - const unsigned int process_name_padding_length = 15; - std::string padded_process_name = process_name; - if (process_name_padding_length > padded_process_name.size()) - padded_process_name.insert(padded_process_name.size(), process_name_padding_length, ' '); - if (padded_process_name.size() > process_name_padding_length) - padded_process_name = padded_process_name.substr(0, process_name_padding_length); - return padded_process_name; +const std::string clear_color = "\033[0m"; + +std::string current_process_name; + +spdlog::level::level_enum global_level = spdlog::level::level_enum::trace; + +std::string get_process_name() { + std::string process_name; +#ifndef __linux__ + return process_name; +#endif + // first get pid and use it to get the binary name of the running process + auto pid = getpid(); + auto proc = "/proc/" + std::to_string(pid) + "/cmdline"; + fs::path proc_path = fs::path(proc); + std::ifstream cmdline_file(proc_path.c_str()); + std::vector cmdline; + if (cmdline_file.is_open()) { + std::string entry; + char input; + while (cmdline_file.get(input)) { + if (input == '\0') { + cmdline.push_back(entry); + entry = ""; + } else { + entry += input; + } + } + if (not cmdline.empty()) { + auto cmdline_path = fs::path(cmdline.at(0)); + process_name = cmdline_path.filename(); + } + cmdline_file.close(); + } + return process_name; +} + +void init(const std::string& logconf) { + init(logconf, ""); } -attrs::mutable_constant current_process_name(process_name_padding(logging::aux::get_process_name())); +class Filter { +public: + explicit Filter(const logging::filter& filter) : filter(filter) { + } -// The operator puts a human-friendly representation of the severity level to the stream -std::ostream& operator<<(std::ostream& strm, severity_level level) { - if (static_cast(level) < severity_strings.size()) { - strm << severity_strings_colors.at(level) << severity_strings.at(level) << "\033[0m"; - } else { - strm << static_cast(level); + bool filter_msg(const spdlog::details::log_msg& msg) { + auto src = logging::attribute_set(); + // TODO: proper conversion function between msg.level an severity_level + src["Severity"] = attrs::constant(static_cast(msg.level)); + src["Process"] = attrs::constant(msg.logger_name.data()); + // FIXME: support more of the boost log Filter syntax? + auto set = logging::attribute_value_set(src, logging::attribute_set(), logging::attribute_set()); + return filter(set); } - return strm; +private: + logging::filter filter; +}; + +class ConsoleFilterSink : public spdlog::sinks::ansicolor_stdout_sink_mt, public Filter { +public: + explicit ConsoleFilterSink(const logging::filter& filter) : Filter(filter) { + } + +private: + void log(const spdlog::details::log_msg& msg) override { + if (not filter_msg(msg)) { + return; + } + + spdlog::sinks::ansicolor_stdout_sink_mt::log(msg); + } +}; + +class TextFileFilterSink : public Everest::Logging::rotating_file_sink_mt, public Filter { +public: + explicit TextFileFilterSink(const logging::filter& filter, spdlog::filename_t base_filename, std::size_t max_size, + std::size_t max_files, bool rotate_on_open) : + rotating_file_sink_mt(base_filename, max_size, max_files, rotate_on_open), Filter(filter) { + } + +private: + void sink_it_(const spdlog::details::log_msg& msg) override { + if (not filter_msg(msg)) { + return; + } + + Everest::Logging::rotating_file_sink_mt::sink_it_(msg); + } +}; + +class SyslogFilterSink : public spdlog::sinks::syslog_sink_mt, public Filter { +public: + explicit SyslogFilterSink(const logging::filter& filter, std::string indet, int syslog_option, int syslog_facility, + bool enable_formatting) : + spdlog::sinks::syslog_sink_mt(indet, syslog_option, syslog_facility, enable_formatting), Filter(filter) { + } + +private: + void sink_it_(const spdlog::details::log_msg& msg) override { + if (not filter_msg(msg)) { + return; + } + + spdlog::sinks::syslog_sink_mt::sink_it_(msg); + } +}; +bool is_level(const logging::filter& filter, severity_level level) { + auto src = logging::attribute_set(); + src["Severity"] = attrs::constant(level); + src["Process"] = attrs::constant(current_process_name); + auto set = logging::attribute_value_set(src, logging::attribute_set(), logging::attribute_set()); + return filter(set); } +spdlog::level::level_enum get_level_from_filter(const logging::filter& filter) { + if (is_level(filter, severity_level::verbose)) { + return spdlog::level::level_enum::trace; + } else if (is_level(filter, severity_level::debug)) { + return spdlog::level::level_enum::debug; + } else if (is_level(filter, severity_level::info)) { + return spdlog::level::level_enum::info; + } else if (is_level(filter, severity_level::warning)) { + return spdlog::level::level_enum::warn; + } else if (is_level(filter, severity_level::error)) { + return spdlog::level::level_enum::err; + } else if (is_level(filter, severity_level::critical)) { + return spdlog::level::level_enum::critical; + } + return spdlog::level::level_enum::info; +} + +class EverestColorLevelFormatter : public spdlog::custom_flag_formatter { +public: + void format(const spdlog::details::log_msg& msg, const std::tm&, spdlog::memory_buf_t& dest) override { + switch (msg.level) { + case spdlog::level::level_enum::trace: { + auto& color = severity_strings_colors.at(level_verb); + auto& verb = severity_strings.at(level_verb); + format_message(color, verb, dest); + break; + } + case spdlog::level::level_enum::debug: { + auto& color = severity_strings_colors.at(level_debg); + auto& debg = severity_strings.at(level_debg); + format_message(color, debg, dest); + break; + } + case spdlog::level::level_enum::info: { + auto& color = severity_strings_colors.at(level_info); + auto& info = severity_strings.at(level_info); + format_message(color, info, dest); + break; + } + case spdlog::level::level_enum::warn: { + auto& color = severity_strings_colors.at(level_warn); + auto& warn = severity_strings.at(level_warn); + format_message(color, warn, dest); + break; + } + case spdlog::level::level_enum::err: { + auto& color = severity_strings_colors.at(level_erro); + auto& erro = severity_strings.at(level_erro); + format_message(color, erro, dest); + break; + } + case spdlog::level::level_enum::critical: { + auto& color = severity_strings_colors.at(level_crit); + auto& crit = severity_strings.at(level_crit); + format_message(color, crit, dest); + break; + } + case spdlog::level::level_enum::off: + [[fallthrough]]; + case spdlog::level::level_enum::n_levels: + break; + } + } + + std::unique_ptr clone() const override { + return spdlog::details::make_unique(); + } + +private: + void format_message(const std::string& color, const std::string& loglevel, spdlog::memory_buf_t& dest) { + if (not color.empty()) { + dest.append(color.data(), color.data() + color.size()); + } + dest.append(loglevel.data(), loglevel.data() + loglevel.size()); + if (not color.empty()) { + dest.append(clear_color.data(), clear_color.data() + clear_color.size()); + } + } +}; + +class EverestLevelFormatter : public spdlog::custom_flag_formatter { +public: + void format(const spdlog::details::log_msg& msg, const std::tm&, spdlog::memory_buf_t& dest) override { + switch (msg.level) { + case spdlog::level::level_enum::trace: { + auto& verb = severity_strings.at(level_verb); + format_message(verb, dest); + break; + } + case spdlog::level::level_enum::debug: { + auto& debg = severity_strings.at(level_debg); + format_message(debg, dest); + break; + } + case spdlog::level::level_enum::info: { + auto& info = severity_strings.at(level_info); + format_message(info, dest); + break; + } + case spdlog::level::level_enum::warn: { + auto& warn = severity_strings.at(level_warn); + format_message(warn, dest); + break; + } + case spdlog::level::level_enum::err: { + auto& erro = severity_strings.at(level_erro); + format_message(erro, dest); + break; + } + case spdlog::level::level_enum::critical: { + auto& crit = severity_strings.at(level_crit); + format_message(crit, dest); + break; + } + case spdlog::level::level_enum::off: + [[fallthrough]]; + case spdlog::level::level_enum::n_levels: + break; + } + } + + std::unique_ptr clone() const override { + return spdlog::details::make_unique(); + } + +private: + void format_message(const std::string& loglevel, spdlog::memory_buf_t& dest) { + dest.append(loglevel.data(), loglevel.data() + loglevel.size()); + } +}; + +class EverestFuncnameFormatter : public spdlog::custom_flag_formatter { +public: + void format(const spdlog::details::log_msg& msg, const std::tm&, spdlog::memory_buf_t& dest) override { + if (msg.source.funcname == nullptr) { + return; + } + std::string funcname = msg.source.funcname; + dest.append(funcname.data(), funcname.data() + funcname.size()); + } + + std::unique_ptr clone() const override { + return spdlog::details::make_unique(); + } +}; + // The operator parses the severity level from the stream std::istream& operator>>(std::istream& strm, severity_level& level) { if (strm.good()) { @@ -100,34 +351,16 @@ std::istream& operator>>(std::istream& strm, severity_level& level) { return strm; } -void init(const std::string& logconf) { - init(logconf, ""); -} +std::vector> sinks; void init(const std::string& logconf, std::string process_name) { - BOOST_LOG_FUNCTION(); - - // add useful attributes - logging::add_common_attributes(); - - std::string padded_process_name; - - if (!process_name.empty()) { - padded_process_name = process_name_padding(process_name); - } - - logging::core::get()->add_global_attribute("Process", current_process_name); - if (!padded_process_name.empty()) { - current_process_name.set(padded_process_name); + if (process_name.empty()) { + current_process_name = get_process_name(); + } else { + current_process_name = process_name; } - logging::core::get()->add_global_attribute("Scope", attrs::named_scope()); - - // Before initializing the library from settings, we need to register any custom filter and formatter factories - logging::register_simple_filter_factory("Severity"); - logging::register_simple_formatter_factory("Severity"); - // open logging.ini config file located at our base_dir and use it to configure boost::log logging (filters and - // format) + // open logging.ini config file located at our base_dir and use it to configure filters and format fs::path logging_path = fs::path(logconf); std::ifstream logging_config(logging_path.c_str()); if (!logging_config.is_open()) { @@ -136,32 +369,99 @@ void init(const std::string& logconf, std::string process_name) { } auto settings = logging::parse_settings(logging_config); + logging::register_simple_filter_factory("Severity"); - auto sink = settings["Sinks.Console"].get_section(); + auto core = settings["Core"].get_section(); - severity_strings_colors[severity_level::verbose] = - sink["SeverityStringColorTrace"].get().get_value_or(""); - severity_strings_colors[severity_level::debug] = - sink["SeverityStringColorDebug"].get().get_value_or(""); - severity_strings_colors[severity_level::info] = sink["SeverityStringColorInfo"].get().get_value_or(""); - severity_strings_colors[severity_level::warning] = - sink["SeverityStringColorWarning"].get().get_value_or(""); - severity_strings_colors[severity_level::error] = - sink["SeverityStringColorError"].get().get_value_or(""); - severity_strings_colors[severity_level::critical] = - sink["SeverityStringColorCritical"].get().get_value_or(""); + auto filter = core["Filter"].get().get_value_or(""); - logging::init_from_settings(settings); + auto parsed_filter = logging::parse_filter(filter); + + global_level = get_level_from_filter(parsed_filter); + + if (not settings.has_section("Sinks")) { + EVEREST_INTERNAL_LOG_AND_THROW(EverestConfigError( + std::string("No \"Sinks\" section in the logging configuration, at least one sink has be be present"))); + } + + for (auto sink : settings["Sinks"].get_section()) { + auto format = sink["Format"].get().get_value_or("%TimeStamp% [%Severity%] %Message%"); + + // parse the format into a format string that spdlog can use + std::vector> replacements = {{"%TimeStamp%", "%Y-%m-%d %H:%M:%S.%f"}, + {"%Process%", "%-15!n"}, + {"%ProcessID%", "%P"}, + {"%Severity%", "%l"}, + {"%ThreadID%", "%t"}, + {"%function%", "%!"}, + {"%file%", "%s"}, + {"%line%", "%#"}, + {"%Message%", "%v"}}; + for (auto& replace : replacements) { + auto pos = format.find(replace.first); + while (pos != std::string::npos) { + format.replace(pos, replace.first.size(), replace.second); + pos = format.find(replace.first, pos + replace.second.size()); + } + } + + auto formatter = std::make_unique(); + formatter->add_flag('!').set_pattern(format); + + auto destination = sink["Destination"].get().get_value_or(""); + if (destination == "Console") { + formatter->add_flag('l').set_pattern(format); + + severity_strings_colors[severity_level::verbose] = + sink["SeverityStringColorTrace"].get().get_value_or(""); + severity_strings_colors[severity_level::debug] = + sink["SeverityStringColorDebug"].get().get_value_or(""); + severity_strings_colors[severity_level::info] = + sink["SeverityStringColorInfo"].get().get_value_or(""); + severity_strings_colors[severity_level::warning] = + sink["SeverityStringColorWarning"].get().get_value_or(""); + severity_strings_colors[severity_level::error] = + sink["SeverityStringColorError"].get().get_value_or(""); + severity_strings_colors[severity_level::critical] = + sink["SeverityStringColorCritical"].get().get_value_or(""); + auto console_sink = std::make_shared(parsed_filter); + console_sink->set_formatter(std::move(formatter)); + sinks.push_back(console_sink); + } else if (destination == "TextFile") { + formatter->add_flag('l').set_pattern(format); + + auto file_name = sink["FileName"].get().get_value_or("%5N.log"); + auto rotation_size = sink["RotationSize"].get().get_value_or(0); + auto rotate_on_open = sink["RotateOnOpen"].get().get_value_or(false); + auto max_files = sink["MaxFiles"].get().get_value_or(0); + + auto file_sink = std::make_shared(parsed_filter, file_name, rotation_size, max_files, + rotate_on_open); + file_sink->set_formatter(std::move(formatter)); + sinks.push_back(file_sink); + } else if (destination == "Syslog") { + // TODO implement LocalAddress and TargetAddress settings + auto enable_formatting = sink["EnableFormatting"].get().get_value_or(false); + + formatter->add_flag('l').set_pattern(format); + auto syslog_sink = + std::make_shared(parsed_filter, current_process_name, 0, LOG_USER, enable_formatting); + syslog_sink->set_formatter(std::move(formatter)); + sinks.push_back(syslog_sink); + } + } + + update_process_name(current_process_name); EVLOG_debug << "Logger initialized (using " << logconf << ")..."; } void update_process_name(std::string process_name) { if (!process_name.empty()) { - std::string padded_process_name; - - padded_process_name = process_name_padding(process_name); - current_process_name.set(padded_process_name); + current_process_name = process_name; + auto filter_logger = std::make_shared(current_process_name, std::begin(sinks), std::end(sinks)); + spdlog::set_default_logger(filter_logger); + spdlog::set_level(global_level); } } } // namespace Logging diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 7751b2e..bce6bca 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1,17 +1,63 @@ -find_package(GTest REQUIRED) +setup_target_for_coverage_gcovr_html( + NAME ${PROJECT_NAME}_gcovr_coverage + EXECUTABLE ctest + DEPENDENCIES ${PROJECT_NAME}_tests + EXCLUDE "examples/*" +) + +setup_target_for_coverage_gcovr_xml( + NAME ${PROJECT_NAME}_gcovr_coverage_xml + EXECUTABLE ctest + DEPENDENCIES ${PROJECT_NAME}_tests + EXCLUDE "examples/*" +) + +setup_target_for_coverage_lcov( + NAME ${PROJECT_NAME}_lcov_coverage + EXECUTABLE ctest + DEPENDENCIES ${PROJECT_NAME}_tests + LCOV_ARGS "--ignore-errors=empty,missing" + EXCLUDE "examples/*" +) set(TEST_TARGET_NAME ${PROJECT_NAME}_tests) -add_executable(${TEST_TARGET_NAME} liblog_test.cpp) +add_executable(${TEST_TARGET_NAME}) -target_include_directories(${TEST_TARGET_NAME} PUBLIC ${GTEST_INCLUDE_DIRS}) target_include_directories(log PUBLIC $ $ ) + target_link_libraries(${TEST_TARGET_NAME} PRIVATE - ${GTEST_LIBRARIES} - ${GTEST_MAIN_LIBRARIES} + everest::log + GTest::gtest_main + GTest::gmock_main +) + +target_sources(${TEST_TARGET_NAME} PRIVATE + liblog_test.cpp ) -gtest_discover_tests(${TEST_TARGET_NAME}) +if (BUILD_BACKTRACE_SUPPORT) + target_compile_definitions(${TEST_TARGET_NAME} PRIVATE WITH_LIBBACKTRACE) +endif() + +add_test(${TEST_TARGET_NAME} ${TEST_TARGET_NAME}) + +# copy logging configs +configure_file(logging_configs/no_sinks.ini logging_configs/no_sinks.ini COPYONLY) +configure_file(logging_configs/console.ini logging_configs/console.ini COPYONLY) +configure_file(logging_configs/console_defaults.ini logging_configs/console_defaults.ini COPYONLY) +configure_file(logging_configs/textfile_verb.ini logging_configs/textfile_verb.ini COPYONLY) +configure_file(logging_configs/textfile_verb_rotate.ini logging_configs/textfile_verb_rotate.ini COPYONLY) +configure_file(logging_configs/textfile_verb_rotate_on_open.ini logging_configs/textfile_verb_rotate_on_open.ini COPYONLY) +configure_file(logging_configs/textfile_verb_broken_filename.ini logging_configs/textfile_verb_broken_filename.ini COPYONLY) +configure_file(logging_configs/textfile_verb_broken_format_string.ini logging_configs/textfile_verb_broken_format_string.ini COPYONLY) +configure_file(logging_configs/textfile_debg.ini logging_configs/textfile_debg.ini COPYONLY) +configure_file(logging_configs/textfile_info.ini logging_configs/textfile_info.ini COPYONLY) +configure_file(logging_configs/textfile_warn.ini logging_configs/textfile_warn.ini COPYONLY) +configure_file(logging_configs/textfile_erro.ini logging_configs/textfile_erro.ini COPYONLY) +configure_file(logging_configs/textfile_crit.ini logging_configs/textfile_crit.ini COPYONLY) +configure_file(logging_configs/textfile_off.ini logging_configs/textfile_off.ini COPYONLY) +configure_file(logging_configs/syslog.ini logging_configs/syslog.ini COPYONLY) diff --git a/tests/liblog_test.cpp b/tests/liblog_test.cpp index 578b41d..1036c28 100644 --- a/tests/liblog_test.cpp +++ b/tests/liblog_test.cpp @@ -1,9 +1,37 @@ // SPDX-License-Identifier: Apache-2.0 -// Copyright 2020 - 2023 Pionix GmbH and Contributors to EVerest +// Copyright Pionix GmbH and Contributors to EVerest +#include +#include +#include +#include + +#include #include +#include + +#include +#include namespace Everest { namespace Logging { +namespace tests { + +class TestSink : public spdlog::sinks::base_sink { +public: + // return number of log messages logged + size_t count() { + return this->counter; + } + +protected: + void sink_it_(const spdlog::details::log_msg& message) override { + this->counter += 1; + } + + void flush_() override { + } + size_t counter = 0; +}; class LibLogUnitTest : public ::testing::Test { protected: @@ -11,12 +39,277 @@ class LibLogUnitTest : public ::testing::Test { } void TearDown() override { + auto logger = spdlog::default_logger(); + for (auto& sink : logger->sinks()) { + sink->set_level(spdlog::level::off); + } + } + + void log_with_all_loglevels() { + EVLOG_verbose << "This is a VERBOSE message."; + EVLOG_debug << "This is a DEBUG message."; + EVLOG_info << "This is a INFO message."; + EVLOG_warning << "This is a WARNING message."; + EVLOG_error << "This is a ERROR message."; + EVLOG_critical << "This is a CRITICAL message."; + } + + int count_log_entries(const std::filesystem::path& file_name) { + std::ifstream log(file_name.string()); + EXPECT_TRUE(log.is_open()); + std::string line; + auto count = 0; + while (std::getline(log, line)) { + count += 1; + } + log.close(); + return count; } }; -TEST(LibLogUnitTest, test_truth) { - ASSERT_TRUE(1 == 1); +TEST_F(LibLogUnitTest, test_log_source_file_linenr_function) { + auto log_source = LogSource("file", 42, "function"); + EXPECT_TRUE(log_source.log_file); + EXPECT_TRUE(log_source.log_line); + EXPECT_TRUE(log_source.log_function); +} + +TEST_F(LibLogUnitTest, test_log_source_function) { + auto log_source = LogSource("function"); + EXPECT_FALSE(log_source.log_file); + EXPECT_FALSE(log_source.log_line); + EXPECT_TRUE(log_source.log_function); +} + +TEST_F(LibLogUnitTest, test_evthrow) { + ASSERT_THROW(EVTHROW(std::runtime_error("hello there")), std::runtime_error); +} + +TEST_F(LibLogUnitTest, test_evlog_and_throw) { + ASSERT_THROW(EVLOG_AND_THROW(std::runtime_error("hello there")), std::runtime_error); +} + +TEST_F(LibLogUnitTest, test_everest_logger) { + auto test_sink = std::make_shared(); + auto test_logger = std::make_shared("", test_sink); + spdlog::set_default_logger(test_logger); + spdlog::set_level(spdlog::level::level_enum::trace); + EVLOG_verbose << "hello there"; + EVLOG_info << "hello there"; + EVLOG_warning << "hello there"; + ASSERT_EQ(test_sink->count(), 3); +} + +TEST_F(LibLogUnitTest, test_init_wrong_filename) { + auto test_sink = std::make_shared(); + auto test_logger = std::make_shared("", test_sink); + spdlog::set_default_logger(test_logger); + spdlog::set_level(spdlog::level::level_enum::trace); + ASSERT_THROW(Everest::Logging::init("this_file_does_not_exist", "liblog_test"), Everest::EverestConfigError); +} + +TEST_F(LibLogUnitTest, test_init_no_sinks) { + auto test_sink = std::make_shared(); + auto test_logger = std::make_shared("", test_sink); + spdlog::set_default_logger(test_logger); + spdlog::set_level(spdlog::level::level_enum::trace); + ASSERT_THROW(Everest::Logging::init("logging_configs/no_sinks.ini", "liblog_test"), Everest::EverestConfigError); +} + +TEST_F(LibLogUnitTest, test_init_console_sink) { + Everest::Logging::init("logging_configs/console.ini", "liblog_test"); + log_with_all_loglevels(); +} + +TEST_F(LibLogUnitTest, test_init_console_sink_defaults) { + Everest::Logging::init("logging_configs/console_defaults.ini", "liblog_test"); + log_with_all_loglevels(); +} + +TEST_F(LibLogUnitTest, test_init_console_sink_no_process_name) { + Everest::Logging::init("logging_configs/console.ini"); + log_with_all_loglevels(); +} + +TEST_F(LibLogUnitTest, test_init_console_sink_correct_filter) { + Everest::Logging::init("logging_configs/console.ini", "EVerest"); + log_with_all_loglevels(); +} + +TEST_F(LibLogUnitTest, test_init_textfile_sink_verb) { + auto log_dir = std::filesystem::path("liblog_test_logs_verb"); + std::filesystem::remove_all(log_dir); + auto file_name = log_dir / "FileLog00000.log"; + Everest::Logging::init("logging_configs/textfile_verb.ini", "EVerest"); + log_with_all_loglevels(); + spdlog::default_logger()->flush(); + + ASSERT_EQ(count_log_entries(file_name), 7); +} + +TEST_F(LibLogUnitTest, test_init_textfile_sink_debg) { + auto log_dir = std::filesystem::path("liblog_test_logs_debg"); + std::filesystem::remove_all(log_dir); + auto file_name = log_dir / "FileLog00000.log"; + Everest::Logging::init("logging_configs/textfile_debg.ini", "EVerest"); + log_with_all_loglevels(); + spdlog::default_logger()->flush(); + + ASSERT_EQ(count_log_entries(file_name), 6); +} + +TEST_F(LibLogUnitTest, test_init_textfile_sink_info) { + auto log_dir = std::filesystem::path("liblog_test_logs_info"); + std::filesystem::remove_all(log_dir); + auto file_name = log_dir / "FileLog00000.log"; + Everest::Logging::init("logging_configs/textfile_info.ini", "EVerest"); + log_with_all_loglevels(); + spdlog::default_logger()->flush(); + + ASSERT_EQ(count_log_entries(file_name), 4); +} + +TEST_F(LibLogUnitTest, test_init_textfile_sink_warn) { + auto log_dir = std::filesystem::path("liblog_test_logs_warn"); + std::filesystem::remove_all(log_dir); + auto file_name = log_dir / "FileLog00000.log"; + Everest::Logging::init("logging_configs/textfile_warn.ini", "EVerest"); + log_with_all_loglevels(); + spdlog::default_logger()->flush(); + + ASSERT_EQ(count_log_entries(file_name), 3); +} + +TEST_F(LibLogUnitTest, test_init_textfile_sink_erro) { + auto log_dir = std::filesystem::path("liblog_test_logs_erro"); + std::filesystem::remove_all(log_dir); + auto file_name = log_dir / "FileLog00000.log"; + Everest::Logging::init("logging_configs/textfile_erro.ini", "EVerest"); + log_with_all_loglevels(); + spdlog::default_logger()->flush(); + + ASSERT_EQ(count_log_entries(file_name), 2); +} + +TEST_F(LibLogUnitTest, test_init_textfile_sink_crit) { + auto log_dir = std::filesystem::path("liblog_test_logs_crit"); + std::filesystem::remove_all(log_dir); + auto file_name = log_dir / "FileLog00000.log"; + Everest::Logging::init("logging_configs/textfile_crit.ini", "EVerest"); + log_with_all_loglevels(); + spdlog::default_logger()->flush(); + + ASSERT_EQ(count_log_entries(file_name), 1); +} + +TEST_F(LibLogUnitTest, test_init_textfile_sink_off) { + auto log_dir = std::filesystem::path("liblog_test_logs_off"); + std::filesystem::remove_all(log_dir); + auto file_name = log_dir / "FileLog00000.log"; + Everest::Logging::init("logging_configs/textfile_off.ini", "EVerest"); + log_with_all_loglevels(); + spdlog::default_logger()->flush(); + + ASSERT_EQ(count_log_entries(file_name), 0); +} + +TEST_F(LibLogUnitTest, test_init_textfile_sink_verb_rotate) { + auto log_dir = std::filesystem::path("liblog_test_logs_verb_rotate"); + std::filesystem::remove_all(log_dir); + Everest::Logging::init("logging_configs/textfile_verb_rotate.ini", "EVerest"); + log_with_all_loglevels(); + spdlog::default_logger()->flush(); + + auto count = 0; + for (auto& entry : std::filesystem::directory_iterator(log_dir)) { + count += 1; + } + + ASSERT_EQ(count, 5); +} + +TEST_F(LibLogUnitTest, test_init_textfile_sink_verb_rotate_on_open) { + auto log_dir = std::filesystem::path("liblog_test_logs_verb_rotate_on_open"); + std::filesystem::remove_all(log_dir); + Everest::Logging::init("logging_configs/textfile_verb_rotate_on_open.ini", "EVerest"); + log_with_all_loglevels(); + spdlog::default_logger()->flush(); + + auto count = 0; + for (auto& entry : std::filesystem::directory_iterator(log_dir)) { + count += 1; + } + + ASSERT_EQ(count, 4); + + Everest::Logging::init("logging_configs/textfile_verb_rotate_on_open.ini", "EVerest"); + log_with_all_loglevels(); + spdlog::default_logger()->flush(); + + auto count2 = 0; + for (auto& entry : std::filesystem::directory_iterator(log_dir)) { + count2 += 1; + } + + ASSERT_EQ(count2, 5); +} + +TEST_F(LibLogUnitTest, test_init_textfile_sink_verb_broken_filename) { + auto log_dir = std::filesystem::path("liblog_test_logs_verb_rotate_broken_filename"); + std::filesystem::remove_all(log_dir); + auto file_name = log_dir / "NoFormatString00000.log"; + Everest::Logging::init("logging_configs/textfile_verb_broken_filename.ini", "EVerest"); + log_with_all_loglevels(); + spdlog::default_logger()->flush(); + + auto count = 0; + for (auto& entry : std::filesystem::directory_iterator(log_dir)) { + count += 1; + } + + ASSERT_EQ(count, 5); + + ASSERT_EQ(count_log_entries(file_name), 1); +} + +TEST_F(LibLogUnitTest, test_init_textfile_sink_verb_broken_format_string) { + auto log_dir = std::filesystem::path("liblog_test_logs_verb_rotate_broken_format_string"); + std::filesystem::remove_all(log_dir); + auto file_name = log_dir / "BrokenFormatString%s00000.log"; + Everest::Logging::init("logging_configs/textfile_verb_broken_format_string.ini", "EVerest"); + log_with_all_loglevels(); + spdlog::default_logger()->flush(); + + auto count = 0; + for (auto& entry : std::filesystem::directory_iterator(log_dir)) { + count += 1; + } + + ASSERT_EQ(count, 5); + + ASSERT_EQ(count_log_entries(file_name), 1); +} + +TEST_F(LibLogUnitTest, test_init_syslog_sink) { + Everest::Logging::init("logging_configs/syslog.ini", "EVerest"); + log_with_all_loglevels(); +} + +TEST_F(LibLogUnitTest, test_init_syslog_sink_filtered) { + Everest::Logging::init("logging_configs/syslog.ini", "Everest"); + log_with_all_loglevels(); +} + +TEST_F(LibLogUnitTest, test_trace) { + auto trace_str = Everest::Logging::trace(); +#ifdef WITH_LIBBACKTRACE + EXPECT_THAT(trace_str, + ::testing::StartsWith("#0: Everest::Logging::tests::LibLogUnitTest_test_trace_Test::TestBody()")); +#else + ASSERT_EQ(trace_str, "Backtrace functionality not built in\n"); +#endif } +} // namespace tests } // namespace Logging } // namespace Everest diff --git a/tests/logging_configs/console.ini b/tests/logging_configs/console.ini new file mode 100644 index 0000000..b239832 --- /dev/null +++ b/tests/logging_configs/console.ini @@ -0,0 +1,14 @@ +[Core] +DisableLogging=false +Filter="(%Severity% >= VERB and %Process% contains EVerest)" + +[Sinks.Console] +Destination=Console +Format="%TimeStamp% \033[1;32m%Process%\033[0m [\033[1;32m%ProcessID%\033[0m] [%Severity%] {\033[1;34m%ThreadID%\033[0m} \033[1;36m%function%\033[0m \033[1;30m%file%:\033[0m\033[1;32m%line%\033[0m: %Message%" +Asynchronous=false +AutoFlush=true +SeverityStringColorDebug="\033[1;30m" +SeverityStringColorInfo="\033[1;37m" +SeverityStringColorWarning="\033[1;33m" +SeverityStringColorError="\033[1;31m" +SeverityStringColorCritical="\033[1;35m" diff --git a/tests/logging_configs/console_crit.ini b/tests/logging_configs/console_crit.ini new file mode 100644 index 0000000..56f99e0 --- /dev/null +++ b/tests/logging_configs/console_crit.ini @@ -0,0 +1,14 @@ +[Core] +DisableLogging=false +Filter="(%Severity% >= CRIT and %Process% contains EVerest)" + +[Sinks.Console] +Destination=Console +Format="%TimeStamp% \033[1;32m%Process%\033[0m [\033[1;32m%ProcessID%\033[0m] [%Severity%] {\033[1;34m%ThreadID%\033[0m} \033[1;36m%function%\033[0m \033[1;30m%file%:\033[0m\033[1;32m%line%\033[0m: %Message%" +Asynchronous=false +AutoFlush=true +SeverityStringColorDebug="\033[1;30m" +SeverityStringColorInfo="\033[1;37m" +SeverityStringColorWarning="\033[1;33m" +SeverityStringColorError="\033[1;31m" +SeverityStringColorCritical="\033[1;35m" diff --git a/tests/logging_configs/console_debg.ini b/tests/logging_configs/console_debg.ini new file mode 100644 index 0000000..1601fd1 --- /dev/null +++ b/tests/logging_configs/console_debg.ini @@ -0,0 +1,14 @@ +[Core] +DisableLogging=false +Filter="(%Severity% >= DEBG and %Process% contains EVerest)" + +[Sinks.Console] +Destination=Console +Format="%TimeStamp% \033[1;32m%Process%\033[0m [\033[1;32m%ProcessID%\033[0m] [%Severity%] {\033[1;34m%ThreadID%\033[0m} \033[1;36m%function%\033[0m \033[1;30m%file%:\033[0m\033[1;32m%line%\033[0m: %Message%" +Asynchronous=false +AutoFlush=true +SeverityStringColorDebug="\033[1;30m" +SeverityStringColorInfo="\033[1;37m" +SeverityStringColorWarning="\033[1;33m" +SeverityStringColorError="\033[1;31m" +SeverityStringColorCritical="\033[1;35m" diff --git a/tests/logging_configs/console_defaults.ini b/tests/logging_configs/console_defaults.ini new file mode 100644 index 0000000..d4aa642 --- /dev/null +++ b/tests/logging_configs/console_defaults.ini @@ -0,0 +1,5 @@ +[Core] +DisableLogging=false + +[Sinks.Console] +Destination=Console diff --git a/tests/logging_configs/console_erro.ini b/tests/logging_configs/console_erro.ini new file mode 100644 index 0000000..44fdeba --- /dev/null +++ b/tests/logging_configs/console_erro.ini @@ -0,0 +1,14 @@ +[Core] +DisableLogging=false +Filter="(%Severity% >= ERRO and %Process% contains EVerest)" + +[Sinks.Console] +Destination=Console +Format="%TimeStamp% \033[1;32m%Process%\033[0m [\033[1;32m%ProcessID%\033[0m] [%Severity%] {\033[1;34m%ThreadID%\033[0m} \033[1;36m%function%\033[0m \033[1;30m%file%:\033[0m\033[1;32m%line%\033[0m: %Message%" +Asynchronous=false +AutoFlush=true +SeverityStringColorDebug="\033[1;30m" +SeverityStringColorInfo="\033[1;37m" +SeverityStringColorWarning="\033[1;33m" +SeverityStringColorError="\033[1;31m" +SeverityStringColorCritical="\033[1;35m" diff --git a/tests/logging_configs/console_info.ini b/tests/logging_configs/console_info.ini new file mode 100644 index 0000000..354074d --- /dev/null +++ b/tests/logging_configs/console_info.ini @@ -0,0 +1,14 @@ +[Core] +DisableLogging=false +Filter="(%Severity% >= INFO and %Process% contains EVerest)" + +[Sinks.Console] +Destination=Console +Format="%TimeStamp% \033[1;32m%Process%\033[0m [\033[1;32m%ProcessID%\033[0m] [%Severity%] {\033[1;34m%ThreadID%\033[0m} \033[1;36m%function%\033[0m \033[1;30m%file%:\033[0m\033[1;32m%line%\033[0m: %Message%" +Asynchronous=false +AutoFlush=true +SeverityStringColorDebug="\033[1;30m" +SeverityStringColorInfo="\033[1;37m" +SeverityStringColorWarning="\033[1;33m" +SeverityStringColorError="\033[1;31m" +SeverityStringColorCritical="\033[1;35m" diff --git a/tests/logging_configs/console_warn.ini b/tests/logging_configs/console_warn.ini new file mode 100644 index 0000000..86ec5d5 --- /dev/null +++ b/tests/logging_configs/console_warn.ini @@ -0,0 +1,14 @@ +[Core] +DisableLogging=false +Filter="(%Severity% >= WARN and %Process% contains EVerest)" + +[Sinks.Console] +Destination=Console +Format="%TimeStamp% \033[1;32m%Process%\033[0m [\033[1;32m%ProcessID%\033[0m] [%Severity%] {\033[1;34m%ThreadID%\033[0m} \033[1;36m%function%\033[0m \033[1;30m%file%:\033[0m\033[1;32m%line%\033[0m: %Message%" +Asynchronous=false +AutoFlush=true +SeverityStringColorDebug="\033[1;30m" +SeverityStringColorInfo="\033[1;37m" +SeverityStringColorWarning="\033[1;33m" +SeverityStringColorError="\033[1;31m" +SeverityStringColorCritical="\033[1;35m" diff --git a/tests/logging_configs/no_sinks.ini b/tests/logging_configs/no_sinks.ini new file mode 100644 index 0000000..6aa70a5 --- /dev/null +++ b/tests/logging_configs/no_sinks.ini @@ -0,0 +1,3 @@ +[Core] +DisableLogging=false +Filter="(%Severity% >= VERB and %Process% contains EVerest)" diff --git a/tests/logging_configs/syslog.ini b/tests/logging_configs/syslog.ini new file mode 100644 index 0000000..76dc5ca --- /dev/null +++ b/tests/logging_configs/syslog.ini @@ -0,0 +1,8 @@ +[Core] +DisableLogging=false +Filter="(%Severity% >= VERB and %Process% contains EVerest)" + +[Sinks.Syslog] +Destination=Syslog +Format="%TimeStamp% [%Severity%] %Message%" +EnableFormatting=false diff --git a/tests/logging_configs/textfile_crit.ini b/tests/logging_configs/textfile_crit.ini new file mode 100644 index 0000000..df79f1f --- /dev/null +++ b/tests/logging_configs/textfile_crit.ini @@ -0,0 +1,10 @@ +[Core] +DisableLogging=false +Filter="(%Severity% >= CRIT and %Process% contains EVerest)" + +[Sinks.TextFile] +Destination=TextFile +FileName="liblog_test_logs_crit/FileLog%5N.log" +Format = "%TimeStamp% [%Severity%] %Message%" +RotationSize = 500 # bytes +MaxFiles = 5 diff --git a/tests/logging_configs/textfile_debg.ini b/tests/logging_configs/textfile_debg.ini new file mode 100644 index 0000000..a8ecd1b --- /dev/null +++ b/tests/logging_configs/textfile_debg.ini @@ -0,0 +1,10 @@ +[Core] +DisableLogging=false +Filter="(%Severity% >= DEBG and %Process% contains EVerest)" + +[Sinks.TextFile] +Destination=TextFile +FileName="liblog_test_logs_debg/FileLog%5N.log" +Format = "%TimeStamp% [%Severity%] %Message%" +RotationSize = 500 # bytes +MaxFiles = 5 diff --git a/tests/logging_configs/textfile_erro.ini b/tests/logging_configs/textfile_erro.ini new file mode 100644 index 0000000..87ec90d --- /dev/null +++ b/tests/logging_configs/textfile_erro.ini @@ -0,0 +1,10 @@ +[Core] +DisableLogging=false +Filter="(%Severity% >= ERRO and %Process% contains EVerest)" + +[Sinks.TextFile] +Destination=TextFile +FileName="liblog_test_logs_erro/FileLog%5N.log" +Format = "%TimeStamp% [%Severity%] %Message%" +RotationSize = 500 # bytes +MaxFiles = 5 diff --git a/tests/logging_configs/textfile_info.ini b/tests/logging_configs/textfile_info.ini new file mode 100644 index 0000000..33699a3 --- /dev/null +++ b/tests/logging_configs/textfile_info.ini @@ -0,0 +1,10 @@ +[Core] +DisableLogging=false +Filter="(%Severity% >= INFO and %Process% contains EVerest)" + +[Sinks.TextFile] +Destination=TextFile +FileName="liblog_test_logs_info/FileLog%5N.log" +Format = "%TimeStamp% [%Severity%] %Message%" +RotationSize = 500 # bytes +MaxFiles = 5 diff --git a/tests/logging_configs/textfile_off.ini b/tests/logging_configs/textfile_off.ini new file mode 100644 index 0000000..89e3f58 --- /dev/null +++ b/tests/logging_configs/textfile_off.ini @@ -0,0 +1,10 @@ +[Core] +DisableLogging=false +Filter="(%Severity% > CRIT and %Process% contains EVerest)" + +[Sinks.TextFile] +Destination=TextFile +FileName="liblog_test_logs_off/FileLog%5N.log" +Format = "%TimeStamp% [%Severity%] %Message%" +RotationSize = 500 # bytes +MaxFiles = 5 diff --git a/tests/logging_configs/textfile_verb.ini b/tests/logging_configs/textfile_verb.ini new file mode 100644 index 0000000..2299649 --- /dev/null +++ b/tests/logging_configs/textfile_verb.ini @@ -0,0 +1,10 @@ +[Core] +DisableLogging=false +Filter="(%Severity% >= VERB and %Process% contains EVerest)" + +[Sinks.TextFile] +Destination=TextFile +FileName="liblog_test_logs_verb/FileLog%5N.log" +Format = "%TimeStamp% [%Severity%] %Message%" +RotationSize = 500 # bytes +MaxFiles = 5 diff --git a/tests/logging_configs/textfile_verb_broken_filename.ini b/tests/logging_configs/textfile_verb_broken_filename.ini new file mode 100644 index 0000000..80aef15 --- /dev/null +++ b/tests/logging_configs/textfile_verb_broken_filename.ini @@ -0,0 +1,10 @@ +[Core] +DisableLogging=false +Filter="(%Severity% >= VERB and %Process% contains EVerest)" + +[Sinks.TextFile] +Destination=TextFile +FileName="liblog_test_logs_verb_rotate_broken_filename/NoFormatString.log" +Format = "%TimeStamp% [%Severity%] %Message%" +RotationSize = 10 # bytes +MaxFiles = 5 diff --git a/tests/logging_configs/textfile_verb_broken_format_string.ini b/tests/logging_configs/textfile_verb_broken_format_string.ini new file mode 100644 index 0000000..9251a57 --- /dev/null +++ b/tests/logging_configs/textfile_verb_broken_format_string.ini @@ -0,0 +1,10 @@ +[Core] +DisableLogging=false +Filter="(%Severity% >= VERB and %Process% contains EVerest)" + +[Sinks.TextFile] +Destination=TextFile +FileName="liblog_test_logs_verb_rotate_broken_format_string/BrokenFormatString%s.log" +Format = "%TimeStamp% [%Severity%] %Message%" +RotationSize = 10 # bytes +MaxFiles = 5 diff --git a/tests/logging_configs/textfile_verb_rotate.ini b/tests/logging_configs/textfile_verb_rotate.ini new file mode 100644 index 0000000..f802b5f --- /dev/null +++ b/tests/logging_configs/textfile_verb_rotate.ini @@ -0,0 +1,10 @@ +[Core] +DisableLogging=false +Filter="(%Severity% >= VERB and %Process% contains EVerest)" + +[Sinks.TextFile] +Destination=TextFile +FileName="liblog_test_logs_verb_rotate/FileLog%5N.log" +Format = "%TimeStamp% [%Severity%] %Message%" +RotationSize = 10 # bytes +MaxFiles = 5 diff --git a/tests/logging_configs/textfile_verb_rotate_on_open.ini b/tests/logging_configs/textfile_verb_rotate_on_open.ini new file mode 100644 index 0000000..b324392 --- /dev/null +++ b/tests/logging_configs/textfile_verb_rotate_on_open.ini @@ -0,0 +1,11 @@ +[Core] +DisableLogging=false +Filter="(%Severity% >= VERB and %Process% contains EVerest)" + +[Sinks.TextFile] +Destination=TextFile +FileName="liblog_test_logs_verb_rotate_on_open/FileLog%5N.log" +Format = "%TimeStamp% [%Severity%] %Message%" +RotationSize = 150 # bytes +RotateOnOpen = true +MaxFiles = 5 diff --git a/tests/logging_configs/textfile_warn.ini b/tests/logging_configs/textfile_warn.ini new file mode 100644 index 0000000..da28711 --- /dev/null +++ b/tests/logging_configs/textfile_warn.ini @@ -0,0 +1,10 @@ +[Core] +DisableLogging=false +Filter="(%Severity% >= WARN and %Process% contains EVerest)" + +[Sinks.TextFile] +Destination=TextFile +FileName="liblog_test_logs_warn/FileLog%5N.log" +Format = "%TimeStamp% [%Severity%] %Message%" +RotationSize = 500 # bytes +MaxFiles = 5