microLog 后端开发指南
概述
microLog 采用抽象工厂模式 与门面模式进行架构设计,能够方便地扩展新的日志后端。本文档将逐步说明如何开发自定义后端实现,并提供完整的示例与最佳实践。
文中示例代码仅为展示原理,并没有进行编译测试
架构设计
#mermaid-svg-1AlTiiOoMoNhxPmb{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-1AlTiiOoMoNhxPmb .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-1AlTiiOoMoNhxPmb .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-1AlTiiOoMoNhxPmb .error-icon{fill:#552222;}#mermaid-svg-1AlTiiOoMoNhxPmb .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-1AlTiiOoMoNhxPmb .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-1AlTiiOoMoNhxPmb .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-1AlTiiOoMoNhxPmb .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-1AlTiiOoMoNhxPmb .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-1AlTiiOoMoNhxPmb .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-1AlTiiOoMoNhxPmb .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-1AlTiiOoMoNhxPmb .marker{fill:#333333;stroke:#333333;}#mermaid-svg-1AlTiiOoMoNhxPmb .marker.cross{stroke:#333333;}#mermaid-svg-1AlTiiOoMoNhxPmb svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-1AlTiiOoMoNhxPmb p{margin:0;}#mermaid-svg-1AlTiiOoMoNhxPmb .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-1AlTiiOoMoNhxPmb .cluster-label text{fill:#333;}#mermaid-svg-1AlTiiOoMoNhxPmb .cluster-label span{color:#333;}#mermaid-svg-1AlTiiOoMoNhxPmb .cluster-label span p{background-color:transparent;}#mermaid-svg-1AlTiiOoMoNhxPmb .label text,#mermaid-svg-1AlTiiOoMoNhxPmb span{fill:#333;color:#333;}#mermaid-svg-1AlTiiOoMoNhxPmb .node rect,#mermaid-svg-1AlTiiOoMoNhxPmb .node circle,#mermaid-svg-1AlTiiOoMoNhxPmb .node ellipse,#mermaid-svg-1AlTiiOoMoNhxPmb .node polygon,#mermaid-svg-1AlTiiOoMoNhxPmb .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-1AlTiiOoMoNhxPmb .rough-node .label text,#mermaid-svg-1AlTiiOoMoNhxPmb .node .label text,#mermaid-svg-1AlTiiOoMoNhxPmb .image-shape .label,#mermaid-svg-1AlTiiOoMoNhxPmb .icon-shape .label{text-anchor:middle;}#mermaid-svg-1AlTiiOoMoNhxPmb .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-1AlTiiOoMoNhxPmb .rough-node .label,#mermaid-svg-1AlTiiOoMoNhxPmb .node .label,#mermaid-svg-1AlTiiOoMoNhxPmb .image-shape .label,#mermaid-svg-1AlTiiOoMoNhxPmb .icon-shape .label{text-align:center;}#mermaid-svg-1AlTiiOoMoNhxPmb .node.clickable{cursor:pointer;}#mermaid-svg-1AlTiiOoMoNhxPmb .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-1AlTiiOoMoNhxPmb .arrowheadPath{fill:#333333;}#mermaid-svg-1AlTiiOoMoNhxPmb .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-1AlTiiOoMoNhxPmb .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-1AlTiiOoMoNhxPmb .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-1AlTiiOoMoNhxPmb .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-1AlTiiOoMoNhxPmb .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-1AlTiiOoMoNhxPmb .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-1AlTiiOoMoNhxPmb .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-1AlTiiOoMoNhxPmb .cluster text{fill:#333;}#mermaid-svg-1AlTiiOoMoNhxPmb .cluster span{color:#333;}#mermaid-svg-1AlTiiOoMoNhxPmb div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-1AlTiiOoMoNhxPmb .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-1AlTiiOoMoNhxPmb rect.text{fill:none;stroke-width:0;}#mermaid-svg-1AlTiiOoMoNhxPmb .icon-shape,#mermaid-svg-1AlTiiOoMoNhxPmb .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-1AlTiiOoMoNhxPmb .icon-shape p,#mermaid-svg-1AlTiiOoMoNhxPmb .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-1AlTiiOoMoNhxPmb .icon-shape .label rect,#mermaid-svg-1AlTiiOoMoNhxPmb .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-1AlTiiOoMoNhxPmb .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-1AlTiiOoMoNhxPmb .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-1AlTiiOoMoNhxPmb :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 持有 mutex、threadPool,格式化在调用线程完成
用户代码
auto log = createLog<tinyLogType::CUSTOM>(args...);
log->info("hello world");
LOG<Backend> 门面层(模板类)
后端层(自定义实现)
-
继承 logItfc
-
实现 vlog / get_fd__ / is_valid__
logFactoryHelper 抽象工厂
注册所有后端类型,通过索引创建实例
核心接口
logItfc 基类
所有后端必须继承 logItfc 并实现以下纯虚函数:
cpp
class logItfc {
public:
enum class level_t { DEBUG = 0, INFO = 1, WARN = 2, ERROR = 3 };
// 必须实现:日志记录核心方法
virtual void vlog(level_t level, const char* fmt, va_list args) = 0;
// 必须实现:获取文件描述符(无则返回 -1)
virtual int get_fd__() const = 0;
// 必须实现:检查后端是否有效
virtual bool is_valid__() const = 0;
protected:
// 可用的工具方法
const char* level_2_string__(level_t level); // 级别转字符串
std::string nowStr(); // 获取当前时间字符串
std::shared_ptr<wheels::threadPool> pt_runner__; // 线程池指针
};
开发步骤
步骤 1:定义后端枚举类型
在 include/log.hpp 的 tinyLogType 枚举中添加新类型:
cpp
enum class tinyLogType {
UDP = 0,
LOCAL = 1,
PIPE = 2,
UNIXSOCK = 3,
TERMINAL = 4,
TCP = 5,
SQLITE3 = 6,
MYSQL = 7,
POSTGRESQL = 8,
CUSTOM = 9, // 添加新类型,按顺序递增
};
步骤 2:创建后端头文件
创建 include/logDetail/custom_log.hpp:
cpp
#pragma once
#include <string>
#include <cstdint>
#include "logDetail/itfc.hpp"
namespace microLog {
class customLog : public logItfc {
private:
// 自定义成员变量
std::string m_config__;
bool m_inited__;
public:
// 构造函数:接收自定义参数
customLog(const std::string& config);
~customLog();
// 必须实现的接口
virtual int get_fd__() const override;
virtual bool is_valid__() const override;
virtual void vlog(level_t level, const char* fmt, va_list args) override;
// 自定义方法(可选)
bool init();
};
} // namespace microLog
步骤 3:实现后端
创建 src/logDetail/custom_log.cpp:
cpp
#include <cstdarg>
#include <cstdio>
#include "logDetail/custom_log.hpp"
namespace microLog {
customLog::customLog(const std::string& config)
: m_config__(config), m_inited__(false)
{
if (!init()) {
throw std::runtime_error("customLog 初始化失败");
}
}
customLog::~customLog() {
// 清理资源
}
bool customLog::init() {
// 初始化逻辑:连接资源、创建对象等
m_inited__ = true;
return true;
}
int customLog::get_fd__() const {
// 如果后端使用文件描述符,返回 fd;否则返回 -1
return -1;
}
bool customLog::is_valid__() const {
// 返回后端是否有效
return m_inited__;
}
void customLog::vlog(level_t level, const char* fmt, va_list args) {
if (!is_valid__()) { return; }
// 1. 获取时间戳
std::string ts = nowStr();
// 2. 获取级别字符串
const char* level_str = level_2_string__(level);
// 3. 格式化日志内容
char buf[4096] = {0};
va_list args_copy;
va_copy(args_copy, args);
vsnprintf(buf, sizeof(buf), fmt, args_copy);
va_end(args_copy);
// 4. 输出到自定义目标(示例:控制台)
printf("[%s] [%s] %s\n", ts.c_str(), level_str, buf);
}
} // namespace microLog
步骤 4:注册到抽象工厂
在 include/log.hpp 的 logFactoryHelper 中添加新后端:
cpp
#include "logDetail/custom_log.hpp" // 添加头文件
namespace microLog {
using logFactoryHelper = wheels::dm::abstractFactory<
LOG<udpLog>,
LOG<localLog>,
LOG<pipeLog>,
LOG<unixSock>,
LOG<terminal>,
LOG<tcpLog>,
LOG<sqlite3Log>,
LOG<mysqlLog>,
LOG<postgresqlLog>,
LOG<customLog> // 添加新后端
>;
}
步骤 5:配置 CMakeLists.txt
在 src/logDetail/CMakeLists.txt 中添加新源文件:
cmake
set(LOG_DETAIL_SRC
...
custom_log.cpp
...
)
如果新后端依赖外部库,需添加相应的 find_package 和 target_link_libraries。
使用示例
cpp
#include "log.hpp"
// 创建自定义后端实例
auto log = createLog<tinyLogType::CUSTOM>(4, "my_config");
// 使用日志
log->info("系统启动");
log->debug("调试信息: %d", 42);
log->warn("警告: 资源不足");
log->error("错误: %s", "连接失败");
完整示例:Redis 后端
头文件 redis_log.hpp
cpp
#pragma once
#include <string>
#include <cstdint>
#include "logDetail/itfc.hpp"
#ifdef HAVE_REDIS
# include <hiredis/hiredis.h>
#endif
namespace microLog {
class redisLog : public logItfc {
private:
#ifdef HAVE_REDIS
redisContext* m_ctx__;
#endif
std::string m_host__;
uint16_t m_port__;
std::string m_key__;
bool m_inited__;
public:
redisLog(const std::string& host = "localhost",
uint16_t port = 6379,
const std::string& key = "microLog");
~redisLog();
virtual int get_fd__() const override;
virtual bool is_valid__() const override;
virtual void vlog(level_t level, const char* fmt, va_list args) override;
};
} // namespace microLog
实现文件 redis_log.cpp
cpp
#include <cstdarg>
#include <cstdio>
#include <string>
#include "logDetail/redis_log.hpp"
namespace microLog {
redisLog::redisLog(const std::string& host, uint16_t port, const std::string& key)
: m_host__(host), m_port__(port), m_key__(key), m_inited__(false)
{
#ifdef HAVE_REDIS
m_ctx__ = redisConnect(host.c_str(), port);
if (!m_ctx__ || m_ctx__->err) {
if (m_ctx__) {
std::cerr << "Redis 连接失败: " << m_ctx__->errstr << std::endl;
redisFree(m_ctx__);
m_ctx__ = nullptr;
} else {
std::cerr << "Redis 连接失败: 内存分配错误" << std::endl;
}
return;
}
m_inited__ = true;
#else
throw std::runtime_error("Redis 支持未启用");
#endif
}
redisLog::~redisLog() {
#ifdef HAVE_REDIS
if (m_ctx__) {
redisFree(m_ctx__);
m_ctx__ = nullptr;
}
#endif
}
int redisLog::get_fd__() const {
return -1;
}
bool redisLog::is_valid__() const {
#ifdef HAVE_REDIS
return m_inited__ && m_ctx__ && !m_ctx__->err;
#else
return false;
#endif
}
void redisLog::vlog(level_t level, const char* fmt, va_list args) {
#ifdef HAVE_REDIS
if (!is_valid__()) { return; }
char buf[4096] = {0};
std::string ts = nowStr();
snprintf(buf, sizeof(buf), "[%s] [%s] ", ts.c_str(), level_2_string__(level));
char msg[4096] = {0};
va_list args_copy;
va_copy(args_copy, args);
vsnprintf(msg, sizeof(msg), fmt, args_copy);
va_end(args_copy);
std::string log_entry = std::string(buf) + msg;
redisReply* reply = (redisReply*)redisCommand(m_ctx__, "LPUSH %s %s",
m_key__.c_str(), log_entry.c_str());
if (reply) {
freeReplyObject(reply);
}
#endif
}
} // namespace microLog
最佳实践
1. 线程安全
若后端包含共享状态,应使用互斥锁保护:
cpp
class customLog : public logItfc {
private:
std::mutex m_mutex__;
// ...
void vlog(level_t level, const char* fmt, va_list args) {
std::lock_guard<std::mutex> lock(m_mutex__);
// ...
}
};
2. 资源管理
在析构函数中正确释放资源:
cpp
customLog::~customLog() {
close(m_fd__);
delete m_object__;
// ...
}
3. 错误处理
使用异常或返回值处理初始化失败:
cpp
customLog::customLog() {
if (!init()) {
throw std::runtime_error("customLog 初始化失败");
}
}
4. 条件编译
对于依赖外部库的后端,使用条件编译:
cpp
#ifdef HAVE_REDIS
# include <hiredis/hiredis.h>
#endif
void vlog(...) {
#ifdef HAVE_REDIS
// 实现逻辑
#endif
}
5. 批量写入优化
对于网络或数据库后端,建议实现批量提交:
cpp
class customLog : public logItfc {
private:
std::vector<std::string> m_buffer__;
size_t m_batch_size__;
std::mutex m_mutex__;
void flush() {
// 批量提交所有缓冲数据
}
void vlog(...) {
{
std::lock_guard<std::mutex> lock(m_mutex__);
m_buffer__.push_back(entry);
if (m_buffer__.size() >= m_batch_size__) {
flush();
}
}
}
};
6. 连接重连
对于网络后端,实现自动重连机制:
cpp
bool customLog::check_connection() {
if (!m_connected__) {
reconnect();
}
return m_connected__;
}
void vlog(...) {
if (!check_connection()) { return; }
// 写入逻辑
}
调试技巧
1. 验证编译
bash
mkdir -p build && cd build
cmake .. -DBUILD_TESTS=ON
make -j4
2. 添加测试
创建 test/test_custom.cpp:
cpp
#include "log.hpp"
int main() {
auto log = createLog<tinyLogType::CUSTOM>(4, "test_config");
if (!log) {
std::cerr << "创建失败" << std::endl;
return 1;
}
log->info("测试信息");
log->debug("测试调试");
log->warn("测试警告");
log->error("测试错误");
std::cout << "测试通过" << std::endl;
return 0;
}
3. 启用调试模式
bash
cmake .. -DBUILD_DEBUG=ON
已有后端参考
| 后端 | 文件路径 | 特点 |
|---|---|---|
| LOCAL | logDetail/local.hpp |
环形文件,嵌入式友好 |
| UDP | logDetail/udp.hpp |
无连接,高性能 |
| TCP | logDetail/tcp.hpp |
可靠传输 |
| PIPE | logDetail/pipe.hpp |
进程间通信 |
| UNIXSOCK | logDetail/unixSock.hpp |
本地套接字 |
| TERMINAL | logDetail/terminal.hpp |
控制台输出,彩色支持 |
| SQLITE3 | logDetail/sqlite3_log.hpp |
嵌入式数据库 |
| MYSQL | logDetail/mysql_log.hpp |
双连接 + 事务批量提交 |
| POSTGRESQL | logDetail/postgresql_log.hpp |
双连接 + 事务批量提交 |
常见问题
Q1: 如何处理阻塞操作?
A : 在 vlog 中不要执行长时间阻塞操作。如果必须阻塞,可考虑使用异步线程处理。
Q2: 是否需要继承 std::enable_shared_from_this?
A : 若需要在回调中获取自身的 shared_ptr,建议继承:
cpp
class customLog : public logItfc, public std::enable_shared_from_this<customLog> {
// 使用 shared_from_this() 获取 shared_ptr
};
Q3: 如何传递配置参数?
A : 通过构造函数传递。createLog 使用完美转发:
cpp
auto log = createLog<tinyLogType::CUSTOM>(4, arg1, arg2, arg3);
Q4: 线程池如何使用?
A : pt_runner__ 是线程池指针,可用于异步任务:
cpp
void vlog(...) {
if (pt_runner__) {
pt_runner__->pushTask([this, entry]() {
// 异步处理
});
}
}