1. 项目概述
日志系统是每个开发者都绕不开的基础设施组件。作为一个在Linux环境下摸爬滚打多年的老码农,我见过太多因为日志设计不当导致的"午夜惊魂"——凌晨三点被叫起来排查问题,结果发现日志要么缺失关键信息,要么格式混乱难以分析。今天我就带大家从零开始,用C++实现一个工业级可用的日志系统。
这个日志系统的核心特点包括:
- 采用策略模式实现灵活的日志输出方式(控制台/文件)
- 线程安全的日志写入机制
- 符合行业标准的日志格式
- 支持自动创建日志目录
- 简洁易用的API接口
2. 设计思路解析
2.1 为什么选择策略模式
策略模式的核心思想是将算法家族分别封装起来,让它们之间可以互相替换。这种模式让算法的变化独立于使用算法的客户。
在日志系统中,我们通常需要支持多种输出方式:
- 开发阶段:输出到控制台方便调试
- 生产环境:输出到文件便于长期保存
- 特殊场景:可能需要同时输出到控制台和文件
使用策略模式可以让我们在不修改核心日志逻辑的情况下,灵活切换不同的输出策略。这比用一堆if-else来判断输出方式要优雅得多。
2.2 线程安全设计考量
日志系统在多线程环境下必须保证线程安全,否则会出现日志内容错乱的问题。我们采用以下措施确保线程安全:
- 使用互斥锁保护共享资源(控制台/stdout和文件)
- 采用RAII技术管理锁的生命周期
- 每个策略类维护自己的锁,避免全局锁竞争
这里特别说明下RAII(Resource Acquisition Is Initialization)技术:通过在对象构造时获取资源,在析构时释放资源,确保资源不会泄漏。我们的LockGuard类就是典型的RAII实现。
3. 核心实现细节
3.1 基础锁封装
首先我们需要一个基础的互斥锁封装,这是保证线程安全的基础:
cpp复制// Mutex.hpp
#pragma once
#include <pthread.h>
class Mutex {
public:
Mutex() {
pthread_mutex_init(&mutex, nullptr);
}
~Mutex() {
pthread_mutex_destroy(&mutex);
}
void Lock() {
pthread_mutex_lock(&mutex);
}
void Unlock() {
pthread_mutex_unlock(&mutex);
}
private:
pthread_mutex_t mutex;
};
class LockGuard {
public:
LockGuard(Mutex &mutex) : _Mutex(mutex) {
_Mutex.Lock();
}
~LockGuard() {
_Mutex.Unlock();
}
private:
Mutex &_Mutex;
};
注意:锁对象本身不应该被拷贝,所以我们使用引用而不是值传递。这也是为什么LockGuard中保存的是Mutex的引用。
3.2 日志策略接口设计
定义策略模式的抽象基类:
cpp复制// LogStrategy.hpp
#include <string>
#include <memory>
const std::string end = "\r\n";
class LogStrategy {
public:
virtual ~LogStrategy() = default;
virtual void SyncLog(const std::string& message) = 0;
};
这个接口非常简单,只有一个纯虚函数SyncLog用于实际输出日志。所有具体的策略都需要实现这个接口。
3.3 控制台输出策略
cpp复制// ConsoleLogStrategy.hpp
#include "LogStrategy.hpp"
#include "Mutex.hpp"
#include <iostream>
class ConsoleLogStrategy : public LogStrategy {
public:
void SyncLog(const std::string& message) override {
LockGuard lockguard(_mutex);
std::cout << message << end;
}
private:
Mutex _mutex;
};
控制台输出相对简单,主要注意两点:
- 使用锁保证多线程安全
- 统一的行结束符处理
3.4 文件输出策略
文件输出策略要复杂一些,需要考虑以下问题:
cpp复制// FileLogStrategy.hpp
#include "LogStrategy.hpp"
#include "Mutex.hpp"
#include <filesystem>
#include <fstream>
const std::string defaultpath = "./log";
const std::string defaultfile = "my.log";
class FileLogStrategy : public LogStrategy {
public:
FileLogStrategy(const std::string& path = defaultpath,
const std::string& file = defaultfile)
: _path(path), _file(file) {
LockGuard lockguard(_mutex);
if (!std::filesystem::exists(_path)) {
std::filesystem::create_directories(_path);
}
}
void SyncLog(const std::string& message) override {
LockGuard lockguard(_mutex);
std::string fullPath = _path + (_path.back() == '/' ? "" : "/") + _file;
std::ofstream out(fullPath, std::ios::app);
out << message << end;
}
private:
std::string _path;
std::string _file;
Mutex _mutex;
};
关键点说明:
- 自动创建不存在的目录(使用C++17的filesystem)
- 路径拼接时处理可能的重复斜杠
- 以追加模式打开文件
- 同样使用锁保证线程安全
3.5 日志器主体实现
cpp复制// Logger.hpp
#include <memory>
#include "LogStrategy.hpp"
class Logger {
public:
Logger() {
SetConsoleStrategy();
}
void SetConsoleStrategy() {
_strategy = std::make_unique<ConsoleLogStrategy>();
}
void SetFileStrategy(const std::string& path = "./log",
const std::string& file = "my.log") {
_strategy = std::make_unique<FileLogStrategy>(path, file);
}
void Log(const std::string& message) {
_strategy->SyncLog(message);
}
private:
std::unique_ptr<LogStrategy> _strategy;
};
这个Logger类提供了简洁的接口:
- 默认使用控制台输出
- 可以随时切换输出策略
- 统一的Log接口记录日志
4. 日志格式化与扩展
4.1 标准日志格式
一个良好的日志格式应该包含以下信息:
- 时间戳(可读格式)
- 日志级别(DEBUG/INFO/WARNING/ERROR等)
- 进程/线程ID
- 源代码位置(文件名和行号)
- 实际日志消息
示例格式:
code复制[2024-08-04 12:27:03] [DEBUG] [202938] [main.cc] [16] - hello world
4.2 实现日志格式化
我们可以扩展Logger类,添加格式化功能:
cpp复制// 在Logger类中添加
enum class LogLevel {
DEBUG,
INFO,
WARNING,
ERROR
};
void Log(LogLevel level, const std::string& file, int line, const std::string& message) {
time_t now = time(nullptr);
tm* local = localtime(&now);
char timeStr[20];
strftime(timeStr, sizeof(timeStr), "%Y-%m-%d %H:%M:%S", local);
std::string levelStr;
switch(level) {
case LogLevel::DEBUG: levelStr = "DEBUG"; break;
case LogLevel::INFO: levelStr = "INFO"; break;
case LogLevel::WARNING: levelStr = "WARNING"; break;
case LogLevel::ERROR: levelStr = "ERROR"; break;
}
pid_t pid = getpid();
std::ostringstream oss;
oss << "[" << timeStr << "] "
<< "[" << levelStr << "] "
<< "[" << pid << "] "
<< "[" << file << "] "
<< "[" << line << "] - "
<< message;
_strategy->SyncLog(oss.str());
}
4.3 使用宏简化调用
为了更方便地使用,我们可以定义一组宏:
cpp复制#define LOG_DEBUG(message) Log(LogLevel::DEBUG, __FILE__, __LINE__, message)
#define LOG_INFO(message) Log(LogLevel::INFO, __FILE__, __LINE__, message)
#define LOG_WARNING(message) Log(LogLevel::WARNING, __FILE__, __LINE__, message)
#define LOG_ERROR(message) Log(LogLevel::ERROR, __FILE__, __LINE__, message)
这样使用时只需要:
cpp复制LOG_INFO("System initialized");
LOG_ERROR("Failed to open file: " + filename);
5. 性能优化与扩展
5.1 异步日志
当前的实现是同步日志,即每次调用Log都会立即写入。在高性能场景下,可以考虑实现异步日志:
- 使用生产者-消费者模型
- Log接口只将日志放入队列
- 单独的工作线程负责从队列取出日志并写入
这样可以减少I/O操作对主线程的影响。
5.2 日志滚动
当日志文件过大时,应该自动创建新的日志文件。常见的滚动策略:
- 按大小滚动(如超过100MB)
- 按时间滚动(如每天一个新文件)
5.3 日志级别过滤
在生产环境中,我们可能只想记录WARNING及以上级别的日志。可以添加级别过滤功能:
cpp复制void SetLogLevel(LogLevel level) {
_minLevel = level;
}
void Log(LogLevel level, ...) {
if (level < _minLevel) return;
// 原有实现
}
6. 实际使用示例
6.1 基本使用
cpp复制Logger logger;
logger.Log("This is a test message");
// 切换到文件输出
logger.SetFileStrategy("/var/log/myapp", "app.log");
logger.Log("This will go to file");
6.2 使用格式化日志
cpp复制Logger logger;
logger.SetFileStrategy();
LOG_INFO("Application started");
// 输出示例: [2024-08-04 12:27:03] [INFO] [12345] [main.cc] [10] - Application started
try {
// some operation
} catch (const std::exception& e) {
LOG_ERROR("Operation failed: " + std::string(e.what()));
}
7. 常见问题与解决方案
7.1 性能瓶颈
问题:日志写入成为性能瓶颈
解决方案:
- 实现异步日志
- 批量写入(攒够一定数量或时间再写入)
- 考虑使用更快的存储(如SSD)
7.2 日志丢失
问题:程序崩溃时最近的日志丢失
解决方案:
- 更频繁的flush操作(但会影响性能)
- 实现崩溃恢复机制
7.3 日志文件过大
问题:日志文件占用过多磁盘空间
解决方案:
- 实现日志滚动
- 添加日志压缩功能
- 设置日志保留策略(如只保留最近7天的日志)
8. 生产环境建议
在实际生产环境中使用这个日志系统时,建议:
- 根据应用特点选择合适的日志级别
- 为不同模块使用不同的日志文件
- 定期监控日志文件大小
- 实现日志报警机制(如ERROR日志发送邮件通知)
- 考虑集成到现有的日志收集系统(如ELK)
这个日志系统虽然简单,但包含了日志系统的核心功能。根据实际需求,你可以继续扩展它,比如添加网络日志输出、支持结构化日志(如JSON格式)、或者集成到更大的监控系统中。