feat(ca): 认证独立化 CA 模块 v1.0.0 + gateway v0.9.0 纯转发瘦身
拓扑:waf:443 → ca:8443(认证+前置转发)→ gateway:8445(仅 loopback 纯转发), WAF upstream 不变,core/dnfs2/前端零改动。 ca v1.0.0(新模块): - 认证资产逐字搬迁自 gateway v0.8.0(9 条认证路由、Token 双轨吊销、 PBKDF2、限流、WS 代理鉴权、wrapperHttp、优雅关闭) - handleForward 前置转发层:固定上游 gateway:8445;无 Target-Url 公开直转, 有则本地验签后注入 X-Auth-User/X-Auth-Admin/Authorization(strip-then-set) - is_special_header 新增剥离 x-auth-user/x-auth-admin;上游不可达统一 502 - confs/configs/ca.ini 新增([Upstream] gateway + UPSTREAM_GATEWAY env) gateway v0.9.0(2470→约 560 行): - 删除全部认证代码,退居 127.0.0.1:8445 - 启动 loopback 强制自检(非 loopback 拒绝启动) - X-Auth-User 存在性不变量:显式 Target-Url 无该头一律 401 - gateway.ini 精简至 [Server]/[Proxy]/[Downstream db] 验收:黑盒 34 断言对 8443 原样全绿(32 passed/2 skipped,与基线一致); 探针实测伪造 X-Auth-User 经 CA 被剥离(echo 实证)、直连 8445 无头 401、 白名单外 403、gateway 宕机 CA 502。core/init.txt 加入 ca/_run.sh。
This commit is contained in:
1 parent
bbea1ee9d8
commit
407ce3c366
19 files changed
+3991
-2523
No files matched your search
@@ -0,0 +1,74 @@
|
||||
#PRJ
|
||||
*.db
|
||||
*.crt
|
||||
*.key
|
||||
|
||||
# Prerequisites
|
||||
*.d
|
||||
|
||||
# Compiled Object files
|
||||
*.slo
|
||||
*.lo
|
||||
*.o
|
||||
*.obj
|
||||
|
||||
# Precompiled Headers
|
||||
*.gch
|
||||
*.pch
|
||||
|
||||
# Linker files
|
||||
*.ilk
|
||||
|
||||
# Debugger Files
|
||||
*.pdb
|
||||
|
||||
# Compiled Dynamic libraries
|
||||
*.so
|
||||
*.dylib
|
||||
*.dll
|
||||
*.so.*
|
||||
|
||||
|
||||
# Fortran module files
|
||||
*.mod
|
||||
*.smod
|
||||
|
||||
# Compiled Static libraries
|
||||
*.lai
|
||||
*.la
|
||||
*.a
|
||||
*.lib
|
||||
|
||||
# Executables
|
||||
*.exe
|
||||
*.out
|
||||
*.app
|
||||
|
||||
# Build directories
|
||||
build/
|
||||
Build/
|
||||
build-*/
|
||||
|
||||
# CMake generated files
|
||||
CMakeFiles/
|
||||
CMakeCache.txt
|
||||
cmake_install.cmake
|
||||
#Makefile
|
||||
install_manifest.txt
|
||||
compile_commands.json
|
||||
|
||||
# Temporary files
|
||||
*.tmp
|
||||
*.log
|
||||
*.bak
|
||||
*.swp
|
||||
|
||||
# vcpkg
|
||||
vcpkg_installed/
|
||||
|
||||
# debug information files
|
||||
*.dwo
|
||||
|
||||
# test output & cache
|
||||
Testing/
|
||||
.cache/
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
VERSION := 1.0.0
|
||||
APP_NAME := ca
|
||||
INSTL_DIR := /usr/local/bin
|
||||
CPP := g++
|
||||
INCS := -I inc/ -I ../libmsg/inc
|
||||
MY_LIBS := -L/usr/lib -L../libmsg/build -lus_http -lus_httpclient -lus_crypt -lus_msg
|
||||
LIBS := -pthread -lcurl -lssl -lz -lcrypto -lboost_json -lboost_program_options -Wl,-rpath,$(shell cd ../libmsg/build && pwd)
|
||||
CPPFLAGS := --std=c++20 -pipe -pthread # -Wall -Werror
|
||||
BUILD_DIR = build
|
||||
$(shell mkdir -p $(BUILD_DIR))
|
||||
|
||||
#--------
|
||||
all: $(BUILD_DIR)/$(APP_NAME).out
|
||||
$(BUILD_DIR)/$(APP_NAME).out: $(BUILD_DIR)/main.o
|
||||
$(CPP) $^ $(LIBS) $(MY_LIBS) -o $@
|
||||
$(BUILD_DIR)/main.o: src/main.cpp inc/DbC.hpp inc/LogC.hpp inc/Conf.hpp inc/ExpBase.hpp inc/LocalTime.hpp
|
||||
$(CPP) $(CPPFLAGS) -c $< $(INCS) -o $@
|
||||
clear:
|
||||
@rm -rvf build/
|
||||
+142
@@ -0,0 +1,142 @@
|
||||
# ca
|
||||
|
||||
UniServe Framework 的中央认证服务(Central Auth),C++20 实现,v1.0.0 起承接原 gateway v0.8.0 的全部认证职责。
|
||||
|
||||
## 拓扑定位(waf → ca → gateway 前置拓扑)
|
||||
|
||||
```
|
||||
客户端 ──HTTPS──▶ waf:443 (终止 TLS + coraza 规则) ──HTTP──▶ ca:8443 (认证+前置转发) ──HTTP──▶ gateway:8445 (纯转发) ──▶ static:9200 / dnfs2:8006 / core:5200
|
||||
```
|
||||
|
||||
- CA 接管原 gateway 的 **8443** 端口,WAF upstream 配置不变(`waf/config.yaml` 仍指向 8443)。
|
||||
- gateway 退居 **8445**(仅 loopback),退化为纯转发服务,不再验签。
|
||||
- 验签在 CA 本地完成(RSA 密钥同进程),对受保护请求零网络跳。
|
||||
- 下游 core / dnfs2 / 前端 / WAF 全部零改动。
|
||||
|
||||
## 职责
|
||||
|
||||
- **账号鉴权**:`/register` 注册(需邮箱验证码,经 pymsg 调 smtp:8008 校验)、`/login` 登录,签发 RSA 签名的 Token。
|
||||
- **Token 生命周期**:`/logout`(jti 吊销)、`/refresh_token`(宽限期刷新 + 次数上限)、`/whoami`、`/admin/verify`(供 core 等内部服务调用)。
|
||||
- **前置转发层**:通配 `GET/POST *` 把请求转发到唯一上游 gateway:8445:
|
||||
- 无 `Target-Url` 头 = 公开请求:剥离伪造身份头(`X-Auth-User`/`X-Auth-Admin`)后直转。
|
||||
- 有 `Target-Url` 头 = 受保护请求:本地验签(RSA + 过期 + jti 黑名单 + 用户版本号),通过后注入 `X-Auth-User`(管理员追加 `X-Auth-Admin: 1`)与 `Authorization: Bearer` 再转发。
|
||||
- 上游不可达/超时统一返回 `502 Bad Gateway`(区别于 CA 自身故障的 500)。
|
||||
- **WebSocket 反向代理(仅管理员)**:`[WsProxy]` 段配置的路径在 HTTP Upgrade 握手时校验 Token(`is_admin=true`),通过后透明桥接到后端 ws:// 或 wss:// 服务。
|
||||
- **CAPTCHA 代理**:`GET /captcha/image` 代理 captcha2 签发图形验证码(PNG + `X-Captcha-Token` 响应头,10 次/分钟/IP,禁缓存)。
|
||||
- **邮箱验证码**:`/register/send_code`、`/reset/send_code` 发送 6 位验证码邮件(10 分钟有效);`/reset_password` 重置密码(防枚举恒成功 + 改密后版本号吊销全端 Token)。
|
||||
- **速率限制**:登录 5 次/5 分钟、注册 3 次/10 分钟、发验证码 5 次/10 分钟(IP 级)+ 3 次/小时/10 次/天(邮箱级),按 `X-Forwarded-For` 客户端 IP 计数(由 WAF 注入),超限一律返回 429。
|
||||
- **OPTIONS 预检**:由 libhttp 处理,`Access-Control-Allow-Origin` 反射请求的 Origin 头(见审计 P2-3)。
|
||||
|
||||
## 身份头契约(strip-then-set)
|
||||
|
||||
| 头 | 方向 | 语义 |
|
||||
|----|------|------|
|
||||
| `X-Auth-User` | CA → gateway → 下游 | 已认证用户名(邮箱)。客户端携带的同名单被 CA 剥离,仅 CA 注入的值可信 |
|
||||
| `X-Auth-Admin: 1` | CA → gateway → 下游 | 仅当 Token `is_admin=true` 时注入 |
|
||||
| `Authorization: Bearer <token>` | CA → gateway → 下游 | Token 来自 Cookie 时由 CA 补齐;来自客户端 Authorization 头时透传 |
|
||||
|
||||
gateway 侧仅做**存在性不变量**检查(显式 `Target-Url` 请求必须携带 `X-Auth-User`,否则 401),不复验签名——信任边界是 loopback。
|
||||
|
||||
## 安全特性
|
||||
|
||||
| 特性 | 实现 |
|
||||
|------|------|
|
||||
| 密码哈希 | PBKDF2-HMAC-SHA256,100000 轮,16 字节随机盐,32 字节哈希 |
|
||||
| 密码强度校验 | 至少 8 字符;大写/小写/数字/特殊字符中至少 3 类 |
|
||||
| Token 机制 | `Base64(payload) + "_" + RSA-Sign(payload)`,验签 + 过期校验(默认 1 小时) |
|
||||
| Token 吊销 | 双轨纯内存:jti 黑名单(5000 条阈值清理)+ 用户版本号(改密全端吊销) |
|
||||
| 常数时间比较 | PBKDF2 验证用 `diff |= hash[i] ^ expected[i]` 防 timing attack |
|
||||
| 防用户枚举 | 登录对不存在用户执行 dummy PBKDF2 拉平时序;重置密码恒返回成功 |
|
||||
| CAPTCHA | 统一走 captcha2 远端校验(Token 一次性消费);`/login` 强制 |
|
||||
| TLS 可选 | `Server.tls = off` 时纯 HTTP(配合 WAF),`on` 时用 `full_chain.crt` / `server.key` |
|
||||
| 下游容错 | pymsg 客户端 `retry_on_refused`:db/confs 未就绪时阻塞等待;日志客户端 fail-safe 静默丢弃 |
|
||||
|
||||
## 接口
|
||||
|
||||
### `GET /captcha/image`
|
||||
|
||||
代理 captcha2 签发图形验证码:响应体为 PNG,答案 Token 在 `X-Captcha-Token` 响应头(64 位 hex,一次性消费),`Cache-Control: no-store`。限流 10 次/分钟/IP(429)。
|
||||
|
||||
### `POST /login`
|
||||
|
||||
```json
|
||||
{
|
||||
"username": "user@example.com",
|
||||
"password": "P@ssw0rd",
|
||||
"captcha_token": "a1b2c3d4...",
|
||||
"captcha_answer": "Ab3xy"
|
||||
}
|
||||
```
|
||||
|
||||
成功:`{"success": true, "data": {"token": "<base64_payload>_<rsa_sign>"}}`,同时签发 `HttpOnly + Secure + SameSite=Lax` 的 `q4_token` Cookie。
|
||||
错误码:`400`(参数/CAPTCHA 错误)、`401`(账号/密码错)、`429`(限流)。
|
||||
|
||||
### `POST /register` / `POST /register/send_code` / `POST /reset/send_code` / `POST /reset_password`
|
||||
|
||||
与 gateway v0.8.0 契约完全一致(详见 `docs/reports/auth_service_plan.md` §5 与黑盒 tests/blackbox/test_gateway.py)。
|
||||
|
||||
### `POST /logout` / `POST /refresh_token` / `POST /whoami` / `POST /admin/verify`
|
||||
|
||||
- `/logout`:吊销当前 Token 的 jti 并清除 Cookie。
|
||||
- `/refresh_token`:宽限期(iat + keep_time × refresh_grace_multiplier)内换发新 Token,旧 jti 吊销,刷新次数超 `max_refresh` 后需重新登录。
|
||||
- `/whoami`:恒 200,未登录返回 `{"login": false}`。
|
||||
- `/admin/verify`:fail-closed,非管理员/无效 Token 一律 401(供 core:5200 管理员接口鉴权)。
|
||||
|
||||
### `GET/POST *`(前置转发)
|
||||
|
||||
```
|
||||
Authorization: Bearer <base64_payload>_<rsa_sign> // 或 Cookie: q4_token=...
|
||||
Target-Url: http://127.0.0.1:8006 // 可选;携带即视为受保护请求
|
||||
```
|
||||
|
||||
- 无 `Target-Url`:公开请求,剥身份头直转 gateway(由其 `Proxy.default` 路由)。
|
||||
- 有 `Target-Url`:验签通过后注入身份头转发;`Target-Url` 白名单由 gateway 侧 `Proxy.opened` 裁决(403)。
|
||||
- 上游故障:`502 {"success": false, "message": "Bad Gateway"}`。
|
||||
|
||||
### WebSocket 代理(`[WsProxy]` 配置的路径)
|
||||
|
||||
握手阶段从 `Authorization: Bearer` / `Cookie: q4_token` 提取 Token,验签且 `is_admin=true` 才升级连接并桥接后端;否则拒绝。路由精确匹配,优先级高于 HTTP 通配 `*`。
|
||||
|
||||
## 配置
|
||||
|
||||
两层加载:**confs 统一配置服务**(启动时 pymsg `get` 拉取 `ca.ini`,`retry_on_refused=true`)→ **环境变量覆盖**(最高优先级)。
|
||||
|
||||
配置文件:[confs/configs/ca.ini](../confs/configs/ca.ini)。
|
||||
|
||||
- `Server.address`/`port`/`tls`:监听地址/端口/TLS(默认 `127.0.0.1:8443`,env `LISTEN_ADDR`/`LISTEN_PORT`)。
|
||||
- `Upstream.gateway`:唯一上游 gateway 地址(默认 `http://127.0.0.1:8445`,env `UPSTREAM_GATEWAY`)。
|
||||
- `Token.*`:`keep_time` / `refresh_grace_multiplier` / `max_refresh`。
|
||||
- `Admin.users`:管理员白名单(分号分隔邮箱),登录命中时 Token payload 写入 `is_admin=true`。
|
||||
- `CORS.allowed_origins`:已读取但尚未用于预检响应(审计 P2-3)。
|
||||
- `WsProxy.routes`:`/path=ws://host:port/target`,分号分隔。
|
||||
- `Downstream.*`:db/captcha/smtp 地址(env `DB_*`/`CAPTCHA_*`/`SMTP_*`/`CONFS_*`)。
|
||||
|
||||
## 编译与运行
|
||||
|
||||
```bash
|
||||
bash _set.sh # make clean && make
|
||||
bash _run.sh # 从 confs 拉取配置启动(先 sleep 2 等待 confs/db)
|
||||
```
|
||||
|
||||
命令行参数:`-h/--help`、`-v/--version`。日志经 LogC 异步批量推送 db:8005 `Logs` 表(service 名 `ca`)。
|
||||
|
||||
## 目录结构
|
||||
|
||||
```
|
||||
ca/
|
||||
├── src/
|
||||
│ └── main.cpp # 全部实现:认证 handler、Token 机制、限流、前置转发层、WS 代理
|
||||
├── inc/
|
||||
│ ├── DbC.hpp # db 服务客户端(pymsg,retry_on_refused=true;client 名 "ca")
|
||||
│ ├── LogC.hpp # 统一日志客户端(异步批量,fail-safe;client 名 "ca-log")
|
||||
│ ├── Conf.hpp # ConfIni:ini 解析 + env 覆盖
|
||||
│ ├── ExpBase.hpp # 异常基类(带 source_location)
|
||||
│ └── LocalTime.hpp # 本地时间工具
|
||||
├── Makefile
|
||||
├── _set.sh / _run.sh
|
||||
└── README.md
|
||||
```
|
||||
|
||||
## 版本
|
||||
|
||||
`1.0.0`:认证独立化首发——自 gateway v0.8.0 整体搬迁全部认证资产(逐字保留实现),新增前置转发层(固定上游 + strip-then-set 身份头契约 + 上游 502 语义)。原 gateway 同步瘦身为 v0.9.0 纯转发服务。
|
||||
Executable
+10
@@ -0,0 +1,10 @@
|
||||
#!/bin/bash
|
||||
# ca 启动脚本
|
||||
# 配置统一由 confs 服务管理(confs/configs/ca.ini),
|
||||
# 启动时从 confs(CONFS_HOST:CONFS_PORT,默认 127.0.0.1:8010)拉取配置。
|
||||
cd "$(dirname "$0")" || exit
|
||||
|
||||
# 等待依赖服务启动(confs/db)
|
||||
sleep 2
|
||||
|
||||
exec ./build/ca.out "$@"
|
||||
Executable
+3
@@ -0,0 +1,3 @@
|
||||
#!/bin/bash
|
||||
cd "$(dirname "$0")" || exit
|
||||
make clear && make $@
|
||||
@@ -0,0 +1,77 @@
|
||||
#pragma once
|
||||
#include <iostream>
|
||||
#include <exception>
|
||||
#include <string>
|
||||
#include <sstream>
|
||||
#include <cstdlib>
|
||||
#include <boost/property_tree/ptree.hpp>
|
||||
#include <boost/property_tree/ini_parser.hpp>
|
||||
namespace property_tree = boost::property_tree;
|
||||
using ptree = property_tree::ptree;
|
||||
namespace qing {
|
||||
|
||||
class ConfIni {
|
||||
public:
|
||||
/// 默认构造(空配置,后续通过赋值覆盖,仅供 confs 加载流程内部使用)
|
||||
ConfIni() = default;
|
||||
|
||||
/// 从文件路径解析 ini(保留用于开发/调试模式)
|
||||
ConfIni(const std::string &filename) {
|
||||
if (filename.empty()) return; // 空文件名不报错,后续会通过赋值覆盖
|
||||
try {
|
||||
std::cout << "[conf] 从文件加载配置: " << filename << std::endl;
|
||||
property_tree::ini_parser::read_ini(filename, pt);
|
||||
}
|
||||
catch (const boost::property_tree::ini_parser_error& e) {
|
||||
std::cerr << "[conf] INI 解析错误: " << e.what() << std::endl;
|
||||
throw IniConfigureFileParseException("INI parsing error!");
|
||||
}
|
||||
}
|
||||
|
||||
/// 从 ini 文本字符串解析(用于从 confs 拉取后解析)
|
||||
static ConfIni fromString(const std::string &ini_text) {
|
||||
ConfIni c;
|
||||
try {
|
||||
std::istringstream iss(ini_text);
|
||||
property_tree::ini_parser::read_ini(iss, c.pt);
|
||||
}
|
||||
catch (const boost::property_tree::ini_parser_error& e) {
|
||||
std::cerr << "[conf] INI 文本解析错误: " << e.what() << std::endl;
|
||||
throw IniConfigureFileParseException("INI parsing error!");
|
||||
}
|
||||
return c;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
T get(const std::string &path, const T &_defaut) {
|
||||
return pt.get<T>(path, _defaut);
|
||||
}
|
||||
|
||||
/// 读取环境变量,不存在则返回默认值
|
||||
static std::string env(const char *name, const std::string &default_val = "") {
|
||||
const char *v = std::getenv(name);
|
||||
return v ? std::string(v) : default_val;
|
||||
}
|
||||
|
||||
static int env_int(const char *name, int default_val) {
|
||||
const char *v = std::getenv(name);
|
||||
if (!v || !*v) return default_val;
|
||||
try { return std::stoi(v); }
|
||||
catch (...) { return default_val; }
|
||||
}
|
||||
|
||||
class IniConfigureFileParseException: public std::exception {
|
||||
public:
|
||||
explicit IniConfigureFileParseException(const std::string &msg)
|
||||
: message(msg) {}
|
||||
const char* what() const noexcept override {
|
||||
return message.c_str();
|
||||
}
|
||||
private:
|
||||
std::string message;
|
||||
};
|
||||
|
||||
private:
|
||||
ptree pt;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
#pragma once
|
||||
#include <us/HttpClient.hpp>
|
||||
#include <us/Msg.hpp>
|
||||
#include <boost/json.hpp>
|
||||
namespace qing {
|
||||
namespace json = boost::json;
|
||||
class DbC {
|
||||
public:
|
||||
/* 本机 db 轻量查询的默认超时(秒);慢操作由调用方显式放宽。
|
||||
* smtp/captcha 等其他下游的超时常量见 main.cpp。 */
|
||||
static constexpr double DEFAULT_TIMEOUT = 5.0;
|
||||
|
||||
/// 初始化 db 服务地址(启动时从配置设置,默认 127.0.0.1:8005)
|
||||
static void init(const std::string& host, int port) {
|
||||
_host = host;
|
||||
_port = port;
|
||||
}
|
||||
|
||||
// 【保留】旧版 HTTP 接口:通过 HttpClient POST 到 dbv2 HTTP /execute 端点。
|
||||
// 保留用于兼容性参考,新代码优先使用 call_msg()(pymsg TCP 协议,更可靠)。
|
||||
static json::value call_http(json::value& params) {
|
||||
|
||||
auto client = pmc::net::HttpClient();
|
||||
client.set_header("Content-Type: application/json");
|
||||
std::string payload = json::serialize(params);
|
||||
std::string res = "";
|
||||
std::string url = "http://" + _host + ":" + std::to_string(_port) + "/execute";
|
||||
client.post_stream_sync(url, payload, [&res](const auto& ret) {
|
||||
res += ret;
|
||||
});
|
||||
return json::parse(res);
|
||||
}
|
||||
|
||||
// 新版 pymsg 接口:通过 MsgClient(TCP 长度前缀帧协议)调用 dbv2。
|
||||
// 自动添加 _route = "execute",返回 dbv2 pymsg 完整 JSON 响应
|
||||
// {"success":..., "data":[...]}。
|
||||
// 响应格式与 call_http 兼容,调用方可直接替换。
|
||||
// db 是强依赖,启用 retry_on_refused:db 未启动时阻塞等待,避免启动顺序问题。
|
||||
static json::value call_msg(const json::value& params,
|
||||
double timeout = DEFAULT_TIMEOUT) {
|
||||
json::value jv_route = params;
|
||||
jv_route.as_object()["_route"] = "execute";
|
||||
pmc::net::MsgClient client(_host, _port, "ca");
|
||||
std::string resp = client.call(
|
||||
json::serialize(jv_route), timeout,
|
||||
/*retry_on_refused=*/true);
|
||||
return json::parse(resp);
|
||||
}
|
||||
|
||||
static const std::string& host() { return _host; }
|
||||
static int port() { return _port; }
|
||||
|
||||
private:
|
||||
static inline std::string _host = "127.0.0.1";
|
||||
static inline int _port = 8005;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,411 @@
|
||||
#ifndef EXCEPTION_BASE_H
|
||||
#define EXCEPTION_BASE_H
|
||||
|
||||
#include <exception>
|
||||
#include <string>
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
#include <source_location>
|
||||
#include <sstream>
|
||||
#include <type_traits>
|
||||
|
||||
/**
|
||||
* @brief 通用异常基类
|
||||
*
|
||||
* 特性:
|
||||
* 1. 完整的错误信息管理(错误码、消息、位置信息)
|
||||
* 2. 支持异常链(嵌套异常)
|
||||
* 3. 多态支持(clone、自定义消息)
|
||||
* 4. 线程安全的 what() 缓存
|
||||
* 5. 支持 C++20 source_location
|
||||
*/
|
||||
class ExceptionBase : public std::exception {
|
||||
public:
|
||||
// ============ 构造函数 ============
|
||||
|
||||
explicit ExceptionBase(
|
||||
int code = 0,
|
||||
const std::string& message = "",
|
||||
const std::source_location& location = std::source_location::current()
|
||||
) noexcept
|
||||
: m_code(code)
|
||||
, m_message(message)
|
||||
, m_file(location.file_name())
|
||||
, m_line(location.line())
|
||||
, m_function(location.function_name())
|
||||
, m_what_cache()
|
||||
{
|
||||
}
|
||||
|
||||
// 支持从字符串字面量构造
|
||||
explicit ExceptionBase(
|
||||
const std::string& message,
|
||||
const std::source_location& location = std::source_location::current()
|
||||
) noexcept
|
||||
: m_code(0)
|
||||
, m_message(message)
|
||||
, m_file(location.file_name())
|
||||
, m_line(location.line())
|
||||
, m_function(location.function_name())
|
||||
, m_what_cache()
|
||||
{
|
||||
}
|
||||
|
||||
// ============ 拷贝和移动 ============
|
||||
|
||||
ExceptionBase(const ExceptionBase& other) noexcept
|
||||
: std::exception(other)
|
||||
, m_code(other.m_code)
|
||||
, m_message(other.m_message)
|
||||
, m_file(other.m_file)
|
||||
, m_line(other.m_line)
|
||||
, m_function(other.m_function)
|
||||
, m_nested(other.m_nested ? other.m_nested->clone() : nullptr)
|
||||
, m_what_cache()
|
||||
{
|
||||
}
|
||||
|
||||
ExceptionBase(ExceptionBase&& other) noexcept
|
||||
: std::exception(std::move(other))
|
||||
, m_code(other.m_code)
|
||||
, m_message(std::move(other.m_message))
|
||||
, m_file(std::move(other.m_file))
|
||||
, m_line(other.m_line)
|
||||
, m_function(std::move(other.m_function))
|
||||
, m_nested(std::move(other.m_nested))
|
||||
, m_what_cache(std::move(other.m_what_cache))
|
||||
{
|
||||
other.m_code = 0;
|
||||
other.m_line = 0;
|
||||
}
|
||||
|
||||
virtual ~ExceptionBase() noexcept = default;
|
||||
|
||||
// ============ 赋值运算符 ============
|
||||
|
||||
ExceptionBase& operator=(const ExceptionBase& other) noexcept {
|
||||
if (this != &other) {
|
||||
m_code = other.m_code;
|
||||
m_message = other.m_message;
|
||||
m_file = other.m_file;
|
||||
m_line = other.m_line;
|
||||
m_function = other.m_function;
|
||||
m_nested = other.m_nested ? other.m_nested->clone() : nullptr;
|
||||
m_what_cache.clear();
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
ExceptionBase& operator=(ExceptionBase&& other) noexcept {
|
||||
if (this != &other) {
|
||||
m_code = other.m_code;
|
||||
m_message = std::move(other.m_message);
|
||||
m_file = std::move(other.m_file);
|
||||
m_line = other.m_line;
|
||||
m_function = std::move(other.m_function);
|
||||
m_nested = std::move(other.m_nested);
|
||||
m_what_cache = std::move(other.m_what_cache);
|
||||
other.m_code = 0;
|
||||
other.m_line = 0;
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
// ============ 访问器 ============
|
||||
|
||||
int code() const noexcept { return m_code; }
|
||||
const std::string& message() const noexcept { return m_message; }
|
||||
const std::string& file() const noexcept { return m_file; }
|
||||
int line() const noexcept { return m_line; }
|
||||
const std::string& function() const noexcept { return m_function; }
|
||||
const ExceptionBase* nested() const noexcept { return m_nested.get(); }
|
||||
|
||||
// ============ 异常链操作 ============
|
||||
|
||||
void set_nested(std::unique_ptr<ExceptionBase> nested) noexcept {
|
||||
m_nested = std::move(nested);
|
||||
m_what_cache.clear();
|
||||
}
|
||||
|
||||
template<typename ExType>
|
||||
void set_nested(const ExType& nested_ex) noexcept {
|
||||
static_assert(std::is_base_of<ExceptionBase, ExType>::value,
|
||||
"ExType must inherit from ExceptionBase");
|
||||
m_nested = std::make_unique<ExType>(nested_ex);
|
||||
m_what_cache.clear();
|
||||
}
|
||||
|
||||
// ============ std::exception 接口 ============
|
||||
|
||||
const char* what() const noexcept override {
|
||||
if (m_what_cache.empty()) {
|
||||
build_what_message();
|
||||
}
|
||||
return m_what_cache.c_str();
|
||||
}
|
||||
|
||||
// ============ 多态支持 ============
|
||||
|
||||
virtual std::unique_ptr<ExceptionBase> clone() const {
|
||||
return std::make_unique<ExceptionBase>(*this);
|
||||
}
|
||||
|
||||
// ============ 重新抛出 ============
|
||||
|
||||
[[noreturn]] void rethrow() const {
|
||||
throw *this;
|
||||
}
|
||||
|
||||
protected:
|
||||
// ============ 可重写的消息构建方法 ============
|
||||
|
||||
virtual std::string build_message() const {
|
||||
std::string result;
|
||||
|
||||
// 错误码
|
||||
if (m_code != 0) {
|
||||
result += "[Error " + std::to_string(m_code) + "] ";
|
||||
}
|
||||
|
||||
// 错误消息
|
||||
result += m_message;
|
||||
|
||||
/* ===== 以下「位置信息」与「嵌套异常」暂时以注释方式保留 =====
|
||||
* 原因:黑盒检测发现 what() 的返回值会透传到 HTTP 响应体,
|
||||
* 导致泄露源码文件路径、行号、C++ 模板类型签名(函数名),
|
||||
* 便于攻击者定位代码、确认技术栈、构造针对性 payload(P0-3)。
|
||||
* 排障:完整信息仍由各 handler 通过 std::cerr 输出到服务端日志。
|
||||
* 恢复:如需调试可临时取消下方注释;生产环境必须保持注释状态,
|
||||
* 并确保 what() 不直接进入客户端响应。
|
||||
* ====================================================================
|
||||
// 位置信息
|
||||
if (!m_file.empty()) {
|
||||
result += " (at " + m_file;
|
||||
if (m_line > 0) {
|
||||
result += ":" + std::to_string(m_line);
|
||||
}
|
||||
if (!m_function.empty()) {
|
||||
result += " in " + m_function;
|
||||
}
|
||||
result += ")";
|
||||
}
|
||||
|
||||
// 嵌套异常
|
||||
if (m_nested) {
|
||||
result += "\n Caused by: " + std::string(m_nested->what());
|
||||
}
|
||||
* ==================================================================== */
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private:
|
||||
// ============ 内部方法 ============
|
||||
|
||||
void build_what_message() const noexcept {
|
||||
try {
|
||||
m_what_cache = build_message();
|
||||
} catch (...) {
|
||||
// 降级处理
|
||||
m_what_cache = "Exception: " + m_message;
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
// ============ 成员变量 ============
|
||||
|
||||
int m_code;
|
||||
std::string m_message;
|
||||
std::string m_file;
|
||||
int m_line;
|
||||
std::string m_function;
|
||||
std::unique_ptr<ExceptionBase> m_nested;
|
||||
mutable std::string m_what_cache;
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// 宏定义 - 方案二完整版
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* @brief 抛出指定类型的异常(自动添加 source_location)
|
||||
*
|
||||
* 适用场景:子类构造函数与 ExceptionBase 相同
|
||||
*
|
||||
* 使用示例:
|
||||
* THROW_EX(NetworkException, 404, "Server not found")
|
||||
* THROW_EX(DatabaseException, 100, "Connection timeout")
|
||||
*/
|
||||
#define THROW_EX(ExType, code, msg) \
|
||||
throw ExType(code, msg, std::source_location::current())
|
||||
|
||||
/**
|
||||
* @brief 抛出异常(仅消息,不传错误码)
|
||||
*
|
||||
* 适用场景:子类支持只传消息的构造函数
|
||||
*
|
||||
* 使用示例:
|
||||
* THROW_EX_MSG(ValidationException, "Invalid input data")
|
||||
*/
|
||||
#define THROW_EX_MSG(ExType, msg) \
|
||||
throw ExType(msg, std::source_location::current())
|
||||
|
||||
/**
|
||||
* @brief 抛出异常(支持任意构造函数参数)
|
||||
*
|
||||
* 适用场景:子类有额外的构造函数参数
|
||||
*
|
||||
* 使用示例:
|
||||
* // 子类定义
|
||||
* class FileException : public ExceptionBase {
|
||||
* public:
|
||||
* FileException(int code, const std::string& msg,
|
||||
* const std::string& filename,
|
||||
* const std::source_location& loc = std::source_location::current())
|
||||
* : ExceptionBase(code, msg, loc), m_filename(filename) {}
|
||||
* };
|
||||
*
|
||||
* // 使用
|
||||
* THROW_EX_CUSTOM(FileException, 404, "File not found", "/etc/config.txt")
|
||||
*/
|
||||
#define THROW_EX_CUSTOM(ExType, ...) \
|
||||
throw ExType(__VA_ARGS__, std::source_location::current())
|
||||
|
||||
/**
|
||||
* @brief 抛出异常并携带嵌套异常
|
||||
*
|
||||
* 适用场景:需要保留原始异常信息
|
||||
*
|
||||
* 使用示例:
|
||||
* try {
|
||||
* connect_to_server();
|
||||
* } catch (const NetworkException& e) {
|
||||
* THROW_EX_NESTED(ServiceException, 500, "Service failed", e)
|
||||
* }
|
||||
*/
|
||||
#define THROW_EX_NESTED(ExType, code, msg, nested_ex) \
|
||||
do { \
|
||||
auto ex = ExType(code, msg, std::source_location::current()); \
|
||||
ex.set_nested(nested_ex); \
|
||||
throw ex; \
|
||||
} while(0)
|
||||
|
||||
/**
|
||||
* @brief 抛出异常并携带嵌套异常(支持自定义参数)
|
||||
*
|
||||
* 使用示例:
|
||||
* THROW_EX_NESTED_CUSTOM(FileException, 500, "Read failed",
|
||||
* original_ex, "/data/file.txt")
|
||||
*/
|
||||
#define THROW_EX_NESTED_CUSTOM(ExType, code, msg, nested_ex, ...) \
|
||||
do { \
|
||||
auto ex = ExType(code, msg, __VA_ARGS__, std::source_location::current()); \
|
||||
ex.set_nested(nested_ex); \
|
||||
throw ex; \
|
||||
} while(0)
|
||||
|
||||
/**
|
||||
* @brief 检查条件,失败则抛出异常
|
||||
*
|
||||
* 使用示例:
|
||||
* THROW_IF(ptr == nullptr, NullPointerException, 100, "Pointer is null")
|
||||
* THROW_IF(data.empty(), ValidationException, 200, "Data cannot be empty")
|
||||
*/
|
||||
#define THROW_IF(cond, ExType, code, msg) \
|
||||
do { \
|
||||
if (cond) { \
|
||||
THROW_EX(ExType, code, msg); \
|
||||
} \
|
||||
} while(0)
|
||||
|
||||
/**
|
||||
* @brief 检查条件,失败则抛出异常(仅消息)
|
||||
*/
|
||||
#define THROW_IF_MSG(cond, ExType, msg) \
|
||||
do { \
|
||||
if (cond) { \
|
||||
THROW_EX_MSG(ExType, msg); \
|
||||
} \
|
||||
} while(0)
|
||||
|
||||
/**
|
||||
* @brief 检查条件,失败则抛出异常(自定义参数)
|
||||
*/
|
||||
#define THROW_IF_CUSTOM(cond, ExType, ...) \
|
||||
do { \
|
||||
if (cond) { \
|
||||
THROW_EX_CUSTOM(ExType, __VA_ARGS__); \
|
||||
} \
|
||||
} while(0)
|
||||
|
||||
/**
|
||||
* @brief 检查条件,失败则抛出异常(带嵌套异常)
|
||||
*/
|
||||
#define THROW_IF_NESTED(cond, ExType, code, msg, nested_ex) \
|
||||
do { \
|
||||
if (cond) { \
|
||||
THROW_EX_NESTED(ExType, code, msg, nested_ex); \
|
||||
} \
|
||||
} while(0)
|
||||
|
||||
/**
|
||||
* @brief 检查条件,失败则抛出异常(带嵌套异常和自定义参数)
|
||||
*/
|
||||
#define THROW_IF_NESTED_CUSTOM(cond, ExType, code, msg, nested_ex, ...) \
|
||||
do { \
|
||||
if (cond) { \
|
||||
THROW_EX_NESTED_CUSTOM(ExType, code, msg, nested_ex, __VA_ARGS__); \
|
||||
} \
|
||||
} while(0)
|
||||
|
||||
// ============================================================================
|
||||
// 辅助宏 - 用于格式化错误消息
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* @brief 格式化错误消息(使用流式语法)
|
||||
*
|
||||
* 使用示例:
|
||||
* THROW_EX_FMT(NetworkException, 404, "Failed to connect to " << host << ":" << port)
|
||||
*/
|
||||
#define THROW_EX_FMT(ExType, code, msg_expr) \
|
||||
do { \
|
||||
std::ostringstream _oss; \
|
||||
_oss << msg_expr; \
|
||||
THROW_EX(ExType, code, _oss.str()); \
|
||||
} while(0)
|
||||
|
||||
/**
|
||||
* @brief 格式化错误消息(使用 printf 风格)
|
||||
*
|
||||
* 使用示例:
|
||||
* THROW_EX_PRINTF(NetworkException, 404, "Failed to connect to %s:%d", host.c_str(), port)
|
||||
*/
|
||||
#define THROW_EX_PRINTF(ExType, code, fmt, ...) \
|
||||
do { \
|
||||
char _buf[1024]; \
|
||||
snprintf(_buf, sizeof(_buf), fmt, __VA_ARGS__); \
|
||||
THROW_EX(ExType, code, std::string(_buf)); \
|
||||
} while(0)
|
||||
|
||||
/**
|
||||
* @brief 条件检查 + 格式化消息
|
||||
*/
|
||||
#define THROW_IF_FMT(cond, ExType, code, msg_expr) \
|
||||
do { \
|
||||
if (cond) { \
|
||||
THROW_EX_FMT(ExType, code, msg_expr); \
|
||||
} \
|
||||
} while(0)
|
||||
|
||||
/**
|
||||
* @brief 条件检查 + printf 风格格式化
|
||||
*/
|
||||
#define THROW_IF_PRINTF(cond, ExType, code, fmt, ...) \
|
||||
do { \
|
||||
if (cond) { \
|
||||
THROW_EX_PRINTF(ExType, code, fmt, __VA_ARGS__); \
|
||||
} \
|
||||
} while(0)
|
||||
|
||||
#endif // EXCEPTION_BASE_H
|
||||
@@ -0,0 +1,71 @@
|
||||
#pragma once
|
||||
#include <chrono>
|
||||
#include <string>
|
||||
#include <optional>
|
||||
#include <sstream>
|
||||
#include <iomanip>
|
||||
#include <ctime>
|
||||
namespace qing {
|
||||
|
||||
class LocalTime {
|
||||
public:
|
||||
// 获取当前本地时间字符串
|
||||
static std::string getCurrent(const std::string& format = "%Y-%m-%d %H:%M:%S") {
|
||||
auto now = std::chrono::system_clock::now();
|
||||
std::time_t t = std::chrono::system_clock::to_time_t(now);
|
||||
std::tm tm_buf{};
|
||||
localtime_r(&t, &tm_buf);
|
||||
char buf[128];
|
||||
std::strftime(buf, sizeof(buf), format.c_str(), &tm_buf);
|
||||
return std::string(buf);
|
||||
}
|
||||
|
||||
// 解析时间字符串为 system_clock::time_point(本地时间 → UTC)
|
||||
static std::optional<std::chrono::system_clock::time_point> parse(
|
||||
const std::string& str,
|
||||
const std::string& format = "%Y-%m-%d %H:%M:%S"
|
||||
) {
|
||||
std::tm tm_buf{};
|
||||
std::istringstream iss{str};
|
||||
iss >> std::get_time(&tm_buf, format.c_str());
|
||||
if (iss.fail()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
// mktime 将本地时间转换为 time_t(UTC)
|
||||
std::time_t t = std::mktime(&tm_buf);
|
||||
if (t == -1) {
|
||||
return std::nullopt;
|
||||
}
|
||||
return std::chrono::system_clock::from_time_t(t);
|
||||
}
|
||||
|
||||
// ----- 过期判断 -----
|
||||
static bool isExpired(const std::chrono::system_clock::time_point& tp) {
|
||||
return std::chrono::system_clock::now() > tp;
|
||||
}
|
||||
|
||||
template <typename Rep, typename Period>
|
||||
static bool isExpired(const std::chrono::system_clock::time_point& tp,
|
||||
const std::chrono::duration<Rep, Period>& gracePeriod) {
|
||||
return std::chrono::system_clock::now() > tp + gracePeriod;
|
||||
}
|
||||
|
||||
static bool isExpired(const std::string& timeStr,
|
||||
const std::string& format = "%Y-%m-%d %H:%M:%S") {
|
||||
auto parsed = parse(timeStr, format);
|
||||
if (!parsed) return true;
|
||||
return isExpired(*parsed);
|
||||
}
|
||||
|
||||
template <typename Rep, typename Period>
|
||||
static bool isExpired(const std::string& timeStr,
|
||||
const std::chrono::duration<Rep, Period>& gracePeriod,
|
||||
const std::string& format = "%Y-%m-%d %H:%M:%S") {
|
||||
auto parsed = parse(timeStr, format);
|
||||
if (!parsed) return true;
|
||||
return isExpired(*parsed, gracePeriod);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
}
|
||||
+206
@@ -0,0 +1,206 @@
|
||||
#pragma once
|
||||
|
||||
/** LogC — ca 统一日志客户端(C++ 版,对应各 Python 服务的 log_client.py)
|
||||
*
|
||||
* 设计要点(与 Python 版对齐):
|
||||
* - 通过 pymsg TCP 调用 dbv2 execute 路由,逐条 INSERT 到 Logs 表
|
||||
* - 异步非阻塞:基于 pmc::net::AsyncQueue 内存队列 + 后台线程批量推送,不拖慢业务
|
||||
* - fail-safe:db 服务不可用时静默丢弃(仅 stderr 提示),绝不影响业务
|
||||
* - 缓冲批量:队列满 50 条或超时 2 秒时自动 flush
|
||||
* - 背压保护:队列上限 10000 条,满时丢最旧日志,防止内存无限增长
|
||||
*
|
||||
* 用法:
|
||||
* #include "LogC.hpp"
|
||||
*
|
||||
* qing::LogC::instance().info("ca", "启动完成");
|
||||
* qing::LogC::instance().error("ca", "数据库连接失败",
|
||||
* std::nullopt, R"({"errno":2002})");
|
||||
*
|
||||
* // 进程退出前:
|
||||
* qing::LogC::instance().shutdown(); // 停止后台线程并 flush 剩余日志(幂等)
|
||||
*/
|
||||
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <cstdio>
|
||||
#include <ctime>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <boost/json.hpp>
|
||||
|
||||
#include <us/AsyncQueue.hpp>
|
||||
#include <us/Msg.hpp>
|
||||
|
||||
namespace qing {
|
||||
|
||||
namespace json = boost::json;
|
||||
|
||||
class LogC {
|
||||
public:
|
||||
struct Entry {
|
||||
std::string service;
|
||||
std::string level;
|
||||
std::string message;
|
||||
std::optional<std::string> trace_id;
|
||||
std::optional<std::string> extra; // 已序列化的 JSON 字符串(存 Logs.extra TEXT)
|
||||
};
|
||||
|
||||
static LogC& instance() {
|
||||
static LogC inst;
|
||||
return inst;
|
||||
}
|
||||
|
||||
// ---- 公开接口 --------------------------------------------------------
|
||||
|
||||
// 推送一条日志(非阻塞,立即返回)
|
||||
void push(const std::string& service, const std::string& level,
|
||||
const std::string& message,
|
||||
std::optional<std::string> trace_id = std::nullopt,
|
||||
std::optional<std::string> extra = std::nullopt) {
|
||||
_queue.push(Entry{service, level, message,
|
||||
std::move(trace_id), std::move(extra)});
|
||||
}
|
||||
|
||||
void debug(const std::string& service, const std::string& message,
|
||||
std::optional<std::string> trace_id = std::nullopt,
|
||||
std::optional<std::string> extra = std::nullopt) {
|
||||
push(service, "DEBUG", message, std::move(trace_id), std::move(extra));
|
||||
}
|
||||
void info(const std::string& service, const std::string& message,
|
||||
std::optional<std::string> trace_id = std::nullopt,
|
||||
std::optional<std::string> extra = std::nullopt) {
|
||||
push(service, "INFO", message, std::move(trace_id), std::move(extra));
|
||||
}
|
||||
void warning(const std::string& service, const std::string& message,
|
||||
std::optional<std::string> trace_id = std::nullopt,
|
||||
std::optional<std::string> extra = std::nullopt) {
|
||||
push(service, "WARNING", message, std::move(trace_id), std::move(extra));
|
||||
}
|
||||
void error(const std::string& service, const std::string& message,
|
||||
std::optional<std::string> trace_id = std::nullopt,
|
||||
std::optional<std::string> extra = std::nullopt) {
|
||||
push(service, "ERROR", message, std::move(trace_id), std::move(extra));
|
||||
}
|
||||
void critical(const std::string& service, const std::string& message,
|
||||
std::optional<std::string> trace_id = std::nullopt,
|
||||
std::optional<std::string> extra = std::nullopt) {
|
||||
push(service, "CRITICAL", message, std::move(trace_id), std::move(extra));
|
||||
}
|
||||
|
||||
// 手动 flush:通知后台线程立即发送当前积攒的批次,并等待其完成
|
||||
void flush() {
|
||||
_queue.flush();
|
||||
}
|
||||
|
||||
// 停止后台线程并 flush 剩余日志(进程退出前调用,幂等)
|
||||
void shutdown() {
|
||||
_queue.shutdown();
|
||||
}
|
||||
|
||||
// ---- 配置(构造后、首次推送前可改)------------------------------------
|
||||
void set_endpoint(const std::string& addr, int port) {
|
||||
std::lock_guard<std::mutex> lock(_endpoint_mtx);
|
||||
_db_addr = addr;
|
||||
_db_port = port;
|
||||
}
|
||||
|
||||
private:
|
||||
LogC()
|
||||
: _queue(
|
||||
[this](std::vector<Entry>&& batch) { this->_send_batch(std::move(batch)); },
|
||||
{_BATCH_SIZE, _FLUSH_INTERVAL, _MAX_QUEUE_SIZE, 0.5},
|
||||
[](const std::exception& e, std::vector<Entry>&&) {
|
||||
std::cerr << "[LogC] 发送失败: " << e.what() << std::endl;
|
||||
})
|
||||
{
|
||||
_queue.start();
|
||||
}
|
||||
|
||||
~LogC() { shutdown(); }
|
||||
|
||||
LogC(const LogC&) = delete;
|
||||
LogC& operator=(const LogC&) = delete;
|
||||
|
||||
// 通过 pymsg 使用 execute 路由逐条 INSERT 到 dbv2 Logs 表
|
||||
void _send_batch(const std::vector<Entry>& batch) {
|
||||
std::string addr;
|
||||
int port;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(_endpoint_mtx);
|
||||
addr = _db_addr;
|
||||
port = _db_port;
|
||||
}
|
||||
|
||||
const std::string now = _utc_now_iso(); // 批次共享同一时间戳(对齐 Python 版)
|
||||
try {
|
||||
pmc::net::MsgClient client(addr, port, "ca-log");
|
||||
for (const auto& e : batch) {
|
||||
json::object values;
|
||||
values["ts"] = now;
|
||||
values["service"] = e.service;
|
||||
values["level"] = e.level;
|
||||
values["message"] = e.message;
|
||||
values["trace_id"] = e.trace_id ? json::value(*e.trace_id)
|
||||
: json::value(nullptr);
|
||||
values["extra"] = e.extra ? json::value(*e.extra)
|
||||
: json::value(nullptr);
|
||||
|
||||
json::object payload;
|
||||
payload["_route"] = "execute";
|
||||
payload["action"] = "insert";
|
||||
payload["table"] = "Logs";
|
||||
payload["values"] = std::move(values);
|
||||
|
||||
const std::string resp = client.call(
|
||||
json::serialize(payload), _TIMEOUT,
|
||||
/*retry_on_refused=*/false); // 日志是 fail-safe,db 不可用时静默丢弃,绝不阻塞重试
|
||||
auto obj = json::parse(resp).as_object();
|
||||
if (!obj.at("success").as_bool()) {
|
||||
std::string err = "unknown";
|
||||
if (auto* ev = obj.if_contains("error")) {
|
||||
if (ev->is_string()) err = ev->as_string().c_str();
|
||||
}
|
||||
std::cerr << "[LogC] INSERT 失败: " << err << std::endl;
|
||||
}
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
// fail-safe:db 不可用时静默丢弃,绝不影响业务
|
||||
std::cerr << "[LogC] 发送失败: " << e.what() << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
// UTC ISO8601 时间戳(对齐 Python datetime.now(timezone.utc).isoformat())
|
||||
// 形如 2026-07-29T08:30:00.123456+00:00,字典序即时间序,便于 db 按 ts 清理
|
||||
static std::string _utc_now_iso() {
|
||||
const auto now = std::chrono::system_clock::now();
|
||||
const auto t = std::chrono::system_clock::to_time_t(now);
|
||||
const auto us = std::chrono::duration_cast<std::chrono::microseconds>(
|
||||
now.time_since_epoch()).count() % 1000000;
|
||||
std::tm tm{};
|
||||
::gmtime_r(&t, &tm);
|
||||
char buf[40];
|
||||
std::snprintf(buf, sizeof buf,
|
||||
"%04d-%02d-%02dT%02d:%02d:%02d.%06d+00:00",
|
||||
tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday,
|
||||
tm.tm_hour, tm.tm_min, tm.tm_sec, static_cast<int>(us));
|
||||
return buf;
|
||||
}
|
||||
|
||||
// ---- 配置项 -----------------------------------------------------------
|
||||
std::string _db_addr = "127.0.0.1";
|
||||
int _db_port = 8005;
|
||||
std::mutex _endpoint_mtx; // 保护 _db_addr/_db_port 读写(set_endpoint 与后台线程并发)
|
||||
|
||||
static constexpr size_t _BATCH_SIZE = 50; // 队列满 50 条时 flush
|
||||
static constexpr double _FLUSH_INTERVAL = 2.0; // 超时 2 秒强制 flush
|
||||
static constexpr size_t _MAX_QUEUE_SIZE = 10000; // 队列上限,满时丢最旧
|
||||
static constexpr double _TIMEOUT = 5.0; // socket 超时(秒)
|
||||
|
||||
// ---- 核心队列 ---------------------------------------------------------
|
||||
pmc::net::AsyncQueue<Entry> _queue;
|
||||
};
|
||||
|
||||
} // namespace qing
|
||||
+2463
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user