C++自定义日志记录实现过程详细介绍

作者:微信公众号:【架构师老卢】
4-29 9:56
28

概述:在用 C++ 编写自己的 Web 服务器时,我只是决定我需要自己的日志记录库,因为嘿,为什么不呢?可以说,踏上用 C++ 编写日志库的旅程是_一个有趣的_选择。有人甚至可能会争辩说这是一种受虐狂的练习。但在这里,我在战壕里,除了我的智慧之外,什么都没有,对宏的依赖值得怀疑,并且顽固地拒绝使用现成的解决方案。记录器界面:杰作?我英勇努力的核心是 Logger 界面。这是我的纸牌屋的骨架,我的意思是,堡垒。瞧瞧它的壮丽:#ifndef LOGGER_H_#define LOGGER_H_#include iostream#include string#include chrono#include

在用 C++ 编写自己的 Web 服务器时,我只是决定我需要自己的日志记录库,因为嘿,为什么不呢?可以说,踏上用 C++ 编写日志库的旅程是_一个有趣的_选择。有人甚至可能会争辩说这是一种受虐狂的练习。但在这里,我在战壕里,除了我的智慧之外,什么都没有,对宏的依赖值得怀疑,并且顽固地拒绝使用现成的解决方案。

核心是 Logger 界面:

#ifndef LOGGER_H_
#define LOGGER_H_

#include <iostream>
#include <string>
#include <chrono>
#include <ctime>
#include <iomanip>
#include <fstream>
#include <mutex>

namespace logtard // logger + ret4rded
{

    // Define the LogLevel enum to specify the severity of the log messages.
    enum class LogLevel
    {
        DEBUG,
        INFO,
        WARNING,
        ERROR,
        CRITICAL
    };

    // Logger base class with virtual methods for logging messages at different severity levels.
    class Logger
    {
    protected:
        std::mutex logMutex; // we don't want any race conditions, do we?
        std::string logLevelToString(LogLevel level);

    public:
        virtual ~Logger() {}
        virtual void log(const std::string &message, LogLevel level, const char *file, int line) = 0;
#define LOG(logger, level, message) (logger).log(message, level, __FILE__, __LINE__)
#define LOG_DEBUG(logger, message) LOG(logger, logtard::LogLevel::DEBUG, message)
#define LOG_INFO(logger, message) LOG(logger, logtard::LogLevel::INFO, message)
#define LOG_WARNING(logger, message) LOG(logger, logtard::LogLevel::WARNING, message)
#define LOG_ERROR(logger, message) LOG(logger, logtard::LogLevel::ERROR, message)
#define LOG_CRITICAL(logger, message) LOG(logger, logtard::LogLevel::CRITICAL, message)
    };

} // namespace logtard

#endif // LOGGER_H_

然后,因为我显然喜欢自相矛盾,所以我引入了宏。还记得反对使用宏的热情咆哮吗?那好吧......

这些宏是名副其实的潘多拉魔盒,确保每条消息都标有其源文件和行号。因为当调试变得有趣时,为什么要让它变得_简单_呢?

使用 ConsoleLogger 和 FileLogger 实现 Logger 接口

欢迎来到 logtard 的宏大叙事,ConsoleLoggerFileLogger 假装是我们从来不知道我们需要的超级英雄,肩负着将我们混乱的日志排成一列的艰巨职责。它们在设计时牢记了 SOLID 原则,即“我遵循了说明”的技术术语,确保这些伐木工不仅完成他们的工作——他们以一种优越的氛围来完成工作。

ConsoleLogger:终端的喧嚣

ConsoleLogger 是我们在黑暗中的光,引导我们...颜色。因为没有什么比红色粗体消息更能说明“关键系统故障”了,对吧?这不仅仅是记录消息;这是关于发表声明,在单调的终端窗口上画彩虹。

void ConsoleLogger::log(const std::string &message, logtard::LogLevel level, const char *file, int line) {
 // Here, we protect our precious log operation with a mutex, because heaven forbid two messages try to print at the same time.
 std::lock_guard<std::mutex> guard(logMutex);
// And then, we dress our logs in the color-coded attire of seriousness or doom.
}

FileLogger:我们坚持的历史学家

然后是FileLogger,像法庭抄写员一样尽职尽责地记录每一个事件,因为有一天肯定会有人阅读数千行日志。它将我们的话语投入到某个被遗忘的目录的永恒空白中,确保我们的数字沉思被保存下来供后代使用——或者直到磁盘填满。

FileLogger::FileLogger(const std::string &path) {
 // Opens the file in append mode because, obviously, we'd never want to overwrite our precious log history.
 logFile.open(path, std::ios::out | std::ios::app);
 if (!logFile.is_open()) {
 std::cerr << "Failed to open log file: " << path << std::endl;
 // Just a casual note to let you know everything's gone south.
 }
}

据称实施SOLID原则

部署这些日志记录美德的典范非常简单:

int main() {
 logtard::ConsoleLogger consoleLogger;
 // Because why log to a file when you can spam the console?
LOG_INFO(consoleLogger, "This is an informational message.");
 // And off we go, logging messages into the abyss.
}

据称,通过坚持 SOLID 原则,我确保每个记录器都像猫在激光笔上一样专注于其任务,它们像加长豪华轿车一样可扩展,并且像电池一样可互换。这种方法不仅使使用 ConsoleLoggerFileLogge变得轻而易举;据说它为我的日志框架注入了无与伦比的健壮性和灵活性——因为,当然,这是一直以来的计划。

CMake 设置:必要的邪恶

继续前进,我们有了 CMake——我们勉强尊重它在构建和管理依赖项方面的实用性的工具。这个图书馆的CMakeLists.txt是我雄心壮志(或者也许是我过度自信)的纪念碑:

cmake_minimum_required(VERSION 3.10)
project(logtard VERSION 1.0)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED True)

file(GLOB SOURCES "src/*.cpp")

# Choose one of the following depending on your needs
add_library(logtard STATIC ${SOURCES})
# add_library(logtard SHARED ${SOURCES})

target_include_directories(logtard PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include)

# Optional: Set version properties for shared libraries
if(BUILD_SHARED_LIBS)
    set_target_properties(logtard PROPERTIES
        VERSION ${PROJECT_VERSION}
        SOVERSION ${PROJECT_VERSION_MAJOR}
    )
endif()

# Optional: Install rules
install(TARGETS logtard
    ARCHIVE DESTINATION lib
    LIBRARY DESTINATION lib
    RUNTIME DESTINATION bin # For Windows DLLs
)
install(DIRECTORY include/ DESTINATION include)

# Enable testing with CMake
enable_testing()

# Include the directory where the test code is located
add_subdirectory(tests)

由于我将使用 Google Test,我还需要在 /tests 文件夹中设置另一个CMakeLists.txt(两倍的痛苦,是的......

cmake_minimum_required(VERSION 3.14)
project(logtard_test)

# Set the C++ standard you're using
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED True)

# Include FetchContent module
include(FetchContent)

# For Windows: Prevent overriding the parent project's compiler/linker settings
set(gtest_force_shared_crt ON CACHE BOOL "" FORCE)

set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>DLL")

# Declare GoogleTest as a fetch content
FetchContent_Declare(
  googletest
  URL https://github.com/google/googletest/archive/refs/tags/release-1.11.0.zip
)

# Make GoogleTest available for use
FetchContent_MakeAvailable(googletest)

# Now you can safely link Google Test's targets
add_executable(logtard_test console_logger_test.cpp file_logger_test.cpp)  # Replace some_test_file.cpp with your actual test file(s)
target_link_libraries(logtard_test gtest_main logtard) # Assuming 'logtard' is your library's target name

# If your project or tests require threading, link against Threads::Threads
find_package(Threads REQUIRED)
target_link_libraries(logtard_test Threads::Threads)

# Add the tests to CMake's testing framework
include(GoogleTest)
gtest_discover_tests(logtard_test)

通过这一点,我们可以唤起谷歌测试的精神,召唤他们来评估我们创造的价值。对于没有一套测试来证明其勇气的日志库来说,什么是?

单元测试

哦,喜悦!是时候使用非常直观的 Google 测试框架编写单元测试了。因为,众所周知,一款精致软件的真正标志不是它的性能或可用性,而是它有一个让直升机父母感到自豪的单元测试覆盖率。

设置的神圣仪式

首先,让我们执行设置测试环境的神圣仪式。在这里,我们慷慨地劫持了 std::cout,因为当您可以将其重新路由到 void(或 std::stringstream,但我们不要分裂头发)时,谁需要控制台输出到实际控制台。

class ConsoleLoggerTest : public ::testing::Test {
  protected:
   std::stringstream buffer; // The void awaits
   std::streambuf *sbuf; // The keeper of the original cout destination
   logtard::ConsoleLogger logger; // Our unsuspecting test subject
  void SetUp() override {
   sbuf = std::cout.rdbuf(); // Remember where cout used to go
   std::cout.rdbuf(buffer.rdbuf()); // Now it goes to the void
   }
  void TearDown() override {
   std::cout.rdbuf(sbuf); // Return cout to its rightful owner
   }
};

测试

现在,进入主要事件:编写测试如此微不足道,它们几乎无法证明自己的存在。首先,让我们确保我们的 ConsoleLogger 实际上可以记录具有 DEBUG 级别的消息。开创性的,我知道。

TEST_F(ConsoleLoggerTest, LogsDebugMessage) {
 logger.log("Test message", logtard::LogLevel::DEBUG, "test_file.cpp", 42);
// The string magic should happen here
 EXPECT_NE(buffer.str().find("[DEBUG]"), std::string::npos);
 EXPECT_NE(buffer.str().find("Test message"), std::string::npos);
 EXPECT_NE(buffer.str().find("test_file.cpp:42"), std::string::npos);
}

接下来,因为我们是受虐狂,让我们也测试一下 INFO 级别。这一次,没有文件或线路信息——因为显然,我们想看看我们的记录器是否尊重隐私。

TEST_F(ConsoleLoggerTest, LogsInfoMessage) {
 logger.log("Another test message", logtard::LogLevel::INFO, nullptr, 0);
// Here we go again
 EXPECT_NE(buffer.str().find("[INFO]"), std::string::npos);
 EXPECT_NE(buffer.str().find("Another test message"), std::string::npos);
}

拥抱混沌

通过这些测试,我们开始了一场惊心动魄的冒险,以确认是的,我们的 ConsoleLogger 做了它应该做的事情。 多么令人振奋!现在,你也可以沉浸在知道当你打电话给logger.log时,它可能会做一些类似于日志记录的事情。恭喜你,你已经掌握了用谷歌测试测试显而易见的艺术。 随意庆祝一下,想想你本来可以花掉时间的无数种方式。

在压力下确保优雅

当涉及到在多线程环境中进行登录时,确保记录器能够跟上同时请求的步伐,而不会绊倒自己的脚,这类似于编排芭蕾舞。每个舞者(或线程,如果你愿意的话)必须完美地表演他们的角色,与其他舞者完美和谐,否则就会冒着整个表演的风险(阅读:应用程序稳定性)。因此,我们的 FileLogger 的竞争条件测试不仅仅是一个测试;这是对我们记录器在最强烈压力下保持镇定能力的严格检查。

搭建舞台

我们的舞台设置了一个 FileLogger,而不仅仅是任何记录器,而是一个即将面临火力考验(或线程,更准确地说)的记录器。在这里,我们介绍十个线程,每个线程都发挥着重要作用:

const int numberOfThreads = 10;  
logtard::FileLogger logger(tempFilePath);

演出开始

随着帷幕的升起,我们的线开始跳舞,每根线都调用“日志”方法一百次。这种重复的动作不仅仅是为了表演;它模拟了真实世界应用程序的无情需求,其中日志条目与我们最勤奋的舞者的脚步一样频繁。

auto logAction = [&logger](int threadId) {
for (int j = 0; j < 100; ++j) {
logger.log("Message " + std::to_string(j) + " from thread " + std::to_string(threadId), logtard::LogLevel::INFO, __FILE__, __LINE__);
}
};

和谐还是浩劫?

当我们的线穿梭时,问题就变成了:他们会保持和谐,还是会陷入混乱?换句话说,我们的FileLogger能否在不丢失任何步骤的情况下处理并发访问?还是日志条目?

关键时刻

随着表演的完成,是时候进入关键时刻了。我们搜索日志文件,寻找任何迹象表明我们的线程已经动摇,甚至在战斗中丢失了一条消息:

auto logAction = [&logger](int threadId) {
 for (int j = 0; j < 100; ++j) {
 logger.log("Message " + std::to_string(j) + " from thread " + std::to_string(threadId), logtard::LogLevel::INFO, __FILE__, __LINE__);
 }
};

完美无瑕的结局还是摇摇欲坠的结局?

我们的 FileLogger 的完整性,以及我们的日志记录库的结构,都悬而未决。每条线索的声音都会被听到,还是一些信息会消失在虚空中,成为种族条件残酷异想天开的受害者?

在此测试中,我们不仅要确认日志记录库的功能;我们力求肯定其可靠性、稳健性和在压力下的表现能力。因为归根结底,当执行的线索交织在一起时,一个无法坚定不移的记录者根本不是记录者,而只是并发芭蕾舞的旁观者。

使用 GitHub Actions 和 CMake 假装知道 CI 的高级艺术

勇敢的灵魂,欢迎来到在 GitHub Actions 上为您的 CMake 项目设置持续集成的惊心动魄的冒险。因为,很明显,你的项目需要的不是更多的功能或错误修复,而是一个复杂的CI管道,让你看起来非常专业,而且一点也不像是在拖延实际的开发工作。

仪式开始

首先,让我们给这个工作流程起一个名字,尖叫着“我读了手册!”:CMake在多个平台上。这个名字对你的合作者(和你自己)来说是一个微妙的暗示,是的,你确实的目标是统治世界,一次一个构建。

name: CMake on multiple platforms

现在,让我们定义何时应该进行这种庄严的仪式。显然,只对分支的推送和拉取请求,因为谁在乎其他分支,对吧?它们可能只是永远不会见到曙光的功能分支。当然,我们不想在更改 Markdown 文件时浪费资源,不是吗?

on:
  push:
    branches: ["main"]
    paths-ignore:
      - "**/*.md"
  pull_request:
    branches: ["main"]
    paths-ignore:
      - "**/*.md"

盛大配置舞蹈

看哪,可能性的矩阵!在这里,我们将定义操作系统和编译器的神圣组合,以确保我们的构建像开发人员在技术聚会上吹嘘一样跨平台。

jobs:
 build:
 runs-on: ${{ matrix.os }}
 strategy:
 fail-fast: false
 matrix:
 os: [ubuntu-latest, windows-latest]
 build_type: [Release]

惊叹于包含适用于 Linux 的 GCC 和 Clang,以及适用于 Windows 的 MSVC。因为,很明显,只在一个编译器上进行测试是为业余爱好者准备的,如果不彻底(或过度自信),我们什么都不是。

启蒙的步骤

1. 签出:首先,我们克隆我们的存储库,因为它不像 GitHub Actions 知道我们正在尝试构建*这个*存储库,对吧?

2. 设置可重用的字符串:在这里,我们执行神秘的咒语将我们的构建目录路径存储在环境变量中,因为多次输入它是为平民准备的。

3. 配置 CMake:现在,我们从深处召唤 CMake 来配置我们的构建。在此步骤中,您可以假装了解从 Stack Overflow 复制的所有 CMake 选项。

- name: Configure CMake
 run: >
 cmake -B ${{ steps.strings.outputs.build-output-dir }}
 -DCMAKE_BUILD_TYPE=${{ matrix.build_type }}
 -S ${{ github.workspace }}

4. 构建:挥动魔杖(或键盘),我们编译代码,交叉手指,我们没有错过某个地方的分号。

- name: Build
 run: cmake - build ${{ steps.strings.outputs.build-output-dir }} - config ${{ matrix.build_type }}

5. 测试:最后,我们使用 ctest 运行测试,因为如果不自动验证您的代码是否像您在 LinkedIn 上声称的那样完美无缺,那么 CI 管道是什么?

- name: Test
 working-directory: ${{ steps.strings.outputs.build-output-dir }}
 run: ctest - build-config ${{ matrix.build_type }}

故事的寓意

这就是 GitHub Actions 工作流程,它不仅可以跨多个平台构建您的项目,还可以让您放心,是的,您确实是一位接受 CI 最佳实践的现代开发人员......即使您宁愿编写代码也不愿摆弄 YAML 文件。

未来是光明的:我可能会接触到的待办事项和“功能”

哦,你以为就是这样吗?这个日志记录库是完美的巅峰之作?多么古朴。不要害怕,因为我有积压的待办事项和潜在的“增强功能”,在可预见的未来,它们肯定会让我忙碌(甚至可能感到沮丧)。让我们深入了解这个愿望清单,好吗?

性能基准:因为我们热爱数字

首先,我可能会考虑建立一些性能基准。因为没有什么比细致地测量我们的日志记录操作在高压力场景中如何影响应用程序性能更能尖叫“我有太多空闲时间”了。毕竟,我不希望我的日志消息成为压垮骆驼的最后一根稻草。

功能“增强”(或者,如何使你的生活过于复杂)

日志轮换:啊,日志占用太多空间的老问题。我可能会为我的文件记录器实现日志轮换,这样您就不必在磁盘求饶时手动删除日志。但话又说回来,体力劳动可以塑造性格,对吧?

自定义格式支持:因为谁不喜欢花费数小时调整日志消息格式?我会考虑让你定义自定义格式,包括时间戳以及你是否要查看文件名和行号。因为很明显,每个人的日志记录偏好都像雪花一样独特。

异步日志记录:为了不减慢应用程序速度,我可能会涉足异步日志记录。这样一来,您的日志消息就可以悄悄地排队等待以后被忽略,而不会影响应用的性能。

对结构化日志的支持:最后但并非最不重要的一点是,对于那些梦想使用 JSON 的人,我们正在考虑添加对结构化日志记录的支持。这样一来,您就可以像大海捞针一样轻松地筛选日志,前提是您拥有正确的日志管理工具,这些工具的成本可能超过您的整个 IT 预算。

所以你有它——一个充满我希望遵守的承诺的路线图(但没有保证)。毕竟,我是开发人员,不是魔术师。请继续关注,谁知道呢?也许有一天,你会醒来发现我实际上已经实现了这些功能之一。在那之前,请继续以老式的方式记录,并记住:如果所有其他方法都失败了,那么总会有 printf

阅读排行