在Linux系统开发中,日志系统是每个开发者都绕不开的基础设施。一个设计良好的日志系统不仅能帮助我们快速定位问题,还能记录系统运行状态,为后续的性能分析和优化提供数据支持。今天我要分享的是如何从零开始实现一个轻量级但功能完备的日志系统。
这个日志系统采用了策略模式设计,支持两种日志输出方式:控制台输出和文件输出。它具备以下特点:
策略模式是我们这个日志系统的核心设计模式。它的主要思想是将算法(在这里是日志的输出策略)封装成独立的类,使得它们可以相互替换而不影响客户端代码。
在我们的实现中:
LogStrategy是抽象基类,定义了日志同步的接口ConsoleLogStrategy实现了向控制台输出的策略FileLogStrategy实现了向文件输出的策略这种设计带来的好处是:
一个良好的日志格式应该包含足够的信息,同时保持可读性。我们设计的日志格式如下:
code复制[2024-08-04 12:27:03] [DEBUG] [202938] [main.cc] [16] - hello world
各字段说明:
提示:在实际项目中,可以根据需要调整日志格式,比如添加线程ID、模块名等信息。
在多线程环境下,日志系统必须是线程安全的。我们使用互斥锁来保证这一点:
cpp复制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;
};
使用RAII技术的LockGuard确保在任何情况下锁都会被正确释放,避免了死锁问题。
cpp复制class ConsoleLogStrategy : public LogStrategy {
public:
void SyncLog(const string &message) override {
LockGuard lockguard(_mutex);
cout << message << "\r\n";
}
private:
Mutex _mutex;
};
注意:即使是向控制台输出也需要加锁,因为多个线程同时输出会导致日志信息交错。
文件输出策略稍微复杂一些,需要处理文件路径的创建和文件的打开:
cpp复制class FileLogStrategy : public LogStrategy {
public:
FileLogStrategy(const string &path = "./log", const string &file = "my.log")
: _path(path), _file(file) {
LockGuard lockguard(_mutex);
if (!filesystem::exists(_path))
filesystem::create_directories(_path);
}
void SyncLog(const string &message) override {
string fullPath = _path + (_path.back() == '/' ? "" : "/") + _file;
ofstream out(fullPath, ios::app);
out << message << "\r\n";
}
private:
string _path;
string _file;
Mutex _mutex;
};
这里使用了C++17的filesystem库来处理路径操作,比传统的C函数更加安全和方便。
日志消息的构建使用了流式接口,通过重载<<运算符实现:
cpp复制class LogMessage {
public:
template <class T>
LogMessage &operator<<(const T &message) {
stringstream ss;
ss << message;
_loginfo += ss.str();
return *this;
}
~LogMessage() {
_logger.Strategy->SyncLog(_loginfo);
}
private:
string _loginfo;
Logger &_logger;
};
这种设计使得日志记录可以像使用标准输出流一样自然:
cpp复制LOG(LogLevel::DEBUG) << "User " << username << " logged in from " << ip;
我们定义了5种日志级别:
cpp复制enum class LogLevel {
DEBUG, // 调试信息
INFO, // 普通信息
WARNING, // 警告
ERROR, // 错误
FATAL // 致命错误
};
在实际项目中,可以通过编译时宏定义来控制哪些级别的日志会被实际输出,这在发布版本中可以减少不必要的日志开销。
日志系统虽然不应该是性能瓶颈,但也需要注意以下几点:
FileLogStrategy中保持文件句柄打开生产环境中还需要考虑日志轮转(log rotation)的问题,防止日志文件无限增长。可以通过以下方式实现:
以下是完整的日志系统实现,分为两个头文件:
cpp复制#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;
};
cpp复制#pragma once
#include <iostream>
#include <sstream>
#include <filesystem>
#include <fstream>
#include <sys/types.h>
#include <unistd.h>
#include "Mutex.hpp"
using namespace std;
namespace LogModule {
// 日志等级
enum class LogLevel {
DEBUG, INFO, WARNING, ERROR, FATAL
};
string LevelToStr(LogLevel level) {
switch (level) {
case LogLevel::DEBUG: return "DEBUG";
case LogLevel::INFO: return "INFO";
case LogLevel::WARNING: return "WARNING";
case LogLevel::ERROR: return "ERROR";
case LogLevel::FATAL: return "FATAL";
default: return "UNKNOWN";
}
}
string GetTime() {
time_t curr = time(nullptr);
struct tm curr_time;
localtime_r(&curr, &curr_time);
char buffer[128];
snprintf(buffer, sizeof(buffer), "%04d-%02d-%02d %02d:%02d:%02d",
curr_time.tm_year + 1900,
curr_time.tm_mon + 1,
curr_time.tm_mday,
curr_time.tm_hour,
curr_time.tm_min,
curr_time.tm_sec);
return buffer;
}
class LogStrategy {
public:
virtual void SyncLog(const string &message) = 0;
};
class ConsoleLogStrategy : public LogStrategy {
public:
void SyncLog(const string &message) override {
LockGuard lockguard(_mutex);
cout << message << "\r\n";
}
private:
Mutex _mutex;
};
class FileLogStrategy : public LogStrategy {
public:
FileLogStrategy(const string &path = "./log", const string &file = "my.log")
: _path(path), _file(file) {
LockGuard lockguard(_mutex);
if (!filesystem::exists(_path))
filesystem::create_directories(_path);
}
void SyncLog(const string &message) override {
string fullPath = _path + (_path.back() == '/' ? "" : "/") + _file;
ofstream out(fullPath, ios::app);
out << message << "\r\n";
}
private:
string _path;
string _file;
Mutex _mutex;
};
class Logger {
public:
Logger() {
Strategy = make_unique<ConsoleLogStrategy>();
}
void EnableConsoleLogStrategy() {
Strategy = make_unique<ConsoleLogStrategy>();
}
void EnableFileLogStrategy() {
Strategy = make_unique<FileLogStrategy>();
}
class LogMessage {
public:
LogMessage(const LogLevel &level, const string &name, const int &line, Logger &logger)
: _level(level), _name(name), _logger(logger), _line(line) {
_pid = getpid();
_time = GetTime();
stringstream ss;
ss << "[" << _time << "] "
<< "[" << LevelToStr(_level) << "] "
<< "[" << _pid << "] "
<< "[" << _name << "] "
<< "[" << _line << "] - ";
_loginfo = ss.str();
}
template <class T>
LogMessage &operator<<(const T &message) {
stringstream ss;
ss << message;
_loginfo += ss.str();
return *this;
}
~LogMessage() {
_logger.Strategy->SyncLog(_loginfo);
}
private:
string _time;
LogLevel _level;
pid_t _pid;
string _name;
int _line;
string _loginfo;
Logger &_logger;
};
LogMessage operator()(const LogLevel &level, const string &name, const int &line) {
return LogMessage(level, name, line, *this);
}
private:
unique_ptr<LogStrategy> Strategy;
};
extern Logger logger;
#define LOG(level) logger(level, __FILE__, __LINE__)
#define ENABLE_CONSOLE_LOG() logger.EnableConsoleLogStrategy()
#define ENABLE_FILE_LOG() logger.EnableFileLogStrategy()
}
cpp复制#include "Log.hpp"
using namespace LogModule;
int main() {
// 默认输出到控制台
LOG(LogLevel::INFO) << "Application started";
// 切换到文件输出
ENABLE_FILE_LOG();
LOG(LogLevel::DEBUG) << "Debug information";
LOG(LogLevel::ERROR) << "Something went wrong";
return 0;
}
cpp复制#include <thread>
#include <vector>
#include "Log.hpp"
using namespace LogModule;
void worker(int id) {
for (int i = 0; i < 10; ++i) {
LOG(LogLevel::INFO) << "Thread " << id << " count " << i;
}
}
int main() {
ENABLE_FILE_LOG();
vector<thread> threads;
for (int i = 0; i < 5; ++i) {
threads.emplace_back(worker, i);
}
for (auto &t : threads) {
t.join();
}
return 0;
}
这个基础日志系统还可以进一步扩展:
在实际项目中,我通常会根据具体需求对这些扩展点进行选择性实现。比如在需要集中管理日志的分布式系统中,网络日志功能就非常有用;而在性能敏感的场景下,则可能需要实现异步日志和采样功能。