添加高性能异步日志系统
This commit is contained in:
parent
9c73d52dce
commit
605bedc3e0
42
src/common/logger/CMakeLists.txt
Normal file
42
src/common/logger/CMakeLists.txt
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
cmake_minimum_required(VERSION 3.8)
|
||||||
|
project(logger)
|
||||||
|
|
||||||
|
if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")
|
||||||
|
add_compile_options(-Wall -Wextra -Wpedantic -O2)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# ======== ROS2 dependencies ========
|
||||||
|
find_package(ament_cmake REQUIRED)
|
||||||
|
|
||||||
|
# ======== Build library ========
|
||||||
|
add_library(logger SHARED
|
||||||
|
src/logger.cpp
|
||||||
|
)
|
||||||
|
|
||||||
|
# ======== Include directories ========
|
||||||
|
target_include_directories(logger PUBLIC
|
||||||
|
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
|
||||||
|
$<INSTALL_INTERFACE:include>
|
||||||
|
)
|
||||||
|
|
||||||
|
# ======== Link libraries ========
|
||||||
|
target_link_libraries(logger
|
||||||
|
pthread
|
||||||
|
)
|
||||||
|
|
||||||
|
# ======== Install library ========
|
||||||
|
install(TARGETS logger
|
||||||
|
EXPORT export_${PROJECT_NAME}
|
||||||
|
ARCHIVE DESTINATION lib
|
||||||
|
LIBRARY DESTINATION lib
|
||||||
|
RUNTIME DESTINATION bin
|
||||||
|
)
|
||||||
|
|
||||||
|
# ======== Install headers ========
|
||||||
|
install(DIRECTORY include/ DESTINATION include)
|
||||||
|
|
||||||
|
# ======== Export targets ========
|
||||||
|
ament_export_targets(export_${PROJECT_NAME} HAS_LIBRARY_TARGET)
|
||||||
|
ament_export_dependencies(ament_cmake)
|
||||||
|
|
||||||
|
ament_package()
|
||||||
236
src/common/logger/README.md
Normal file
236
src/common/logger/README.md
Normal file
@ -0,0 +1,236 @@
|
|||||||
|
# 高性能日志系统
|
||||||
|
|
||||||
|
## 1. 设计概述
|
||||||
|
|
||||||
|
本日志系统是一个高性能、异步、线程安全的日志系统,参照ROS2日志设计,适用于C++项目。
|
||||||
|
|
||||||
|
### 1.1 核心特性
|
||||||
|
|
||||||
|
- **异步日志写入**:主线程将日志放入队列,独立线程负责写入文件
|
||||||
|
- **多级别日志**:支持DEBUG、INFO、WARN、ERROR、FATAL五个级别
|
||||||
|
- **双端输出**:同时输出到终端和文件
|
||||||
|
- **终端颜色**:不同级别日志显示不同颜色,便于区分
|
||||||
|
- **日志节流**:支持按时间节流,防止日志爆炸
|
||||||
|
- **文件滚动**:按大小自动滚动日志文件(默认10MB)
|
||||||
|
- **线程安全**:支持多线程并发写入
|
||||||
|
- **易用API**:提供简洁的宏定义接口
|
||||||
|
|
||||||
|
### 1.2 架构设计
|
||||||
|
|
||||||
|
```
|
||||||
|
+----------------+ +----------------+ +----------------+
|
||||||
|
| 业务线程 | | 日志队列 | | 日志写入线程 |
|
||||||
|
| (LOG_* 宏) | ---> | (线程安全) | ---> | (文件+终端输出)|
|
||||||
|
+----------------+ +----------------+ +----------------+
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2. 安装与集成
|
||||||
|
|
||||||
|
### 2.1 CMakeLists.txt配置
|
||||||
|
|
||||||
|
```cmake
|
||||||
|
# 添加依赖
|
||||||
|
find_package(logger REQUIRED)
|
||||||
|
|
||||||
|
# 链接库
|
||||||
|
ament_target_dependencies(your_target
|
||||||
|
logger
|
||||||
|
# 其他依赖
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.2 头文件包含
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
#include "logger/logger.h"
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. 使用方法
|
||||||
|
|
||||||
|
### 3.1 初始化与关闭
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// 初始化日志系统
|
||||||
|
logger::Logger::Init("node_name", "./log");
|
||||||
|
|
||||||
|
// 业务逻辑...
|
||||||
|
|
||||||
|
// 关闭日志系统(可选,程序结束时会自动关闭)
|
||||||
|
logger::Logger::Shutdown();
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 日志宏使用
|
||||||
|
|
||||||
|
#### 3.2.1 基本日志
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
LOG_DEBUG("Debug message: %d", value);
|
||||||
|
LOG_INFO("Info message: %d", value);
|
||||||
|
LOG_WARN("Warn message: %d", value);
|
||||||
|
LOG_ERROR("Error message: %d", value);
|
||||||
|
LOG_FATAL("Fatal message: %d", value);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3.2.2 带节流的日志
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// 每2000毫秒最多输出一次
|
||||||
|
LOG_INFO_THROTTLE(2000, "Throttled info: %d", value);
|
||||||
|
LOG_WARN_THROTTLE(1000, "Throttled warn: %d", value);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.3 日志级别控制
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// 设置日志级别
|
||||||
|
logger::Logger::SetLogLevel(logger::LogLevel::DEBUG);
|
||||||
|
logger::Logger::SetLogLevel(logger::LogLevel::INFO);
|
||||||
|
logger::Logger::SetLogLevel(logger::LogLevel::WARN);
|
||||||
|
logger::Logger::SetLogLevel(logger::LogLevel::ERROR);
|
||||||
|
logger::Logger::SetLogLevel(logger::LogLevel::FATAL);
|
||||||
|
|
||||||
|
// 获取当前日志级别
|
||||||
|
logger::LogLevel level = logger::Logger::GetLogLevel();
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. 配置参数
|
||||||
|
|
||||||
|
### 4.1 初始化参数
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
/**
|
||||||
|
* @param name 日志记录器名称
|
||||||
|
* @param log_dir 日志文件存储目录,默认值为"./log"
|
||||||
|
* @param max_file_size 单个日志文件最大大小(字节),默认值为10MB
|
||||||
|
* @param flush_interval 日志刷新间隔(毫秒),默认值为1000ms
|
||||||
|
*/
|
||||||
|
static void Init(const std::string& name,
|
||||||
|
const std::string& log_dir = "./log",
|
||||||
|
size_t max_file_size = 10 * 1024 * 1024,
|
||||||
|
size_t flush_interval = 1000);
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. 日志格式
|
||||||
|
|
||||||
|
```
|
||||||
|
[YYYY-MM-DD HH:MM:SS.ms] [LEVEL] [THREAD_ID] [NAME] message
|
||||||
|
```
|
||||||
|
|
||||||
|
示例:
|
||||||
|
```
|
||||||
|
[2026-01-19 16:00:00.123] [INFO] [123456789] [test_node] Hello, Logger!
|
||||||
|
```
|
||||||
|
|
||||||
|
## 6. 终端颜色输出
|
||||||
|
|
||||||
|
日志系统为不同级别的日志提供了不同的终端颜色,便于直观区分:
|
||||||
|
|
||||||
|
| 日志级别 | 颜色 | ANSI 代码 | 描述 |
|
||||||
|
|---------|--------|-----------|------------|
|
||||||
|
| DEBUG | 蓝色 | \033[34m | 调试信息 |
|
||||||
|
| INFO | 无颜色 | 无 | 正常信息 |
|
||||||
|
| WARN | 黄色 | \033[33m | 警告信息 |
|
||||||
|
| ERROR | 红色 | \033[31m | 错误信息 |
|
||||||
|
| FATAL | 紫色 | \033[35m | 致命错误 |
|
||||||
|
|
||||||
|
**说明**:
|
||||||
|
- 颜色仅在终端输出中显示,日志文件中为纯文本
|
||||||
|
- 颜色输出使用ANSI转义序列,兼容大多数现代终端
|
||||||
|
- 颜色设置会自动重置,不会影响后续终端输出
|
||||||
|
- 可以通过修改代码禁用颜色输出(如需)
|
||||||
|
|
||||||
|
## 7. 性能优化
|
||||||
|
|
||||||
|
1. **异步写入**:避免主线程阻塞
|
||||||
|
2. **批量处理**:减少磁盘I/O次数
|
||||||
|
3. **日志节流**:防止日志爆炸
|
||||||
|
4. **线程安全队列**:高效的多线程通信
|
||||||
|
5. **合理的刷新间隔**:平衡实时性和性能
|
||||||
|
|
||||||
|
## 8. 最佳实践
|
||||||
|
|
||||||
|
### 8.1 日志级别使用建议
|
||||||
|
|
||||||
|
- **DEBUG**:开发调试信息,生产环境建议关闭
|
||||||
|
- **INFO**:正常运行状态信息
|
||||||
|
- **WARN**:警告信息,需要关注但不影响正常运行
|
||||||
|
- **ERROR**:错误信息,可能影响部分功能
|
||||||
|
- **FATAL**:致命错误,会导致程序退出
|
||||||
|
|
||||||
|
### 8.2 日志内容建议
|
||||||
|
|
||||||
|
- 包含足够的上下文信息
|
||||||
|
- 使用清晰的格式
|
||||||
|
- 避免敏感信息
|
||||||
|
- 合理使用节流功能
|
||||||
|
|
||||||
|
### 8.3 性能考虑
|
||||||
|
|
||||||
|
- 避免在高频调用的函数中使用详细日志
|
||||||
|
- 对频繁输出的日志使用节流功能
|
||||||
|
- 生产环境建议使用INFO或更高级别
|
||||||
|
|
||||||
|
## 9. 示例代码
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
#include "logger/logger.h"
|
||||||
|
#include <thread>
|
||||||
|
|
||||||
|
void TestThread(int id) {
|
||||||
|
for (int i = 0; i < 5; ++i) {
|
||||||
|
LOG_INFO_THROTTLE(1000, "Thread %d: info message %d", id, i);
|
||||||
|
std::this_thread::sleep_for(std::chrono::milliseconds(500));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
// 初始化日志系统
|
||||||
|
logger::Logger::Init("test_logger");
|
||||||
|
|
||||||
|
// 设置日志级别
|
||||||
|
logger::Logger::SetLogLevel(logger::LogLevel::INFO);
|
||||||
|
|
||||||
|
// 测试不同级别日志
|
||||||
|
LOG_INFO("Logger initialized");
|
||||||
|
LOG_WARN("This is a warning");
|
||||||
|
LOG_ERROR("This is an error");
|
||||||
|
|
||||||
|
// 测试多线程日志
|
||||||
|
std::thread t1(TestThread, 1);
|
||||||
|
std::thread t2(TestThread, 2);
|
||||||
|
|
||||||
|
t1.join();
|
||||||
|
t2.join();
|
||||||
|
|
||||||
|
LOG_INFO("Test completed");
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 10. 版本变更
|
||||||
|
|
||||||
|
### 10.1 v1.0.0 (2026-01-19)
|
||||||
|
|
||||||
|
- 初始版本
|
||||||
|
- 实现基本日志功能
|
||||||
|
- 支持五种日志级别
|
||||||
|
- 异步日志写入
|
||||||
|
- 日志文件滚动
|
||||||
|
|
||||||
|
### 10.2 v1.1.0 (2026-01-19)
|
||||||
|
|
||||||
|
- 增加日志节流功能
|
||||||
|
- 支持所有级别日志同时输出到终端和文件
|
||||||
|
- 优化日志格式
|
||||||
|
- 增加详细文档
|
||||||
|
|
||||||
|
### 10.3 v1.2.0 (2026-01-19)
|
||||||
|
|
||||||
|
- 增加终端颜色输出功能
|
||||||
|
- 不同级别日志显示不同颜色
|
||||||
|
- 完善文档说明
|
||||||
|
|
||||||
|
## 11. 联系方式
|
||||||
|
|
||||||
|
如有问题或建议,请联系开发团队。
|
||||||
80
src/common/logger/include/logger/log_level.h
Normal file
80
src/common/logger/include/logger/log_level.h
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
#ifndef LOGGER_LOG_LEVEL_H
|
||||||
|
#define LOGGER_LOG_LEVEL_H
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace logger
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 日志级别枚举
|
||||||
|
*/
|
||||||
|
enum class LogLevel
|
||||||
|
{
|
||||||
|
DEBUG = 0, ///< 调试信息
|
||||||
|
INFO = 1, ///< 普通信息
|
||||||
|
WARN = 2, ///< 警告信息
|
||||||
|
ERROR = 3, ///< 错误信息
|
||||||
|
FATAL = 4 ///< 致命错误信息
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 将日志级别转换为字符串
|
||||||
|
* @param level 日志级别
|
||||||
|
* @return 日志级别对应的字符串
|
||||||
|
*/
|
||||||
|
inline std::string LogLevelToString(LogLevel level)
|
||||||
|
{
|
||||||
|
switch (level)
|
||||||
|
{
|
||||||
|
case LogLevel::DEBUG:
|
||||||
|
return "DEBUG";
|
||||||
|
case LogLevel::INFO:
|
||||||
|
return "INFO";
|
||||||
|
case LogLevel::WARN:
|
||||||
|
return "WARN";
|
||||||
|
case LogLevel::ERROR:
|
||||||
|
return "ERROR";
|
||||||
|
case LogLevel::FATAL:
|
||||||
|
return "FATAL";
|
||||||
|
default:
|
||||||
|
return "UNKNOWN";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 将字符串转换为日志级别
|
||||||
|
* @param level_str 日志级别字符串
|
||||||
|
* @return 对应的日志级别
|
||||||
|
*/
|
||||||
|
inline LogLevel StringToLogLevel(const std::string& level_str)
|
||||||
|
{
|
||||||
|
if (level_str == "DEBUG")
|
||||||
|
{
|
||||||
|
return LogLevel::DEBUG;
|
||||||
|
}
|
||||||
|
else if (level_str == "INFO")
|
||||||
|
{
|
||||||
|
return LogLevel::INFO;
|
||||||
|
}
|
||||||
|
else if (level_str == "WARN")
|
||||||
|
{
|
||||||
|
return LogLevel::WARN;
|
||||||
|
}
|
||||||
|
else if (level_str == "ERROR")
|
||||||
|
{
|
||||||
|
return LogLevel::ERROR;
|
||||||
|
}
|
||||||
|
else if (level_str == "FATAL")
|
||||||
|
{
|
||||||
|
return LogLevel::FATAL;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return LogLevel::INFO; // 默认日志级别
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace logger
|
||||||
|
|
||||||
|
#endif // LOGGER_LOG_LEVEL_H
|
||||||
197
src/common/logger/include/logger/logger.h
Normal file
197
src/common/logger/include/logger/logger.h
Normal file
@ -0,0 +1,197 @@
|
|||||||
|
#ifndef LOGGER_LOGGER_H
|
||||||
|
#define LOGGER_LOGGER_H
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
|
#include <chrono>
|
||||||
|
#include <condition_variable>
|
||||||
|
#include <functional>
|
||||||
|
#include <memory>
|
||||||
|
#include <mutex>
|
||||||
|
#include <queue>
|
||||||
|
#include <string>
|
||||||
|
#include <thread>
|
||||||
|
|
||||||
|
#include "logger/log_level.h"
|
||||||
|
|
||||||
|
namespace logger
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 日志消息结构
|
||||||
|
*/
|
||||||
|
struct LogMessage
|
||||||
|
{
|
||||||
|
LogLevel level; ///< 日志级别
|
||||||
|
std::string message; ///< 日志内容
|
||||||
|
std::chrono::system_clock::time_point timestamp; ///< 日志时间戳
|
||||||
|
std::thread::id thread_id; ///< 线程ID
|
||||||
|
std::string logger_name; ///< 日志记录器名称
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 日志记录器类
|
||||||
|
* 采用单例模式,支持异步日志写入
|
||||||
|
*/
|
||||||
|
class Logger
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
/**
|
||||||
|
* @brief 初始化日志记录器
|
||||||
|
* @param name 日志记录器名称
|
||||||
|
* @param log_dir 日志文件存储目录,默认值为"./log"
|
||||||
|
* @param max_file_size 单个日志文件最大大小(字节),默认值为10MB
|
||||||
|
* @param flush_interval 日志刷新间隔(毫秒),默认值为1000ms
|
||||||
|
*/
|
||||||
|
static void Init(const std::string& name, const std::string& log_dir = "./log",
|
||||||
|
size_t max_file_size = 10 * 1024 * 1024, size_t flush_interval = 1000);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 关闭日志记录器
|
||||||
|
*/
|
||||||
|
static void Shutdown();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 设置日志级别
|
||||||
|
* @param level 日志级别
|
||||||
|
*/
|
||||||
|
static void SetLogLevel(LogLevel level);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 获取当前日志级别
|
||||||
|
* @return 当前日志级别
|
||||||
|
*/
|
||||||
|
static LogLevel GetLogLevel();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 添加日志消息到队列
|
||||||
|
* @param level 日志级别
|
||||||
|
* @param format 日志格式化字符串
|
||||||
|
* @param args 日志参数
|
||||||
|
*/
|
||||||
|
template <typename... Args>
|
||||||
|
static void Log(LogLevel level, const char* format, Args&&... args);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 添加带节流的日志消息到队列
|
||||||
|
* @param level 日志级别
|
||||||
|
* @param throttle_ms 节流时间(毫秒)
|
||||||
|
* @param file 文件路径
|
||||||
|
* @param line 行号
|
||||||
|
* @param format 日志格式化字符串
|
||||||
|
* @param args 日志参数
|
||||||
|
*/
|
||||||
|
template <typename... Args>
|
||||||
|
static void LogThrottled(LogLevel level, uint64_t throttle_ms, const char* file, int line, const char* format,
|
||||||
|
Args&&... args);
|
||||||
|
|
||||||
|
private:
|
||||||
|
/**
|
||||||
|
* @brief 构造函数
|
||||||
|
*/
|
||||||
|
Logger();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 析构函数
|
||||||
|
*/
|
||||||
|
~Logger();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 日志写入线程函数
|
||||||
|
*/
|
||||||
|
void WriteLogThread();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 初始化日志文件
|
||||||
|
*/
|
||||||
|
void InitLogFile();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 滚动日志文件
|
||||||
|
*/
|
||||||
|
void RotateLogFile();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 格式化日志消息
|
||||||
|
* @param msg 日志消息
|
||||||
|
* @return 格式化后的日志字符串
|
||||||
|
*/
|
||||||
|
std::string FormatLogMessage(const LogMessage& msg);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 获取单例实例
|
||||||
|
* @return 日志记录器单例
|
||||||
|
*/
|
||||||
|
static Logger& GetInstance();
|
||||||
|
|
||||||
|
private:
|
||||||
|
struct LogKey
|
||||||
|
{
|
||||||
|
std::string file; ///< 文件路径
|
||||||
|
int line; ///< 行号
|
||||||
|
LogLevel level; ///< 日志级别
|
||||||
|
std::string format; ///< 日志格式化字符串
|
||||||
|
|
||||||
|
bool operator==(const LogKey& other) const
|
||||||
|
{
|
||||||
|
return file == other.file && line == other.line && level == other.level && format == other.format;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 自定义哈希函数支持
|
||||||
|
struct Hash
|
||||||
|
{
|
||||||
|
std::size_t operator()(const LogKey& key) const
|
||||||
|
{
|
||||||
|
return std::hash<std::string>{}(key.file) ^ std::hash<int>{}(key.line) ^
|
||||||
|
std::hash<int>{}(static_cast<int>(key.level)) ^ std::hash<std::string>{}(key.format);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
using LogThrottleMap = std::unordered_map<LogKey, std::chrono::system_clock::time_point, LogKey::Hash>;
|
||||||
|
|
||||||
|
std::string name_; ///< 日志记录器名称
|
||||||
|
std::string log_dir_; ///< 日志文件目录
|
||||||
|
size_t max_file_size_; ///< 单个日志文件最大大小
|
||||||
|
size_t flush_interval_; ///< 日志刷新间隔(毫秒)
|
||||||
|
std::atomic<LogLevel> log_level_; ///< 当前日志级别
|
||||||
|
|
||||||
|
std::queue<LogMessage> log_queue_; ///< 日志消息队列
|
||||||
|
std::mutex queue_mutex_; ///< 队列互斥锁
|
||||||
|
std::condition_variable queue_cv_; ///< 队列条件变量
|
||||||
|
std::atomic<bool> running_; ///< 运行状态标记
|
||||||
|
std::thread write_thread_; ///< 日志写入线程
|
||||||
|
|
||||||
|
FILE* log_file_; ///< 当前日志文件指针
|
||||||
|
std::mutex file_mutex_; ///< 文件互斥锁
|
||||||
|
std::chrono::system_clock::time_point last_flush_time_; ///< 上次刷新时间
|
||||||
|
|
||||||
|
// 节流日志相关
|
||||||
|
LogThrottleMap throttle_map_; ///< 存储每个日志语句的最后执行时间
|
||||||
|
std::mutex throttle_mutex_; ///< 节流日志互斥锁
|
||||||
|
};
|
||||||
|
|
||||||
|
// 日志宏定义
|
||||||
|
#define LOG_DEBUG(format, ...) logger::Logger::Log(logger::LogLevel::DEBUG, format, ##__VA_ARGS__)
|
||||||
|
#define LOG_INFO(format, ...) logger::Logger::Log(logger::LogLevel::INFO, format, ##__VA_ARGS__)
|
||||||
|
#define LOG_WARN(format, ...) logger::Logger::Log(logger::LogLevel::WARN, format, ##__VA_ARGS__)
|
||||||
|
#define LOG_ERROR(format, ...) logger::Logger::Log(logger::LogLevel::ERROR, format, ##__VA_ARGS__)
|
||||||
|
#define LOG_FATAL(format, ...) logger::Logger::Log(logger::LogLevel::FATAL, format, ##__VA_ARGS__)
|
||||||
|
|
||||||
|
// 带节流的日志宏定义(单位:毫秒)
|
||||||
|
#define LOG_DEBUG_THROTTLE(throttle_ms, format, ...) \
|
||||||
|
logger::Logger::LogThrottled(logger::LogLevel::DEBUG, throttle_ms, __FILE__, __LINE__, format, ##__VA_ARGS__)
|
||||||
|
#define LOG_INFO_THROTTLE(throttle_ms, format, ...) \
|
||||||
|
logger::Logger::LogThrottled(logger::LogLevel::INFO, throttle_ms, __FILE__, __LINE__, format, ##__VA_ARGS__)
|
||||||
|
#define LOG_WARN_THROTTLE(throttle_ms, format, ...) \
|
||||||
|
logger::Logger::LogThrottled(logger::LogLevel::WARN, throttle_ms, __FILE__, __LINE__, format, ##__VA_ARGS__)
|
||||||
|
#define LOG_ERROR_THROTTLE(throttle_ms, format, ...) \
|
||||||
|
logger::Logger::LogThrottled(logger::LogLevel::ERROR, throttle_ms, __FILE__, __LINE__, format, ##__VA_ARGS__)
|
||||||
|
#define LOG_FATAL_THROTTLE(throttle_ms, format, ...) \
|
||||||
|
logger::Logger::LogThrottled(logger::LogLevel::FATAL, throttle_ms, __FILE__, __LINE__, format, ##__VA_ARGS__)
|
||||||
|
|
||||||
|
} // namespace logger
|
||||||
|
|
||||||
|
// 模板函数实现
|
||||||
|
#include "logger/logger_impl.h"
|
||||||
|
|
||||||
|
#endif // LOGGER_LOGGER_H
|
||||||
140
src/common/logger/include/logger/logger_impl.h
Normal file
140
src/common/logger/include/logger/logger_impl.h
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
#ifndef LOGGER_LOGGER_IMPL_H
|
||||||
|
#define LOGGER_LOGGER_IMPL_H
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <cstdio>
|
||||||
|
#include <cstring>
|
||||||
|
#include <mutex>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
#include "logger/logger.h"
|
||||||
|
|
||||||
|
// 抑制格式安全警告,因为format参数是来自用户可控的字符串字面量
|
||||||
|
#pragma GCC diagnostic push
|
||||||
|
#pragma GCC diagnostic ignored "-Wformat-security"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 字符串格式化辅助函数
|
||||||
|
* @param format 格式化字符串
|
||||||
|
* @param args 可变参数
|
||||||
|
* @return 格式化后的字符串
|
||||||
|
*/
|
||||||
|
template <typename... Args>
|
||||||
|
inline std::string StringFormat(const char* format, Args&&... args)
|
||||||
|
{
|
||||||
|
// 计算所需缓冲区大小
|
||||||
|
int size = snprintf(nullptr, 0, format, std::forward<Args>(args)...);
|
||||||
|
if (size <= 0)
|
||||||
|
{
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分配缓冲区并格式化
|
||||||
|
std::array<char, 1024> small_buf;
|
||||||
|
if (size < static_cast<int>(small_buf.size()))
|
||||||
|
{
|
||||||
|
// 使用__attribute__((format(printf, 2, 3)))抑制格式安全警告
|
||||||
|
// 因为format参数是来自用户可控的字符串字面量
|
||||||
|
snprintf(small_buf.data(), small_buf.size(), format, std::forward<Args>(args)...);
|
||||||
|
return small_buf.data();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
std::string buf(size + 1, '\0');
|
||||||
|
snprintf(buf.data(), buf.size(), format, std::forward<Args>(args)...);
|
||||||
|
return buf;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 恢复诊断设置
|
||||||
|
#pragma GCC diagnostic pop
|
||||||
|
|
||||||
|
namespace logger
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 日志记录模板函数实现
|
||||||
|
*/
|
||||||
|
template <typename... Args>
|
||||||
|
inline void Logger::Log(LogLevel level, const char* format, Args&&... args)
|
||||||
|
{
|
||||||
|
// 检查日志级别
|
||||||
|
if (level < GetInstance().log_level_)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建日志消息
|
||||||
|
LogMessage msg;
|
||||||
|
msg.level = level;
|
||||||
|
msg.timestamp = std::chrono::system_clock::now();
|
||||||
|
msg.thread_id = std::this_thread::get_id();
|
||||||
|
msg.logger_name = GetInstance().name_;
|
||||||
|
msg.message = ::StringFormat(format, std::forward<Args>(args)...);
|
||||||
|
|
||||||
|
// 添加到队列
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(GetInstance().queue_mutex_);
|
||||||
|
GetInstance().log_queue_.push(std::move(msg));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 通知写入线程
|
||||||
|
GetInstance().queue_cv_.notify_one();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 带节流的日志记录模板函数实现
|
||||||
|
*/
|
||||||
|
template <typename... Args>
|
||||||
|
inline void Logger::LogThrottled(LogLevel level, uint64_t throttle_ms, const char* file, int line, const char* format,
|
||||||
|
Args&&... args)
|
||||||
|
{
|
||||||
|
// 检查日志级别
|
||||||
|
if (level < GetInstance().log_level_)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否需要执行日志
|
||||||
|
bool should_log = false;
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(GetInstance().throttle_mutex_);
|
||||||
|
|
||||||
|
// 创建日志唯一标识
|
||||||
|
LogKey key;
|
||||||
|
key.file = file;
|
||||||
|
key.line = line;
|
||||||
|
key.level = level;
|
||||||
|
key.format = format;
|
||||||
|
|
||||||
|
auto now = std::chrono::system_clock::now();
|
||||||
|
auto it = GetInstance().throttle_map_.find(key);
|
||||||
|
|
||||||
|
if (it == GetInstance().throttle_map_.end())
|
||||||
|
{
|
||||||
|
// 第一次执行该日志
|
||||||
|
should_log = true;
|
||||||
|
GetInstance().throttle_map_[key] = now;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// 检查时间差
|
||||||
|
auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(now - it->second);
|
||||||
|
if (duration.count() >= throttle_ms)
|
||||||
|
{
|
||||||
|
should_log = true;
|
||||||
|
it->second = now;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果需要执行日志
|
||||||
|
if (should_log)
|
||||||
|
{
|
||||||
|
Log(level, format, std::forward<Args>(args)...);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace logger
|
||||||
|
|
||||||
|
#endif // LOGGER_LOGGER_IMPL_H
|
||||||
19
src/common/logger/package.xml
Normal file
19
src/common/logger/package.xml
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<?xml version="1.0"?>
|
||||||
|
<?xml-model href="http://download.ros.org/schema/package_format3.xsd" schematypens="http://www.w3.org/2001/XMLSchema"?>
|
||||||
|
<package format="3">
|
||||||
|
<name>logger</name>
|
||||||
|
<version>1.0.0</version>
|
||||||
|
<description>High performance logging system for sweeper project</description>
|
||||||
|
<maintainer email="admin@example.com">admin</maintainer>
|
||||||
|
<license>Apache License 2.0</license>
|
||||||
|
|
||||||
|
<buildtool_depend>ament_cmake</buildtool_depend>
|
||||||
|
|
||||||
|
<build_depend>rclcpp</build_depend>
|
||||||
|
|
||||||
|
<exec_depend>rclcpp</exec_depend>
|
||||||
|
|
||||||
|
<export>
|
||||||
|
<build_type>ament_cmake</build_type>
|
||||||
|
</export>
|
||||||
|
</package>
|
||||||
269
src/common/logger/src/logger.cpp
Normal file
269
src/common/logger/src/logger.cpp
Normal file
@ -0,0 +1,269 @@
|
|||||||
|
#include "logger/logger.h"
|
||||||
|
|
||||||
|
#include <cstdio>
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <ctime>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <iostream>
|
||||||
|
|
||||||
|
namespace logger
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 构造函数
|
||||||
|
*/
|
||||||
|
Logger::Logger()
|
||||||
|
: name_(""),
|
||||||
|
log_dir_("./log"),
|
||||||
|
max_file_size_(10 * 1024 * 1024),
|
||||||
|
flush_interval_(1000),
|
||||||
|
log_level_(LogLevel::INFO),
|
||||||
|
running_(false),
|
||||||
|
log_file_(nullptr)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 析构函数
|
||||||
|
*/
|
||||||
|
Logger::~Logger() { Shutdown(); }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 初始化日志记录器
|
||||||
|
*/
|
||||||
|
void Logger::Init(const std::string& name, const std::string& log_dir, size_t max_file_size, size_t flush_interval)
|
||||||
|
{
|
||||||
|
Logger& logger = GetInstance();
|
||||||
|
logger.name_ = name;
|
||||||
|
logger.log_dir_ = log_dir;
|
||||||
|
logger.max_file_size_ = max_file_size;
|
||||||
|
logger.flush_interval_ = flush_interval;
|
||||||
|
|
||||||
|
// 创建日志目录
|
||||||
|
std::filesystem::create_directories(logger.log_dir_);
|
||||||
|
|
||||||
|
// 初始化日志文件
|
||||||
|
logger.InitLogFile();
|
||||||
|
|
||||||
|
// 启动日志写入线程
|
||||||
|
logger.running_ = true;
|
||||||
|
logger.write_thread_ = std::thread(&Logger::WriteLogThread, &logger);
|
||||||
|
|
||||||
|
LOG_INFO("Logger initialized: name=%s, log_dir=%s, max_file_size=%zu, flush_interval=%zu", name.c_str(),
|
||||||
|
log_dir.c_str(), max_file_size, flush_interval);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 关闭日志记录器
|
||||||
|
*/
|
||||||
|
void Logger::Shutdown()
|
||||||
|
{
|
||||||
|
Logger& logger = GetInstance();
|
||||||
|
if (logger.running_)
|
||||||
|
{
|
||||||
|
logger.running_ = false;
|
||||||
|
logger.queue_cv_.notify_one();
|
||||||
|
|
||||||
|
if (logger.write_thread_.joinable())
|
||||||
|
{
|
||||||
|
logger.write_thread_.join();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 刷新并关闭日志文件
|
||||||
|
if (logger.log_file_)
|
||||||
|
{
|
||||||
|
fflush(logger.log_file_);
|
||||||
|
fclose(logger.log_file_);
|
||||||
|
logger.log_file_ = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_INFO("Logger shutdown");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 设置日志级别
|
||||||
|
*/
|
||||||
|
void Logger::SetLogLevel(LogLevel level)
|
||||||
|
{
|
||||||
|
GetInstance().log_level_ = level;
|
||||||
|
LOG_INFO("Log level set to %s", LogLevelToString(level).c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 获取当前日志级别
|
||||||
|
*/
|
||||||
|
LogLevel Logger::GetLogLevel() { return GetInstance().log_level_; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 初始化日志文件
|
||||||
|
*/
|
||||||
|
void Logger::InitLogFile()
|
||||||
|
{
|
||||||
|
// 生成日志文件名
|
||||||
|
auto now = std::chrono::system_clock::now();
|
||||||
|
auto now_c = std::chrono::system_clock::to_time_t(now);
|
||||||
|
struct tm tm_info;
|
||||||
|
localtime_r(&now_c, &tm_info);
|
||||||
|
|
||||||
|
char filename[256];
|
||||||
|
snprintf(filename, sizeof(filename), "%s/%s_%04d%02d%02d_%02d%02d%02d.log", log_dir_.c_str(), name_.c_str(),
|
||||||
|
tm_info.tm_year + 1900, tm_info.tm_mon + 1, tm_info.tm_mday, tm_info.tm_hour, tm_info.tm_min,
|
||||||
|
tm_info.tm_sec);
|
||||||
|
|
||||||
|
// 打开日志文件
|
||||||
|
log_file_ = fopen(filename, "a");
|
||||||
|
if (!log_file_)
|
||||||
|
{
|
||||||
|
std::cerr << "Failed to open log file: " << filename << std::endl;
|
||||||
|
exit(EXIT_FAILURE);
|
||||||
|
}
|
||||||
|
|
||||||
|
last_flush_time_ = std::chrono::system_clock::now();
|
||||||
|
|
||||||
|
LOG_INFO("Log file initialized: %s", filename);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 滚动日志文件
|
||||||
|
*/
|
||||||
|
void Logger::RotateLogFile()
|
||||||
|
{
|
||||||
|
if (log_file_)
|
||||||
|
{
|
||||||
|
fflush(log_file_);
|
||||||
|
fclose(log_file_);
|
||||||
|
log_file_ = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
InitLogFile();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 格式化日志消息
|
||||||
|
*/
|
||||||
|
std::string Logger::FormatLogMessage(const LogMessage& msg)
|
||||||
|
{
|
||||||
|
// 格式化时间
|
||||||
|
auto now = msg.timestamp;
|
||||||
|
auto now_c = std::chrono::system_clock::to_time_t(now);
|
||||||
|
auto now_ms = std::chrono::duration_cast<std::chrono::milliseconds>(now.time_since_epoch()) % 1000;
|
||||||
|
|
||||||
|
struct tm tm_info;
|
||||||
|
localtime_r(&now_c, &tm_info);
|
||||||
|
|
||||||
|
char time_buf[64];
|
||||||
|
snprintf(time_buf, sizeof(time_buf), "%04d-%02d-%02d %02d:%02d:%02d.%03d", tm_info.tm_year + 1900,
|
||||||
|
tm_info.tm_mon + 1, tm_info.tm_mday, tm_info.tm_hour, tm_info.tm_min, tm_info.tm_sec,
|
||||||
|
static_cast<int>(now_ms.count()));
|
||||||
|
|
||||||
|
// 格式化线程ID
|
||||||
|
char thread_buf[32];
|
||||||
|
snprintf(thread_buf, sizeof(thread_buf), "%lu", std::hash<std::thread::id>{}(msg.thread_id));
|
||||||
|
|
||||||
|
// 组合日志消息
|
||||||
|
char log_buf[2048];
|
||||||
|
snprintf(log_buf, sizeof(log_buf), "[%s] [%s] [%s] [%s] %s\n", time_buf, LogLevelToString(msg.level).c_str(),
|
||||||
|
thread_buf, msg.logger_name.c_str(), msg.message.c_str());
|
||||||
|
|
||||||
|
return log_buf;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 日志写入线程函数
|
||||||
|
*/
|
||||||
|
void Logger::WriteLogThread()
|
||||||
|
{
|
||||||
|
std::queue<LogMessage> local_queue;
|
||||||
|
|
||||||
|
while (running_ || !log_queue_.empty())
|
||||||
|
{
|
||||||
|
{ // 从队列获取日志消息
|
||||||
|
std::unique_lock<std::mutex> lock(queue_mutex_);
|
||||||
|
|
||||||
|
// 等待新的日志消息或超时
|
||||||
|
queue_cv_.wait_for(lock, std::chrono::milliseconds(flush_interval_),
|
||||||
|
[this]() { return !log_queue_.empty() || !running_; });
|
||||||
|
|
||||||
|
// 将队列中的消息移动到本地队列,减少锁持有时间
|
||||||
|
if (!log_queue_.empty())
|
||||||
|
{
|
||||||
|
local_queue.swap(log_queue_);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 写入本地队列中的所有日志
|
||||||
|
while (!local_queue.empty())
|
||||||
|
{
|
||||||
|
LogMessage msg = std::move(local_queue.front());
|
||||||
|
local_queue.pop();
|
||||||
|
|
||||||
|
std::string formatted_msg = FormatLogMessage(msg);
|
||||||
|
|
||||||
|
{ // 写入日志文件
|
||||||
|
std::lock_guard<std::mutex> lock(file_mutex_);
|
||||||
|
if (log_file_)
|
||||||
|
{
|
||||||
|
fwrite(formatted_msg.c_str(), 1, formatted_msg.size(), log_file_);
|
||||||
|
|
||||||
|
// 检查文件大小是否需要滚动
|
||||||
|
if (ftell(log_file_) >= static_cast<long>(max_file_size_))
|
||||||
|
{
|
||||||
|
RotateLogFile();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 定期刷新缓冲区
|
||||||
|
auto now = std::chrono::system_clock::now();
|
||||||
|
if (std::chrono::duration_cast<std::chrono::milliseconds>(now - last_flush_time_) >=
|
||||||
|
std::chrono::milliseconds(flush_interval_))
|
||||||
|
{
|
||||||
|
fflush(log_file_);
|
||||||
|
last_flush_time_ = now;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 同时输出到控制台(带颜色,INFO级别无颜色)
|
||||||
|
std::string colored_msg;
|
||||||
|
switch (msg.level)
|
||||||
|
{
|
||||||
|
case LogLevel::DEBUG:
|
||||||
|
colored_msg = "\033[34m" + formatted_msg + "\033[0m"; // 蓝色
|
||||||
|
break;
|
||||||
|
case LogLevel::INFO:
|
||||||
|
colored_msg = formatted_msg; // INFO级别无颜色
|
||||||
|
break;
|
||||||
|
case LogLevel::WARN:
|
||||||
|
colored_msg = "\033[33m" + formatted_msg + "\033[0m"; // 黄色
|
||||||
|
break;
|
||||||
|
case LogLevel::ERROR:
|
||||||
|
colored_msg = "\033[31m" + formatted_msg + "\033[0m"; // 红色
|
||||||
|
break;
|
||||||
|
case LogLevel::FATAL:
|
||||||
|
colored_msg = "\033[35m" + formatted_msg + "\033[0m"; // 紫色
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
colored_msg = formatted_msg;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
std::cerr << colored_msg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 最后刷新日志文件
|
||||||
|
if (log_file_)
|
||||||
|
{
|
||||||
|
fflush(log_file_);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 获取单例实例
|
||||||
|
*/
|
||||||
|
Logger& Logger::GetInstance()
|
||||||
|
{
|
||||||
|
static Logger instance;
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace logger
|
||||||
57
src/common/logger/test_logger.cpp
Normal file
57
src/common/logger/test_logger.cpp
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
#include <chrono>
|
||||||
|
#include <thread>
|
||||||
|
|
||||||
|
#include "logger/logger.h"
|
||||||
|
|
||||||
|
void TestThread(int id)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < 5; ++i)
|
||||||
|
{
|
||||||
|
LOG_DEBUG("Thread %d: debug message %d", id, i);
|
||||||
|
LOG_INFO("Thread %d: info message %d", id, i);
|
||||||
|
LOG_WARN("Thread %d: warn message %d", id, i);
|
||||||
|
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int main()
|
||||||
|
{
|
||||||
|
// 初始化日志系统
|
||||||
|
logger::Logger::Init("test_logger", "./test_logs", 1 * 1024 * 1024);
|
||||||
|
|
||||||
|
// 设置日志级别为DEBUG
|
||||||
|
logger::Logger::SetLogLevel(logger::LogLevel::DEBUG);
|
||||||
|
|
||||||
|
// 测试不同级别的日志
|
||||||
|
LOG_DEBUG("Debug message");
|
||||||
|
LOG_INFO("Info message");
|
||||||
|
LOG_WARN("Warn message");
|
||||||
|
LOG_ERROR("Error message");
|
||||||
|
LOG_FATAL("Fatal message");
|
||||||
|
|
||||||
|
// 测试带参数的日志
|
||||||
|
LOG_INFO("Test with integers: %d, %d, %d", 1, 2, 3);
|
||||||
|
LOG_INFO("Test with floats: %f, %f", 1.1, 2.2);
|
||||||
|
LOG_INFO("Test with strings: %s, %s", "hello", "world");
|
||||||
|
|
||||||
|
// 测试多线程日志
|
||||||
|
std::thread t1(TestThread, 1);
|
||||||
|
std::thread t2(TestThread, 2);
|
||||||
|
std::thread t3(TestThread, 3);
|
||||||
|
|
||||||
|
t1.join();
|
||||||
|
t2.join();
|
||||||
|
t3.join();
|
||||||
|
|
||||||
|
// 测试日志文件滚动(生成大量日志)
|
||||||
|
for (int i = 0; i < 2000; ++i)
|
||||||
|
{
|
||||||
|
LOG_INFO("Large log message %d: This is a test to generate large amount of log data to test log file rotation.",
|
||||||
|
i);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 关闭日志系统
|
||||||
|
logger::Logger::Shutdown();
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user