OmniStacksStaticModules 2.0.0
Loading...
Searching...
No Matches
InterfaceLog.cppm
Go to the documentation of this file.
1
19export module InterfaceLog;
20
21import std;
22import bq.log;
23
24export namespace interface_log {
25
26// ---- 公共常量与状态 -------------------------------------------------------
27
29inline constexpr std::size_t kMaxLines = 1000;
30
32inline constexpr std::string_view kDeveloperWebsite = "https://suchaharcan.github.io/";
33
35enum class ApiStatus {
36 ok,
37 failed,
38};
39
45
50struct ApiName {
51 std::string value;
52 explicit ApiName(std::string name) : value(std::move(name)) {}
53};
54
59struct LogText {
60 std::string value;
61 explicit LogText(std::string text) : value(std::move(text)) {}
62};
63
70std::expected<std::monostate, LogError> init(
71 const std::filesystem::path& log_path = "log/interface.log");
72
79std::size_t cleanup_old_logs(const std::filesystem::path& log_path, int months_back = 3);
80
82void set_enabled(bool enabled);
83
85bool enabled();
86
96void record(ApiName api, LogText input, LogText output, ApiStatus status);
97
102std::vector<std::string> recent();
103
105std::size_t count();
106
108void clear();
109
111std::filesystem::path log_path();
112
118template <std::invocable<std::string_view> Visitor>
119void for_each_recent(Visitor&& visitor);
120
122std::string_view status_name(ApiStatus status) noexcept;
123
125std::string_view log_error_message(LogError error) noexcept;
126
127} // namespace interface_log
128
129// ---- 非导出内部状态 -------------------------------------------------------
130// 内部状态放在非导出的模块作用域(module linkage,非 TU-local),
131// 保证导出模板 for_each_recent 在其他 TU 实例化时合法可达。
132// 所有可变状态都由 g_mutex 串行化;bqlog 只是可选落盘后端,内存环形缓冲
133// 在 bqlog 未初始化或运行期异常时仍能继续工作。
134namespace interface_log {
135
136std::mutex g_mutex;
137std::deque<std::string> g_lines;
138std::optional<bq::log> g_logger;
139std::filesystem::path g_log_path;
140bool g_initialized = false;
142
147std::string now_string() {
148 return std::format("{:%Y-%m-%d %H:%M:%S}", std::chrono::system_clock::now());
149}
150
151} // namespace interface_log
152
153namespace interface_log {
154
155std::expected<std::monostate, LogError> init(const std::filesystem::path& log_path_value) {
156 // 空路径属于可恢复配置错误;调用方仍可依赖内存日志继续运行。
157 if (log_path_value.empty()) {
158 return std::unexpected(LogError::invalid_path);
159 }
160
161 std::lock_guard lock(g_mutex);
162 g_recording_enabled = true;
163 // 同一路径重复初始化保持幂等,避免每个测试/Fixture 重建文件后端。
164 if (g_initialized && g_log_path == log_path_value) {
165 return {};
166 }
167
168 try {
169 // 先创建父目录,再交给 bqlog 创建带日期序号的文本文件。
170 if (log_path_value.has_parent_path()) {
171 std::filesystem::create_directories(log_path_value.parent_path());
172 }
173
174 // 单文件上限 1 MiB;超过后由 bqlog 滚动,项目启动时再做三个月清理。
175 const std::string config = std::format(
176 "appenders_config.appender_interface.type=text_file\n"
177 "appenders_config.appender_interface.file_name={}\n"
178 "appenders_config.appender_interface.levels=[all]\n"
179 "appenders_config.appender_interface.max_file_size=1048576\n",
180 log_path_value.string());
181
182 // 日志名附加路径哈希,避免并行测试进程共享同名日志对象。
183 const std::string log_name =
184 std::format("stacks_tools_interface_{}", std::hash<std::string>{}(log_path_value.string()));
185 g_logger.emplace(bq::log::create_log(log_name, config));
186 g_log_path = log_path_value;
187 g_initialized = true;
188 // 水印属于后端初始化记录,便于从用户日志直接识别插件来源。
189 g_logger->info("Omni-Stacks developer={} interface log initialized path:{}", kDeveloperWebsite,
190 log_path_value.string());
191 // 立即 flush,保证 CLI 崩溃或 Alfred 终止进程时仍能看到最近一次调用。
192 g_logger->force_flush();
193 return {};
194 } catch (const std::exception&) {
195 // bqlog 失败不销毁进程内环形缓冲;只记录后端不可用状态。
196 g_initialized = false;
197 g_log_path = log_path_value;
198 return std::unexpected(LogError::backend_unavailable);
199 }
200}
201
202std::size_t cleanup_old_logs(const std::filesystem::path& log_path_value, int months_back) {
203 // 负保留期和空路径视为禁用清理,不做任何删除。
204 if (log_path_value.empty() || months_back < 0) {
205 return 0;
206 }
207 const std::filesystem::path directory =
208 log_path_value.has_parent_path() ? log_path_value.parent_path() : std::filesystem::path(".");
209 std::error_code error;
210 if (!std::filesystem::is_directory(directory, error)) {
211 return 0;
212 }
213
214 // 以“当天”而非精确时刻作为截止点;日志按自然日命名,便于稳定比较。
215 const std::chrono::sys_days cutoff = std::chrono::floor<std::chrono::days>(
216 std::chrono::system_clock::now() - std::chrono::months{months_back});
217 const std::chrono::year_month_day cutoff_date{cutoff};
218
219 // 将 YYYY-MM-DD 压缩成 YYYYMMDD 无符号整数,与文件名中的日期段同构。
220 const unsigned cutoff_number =
221 static_cast<unsigned>(static_cast<int>(cutoff_date.year())) * 10000U +
222 static_cast<unsigned>(cutoff_date.month()) * 100U + static_cast<unsigned>(cutoff_date.day());
223
224 // 只匹配本插件管理的命名空间,绝不按目录批量删除其他 Alfred 日志。
225 const std::regex dated_log(R"((?:interface\.log_|trace-)([0-9]{8}))");
226
227 std::size_t removed = 0;
228 for (const auto& entry : std::filesystem::directory_iterator(directory, error)) {
229 // 遍历错误时跳过当前项并继续,清理属于尽力而为的维护操作。
230 if (error || !entry.is_regular_file(error)) {
231 continue;
232 }
233 const std::string filename = entry.path().filename().string();
234 std::smatch match;
235 bool expired = false;
236 if (std::regex_search(filename, match, dated_log) && match.size() > 1) {
237 const auto stamp = static_cast<unsigned>(std::stoul(match[1].str()));
238 expired = stamp < cutoff_number;
239 } else if (filename == "trace.log" || filename == "trace.log.binary-stderr") {
240 // 无日期后缀的旧格式 trace 按文件 mtime 判断;统一转换到 system_clock。
241 const auto write_time = entry.last_write_time(error);
242 if (!error) {
243 expired = std::filesystem::file_time_type::clock::to_sys(write_time) < cutoff;
244 }
245 }
246 if (expired) {
247 std::filesystem::remove(entry.path(), error);
248 if (!error) {
249 ++removed;
250 }
251 }
252 error.clear();
253 }
254 return removed;
255}
256
257void record(ApiName api, LogText input, LogText output, ApiStatus status) {
258 // 先把一次调用压成单行文本,使内存缓冲与 bqlog 文件拥有完全一致的记录格式。
259 const std::string line = std::format("[{}] api={} input={} output={} status={}", now_string(),
260 api.value, input.value, output.value, status_name(status));
261
262 std::lock_guard lock(g_mutex);
263 // 关闭日志时尽早返回,不消耗环形缓冲,也不触发文件 flush。
264 if (!g_recording_enabled) {
265 return;
266 }
267 g_lines.push_back(line);
268 // 环形缓冲只淘汰队首,保持“最近 kMaxLines 条”的时间顺序。
269 while (g_lines.size() > kMaxLines) {
270 g_lines.pop_front();
271 }
272 if (g_initialized) {
273 try {
274 g_logger->info("{}", line);
275 g_logger->force_flush();
276 } catch (const std::exception&) {
277 // 环形缓冲仍保留记录;bqlog 异常不破坏业务接口。
278 }
279 }
280}
281
282void set_enabled(bool value) {
283 // 启停是进程级状态,切换时与 record 使用同一把锁,避免部分线程继续写文件。
284 std::lock_guard lock(g_mutex);
285 g_recording_enabled = value;
286}
287
288bool enabled() {
289 std::lock_guard lock(g_mutex);
290 return g_recording_enabled;
291}
292
293std::vector<std::string> recent() {
294 std::lock_guard lock(g_mutex);
295 return {g_lines.begin(), g_lines.end()};
296}
297
298std::size_t count() {
299 std::lock_guard lock(g_mutex);
300 return g_lines.size();
301}
302
303void clear() {
304 std::lock_guard lock(g_mutex);
305 g_lines.clear();
306}
307
308std::filesystem::path log_path() {
309 std::lock_guard lock(g_mutex);
310 return g_log_path;
311}
312
313template <std::invocable<std::string_view> Visitor>
314void for_each_recent(Visitor&& visitor) {
315 std::lock_guard lock(g_mutex);
316 for (const auto& line : g_lines) {
317 std::invoke(visitor, std::string_view(line));
318 }
319}
320
321std::string_view status_name(ApiStatus status) noexcept {
322 // 显式穷举状态;后续新增枚举值时编译器可配合警告提示遗漏分支。
323 switch (status) {
324 case ApiStatus::ok:
325 return "ok";
327 return "fail";
328 }
329 return "unknown";
330}
331
332std::string_view log_error_message(LogError error) noexcept {
333 // 业务层只消费稳定字符串,不直接暴露 bqlog 的平台错误文本。
334 switch (error) {
336 return "log path is empty";
338 return "bqlog backend unavailable";
339 }
340 return "unknown log error";
341}
342
343} // namespace interface_log
接口输入/输出日志模块(STATIC 模块库)。
std::filesystem::path g_log_path
当前后端日志基准路径。
void clear()
清空内存环形缓冲(不动 bqlog 文件)。
void set_enabled(bool enabled)
启用/禁用接口记录(内存与文件同时生效)。
std::size_t cleanup_old_logs(const std::filesystem::path &log_path, int months_back=3)
删除超过保留期的接口日志与每日 trace 日志。
bool g_recording_enabled
Alfred 配置控制的全局日志开关。
std::vector< std::string > recent()
取出最近 <= kMaxLines 条记录(最旧在前)。
std::string_view log_error_message(LogError error) noexcept
LogError 到可读消息(供 CLI / 测试使用)。
std::string_view status_name(ApiStatus status) noexcept
状态枚举到可读名称(供日志/测试使用)。
void record(ApiName api, LogText input, LogText output, ApiStatus status)
记录一次接口调用。
std::mutex g_mutex
保护下列全部可变状态。
std::deque< std::string > g_lines
最近调用记录;队首最旧。
std::size_t count()
当前环形缓冲条数(<= kMaxLines)。
constexpr std::size_t kMaxLines
环形缓冲最大记录条数(= 1000)。
std::optional< bq::log > g_logger
可选 bqlog 文件后端。
bool g_initialized
bqlog 是否已成功初始化。
LogError
可恢复的日志初始化错误。
@ backend_unavailable
bqlog 后端初始化失败(环形缓冲仍可用)
@ invalid_path
日志路径为空
std::expected< std::monostate, LogError > init(const std::filesystem::path &log_path="log/interface.log")
初始化 bqlog TextFileAppender(可重复调用;路径变化会重建后端)。
ApiStatus
接口调用结果状态。
@ failed
调用失败(错误已记录在 output 文本中)
void for_each_recent(Visitor &&visitor)
在锁内逐条访问环形缓冲(函数对象设计)。
std::filesystem::path log_path()
当前配置的日志路径。
bool enabled()
当前是否启用接口记录。
std::string now_string()
当前时间字符串(本地时间,秒级精度)。
constexpr std::string_view kDeveloperWebsite
开发者网站水印;每次初始化 bqlog 与 CLI 调用日志都会写出。
接口名强类型。 语义:调用方必须显式构造 ApiName,避免把任意字符串误当接口名。
ApiName(std::string name)
接口输入/输出文本强类型。 语义:与 ApiName 一样显式构造,参数顺序在编译期被类型约束。
LogText(std::string text)