├── .github ├── FUNDING.yml └── workflows │ ├── build_and_test-Clang-10.yml │ ├── build_and_test-Clang-Cl.yml │ ├── build_and_test-GCC-10.yml │ ├── build_and_test-MSVC.yml │ └── generate_docs.yml ├── .gitignore ├── CMakeLists.txt ├── CMakeSettings.json ├── Doxyfile ├── LICENSE_1_0.txt ├── README.md ├── docs ├── ConsoleSinkDocs.hpp ├── ExampleDocs.hpp └── RecordDocs.hpp ├── examples ├── CMakeLists.txt ├── ColorizedConsoleLogging │ ├── CMakeLists.txt │ └── main.cpp ├── CustomRecordType │ ├── CMakeLists.txt │ └── main.cpp ├── CustomizeBaseRecord │ ├── CMakeLists.txt │ └── main.cpp ├── EasyStart │ ├── CMakeLists.txt │ └── main.cpp └── FileLogging │ ├── CMakeLists.txt │ └── main.cpp ├── include ├── Simple-Log │ ├── BasicSink.hpp │ ├── ConsoleSink.hpp │ ├── Core.hpp │ ├── FileSink.hpp │ ├── Filters.hpp │ ├── FlushPolicies.hpp │ ├── ISink.hpp │ ├── Logger.hpp │ ├── OStreamSink.hpp │ ├── Predicates.hpp │ ├── PresetTypes.hpp │ ├── ReadyToGo.hpp │ ├── Record.hpp │ ├── RecordBuilder.hpp │ ├── RecordQueue.hpp │ ├── Simple-Log.hpp │ ├── StringPattern.hpp │ └── TupleAlgorithms.hpp └── third_party │ └── rang │ └── rang.hpp └── tests ├── BasicSinkTests.cpp ├── CMakeLists.txt ├── ConsoleSinkTests.cpp ├── CoreTests.cpp ├── FileSinkTests.cpp ├── FilterTests.cpp ├── FlushPolicyTests.cpp ├── LoggerTests.cpp ├── OStreamSinkTests.cpp ├── PredicateTests.cpp ├── RecordBuilderTests.cpp ├── RecordQueueTests.cpp ├── RecordTests.cpp ├── StringPatternTest.cpp ├── TupleAlgorithmTests.cpp └── main.cpp /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | #github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | #patreon: # Replace with a single Patreon username 5 | #open_collective: # Replace with a single Open Collective username 6 | #ko_fi: # Replace with a single Ko-fi username 7 | #tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | #community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | #liberapay: # Replace with a single Liberapay username 10 | #issuehunt: # Replace with a single IssueHunt username 11 | #otechie: # Replace with a single Otechie username 12 | #custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | 14 | github: [DNKpp] 15 | ko_fi: DNKpp2011 16 | -------------------------------------------------------------------------------- /.github/workflows/build_and_test-Clang-10.yml: -------------------------------------------------------------------------------- 1 | name: Build & Test - Clang-10 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build-ubuntu: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v2 10 | - name: Setup cmake 11 | uses: jwlawson/actions-setup-cmake@v1.7 12 | with: 13 | cmake-version: '3.19.x' 14 | 15 | - uses: seanmiddleditch/gha-setup-ninja@master 16 | - name: Install Clang-10 17 | run: | 18 | sudo apt-get update 19 | sudo apt install clang-10 20 | - name: Configure & Build 21 | run: | 22 | cmake . -Bbuild_clang -DCMAKE_C_COMPILER=$(which clang-10) -DCMAKE_CXX_COMPILER=$(which clang++-10) -DSIMPLE_LOG_BUILD_EXAMPLES=ON 23 | cmake --build build_clang 24 | - name: Test 25 | run: 26 | cmake --build build_clang --target test 27 | -------------------------------------------------------------------------------- /.github/workflows/build_and_test-Clang-Cl.yml: -------------------------------------------------------------------------------- 1 | name: Build & Test - Clang-Cl 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build-windows: 7 | runs-on: windows-latest 8 | steps: 9 | - uses: actions/checkout@v2 10 | - name: Setup cmake 11 | uses: jwlawson/actions-setup-cmake@v1.7 12 | with: 13 | cmake-version: '3.19.x' 14 | 15 | - name: Configure & Build 16 | run: | 17 | cmake . -Bbuild_clang-cl -T clangcl -DSIMPLE_LOG_BUILD_EXAMPLES=ON 18 | cmake --build build_clang-cl 19 | - name: Test (Clang-Cl) 20 | run: 21 | cmake --build build_clang-cl --target RUN_TESTS 22 | -------------------------------------------------------------------------------- /.github/workflows/build_and_test-GCC-10.yml: -------------------------------------------------------------------------------- 1 | name: Build & Test - GCC-10 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build-ubuntu: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v2 10 | - name: Setup cmake 11 | uses: jwlawson/actions-setup-cmake@v1.7 12 | with: 13 | cmake-version: '3.19.x' 14 | 15 | - uses: seanmiddleditch/gha-setup-ninja@master 16 | - name: Install GCC-10 17 | run: | 18 | sudo apt-get update 19 | sudo apt install g++-10 20 | sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-10 100 --slave /usr/bin/g++ g++ /usr/bin/g++-10 --slave /usr/bin/gcov gcov /usr/bin/gcov-10 21 | - name: Configure & Build 22 | run: | 23 | cmake . -Bbuild_gcc -G "Ninja Multi-Config" -DSIMPLE_LOG_BUILD_EXAMPLES=ON 24 | cmake --build build_gcc 25 | - name: Test 26 | run: 27 | cmake --build build_gcc --target test 28 | -------------------------------------------------------------------------------- /.github/workflows/build_and_test-MSVC.yml: -------------------------------------------------------------------------------- 1 | name: Build & Test - MSVC 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build-windows: 7 | runs-on: windows-latest 8 | steps: 9 | - uses: actions/checkout@v2 10 | - name: Setup cmake 11 | uses: jwlawson/actions-setup-cmake@v1.7 12 | with: 13 | cmake-version: '3.19.x' 14 | 15 | - name: Configure & Build 16 | run: | 17 | cmake . -Bbuild_msvc -DSIMPLE_LOG_BUILD_EXAMPLES=ON 18 | cmake --build build_msvc 19 | - name: Test 20 | run: 21 | cmake --build build_msvc --target RUN_TESTS 22 | -------------------------------------------------------------------------------- /.github/workflows/generate_docs.yml: -------------------------------------------------------------------------------- 1 | name: Generate Docs 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | docs: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | 14 | - name: Run Doxygen 15 | uses: mattnotmitt/doxygen-action@v1 16 | 17 | - name: Deploy 18 | uses: peaceiris/actions-gh-pages@v3 19 | with: 20 | github_token: ${{ secrets.GITHUB_TOKEN }} 21 | publish_dir: ./docs_out/html 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vs/ 2 | out/ 3 | docs_out/ 4 | *.user 5 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.14 FATAL_ERROR) 2 | 3 | project(simple_log 4 | VERSION 0.7 5 | LANGUAGES CXX 6 | ) 7 | 8 | include(CTest) 9 | 10 | option(SIMPLE_LOG_BUILD_EXAMPLES "Enables example building" OFF) 11 | 12 | set(TestsDir "${CMAKE_CURRENT_SOURCE_DIR}/tests/") 13 | set(ExamplesDir "${CMAKE_CURRENT_SOURCE_DIR}/examples/") 14 | 15 | add_library(${PROJECT_NAME} INTERFACE) 16 | target_include_directories( 17 | ${PROJECT_NAME} 18 | INTERFACE 19 | ${CMAKE_CURRENT_SOURCE_DIR}/include 20 | ) 21 | 22 | target_compile_features( 23 | ${PROJECT_NAME} 24 | INTERFACE 25 | cxx_std_20 26 | ) 27 | 28 | if (CMAKE_CXX_COMPILER_ID STREQUAL "GNU" OR CMAKE_CXX_COMPILER_ID STREQUAL "Clang") 29 | target_link_options(${PROJECT_NAME} 30 | INTERFACE -pthread -latomic 31 | ) 32 | endif() 33 | 34 | if (BUILD_TESTING) 35 | add_subdirectory(${TestsDir}) 36 | endif() 37 | 38 | if (SIMPLE_LOG_BUILD_EXAMPLES) 39 | add_subdirectory(${ExamplesDir}) 40 | endif() 41 | -------------------------------------------------------------------------------- /CMakeSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "configurations": [ 3 | { 4 | "name": "x64-Debug", 5 | "generator": "Ninja", 6 | "configurationType": "Debug", 7 | "inheritEnvironments": [ "msvc_x64_x64" ], 8 | "buildRoot": "${projectDir}\\out\\build\\${name}", 9 | "installRoot": "${projectDir}\\out\\install\\${name}", 10 | "cmakeCommandArgs": "", 11 | "buildCommandArgs": "", 12 | "ctestCommandArgs": "", 13 | "variables": [ 14 | { 15 | "name": "SIMPLE_LOG_BUILD_EXAMPLES", 16 | "value": "True", 17 | "type": "BOOL" 18 | } 19 | ] 20 | }, 21 | { 22 | "name": "Linux-GCC-Debug", 23 | "generator": "Ninja", 24 | "configurationType": "Debug", 25 | "cmakeExecutable": "cmake", 26 | "remoteCopySourcesExclusionList": [ ".vs", ".git", "out" ], 27 | "cmakeCommandArgs": "", 28 | "buildCommandArgs": "", 29 | "ctestCommandArgs": "", 30 | "inheritEnvironments": [ "linux_x64" ], 31 | "remoteMachineName": "${defaultRemoteMachineName}", 32 | "remoteCMakeListsRoot": "$HOME/.vs/${projectDirName}/${workspaceHash}/src", 33 | "remoteBuildRoot": "$HOME/.vs/${projectDirName}/${workspaceHash}/out/build/${name}", 34 | "remoteInstallRoot": "$HOME/.vs/${projectDirName}/${workspaceHash}/out/install/${name}", 35 | "remoteCopySources": true, 36 | "rsyncCommandArgs": "-t --delete --delete-excluded", 37 | "remoteCopyBuildOutput": false, 38 | "remoteCopySourcesMethod": "rsync", 39 | "addressSanitizerRuntimeFlags": "detect_leaks=0", 40 | "variables": [ 41 | { 42 | "name": "SIMPLE_LOG_BUILD_EXAMPLES", 43 | "value": "True", 44 | "type": "BOOL" 45 | } 46 | ] 47 | }, 48 | { 49 | "name": "Linux-GCC-Release", 50 | "generator": "Ninja", 51 | "configurationType": "Release", 52 | "cmakeExecutable": "cmake", 53 | "remoteCopySourcesExclusionList": [ ".vs", ".git", "out" ], 54 | "cmakeCommandArgs": "", 55 | "buildCommandArgs": "", 56 | "ctestCommandArgs": "", 57 | "inheritEnvironments": [ "linux_x64" ], 58 | "remoteMachineName": "${defaultRemoteMachineName}", 59 | "remoteCMakeListsRoot": "$HOME/.vs/${projectDirName}/${workspaceHash}/src", 60 | "remoteBuildRoot": "$HOME/.vs/${projectDirName}/${workspaceHash}/out/build/${name}", 61 | "remoteInstallRoot": "$HOME/.vs/${projectDirName}/${workspaceHash}/out/install/${name}", 62 | "remoteCopySources": true, 63 | "rsyncCommandArgs": "-t --delete --delete-excluded", 64 | "remoteCopyBuildOutput": false, 65 | "remoteCopySourcesMethod": "rsync", 66 | "addressSanitizerRuntimeFlags": "detect_leaks=0", 67 | "variables": [ 68 | { 69 | "name": "SIMPLE_LOG_BUILD_EXAMPLES", 70 | "value": "True", 71 | "type": "BOOL" 72 | } 73 | ] 74 | }, 75 | { 76 | "name": "x64-Release", 77 | "generator": "Ninja", 78 | "configurationType": "Release", 79 | "buildRoot": "${projectDir}\\out\\build\\${name}", 80 | "installRoot": "${projectDir}\\out\\install\\${name}", 81 | "cmakeCommandArgs": "", 82 | "buildCommandArgs": "", 83 | "ctestCommandArgs": "", 84 | "inheritEnvironments": [ "msvc_x64_x64" ], 85 | "variables": [ 86 | { 87 | "name": "SIMPLE_LOG_BUILD_EXAMPLES", 88 | "value": "True", 89 | "type": "BOOL" 90 | } 91 | ] 92 | }, 93 | { 94 | "name": "Linux-Clang-Debug", 95 | "generator": "Ninja", 96 | "configurationType": "Debug", 97 | "cmakeExecutable": "cmake", 98 | "remoteCopySourcesExclusionList": [ ".vs", ".git", "out" ], 99 | "cmakeCommandArgs": "", 100 | "buildCommandArgs": "", 101 | "ctestCommandArgs": "", 102 | "inheritEnvironments": [ "linux_clang_x64" ], 103 | "remoteMachineName": "${defaultRemoteMachineName}", 104 | "remoteCMakeListsRoot": "$HOME/.vs/${projectDirName}/${workspaceHash}/src", 105 | "remoteBuildRoot": "$HOME/.vs/${projectDirName}/${workspaceHash}/out/build/${name}", 106 | "remoteInstallRoot": "$HOME/.vs/${projectDirName}/${workspaceHash}/out/install/${name}", 107 | "remoteCopySources": true, 108 | "rsyncCommandArgs": "-t --delete --delete-excluded", 109 | "remoteCopyBuildOutput": false, 110 | "remoteCopySourcesMethod": "rsync", 111 | "addressSanitizerRuntimeFlags": "detect_leaks=0", 112 | "variables": [ 113 | { 114 | "name": "SIMPLE_LOG_BUILD_EXAMPLES", 115 | "value": "True", 116 | "type": "BOOL" 117 | } 118 | ] 119 | }, 120 | { 121 | "name": "Linux-Clang-Release", 122 | "generator": "Ninja", 123 | "configurationType": "RelWithDebInfo", 124 | "cmakeExecutable": "cmake", 125 | "remoteCopySourcesExclusionList": [ ".vs", ".git", "out" ], 126 | "cmakeCommandArgs": "", 127 | "buildCommandArgs": "", 128 | "ctestCommandArgs": "", 129 | "inheritEnvironments": [ "linux_clang_x64" ], 130 | "remoteMachineName": "${defaultRemoteMachineName}", 131 | "remoteCMakeListsRoot": "$HOME/.vs/${projectDirName}/${workspaceHash}/src", 132 | "remoteBuildRoot": "$HOME/.vs/${projectDirName}/${workspaceHash}/out/build/${name}", 133 | "remoteInstallRoot": "$HOME/.vs/${projectDirName}/${workspaceHash}/out/install/${name}", 134 | "remoteCopySources": true, 135 | "rsyncCommandArgs": "-t --delete --delete-excluded", 136 | "remoteCopyBuildOutput": false, 137 | "remoteCopySourcesMethod": "rsync", 138 | "addressSanitizerRuntimeFlags": "detect_leaks=0", 139 | "variables": [ 140 | { 141 | "name": "SIMPLE_LOG_BUILD_EXAMPLES", 142 | "value": "True", 143 | "type": "BOOL" 144 | } 145 | ] 146 | }, 147 | { 148 | "name": "x64-Clang-Debug", 149 | "generator": "Ninja", 150 | "configurationType": "Debug", 151 | "buildRoot": "${projectDir}\\out\\build\\${name}", 152 | "installRoot": "${projectDir}\\out\\install\\${name}", 153 | "cmakeCommandArgs": "", 154 | "buildCommandArgs": "", 155 | "ctestCommandArgs": "", 156 | "inheritEnvironments": [ "clang_cl_x64_x64" ], 157 | "variables": [ 158 | { 159 | "name": "SIMPLE_LOG_BUILD_EXAMPLES", 160 | "value": "True", 161 | "type": "BOOL" 162 | } 163 | ] 164 | }, 165 | { 166 | "name": "x64-Clang-Release", 167 | "generator": "Ninja", 168 | "configurationType": "RelWithDebInfo", 169 | "buildRoot": "${projectDir}\\out\\build\\${name}", 170 | "installRoot": "${projectDir}\\out\\install\\${name}", 171 | "cmakeCommandArgs": "", 172 | "buildCommandArgs": "", 173 | "ctestCommandArgs": "", 174 | "inheritEnvironments": [ "clang_cl_x64_x64" ], 175 | "variables": [ 176 | { 177 | "name": "SIMPLE_LOG_BUILD_EXAMPLES", 178 | "value": "True", 179 | "type": "BOOL" 180 | } 181 | ] 182 | }, 183 | { 184 | "name": "x64-ReleaseWithDebInfo", 185 | "generator": "Ninja", 186 | "configurationType": "RelWithDebInfo", 187 | "buildRoot": "${projectDir}\\out\\build\\${name}", 188 | "installRoot": "${projectDir}\\out\\install\\${name}", 189 | "cmakeCommandArgs": "", 190 | "buildCommandArgs": "", 191 | "ctestCommandArgs": "", 192 | "inheritEnvironments": [ "msvc_x64_x64" ], 193 | "variables": [ 194 | { 195 | "name": "SIMPLE_LOG_BUILD_EXAMPLES", 196 | "value": "True", 197 | "type": "BOOL" 198 | } 199 | ] 200 | } 201 | ] 202 | } -------------------------------------------------------------------------------- /LICENSE_1_0.txt: -------------------------------------------------------------------------------- 1 | Boost Software License - Version 1.0 - August 17th, 2003 2 | 3 | Permission is hereby granted, free of charge, to any person or organization 4 | obtaining a copy of the software and accompanying documentation covered by 5 | this license (the "Software") to use, reproduce, display, distribute, 6 | execute, and transmit the Software, and to prepare derivative works of the 7 | Software, and to permit third-parties to whom the Software is furnished to 8 | do so, all subject to the following: 9 | 10 | The copyright notices in the Software and this entire statement, including 11 | the above license grant, this restriction and the following disclaimer, 12 | must be included in all copies of the Software, in whole or in part, and 13 | all derivative works of the Software, unless such copies or derivative 14 | works are solely in the form of machine-executable object code generated by 15 | a source language processor. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT 20 | SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE 21 | FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, 22 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Simple-Log C++20 library 2 | ![Build & Test - MSVC](https://github.com/DNKpp/Simple-Log/workflows/Build%20&%20Test%20-%20MSVC/badge.svg) 3 | ![Build & Test - Clang-Cl](https://github.com/DNKpp/Simple-Log/workflows/Build%20&%20Test%20-%20Clang-Cl/badge.svg) 4 | ![Build & Test - Clang-10](https://github.com/DNKpp/Simple-Log/workflows/Build%20&%20Test%20-%20Clang-10/badge.svg) 5 | ![Build & Test - GCC-10](https://github.com/DNKpp/Simple-Log/workflows/Build%20&%20Test%20-%20GCC-10/badge.svg) 6 | 7 | [![Codacy Badge](https://app.codacy.com/project/badge/Grade/55561677ef904762a567b432eb4382b9)](https://www.codacy.com/gh/DNKpp/Simple-Log/dashboard?utm_source=github.com&utm_medium=referral&utm_content=DNKpp/Simple-Log&utm_campaign=Badge_Grade) 8 | 9 | ## Author 10 | Dominic Koepke 11 | Mail: [DNKpp2011@gmail.com](mailto:dnkpp2011@gmail.com) 12 | 13 | ## License 14 | 15 | [BSL-1.0](https://github.com/DNKpp/Simple-Log/blob/master/LICENSE_1_0.txt) (free, open source) 16 | 17 | ```text 18 | Copyright Dominic Koepke 2021 - 2021. 19 | Distributed under the Boost Software License, Version 1.0. 20 | (See accompanying file LICENSE_1_0.txt or copy at 21 | https://www.boost.org/LICENSE_1_0.txt) 22 | ``` 23 | 24 | ## Description 25 | This is a highly customizable multithreaded logging library, which makes heavy use of loosly coupled concepts rather than macros. Other than many other libraries, there are no singleton classes or forced global objects. It's up to the users if they 26 | want globals or not. 27 | 28 | If your goal is simply logging everything to console or a file, than you may want to begin with the ````-header, which will set-up everything you'll need to be able start logging (also look at the short examples at the bottom of this readme). 29 | As you'll get used to the library you'll probably want to start customizing the behaviour of your sinks or even exchange types of Record's properties. This library lets you do this. Just head over to docs page https://dnkpp.github.io/Simple-Log/ or have a look at /src/examples directory. 30 | If you need an example for some advanced technics, don't hesitate asking me. As this library is growing I'll add more and more (hopefully useful) examples. 31 | 32 | A friendly reminder at the end: This library is currently in an alpha state, where it may be possible that some API breaks will happen. If you need a fully stable library from now on, this is unfortunatly not what you're looking for. I'm sorry, but perhaps 33 | it will be worth a second look in the near future. 34 | 35 | ## Installation with CMake 36 | This library can easily be integrated into your project via CMake target_link_libraries command. 37 | 38 | ```cmake 39 | target_link_libraries( 40 | 41 | PRIVATE 42 | simple_log 43 | ) 44 | ``` 45 | This will add the the include path "/include", thus you are able to include all headers via 46 | ```cpp 47 | #include 48 | ``` 49 | 50 | ### FetchContent 51 | It is possible fetching this library via CMakes FetchContent module. 52 | 53 | ```cmake 54 | cmake_minimum_required(VERSION 3.14 FATAL_ERROR) 55 | 56 | project() 57 | 58 | include(FetchContent) 59 | 60 | FetchContent_Declare( 61 | simple_log 62 | GIT_REPOSITORY https://github.com/DNKpp/Simple-Log 63 | GIT_TAG origin/master 64 | ) 65 | FetchContent_MakeAvailable(simple_log) 66 | 67 | target_link_libraries( 68 | 69 | PRIVATE simple_log 70 | ) 71 | ``` 72 | 73 | ## Examples 74 | 75 | ### Easy Start 76 | This is an example, which will print every message onto the console. Users will automatically receive a Core instance (gCore), Console Sink (gConsoleSink) and a Logger (gLog). 77 | ```cpp 78 | /* With inclusion of this header, there will be automatically gCore, gConsoleSink and gLog constructed, which you might use.*/ 79 | #include 80 | 81 | // just import everything into the current namespace 82 | using namespace sl::log::ready_to_go; 83 | 84 | int main() 85 | { 86 | // This line will be printed on the console with the severity "info". 87 | gLog() << "Hello, World!"; 88 | // You may adjust the severity for the currently created Record like so. 89 | gLog() << SetSev(SevLvl::debug) << "Mighty debug message"; 90 | // The severity manipulator doesn't has to appear at front. Place it anywhere in your Record construction chain. 91 | gLog() << "Print my important hint!" << SetSev(SevLvl::hint); 92 | } 93 | 94 | /*Core will make sure, that all pending Records will be processed before it gets destructed.*/ 95 | /* 96 | * The above code may generate this output: 97 | 20:18:59.357 >>> info:: Hello, World! 98 | 20:18:59.357 >>> debug:: Mighty debug message 99 | 20:18:59.357 >>> hint:: Print my important hint! 100 | * 101 | * Keep in mind, you are completely free how you are going to format your message. This is just the default one. 102 | */ 103 | ``` 104 | 105 | ### Easy File logging 106 | ```cpp 107 | /* With inclusion of this header, there will be automatically gCore, gConsoleSink and gLog constructed, which you might use.*/ 108 | #include 109 | 110 | // just pull everything into the current namespace 111 | using namespace sl::log::ready_to_go; 112 | 113 | // this creates a new FileSink object, which will store all incoming messages in logfile.log 114 | auto& gFileSink = gCore.makeSink("logfile.log"); 115 | 116 | int main() 117 | { 118 | // Let our FileSink only handle important messages, e.g. warning and above 119 | gFileSink.setFilter(makeSeverityFilterFor(GreaterEquals{ SevLvl::warning })); 120 | 121 | // this message will only appear on the console 122 | gLog() << "Hello, World!"; 123 | 124 | // while this message will also be saved in our logfile.log. Go ahead and see it yourself ;) 125 | gLog() << SetSev(SevLvl::warning) << "I'm an exemplary warning!"; 126 | } 127 | ``` 128 | -------------------------------------------------------------------------------- /docs/ConsoleSinkDocs.hpp: -------------------------------------------------------------------------------- 1 | // Copyright Dominic Koepke 2021 - 2021. 2 | // Distributed under the Boost Software License, Version 1.0. 3 | // (See accompanying file LICENSE_1_0.txt or copy at 4 | // https://www.boost.org/LICENSE_1_0.txt) 5 | 6 | /** \addtogroup ConsoleSink 7 | * @{ 8 | * 9 | * \details \ref ConsoleSink ConsoleSinks are tightly linked to the std::cout object. 10 | * 11 | * 12 | * \section Colorize_Messages Colorize Messages 13 | * The library offers a quick way setting up your intended color (and style) table, based on any Record property. Here is a quick example which illustrates the setup: 14 | * 15 | * \dontinclude{lineno} ColorizedConsoleLogging/main.cpp 16 | * \skip Simple-Log/ 17 | * \until Above lines will 18 | */ 19 | 20 | /** @}*/ 21 | -------------------------------------------------------------------------------- /docs/ExampleDocs.hpp: -------------------------------------------------------------------------------- 1 | // Copyright Dominic Koepke 2021 - 2021. 2 | // Distributed under the Boost Software License, Version 1.0. 3 | // (See accompanying file LICENSE_1_0.txt or copy at 4 | // https://www.boost.org/LICENSE_1_0.txt) 5 | 6 | /** 7 | * \example{lineno} EasyStart/main.cpp 8 | */ 9 | 10 | /** 11 | * \example{lineno} FileLogging/main.cpp 12 | */ 13 | 14 | /** 15 | * \example{lineno} ColorizedConsoleLogging/main.cpp 16 | */ 17 | 18 | /** 19 | * \example{lineno} CustomizeBaseRecord/main.cpp 20 | */ 21 | 22 | /** 23 | * \example{lineno} CustomRecordType/main.cpp 24 | */ 25 | -------------------------------------------------------------------------------- /docs/RecordDocs.hpp: -------------------------------------------------------------------------------- 1 | // Copyright Dominic Koepke 2021 - 2021. 2 | // Distributed under the Boost Software License, Version 1.0. 3 | // (See accompanying file LICENSE_1_0.txt or copy at 4 | // https://www.boost.org/LICENSE_1_0.txt) 5 | 6 | /** \addtogroup Record 7 | * @{ 8 | * \details Records are basically a collection of information, which has been gathered either implicitly or explicitly while a logging Message was created. It is not intended that users ever use a plain Record or set it up by theirselves. 9 | * The library provides a simple interface which does all the trivial part of a logging step, while users can concentrate on setting up their message.\n Have a look at Logger and RecordBuilder if you want to know more about how to setup Records. 10 | * 11 | * \section Custom_Record_Types Custom Record Types 12 | * In the \ref PreparedTypes "preset namespace" of this library users can find a Record_t alias, which will accept values of the \ref preset::SevLvl "SevLvl" enum as SeverityLevel and std::strings as Channel type. Users may exchange any or all of these 13 | * predefined types with their own. If you use any Filter, FlushPolicy or anything else which watches out for a specific Channel, it might be wise to exchange the std::string type with something less expensive to compare, e.g. an enum type or plain int. 14 | * For example users could use the BaseRecord type, but exchanging some member types with any other type or even create their custom Record type from scratch. Here is an example for the former: 15 | * 16 | * \dontinclude{lineno} CustomizeBaseRecord/main.cpp 17 | * \skip Simple-Log/ 18 | * \until network >> 19 | * 20 | * If your needs are more specific and you want to add things such as custom properties to the Record type, this might be an example for you: 21 | * \dontinclude{lineno} CustomRecordType/main.cpp 22 | * \skip Simple-Log/ 23 | * \until palindrome >> 24 | */ 25 | 26 | /** @}*/ 27 | -------------------------------------------------------------------------------- /examples/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.14 FATAL_ERROR) 2 | 3 | project(simple_log_examples) 4 | 5 | add_subdirectory(EasyStart) 6 | add_subdirectory(FileLogging) 7 | add_subdirectory(CustomizeBaseRecord) 8 | add_subdirectory(CustomRecordType) 9 | add_subdirectory(ColorizedConsoleLogging) 10 | -------------------------------------------------------------------------------- /examples/ColorizedConsoleLogging/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.14 FATAL_ERROR) 2 | 3 | project(simple_log_example_colorized_console_logging) 4 | 5 | add_executable( 6 | ${PROJECT_NAME} 7 | main.cpp 8 | ) 9 | 10 | target_link_libraries( 11 | ${PROJECT_NAME} 12 | PRIVATE 13 | simple_log 14 | ) 15 | -------------------------------------------------------------------------------- /examples/ColorizedConsoleLogging/main.cpp: -------------------------------------------------------------------------------- 1 | // Copyright Dominic Koepke 2021 - 2021. 2 | // Distributed under the Boost Software License, Version 1.0. 3 | // (See accompanying file LICENSE_1_0.txt or copy at 4 | // https://www.boost.org/LICENSE_1_0.txt) 5 | 6 | #include 7 | 8 | // just pull everything into the current namespace 9 | using namespace sl::log::preset; 10 | using namespace sl::log; 11 | 12 | int main() 13 | { 14 | Core_t core; 15 | auto log = makeLogger(core, SevLvl::info); 16 | 17 | auto& gConsoleSink = core.makeSink(); 18 | 19 | using Color = ConsoleTextStyle::Color; 20 | using Style = ConsoleTextStyle::Style; 21 | gConsoleSink.setTextStylePolicy( 22 | makeConsoleTextStyleTableFor( 23 | &Record_t::severity, // use the severity property 24 | { 25 | // the desired SevLvl gets linked to a color and style setup 26 | { SevLvl::debug, { .textColor = Color::yellow, .bgColor = Color::gray } }, 27 | { SevLvl::hint, { .bgColor = Color::green } }, 28 | { SevLvl::warning, { .bgColor = Color::yellow } }, 29 | { SevLvl::error, { .style = Style::bold, .textColor = Color::red, .bgColor = Color::yellow } }, 30 | { SevLvl::fatal, { .style = Style::crossed, .textColor = Color::gray, .bgColor = Color::red } } 31 | } 32 | ) 33 | ); 34 | 35 | log() << "Hello, World!"; 36 | log() << SetSev(SevLvl::debug) << "Mighty debug message"; 37 | log() << "Print as hint!" << SetSev(SevLvl::hint); 38 | log() << SetSev(SevLvl::warning) << "be warned..."; 39 | log() << "Print my important error!" << SetSev(SevLvl::error); 40 | log() << "Print it fatal!" << SetSev(SevLvl::fatal); 41 | } 42 | // Above lines will be printed nicely colorful onto the console. Go, test it out ;) 43 | -------------------------------------------------------------------------------- /examples/CustomRecordType/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.14 FATAL_ERROR) 2 | 3 | project(simple_log_example_custom_record_type) 4 | 5 | add_executable( 6 | ${PROJECT_NAME} 7 | main.cpp 8 | ) 9 | 10 | target_link_libraries( 11 | ${PROJECT_NAME} 12 | PRIVATE 13 | simple_log 14 | ) 15 | -------------------------------------------------------------------------------- /examples/CustomRecordType/main.cpp: -------------------------------------------------------------------------------- 1 | // Copyright Dominic Koepke 2021 - 2021. 2 | // Distributed under the Boost Software License, Version 1.0. 3 | // (See accompanying file LICENSE_1_0.txt or copy at 4 | // https://www.boost.org/LICENSE_1_0.txt) 5 | 6 | #include 7 | #include 8 | 9 | #include 10 | 11 | using namespace sl::log; 12 | using preset::SevLvl; 13 | 14 | class MyCustomRecord : 15 | public preset::Record_t 16 | { 17 | public: 18 | using Domain_t = std::string; 19 | 20 | Domain_t domain; 21 | }; 22 | 23 | // Yes, these are a few typedefs which might seem tedious at the first glance, but you'll usually need to do this once per program. 24 | using Core_t = Core; 25 | using ConsoleSink_t = ConsoleSink; 26 | using FileSink_t = FileSink; 27 | using Logger_t = BaseLogger; 28 | 29 | // Now that our Record class contains an additional domain property, we would like to have a manipulator for our RecordBuilder 30 | using RecordBuilder_t = RecordBuilder; 31 | 32 | class SetDomain 33 | { 34 | public: 35 | using Domain_t = MyCustomRecord::Domain_t; 36 | 37 | explicit SetDomain(Domain_t data) : 38 | m_Data{ std::move(data) } 39 | { 40 | } 41 | 42 | void operator ()(MyCustomRecord& rec) 43 | { 44 | rec.domain = std::move(m_Data); 45 | } 46 | 47 | private: 48 | Domain_t m_Data; 49 | }; 50 | 51 | inline Core_t gCore; 52 | inline auto gLog = makeLogger(gCore, SevLvl::info); 53 | inline auto& gConsoleSink 54 | { 55 | []() -> auto& 56 | { 57 | // let's create the console sink in disabled state. Will become automatically enabled after this scope is left. 58 | auto wrappedSink = gCore.makeDisabledSink(); 59 | // setting up a custom formatter, thus for each Record only the domain followed by the message will be printed. 60 | wrappedSink->setFormatter( 61 | [](const MyCustomRecord& record) 62 | { 63 | std::stringstream out; 64 | out << record.domain << " >> " << record.message(); 65 | return std::move(out).str(); 66 | } 67 | ); 68 | 69 | return *wrappedSink; 70 | }() 71 | }; 72 | 73 | int main() 74 | { 75 | gLog() << "Hello, World!"; 76 | // this clearly belongs to the domain of palindromes, thus let's tag it like this! 77 | gLog() << SetDomain("palindrome") << "A Man, A Plan, A Canal, Panama!"; 78 | } 79 | 80 | /* The output of the console looks like this: 81 | >> Hello, World! 82 | palindrome >> A Man, A Plan, A Canal, Panama! 83 | 84 | */ 85 | -------------------------------------------------------------------------------- /examples/CustomizeBaseRecord/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.14 FATAL_ERROR) 2 | 3 | project(simple_log_example_customize_base_record) 4 | 5 | add_executable( 6 | ${PROJECT_NAME} 7 | main.cpp 8 | ) 9 | 10 | target_link_libraries( 11 | ${PROJECT_NAME} 12 | PRIVATE 13 | simple_log 14 | ) 15 | -------------------------------------------------------------------------------- /examples/CustomizeBaseRecord/main.cpp: -------------------------------------------------------------------------------- 1 | // Copyright Dominic Koepke 2021 - 2021. 2 | // Distributed under the Boost Software License, Version 1.0. 3 | // (See accompanying file LICENSE_1_0.txt or copy at 4 | // https://www.boost.org/LICENSE_1_0.txt) 5 | 6 | #include 7 | #include 8 | 9 | #include 10 | 11 | using namespace sl::log; 12 | using preset::SevLvl; 13 | 14 | enum class Channel 15 | { 16 | standard, 17 | network, 18 | stats 19 | }; 20 | 21 | // A simple overload, thus our Channel enum can be printed into every ostream object. 22 | inline std::ostream& operator <<(std::ostream& out, Channel lvl) 23 | { 24 | constexpr const char* str[] = { "standard", "network", "stats" }; 25 | out << str[static_cast(lvl)]; 26 | return out; 27 | } 28 | 29 | // Yes, these are a few typedefs which might seem tedious at the first glance, but you'll usually need to do this once per program. 30 | using Record_t = BaseRecord; 31 | using Core_t = Core; 32 | using ConsoleSink_t = ConsoleSink; 33 | using FileSink_t = FileSink; 34 | using Logger_t = BaseLogger; 35 | 36 | inline Core_t gCore; 37 | // every Record, which will be created by this Logger will be from Channel "standard" by default 38 | inline auto gLog = makeLogger(gCore, SevLvl::info, Channel::standard); 39 | inline auto& gConsoleSink 40 | { 41 | []() -> auto& 42 | { 43 | // let's create the console sink in disabled state. Will become automatically enabled after this scope is left. 44 | auto wrappedSink = gCore.makeDisabledSink(); 45 | // Only messages with the Channel network shall be printed onto the console 46 | wrappedSink->setFilter(makeChannelFilterFor(Equals{ Channel::network })); 47 | // setting up a custom formatter, thus for each Record only the channel followed by the message will be printed. 48 | wrappedSink->setFormatter( 49 | [](const Record_t& record) 50 | { 51 | std::stringstream out; 52 | out << record.channel() << " >> " << record.message(); 53 | return std::move(out).str(); 54 | } 55 | ); 56 | 57 | return *wrappedSink; 58 | }() 59 | }; 60 | 61 | int main() 62 | { 63 | gLog() << "Hello, World!"; // this message is ignored by our Console sink 64 | gLog() << SetChan(Channel::network) << "Hello, Network!"; // our Console sink handles this record. 65 | } 66 | 67 | /* The output of the console looks like this: 68 | network >> Hello, Network! 69 | 70 | */ 71 | -------------------------------------------------------------------------------- /examples/EasyStart/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.14 FATAL_ERROR) 2 | 3 | project(simple_log_example_easy_start) 4 | 5 | add_executable( 6 | ${PROJECT_NAME} 7 | main.cpp 8 | ) 9 | 10 | target_link_libraries( 11 | ${PROJECT_NAME} 12 | PRIVATE 13 | simple_log 14 | ) 15 | -------------------------------------------------------------------------------- /examples/EasyStart/main.cpp: -------------------------------------------------------------------------------- 1 | // Copyright Dominic Koepke 2021 - 2021. 2 | // Distributed under the Boost Software License, Version 1.0. 3 | // (See accompanying file LICENSE_1_0.txt or copy at 4 | // https://www.boost.org/LICENSE_1_0.txt) 5 | 6 | /* With inclusion of this header, there will be automatically gCore, gConsoleSink and gLog constructed, which you might use.*/ 7 | #include 8 | 9 | // just pull everything into the current namespace 10 | using namespace sl::log::ready_to_go; 11 | 12 | int main() 13 | { 14 | // This line will be printed on the console with the severity "info". 15 | gLog() << "Hello, World!"; 16 | // You may adjust the severity for the currently created Record like so. 17 | gLog() << SetSev(SevLvl::debug) << "Mighty debug message"; 18 | // The severity manipulator doesn't has to appear at front. Place it anywhere in your Record construction chain. 19 | gLog() << "Print my important hint!" << SetSev(SevLvl::hint); 20 | } 21 | 22 | /*Core will make sure, that all pending Records will be processed before it gets destructed.*/ 23 | /* 24 | * The above code may generate this output: 25 | 20:18:59.357 >>> info:: Hello, World! 26 | 20:18:59.357 >>> debug:: Mighty debug message 27 | 20:18:59.357 >>> hint:: Print my important hint! 28 | * 29 | * Keep in mind, you are completely free how you are going to format your message. This is just the default one. 30 | */ 31 | -------------------------------------------------------------------------------- /examples/FileLogging/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.14 FATAL_ERROR) 2 | 3 | project(simple_log_example_file_logging) 4 | 5 | add_executable( 6 | ${PROJECT_NAME} 7 | main.cpp 8 | ) 9 | 10 | target_link_libraries( 11 | ${PROJECT_NAME} 12 | PRIVATE 13 | simple_log 14 | ) 15 | -------------------------------------------------------------------------------- /examples/FileLogging/main.cpp: -------------------------------------------------------------------------------- 1 | // Copyright Dominic Koepke 2021 - 2021. 2 | // Distributed under the Boost Software License, Version 1.0. 3 | // (See accompanying file LICENSE_1_0.txt or copy at 4 | // https://www.boost.org/LICENSE_1_0.txt) 5 | 6 | /* With inclusion of this header, there will be automatically gCore, gConsoleSink and gLog constructed, which you might use.*/ 7 | #include 8 | 9 | // just pull everything into the current namespace 10 | using namespace sl::log::ready_to_go; 11 | 12 | // this creates a new FileSink object, which will store all incoming messages in logfile.log 13 | auto& gFileSink = gCore.makeSink("logfile.log"); 14 | 15 | int main() 16 | { 17 | // Let our FileSink only handle important messages, e.g. warning and above 18 | gFileSink.setFilter(makeSeverityFilterFor(GreaterEquals{ SevLvl::warning })); 19 | 20 | // this message will only appear on the console 21 | gLog() << "Hello, World!"; 22 | 23 | // while this message will also be saved in our logfile.log. Go ahead and see it yourself ;) 24 | gLog() << SetSev(SevLvl::warning) << "I'm an exemplary warning!"; 25 | } 26 | -------------------------------------------------------------------------------- /include/Simple-Log/BasicSink.hpp: -------------------------------------------------------------------------------- 1 | // Copyright Dominic Koepke 2021 - 2021. 2 | // Distributed under the Boost Software License, Version 1.0. 3 | // (See accompanying file LICENSE_1_0.txt or copy at 4 | // https://www.boost.org/LICENSE_1_0.txt) 5 | 6 | #ifndef SL_LOG_BASIC_SINK_HPP 7 | #define SL_LOG_BASIC_SINK_HPP 8 | 9 | #pragma once 10 | 11 | #include "ISink.hpp" 12 | #include "Record.hpp" 13 | 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | 21 | namespace sl::log 22 | { 23 | /** \addtogroup Sinks 24 | * @{ 25 | */ 26 | 27 | /** 28 | * \brief Concept for invokable formatter objects 29 | */ 30 | template 31 | concept RecordFormatterFor = 32 | Record && 33 | std::is_invocable_r_v; 34 | 35 | /** 36 | * \brief Concept for invokable filter objects 37 | */ 38 | template 39 | concept RecordFilterFor = 40 | Record && 41 | std::predicate; 42 | 43 | /** 44 | * \brief Abstract Sink class which offers basic filtering, formatting functionality 45 | * \tparam TRecord Used Record type. 46 | * \details This Sink class implements the enabling functionality, as well as filtering and formatting Records. Users who want to print messages into a std::ostream like object, should 47 | * look at the OStreamSink or its derived classes. 48 | */ 49 | template 50 | class BasicSink : 51 | public ISink 52 | { 53 | using Super = ISink; 54 | 55 | public: 56 | using typename Super::Record_t; 57 | using Projections_t = RecordGetters; 58 | using Formatter_t = std::function; 59 | using Filter_t = std::function; 60 | 61 | protected: 62 | static constexpr Formatter_t defaultFormatter() noexcept 63 | { 64 | return [](const Record_t& rec) 65 | { 66 | using namespace std::chrono; 67 | using namespace std::chrono_literals; 68 | 69 | // ToDo: replace with c++20 chrono and format 70 | const auto today = Projections_t::timePoint(rec).time_since_epoch() % 24h; 71 | const auto hour = duration_cast(today); 72 | const auto minute = duration_cast(today) % 1h; 73 | const auto second = duration_cast(today) % 1min; 74 | const auto millisecond = duration_cast(today) % 1s; 75 | 76 | std::ostringstream out; 77 | out << std::setfill('0') << 78 | std::setw(2) << hour.count() << ":" << 79 | std::setw(2) << minute.count() << ":" << 80 | std::setw(2) << second.count() << "." << 81 | std::setw(3) << millisecond.count() << 82 | " >>> "; 83 | 84 | out << Projections_t::severity(rec) << ":: "; 85 | out << Projections_t::message(rec); 86 | return std::move(out).str(); 87 | }; 88 | } 89 | 90 | static constexpr Filter_t defaultFilter() noexcept 91 | { 92 | return [](const Record_t& rec) { return true; }; 93 | } 94 | 95 | public: 96 | /** 97 | * \brief Constructor 98 | */ 99 | explicit BasicSink() noexcept = default; 100 | 101 | /** 102 | * \brief Default destructor 103 | */ 104 | ~BasicSink() noexcept = default; 105 | 106 | /** 107 | * \brief Deleted copy constructor 108 | */ 109 | BasicSink(const BasicSink&) = delete; 110 | /** 111 | * \brief Deleted copy assign operator 112 | */ 113 | BasicSink& operator =(const BasicSink&) = delete; 114 | 115 | /** 116 | * \brief Deleted move constructor 117 | */ 118 | BasicSink(BasicSink&&) = delete; 119 | /** 120 | * \brief Deleted move assign operator 121 | */ 122 | BasicSink& operator =(BasicSink&&) = delete; 123 | 124 | /** 125 | * \brief Handles the given Record object 126 | * \details Before the Record gets passed to the actual destination, it will be checked if the Sink object is enabled and if the Record should be filtered. 127 | * If these checks are passed, the abstract writeMessage function will be invoked with the finally formatted message string. 128 | * \param record Record object 129 | */ 130 | void log(const Record_t& record) final override 131 | { 132 | if (!m_Enabled) 133 | return; 134 | 135 | if (std::scoped_lock lock{ m_FilterMx }; !std::invoke(m_Filter, record)) 136 | return; 137 | 138 | const auto message = [&] 139 | { 140 | std::scoped_lock lock{ m_FormatterMx }; 141 | return std::invoke(m_Formatter, record); 142 | }(); 143 | 144 | writeMessage(record, message); 145 | } 146 | 147 | /** 148 | * \brief Enables or disables the Sink object 149 | * \details Disabled Sinks will not handle any incoming Record s 150 | * \param enable True will enable the Sink object. 151 | */ 152 | void setEnabled(bool enable = true) noexcept final override 153 | { 154 | m_Enabled = enable; 155 | } 156 | 157 | /** 158 | * \brief Checks if the Sink object is enabled. 159 | * \return Returns true if object is enabled. 160 | */ 161 | [[nodiscard]] 162 | bool isEnabled() const noexcept final override 163 | { 164 | return m_Enabled; 165 | } 166 | 167 | /** 168 | * \brief Sets the active formatter 169 | * \details It's the formatters job to extract the necessary information from Records and built the final message string. 170 | * 171 | * A custom formatter should use the following signature: 172 | * \code{.cpp} 173 | * std::string(const Record&) 174 | * \endcode 175 | * 176 | * \tparam TFormatter Type of the passed formatter (automatically deduced) 177 | * \param formatter An invokable formatter object 178 | */ 179 | template TFormatter> 180 | void setFormatter(TFormatter&& formatter) 181 | { 182 | std::scoped_lock lock{ m_FormatterMx }; 183 | m_Formatter = std::forward(formatter); 184 | } 185 | 186 | /** 187 | * \brief Replaces the active formatter with the default one 188 | */ 189 | void removeFormatter() 190 | { 191 | std::scoped_lock lock{ m_FormatterMx }; 192 | m_Formatter = defaultFormatter(); 193 | } 194 | 195 | /** 196 | * \brief Sets the active filter 197 | * \details It's the filters job to decide, which Record will be printed (filter returns true) and which will be skipped (filter returns false). Therefore a filter must be an invokable of the following signature: 198 | * \code{.cpp} 199 | * bool(const Record&) 200 | * \endcode 201 | * \remark The filters return type doesn't have to be bool, but the returned object must at least be implicitly convertible to bool. 202 | * 203 | * \tparam TFilter Type of the passed filter (automatically deduced) 204 | * \param filter An invokable filter object 205 | */ 206 | template TFilter> 207 | void setFilter(TFilter&& filter) 208 | { 209 | std::scoped_lock lock{ m_FilterMx }; 210 | m_Filter = std::forward(filter); 211 | } 212 | 213 | /** 214 | * \brief Replaces the active filter with the default one 215 | */ 216 | void removeFilter() 217 | { 218 | std::scoped_lock lock{ m_FilterMx }; 219 | m_Filter = defaultFilter(); 220 | } 221 | 222 | protected: 223 | /** 224 | * \brief This function will be called when the actual message should be printed. 225 | * \details Subclasses must implement this function and have to print the message to the desired destination themselves. 226 | * \param record The Record object. 227 | * \param message The actual message, which should be printed. 228 | * \version since alpha-0.6 229 | */ 230 | virtual void writeMessage(const Record_t& record, std::string_view message) = 0; 231 | 232 | private: 233 | std::mutex m_FormatterMx; 234 | Formatter_t m_Formatter{ defaultFormatter() }; 235 | 236 | std::mutex m_FilterMx; 237 | Filter_t m_Filter{ defaultFilter() }; 238 | 239 | std::atomic_bool m_Enabled{ false }; 240 | }; 241 | 242 | /** @}*/ 243 | } 244 | 245 | #endif 246 | -------------------------------------------------------------------------------- /include/Simple-Log/ConsoleSink.hpp: -------------------------------------------------------------------------------- 1 | // Copyright Dominic Koepke 2021 - 2021. 2 | // Distributed under the Boost Software License, Version 1.0. 3 | // (See accompanying file LICENSE_1_0.txt or copy at 4 | // https://www.boost.org/LICENSE_1_0.txt) 5 | 6 | #ifndef SL_LOG_CONSOLE_SINK_HPP 7 | #define SL_LOG_CONSOLE_SINK_HPP 8 | 9 | #pragma once 10 | 11 | #include "OStreamSink.hpp" 12 | #include "Record.hpp" 13 | 14 | // rang.hpp includes windows.hpp internally, thus macros min and max will be defined. This macro prevents this. 15 | #define NOMINMAX 16 | #include "third_party/rang/rang.hpp" 17 | 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | 24 | namespace sl::log 25 | { 26 | /** \addtogroup Sinks 27 | * @{ 28 | */ 29 | 30 | /** \addtogroup ConsoleSink 31 | * @{ 32 | */ 33 | 34 | /** 35 | * \brief Collection of possible style and color options for text printed onto the console 36 | * \details Each enum type will be casted to the third-party lib "rang" counterpart, which is responsible for all of the troublesome work. 37 | * Some style options might not work on every console. 38 | * 39 | * Go to https://github.com/agauniyal/rang/tree/master if you are interested about all the details. 40 | * \version since alpha-0.6 41 | */ 42 | struct ConsoleTextStyle 43 | { 44 | enum class Color 45 | { 46 | black = 0, 47 | red, 48 | green, 49 | yellow, 50 | blue, 51 | magenta, 52 | cyan, 53 | gray, 54 | standard, 55 | 56 | brightBlack, 57 | brightRed, 58 | brightGreen, 59 | brightYellow, 60 | brightBlue, 61 | brightMagenta, 62 | brightCyan, 63 | brightGray, 64 | }; 65 | 66 | enum class Style 67 | { 68 | standard = 0, 69 | bold, 70 | dim, 71 | italic, 72 | underline, 73 | reversed = 7, 74 | crossed = 9 75 | }; 76 | 77 | /** 78 | * \brief determines the style 79 | */ 80 | Style style = Style::standard; 81 | 82 | /** 83 | * \brief determines the text color 84 | */ 85 | Color textColor = Color::standard; 86 | 87 | /** 88 | * \brief determines the background color 89 | */ 90 | Color bgColor = Color::standard; 91 | }; 92 | 93 | /** 94 | * \brief Concept which checks for validity of given text style policy. 95 | */ 96 | template 97 | concept ConsoleTextStylePolicyFor = 98 | Record && 99 | std::is_invocable_r_v; 100 | 101 | /** 102 | * \brief A constant object used for resetting the style back to default 103 | * \version since alpha-0.6 104 | */ 105 | constexpr ConsoleTextStyle defaultConsoleTextStyle; 106 | 107 | /** @}*/ 108 | /** @}*/ 109 | } 110 | 111 | namespace sl::log::detail 112 | { 113 | std::ostream& applyTextColor(std::ostream& out, ConsoleTextStyle::Color color) noexcept; 114 | std::ostream& applyBackgroundColor(std::ostream& out, ConsoleTextStyle::Color color) noexcept; 115 | std::ostream& applyStyle(std::ostream& out, ConsoleTextStyle::Style style) noexcept; 116 | } 117 | 118 | namespace sl::log 119 | { 120 | /** \addtogroup Sinks 121 | * @{ 122 | */ 123 | 124 | /** \addtogroup ConsoleSink 125 | * @{ 126 | */ 127 | 128 | /** 129 | * \brief Operator << overload for ConsoleTextStyle type 130 | * \param out the stream object 131 | * \param style the provided style 132 | * \return Returns the parameter out as reference 133 | * \version since alpha-0.6 134 | */ 135 | inline std::ostream& operator <<(std::ostream& out, const ConsoleTextStyle& style) 136 | { 137 | detail::applyStyle(out, style.style); 138 | detail::applyTextColor(out, style.textColor); 139 | detail::applyBackgroundColor(out, style.bgColor); 140 | return out; 141 | } 142 | 143 | /** 144 | * \brief Convenience class for setting up style policies for a given Record member 145 | * \tparam TProjection The type of the projection 146 | * \tparam TTable The type of the internally used container. Can be either std::map or std::unordered_map 147 | * \details Users have to provide a finally established container, which will then used for determining the appearance of the current Record. 148 | * If no matching entry is found, defaultConsoleTextStyle will be used instead. 149 | * 150 | * Instead of directly constructing instances of this class, users should use the makeConsoleTextStyleTableFor function. 151 | * \version since alpha-0.6 152 | */ 153 | template 154 | class ConsoleTextStyleTable 155 | { 156 | public: 157 | using Projection_t = std::remove_cvref_t; 158 | using Table_t = std::remove_cvref_t; 159 | using Key_t = typename Table_t::key_type; 160 | 161 | /** 162 | * \brief Constructor 163 | * \param projection The projection to a member of the currently used Record type 164 | * \param table The finally established container object 165 | */ 166 | ConsoleTextStyleTable(TProjection projection, TTable table) : 167 | m_Projection{ std::move(projection) }, 168 | m_StyleTable{ std::move(table) } 169 | { 170 | } 171 | 172 | /** 173 | * \brief Invocation operator 174 | * \tparam TRecord The currently used Record type 175 | * \param record The actual Record object 176 | * \return Returns the ConsoleTextStyle which should be applied for this Record 177 | * \details If no matching established is found, defaultConsoleTextStyle will be used instead. 178 | */ 179 | template 180 | ConsoleTextStyle operator ()(const TRecord& record) const 181 | { 182 | if (auto itr = m_StyleTable.find(std::invoke(m_Projection, record)); itr != std::end(m_StyleTable)) 183 | { 184 | return itr->second; 185 | } 186 | return defaultConsoleTextStyle; 187 | } 188 | 189 | /** 190 | * \brief Inserts a style policy 191 | * \param key The key, on which the style will be applied 192 | * \param style The style which will be used, when the key is detected 193 | * \details Inserts or, if key is already present, overrides the style. 194 | */ 195 | void insert(Key_t key, ConsoleTextStyle style) 196 | { 197 | m_StyleTable.insert_or_assign(std::move(key), std::move(style)); 198 | } 199 | 200 | private: 201 | Projection_t m_Projection; 202 | Table_t m_StyleTable; 203 | }; 204 | 205 | /** 206 | * \brief The factory function for creating ConsoleTextStyleTable instances 207 | * \tparam TRecord The currently used Record type 208 | * \tparam TProjection The projection type 209 | * \tparam TTable The table type. The default type is of std::unordered_map. Users may exchange this with std::map. 210 | * \param projection The projection to a member of the currently used Record type 211 | * \param table The finally established container object 212 | * \return Returns a newly created ConsoleTextStyleTable instance 213 | * \details This is the preferable way creating a ConsoleTextStyleTable object for a Record property, because the projection and the table type 214 | * becomes strong checked via concept and therefore will provide much clearer feedback in cases of error, while creating ConsoleTextStyleTable objects 215 | * manually will probably result in harder to read error message. 216 | * \version since alpha-0.6 217 | */ 218 | template TProjection, 220 | class TTable = std::unordered_map< 221 | std::remove_cvref_t>, 222 | ConsoleTextStyle 223 | >> 224 | auto makeConsoleTextStyleTableFor(TProjection projection, TTable table) 225 | { 226 | return ConsoleTextStyleTable{ std::move(projection), std::move(table) }; 227 | } 228 | 229 | /** 230 | * \brief Sink class for directly logging onto std::cout 231 | * \tparam TRecord Used Record type. 232 | * \details This Sink class directly uses a std::cout object for printing each recorded message. Users may register \ref ConsoleTextStyle "ConsoleTextStyles" which will then colorized or 233 | * printed the messages in a specific style. 234 | * \version since alpha-0.6 235 | */ 236 | template 237 | class ConsoleSink final : 238 | public OStreamSink 239 | { 240 | using Super = OStreamSink; 241 | 242 | public: 243 | using typename Super::Record_t; 244 | using typename Super::Formatter_t; 245 | using typename Super::Filter_t; 246 | using typename Super::FlushPolicy_t; 247 | using TextStylePolicy_t = std::function; 248 | 249 | protected: 250 | /** 251 | * \brief Constructs the default TextStylePolicy 252 | * \return a invokable object 253 | */ 254 | [[nodiscard]] 255 | static constexpr TextStylePolicy_t defaultTextStylePolicy() 256 | { 257 | return [](const Record_t&) { return defaultConsoleTextStyle; }; 258 | } 259 | 260 | public: 261 | /** 262 | * \brief Constructor 263 | */ 264 | explicit ConsoleSink() : 265 | Super{ std::cout } 266 | { 267 | } 268 | 269 | /** 270 | * \brief Default destructor 271 | * \details Destructor does not perform any actions on the internal stream objects, due to it's potential dangling state. Derived classes must handle closing and flushing themselves. 272 | */ 273 | ~ConsoleSink() noexcept = default; 274 | 275 | /** 276 | * \brief Deleted copy constructor 277 | */ 278 | ConsoleSink(const ConsoleSink&) = delete; 279 | /** 280 | * \brief Deleted copy assign operator 281 | */ 282 | ConsoleSink& operator =(const ConsoleSink&) = delete; 283 | 284 | /** 285 | * \brief Deleted move constructor 286 | */ 287 | ConsoleSink(ConsoleSink&&) = delete; 288 | /** 289 | * \brief Deleted move assign operator 290 | */ 291 | ConsoleSink& operator =(ConsoleSink&&) = delete; 292 | 293 | /** 294 | * \brief Sets the active ConsoleTextStylePolicy 295 | * \tparam TStylePolicy Type of the passed ConsoleTextStylePolicy (automatically deduced) 296 | * \param policy The new ConsoleTextStylePolicy object 297 | */ 298 | template TStylePolicy> 299 | void setTextStylePolicy(TStylePolicy&& policy) 300 | { 301 | std::scoped_lock lock{ m_TextStylePolicyMx }; 302 | m_TextStylePolicy = std::forward(policy); 303 | } 304 | 305 | /** 306 | * \brief Replaces the current ConsoleTextStylePolicy with the default one 307 | */ 308 | void removeTextStylePolicy() 309 | { 310 | std::scoped_lock lock{ m_TextStylePolicyMx }; 311 | m_TextStylePolicy = defaultTextStylePolicy(); 312 | } 313 | 314 | private: 315 | std::mutex m_TextStylePolicyMx; 316 | TextStylePolicy_t m_TextStylePolicy{ defaultTextStylePolicy() }; 317 | 318 | void beforeMessageWrite(const Record_t& record, std::string_view message) override 319 | { 320 | std::scoped_lock lock{ m_TextStylePolicyMx }; 321 | assert(m_TextStylePolicy && "TextStylePolicy must not be empty."); 322 | std::cout << std::invoke(m_TextStylePolicy, record); 323 | } 324 | 325 | void afterMessageWrite(const Record_t& record, std::string_view message) override 326 | { 327 | std::cout << defaultConsoleTextStyle; 328 | } 329 | }; 330 | 331 | /** @}*/ 332 | /** @}*/ 333 | } 334 | 335 | namespace sl::log::detail 336 | { 337 | inline std::ostream& applyTextColor(std::ostream& out, ConsoleTextStyle::Color color) noexcept 338 | { 339 | using Color = ConsoleTextStyle::Color; 340 | if (color < Color::brightBlack) 341 | { 342 | const auto begin = static_cast(rang::fg::black); 343 | out << static_cast(static_cast(color) + begin); 344 | } 345 | else 346 | { 347 | const auto begin = static_cast(rang::fgB::black); 348 | const auto localColor = static_cast(begin + 349 | static_cast(color) - 350 | static_cast(Color::brightBlack)); 351 | out << static_cast(localColor); 352 | } 353 | return out; 354 | } 355 | 356 | inline std::ostream& applyBackgroundColor(std::ostream& out, ConsoleTextStyle::Color color) noexcept 357 | { 358 | using Color = ConsoleTextStyle::Color; 359 | if (color < Color::brightBlack) 360 | { 361 | const auto begin = static_cast(rang::bg::black); 362 | out << static_cast(static_cast(color) + begin); 363 | } 364 | else 365 | { 366 | const auto begin = static_cast(rang::bgB::black); 367 | const auto localColor = static_cast(begin + 368 | static_cast(color) - 369 | static_cast(Color::brightBlack)); 370 | out << static_cast(localColor); 371 | } 372 | return out; 373 | } 374 | 375 | inline std::ostream& applyStyle(std::ostream& out, ConsoleTextStyle::Style style) noexcept 376 | { 377 | const auto value = static_cast(style); 378 | out << value; 379 | return out; 380 | } 381 | } 382 | 383 | #endif 384 | -------------------------------------------------------------------------------- /include/Simple-Log/Core.hpp: -------------------------------------------------------------------------------- 1 | // Copyright Dominic Koepke 2021 - 2021. 2 | // Distributed under the Boost Software License, Version 1.0. 3 | // (See accompanying file LICENSE_1_0.txt or copy at 4 | // https://www.boost.org/LICENSE_1_0.txt) 5 | 6 | #ifndef SL_LOG_CORE_HPP 7 | #define SL_LOG_CORE_HPP 8 | 9 | #pragma once 10 | 11 | #include "ISink.hpp" 12 | #include "Record.hpp" 13 | #include "RecordQueue.hpp" 14 | 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | 22 | namespace sl::log 23 | { 24 | /** \addtogroup Core 25 | * @{ 26 | */ 27 | 28 | /** 29 | * \brief The central point of the whole library. Needs to be instantiated at least once. 30 | * \tparam TRecord Used Record type. 31 | * 32 | * \details Objects of this class act like a broker between the (multiple) Logger s on the frontend and the (multiple) Sink s on the backend. Due to this there must at least one living 33 | * instance of Core during the whole program runtime, but it isn't restricted to exist uniquely. It can be totally fine to create one global Core, which will be used for general logging purposes, and multiple others for a 34 | * much lesser scope, like state logging on entity level. Logger will be permanently linked to one specific Core instance, thus Core instances must outlive their corresponding Logger s. 35 | * 36 | * Each instance of Core consists of the following: 37 | * \li one BlockingQueue in which all Records will get added 38 | * \li multiple Sink objects 39 | * \li one Worker thread, which will pull Records from that BlockingQueue and hand them over to the Sinks 40 | * 41 | * Due to this the Core is thread-safe by design. 42 | * 43 | * It is fine to add Sinks during later stages of your program. This hasn't necessarily to be done right after Core's creation. When Core goes out of scope, or is about to get destructed otherwise, it will block any new Records which could be 44 | * pushed into but will also let the Worker finish its work. Therefor it will wait in the destructor, unless there is an exception throwing, in which case Core will force the Worker to quit its work instantly. 45 | * 46 | * Core instances are neither copy- nor movable. 47 | */ 48 | 49 | template 50 | class Core 51 | { 52 | public: 53 | using Record_t = std::remove_cvref_t; 54 | 55 | private: 56 | using ISink_t = ISink; 57 | using RecordQueue_t = RecordQueue; 58 | using SinkContainer_t = std::vector>; 59 | 60 | public: 61 | /** 62 | * \brief Default Constructor 63 | * \details The internal Worker thread will directly start running. 64 | */ 65 | Core() noexcept : 66 | m_Worker{ m_WorkerInstruction, m_Records, m_SinkMx, m_Sinks } 67 | { 68 | m_WorkerFuture = std::async(std::launch::async, m_Worker); 69 | } 70 | 71 | /** 72 | * \brief Destructor 73 | * \details Will block until the internal Record queue is empty or an exception rises, which will then force the Worker thread to quit. 74 | */ 75 | ~Core() noexcept 76 | { 77 | try 78 | { 79 | m_WorkerInstruction = Worker::Instruction::quit; 80 | m_WorkerFuture.wait(); 81 | } 82 | catch (...) 83 | { 84 | m_WorkerInstruction = Worker::Instruction::forceQuit; 85 | } 86 | } 87 | 88 | /** 89 | * \brief Deleted copy constructor 90 | */ 91 | Core(const Core&) = delete; 92 | /** 93 | * \brief Deleted copy assign operator 94 | */ 95 | Core& operator =(const Core&) = delete; 96 | 97 | /** 98 | * \brief Deleted move constructor 99 | */ 100 | Core(Core&&) = delete; 101 | /** 102 | * \brief Deleted move assign operator 103 | */ 104 | Core& operator =(Core&&) = delete; 105 | 106 | /** 107 | * \brief Queues the Record internally 108 | * \param record The record which will be queued 109 | * \details This function should not be called directly on logging purposes. It serves as a simple interface for the corresponding Logger objects. 110 | */ 111 | void log(Record_t&& record) 112 | { 113 | // will reject newly generated records, after run has become false 114 | if (m_WorkerInstruction == Worker::Instruction::run) 115 | { 116 | m_Records.push(std::move(record)); 117 | } 118 | } 119 | 120 | /** 121 | * \brief Creates Sink and registers it at this Core instance 122 | * \tparam TSink Concrete Sink type 123 | * \tparam TArgs Constructor argument types (will be deducted automatically) 124 | * \param args The constructor arguments for the newly generated Sink object. Will be forwarded as is. 125 | * \return reference to the managed Sink object 126 | * \details This function creates a new Sink object and returns a reference to the caller. This Sink will be linked to and managed by the called Core instance. 127 | */ 128 | template TSink, class... TArgs> 129 | requires std::constructible_from 130 | TSink& makeSink(TArgs&&... args) 131 | { 132 | auto& ref = makeSinkImpl(std::forward(args)...); 133 | ref.setEnabled(); 134 | return ref; 135 | } 136 | 137 | /** 138 | * \brief Creates Sink disabled and registers it at this Core instance 139 | * \tparam TSink Concrete Sink type 140 | * \tparam TArgs Constructor argument types (will be deducted automatically) 141 | * \param args The constructor arguments for the newly generated Sink object. Will be forwarded as is. 142 | * \return Wrapped reference to the managed Sink object. 143 | * \details This function creates a new disabled Sink object and returns a wrapped reference to the caller. When this wrapper goes out of scope or gets destructed otherwise, 144 | * the attached Sink will become enabled. Use this if you want to make sure, that no messages will be handled before your Sink is finally setup. 145 | * This Sink will be linked to and managed by the called Core instance. 146 | */ 147 | template TSink, class... TArgs> 148 | requires std::constructible_from 149 | ScopedSinkDisabling makeDisabledSink(TArgs&&... args) 150 | { 151 | auto& ref = makeSinkImpl(std::forward(args)...); 152 | return ScopedSinkDisabling{ ref }; 153 | } 154 | 155 | /** 156 | * \brief Removes the given Sink and destroys it 157 | * \param sink The sink which will be destroyed 158 | * \return Returns true, if sink has been destroyed. 159 | * \details If this sink is registered at this Core instance, then it will be destroyed immediately. Otherwise nothing will change. 160 | */ 161 | bool removeSink(const ISink_t& sink) 162 | { 163 | std::scoped_lock lock{ m_SinkMx }; 164 | 165 | if (auto itr = std::ranges::find(m_Sinks, &sink, &std::unique_ptr::get); itr != std::end(m_Sinks)) 166 | { 167 | using std::swap; 168 | swap(*itr, m_Sinks.back()); 169 | m_Sinks.resize(std::size(m_Sinks) - 1); 170 | return true; 171 | } 172 | return false; 173 | } 174 | 175 | private: 176 | class Worker 177 | { 178 | public: 179 | enum class Instruction 180 | { 181 | run, 182 | quit, 183 | forceQuit 184 | }; 185 | 186 | Worker( 187 | const std::atomic& instruction, 188 | RecordQueue_t& records, 189 | std::mutex& sinkMx, 190 | const SinkContainer_t& sinks 191 | ) : 192 | m_Instruction{ instruction }, 193 | m_Records{ records }, 194 | m_SinkMx{ sinkMx }, 195 | m_Sinks{ sinks } 196 | { 197 | } 198 | 199 | void operator ()() const 200 | { 201 | for (auto instruction = m_Instruction.load(); 202 | instruction != Instruction::forceQuit && 203 | (instruction != Instruction::quit || !std::empty(m_Records)); 204 | instruction = m_Instruction) 205 | { 206 | if (auto optRecord = m_Records.take(std::chrono::milliseconds{ 200 })) 207 | { 208 | std::scoped_lock lock{ m_SinkMx }; 209 | std::for_each( 210 | std::execution::par, 211 | std::begin(m_Sinks), 212 | std::end(m_Sinks), 213 | [&record = *optRecord](auto& sink) 214 | { 215 | sink->log(record); 216 | } 217 | ); 218 | } 219 | } 220 | } 221 | 222 | private: 223 | const std::atomic& m_Instruction; 224 | 225 | RecordQueue_t& m_Records; 226 | 227 | std::mutex& m_SinkMx; 228 | const SinkContainer_t& m_Sinks; 229 | }; 230 | 231 | RecordQueue_t m_Records; 232 | 233 | std::mutex m_SinkMx; 234 | SinkContainer_t m_Sinks; 235 | 236 | std::atomic m_WorkerInstruction{ Worker::Instruction::run }; 237 | Worker m_Worker; 238 | std::future m_WorkerFuture; 239 | 240 | template TSink, class... TArgs> 241 | requires std::constructible_from 242 | TSink& makeSinkImpl(TArgs&&... args) 243 | { 244 | auto sink = std::make_unique(std::forward(args)...); 245 | auto& ref = *sink; 246 | std::scoped_lock lock{ m_SinkMx }; 247 | m_Sinks.emplace_back(std::move(sink)); 248 | return ref; 249 | } 250 | }; 251 | 252 | /** @}*/ 253 | } 254 | 255 | #endif 256 | -------------------------------------------------------------------------------- /include/Simple-Log/FileSink.hpp: -------------------------------------------------------------------------------- 1 | // Copyright Dominic Koepke 2021 - 2021. 2 | // Distributed under the Boost Software License, Version 1.0. 3 | // (See accompanying file LICENSE_1_0.txt or copy at 4 | // https://www.boost.org/LICENSE_1_0.txt) 5 | 6 | #ifndef SL_LOG_FILE_SINK_HPP 7 | #define SL_LOG_FILE_SINK_HPP 8 | 9 | #pragma once 10 | 11 | #include "Record.hpp" 12 | #include "OStreamSink.hpp" 13 | #include "StringPattern.hpp" 14 | 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | 21 | namespace sl::log 22 | { 23 | /** \addtogroup Sinks 24 | * @{ 25 | */ 26 | 27 | /** 28 | * \brief Concept for invokable file state handler objects 29 | */ 30 | template 31 | concept FileStateHandler = std::is_invocable_r_v; 32 | 33 | /** 34 | * \brief Class for logging into files 35 | * \tparam TRecord Used Record type. 36 | * \details Instances of this class are linked to a specific file and writes every Record into it. Users can setup \ref Rotation "rotation" and cleanup rules. 37 | * 38 | * \section FileNamePattern File name pattern 39 | * Users can provide a pattern string, which will be used for newly generated file names. Those file names will be generated when a new file shall be opened. 40 | * There are mainly two parts of those strings: 41 | * \li Constant parts are all substrings which do not contain any known tokens and will simply be used as is in file names. 42 | * \li Dynamic parts are substrings which contain know tokens, which will be replaced with actual data when a new file name will be generated. Look at the \ref Tokens "token" section of this page for further details. 43 | * 44 | * \subsection Tokens 45 | * Each token begins with \c % 46 | * Token | Effect 47 | * ---------|-------- 48 | * \%Y | year in 4 digits 49 | * \%m | month in 2 digits (including leading zeros) 50 | * \%d | day of month in 2 digits (including leading zeros) 51 | * \%j | day of year 52 | * \%H | hour in 24h format (2 digits with leading zeros) 53 | * \%M | minute (2 digits with leading zeros) 54 | * \%S | second (2 digits with leading zeros) 55 | * \%N | incrementing number starting at 1 56 | * \%xN | incrementing number with x digits and leading zeros. Replace \a x with an actual number (might be multiple digits). Starts at 1. 57 | * 58 | * \section Rotation Rotation 59 | * Rotation rules are used as an indicator when to switch to a new file. Currently are two basic rules implemented: 60 | * \li file size 61 | * \li duration 62 | * 63 | * Each rule will be checked before a new Record will be written to file. If a new file shall be opened, a new file name will be generated via provided file name pattern. 64 | * If this file already exists, it will be overriden (or an exception raises if it is already in use). 65 | * 66 | * \section Cleanup Cleanup 67 | * While \ref Rotation "rotation" is used for indicating when to switch files, CleanupRule s are used for determining when to delete old files. Cleanup process will watch at the currently provided directory 68 | * and will only observe files with equally file extensions as provided in the file name pattern. If no extension (e.g. \c .abcd) is used, all files without an extension will be observed. 69 | * 70 | * \section FileStateHandler File state handler 71 | * These handler are used to generate messages on state changes for the internal managed file. Users can provide invokable objects, which will be called and shall return a std::string, which will then written synchronized 72 | * to the std::fstream. The synchronization is the reason, why the user is not allowed to write to the stream themselves.\n 73 | * Handlers shall have the following signature: 74 | * \code{.cpp} 75 | * std::string() 76 | * \endcode 77 | */ 78 | template 79 | class FileSink final : 80 | public OStreamSink 81 | { 82 | using Super = OStreamSink; 83 | 84 | public: 85 | using typename Super::Record_t; 86 | using typename Super::Formatter_t; 87 | using typename Super::Filter_t; 88 | using typename Super::FlushPolicy_t; 89 | 90 | /** 91 | * \brief Type for configuring FileSink rotation rules 92 | */ 93 | struct RotationRule 94 | { 95 | std::optional fileSize; 96 | std::optional duration; 97 | }; 98 | 99 | /** 100 | * \brief Type for configuring FileSink cleanup rules 101 | */ 102 | struct CleanupRule 103 | { 104 | std::optional directorySize; 105 | std::optional fileCount; 106 | }; 107 | 108 | /** 109 | * \brief Constructor 110 | * \param fileNamePattern Pattern string from which new file names will be generated. 111 | * \param directory The directory where all files of this sink will be generated. 112 | * \details Constructs a new FileSink instance, which uses the provided file name pattern for newly opened files at the specified directory. 113 | * \remark It is not intended, that the pattern string contains any directory information. Use the directory property instead. 114 | * \throws SinkException if pattern string is empty or contains directory information 115 | */ 116 | explicit FileSink(std::string fileNamePattern, std::filesystem::path directory = std::filesystem::current_path()) : 117 | Super{ m_FileStream } 118 | { 119 | setFileNamePattern(std::move(fileNamePattern)); 120 | setDirectory(std::move(directory)); 121 | } 122 | 123 | /** 124 | * \brief Destructor 125 | * \details Finalizes and closes the current opened file (if any). 126 | */ 127 | ~FileSink() noexcept 128 | { 129 | try 130 | { 131 | if (m_FileStream.is_open()) 132 | { 133 | closeFile(); 134 | } 135 | } 136 | catch (...) 137 | { 138 | } 139 | } 140 | 141 | /** 142 | * \brief Deleted copy constructor 143 | */ 144 | FileSink(const FileSink&) = delete; 145 | /** 146 | * \brief Deleted copy assign operator 147 | */ 148 | FileSink& operator =(const FileSink&) = delete; 149 | /** 150 | * \brief Deleted move constructor 151 | */ 152 | FileSink(FileSink&&) = delete; 153 | /** 154 | * \brief Deleted move assign operator 155 | */ 156 | FileSink& operator =(FileSink&&) = delete; 157 | 158 | /** 159 | * \brief Applies a new RotationRule configuration. 160 | * \param rule The new RotationRule configuration. 161 | */ 162 | void setRotationRule(RotationRule rule) noexcept 163 | { 164 | std::scoped_lock lock{ m_RotationRuleMx }; 165 | m_RotationRule = rule; 166 | } 167 | 168 | /** 169 | * \brief Returns a copy of the current RotationRule configuration 170 | */ 171 | [[nodiscard]] 172 | RotationRule rotationRule() const noexcept 173 | { 174 | return load(m_RotationRule, m_RotationRuleMx); 175 | } 176 | 177 | /** 178 | * \brief Rotates the current file 179 | * \details This function has no effect, if the file stream is not already open. 180 | */ 181 | void rotate() 182 | { 183 | if (m_FileStream.is_open()) 184 | { 185 | closeFile(); 186 | openFile(); 187 | } 188 | } 189 | 190 | /** 191 | * \brief Applies a new CleanupRule configuration. 192 | * \param rule The new CleanupRule configuration. 193 | */ 194 | void setCleanupRule(CleanupRule rule) noexcept 195 | { 196 | std::scoped_lock lock{ m_CleanupRuleMx }; 197 | m_CleanupRule = rule; 198 | } 199 | 200 | /** 201 | * \brief Returns a copy of the current CleanupRule configuration 202 | */ 203 | [[nodiscard]] 204 | CleanupRule cleanupRule() const noexcept 205 | { 206 | return load(m_CleanupRule, m_CleanupRuleMx); 207 | } 208 | 209 | /** 210 | * \brief Applies a new handler for opening files 211 | * \details For further details look at \ref FileStateHandler "file state handler". 212 | * \tparam THandler Type of handler (automatically deduced) 213 | * \param handler invokable handler object 214 | */ 215 | template 216 | void setOpeningHandler(THandler&& handler) noexcept 217 | { 218 | std::scoped_lock lock{ m_OpeningHandlerMx }; 219 | m_OpeningHandler = std::forward(handler); 220 | } 221 | 222 | /** 223 | * \brief Removes the active opening handler 224 | */ 225 | void removeOpeningHandler() noexcept 226 | { 227 | std::scoped_lock lock{ m_OpeningHandlerMx }; 228 | m_OpeningHandler = nullptr; 229 | } 230 | 231 | /** 232 | * \brief Applies a new handler for closing files 233 | * \details For further details look at \ref FileStateHandler "file state handler". 234 | * \tparam THandler Type of handler (automatically deduced) 235 | * \param handler invokable handler object 236 | */ 237 | template 238 | void setClosingHandler(THandler&& handler) noexcept 239 | { 240 | std::scoped_lock lock{ m_ClosingHandlerMx }; 241 | m_ClosingHandler = std::forward(handler); 242 | } 243 | 244 | /** 245 | * \brief Removes the active closing handler 246 | */ 247 | void removeClosingHandler() noexcept 248 | { 249 | std::scoped_lock lock{ m_ClosingHandlerMx }; 250 | m_ClosingHandler = nullptr; 251 | } 252 | 253 | /** 254 | * \brief Sets the directory in which the log files will be created 255 | * \param directory Path object 256 | * \details If the given directory does not exist, it will be created. 257 | */ 258 | void setDirectory(std::filesystem::path directory) 259 | { 260 | std::scoped_lock lock{ m_FilePathNameMx }; 261 | m_Directory = std::move(directory); 262 | create_directories(m_Directory); 263 | } 264 | 265 | /** 266 | * \brief Getter of the directory member 267 | * \return A copy of the path object 268 | */ 269 | [[nodiscard]] 270 | std::filesystem::path directory() const noexcept 271 | { 272 | std::scoped_lock lock{ m_FilePathNameMx }; 273 | return m_Directory; 274 | } 275 | 276 | /** 277 | * \brief Sets the file name pattern for generated log files 278 | * \param fileNamePattern Pattern string 279 | * \details For further details look \ref FileNamePattern "here". 280 | * \remark It is not intended, that the pattern string contains any directory information. Use the directory property instead. 281 | * \throws SinkException if pattern string is empty or contains directory information 282 | */ 283 | void setFileNamePattern(std::string fileNamePattern) 284 | { 285 | if (std::empty(fileNamePattern)) 286 | { 287 | throw SinkException{ "FileNamePattern must not be empty." }; 288 | } 289 | 290 | if (std::filesystem::path(fileNamePattern).has_parent_path()) 291 | { 292 | throw SinkException{ "FileNamePattern must contain any directory information. Use directory property instead." }; 293 | } 294 | 295 | std::scoped_lock lock{ m_FilePathNameMx }; 296 | m_FileNamePattern.setPatternString(std::move(fileNamePattern)); 297 | } 298 | 299 | /** 300 | * \brief Getter of the used file name pattern string 301 | * \return Returns a copy of the active pattern string 302 | */ 303 | [[nodiscard]] 304 | std::string fileNamePattern() const noexcept 305 | { 306 | std::scoped_lock lock{ m_FilePathNameMx }; 307 | return std::string{ m_FileNamePattern.patternString() }; 308 | } 309 | 310 | private: 311 | using FileStateHandler = std::function; 312 | 313 | mutable std::mutex m_FilePathNameMx; 314 | StringPattern m_FileNamePattern; 315 | std::filesystem::path m_Directory; 316 | 317 | std::ofstream m_FileStream; 318 | std::optional m_CurrentFilePath; 319 | 320 | // rotation related 321 | std::atomic m_FileOpeningTime; 322 | mutable std::mutex m_RotationRuleMx; 323 | RotationRule m_RotationRule; 324 | 325 | // cleanup related 326 | mutable std::mutex m_CleanupRuleMx; 327 | CleanupRule m_CleanupRule; 328 | 329 | // File Handler 330 | std::mutex m_OpeningHandlerMx; 331 | FileStateHandler m_OpeningHandler; 332 | std::mutex m_ClosingHandlerMx; 333 | FileStateHandler m_ClosingHandler; 334 | 335 | void openFile() 336 | { 337 | auto filePath = [&] 338 | { 339 | std::scoped_lock lock{ m_FilePathNameMx }; 340 | return m_Directory / m_FileNamePattern.next(); 341 | }(); 342 | 343 | m_FileStream.open(filePath); 344 | if (!m_FileStream.is_open()) 345 | { 346 | throw SinkException{ "FileSink: Attempted opening file \"" + filePath.string() + "\" but failed." }; 347 | } 348 | 349 | m_CurrentFilePath = std::move(filePath); 350 | m_FileOpeningTime = std::chrono::steady_clock::now(); 351 | 352 | if (std::scoped_lock lock{ m_OpeningHandlerMx }; m_OpeningHandler) 353 | { 354 | Super::writeToStream(m_OpeningHandler()); 355 | } 356 | } 357 | 358 | void closeFile() 359 | { 360 | assert(m_FileStream.is_open() && "FileStream must be open."); 361 | assert(m_CurrentFilePath && !std::empty(*m_CurrentFilePath)); 362 | 363 | if (std::scoped_lock lock{ m_ClosingHandlerMx }; m_ClosingHandler) 364 | { 365 | Super::writeToStream(m_ClosingHandler()); 366 | } 367 | 368 | m_FileStream.close(); 369 | removeFilesIfNecessary(); 370 | m_CurrentFilePath.reset(); 371 | m_FileOpeningTime.store({}); 372 | } 373 | 374 | void removeFilesIfNecessary() 375 | { 376 | // ToDo: use c++20 ranges::view 377 | auto directoryItr = std::filesystem::directory_iterator(m_CurrentFilePath->parent_path()); 378 | 379 | std::vector files; 380 | for (const auto& entry : directoryItr) 381 | { 382 | if (is_regular_file(entry) && entry.path().extension() == m_CurrentFilePath->extension() && 383 | !equivalent(entry.path(), *m_CurrentFilePath)) 384 | { 385 | files.emplace_back(entry); 386 | } 387 | } 388 | 389 | std::ranges::sort(files, std::greater(), [](const auto& file) { return last_write_time(file); }); 390 | fulfillFileCountCleanup(files); 391 | fulfillDirectorySizeCleanup(files); 392 | } 393 | 394 | template 395 | static T load(const T& object, TMutex& mutex) 396 | { 397 | std::scoped_lock lock{ mutex }; 398 | return object; 399 | } 400 | 401 | void fulfillFileCountCleanup(std::vector& files) const 402 | { 403 | auto cleanupRule = load(m_CleanupRule, m_CleanupRuleMx); 404 | if (!cleanupRule.fileCount) 405 | return; 406 | 407 | while (*cleanupRule.fileCount < std::size(files)) 408 | { 409 | auto& file = files.back(); 410 | remove(file); 411 | files.pop_back(); 412 | } 413 | } 414 | 415 | void fulfillDirectorySizeCleanup(std::vector& files) const 416 | { 417 | auto cleanupRule = load(m_CleanupRule, m_CleanupRuleMx); 418 | if (!cleanupRule.directorySize) 419 | return; 420 | 421 | auto size = std::accumulate( 422 | std::begin(files), 423 | std::end(files), 424 | 0ull, 425 | [](auto value, const auto& file) { return value + file.file_size(); } 426 | ); 427 | while (*cleanupRule.directorySize < size) 428 | { 429 | auto& file = files.back(); 430 | size -= file_size(file); 431 | remove(file); 432 | files.pop_back(); 433 | } 434 | } 435 | 436 | [[nodiscard]] 437 | bool shallRotate() const 438 | { 439 | assert(m_FileStream.is_open() && m_CurrentFilePath && !std::empty(*m_CurrentFilePath)); 440 | 441 | auto rotationRule = load(m_RotationRule, m_RotationRuleMx); 442 | return (rotationRule.fileSize && *rotationRule.fileSize < file_size(*m_CurrentFilePath)) || 443 | (rotationRule.duration && m_FileOpeningTime.load() + *rotationRule.duration < std::chrono::steady_clock::now() 444 | ); 445 | } 446 | 447 | void beforeMessageWrite(const Record_t& record, std::string_view message) override 448 | { 449 | if (!m_FileStream.is_open()) 450 | { 451 | openFile(); 452 | } 453 | else if (shallRotate()) 454 | { 455 | closeFile(); 456 | openFile(); 457 | } 458 | } 459 | }; 460 | 461 | /** @}*/ 462 | } 463 | 464 | #endif 465 | -------------------------------------------------------------------------------- /include/Simple-Log/Filters.hpp: -------------------------------------------------------------------------------- 1 | // Copyright Dominic Koepke 2021 - 2021. 2 | // Distributed under the Boost Software License, Version 1.0. 3 | // (See accompanying file LICENSE_1_0.txt or copy at 4 | // https://www.boost.org/LICENSE_1_0.txt) 5 | 6 | #ifndef SL_LOG_FILTERS_HPP 7 | #define SL_LOG_FILTERS_HPP 8 | 9 | #pragma once 10 | 11 | #include "Record.hpp" 12 | #include "TupleAlgorithms.hpp" 13 | 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | 20 | namespace sl::log 21 | { 22 | /** \addtogroup Filter 23 | * @{ 24 | * \details Filters operate on Sink level and determine if a Record will be handled by that Sink object or skipped. Users are able apply provided filter like the ProjectionFilter types 25 | * or use custom types. Those custom types can be from any type which is invokable by the used Record type and returning a bool (or at least a bool-comparable-type) as result. 26 | * 27 | * There are already some convenience factory function present (e.g. makeMessageProjectionFor), which may help setting up filters quickly. 28 | */ 29 | 30 | /** 31 | * \brief Combines a projection on Record type with a predicate into an invokable object 32 | * \tparam TProjection A Projection type, which has to accept an object of the used Record type and must return something 33 | * \tparam TUnaryPredicate A predicate type, which accepts objects returned by TProjection 34 | * \details Instances of this class are invokable with any types, which follow the Record concept. Due to possible custom implementations 35 | * or adjustments ProjectionFilter can't rely on the libraries BaseRecord type and therefore is not able to pre-check the provided Projection and 36 | * Predicate via concept. 37 | */ 38 | template 39 | class ProjectionFilter 40 | { 41 | public: 42 | using Projection_t = std::remove_cvref_t; 43 | using UnaryPredicate_t = std::remove_cvref_t; 44 | 45 | /** 46 | * \brief Constructor 47 | * \param projection Invokable object 48 | * \param predicate Predicate object 49 | */ 50 | constexpr ProjectionFilter( 51 | TProjection projection, 52 | TUnaryPredicate predicate 53 | ) 54 | noexcept(std::is_nothrow_move_constructible_v && std::is_nothrow_move_constructible_v) : 55 | m_Projection{ std::move(projection) }, 56 | m_Predicate{ std::move(predicate) } 57 | { 58 | } 59 | 60 | /** 61 | * \brief Call-operator 62 | * \tparam TRecord Concrete type of the used Record object 63 | * \param rec Record object 64 | * \return Returns true, if the Record should be handled. False if skipped. 65 | */ 66 | template 67 | constexpr bool operator ()(const TRecord& rec) 68 | { 69 | return std::invoke(m_Predicate, std::invoke(m_Projection, rec)); 70 | } 71 | 72 | private: 73 | Projection_t m_Projection; 74 | UnaryPredicate_t m_Predicate; 75 | }; 76 | 77 | /** 78 | * \brief Chains multiple filter together 79 | * \tparam TAlgorithm The used algorithm, which will determine how each invoke result will be treated. 80 | * \tparam TFilter Type of provided filter objects 81 | * \details This class simply calls each provided filter successively. The provided algorithm will determine, how each invocation result will be treated. 82 | */ 83 | template 84 | class FilterChain 85 | { 86 | public: 87 | using Algorithm_t = std::remove_cvref_t; 88 | 89 | /** 90 | * \brief Constructor 91 | * \param filter Filter objects 92 | */ 93 | constexpr explicit FilterChain( 94 | TFilter ...filter 95 | ) 96 | noexcept(std::is_nothrow_constructible_v && (std::is_nothrow_move_constructible_v && ...)) : 97 | m_Algorithm{}, 98 | m_Filter{ std::move(filter)... } 99 | { 100 | } 101 | 102 | /** 103 | * \brief Constructor overload 104 | * \param algorithm Algorithm object 105 | * \param filter Filter objects 106 | */ 107 | constexpr explicit FilterChain( 108 | TAlgorithm algorithm, 109 | TFilter ...filter 110 | ) noexcept(std::is_nothrow_move_constructible_v && (std::is_nothrow_move_constructible_v && ...)) : 111 | m_Algorithm{ std::move(algorithm) }, 112 | m_Filter{ std::forward(filter)... } 113 | { 114 | } 115 | 116 | /** 117 | * \brief Call-operator 118 | * \tparam TRecord Concrete type of the used Record object 119 | * \param rec Record object 120 | * \return Returns true, if the Record should be handled. False if skipped. 121 | */ 122 | template 123 | constexpr bool operator()(const TRecord& rec) 124 | { 125 | return std::invoke(m_Algorithm, m_Filter, rec); 126 | } 127 | 128 | /** 129 | * \brief Returns whether the are no sub-filters attached. 130 | * \return true if there aren't have any sub-filters attached. 131 | */ 132 | [[nodiscard]] 133 | constexpr bool empty() const noexcept 134 | { 135 | return std::tuple_size_v == 0; 136 | } 137 | 138 | /** 139 | * \brief Obtains the amount of attached sub-filters 140 | * \return The amount of attached sub-filters 141 | */ 142 | [[nodiscard]] 143 | constexpr std::size_t size() const noexcept 144 | { 145 | return std::tuple_size_v; 146 | } 147 | 148 | private: 149 | Algorithm_t m_Algorithm; 150 | std::tuple m_Filter; 151 | }; 152 | 153 | //ToDo: Clang currently doesn't support alias CTAD: http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2019/p1814r0.html 154 | //template ... TFilter> 155 | //using FilterDisjunction = FilterChain; 156 | // 157 | //template ... TFilter> 158 | //using FilterConjunction = FilterChain; 159 | 160 | /** 161 | * \brief Convenience type for chaining multiple filter with AND 162 | * \tparam TFilter Type of provided filter objects 163 | */ 164 | template 165 | class FilterAllOf : 166 | public FilterChain 167 | { 168 | using Algorithm_t = detail::TupleAllOf; 169 | 170 | public: 171 | /** 172 | * \brief Constructor 173 | * \param filter Filter objects 174 | */ 175 | constexpr explicit FilterAllOf( 176 | TFilter ... filter 177 | ) 178 | noexcept((std::is_nothrow_move_constructible_v && ...)) : 179 | FilterChain{ std::move(filter)... } 180 | { 181 | } 182 | }; 183 | 184 | /** 185 | * \brief Convenience type for chaining multiple filter with OR 186 | * \tparam TFilter Type of provided filter objects 187 | */ 188 | template 189 | class FilterAnyOf : 190 | public FilterChain 191 | { 192 | using Algorithm_t = detail::TupleAnyOf; 193 | 194 | public: 195 | /** 196 | * \brief Constructor 197 | * \param filter Filter objects 198 | */ 199 | constexpr explicit FilterAnyOf( 200 | TFilter ... filter 201 | ) 202 | noexcept((std::is_nothrow_move_constructible_v && ...)) : 203 | FilterChain{ std::move(filter)... } 204 | { 205 | } 206 | }; 207 | 208 | /** 209 | * \brief Convenience type for chaining multiple filter with NOR 210 | * \tparam TFilter Type of provided filter objects 211 | */ 212 | template 213 | class FilterNoneOf : 214 | public FilterChain 215 | { 216 | using Algorithm_t = detail::TupleNoneOf; 217 | 218 | public: 219 | /** 220 | * \brief Constructor 221 | * \param filter Filter objects 222 | */ 223 | constexpr explicit FilterNoneOf( 224 | TFilter ... filter 225 | ) 226 | noexcept((std::is_nothrow_move_constructible_v && ...)) : 227 | FilterChain{ std::move(filter)... } 228 | { 229 | } 230 | }; 231 | 232 | /** 233 | * \brief Factory function for creating ProjectionFilter of Record::message member 234 | * \tparam TRecord Concrete Record type on which to apply the projection 235 | * \tparam TUnaryPredicate Invokable type, which has to accept objects the actual Record::Message_t type 236 | * \param predicate Predicate object 237 | * \return ProjectionFilter object 238 | * \details This is the preferable way creating a filter for the Record::message member, because the predicate becomes strong checked via 239 | * concept and therefore will provide much clearer feedback in cases of error, while creating ProjectionFilter objects manually will 240 | * potentially result in harder to read error message. 241 | */ 242 | template &> TUnaryPredicate> 243 | constexpr auto makeMessageFilterFor(TUnaryPredicate&& predicate) 244 | { 245 | return ProjectionFilter{ RecordGetters::message, std::forward(predicate) }; 246 | } 247 | 248 | /** 249 | * \brief Factory function for creating ProjectionFilter of Record::severity member 250 | * \tparam TRecord Concrete Record type on which to apply the projection 251 | * \tparam TUnaryPredicate Invokable type, which has to accept objects the actual Record::SeverityLevel_t type 252 | * \param predicate Predicate object 253 | * \return ProjectionFilter object 254 | * \details This is the preferable way creating a filter for the Record::severity member, because the predicate becomes strong checked via 255 | * concept and therefore will provide much clearer feedback in cases of error, while creating ProjectionFilter objects manually will 256 | * potentially result in harder to read error message. 257 | */ 258 | template &> TUnaryPredicate> 259 | constexpr auto makeSeverityFilterFor(TUnaryPredicate&& predicate) 260 | { 261 | return ProjectionFilter{ RecordGetters::severity, std::forward(predicate) }; 262 | } 263 | 264 | /** 265 | * \brief Factory function for creating ProjectionFilter of Record::channel member 266 | * \tparam TRecord Concrete Record type on which to apply the projection 267 | * \tparam TUnaryPredicate Invokable type, which has to accept objects the actual Record::Channel_t type 268 | * \param predicate Predicate object 269 | * \return ProjectionFilter object 270 | * \details This is the preferable way creating a filter for the Record::channel member, because the predicate becomes strong checked via 271 | * concept and therefore will provide much clearer feedback in cases of error, while creating ProjectionFilter objects manually will 272 | * potentially result in harder to read error message. 273 | */ 274 | template &> TUnaryPredicate> 275 | constexpr auto makeChannelFilterFor(TUnaryPredicate&& predicate) 276 | { 277 | return ProjectionFilter{ RecordGetters::channel, std::forward(predicate) }; 278 | } 279 | 280 | /** 281 | * \brief Factory function for creating ProjectionFilter of Record::timePoint member 282 | * \tparam TRecord Concrete Record type on which to apply the projection 283 | * \tparam TUnaryPredicate Invokable type, which has to accept objects the actual Record::TimePoint_t type 284 | * \param predicate Predicate object 285 | * \return ProjectionFilter object 286 | * \details This is the preferable way creating a filter for the Record::timePoint member, because the predicate becomes strong checked via 287 | * concept and therefore will provide much clearer feedback in cases of error, while creating ProjectionFilter objects manually will 288 | * potentially result in harder to read error message. 289 | */ 290 | template &> TUnaryPredicate> 291 | constexpr auto makeTimePointFilterFor(TUnaryPredicate&& predicate) 292 | { 293 | return ProjectionFilter{ RecordGetters::timePoint, std::forward(predicate) }; 294 | } 295 | 296 | /** @}*/ 297 | } 298 | 299 | #endif 300 | -------------------------------------------------------------------------------- /include/Simple-Log/ISink.hpp: -------------------------------------------------------------------------------- 1 | // Copyright Dominic Koepke 2021 - 2021. 2 | // Distributed under the Boost Software License, Version 1.0. 3 | // (See accompanying file LICENSE_1_0.txt or copy at 4 | // https://www.boost.org/LICENSE_1_0.txt) 5 | 6 | #ifndef SL_LOG_ISINK_HPP 7 | #define SL_LOG_ISINK_HPP 8 | 9 | #pragma once 10 | 11 | #include "Record.hpp" 12 | 13 | #include 14 | #include 15 | 16 | namespace sl::log 17 | { 18 | /** \addtogroup Sinks 19 | * @{ 20 | */ 21 | 22 | /** 23 | * \brief Sink interface class 24 | * \tparam TRecord Used Record type. 25 | * \details The interface class for each derived Sink type. If you want to implement your custom Sink type, have a look at BasicSink first. This might be a better starting point for customizations. 26 | */ 27 | template 28 | class ISink 29 | { 30 | public: 31 | /** 32 | * \brief Used Record type. 33 | */ 34 | using Record_t = std::remove_cvref_t; 35 | 36 | /** 37 | * \brief Deleted copy-constructor 38 | */ 39 | ISink(const ISink&) = delete; 40 | /** 41 | * \brief Deleted copy-assign operator 42 | */ 43 | ISink& operator =(const ISink&) = delete; 44 | 45 | /** 46 | * \brief virtual default destructor 47 | */ 48 | virtual ~ISink() noexcept = default; 49 | 50 | /** 51 | * \brief virtual log function 52 | * \details Will be called from the Worker thread. 53 | * \param record The record about to be processed by the sink 54 | */ 55 | virtual void log(const Record_t& record) = 0; 56 | 57 | /** 58 | * \brief Enables or disables the Sink object 59 | * \details Disabled Sinks will not handle any incoming Record s 60 | * \param enable True will enable the Sink object. 61 | */ 62 | virtual void setEnabled(bool enable = true) noexcept = 0; 63 | 64 | /** 65 | * \brief Checks if the Sink object is enabled. 66 | * \return Returns true if object is enabled. 67 | */ 68 | [[nodiscard]] 69 | virtual bool isEnabled() const noexcept = 0; 70 | 71 | protected: 72 | /** 73 | * \brief Default constructor 74 | */ 75 | ISink() = default; 76 | /** 77 | * \brief Default move-constructor 78 | */ 79 | ISink(ISink&&) = default; 80 | /** 81 | * \brief Default move-assign operator 82 | */ 83 | ISink& operator =(ISink&&) = default; 84 | }; 85 | 86 | /** 87 | * \brief Wrapper class which disables Sinks on construction and enables them on destruction 88 | * \tparam TRecord Record type 89 | * \tparam TSink Sink type 90 | * \details This helper class is useful when you want to get sure, that your Sinks will be finally setup before they are going to handle any records. 91 | * Instances of this class are movable but not copyable. 92 | */ 93 | template > TSink> 94 | class ScopedSinkDisabling 95 | { 96 | public: 97 | /** 98 | * \brief Constructor which disables passed sink 99 | * \param sink 100 | */ 101 | explicit ScopedSinkDisabling(TSink& sink) noexcept : 102 | m_Sink{ &sink } 103 | { 104 | m_Sink->setEnabled(false); 105 | } 106 | 107 | /** 108 | * \brief Constructor which enables passed sink 109 | */ 110 | ~ScopedSinkDisabling() noexcept 111 | { 112 | if (m_Sink) 113 | { 114 | m_Sink->setEnabled(); 115 | m_Sink = nullptr; 116 | } 117 | } 118 | 119 | /** 120 | * \brief Deleted copy constructor 121 | */ 122 | ScopedSinkDisabling(const ScopedSinkDisabling&) = delete; 123 | 124 | /** 125 | * \brief Deleted copy-assign operator 126 | */ 127 | ScopedSinkDisabling& operator =(const ScopedSinkDisabling&) = delete; 128 | 129 | /** 130 | * \brief Move constructor 131 | */ 132 | ScopedSinkDisabling(ScopedSinkDisabling&& other) noexcept 133 | { 134 | *this = std::move(other); 135 | } 136 | 137 | /** 138 | * \brief Move-assign operator 139 | */ 140 | ScopedSinkDisabling& operator =(ScopedSinkDisabling&& other) noexcept 141 | { 142 | using std::swap; 143 | swap(m_Sink, other.m_Sink); 144 | return *this; 145 | } 146 | 147 | /** 148 | * \brief Dereferencing operator 149 | * \return Reference to the wrapped Sink instance. 150 | */ 151 | [[nodiscard]] 152 | TSink& operator *() const noexcept 153 | { 154 | return *m_Sink; 155 | } 156 | 157 | /** 158 | * \brief Dereferencing operator 159 | * \return Pointer to the wrapped Sink instance. 160 | */ 161 | [[nodiscard]] 162 | TSink* operator ->() const noexcept 163 | { 164 | return m_Sink; 165 | } 166 | 167 | /** 168 | * \brief Returns a pointer to the Sink instance 169 | * \return Pointer to the wrapped Sink instance. 170 | */ 171 | [[nodiscard]] 172 | TSink* get() const noexcept 173 | { 174 | return m_Sink; 175 | } 176 | 177 | private: 178 | TSink* m_Sink = nullptr; 179 | }; 180 | 181 | class SinkException final : 182 | public std::runtime_error 183 | { 184 | public: 185 | explicit SinkException(const std::string& message) : 186 | runtime_error{ message } 187 | { 188 | } 189 | 190 | explicit SinkException(const char* message) : 191 | runtime_error{ message } 192 | { 193 | } 194 | }; 195 | 196 | /** @}*/ 197 | } 198 | 199 | #endif 200 | -------------------------------------------------------------------------------- /include/Simple-Log/Logger.hpp: -------------------------------------------------------------------------------- 1 | // Copyright Dominic Koepke 2021 - 2021. 2 | // Distributed under the Boost Software License, Version 1.0. 3 | // (See accompanying file LICENSE_1_0.txt or copy at 4 | // https://www.boost.org/LICENSE_1_0.txt) 5 | 6 | #ifndef SL_LOG_LOGGER_HPP 7 | #define SL_LOG_LOGGER_HPP 8 | 9 | #pragma once 10 | 11 | #include "Core.hpp" 12 | #include "Record.hpp" 13 | #include "RecordBuilder.hpp" 14 | 15 | #include 16 | #include 17 | #include 18 | 19 | #ifdef __cpp_lib_source_location 20 | #include 21 | #endif 22 | 23 | namespace sl::log 24 | { 25 | /** \addtogroup Logger 26 | * @{ 27 | */ 28 | 29 | /** \addtogroup LoggerConcepts Logger Concepts 30 | * @{ 31 | */ 32 | 33 | /** 34 | * \brief Provides a layer of abstraction to Record member types 35 | * \tparam TLogger The used Logger type 36 | * \details There is no need for the users using this type of indirection. The library makes use of this abstraction, thus 37 | * users may specialize it to use Logger types, which does not fit in the original concept and trait syntax provided by this library. 38 | * \version since alpha-v0.7 39 | */ 40 | template 41 | struct LoggerTypedefs 42 | { 43 | using Record_t = typename TLogger::Record_t; 44 | }; 45 | 46 | /** 47 | * \typedef LoggerRecord_t 48 | * \brief Typedef for easier access to \ref Logger "Logger's" Record type. 49 | * \tparam TLogger The used Logger type. 50 | * \version since alpha-v0.7 51 | */ 52 | template 53 | /** \cond Requires */ 54 | requires requires 55 | { 56 | typename LoggerTypedefs::Record_t; 57 | } 58 | /** \endcond */ 59 | using LoggerRecord_t = typename LoggerTypedefs::Record_t; 60 | 61 | /** 62 | * \brief Concept for Logger classes 63 | */ 64 | template 65 | concept Logger = 66 | requires 67 | { 68 | typename LoggerRecord_t; 69 | } && 70 | Record> && 71 | std::is_invocable_r_v>, T>; 72 | 73 | /** @}*/ 74 | 75 | /** 76 | * \brief Convenience class for generating Record s 77 | * \tparam TRecord Used Record type. 78 | * \attention The corresponding Core instance must outlive all related Logger instances. 79 | * \details This class is in fact a template for upcoming Record s. It stores default settings (e.g. severity level and channel) for newly generated Record s. Using its operator () users can start building Record s 80 | * in an effective and elegant manner. Logger instances are rather lightweight, thus could be instantiated on class level, but it is also fine using the same instance in the whole program. 81 | */ 82 | template 83 | class BaseLogger 84 | { 85 | public: 86 | using Record_t = std::remove_cvref_t; 87 | using RecordSetters_t = RecordSetters; 88 | using SeverityLevel_t = RecordSeverity_t; 89 | using Channel_t = RecordChannel_t; 90 | using RecordBuilder_t = RecordBuilder; 91 | using LogCallback_t = std::function; 92 | 93 | /** 94 | * \brief Constructor 95 | * \details Creates a Logger instance and links it to the specified core instance 96 | * \param logCallback Callback which will receive created Records. 97 | * \param defaultSeverityLvl Default severity level for generated Records. 98 | * \param defaultChannel Default channel for generated Records. 99 | */ 100 | explicit BaseLogger( 101 | LogCallback_t logCallback, 102 | SeverityLevel_t defaultSeverityLvl = {}, 103 | Channel_t defaultChannel = {} 104 | ) noexcept : 105 | m_LogCallback{ std::move(logCallback) }, 106 | m_DefaultSeverityLvl{ std::move(defaultSeverityLvl) }, 107 | m_DefaultChannel{ std::move(defaultChannel) } 108 | { 109 | } 110 | 111 | /** 112 | * \brief Destructor 113 | */ 114 | ~BaseLogger() noexcept = default; 115 | 116 | /** 117 | * \brief Copy constructor 118 | */ 119 | BaseLogger(const BaseLogger&) noexcept = default; 120 | /** 121 | * \brief Copy-assign operator 122 | */ 123 | BaseLogger& operator =(const BaseLogger&) noexcept = default; 124 | 125 | /** 126 | * \brief Move constructor 127 | */ 128 | BaseLogger(BaseLogger&&) noexcept = default; 129 | /** 130 | * \brief Move-assign operator 131 | */ 132 | BaseLogger& operator =(BaseLogger&&) noexcept = default; 133 | 134 | /** 135 | * \brief Creates a new instance of RecordBuilder 136 | * \details This is the entry point for producing new Record s. The returned RecordBuilder object will be pre-initialized with each values set to Logger's default, 137 | * which might be overriden during the Record building process. 138 | * \return Newly created RecordBuilder instance. 139 | */ 140 | [[nodiscard]] 141 | #ifdef __cpp_lib_source_location 142 | RecordBuilder_t operator ()(const std::source_location& srcLoc = std::source_location::current()) 143 | #else 144 | RecordBuilder_t operator ()() 145 | #endif 146 | { 147 | assert(m_LogCallback != nullptr && "Log callback must be set."); 148 | Record_t prefabRec; 149 | RecordSetters_t::setTimePoint(prefabRec, std::chrono::system_clock::now()); 150 | RecordSetters_t::setSeverity(prefabRec, m_DefaultSeverityLvl); 151 | RecordSetters_t::setChannel(prefabRec, m_DefaultChannel); 152 | RecordBuilder_t builder{ std::move(prefabRec), m_LogCallback }; 153 | 154 | #ifdef __cpp_lib_source_location 155 | builder.record().sourceLocation = srcLoc; 156 | #endif 157 | 158 | return builder; 159 | } 160 | 161 | /** 162 | * \brief Setter for the default severity level 163 | * \param sevLvl New default value 164 | */ 165 | template USeverityLevel> 166 | void setDefaultSeverity(USeverityLevel&& sevLvl) noexcept 167 | { 168 | m_DefaultSeverityLvl = std::forward(sevLvl); 169 | } 170 | 171 | /** 172 | * \brief Getter for the default severity level 173 | * \return Returns a const reference to the default severity level. 174 | */ 175 | [[nodiscard]] 176 | const SeverityLevel_t& defaultSeverity() const noexcept 177 | { 178 | return m_DefaultSeverityLvl; 179 | } 180 | 181 | /** 182 | * \brief Setter for the default channel 183 | * \param channel New default value 184 | */ 185 | template UChannel> 186 | void setDefaultChannel(UChannel&& channel) noexcept 187 | { 188 | m_DefaultChannel = std::forward(channel); 189 | } 190 | 191 | /** 192 | * \brief Getter for the default channel 193 | * \return Returns a const reference to the default channel. 194 | */ 195 | [[nodiscard]] 196 | const Channel_t& defaultChannel() const noexcept 197 | { 198 | return m_DefaultChannel; 199 | } 200 | 201 | private: 202 | LogCallback_t m_LogCallback; 203 | SeverityLevel_t m_DefaultSeverityLvl; 204 | Channel_t m_DefaultChannel; 205 | }; 206 | 207 | /** 208 | * \brief Creates a Logger object and setup its callback to the given Core instance 209 | * \relates BaseLogger 210 | * \tparam TLogger Concrete Logger type 211 | * \tparam TArgs Constructor argument types (will be deducted automatically) 212 | * \param core The core instance the Logger object should contribute to. 213 | * \param args The constructor arguments for the newly generated Logger object. Will be forwarded as is. 214 | * \return Logger object 215 | * \details This function creates a new Logger object and returns it to the caller. This Logger will receive a callback to the given Core instance, but Core does not 216 | * take over ownership of the created Logger object. If users does not need the Logger object any longer, they may simply let them go out of scope. 217 | */ 218 | template 219 | TLogger makeLogger(Core>& core, TArgs&&... args) 220 | { 221 | return TLogger{ 222 | [&core](LoggerRecord_t&& rec) 223 | { 224 | core.log(std::move(rec)); 225 | }, 226 | std::forward(args)... 227 | }; 228 | } 229 | 230 | /** @}*/ 231 | } 232 | 233 | #endif 234 | -------------------------------------------------------------------------------- /include/Simple-Log/OStreamSink.hpp: -------------------------------------------------------------------------------- 1 | // Copyright Dominic Koepke 2021 - 2021. 2 | // Distributed under the Boost Software License, Version 1.0. 3 | // (See accompanying file LICENSE_1_0.txt or copy at 4 | // https://www.boost.org/LICENSE_1_0.txt) 5 | 6 | #ifndef SL_LOG_OSTREAM_SINK_HPP 7 | #define SL_LOG_OSTREAM_SINK_HPP 8 | 9 | #pragma once 10 | 11 | #include "BasicSink.hpp" 12 | #include "FlushPolicies.hpp" 13 | #include "Record.hpp" 14 | 15 | #include 16 | #include 17 | #include 18 | #include 19 | 20 | namespace sl::log 21 | { 22 | /** \addtogroup Sinks 23 | * @{ 24 | */ 25 | 26 | /** 27 | * \brief An std::ostream orientated Sink class which extends BasicSink 28 | * \tparam TRecord Used Record type. 29 | * \details This Sink class uses a std::ostream reference for printing each recorded message and offers options to manipulate its behaviour: e.g. filtering and formatting messages. 30 | * Users which would like to print messages onto the console, there is already an existing class ConsoleSink. For file related logging FileSink might be more suitable. 31 | */ 32 | template 33 | class OStreamSink : 34 | public BasicSink 35 | { 36 | using Super = BasicSink; 37 | 38 | public: 39 | using typename Super::Record_t; 40 | using typename Super::Formatter_t; 41 | using typename Super::Filter_t; 42 | using FlushPolicy_t = std::unique_ptr>; 43 | 44 | protected: 45 | [[nodiscard]] 46 | static constexpr FlushPolicy_t defaultFlushPolicy() noexcept 47 | { 48 | return std::make_unique>(); 49 | } 50 | 51 | public: 52 | /** 53 | * \brief Constructor 54 | * \param stream The stream object, which will receive finally formatted messages 55 | */ 56 | explicit OStreamSink(std::ostream& stream) : 57 | Super{}, 58 | m_Stream{ stream } 59 | { 60 | } 61 | 62 | /** 63 | * \brief Default destructor 64 | * \details Destructor does not perform any actions on the internal stream objects, due to it's potential dangling state. Derived classes must handle closing and flushing themselves. 65 | */ 66 | ~OStreamSink() noexcept = default; 67 | 68 | /** 69 | * \brief Deleted copy constructor 70 | */ 71 | OStreamSink(const OStreamSink&) = delete; 72 | /** 73 | * \brief Deleted copy assign operator 74 | */ 75 | OStreamSink& operator =(const OStreamSink&) = delete; 76 | 77 | /** 78 | * \brief Deleted move constructor 79 | */ 80 | OStreamSink(OStreamSink&&) = delete; 81 | /** 82 | * \brief Deleted move assign operator 83 | */ 84 | OStreamSink& operator =(OStreamSink&&) = delete; 85 | 86 | /** 87 | * \brief Sets the active Flush-Policy 88 | * \tparam TPolicy Type of the passed Flush-Policy (automatically deduced) 89 | * \param policy The new Flush-Policy object 90 | */ 91 | template TPolicy> 92 | void setFlushPolicy(TPolicy&& policy) 93 | { 94 | std::scoped_lock lock{ m_FlushPolicyMx }; 95 | m_FlushPolicy = std::make_unique>(std::forward(policy)); 96 | } 97 | 98 | /** 99 | * \brief Replaces the current Flush-Policy with the default one 100 | * \details The default Flush-Policy flushes after each handled Record. 101 | */ 102 | void removeFlushPolicy() 103 | { 104 | std::scoped_lock lock{ m_FlushPolicyMx }; 105 | m_FlushPolicy = defaultFlushPolicy(); 106 | } 107 | 108 | /** 109 | * \brief Flushes all pending output of the internal stream 110 | * \remark Internally locks the associated stream mutex. 111 | */ 112 | void flush() 113 | { 114 | std::scoped_lock lock{ m_StreamMx }; 115 | flushImpl(); 116 | } 117 | 118 | protected: 119 | /** 120 | * \brief Writes directly to the internal stream 121 | * \tparam TData Type of data (automatically deduced) 122 | * \param data Data which will be written to the stream. 123 | * 124 | * \details This functions writes directly to the stream object. No filter or formatter will be involved and stream will be flush afterwards. 125 | * This might be useful for writing custom header or footer data to the stream. 126 | * \remark If not already locked, this function will lock the stream associated mutex. 127 | */ 128 | template 129 | void writeToStream(TData&& data) 130 | { 131 | std::scoped_lock lock{ m_StreamMx }; 132 | m_Stream << std::forward(data); 133 | flushImpl(); 134 | } 135 | 136 | /** 137 | * \brief Virtual method which will be called before the actual message is written to the stream 138 | * \param record The current handled Record object 139 | * \param message The final message 140 | * \remark The stream associated mutex is locked before this function gets invoked. 141 | * \version since alpha-0.6 142 | */ 143 | virtual void beforeMessageWrite(const Record_t& record, std::string_view message) 144 | { 145 | } 146 | 147 | /** 148 | * \brief Virtual method which will be called after the actual message is written to the stream 149 | * \param record The current handled Record object 150 | * \param message The final message 151 | * \remark The stream associated mutex is locked before this function gets invoked. 152 | * \version since alpha-0.6 153 | */ 154 | virtual void afterMessageWrite(const Record_t& record, std::string_view message) 155 | { 156 | } 157 | 158 | private: 159 | std::recursive_mutex m_StreamMx; 160 | std::ostream& m_Stream; 161 | 162 | std::mutex m_FlushPolicyMx; 163 | FlushPolicy_t m_FlushPolicy{ defaultFlushPolicy() }; 164 | 165 | void handleFlushPolicy(const Record_t& record, std::size_t messageByteSize) 166 | { 167 | if (std::scoped_lock lock{ m_FlushPolicyMx }; !std::invoke(*m_FlushPolicy, record, messageByteSize)) 168 | return; 169 | 170 | flushImpl(); 171 | } 172 | 173 | void flushImpl() 174 | { 175 | m_Stream << std::flush; 176 | m_FlushPolicy->flushed(); 177 | } 178 | 179 | void writeMessage(const Record_t& record, std::string_view message) final override 180 | { 181 | const auto msgSize = std::size(message) * sizeof(std::string_view::value_type); 182 | 183 | std::scoped_lock lock{ m_StreamMx }; 184 | beforeMessageWrite(record, message); 185 | m_Stream << message << "\n"; 186 | afterMessageWrite(record, message); 187 | handleFlushPolicy(record, msgSize); 188 | } 189 | }; 190 | 191 | /** @}*/ 192 | } 193 | 194 | #endif 195 | -------------------------------------------------------------------------------- /include/Simple-Log/Predicates.hpp: -------------------------------------------------------------------------------- 1 | // Copyright Dominic Koepke 2021 - 2021. 2 | // Distributed under the Boost Software License, Version 1.0. 3 | // (See accompanying file LICENSE_1_0.txt or copy at 4 | // https://www.boost.org/LICENSE_1_0.txt) 5 | 6 | #ifndef SL_LOG_PREDICATES_HPP 7 | #define SL_LOG_PREDICATES_HPP 8 | 9 | #pragma once 10 | 11 | #include 12 | #include 13 | 14 | namespace sl::log 15 | { 16 | /** \addtogroup Predicates Predicates 17 | * @{ 18 | */ 19 | 20 | /** 21 | * \brief Compares equality with constant at invocation 22 | */ 23 | template 24 | class Equals 25 | { 26 | public: 27 | constexpr explicit Equals(T to) : 28 | m_To{ std::move(to) } 29 | { 30 | } 31 | 32 | template U> 33 | constexpr bool operator ()(const U& other) const 34 | { 35 | return other == m_To; 36 | } 37 | 38 | private: 39 | T m_To; 40 | }; 41 | 42 | /** 43 | * \brief Compares non-equality with constant at invocation 44 | */ 45 | template 46 | class NotEquals 47 | { 48 | public: 49 | constexpr explicit NotEquals(T to) : 50 | m_To{ std::move(to) } 51 | { 52 | } 53 | 54 | template U> 55 | constexpr bool operator ()(const U& other) const 56 | { 57 | return other != m_To; 58 | } 59 | 60 | private: 61 | T m_To; 62 | }; 63 | 64 | /** 65 | * \brief Compares less-ordering with constant at invocation 66 | */ 67 | template 68 | class Less 69 | { 70 | public: 71 | constexpr explicit Less(T to) : 72 | m_To{ std::move(to) } 73 | { 74 | } 75 | 76 | template U> 77 | constexpr bool operator ()(const U& other) const 78 | { 79 | return other < m_To; 80 | } 81 | 82 | private: 83 | T m_To; 84 | }; 85 | 86 | /** 87 | * \brief Compares greater-ordering with constant at invocation 88 | */ 89 | template 90 | class Greater 91 | { 92 | public: 93 | constexpr explicit Greater(T to) : 94 | m_To{ std::move(to) } 95 | { 96 | } 97 | 98 | template U> 99 | constexpr bool operator ()(const U& other) const 100 | { 101 | return other > m_To; 102 | } 103 | 104 | private: 105 | T m_To; 106 | }; 107 | 108 | /** 109 | * \brief Compares less-equality-ordering with constant at invocation 110 | */ 111 | template 112 | class LessEquals 113 | { 114 | public: 115 | constexpr explicit LessEquals(T to) : 116 | m_To{ std::move(to) } 117 | { 118 | } 119 | 120 | template U> 121 | constexpr bool operator ()(const U& other) const 122 | { 123 | return other <= m_To; 124 | } 125 | 126 | private: 127 | T m_To; 128 | }; 129 | 130 | /** 131 | * \brief Compares greater-equality-ordering with constant at invocation 132 | */ 133 | template 134 | class GreaterEquals 135 | { 136 | public: 137 | constexpr explicit GreaterEquals(T to) : 138 | m_To{ std::move(to) } 139 | { 140 | } 141 | 142 | template U> 143 | constexpr bool operator ()(const U& other) const 144 | { 145 | return other >= m_To; 146 | } 147 | 148 | private: 149 | T m_To; 150 | }; 151 | 152 | /** 153 | * \brief Compares less-ordering with high and greater-ordering with low constant at invocation 154 | */ 155 | template 156 | class Between 157 | { 158 | public: 159 | constexpr explicit Between(T one, T two) : 160 | m_Low{ std::min(one, two) }, 161 | m_High{ std::max(two, one) } 162 | { 163 | } 164 | 165 | template U> 166 | constexpr bool operator ()(const U& other) const 167 | { 168 | return m_Low < other && other < m_High; 169 | } 170 | 171 | private: 172 | T m_Low; 173 | T m_High; 174 | }; 175 | 176 | /** 177 | * \brief Compares less-equality-ordering with high and greater-equality-ordering with low constant at invocation 178 | */ 179 | template 180 | class BetweenEquals 181 | { 182 | public: 183 | constexpr explicit BetweenEquals(T one, T two) : 184 | m_Low{ std::min(one, two) }, 185 | m_High{ std::max(two, one) } 186 | { 187 | } 188 | 189 | template U> 190 | constexpr bool operator ()(const U& other) const 191 | { 192 | return m_Low <= other && other <= m_High; 193 | } 194 | 195 | private: 196 | T m_Low; 197 | T m_High; 198 | }; 199 | 200 | /** @}*/ 201 | } 202 | 203 | #endif 204 | -------------------------------------------------------------------------------- /include/Simple-Log/PresetTypes.hpp: -------------------------------------------------------------------------------- 1 | // Copyright Dominic Koepke 2021 - 2021. 2 | // Distributed under the Boost Software License, Version 1.0. 3 | // (See accompanying file LICENSE_1_0.txt or copy at 4 | // https://www.boost.org/LICENSE_1_0.txt) 5 | 6 | #ifndef SL_LOG_PRESET_TYPES_HPP 7 | #define SL_LOG_PRESET_TYPES_HPP 8 | 9 | #pragma once 10 | 11 | #include "Simple-Log.hpp" 12 | 13 | namespace sl::log::preset 14 | { 15 | /** \addtogroup PreparedTypes Prepared Types 16 | * @{ 17 | * \details Each type serves as a default solution for simple logging, but users are free to dig deeper and define their custom types and rules. In this namespace are mainly type aliases 18 | * which are a good starting point for getting used to the library. 19 | */ 20 | 21 | /** 22 | * \brief A simple severity level enum type 23 | * \details Each value reflects a distinguishable severity level on which formatter and filter of Sink objects can react. 24 | */ 25 | enum class SevLvl 26 | { 27 | debug, 28 | info, 29 | hint, 30 | warning, 31 | error, 32 | fatal 33 | }; 34 | 35 | /** 36 | * \brief Operator << overload for SeverityLevel type 37 | * \param out the stream object 38 | * \param lvl the severity level 39 | * \return Returns the parameter out as reference 40 | */ 41 | inline std::ostream& operator <<(std::ostream& out, SevLvl lvl) 42 | { 43 | constexpr const char* str[] = { "debug", "info", "hint", "warning", "error", "fatal" }; 44 | out << str[static_cast(lvl)]; 45 | return out; 46 | } 47 | 48 | /** 49 | * \brief Prepared Record type 50 | * \details Record_t is an type alias for the BaseRecord and uses SeverityLevel as severity level type and std::string as channel identifier. 51 | */ 52 | using Record_t = BaseRecord; 53 | /** 54 | * \brief Type alias for log::Core which uses preset::Record_t as Record type 55 | */ 56 | using Core_t = Core; 57 | /** 58 | * \brief Type alias for log::Logger which uses preset::Record_t as Record type 59 | */ 60 | using Logger_t = BaseLogger; 61 | /** 62 | * \brief Type alias for log::ISink which uses preset::Record_t as Record type 63 | */ 64 | using ISink_t = ISink; 65 | /** 66 | * \brief Type alias for log::BasicSink which uses preset::Record_t as Record type 67 | */ 68 | using BasicSink_t = BasicSink; 69 | /** 70 | * \brief Type alias for log::OStreamSink which uses preset::Record_t as Record type 71 | */ 72 | using OStreamSink_t = OStreamSink; 73 | /** 74 | * \brief Type alias for log::FileSink which uses preset::Record_t as Record type 75 | */ 76 | using FileSink_t = FileSink; 77 | /** 78 | * \brief Type alias for log::ConsoleSink which uses preset::Record_t as Record type 79 | */ 80 | using ConsoleSink_t = ConsoleSink; 81 | 82 | /** @}*/ 83 | } 84 | 85 | #endif 86 | -------------------------------------------------------------------------------- /include/Simple-Log/ReadyToGo.hpp: -------------------------------------------------------------------------------- 1 | // Copyright Dominic Koepke 2021 - 2021. 2 | // Distributed under the Boost Software License, Version 1.0. 3 | // (See accompanying file LICENSE_1_0.txt or copy at 4 | // https://www.boost.org/LICENSE_1_0.txt) 5 | 6 | #ifndef SL_LOG_READY_TO_GO_HPP 7 | #define SL_LOG_READY_TO_GO_HPP 8 | 9 | #pragma once 10 | 11 | #include "PresetTypes.hpp" 12 | 13 | namespace sl::log::ready_to_go 14 | { 15 | using namespace log; 16 | using namespace preset; 17 | 18 | inline Core_t gCore; 19 | inline auto& gConsoleSink{ gCore.makeSink() }; 20 | inline auto gLog{ makeLogger(gCore, SevLvl::info) }; 21 | } 22 | 23 | #endif 24 | -------------------------------------------------------------------------------- /include/Simple-Log/Record.hpp: -------------------------------------------------------------------------------- 1 | // Copyright Dominic Koepke 2021 - 2021. 2 | // Distributed under the Boost Software License, Version 1.0. 3 | // (See accompanying file LICENSE_1_0.txt or copy at 4 | // https://www.boost.org/LICENSE_1_0.txt) 5 | 6 | #ifndef SL_LOG_RECORD_HPP 7 | #define SL_LOG_RECORD_HPP 8 | 9 | #pragma once 10 | 11 | #include 12 | #include 13 | #include 14 | 15 | #ifdef __cpp_lib_source_location 16 | #include 17 | #endif 18 | 19 | namespace sl::log 20 | { 21 | /** \addtogroup Record 22 | * @{ 23 | */ 24 | 25 | /** \addtogroup RecordConcepts Record Concepts 26 | * @{ 27 | * \details Users usually do not have to dig into the details of this part of the library, unless they try to inject an already existing but with incompatible interface Record type 28 | * into the framework. Therefore the library provides several helper structs, which built a layer of abstraction onto the regular Record type interface, which users may specialize for their given types. 29 | */ 30 | 31 | /** 32 | * \brief Provides a layer of abstraction to Record member types 33 | * \tparam TRecord The used Record type 34 | * \details There is no need for the users using this type of indirection. The library makes use of this abstraction, thus 35 | * users may specialize it to use Record types, which does not fit in the original concept and trait syntax provided by this library. 36 | * \version since alpha-v0.7 37 | */ 38 | template 39 | struct RecordTypedefs 40 | { 41 | using Message_t = typename TRecord::Message_t; 42 | using Severity_t = typename TRecord::SeverityLevel_t; 43 | using Channel_t = typename TRecord::Channel_t; 44 | using TimePoint_t = typename TRecord::TimePoint_t; 45 | }; 46 | 47 | /** 48 | * \typedef RecordMessage_t 49 | * \brief Typedef for easier access to \ref Record "Record's" message type. 50 | * \tparam TRecord The used Record type. 51 | * \version since alpha-v0.7 52 | */ 53 | template 54 | /** \cond Requires */ 55 | requires requires { typename RecordTypedefs::Message_t; } 56 | /** \endcond */ 57 | using RecordMessage_t = typename RecordTypedefs::Message_t; 58 | 59 | /** 60 | * \typedef RecordSeverity_t 61 | * \brief Typedef for easier access to \ref Record "Record's" severity type. 62 | * \tparam TRecord The used Record type. 63 | * \version since alpha-v0.7 64 | */ 65 | template 66 | /** \cond Requires */ 67 | requires requires { typename RecordTypedefs::Severity_t; } 68 | /** \endcond */ 69 | using RecordSeverity_t = typename RecordTypedefs::Severity_t; 70 | 71 | /** 72 | * \typedef RecordChannel_t 73 | * \brief Typedef for easier access to \ref Record "Record's" channel type. 74 | * \tparam TRecord The used Record type. 75 | * \version since alpha-v0.7 76 | */ 77 | template 78 | /** \cond Requires */ 79 | requires requires { typename RecordTypedefs::Channel_t; } 80 | /** \endcond */ 81 | using RecordChannel_t = typename RecordTypedefs::Channel_t; 82 | 83 | /** 84 | * \typedef RecordTimePoint_t 85 | * \brief Typedef for easier access to \ref Record "Record's" time-point type. 86 | * \tparam TRecord The used Record type. 87 | * \version since alpha-v0.7 88 | */ 89 | template 90 | /** \cond Requires */ 91 | requires requires { typename RecordTypedefs::TimePoint_t; } 92 | /** \endcond */ 93 | using RecordTimePoint_t = typename RecordTypedefs::TimePoint_t; 94 | 95 | /** 96 | * \brief Concept which checks for the necessary member typedefs of a Record type. 97 | * \version since alpha-v0.7 98 | */ 99 | template 100 | concept RecordMemberTypedefs = 101 | requires 102 | { 103 | typename RecordMessage_t; 104 | typename RecordSeverity_t; 105 | typename RecordChannel_t; 106 | typename RecordTimePoint_t; 107 | }; 108 | 109 | /** 110 | * \brief Provides a layer of abstraction to Record member setter 111 | * \tparam TRecord The used Record type 112 | * \details There is no need for the users using this type of indirection. The library makes use of this abstraction, thus 113 | * users may specialize it to use Record types, which does not fit in the original concept and trait syntax provided by this library. 114 | * \version since alpha-v0.7 115 | */ 116 | template 117 | struct RecordGetters 118 | { 119 | constexpr static auto message{ [](auto& record) { return record.message(); } }; 120 | constexpr static auto severity{ [](auto& record) { return record.severity(); } }; 121 | constexpr static auto channel{ [](auto& record) { return record.channel(); } }; 122 | constexpr static auto timePoint{ [](auto& record) { return record.timePoint(); } }; 123 | }; 124 | 125 | /** 126 | * \brief Concept which checks for the necessary member projections of a Record type. 127 | * \version since alpha-v0.7 128 | */ 129 | template 130 | concept RecordMemberGetters = 131 | RecordMemberTypedefs && 132 | requires(const TRecord& rec) 133 | { 134 | { RecordGetters::message(rec) } -> std::convertible_to>; 135 | { RecordGetters::severity(rec) } -> std::convertible_to>; 136 | { RecordGetters::channel(rec) } -> std::convertible_to>; 137 | { RecordGetters::timePoint(rec) } -> std::convertible_to>; 138 | }; 139 | 140 | /** 141 | * \brief Provides a layer of abstraction to Record member setter 142 | * \tparam TRecord The used Record type 143 | * \details There is no need for the users using this type of indirection. The library makes use of this abstraction, thus 144 | * users may specialize it to use Record types, which does not fit in the original concept and trait syntax provided by this library. 145 | * \version since alpha-v0.7 146 | */ 147 | template 148 | struct RecordSetters 149 | { 150 | using Record_t = std::remove_cvref_t; 151 | 152 | constexpr static auto setMessage 153 | { 154 | [](Record_t& record, TMessage&& msg) 155 | { 156 | return record.setMessage(std::forward(msg)); 157 | } 158 | }; 159 | 160 | constexpr static auto setSeverity 161 | { 162 | [](Record_t& record, TSeverity&& sev) 163 | { 164 | return record.setSeverity(std::forward(sev)); 165 | } 166 | }; 167 | 168 | constexpr static auto setChannel 169 | { 170 | [](Record_t& record, TChannel&& chan) 171 | { 172 | return record.setChannel(std::forward(chan)); 173 | } 174 | }; 175 | 176 | constexpr static auto setTimePoint 177 | { 178 | [](Record_t& record, TTimePoint&& timePoint) 179 | { 180 | return record.setTimePoint(std::forward(timePoint)); 181 | } 182 | }; 183 | }; 184 | 185 | /** 186 | * \brief Concept which checks for the necessary member setters of a Record type. 187 | * \version since alpha-v0.7 188 | */ 189 | template 190 | concept RecordMemberSetters = 191 | RecordMemberTypedefs && 192 | requires(TRecord& rec) 193 | { 194 | { RecordSetters::setMessage(rec, std::declval>()) }; 195 | { RecordSetters::setTimePoint(rec, std::declval>()) }; 196 | { RecordSetters::setSeverity(rec, std::declval>()) }; 197 | { RecordSetters::setChannel(rec, std::declval>()) }; 198 | }; 199 | 200 | /** 201 | * \brief Concept which all the necessary concepts for Record types. 202 | */ 203 | template 204 | concept Record = 205 | std::movable && 206 | RecordMemberTypedefs && 207 | RecordMemberGetters && 208 | RecordMemberSetters; 209 | 210 | /** @}*/ 211 | 212 | /** 213 | * \brief A collection of logging related information 214 | * \tparam TSeverityLevel Severity level type 215 | * \tparam TChannel Channel descriptor type 216 | * \tparam TMessage String type for storing messages 217 | * \tparam TTimePoint Type for storing time related information 218 | * 219 | * \details This class serves as a simple collection of gathered information during a logging action. It is a basic implementation of the Record concept 220 | * and users are free to use this class as a starting point for creating expanded sub-classes or even create their own right from scratch. 221 | */ 222 | template 226 | class BaseRecord 227 | { 228 | public: 229 | using Message_t = std::remove_cvref_t; 230 | using SeverityLevel_t = std::remove_cvref_t; 231 | using Channel_t = std::remove_cvref_t; 232 | using TimePoint_t = std::remove_cvref_t; 233 | #ifdef __cpp_lib_source_location 234 | using SourceLocation_t = std::remove_cvref_t; 235 | #endif 236 | 237 | /** 238 | * \brief Const access to the message 239 | * \return Returns the stored message 240 | */ 241 | [[nodiscard]] 242 | const Message_t& message() const noexcept 243 | { 244 | return m_Message; 245 | } 246 | 247 | /** 248 | * \brief Sets the message string 249 | * \tparam UMessage Type which must be convertible to Message_t 250 | * \param msg Message content 251 | */ 252 | template UMessage> 253 | void setMessage(UMessage&& msg) 254 | { 255 | m_Message = std::forward(msg); 256 | } 257 | 258 | /** 259 | * \brief Const access to the time stamp 260 | * \return Returns a const ref 261 | */ 262 | [[nodiscard]] 263 | const TimePoint_t& timePoint() const noexcept 264 | { 265 | return m_TimePoint; 266 | } 267 | 268 | /** 269 | * \brief Sets the Record s time stamp 270 | * \tparam UTimePoint Type which must be convertible to TimePoint_t 271 | * \param timePoint A time point 272 | */ 273 | template UTimePoint> 274 | void setTimePoint(UTimePoint&& timePoint) 275 | { 276 | m_TimePoint = std::forward(timePoint); 277 | } 278 | 279 | /** 280 | * \brief Const access to the severity level 281 | * \return Returns a const ref 282 | */ 283 | [[nodiscard]] 284 | const SeverityLevel_t& severity() const noexcept 285 | { 286 | return m_Severity; 287 | } 288 | 289 | /** 290 | * \brief Sets the Record s severity level 291 | * \tparam USeverityLevel Type which must be convertible to SeverityLevel_t 292 | * \param severity A time point 293 | */ 294 | template USeverityLevel> 295 | void setSeverity(USeverityLevel&& severity) 296 | { 297 | m_Severity = std::forward(severity); 298 | } 299 | 300 | /** 301 | * \brief Const access to the channel 302 | * \return Returns a const ref 303 | */ 304 | [[nodiscard]] 305 | const Channel_t& channel() const noexcept 306 | { 307 | return m_Channel; 308 | } 309 | 310 | /** 311 | * \brief Sets the Record s severity level 312 | * \tparam UChannel Type which must be convertible to Channel_t 313 | * \param channel channel descriptor 314 | */ 315 | template UChannel> 316 | void setChannel(UChannel&& channel) 317 | { 318 | m_Channel = std::forward(channel); 319 | } 320 | 321 | #ifdef __cpp_lib_source_location 322 | /** 323 | * \brief Const access to the source location 324 | * \return Returns a const ref 325 | */ 326 | [[nodiscard]] 327 | const SourceLocation_t& sourceLocation() const noexcept 328 | { 329 | return m_SourceLocation; 330 | } 331 | 332 | 333 | template USourceLocation> 334 | void setChannel(USourceLocation&& sourceLocation) 335 | { 336 | m_SourceLocation = std::forward(sourceLocation); 337 | } 338 | #endif 339 | 340 | private: 341 | Message_t m_Message{}; 342 | TimePoint_t m_TimePoint{}; 343 | SeverityLevel_t m_Severity{}; 344 | Channel_t m_Channel{}; 345 | #ifdef __cpp_lib_source_location 346 | SourceLocation_t m_SourceLocation{}; 347 | #endif 348 | }; 349 | 350 | /** @}*/ 351 | } 352 | 353 | #endif 354 | -------------------------------------------------------------------------------- /include/Simple-Log/RecordBuilder.hpp: -------------------------------------------------------------------------------- 1 | #// Copyright Dominic Koepke 2021 - 2021. 2 | // Distributed under the Boost Software License, Version 1.0. 3 | // (See accompanying file LICENSE_1_0.txt or copy at 4 | // https://www.boost.org/LICENSE_1_0.txt) 5 | 6 | #ifndef SL_LOG_RECORD_BUILDER_HPP 7 | #define SL_LOG_RECORD_BUILDER_HPP 8 | 9 | #pragma once 10 | 11 | #include "Record.hpp" 12 | 13 | #include 14 | #include 15 | #include 16 | 17 | namespace sl::log 18 | { 19 | /** \addtogroup Record 20 | * @{ 21 | */ 22 | 23 | /** 24 | * \brief Manipulates the channel of the current RecordBuilder object 25 | * \tparam TSeverityLevel Severity level type which must later on be convertible to Record's severity level type. 26 | * \details This type is generally designed to be directly used in logging expressions. When handed-over to a RecordBuilder instance by operator <<, 27 | * it will then manipulate the severity level data of its constructing Record object. 28 | */ 29 | template 30 | class SetSev 31 | { 32 | public: 33 | using Sev_t = TSeverityLevel; 34 | 35 | /** 36 | * \brief Constructor accepting severity level data 37 | * \param data Severity level data. 38 | */ 39 | explicit constexpr SetSev( 40 | TSeverityLevel data 41 | ) 42 | noexcept(std::is_nothrow_move_constructible_v) : 43 | m_Data{ std::move(data) } 44 | { 45 | } 46 | 47 | /** 48 | * \brief Changes severity level of the passed Record object 49 | * \tparam TRecord Used Record type. 50 | * \param rec The Record which is about to change 51 | */ 52 | template 53 | void operator ()(TRecord& rec) const 54 | { 55 | rec.setSeverity(m_Data); 56 | } 57 | 58 | private: 59 | Sev_t m_Data; 60 | }; 61 | 62 | /** 63 | * \brief Manipulates the channel of the current RecordBuilder object 64 | * \tparam TChannel Channel type which must later on be convertible to Record's channel type. 65 | * \details This type is generally designed to be directly used in logging expressions. When handed-over to a RecordBuilder instance by operator <<, 66 | * it will then manipulate the Channel data of its constructing Record object. 67 | */ 68 | template 69 | class SetChan 70 | { 71 | public: 72 | using Chan_t = TChannel; 73 | 74 | /** 75 | * \brief Constructor accepting channel data 76 | * \param data Channel data. 77 | */ 78 | explicit constexpr SetChan( 79 | TChannel data 80 | ) noexcept( 81 | std::is_nothrow_move_constructible_v) : 82 | m_Data{ std::move(data) } 83 | { 84 | } 85 | 86 | /** 87 | * \brief Changes channel of the passed Record object 88 | * \tparam TRecord Used Record type. 89 | * \param rec The Record which is about to change 90 | */ 91 | template 92 | void operator ()(TRecord& rec) const 93 | { 94 | rec.setChannel(m_Data); 95 | } 96 | 97 | private: 98 | Chan_t m_Data; 99 | }; 100 | 101 | /** 102 | * \brief Helper class for building new Records 103 | * \tparam TRecord Used Record type. 104 | * \details This is class provides the simple and elegant interface for making logging expressions. Its objects are non-copyable but movable. 105 | * When a RecordBuilder object gets destroyed (mainly because going out of scope) it will automatically send its created Record to the designated Logger object. Users should not instantiate 106 | * objects themselves, but should instead use the Logger objects. 107 | */ 108 | template 109 | class RecordBuilder 110 | { 111 | public: 112 | using Record_t = std::remove_cvref_t; 113 | using SeverityLevel_t = RecordSeverity_t; 114 | using Channel_t = RecordChannel_t; 115 | 116 | private: 117 | using LogCallback_t = std::function; 118 | 119 | public: 120 | /** 121 | * \brief Constructor 122 | * \param prefabRec Prefabricated Record 123 | * \param cb Callback to the associated Logger object 124 | * \details This constructor requires a callback to the associated Logger objects log function. The callback should have the following signature: 125 | * \code 126 | * void(Record) 127 | * \endcode 128 | */ 129 | explicit RecordBuilder(Record_t prefabRec, LogCallback_t cb) noexcept : 130 | m_Record{ std::move(prefabRec) }, 131 | m_LogCallback{ std::move(cb) } 132 | { 133 | } 134 | 135 | /** 136 | * \brief Destructor 137 | * \details If RecordBuilder is valid (in fact, if a callback is installed) it moves its created Record object to the Logger object. 138 | */ 139 | ~RecordBuilder() noexcept 140 | { 141 | if (m_LogCallback) 142 | { 143 | try 144 | { 145 | m_Record.setMessage(std::move(m_Stream).str()); 146 | m_LogCallback(std::move(m_Record)); 147 | } 148 | catch (...) 149 | { 150 | } 151 | } 152 | } 153 | 154 | /** 155 | * \brief Move constructor 156 | * \details Transfers ownership of other's data to this object. If other has an installed callback, it will be cleared. 157 | * \param other Another RecordBuilder object 158 | */ 159 | RecordBuilder(RecordBuilder&& other) noexcept 160 | { 161 | *this = std::move(other); 162 | } 163 | 164 | /** 165 | * \brief Move-assign operator 166 | * \details Transfers ownership of other's data to this object. If other has an installed callback, it will be cleared. 167 | * \param other Another RecordBuilder object 168 | * \return Returns a reference to this. 169 | */ 170 | RecordBuilder& operator =(RecordBuilder&& other) noexcept 171 | { 172 | using std::swap; 173 | swap(m_Record, other.m_Record); 174 | swap(m_Stream, other.m_Stream); 175 | m_LogCallback = std::exchange(other.m_LogCallback, {}); 176 | return *this; 177 | } 178 | 179 | /** 180 | * \brief Deleted copy constructor 181 | */ 182 | RecordBuilder(const RecordBuilder&) = delete; 183 | /** 184 | * \brief Deleted copy-assign operator 185 | */ 186 | RecordBuilder& operator =(const RecordBuilder&) = delete; 187 | 188 | /** 189 | * \brief Accessor to the internal record object 190 | */ 191 | [[nodiscard]] 192 | Record_t& record() noexcept 193 | { 194 | return m_Record; 195 | } 196 | 197 | /** 198 | * \brief Const accessor to the internal record object 199 | */ 200 | [[nodiscard]] 201 | const Record_t& record() const noexcept 202 | { 203 | return m_Record; 204 | } 205 | 206 | /** 207 | * \brief Output operator 208 | * \details Attaches data to the underlying ostream object. 209 | * \tparam T Requires that std::ostringstream has an overload of operator << for T. 210 | * \param data Data about to be attached. 211 | * \return Returns a reference to this. 212 | */ 213 | template 214 | requires requires(T&& data) 215 | { 216 | { std::declval() << std::forward(data) }; 217 | } 218 | RecordBuilder& operator <<(T&& data) 219 | { 220 | m_Stream << std::forward(data); 221 | return *this; 222 | } 223 | 224 | /** 225 | * \brief Special output operator overload for actions on Record 226 | * \param action Action which will be applied to the internal Record object. 227 | * \return Returns a reference to this. 228 | */ 229 | template TAction> 230 | RecordBuilder& operator <<(TAction action) 231 | { 232 | std::invoke(action, m_Record); 233 | return *this; 234 | } 235 | 236 | private: 237 | Record_t m_Record; 238 | std::ostringstream m_Stream; 239 | LogCallback_t m_LogCallback; 240 | }; 241 | 242 | /** @}*/ 243 | } 244 | 245 | #endif 246 | -------------------------------------------------------------------------------- /include/Simple-Log/RecordQueue.hpp: -------------------------------------------------------------------------------- 1 | // Copyright Dominic Koepke 2021 - 2021. 2 | // Distributed under the Boost Software License, Version 1.0. 3 | // (See accompanying file LICENSE_1_0.txt or copy at 4 | // https://www.boost.org/LICENSE_1_0.txt) 5 | 6 | #ifndef SL_LOG_RECORD_QUEUE_HPP 7 | #define SL_LOG_RECORD_QUEUE_HPP 8 | 9 | #pragma once 10 | 11 | #include "Record.hpp" 12 | 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | 20 | namespace sl::log 21 | { 22 | /** \addtogroup Core 23 | * @{ 24 | */ 25 | 26 | /** 27 | * \brief Storage for Record s 28 | * \tparam TRecord Used Record type. 29 | * \details This class is a simple representation of a blocking queue. Its take() function blocks until an element is present in the internal queue or 30 | * the duration exceeded. Each function is thread-safe by design. 31 | */ 32 | template 33 | class RecordQueue 34 | { 35 | public: 36 | using Record_t = std::remove_cvref_t; 37 | 38 | /** 39 | * \brief Pushes Record s to the internal queue 40 | * \details Thread-safe 41 | * \param record The queued Record object. 42 | */ 43 | void push(Record_t record) 44 | { 45 | { 46 | std::scoped_lock lock{ m_RecordMx }; 47 | m_QueuedRecords.emplace(std::move(record)); 48 | } 49 | m_PushVar.notify_one(); 50 | } 51 | 52 | /** 53 | * \brief Takes the first Record from the queue 54 | * \details Thread-safe 55 | * If the internal queue is not empty, this functions takes the first element and returns it. If the queue is empty, this function will block until a new Record gets pushed into the queue 56 | * or the duration exceeds. 57 | * \param waitingDuration The max waiting duration for an element. 58 | * \return Returns an element as optional. Might be nullopt. 59 | */ 60 | [[nodiscard]] 61 | std::optional take(std::optional waitingDuration = std::nullopt) 62 | { 63 | auto isQueueNotEmpty = [&records = m_QueuedRecords]() { return !std::empty(records); }; 64 | 65 | std::unique_lock lock{ m_RecordMx }; 66 | if (waitingDuration) 67 | { 68 | if (m_PushVar.wait_for(lock, *waitingDuration, isQueueNotEmpty)) 69 | { 70 | return takeNextAsOpt(); 71 | } 72 | return std::nullopt; 73 | } 74 | 75 | m_PushVar.wait(lock, isQueueNotEmpty); 76 | return takeNextAsOpt(); 77 | } 78 | 79 | /** 80 | * \brief Checks if the internal queue is empty 81 | * \details Thread-safe 82 | * \return Returns true if is empty. 83 | */ 84 | [[nodiscard]] 85 | bool empty() const noexcept 86 | { 87 | std::scoped_lock lock{ m_RecordMx }; 88 | return std::empty(m_QueuedRecords); 89 | } 90 | 91 | /** 92 | * \brief Checks size of the internal queue 93 | * \details Thread-safe 94 | * \return Returns size 95 | */ 96 | [[nodiscard]] 97 | std::size_t size() const noexcept 98 | { 99 | std::scoped_lock lock{ m_RecordMx }; 100 | return std::size(m_QueuedRecords); 101 | } 102 | 103 | private: 104 | [[nodiscard]] 105 | std::optional takeNextAsOpt() 106 | { 107 | auto record = std::move(m_QueuedRecords.front()); 108 | m_QueuedRecords.pop(); 109 | return std::optional{ std::in_place, std::move(record) }; 110 | } 111 | 112 | mutable std::mutex m_RecordMx; 113 | std::queue m_QueuedRecords; 114 | std::condition_variable m_PushVar; 115 | }; 116 | 117 | /** @}*/ 118 | } 119 | 120 | #endif 121 | -------------------------------------------------------------------------------- /include/Simple-Log/Simple-Log.hpp: -------------------------------------------------------------------------------- 1 | // Copyright Dominic Koepke 2021 - 2021. 2 | // Distributed under the Boost Software License, Version 1.0. 3 | // (See accompanying file LICENSE_1_0.txt or copy at 4 | // https://www.boost.org/LICENSE_1_0.txt) 5 | 6 | #ifndef SL_LOG_SIMPLE_LOG_HPP 7 | #define SL_LOG_SIMPLE_LOG_HPP 8 | 9 | #pragma once 10 | 11 | #include "Filters.hpp" 12 | #include "FlushPolicies.hpp" 13 | #include "Predicates.hpp" 14 | 15 | #include "StringPattern.hpp" 16 | 17 | #include "Record.hpp" 18 | #include "RecordBuilder.hpp" 19 | 20 | #include "Core.hpp" 21 | #include "RecordQueue.hpp" 22 | 23 | #include "Logger.hpp" 24 | 25 | #include "BasicSink.hpp" 26 | #include "ConsoleSink.hpp" 27 | #include "FileSink.hpp" 28 | #include "ISink.hpp" 29 | #include "OStreamSink.hpp" 30 | 31 | #endif 32 | -------------------------------------------------------------------------------- /include/Simple-Log/StringPattern.hpp: -------------------------------------------------------------------------------- 1 | // Copyright Dominic Koepke 2021 - 2021. 2 | // Distributed under the Boost Software License, Version 1.0. 3 | // (See accompanying file LICENSE_1_0.txt or copy at 4 | // https://www.boost.org/LICENSE_1_0.txt) 5 | 6 | #ifndef SL_LOG_STRING_PATTERN_HPP 7 | #define SL_LOG_STRING_PATTERN_HPP 8 | 9 | #pragma once 10 | 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | 19 | namespace sl::log::detail 20 | { 21 | struct IncNumberGenerator 22 | { 23 | unsigned minWidth = 0; 24 | unsigned current = 1; 25 | 26 | void operator()(std::ostream& stream) 27 | { 28 | stream << std::setfill('0') << std::setw(minWidth) << current++; 29 | } 30 | }; 31 | 32 | // ToDo: modernize with c++20 chrono features 33 | struct DateTimeGenerator 34 | { 35 | std::string token; 36 | 37 | void operator()(std::ostream& stream) const 38 | { 39 | const auto now = std::chrono::system_clock::now(); 40 | auto t_c = std::chrono::system_clock::to_time_t(now); 41 | stream << std::put_time(std::localtime(&t_c), token.c_str()); 42 | } 43 | }; 44 | 45 | struct StringGenerator 46 | { 47 | std::string_view str; 48 | 49 | void operator()(std::ostream& stream) const 50 | { 51 | stream << str; 52 | } 53 | }; 54 | 55 | using Generator = std::variant; 56 | 57 | [[nodiscard]] inline Generator makeGeneratorFromMatch(std::string_view token) 58 | { 59 | if (token.starts_with('%')) 60 | { 61 | // ReSharper disable once CppDefaultCaseNotHandledInSwitchStatement 62 | switch (token.back()) 63 | { 64 | case 'H': // hour (2 digits) 65 | case 'M': // minute (2 digits) 66 | case 'S': // second (2 digits) 67 | 68 | case 'Y': // year (4 digits) 69 | case 'm': // month (2 digits) 70 | case 'd': // day of month (2 digits) 71 | case 'j': // day of year (3 digits) 72 | return DateTimeGenerator{ { std::cbegin(token), std::cbegin(token) + 2 } }; 73 | 74 | case 'N': 75 | token.remove_prefix(1); 76 | unsigned width = 0; 77 | std::from_chars(token.data(), &token.back(), width); 78 | return IncNumberGenerator{ .minWidth = width }; 79 | } 80 | } 81 | 82 | // treat everything else as const substring, even if it contains a % 83 | return StringGenerator{ token }; 84 | } 85 | 86 | [[nodiscard]] inline std::vector makeTokenGeneratorsFromPatternString(std::string_view patternString) 87 | { 88 | std::vector generators; 89 | 90 | // ToDo: use ranges 91 | const std::regex regEx{ "%(Y|m|d|H|M|S|j|\\d*N)" }; 92 | std::for_each( 93 | std::cregex_token_iterator 94 | { 95 | patternString.data(), 96 | patternString.data() + std::size(patternString), 97 | regEx, 98 | { -1, 0 } 99 | }, 100 | std::cregex_token_iterator{}, 101 | [&generators](const auto& match) 102 | { 103 | if (match.length() != 0) 104 | { 105 | generators.emplace_back(makeGeneratorFromMatch({ match.first, match.second })); 106 | } 107 | } 108 | ); 109 | return generators; 110 | } 111 | } 112 | 113 | namespace sl::log 114 | { 115 | /** 116 | * \brief Helper class for generating patterned strings 117 | * \details Tokenize the pattern string and creates generators, which will then create the actual substrings on demand. 118 | */ 119 | class StringPattern 120 | { 121 | public: 122 | /** 123 | * \brief Default Constructor 124 | */ 125 | StringPattern() noexcept = default; 126 | 127 | /** 128 | * \brief Constructor 129 | * \param patternString Pattern 130 | */ 131 | explicit StringPattern(std::string patternString) : 132 | m_PatternString{ std::move(patternString) }, 133 | m_TokenGenerators{ detail::makeTokenGeneratorsFromPatternString(m_PatternString) } 134 | { 135 | } 136 | 137 | /** 138 | * \brief Creates a new string 139 | * \details The returned string will be created on demand and follows the pattern string rules. 140 | * \return Generated string. 141 | */ 142 | std::string next() 143 | { 144 | std::ostringstream ss; 145 | for (auto& token : m_TokenGenerators) 146 | { 147 | std::visit( 148 | [&ss](auto& generator) 149 | { 150 | generator(ss); 151 | }, 152 | token 153 | ); 154 | } 155 | return std::move(ss).str(); 156 | } 157 | 158 | /** 159 | * \brief Getter of the used pattern string 160 | * \return A std::string_view on the pattern string 161 | */ 162 | [[nodiscard]] 163 | std::string_view patternString() const noexcept 164 | { 165 | return m_PatternString; 166 | } 167 | 168 | /** 169 | * \brief Sets the pattern string 170 | * \param patternString The given pattern string 171 | */ 172 | void setPatternString(std::string patternString) 173 | { 174 | m_PatternString = std::move(patternString); 175 | m_TokenGenerators = detail::makeTokenGeneratorsFromPatternString(m_PatternString); 176 | } 177 | 178 | private: 179 | std::string m_PatternString; 180 | std::vector m_TokenGenerators; 181 | }; 182 | } 183 | 184 | #endif 185 | -------------------------------------------------------------------------------- /include/Simple-Log/TupleAlgorithms.hpp: -------------------------------------------------------------------------------- 1 | // Copyright Dominic Koepke 2021 - 2021. 2 | // Distributed under the Boost Software License, Version 1.0. 3 | // (See accompanying file LICENSE_1_0.txt or copy at 4 | // https://www.boost.org/LICENSE_1_0.txt) 5 | 6 | #ifndef SL_LOG_TUPLE_ALGORITHMS_HPP 7 | #define SL_LOG_TUPLE_ALGORITHMS_HPP 8 | 9 | #pragma once 10 | 11 | #include 12 | #include 13 | 14 | namespace sl::log::detail 15 | { 16 | class TupleAllOf 17 | { 18 | public: 19 | template 20 | constexpr bool operator ()(TTuple& tuple, TArgs&&... args) const 21 | { 22 | return invoke<0>(tuple, std::forward(args)...); 23 | } 24 | 25 | private: 26 | template 27 | constexpr bool invoke(TTuple& tuple, TArgs&&... args) const 28 | { 29 | if constexpr (index < std::tuple_size_v) 30 | { 31 | if (!std::invoke(std::get(tuple), std::forward(args)...)) 32 | { 33 | return false; 34 | } 35 | return invoke(tuple, std::forward(args)...); 36 | } 37 | return true; 38 | } 39 | }; 40 | 41 | class TupleAnyOf 42 | { 43 | public: 44 | template 45 | constexpr bool operator ()(TTuple& tuple, TArgs&&... args) const 46 | { 47 | return invoke<0>(tuple, std::forward(args)...); 48 | } 49 | 50 | private: 51 | template 52 | constexpr bool invoke(TTuple& tuple, TArgs&&... args) const 53 | { 54 | if constexpr (index < std::tuple_size_v) 55 | { 56 | if (std::invoke(std::get(tuple), std::forward(args)...)) 57 | { 58 | return true; 59 | } 60 | return invoke(tuple, std::forward(args)...); 61 | } 62 | return false; 63 | } 64 | }; 65 | 66 | class TupleNoneOf : 67 | private TupleAnyOf 68 | { 69 | using Super = TupleAnyOf; 70 | 71 | public: 72 | template 73 | constexpr bool operator ()(TTuple& tuple, TArgs&&... args) const 74 | { 75 | return !Super::operator()(tuple, std::forward(args)...); 76 | } 77 | }; 78 | 79 | class TupleForEach 80 | { 81 | public: 82 | template 83 | constexpr void operator ()(TTuple& tuple, TFunc func) const 84 | { 85 | invoke<0>(tuple, func); 86 | } 87 | 88 | private: 89 | template 90 | constexpr void invoke(TTuple& tuple, TFunc func) const 91 | { 92 | if constexpr (index < std::tuple_size_v) 93 | { 94 | std::invoke(func, std::get(tuple)); 95 | invoke(tuple, func); 96 | } 97 | } 98 | }; 99 | } 100 | 101 | #endif 102 | -------------------------------------------------------------------------------- /tests/BasicSinkTests.cpp: -------------------------------------------------------------------------------- 1 | // Copyright Dominic Koepke 2021 - 2021. 2 | // Distributed under the Boost Software License, Version 1.0. 3 | // (See accompanying file LICENSE_1_0.txt or copy at 4 | // https://www.boost.org/LICENSE_1_0.txt) 5 | 6 | #include 7 | 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | #include "Simple-Log/BasicSink.hpp" 14 | #include "Simple-Log/Record.hpp" 15 | 16 | using namespace sl::log; 17 | 18 | using Record_t = BaseRecord; 19 | 20 | namespace 21 | { 22 | class BasicSinkMoc : 23 | public BasicSink 24 | { 25 | public: 26 | BasicSinkMoc(std::ostream* out = nullptr) : 27 | m_Out{ out } 28 | { 29 | } 30 | 31 | private: 32 | void writeMessage(const Record_t& record, std::string_view message) override 33 | { 34 | if (m_Out) 35 | { 36 | (*m_Out) << message << "\n"; 37 | } 38 | } 39 | 40 | std::ostream* m_Out = nullptr; 41 | }; 42 | 43 | struct FormatterMoc 44 | { 45 | std::string operator ()(const Record_t& rec) const 46 | { 47 | assert(invoked); 48 | *invoked = true; 49 | return std::string(overridingStr); 50 | } 51 | 52 | std::string_view overridingStr; 53 | bool* invoked; 54 | }; 55 | 56 | struct FilterMoc 57 | { 58 | bool operator ()(const Record_t& rec) const 59 | { 60 | assert(invoked); 61 | *invoked = true; 62 | return invocationResult; 63 | } 64 | 65 | bool invocationResult; 66 | bool* invoked; 67 | }; 68 | } 69 | 70 | SCENARIO("BasicSinks should be in disabled state when construction succeeded", "[BasicSink][Sink]") 71 | { 72 | GIVEN("a newly constructed BasicSink instance") 73 | WHEN("construction succeeded") 74 | THEN("BasicSink should be disabled by default") 75 | { 76 | BasicSinkMoc sink; 77 | REQUIRE_FALSE(sink.isEnabled()); 78 | } 79 | } 80 | 81 | SCENARIO("BasicSinks::setEnabled should modify member.", "[BasicSink][Sink]") 82 | { 83 | GIVEN("a disabled BasicSink instance") 84 | WHEN("setting as enabled") 85 | THEN("isEnabled should yield a positive result") 86 | { 87 | BasicSinkMoc sink; 88 | sink.setEnabled(true); 89 | REQUIRE(sink.isEnabled()); 90 | } 91 | } 92 | 93 | SCENARIO("BasicSink's log function should be controled by enabled property", "[BasicSink][Sink]") 94 | { 95 | std::ostringstream out; 96 | BasicSinkMoc sink{ &out }; 97 | 98 | GIVEN("a disabled BasicSink instance") 99 | WHEN("calling log") 100 | THEN("nothing should have been written to the stream") 101 | { 102 | sink.log({}); 103 | REQUIRE(std::empty(out.str())); 104 | } 105 | 106 | GIVEN("a enabled BasicSink instance") 107 | WHEN("calling log") 108 | THEN("record should have been written to the stream") 109 | { 110 | sink.setEnabled(true); 111 | sink.log({}); 112 | REQUIRE_FALSE(std::empty(out.str())); 113 | } 114 | } 115 | 116 | SCENARIO("BasicSink's filter property should determine if records get processed or skipped", "[BasicSink][Sink]") 117 | { 118 | std::ostringstream out; 119 | BasicSinkMoc sink{ &out }; 120 | bool invoked = false; 121 | sink.setFilter(FilterMoc{ .invocationResult = true, .invoked = &invoked }); 122 | sink.setEnabled(); 123 | 124 | GIVEN("an enabled BasicSink instance") 125 | { 126 | WHEN("calling log") 127 | { 128 | THEN("filter property should get invoked ") 129 | { 130 | sink.log({}); 131 | REQUIRE(invoked); 132 | } 133 | 134 | AND_THEN("filter should determine if record gets handled") 135 | { 136 | auto shallHandle = GENERATE(true, false); 137 | sink.setFilter(FilterMoc{ .invocationResult = shallHandle, .invoked = &invoked }); 138 | sink.log({}); 139 | REQUIRE(std::empty(out.str()) != shallHandle); 140 | } 141 | } 142 | 143 | AND_WHEN("removeFilter is called") 144 | THEN("then previous filter should get replaced") 145 | { 146 | sink.removeFilter(); 147 | sink.log({}); 148 | REQUIRE_FALSE(invoked); 149 | } 150 | } 151 | } 152 | 153 | SCENARIO("BasicSink's formatter property should format processed records", "[BasicSink][Sink]") 154 | { 155 | std::ostringstream out; 156 | BasicSinkMoc sink{ &out }; 157 | bool invoked = false; 158 | sink.setFormatter(FormatterMoc{ .invoked = &invoked }); 159 | sink.setEnabled(); 160 | 161 | GIVEN("an enabled BasicSink instance") 162 | { 163 | WHEN("calling log") 164 | { 165 | THEN("formatter property should get invoked ") 166 | { 167 | sink.log({}); 168 | REQUIRE(invoked); 169 | } 170 | 171 | AND_THEN("formatter should format records") 172 | { 173 | const auto overridingStr = GENERATE(as{}, "", "Hello, World!"); 174 | sink.setFormatter(FormatterMoc{ .overridingStr = overridingStr, .invoked = &invoked }); 175 | sink.log({}); 176 | REQUIRE(out.str() == overridingStr + "\n"); 177 | } 178 | } 179 | 180 | AND_WHEN("removeFormatter is called") 181 | THEN("then previous formatter should get replaced") 182 | { 183 | sink.removeFormatter(); 184 | sink.log({}); 185 | REQUIRE_FALSE(invoked); 186 | } 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /tests/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.14 FATAL_ERROR) 2 | 3 | project(simple_log_test) 4 | 5 | include(FetchContent) 6 | 7 | FetchContent_Declare( 8 | catch2 9 | GIT_REPOSITORY https://github.com/catchorg/Catch2.git 10 | GIT_TAG origin/v2.x 11 | ) 12 | FetchContent_MakeAvailable(catch2) 13 | 14 | add_executable( 15 | ${PROJECT_NAME} 16 | "main.cpp" 17 | "CoreTests.cpp" 18 | "RecordTests.cpp" 19 | "TupleAlgorithmTests.cpp" 20 | "PredicateTests.cpp" 21 | "FilterTests.cpp" 22 | "RecordQueueTests.cpp" 23 | "StringPatternTest.cpp" 24 | "FlushPolicyTests.cpp" 25 | "OStreamSinkTests.cpp" 26 | "FileSinkTests.cpp" 27 | "ConsoleSinkTests.cpp" 28 | "LoggerTests.cpp" 29 | "RecordBuilderTests.cpp" 30 | "BasicSinkTests.cpp" 31 | ) 32 | 33 | target_link_libraries( 34 | ${PROJECT_NAME} 35 | PRIVATE 36 | simple_log 37 | Catch2::Catch2 38 | ) 39 | 40 | add_test(SimpleLogTestSuite ${PROJECT_NAME}) 41 | -------------------------------------------------------------------------------- /tests/ConsoleSinkTests.cpp: -------------------------------------------------------------------------------- 1 | // Copyright Dominic Koepke 2021 - 2021. 2 | // Distributed under the Boost Software License, Version 1.0. 3 | // (See accompanying file LICENSE_1_0.txt or copy at 4 | // https://www.boost.org/LICENSE_1_0.txt) 5 | 6 | #include 7 | 8 | #include "Simple-Log/ConsoleSink.hpp" 9 | #include "Simple-Log/PresetTypes.hpp" 10 | #include "Simple-Log/Record.hpp" 11 | 12 | using namespace sl::log; 13 | using namespace std::literals; 14 | 15 | using Record_t = preset::Record_t; 16 | using ConsoleSink_t = ConsoleSink; 17 | 18 | TEST_CASE("ConsoleTextStyleTable should yield the same styles as inserted during construction.", "[ConsoleSink][Sink]") 19 | { 20 | using Style = ConsoleTextStyle; 21 | using SevLvl = preset::SevLvl; 22 | auto styleTable = makeConsoleTextStyleTableFor( 23 | &Record_t::severity, 24 | { 25 | { SevLvl::info, { .bgColor = Style::Color::blue } }, 26 | { SevLvl::debug, { .bgColor = Style::Color::yellow } }, 27 | { SevLvl::error, { .bgColor = Style::Color::red } } 28 | } 29 | ); 30 | 31 | Record_t record; 32 | record.setSeverity(SevLvl::info); 33 | REQUIRE(styleTable(record).bgColor == Style::Color::blue); 34 | record.setSeverity(SevLvl::debug); 35 | REQUIRE(styleTable(record).bgColor == Style::Color::yellow); 36 | record.setSeverity(SevLvl::error); 37 | REQUIRE(styleTable(record).bgColor == Style::Color::red); 38 | record.setSeverity(SevLvl::warning); 39 | REQUIRE(styleTable(record).bgColor == Style::Color::standard); 40 | } 41 | 42 | TEST_CASE("ConsoleTextStyleTable should yield the same styles as inserted after construction.", "[ConsoleSink][Sink]") 43 | { 44 | using Style = ConsoleTextStyle; 45 | using SevLvl = preset::SevLvl; 46 | auto styleTable = makeConsoleTextStyleTableFor( 47 | &Record_t::severity, 48 | { 49 | { SevLvl::info, { .bgColor = Style::Color::blue } }, 50 | { SevLvl::debug, { .bgColor = Style::Color::yellow } }, 51 | { SevLvl::error, { .bgColor = Style::Color::red } } 52 | } 53 | ); 54 | 55 | styleTable.insert(SevLvl::warning, { .bgColor = Style::Color::blue }); 56 | styleTable.insert(SevLvl::debug, { .bgColor = Style::Color::standard }); 57 | 58 | Record_t record; 59 | record.setSeverity(SevLvl::warning); 60 | REQUIRE(styleTable(record).bgColor == Style::Color::blue); 61 | record.setSeverity(SevLvl::debug); 62 | REQUIRE(styleTable(record).bgColor == Style::Color::standard); 63 | } 64 | 65 | SCENARIO("ConsoleSink's TextStylePolicy property should be set- and removable.", "[ConsoleSink][Sink]") 66 | { 67 | ConsoleSink_t sink; 68 | 69 | GIVEN("an arbitrary ConsoleSink instance") 70 | { 71 | WHEN("setting TextStylePolicy") 72 | THEN("it should compile") 73 | { 74 | sink.setTextStylePolicy([](const Record_t&) { return ConsoleTextStyle{}; }); 75 | } 76 | 77 | WHEN("removing TextStylePolicy") 78 | THEN("it should compile") 79 | { 80 | sink.removeTextStylePolicy(); 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /tests/CoreTests.cpp: -------------------------------------------------------------------------------- 1 | // Copyright Dominic Koepke 2021 - 2021. 2 | // Distributed under the Boost Software License, Version 1.0. 3 | // (See accompanying file LICENSE_1_0.txt or copy at 4 | // https://www.boost.org/LICENSE_1_0.txt) 5 | 6 | #include 7 | 8 | #include 9 | 10 | #include "Simple-Log/OStreamSink.hpp" 11 | #include "Simple-Log/Core.hpp" 12 | #include "Simple-Log/Record.hpp" 13 | 14 | using namespace sl::log; 15 | 16 | using Record_t = BaseRecord; 17 | using Core_t = Core; 18 | using Sink_t = OStreamSink; 19 | 20 | SCENARIO("Core should create valid Sink instances.", "[Core]") 21 | { 22 | Core_t core; 23 | 24 | GIVEN("an arbitrary Core instance") 25 | { 26 | WHEN("makeSink is called") 27 | THEN("caller receives a reference to a valid enabled Sink object") 28 | { 29 | auto& sink = core.makeSink(std::cout); 30 | 31 | REQUIRE(sink.isEnabled()); 32 | } 33 | 34 | WHEN("makeDisabledSink is called") 35 | THEN("caller receives a wrapped reference to a valid disabled Sink object") 36 | { 37 | Sink_t* sinkPtr = nullptr; 38 | { 39 | auto wrappedSink = core.makeDisabledSink(std::cout); 40 | 41 | REQUIRE(wrappedSink.get() != nullptr); 42 | REQUIRE_FALSE(wrappedSink->isEnabled()); 43 | 44 | sinkPtr = wrappedSink.get(); 45 | } 46 | 47 | AND_WHEN("reference wrapper goes out of scope") 48 | THEN("Sink object will automaticalle become enabled") 49 | { 50 | REQUIRE(sinkPtr->isEnabled()); 51 | } 52 | } 53 | } 54 | } 55 | 56 | SCENARIO("Core should remove registered Sink objects.", "[Core]") 57 | { 58 | Core_t core; 59 | 60 | GIVEN("an arbitrary Core instance") 61 | { 62 | WHEN("removeSink with an registered Sink is called") 63 | THEN("Sink object gets destroyed and function yields true") 64 | { 65 | auto& sink = core.makeSink(std::cout); 66 | 67 | REQUIRE(core.removeSink(sink)); 68 | } 69 | 70 | WHEN("removeSink with an un-registered Sink is called") 71 | THEN("Sink object won't get destroyed and function yields false") 72 | { 73 | Sink_t sink{ std::cout }; 74 | 75 | REQUIRE_FALSE(core.removeSink(sink)); 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /tests/FileSinkTests.cpp: -------------------------------------------------------------------------------- 1 | // Copyright Dominic Koepke 2021 - 2021. 2 | // Distributed under the Boost Software License, Version 1.0. 3 | // (See accompanying file LICENSE_1_0.txt or copy at 4 | // https://www.boost.org/LICENSE_1_0.txt) 5 | 6 | #include 7 | 8 | #include 9 | #include 10 | #include 11 | 12 | #include "Simple-Log/FileSink.hpp" 13 | #include "Simple-Log/Record.hpp" 14 | 15 | using namespace sl::log; 16 | using namespace std::literals; 17 | 18 | using Record_t = BaseRecord; 19 | using FileSink_t = FileSink; 20 | 21 | inline std::filesystem::path defaultTestDir = std::filesystem::current_path() / "FileSinkTest"; 22 | 23 | void cleanupDefaultDirectory() 24 | { 25 | remove_all(defaultTestDir); 26 | } 27 | 28 | SCENARIO("FileSink should have a valid state after construction succeeded", "[FileSink][Sink]") 29 | { 30 | cleanupDefaultDirectory(); 31 | 32 | const auto fileNamePattern = "log.file"s; 33 | 34 | GIVEN("an non-empty file name pattern string") 35 | WHEN("constructing a FileSink instance with that pattern string as argument") 36 | THEN("fileNamePattern should return an equal pattern string") 37 | { 38 | FileSink_t sink{ fileNamePattern }; 39 | REQUIRE(sink.fileNamePattern() == fileNamePattern); 40 | } 41 | 42 | GIVEN("an arbitrary empty file name pattern string") 43 | WHEN("constructing a FileSink instance with that pattern string as argument") 44 | THEN("construction should throw an SinkException") 45 | { 46 | REQUIRE_THROWS_AS(FileSink_t{ "" }, SinkException); 47 | } 48 | 49 | GIVEN("an arbitrary directory") 50 | WHEN("constructing a FileSink instance with directory as argument") 51 | { 52 | FileSink_t sink{ fileNamePattern, defaultTestDir }; 53 | THEN("directory should return that directory") 54 | { 55 | REQUIRE(sink.directory() == defaultTestDir); 56 | } 57 | 58 | THEN("directory should exist after construction") 59 | { 60 | REQUIRE(is_directory(sink.directory())); 61 | } 62 | } 63 | 64 | GIVEN("a FileSink instance with un-specified directory") 65 | WHEN("setting directory afterwards") 66 | THEN("directory should exist") 67 | { 68 | FileSink_t sink{ "log.file" }; 69 | sink.setDirectory(defaultTestDir); 70 | REQUIRE(is_directory(sink.directory())); 71 | } 72 | } 73 | 74 | SCENARIO("FileSink::setFileNamePattern should modify member.", "[FileSink][Sink]") 75 | { 76 | const auto fileNamePattern = "log.file"s; 77 | FileSink_t sink{ fileNamePattern + ".t", defaultTestDir }; 78 | 79 | GIVEN("an arbitrary FileSink instance") 80 | { 81 | WHEN("setting file pattern name with an non-empty string") 82 | THEN("fileNamePattern should return an equal pattern string") 83 | { 84 | sink.setFileNamePattern(fileNamePattern); 85 | 86 | REQUIRE(sink.fileNamePattern() == fileNamePattern); 87 | } 88 | 89 | WHEN("setting file pattern name with an empty string") 90 | THEN("setFileNamePattern should throw an SinkException") 91 | { 92 | REQUIRE_THROWS_AS(sink.setFileNamePattern(""), SinkException); 93 | } 94 | 95 | WHEN("setting file pattern name with an non-empty tokenized string") 96 | THEN("setFileNamePattern should not throw an SinkException") 97 | { 98 | REQUIRE_NOTHROW(sink.setFileNamePattern("log%N.file")); 99 | } 100 | 101 | WHEN("setting file pattern name with an filename string and containing path info") 102 | THEN("setFileNamePattern should throw an SinkException") 103 | { 104 | REQUIRE_THROWS_AS(sink.setFileNamePattern("test/log.file"), SinkException); 105 | } 106 | 107 | WHEN("setting file pattern name with an tokenized string and containing path info") 108 | THEN("setFileNamePattern should throw an SinkException") 109 | { 110 | REQUIRE_THROWS_AS(sink.setFileNamePattern("test/log%N.file"), SinkException); 111 | } 112 | 113 | WHEN("setting file pattern name with only path info") 114 | THEN("setFileNamePattern should throw an SinkException") 115 | { 116 | REQUIRE_THROWS_AS(sink.setFileNamePattern("test/"), SinkException); 117 | } 118 | 119 | WHEN("setting file pattern name with only path info containing a token") 120 | THEN("setFileNamePattern should throw an SinkException") 121 | { 122 | REQUIRE_THROWS_AS(sink.setFileNamePattern("test%N/"), SinkException); 123 | } 124 | } 125 | } 126 | 127 | SCENARIO("FileSink::setDirectory should modify member.", "[FileSink][Sink]") 128 | { 129 | const auto fileNamePattern = "log.file"s; 130 | FileSink_t sink{ fileNamePattern }; 131 | 132 | GIVEN("an arbitrary FileSink instance") 133 | { 134 | WHEN("setting directory") 135 | { 136 | sink.setDirectory(defaultTestDir); 137 | THEN("directory should return similar path") 138 | { 139 | REQUIRE(sink.directory() == defaultTestDir); 140 | } 141 | 142 | AND_THEN("directory should exist after FileSink construction") 143 | { 144 | REQUIRE(is_directory(sink.directory())); 145 | } 146 | } 147 | } 148 | } 149 | 150 | SCENARIO("FileSink's rotate function should switch files.", "[FileSink][Sink]") 151 | { 152 | cleanupDefaultDirectory(); 153 | 154 | const auto fileNamePattern = "log%N.file"s; 155 | FileSink_t sink{ fileNamePattern, defaultTestDir }; 156 | 157 | GIVEN("a FileSink instance which doesn't have an open file yet") 158 | WHEN("calling rotate") 159 | THEN("nothing should happen") 160 | { 161 | REQUIRE_FALSE(exists(sink.directory() / "log1.file")); 162 | 163 | sink.rotate(); 164 | REQUIRE_FALSE(exists(sink.directory() / "log1.file")); 165 | } 166 | 167 | GIVEN("a FileSink instance which has an existing and open file") 168 | { 169 | sink.setEnabled(); 170 | sink.log({}); 171 | 172 | WHEN("calling rotate") 173 | THEN("sink should switch to new file") 174 | { 175 | REQUIRE(exists(sink.directory() / "log1.file")); 176 | sink.rotate(); 177 | REQUIRE(exists(sink.directory() / "log2.file")); 178 | } 179 | 180 | WHEN("setting up pattern name and directory pointing on an existing directory") 181 | AND_WHEN("calling rotate") 182 | THEN("rotate should throw a SinkException") 183 | { 184 | sink.setDirectory(std::filesystem::current_path()); 185 | sink.setFileNamePattern("FileSinkTest"); 186 | sink.setEnabled(true); 187 | 188 | REQUIRE_THROWS_AS(sink.rotate(), SinkException); 189 | } 190 | } 191 | } 192 | 193 | SCENARIO("FileSink openingHandler property should get invoked on file opening.", "[FileSink][Sink]") 194 | { 195 | const auto fileNamePattern = "log.file"s; 196 | 197 | FileSink_t sink{ fileNamePattern, defaultTestDir }; 198 | bool invoked = false; 199 | sink.setOpeningHandler( 200 | [&invoked]() 201 | { 202 | invoked = true; 203 | return ""; 204 | } 205 | ); 206 | 207 | GIVEN("a disabled FileSink instance") 208 | WHEN("opening a file by logging the first record") 209 | THEN("logging should not invoke the openingHandler") 210 | { 211 | sink.log({}); 212 | REQUIRE_FALSE(invoked); 213 | } 214 | 215 | GIVEN("an enabled FileSink instance") 216 | { 217 | sink.setEnabled(); 218 | sink.log({}); 219 | WHEN("opening a file by logging the first record") 220 | THEN("openingHandler should get invoked") 221 | { 222 | REQUIRE(invoked); 223 | } 224 | 225 | WHEN("rotating a file by calling rotate method") 226 | THEN("openingHandler should get invoked") 227 | { 228 | sink.rotate(); 229 | REQUIRE(invoked); 230 | } 231 | } 232 | } 233 | 234 | SCENARIO("FileSink closingHandler property should get invoked on file closing.", "[FileSink][Sink]") 235 | { 236 | const auto fileNamePattern = "log.file"s; 237 | 238 | std::optional sink{ std::in_place, fileNamePattern, defaultTestDir }; 239 | bool invoked = false; 240 | sink->setClosingHandler( 241 | [&invoked]() 242 | { 243 | invoked = true; 244 | return ""; 245 | } 246 | ); 247 | 248 | sink->setEnabled(); 249 | sink->log({}); 250 | 251 | GIVEN("an enabled FileSink instance which has an existing and open file") 252 | { 253 | WHEN("rotating a file by calling rotate method") 254 | THEN("closingHandler should get invoked") 255 | { 256 | sink->rotate(); 257 | REQUIRE(invoked); 258 | } 259 | 260 | WHEN("destructing this FileSink instance") 261 | THEN("closingHandler should get invoked") 262 | { 263 | sink.reset(); 264 | REQUIRE(invoked); 265 | } 266 | } 267 | } 268 | -------------------------------------------------------------------------------- /tests/FilterTests.cpp: -------------------------------------------------------------------------------- 1 | // Copyright Dominic Koepke 2021 - 2021. 2 | // Distributed under the Boost Software License, Version 1.0. 3 | // (See accompanying file LICENSE_1_0.txt or copy at 4 | // https://www.boost.org/LICENSE_1_0.txt) 5 | 6 | #include 7 | 8 | #include 9 | 10 | #include "Simple-Log/Filters.hpp" 11 | #include "Simple-Log/Predicates.hpp" 12 | #include "Simple-Log/Record.hpp" 13 | 14 | using namespace sl::log; 15 | 16 | using Record_t = BaseRecord; 17 | 18 | TEST_CASE("ProjectionFilter should watch on specified Record member", "[Filter]") 19 | { 20 | ProjectionFilter filter{ &Record_t::message, Equals{ 0 } }; 21 | 22 | SECTION("Approve Records with positive predicate result") 23 | { 24 | Record_t record; 25 | record.setMessage(0); 26 | 27 | REQUIRE(filter(record)); 28 | } 29 | 30 | SECTION("Reject Records with negative predicate result") 31 | { 32 | Record_t record; 33 | record.setMessage(1); 34 | 35 | REQUIRE_FALSE(filter(record)); 36 | } 37 | } 38 | 39 | TEST_CASE("makeMessageFilterFor function should create Filter watching on Record's message member", "[Filter]") 40 | { 41 | Record_t record; 42 | auto filter = makeMessageFilterFor(Equals{ 1337 }); 43 | 44 | REQUIRE_FALSE(std::invoke(filter, record)); 45 | 46 | record.setMessage(1337); 47 | 48 | REQUIRE(std::invoke(filter, record)); 49 | } 50 | 51 | TEST_CASE("makeSeverityFilterFor function should create Filter watching on Record's severity member", "[Filter]") 52 | { 53 | Record_t record; 54 | auto filter = makeSeverityFilterFor(Equals{ 1337 }); 55 | 56 | REQUIRE_FALSE(std::invoke(filter, record)); 57 | 58 | record.setSeverity(1337); 59 | 60 | REQUIRE(std::invoke(filter, record)); 61 | } 62 | 63 | TEST_CASE("makeChannelFilterFor function should create Filter watching on Record's channel member", "[Filter]") 64 | { 65 | Record_t record; 66 | auto filter = makeChannelFilterFor(Equals{ 1337 }); 67 | 68 | REQUIRE_FALSE(std::invoke(filter, record)); 69 | 70 | record.setChannel(1337); 71 | 72 | REQUIRE(std::invoke(filter, record)); 73 | } 74 | 75 | TEST_CASE("makeTimePointFilterFor function should create Filter watching on Record's timePoint member", "[Filter]") 76 | { 77 | Record_t record; 78 | auto filter = makeTimePointFilterFor(Equals{ 1337 }); 79 | 80 | REQUIRE_FALSE(std::invoke(filter, record)); 81 | 82 | record.setTimePoint(1337); 83 | 84 | REQUIRE(std::invoke(filter, record)); 85 | } 86 | 87 | // ToDo: Enable if clang is able to compile 88 | //TEMPLATE_TEST_CASE_SIG( 89 | // "make...FilterFor functions should create Filters watching for the corresponding Record member", 90 | // "[Filter]", 91 | // ((auto MakeFunction, auto SetterFunc), MakeFunction, SetterFunc), 92 | // ([](auto predicate) { return makeMessageFilterFor(predicate); }, 93 | // [](Record_t& rec, auto msg) { rec.setMessage(std::move(msg)); }), 94 | // ([](auto predicate) { return makeSeverityFilterFor(predicate); }, 95 | // [](Record_t& rec, auto msg) { rec.setSeverity(std::move(msg)); }), 96 | // ([](auto predicate) { return makeChannelFilterFor(predicate); }, 97 | // [](Record_t& rec, auto msg) { rec.setChannel(std::move(msg)); }), 98 | // ([](auto predicate) { return makeTimePointFilterFor(predicate); }, 99 | // [](Record_t& rec, auto msg) { rec.setTimePoint(std::move(msg)); }) 100 | // ) 101 | //{ 102 | // Record_t record; 103 | // auto filter = std::invoke(MakeFunction, Equals{ 1337 }); 104 | // 105 | // REQUIRE_FALSE(std::invoke(filter, record)); 106 | // 107 | // std::invoke(SetterFunc, record, 1337); 108 | // 109 | // REQUIRE(std::invoke(filter, record)); 110 | //} 111 | 112 | TEST_CASE("FilterChain should determine the amount of attached sub-filters", "[Filter]") 113 | { 114 | SECTION("With no attached sub-filters") 115 | { 116 | FilterChain filterChain 117 | { 118 | detail::TupleAllOf{}, 119 | }; 120 | 121 | REQUIRE(filterChain.empty()); 122 | REQUIRE(filterChain.size() == 0); 123 | } 124 | 125 | SECTION("With some attached sub-filters") 126 | { 127 | constexpr auto dummyFilter = [](const auto& rec) 128 | { 129 | return true; 130 | }; 131 | 132 | FilterChain filterChain 133 | { 134 | detail::TupleAllOf{}, 135 | dummyFilter, 136 | dummyFilter 137 | }; 138 | 139 | REQUIRE_FALSE(filterChain.empty()); 140 | REQUIRE(filterChain.size() == 2); 141 | } 142 | } 143 | 144 | TEST_CASE("FilterChain should determine invocation result based on their applied algorithm", "[Filter]") 145 | { 146 | SECTION("With AllOf algorithm, and each sub-filter returning true, every filter should be invoked") 147 | { 148 | int invokeCounter = 0; 149 | auto countedPred = [&invokeCounter](const auto& rec) 150 | { 151 | ++invokeCounter; 152 | return true; 153 | }; 154 | 155 | FilterChain filter 156 | { 157 | detail::TupleAllOf{}, 158 | countedPred, 159 | countedPred, 160 | countedPred 161 | }; 162 | 163 | filter(Record_t{}); 164 | 165 | REQUIRE(invokeCounter == std::size(filter)); 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /tests/FlushPolicyTests.cpp: -------------------------------------------------------------------------------- 1 | // Copyright Dominic Koepke 2021 - 2021. 2 | // Distributed under the Boost Software License, Version 1.0. 3 | // (See accompanying file LICENSE_1_0.txt or copy at 4 | // https://www.boost.org/LICENSE_1_0.txt) 5 | 6 | #include 7 | 8 | #include 9 | 10 | #include "Simple-Log/FlushPolicies.hpp" 11 | #include "Simple-Log/Predicates.hpp" 12 | #include "Simple-Log/Record.hpp" 13 | 14 | using namespace sl::log; 15 | 16 | using Record_t = BaseRecord; 17 | 18 | namespace 19 | { 20 | struct FlushPolicyMoc 21 | { 22 | template 23 | bool operator ()(const TRecord& record, std::size_t count) 24 | { 25 | if (invocationCounter) 26 | { 27 | ++(*invocationCounter); 28 | } 29 | return true; 30 | } 31 | 32 | void flushed() 33 | { 34 | if (flushedCounter) 35 | { 36 | ++(*flushedCounter); 37 | } 38 | } 39 | 40 | int* invocationCounter = nullptr; 41 | int* flushedCounter = nullptr; 42 | }; 43 | } 44 | 45 | TEST_CASE("ConstInvokable should always return the expected result", "[Flush-Policy]") 46 | { 47 | detail::ConstantInvokable alwaysTrue; 48 | REQUIRE(alwaysTrue()); 49 | 50 | detail::ConstantInvokable alwaysFalse; 51 | REQUIRE_FALSE(alwaysFalse()); 52 | 53 | detail::ConstantInvokable<1337> always1337; 54 | REQUIRE(always1337() == 1337); 55 | } 56 | 57 | TEST_CASE("FlushPolicy should return results determined by predicate", "[Flush-Policy]") 58 | { 59 | auto shallFilter = GENERATE(false, true); 60 | 61 | auto predicate = [shallFilter](const auto& rec, std::size_t byteCount) 62 | { 63 | return shallFilter; 64 | }; 65 | 66 | FlushPolicy policy{ predicate }; 67 | 68 | REQUIRE(policy(Record_t{}, 0) == shallFilter); 69 | } 70 | 71 | TEST_CASE("FlushPolicyChain should determine invocation result based on their applied algorithm", "[Flush-Policy]") 72 | { 73 | SECTION("With AllOf algorithm, and each FlushPolicy returning true, every policy should be invoked") 74 | { 75 | int invokeCounter = 0; 76 | int flushedCounter = 0; 77 | FlushPolicyChain policies 78 | { 79 | detail::TupleAllOf{}, 80 | FlushPolicyMoc{ .invocationCounter = &invokeCounter, .flushedCounter = &flushedCounter }, 81 | FlushPolicyMoc{ .invocationCounter = &invokeCounter, .flushedCounter = &flushedCounter }, 82 | FlushPolicyMoc{ .invocationCounter = &invokeCounter, .flushedCounter = &flushedCounter } 83 | }; 84 | 85 | policies(Record_t{}, 0); 86 | policies.flushed(); 87 | 88 | REQUIRE(invokeCounter == std::size(policies)); 89 | REQUIRE(flushedCounter == std::size(policies)); 90 | } 91 | } 92 | 93 | TEST_CASE("FlushPolicyChain based types should compile fine.", "[Flush-Policy]") 94 | { 95 | SECTION("FlushPolicyAllOf should compile fine.") 96 | { 97 | FlushPolicyAllOf policies 98 | { 99 | FlushPolicyMoc{}, 100 | FlushPolicyMoc{}, 101 | FlushPolicyMoc{}, 102 | }; 103 | 104 | policies(Record_t{}, 0); 105 | policies.flushed(); 106 | REQUIRE_FALSE(policies.empty()); 107 | REQUIRE(policies.size() == 3); 108 | } 109 | 110 | SECTION("FlushPolicyAnyOf should compile fine.") 111 | { 112 | FlushPolicyAllOf policies 113 | { 114 | FlushPolicyMoc{}, 115 | FlushPolicyMoc{}, 116 | FlushPolicyMoc{}, 117 | }; 118 | 119 | policies(Record_t{}, 0); 120 | policies.flushed(); 121 | REQUIRE_FALSE(policies.empty()); 122 | REQUIRE(policies.size() == 3); 123 | } 124 | 125 | SECTION("FlushPolicyNoneOf should compile fine.") 126 | { 127 | FlushPolicyAllOf policies 128 | { 129 | FlushPolicyMoc{}, 130 | FlushPolicyMoc{}, 131 | FlushPolicyMoc{}, 132 | }; 133 | 134 | policies(Record_t{}, 0); 135 | policies.flushed(); 136 | REQUIRE_FALSE(policies.empty()); 137 | REQUIRE(policies.size() == 3); 138 | } 139 | } 140 | 141 | TEST_CASE("AlwaysFlushPolicy should always return true", "[Flush-Policy]") 142 | { 143 | AlwaysFlushPolicy policy{}; 144 | 145 | REQUIRE(policy(Record_t{}, 0)); 146 | REQUIRE(policy(Record_t{}, 0)); 147 | } 148 | 149 | TEST_CASE("makeMessageFlushPolicyFor function should create Policy watching on Record's message member", "[Flush-Policy]") 150 | { 151 | Record_t record; 152 | auto policy = makeMessageFlushPolicyFor(Equals{ 1337 }); 153 | 154 | REQUIRE_FALSE(std::invoke(policy, record, 0)); 155 | 156 | record.setMessage(1337); 157 | 158 | REQUIRE(std::invoke(policy, record, 0)); 159 | } 160 | 161 | TEST_CASE("makeSeverityFlushPolicyFor function should create Policy watching on Record's severity member", "[Flush-Policy]") 162 | { 163 | Record_t record; 164 | auto policy = makeSeverityFlushPolicyFor(Equals{ 1337 }); 165 | 166 | REQUIRE_FALSE(std::invoke(policy, record, 0)); 167 | 168 | record.setSeverity(1337); 169 | 170 | REQUIRE(std::invoke(policy, record, 0)); 171 | } 172 | 173 | TEST_CASE("makeChannelFlushPolicyFor function should create Policy watching on Record's channel member", "[Flush-Policy]") 174 | { 175 | Record_t record; 176 | auto policy = makeChannelFlushPolicyFor(Equals{ 1337 }); 177 | 178 | REQUIRE_FALSE(std::invoke(policy, record, 0)); 179 | 180 | record.setChannel(1337); 181 | 182 | REQUIRE(std::invoke(policy, record, 0)); 183 | } 184 | 185 | TEST_CASE("makeTimePointFlushPolicyFor function should create Policy watching on Record's timePoint member", "[Flush-Policy]") 186 | { 187 | Record_t record; 188 | auto policy = makeTimePointFlushPolicyFor(Equals{ 1337 }); 189 | 190 | REQUIRE_FALSE(std::invoke(policy, record, 0)); 191 | 192 | record.setTimePoint(1337); 193 | 194 | REQUIRE(std::invoke(policy, record, 0)); 195 | } 196 | 197 | TEST_CASE("ByteCountFlushPolicy should flush after a defined amount of data has been buffered", "[Flush-Policy]") 198 | { 199 | const std::size_t threshold = 1024; 200 | ByteCountFlushPolicy policy{ threshold }; 201 | 202 | SECTION("Accumulated amount of bytes less than threshold should yield negative results.") 203 | { 204 | REQUIRE_FALSE(policy(Record_t{}, 100)); 205 | REQUIRE_FALSE(policy(Record_t{}, 100)); 206 | } 207 | 208 | SECTION("Exceeding the threshold should yield once a positive result, followed up by negative results after flushed().") 209 | { 210 | REQUIRE(policy(Record_t{}, 1024)); 211 | policy.flushed(); 212 | REQUIRE_FALSE(policy(Record_t{}, 100)); 213 | } 214 | } 215 | 216 | TEST_CASE("TimedFlushPolicy should flush after a defined duration has been exceeded", "[Flush-Policy]") 217 | { 218 | const std::chrono::milliseconds threshold{ 10 }; 219 | TimedFlushPolicy policy{ threshold * 2 }; 220 | 221 | SECTION("Invoking during that duration should always yield a negative result.") 222 | { 223 | const auto begin = TimedFlushPolicy::Clock_t::now(); 224 | while (threshold < TimedFlushPolicy::Clock_t::now() - begin) 225 | { 226 | REQUIRE_FALSE(policy(Record_t{}, 0)); 227 | } 228 | } 229 | 230 | SECTION("Waiting until duration exceeded should yield a positive result, followed up by negative results after flushed().") 231 | { 232 | std::this_thread::sleep_for(threshold * 2); 233 | REQUIRE(policy(Record_t{}, 0)); 234 | policy.flushed(); 235 | REQUIRE_FALSE(policy(Record_t{}, 0)); 236 | } 237 | } 238 | -------------------------------------------------------------------------------- /tests/LoggerTests.cpp: -------------------------------------------------------------------------------- 1 | // Copyright Dominic Koepke 2021 - 2021. 2 | // Distributed under the Boost Software License, Version 1.0. 3 | // (See accompanying file LICENSE_1_0.txt or copy at 4 | // https://www.boost.org/LICENSE_1_0.txt) 5 | 6 | #include 7 | 8 | #include "Simple-Log/Logger.hpp" 9 | #include "Simple-Log/Record.hpp" 10 | 11 | using namespace sl::log; 12 | 13 | namespace 14 | { 15 | struct LogCallbackMoc 16 | { 17 | template 18 | void operator ()(TRecord) const 19 | { 20 | if (invoked) 21 | { 22 | *invoked = true; 23 | } 24 | } 25 | 26 | bool* invoked = nullptr; 27 | }; 28 | } 29 | 30 | TEMPLATE_PRODUCT_TEST_CASE( 31 | "BaseLoggers should have a valid state after default construction succeeded", 32 | "[BaseLogger][Logger]", 33 | BaseLogger, 34 | ( 35 | (BaseRecord), 36 | (BaseRecord) 37 | ) 38 | ) 39 | { 40 | using Logger_t = TestType; 41 | using Record_t = LoggerRecord_t; 42 | 43 | Logger_t logger{ LogCallbackMoc{} }; 44 | REQUIRE(logger.defaultSeverity() == typename Record_t::SeverityLevel_t{}); 45 | REQUIRE(logger.defaultChannel() == typename Record_t::Channel_t{}); 46 | } 47 | 48 | TEMPLATE_TEST_CASE_SIG( 49 | "BaseLoggers should have a valid state after construction succeeded", 50 | "[BaseLogger][Logger]", 51 | ((auto VSev, auto VChan), VSev, VChan), 52 | (1, 2) 53 | ) 54 | { 55 | using Record_t = BaseRecord; 56 | using Logger_t = BaseLogger; 57 | 58 | Logger_t logger{ LogCallbackMoc{}, VSev, VChan }; 59 | 60 | REQUIRE(logger.defaultSeverity() == VSev); 61 | REQUIRE(logger.defaultChannel() == VChan); 62 | } 63 | 64 | TEMPLATE_TEST_CASE_SIG( 65 | "BaseLoggers getter should yield equal results after setter were used.", 66 | "[BaseLogger][Logger]", 67 | ((auto VSev, auto VChan), VSev, VChan), 68 | (1, 2) 69 | ) 70 | { 71 | using Record_t = BaseRecord; 72 | using Logger_t = BaseLogger; 73 | 74 | Logger_t logger{ LogCallbackMoc{} }; 75 | logger.setDefaultSeverity(VSev); 76 | logger.setDefaultChannel(VChan); 77 | 78 | REQUIRE(logger.defaultSeverity() == VSev); 79 | REQUIRE(logger.defaultChannel() == VChan); 80 | } 81 | 82 | TEST_CASE("BaseLogger should invoke its callback after their created RecordBuilder gets destroyed.", "[BaseLogger][Logger]") 83 | { 84 | using Record_t = BaseRecord; 85 | using Logger_t = BaseLogger; 86 | 87 | bool invoked = false; 88 | Logger_t logger{ LogCallbackMoc{ &invoked } }; 89 | 90 | { 91 | auto recBuilder = logger(); 92 | REQUIRE_FALSE(invoked); 93 | } 94 | REQUIRE(invoked); 95 | } 96 | 97 | namespace custom 98 | { 99 | struct Logger 100 | { 101 | using ORecord_t = BaseRecord; 102 | 103 | using RecordBuilder_t = RecordBuilder; 104 | 105 | template 106 | explicit Logger(TCallback cb) 107 | { 108 | } 109 | 110 | RecordBuilder_t operator()() 111 | { 112 | return RecordBuilder_t{ 113 | {}, 114 | [](ORecord_t&&) 115 | { 116 | } 117 | }; 118 | } 119 | }; 120 | } 121 | 122 | template <> 123 | struct sl::log::LoggerTypedefs 124 | { 125 | using Record_t = custom::Logger::ORecord_t; 126 | }; 127 | 128 | TEST_CASE("Custom Logger type using the abstractions should compile successfully.", "[Logger]") 129 | { 130 | using Logger_t = custom::Logger; 131 | using Typedefs_t = LoggerTypedefs; 132 | using Core_t = Core>; 133 | 134 | REQUIRE(std::is_same_v); 135 | REQUIRE(std::is_same_v, Logger_t::ORecord_t>); 136 | 137 | Core_t core; 138 | auto logger = makeLogger(core); 139 | logger(); 140 | } 141 | -------------------------------------------------------------------------------- /tests/OStreamSinkTests.cpp: -------------------------------------------------------------------------------- 1 | // Copyright Dominic Koepke 2021 - 2021. 2 | // Distributed under the Boost Software License, Version 1.0. 3 | // (See accompanying file LICENSE_1_0.txt or copy at 4 | // https://www.boost.org/LICENSE_1_0.txt) 5 | 6 | #include 7 | 8 | #include 9 | #include 10 | 11 | #include "Simple-Log/OStreamSink.hpp" 12 | #include "Simple-Log/Record.hpp" 13 | 14 | using namespace sl::log; 15 | 16 | using Record_t = BaseRecord; 17 | using OStreamSink_t = OStreamSink; 18 | 19 | namespace 20 | { 21 | struct FlushPolicyMoc 22 | { 23 | bool operator ()(const Record_t& rec, std::size_t byteCount) const noexcept 24 | { 25 | assert(invocationCount); 26 | ++(*invocationCount); 27 | return (*invocationCount & 1ull) != 0; 28 | } 29 | 30 | void flushed() const noexcept 31 | { 32 | assert(flushedSignalCount); 33 | ++(*flushedSignalCount); 34 | } 35 | 36 | std::size_t* invocationCount; 37 | std::size_t* flushedSignalCount; 38 | }; 39 | } 40 | 41 | SCENARIO("OStreamSink's FlushPolicy property should determine when stream has to be flushed.", "[OStreamSink][Sink]") 42 | { 43 | std::ostringstream out; 44 | OStreamSink_t sink{ out }; 45 | sink.setEnabled(); 46 | 47 | std::size_t invocationCount = 0; 48 | std::size_t flushedSignalCount = 0; 49 | 50 | sink.setFlushPolicy( 51 | FlushPolicyMoc{ 52 | .invocationCount = &invocationCount, 53 | .flushedSignalCount = &flushedSignalCount 54 | } 55 | ); 56 | 57 | GIVEN("an enabled OStreamSink instance") 58 | { 59 | WHEN("calling log") 60 | { 61 | sink.log({}); 62 | THEN("FlushPolicy property should get invoked") 63 | { 64 | REQUIRE(invocationCount == 1); 65 | } 66 | 67 | AND_THEN("FlushPolicy property should get notified about flushed") 68 | { 69 | REQUIRE(flushedSignalCount == 1); 70 | } 71 | } 72 | 73 | AND_WHEN("calling flushed") 74 | AND_THEN("FlushPolicy property should get notified about flushed") 75 | { 76 | sink.flush(); 77 | REQUIRE(flushedSignalCount == 1); 78 | } 79 | 80 | AND_WHEN("removeFlushPolicy is called") 81 | THEN("then previous FlushPolicy should get replaced") 82 | { 83 | sink.removeFlushPolicy(); 84 | sink.log({}); 85 | REQUIRE(invocationCount == 0); 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /tests/PredicateTests.cpp: -------------------------------------------------------------------------------- 1 | // Copyright Dominic Koepke 2021 - 2021. 2 | // Distributed under the Boost Software License, Version 1.0. 3 | // (See accompanying file LICENSE_1_0.txt or copy at 4 | // https://www.boost.org/LICENSE_1_0.txt) 5 | 6 | #include 7 | 8 | #include "Simple-Log/Predicates.hpp" 9 | 10 | using namespace sl::log; 11 | 12 | TEMPLATE_TEST_CASE_SIG( 13 | "Single-bounded Unary-Comparison-Predicates should yield expected results on invocation", 14 | "[Total-Ordering][Predicates]", 15 | ((typename TPred, auto Init, auto Value, bool Expected), TPred, Init, Value, Expected), 16 | (Equals, 1337, 1337, true), 17 | (Equals, 1337, 42, false), 18 | 19 | (NotEquals, 1337, 1337, false), 20 | (NotEquals, 1337, 42, true), 21 | 22 | (Less, 1337, 1338, false), 23 | (Less, 1337, 1337, false), 24 | (Less, 1337, 1336, true), 25 | 26 | (LessEquals, 1337, 1338, false), 27 | (LessEquals, 1337, 1337, true), 28 | (LessEquals, 1337, 1336, true), 29 | 30 | (Greater, 1337, 1338, true), 31 | (Greater, 1337, 1337, false), 32 | (Greater, 1337, 1336, false), 33 | 34 | (GreaterEquals, 1337, 1338, true), 35 | (GreaterEquals, 1337, 1337, true), 36 | (GreaterEquals, 1337, 1336, false) 37 | ) 38 | { 39 | TPred predicate{ Init }; 40 | 41 | auto result = std::invoke(predicate, Value); 42 | 43 | REQUIRE(result == Expected); 44 | } 45 | 46 | TEMPLATE_TEST_CASE_SIG( 47 | "Dual-bounded Unary-Comparison-Predicates should yield excpeted results on invocation", 48 | "[Total-Ordering][Predicates]", 49 | ((typename TPred, auto Init1, auto Init2, auto Value, bool Expected), 50 | TPred, Init1, Init2, Value, Expected), 51 | 52 | (Between, 1337, 1337, 1338, false), 53 | (Between, 1337, 1337, 1337, false), 54 | (Between, 1337, 1337, 1336, false), 55 | 56 | (Between, 1336, 1337, 1338, false), 57 | (Between, 1336, 1337, 1337, false), 58 | (Between, 1336, 1337, 1336, false), 59 | (Between, 1336, 1337, 1335, false), 60 | 61 | (Between, 1336, 1338, 1339, false), 62 | (Between, 1336, 1338, 1338, false), 63 | (Between, 1336, 1338, 1337, true), 64 | (Between, 1336, 1338, 1336, false), 65 | (Between, 1336, 1338, 1335, false), 66 | 67 | (BetweenEquals, 1337, 1337, 1338, false), 68 | (BetweenEquals, 1337, 1337, 1337, true), 69 | (BetweenEquals, 1337, 1337, 1336, false), 70 | 71 | (BetweenEquals, 1336, 1337, 1338, false), 72 | (BetweenEquals, 1336, 1337, 1337, true), 73 | (BetweenEquals, 1336, 1337, 1336, true), 74 | (BetweenEquals, 1336, 1337, 1335, false), 75 | 76 | (BetweenEquals, 1336, 1338, 1339, false), 77 | (BetweenEquals, 1336, 1338, 1338, true), 78 | (BetweenEquals, 1336, 1338, 1337, true), 79 | (BetweenEquals, 1336, 1338, 1336, true), 80 | (BetweenEquals, 1336, 1338, 1335, false) 81 | ) 82 | { 83 | TPred predicate{ Init1, Init2 }; 84 | 85 | auto result = std::invoke(predicate, Value); 86 | 87 | REQUIRE(result == Expected); 88 | } 89 | -------------------------------------------------------------------------------- /tests/RecordBuilderTests.cpp: -------------------------------------------------------------------------------- 1 | // Copyright Dominic Koepke 2021 - 2021. 2 | // Distributed under the Boost Software License, Version 1.0. 3 | // (See accompanying file LICENSE_1_0.txt or copy at 4 | // https://www.boost.org/LICENSE_1_0.txt) 5 | 6 | #include 7 | 8 | #include 9 | 10 | #include "Simple-Log/Record.hpp" 11 | #include "Simple-Log/RecordBuilder.hpp" 12 | 13 | using namespace sl::log; 14 | 15 | using Record_t = BaseRecord; 16 | using RecordBuilder_t = RecordBuilder; 17 | 18 | namespace 19 | { 20 | struct LogCallbackMoc 21 | { 22 | void operator ()(Record_t record) const 23 | { 24 | if (invoked) 25 | { 26 | *invoked = true; 27 | } 28 | 29 | if (targetRec) 30 | { 31 | *targetRec = std::move(record); 32 | } 33 | } 34 | 35 | bool* invoked = nullptr; 36 | Record_t* targetRec = nullptr; 37 | }; 38 | } 39 | 40 | TEST_CASE("RecordBuilders should have a valid state after construction succeeded", "[RecordBuilder]") 41 | { 42 | Record_t record; 43 | record.setSeverity(1337); 44 | RecordBuilder_t recBuilder{ record, LogCallbackMoc{} }; 45 | 46 | REQUIRE(std::as_const(recBuilder).record().severity() == record.severity()); 47 | REQUIRE(recBuilder.record().severity() == record.severity()); 48 | } 49 | 50 | TEST_CASE("RecordBuilders should transfer its state to the move target", "[RecordBuilder]") 51 | { 52 | Record_t record; 53 | record.setSeverity(1337); 54 | std::optional targetRecBuilder; 55 | bool invoked = false; 56 | { 57 | RecordBuilder_t recBuilder{ record, LogCallbackMoc{ &invoked } }; 58 | targetRecBuilder = std::move(recBuilder); 59 | } 60 | 61 | REQUIRE_FALSE(invoked); 62 | REQUIRE(targetRecBuilder->record().severity() == record.severity()); 63 | 64 | targetRecBuilder.reset(); 65 | REQUIRE(invoked); 66 | } 67 | 68 | TEST_CASE("RecordBuilder's operator << should append strings to the internal stream.", "[RecordBuilder]") 69 | { 70 | Record_t targetRecord; 71 | std::optional recBuilder{ std::in_place, Record_t{}, LogCallbackMoc{ .targetRec = &targetRecord } }; 72 | *recBuilder << 1337; 73 | recBuilder.reset(); 74 | 75 | REQUIRE(targetRecord.message() == "1337"); 76 | } 77 | 78 | TEST_CASE("SetSev manipulator should manipulate Record's severity property.", "[RecordBuilder]") 79 | { 80 | RecordBuilder_t recBuilder{ Record_t{}, LogCallbackMoc{} }; 81 | recBuilder << SetSev{ 1337 }; 82 | 83 | REQUIRE(recBuilder.record().severity() == 1337); 84 | } 85 | 86 | TEST_CASE("SetChan manipulator should manipulate Record's channel property.", "[RecordBuilder]") 87 | { 88 | RecordBuilder_t recBuilder{ Record_t{}, LogCallbackMoc{} }; 89 | recBuilder << SetChan{ 1337 }; 90 | 91 | REQUIRE(recBuilder.record().channel() == 1337); 92 | } 93 | -------------------------------------------------------------------------------- /tests/RecordQueueTests.cpp: -------------------------------------------------------------------------------- 1 | // Copyright Dominic Koepke 2021 - 2021. 2 | // Distributed under the Boost Software License, Version 1.0. 3 | // (See accompanying file LICENSE_1_0.txt or copy at 4 | // https://www.boost.org/LICENSE_1_0.txt) 5 | 6 | #include "catch2/catch.hpp" 7 | 8 | #include "Simple-Log/Record.hpp" 9 | #include "Simple-Log/RecordQueue.hpp" 10 | 11 | #include 12 | 13 | using namespace sl::log; 14 | 15 | using Record_t = BaseRecord; 16 | 17 | static std::vector generateRecordsWithIncrMsg(std::size_t count) 18 | { 19 | std::vector records; 20 | records.reserve(count); 21 | std::ranges::generate_n( 22 | std::back_inserter(records), 23 | count, 24 | [i = 0]() mutable 25 | { 26 | Record_t record; 27 | record.setMessage(i++); 28 | return record; 29 | } 30 | ); 31 | return records; 32 | } 33 | 34 | SCENARIO("RecordQueue's empty and size should yield expected results.", "[RecordQueue]") 35 | { 36 | RecordQueue queue; 37 | 38 | GIVEN("an empty queue") 39 | { 40 | WHEN("calling empty()") 41 | THEN("should yield true") 42 | { 43 | REQUIRE(queue.empty()); 44 | } 45 | 46 | WHEN("calling size()") 47 | THEN("should yield 0") 48 | { 49 | REQUIRE(queue.size() == 0); // NOLINT(readability-container-size-empty) 50 | } 51 | } 52 | 53 | GIVEN("a non-empty queue") 54 | { 55 | queue.push({}); 56 | WHEN("calling empty()") 57 | THEN("should yield false") 58 | { 59 | REQUIRE_FALSE(queue.empty()); 60 | } 61 | 62 | WHEN("calling size()") 63 | THEN("should yield expected size") 64 | { 65 | REQUIRE(queue.size() == 1); 66 | } 67 | } 68 | } 69 | 70 | SCENARIO("RecordQueue should provide values in insertion-order (FIFO).", "[RecordQueue]") 71 | { 72 | RecordQueue queue; 73 | 74 | GIVEN("an empty queue") 75 | WHEN("pushing new Records") 76 | THEN("queue will grow") 77 | { 78 | auto records = generateRecordsWithIncrMsg(10); 79 | for (std::size_t i = 0; i < std::size(records); ++i) 80 | { 81 | queue.push(records[i]); 82 | } 83 | 84 | REQUIRE(std::size(queue) == std::size(records)); 85 | 86 | AND_WHEN("taking these Records") 87 | THEN("caller will receive them in insertion-order") 88 | { 89 | for (auto& record : records) 90 | { 91 | auto takenRecord = queue.take(); 92 | REQUIRE(takenRecord); 93 | REQUIRE(takenRecord->message() == record.message()); 94 | } 95 | 96 | REQUIRE(std::empty(queue)); 97 | } 98 | } 99 | } 100 | 101 | SCENARIO("Calling take should behave as expected in blocking and non-blocking cases.", "[RecordQueue]") 102 | { 103 | RecordQueue queue; 104 | REQUIRE(std::empty(queue)); 105 | REQUIRE(std::size(queue) == 0); 106 | 107 | GIVEN("an empty queue") 108 | WHEN("requesting Records on empty queue") 109 | THEN("will block and break after specified duration") 110 | { 111 | std::chrono::milliseconds duration{ 100 }; 112 | 113 | auto begin = std::chrono::steady_clock::now(); 114 | auto record = queue.take(duration); 115 | auto end = std::chrono::steady_clock::now(); 116 | auto diff = end - begin; 117 | 118 | REQUIRE(record == std::nullopt); 119 | REQUIRE(duration <= diff); 120 | } 121 | 122 | GIVEN("an empty queue") 123 | WHEN("requesting Records") 124 | THEN("will wait until Record is inserted and will return an valid Record") 125 | { 126 | std::chrono::milliseconds waitDuration{ 100 }; 127 | std::atomic_bool finished{ false }; 128 | auto future = std::async( 129 | std::launch::async, 130 | [&queue, &finished]() 131 | { 132 | const auto rec = queue.take(); 133 | finished = true; 134 | return rec; 135 | } 136 | ); 137 | 138 | std::this_thread::sleep_for(waitDuration); 139 | REQUIRE(finished == false); 140 | 141 | AND_WHEN("pushing Records while blocking") 142 | THEN("will break and return that Record") 143 | { 144 | Record_t inRecord; 145 | inRecord.setMessage(1337); 146 | queue.push(inRecord); 147 | auto futureState = future.wait_for(waitDuration); 148 | auto record = future.get(); 149 | 150 | REQUIRE(futureState == std::future_status::ready); 151 | REQUIRE(record != std::nullopt); 152 | REQUIRE(record->message() == inRecord.message()); 153 | } 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /tests/RecordTests.cpp: -------------------------------------------------------------------------------- 1 | // Copyright Dominic Koepke 2021 - 2021. 2 | // Distributed under the Boost Software License, Version 1.0. 3 | // (See accompanying file LICENSE_1_0.txt or copy at 4 | // https://www.boost.org/LICENSE_1_0.txt) 5 | 6 | #include 7 | 8 | #include 9 | #include 10 | #include 11 | 12 | #include "Simple-Log/Record.hpp" 13 | 14 | using namespace sl::log; 15 | 16 | using DefaultTimePoint_t = std::chrono::system_clock::time_point; 17 | 18 | TEMPLATE_PRODUCT_TEST_CASE( 19 | "BaseRecords should have a valid state after construction succeeded", 20 | "[BaseRecord][Record]", 21 | BaseRecord, 22 | ( 23 | (int, int, int, int), 24 | (std::string, int, int, int), 25 | (int, std::string, int, int), 26 | (int, int, std::string, int), 27 | (int, int, int, std::string) 28 | ) 29 | ) 30 | { 31 | using Record_t = TestType; 32 | 33 | Record_t record; 34 | REQUIRE(record.message() == RecordMessage_t{}); 35 | REQUIRE(record.severity() == RecordSeverity_t{}); 36 | REQUIRE(record.channel() == RecordChannel_t{}); 37 | REQUIRE(record.timePoint() == RecordTimePoint_t{}); 38 | } 39 | 40 | TEMPLATE_PRODUCT_TEST_CASE( 41 | "Record Getter abstraction should yield equal results as actual getters of BaseRecord type.", 42 | "[BaseRecord][Record]", 43 | BaseRecord, 44 | ( 45 | (int, int, int, int), 46 | (std::string, int, int, int), 47 | (int, std::string, int, int), 48 | (int, int, std::string, int), 49 | (int, int, int, std::string) 50 | ) 51 | ) 52 | { 53 | using Record_t = TestType; 54 | using Getters_t = RecordGetters; 55 | 56 | Record_t record; 57 | REQUIRE(Getters_t::message(record) == RecordMessage_t{}); 58 | REQUIRE(Getters_t::severity(record) == RecordSeverity_t{}); 59 | REQUIRE(Getters_t::channel(record) == RecordChannel_t{}); 60 | REQUIRE(Getters_t::timePoint(record) == RecordTimePoint_t{}); 61 | } 62 | 63 | TEMPLATE_TEST_CASE_SIG( 64 | "BaseRecords getter should yield equal results after setter were used.", 65 | "[BaseRecord][Record]", 66 | ((auto VSev, auto VChan, auto VMsg, auto VTimePoint), VSev, VChan, VMsg, VTimePoint), 67 | (1, 2, 3, 4) 68 | ) 69 | { 70 | using Record_t = BaseRecord; 71 | Record_t record; 72 | record.setSeverity(VSev); 73 | record.setChannel(VChan); 74 | record.setMessage(VMsg); 75 | record.setTimePoint(VTimePoint); 76 | 77 | REQUIRE(record.severity() == VSev); 78 | REQUIRE(record.channel() == VChan); 79 | REQUIRE(record.message() == VMsg); 80 | REQUIRE(record.timePoint() == VTimePoint); 81 | } 82 | 83 | TEMPLATE_TEST_CASE_SIG( 84 | "Record Setter abstraction should yield equal results as actual setters of BaseRecord type.", 85 | "[BaseRecord][Record]", 86 | ((auto VSev, auto VChan, auto VMsg, auto VTimePoint), VSev, VChan, VMsg, VTimePoint), 87 | (1, 2, 3, 4) 88 | ) 89 | { 90 | using Record_t = BaseRecord; 91 | using Getters_t = RecordGetters; 92 | using Setters_t = RecordSetters; 93 | 94 | Record_t record; 95 | Setters_t::setMessage(record, VMsg); 96 | Setters_t::setSeverity(record, VSev); 97 | Setters_t::setChannel(record, VChan); 98 | Setters_t::setTimePoint(record, VTimePoint); 99 | 100 | REQUIRE(Getters_t::message(record) == VMsg); 101 | REQUIRE(Getters_t::severity(record) == VSev); 102 | REQUIRE(Getters_t::channel(record) == VChan); 103 | REQUIRE(Getters_t::timePoint(record) == VTimePoint); 104 | } 105 | 106 | namespace custom 107 | { 108 | struct Record 109 | { 110 | std::string oMessage{}; 111 | int oSev{}; 112 | float oChan{}; 113 | DefaultTimePoint_t oTimePoint{}; 114 | }; 115 | 116 | 117 | } 118 | 119 | template <> 120 | struct sl::log::RecordTypedefs 121 | { 122 | using Message_t = std::string; 123 | using Severity_t = int; 124 | using Channel_t = float; 125 | using TimePoint_t = DefaultTimePoint_t; 126 | }; 127 | 128 | template <> 129 | struct sl::log::RecordGetters 130 | { 131 | constexpr static auto message{ [](custom::Record& record) { return record.oMessage; } }; 132 | constexpr static auto severity{ [](custom::Record& record) { return record.oSev; } }; 133 | constexpr static auto channel{ [](custom::Record& record) { return record.oChan; } }; 134 | constexpr static auto timePoint{ [](custom::Record& record) { return record.oTimePoint; } }; 135 | }; 136 | 137 | template <> 138 | struct sl::log::RecordSetters 139 | { 140 | constexpr static auto setMessage{ [](custom::Record& record, auto msg) { record.oMessage = msg; } }; 141 | constexpr static auto setSeverity{ [](custom::Record& record, auto sev) { record.oSev = sev; } }; 142 | constexpr static auto setChannel{ [](custom::Record& record, auto chan) { record.oChan = chan; } }; 143 | constexpr static auto setTimePoint{ [](custom::Record& record, auto tPoint) { record.oTimePoint = tPoint; } }; 144 | }; 145 | 146 | TEST_CASE("Custom Record type using the abstractions should compile successfully.", "[Record]") 147 | { 148 | using Record_t = custom::Record; 149 | using Typedefs_t = RecordTypedefs; 150 | using Getters_t = RecordGetters; 151 | using Setters_t = RecordSetters; 152 | 153 | REQUIRE(std::is_same_v); 154 | REQUIRE(std::is_same_v); 155 | REQUIRE(std::is_same_v); 156 | REQUIRE(std::is_same_v); 157 | 158 | REQUIRE(std::is_same_v, std::string>); 159 | REQUIRE(std::is_same_v, int>); 160 | REQUIRE(std::is_same_v, float>); 161 | REQUIRE(std::is_same_v, DefaultTimePoint_t>); 162 | 163 | const RecordMessage_t msg = "Hello, World!"; 164 | const RecordSeverity_t sev = 1337; 165 | const RecordChannel_t chan = 13.37f; 166 | const RecordTimePoint_t tPoint = DefaultTimePoint_t::clock::now(); 167 | 168 | Record_t record; 169 | Setters_t::setMessage(record, msg); 170 | Setters_t::setSeverity(record, sev); 171 | Setters_t::setChannel(record, chan); 172 | Setters_t::setTimePoint(record, tPoint); 173 | 174 | REQUIRE(Getters_t::message(record) == msg); 175 | REQUIRE(Getters_t::severity(record) == sev); 176 | REQUIRE(Getters_t::channel(record) == chan); 177 | REQUIRE(Getters_t::timePoint(record) == tPoint); 178 | } -------------------------------------------------------------------------------- /tests/StringPatternTest.cpp: -------------------------------------------------------------------------------- 1 | // Copyright Dominic Koepke 2021 - 2021. 2 | // Distributed under the Boost Software License, Version 1.0. 3 | // (See accompanying file LICENSE_1_0.txt or copy at 4 | // https://www.boost.org/LICENSE_1_0.txt) 5 | 6 | #include 7 | 8 | #include 9 | #include 10 | 11 | #include "Simple-Log/StringPattern.hpp" 12 | 13 | using namespace sl::log; 14 | using namespace Catch::Matchers; 15 | 16 | TEST_CASE("IncNumberGenerator generates consecutive numbers of minimum given width", "[StringPattern]") 17 | { 18 | detail::IncNumberGenerator incNoGen; 19 | 20 | REQUIRE(incNoGen.current == 1); 21 | REQUIRE(incNoGen.minWidth == 0); 22 | 23 | auto [current, width, expectedStr] = GENERATE( 24 | table 27 | ( 28 | { 29 | { 1, 0, "1" }, 30 | { 1, 1, "1" }, 31 | { 1, 2, "01" }, 32 | { 10, 0, "10" }, 33 | { 10, 3, "010" } 34 | } 35 | ) 36 | ); 37 | 38 | incNoGen.current = current; 39 | incNoGen.minWidth = width; 40 | 41 | std::ostringstream out; 42 | incNoGen(out); 43 | 44 | REQUIRE(out.str() == expectedStr); 45 | REQUIRE(incNoGen.current == current + 1); 46 | REQUIRE(incNoGen.minWidth == width); 47 | } 48 | 49 | TEST_CASE("StringGenerator generates constant string each time", "[StringPattern]") 50 | { 51 | detail::StringGenerator strGen; 52 | REQUIRE(std::empty(strGen.str)); 53 | 54 | auto str = GENERATE("", " ", "test", "Hello, World!"); 55 | strGen.str = str; 56 | 57 | std::ostringstream out; 58 | strGen(out); 59 | 60 | REQUIRE(out.str() == str); 61 | REQUIRE(strGen.str == str); 62 | } 63 | 64 | TEST_CASE("DateTimeGenerator generates string from current date-time", "[StringPattern]") 65 | { 66 | detail::DateTimeGenerator dateTimeGen; 67 | REQUIRE(std::empty(dateTimeGen.token)); 68 | 69 | auto [token, digits] = GENERATE( 70 | table( 72 | { 73 | { "", 0 }, 74 | {"%H", 2}, 75 | {"%M", 2}, 76 | {"%S", 2}, 77 | {"%Y", 4}, 78 | {"%m", 2}, 79 | {"%d", 2}, 80 | {"%j", 3} 81 | }) 82 | ); 83 | dateTimeGen.token = token; 84 | 85 | for (std::size_t i = 0; i < 5; ++i) 86 | { 87 | std::ostringstream out; 88 | dateTimeGen(out); 89 | 90 | REQUIRE(dateTimeGen.token == token); 91 | REQUIRE_THAT(out.str(), Matches("\\d{" + std::to_string(digits) + "}")); 92 | } 93 | } 94 | 95 | TEST_CASE("makeGeneratorFromMatch makes generators by token", "[StringPattern]") 96 | { 97 | SECTION("generating from constant string") 98 | { 99 | auto str = GENERATE("", " ", "test", "Hello, World!"); 100 | auto gen = detail::makeGeneratorFromMatch(str); 101 | 102 | auto actualGen = std::get_if(&gen); 103 | REQUIRE(actualGen); 104 | REQUIRE(actualGen->str == str); 105 | } 106 | 107 | auto checkIncNumberGen = [](const auto& variant, std::size_t width) 108 | { 109 | auto actualGen = std::get_if(&variant); 110 | REQUIRE(actualGen); 111 | REQUIRE(actualGen->current == 1); 112 | REQUIRE(actualGen->minWidth == width); 113 | }; 114 | 115 | SECTION("generating via %N token") 116 | { 117 | checkIncNumberGen(detail::makeGeneratorFromMatch("%N"), 0); 118 | } 119 | 120 | SECTION("generating via variations of %XN token") 121 | { 122 | auto width = GENERATE(0, 1, take(5, random(0, 100))); 123 | checkIncNumberGen(detail::makeGeneratorFromMatch("%" + std::to_string(width) + "N"), width); 124 | } 125 | 126 | SECTION("generating via date-time %XN token") 127 | { 128 | auto token = GENERATE("%H", "%M", "%S", "%Y", "%m", "%d", "%j"); 129 | auto gen = detail::makeGeneratorFromMatch(token); 130 | 131 | auto actualGen = std::get_if(&gen); 132 | REQUIRE(actualGen); 133 | REQUIRE(actualGen->token == token); 134 | } 135 | } 136 | 137 | TEST_CASE("makeTokenGeneratorsFromPatternString makes one or multiple generators from string pattern", "[StringPattern]") 138 | { 139 | SECTION("generate from empty string") 140 | { 141 | auto gens = detail::makeTokenGeneratorsFromPatternString(""); 142 | REQUIRE(std::empty(gens)); 143 | } 144 | 145 | SECTION("generating from string without any token") 146 | { 147 | auto gens = detail::makeTokenGeneratorsFromPatternString("Hello, World!"); 148 | REQUIRE(std::size(gens) == 1); 149 | REQUIRE(std::holds_alternative(gens.front())); 150 | } 151 | 152 | SECTION("generating from string with mixed date-time and string tokens") 153 | { 154 | auto gens = detail::makeTokenGeneratorsFromPatternString("Hello%M%H%S, World%Y%m%d%j!"); 155 | REQUIRE(std::size(gens) == 10); 156 | REQUIRE(std::holds_alternative(gens[0])); 157 | REQUIRE(std::holds_alternative(gens[1])); 158 | REQUIRE(std::holds_alternative(gens[2])); 159 | REQUIRE(std::holds_alternative(gens[3])); 160 | REQUIRE(std::holds_alternative(gens[4])); 161 | REQUIRE(std::holds_alternative(gens[5])); 162 | REQUIRE(std::holds_alternative(gens[6])); 163 | REQUIRE(std::holds_alternative(gens[7])); 164 | REQUIRE(std::holds_alternative(gens[8])); 165 | REQUIRE(std::holds_alternative(gens[9])); 166 | } 167 | 168 | SECTION("generating from string with mixed inc-number and string tokens") 169 | { 170 | auto gens = detail::makeTokenGeneratorsFromPatternString("Hello%N, World%1337N!"); 171 | REQUIRE(std::size(gens) == 5); 172 | REQUIRE(std::holds_alternative(gens[0])); 173 | REQUIRE(std::holds_alternative(gens[1])); 174 | REQUIRE(std::holds_alternative(gens[2])); 175 | REQUIRE(std::holds_alternative(gens[3])); 176 | REQUIRE(std::holds_alternative(gens[4])); 177 | } 178 | } 179 | 180 | TEST_CASE("StringPattern invocation produces strings, which will be generated on the fly on each invokation", "[StringPattern]") 181 | { 182 | SECTION("generating from constant pattern") 183 | { 184 | auto str = GENERATE("", " ", "test", "Hello, World!"); 185 | StringPattern pattern{ str }; 186 | 187 | for (std::size_t i = 0; i < 5; ++i) 188 | { 189 | REQUIRE(pattern.next() == str); 190 | } 191 | } 192 | 193 | SECTION("generating from string with tokens") 194 | { 195 | using namespace std::string_literals; 196 | auto pattern = GENERATE("%N", ".%N", "%Ntest%N", "%NHello%N%N,World!%N"); 197 | StringPattern patternedGen{ pattern }; 198 | std::regex regex{ "%N" }; 199 | for (std::size_t i = 1; i <= 5; ++i) 200 | { 201 | auto expectedStr = std::regex_replace( 202 | pattern, 203 | regex, 204 | std::to_string(i) 205 | ); 206 | REQUIRE(patternedGen.next() == expectedStr); 207 | } 208 | } 209 | } 210 | 211 | TEST_CASE("StringPattern should return strings as given to setter.", "[StringPattern]") 212 | { 213 | std::string str = GENERATE("", " ", "test", "Hello, World!"); 214 | StringPattern pattern{ str }; 215 | 216 | REQUIRE(pattern.patternString() == str); 217 | } 218 | -------------------------------------------------------------------------------- /tests/TupleAlgorithmTests.cpp: -------------------------------------------------------------------------------- 1 | // Copyright Dominic Koepke 2021 - 2021. 2 | // Distributed under the Boost Software License, Version 1.0. 3 | // (See accompanying file LICENSE_1_0.txt or copy at 4 | // https://www.boost.org/LICENSE_1_0.txt) 5 | 6 | #include 7 | 8 | #include 9 | 10 | #include "Simple-Log/Filters.hpp" 11 | #include "Simple-Log/Record.hpp" 12 | 13 | using namespace sl::log; 14 | 15 | using Record_t = BaseRecord; 16 | using Func_t = std::function; 17 | 18 | constexpr auto trueCb = [](auto& rec) { return true; }; 19 | constexpr auto falseCb = [](auto& rec) { return false; }; 20 | inline const Record_t record; 21 | 22 | template 23 | bool testAlgorithm(TElements&&... elements) 24 | { 25 | auto tuple = std::tie(std::forward(elements)...); 26 | return std::invoke(TAlgorithm{}, tuple, record); 27 | } 28 | 29 | struct VariadicAnd 30 | { 31 | template 32 | bool operator ()(TElements&&... elements) const 33 | { 34 | return (true && ... && elements(record)); 35 | } 36 | }; 37 | 38 | struct VariadicOr 39 | { 40 | template 41 | bool operator ()(TElements&&... elements) const 42 | { 43 | return (false || ... || elements(record)); 44 | } 45 | }; 46 | 47 | struct VariadicNone 48 | { 49 | template 50 | bool operator ()(TElements&&... elements) const 51 | { 52 | return !(false || ... || elements(record)); 53 | } 54 | }; 55 | 56 | TEMPLATE_TEST_CASE( 57 | "Testing tuple algorithms invoking with different amount of invocable objects", 58 | "[Tuple_Algorithms]", 59 | (std::tuple), 60 | (std::tuple), 61 | (std::tuple) 62 | ) 63 | { 64 | using Algo_t = std::tuple_element_t<0, TestType>; 65 | using BoolCombine_t = std::tuple_element_t<1, TestType>; 66 | BoolCombine_t combine; 67 | 68 | SECTION("invoking with zero elements") 69 | { 70 | REQUIRE(testAlgorithm() == combine()); 71 | } 72 | 73 | SECTION("invoking with one element") 74 | { 75 | auto first = GENERATE(as{}, trueCb, falseCb); 76 | auto expectedResult = combine(first); 77 | REQUIRE(testAlgorithm(first) == expectedResult); 78 | } 79 | 80 | SECTION("invoking with two elements") 81 | { 82 | auto first = GENERATE(as{}, trueCb, falseCb); 83 | auto second = GENERATE(as{}, trueCb, falseCb); 84 | auto expectedResult = combine(first, second); 85 | REQUIRE(testAlgorithm(first, second) == expectedResult); 86 | } 87 | 88 | SECTION("invoking with three elements") 89 | { 90 | auto first = GENERATE(as{}, trueCb, falseCb); 91 | auto second = GENERATE(as{}, trueCb, falseCb); 92 | auto three = GENERATE(as{}, trueCb, falseCb); 93 | auto expectedResult = combine(first, second, three); 94 | REQUIRE(testAlgorithm(first, second, three) == expectedResult); 95 | } 96 | 97 | SECTION("invoking with four elements") 98 | { 99 | auto first = GENERATE(as{}, trueCb, falseCb); 100 | auto second = GENERATE(as{}, trueCb, falseCb); 101 | auto three = GENERATE(as{}, trueCb, falseCb); 102 | auto four = GENERATE(as{}, trueCb, falseCb); 103 | auto expectedResult = combine(first, second, three, four); 104 | REQUIRE(testAlgorithm(first, second, three, four) == expectedResult); 105 | } 106 | 107 | SECTION("invoking with five elements") 108 | { 109 | auto first = GENERATE(as{}, trueCb, falseCb); 110 | auto second = GENERATE(as{}, trueCb, falseCb); 111 | auto three = GENERATE(as{}, trueCb, falseCb); 112 | auto four = GENERATE(as{}, trueCb, falseCb); 113 | auto five = GENERATE(as{}, trueCb, falseCb); 114 | auto expectedResult = combine(first, second, three, four, five); 115 | REQUIRE(testAlgorithm(first, second, three, four, five) == expectedResult); 116 | } 117 | } 118 | 119 | TEST_CASE("TupleForEach should invoke the given function with each element of the passed tuple instance", "[Tuple_Algorithms]") 120 | { 121 | std::tuple tuple{ 1, 2, 3, 4, 5 }; 122 | int invocationCounter = 0; 123 | detail::TupleForEach{}(tuple, [&invocationCounter](auto x) { ++invocationCounter; }); 124 | 125 | REQUIRE(invocationCounter == std::tuple_size_v); 126 | } 127 | -------------------------------------------------------------------------------- /tests/main.cpp: -------------------------------------------------------------------------------- 1 | // Copyright Dominic Koepke 2020 - 2020. 2 | // Distributed under the Boost Software License, Version 1.0. 3 | // (See accompanying file LICENSE_1_0.txt or copy at 4 | // https://www.boost.org/LICENSE_1_0.txt) 5 | 6 | #define CATCH_CONFIG_RUNNER 7 | #include 8 | 9 | int main(int argc, char** argv) 10 | { 11 | return Catch::Session().run(argc, argv); 12 | } 13 | --------------------------------------------------------------------------------