Compare commits

...
7 Commits
Author SHA1 Message Date
qing 9fc3542771 smtp: CLI 标准化补完(--host/--log-level/--instance + log_client 级别过滤)
- argparse 新增 --host(别名 --addr)、--log-level、--instance
- --log-level 经 logs.set_log_level() 真正订阅日志过滤
- log_client.py: 新增 _LOG_LEVELS/set_log_level()/_normalize_level() + push() 级别过滤
- 启动日志打印 instance/log-level 及配置来源说明
- services.yaml: smtp 声明 --instance 和 --log-level
- 注: SMTP 账号走 .env/ENV(密码不进 CLI),与全项目约定一致
2026-08-10 12:05:46 +08:00
qing d0ee948339 ai_review: CLI 标准化(argparse + --host/--port/--log-level/--instance)
- 新增 --host/--port 覆盖 $AI_REVIEW_HOST/$AI_REVIEW_PORT(CLI > ENV > 默认值)
- 新增 --log-level debug|info|warn|error(启动打印,ai_review 无 db 日志客户端故仅作标准接口)
- 新增 --instance 实例名(注册中心用)
- 配置加载从模块顶层移入 __main__,argparse 解析
- services.yaml: ai_review 声明 --instance 和 --log-level
2026-08-10 12:02:47 +08:00
qing 29eac70f24 captcha2: CLI 标准化(--host/--config/--log-level/--instance + LogC 级别过滤)
- 新增 --host(别名 --addr/-a)覆盖 [server] host
- 新增 --config 覆盖 confs 拉取的配置名(默认 captcha.ini)
- 新增 --log-level debug|info|warn|error(LogC 加级别过滤)
- 新增 --instance 实例名(注册中心用)
- LogC.hpp: 新增 set_level() + push() 级别过滤(atomic 阈值,大写兼容)
- 配置优先级: CLI > ENV > confs > 默认值
- services.yaml: captcha2 声明 --instance 和 --log-level
2026-08-10 12:00:34 +08:00
qing 8eb14cad4a gateway: CLI 标准化(--host/--port/--config/--log-level/--instance + LogC 级别过滤)
- 新增 --host/--port 覆盖 [Server] address/port
- 新增 --config 覆盖 confs 拉取的配置名(默认 gateway.ini)
- 新增 --log-level debug|info|warn|error(LogC 加级别过滤)
- 新增 --instance 实例名(注册中心用)
- LogC.hpp: 新增 set_level() + push() 级别过滤(atomic 阈值,大写兼容)
- 配置优先级: CLI > ENV > confs > 默认值
- services.yaml: gateway 声明 --instance 和 --log-level
2026-08-10 11:55:54 +08:00
qing 4df510712e ca: CLI 标准化(--host/--port/--log-level/--instance + LogC 级别过滤)
- 新增 --host/--port 覆盖 [Server] address/port
- 新增 --config 覆盖 confs 拉取的配置名(默认 ca.ini)
- 新增 --log-level debug|info|warn|error(LogC 加级别过滤)
- 新增 --instance 实例名(注册中心用)
- LogC.hpp: 新增 set_level() + push() 级别过滤(atomic 阈值)
- 配置优先级: CLI > ENV > confs > 默认值
- services.yaml: ca 声明 --instance 和 --log-level
2026-08-10 11:51:43 +08:00
qing 4275f002e0 redirect80: CLI 标准化(argparse + log_client 级别过滤)
- 新增 --host/--port/--redirect-port/--redirect-code CLI 参数(覆盖 confs+ENV)
- 新增 --log-level debug|info|warn|error(log_client 加级别过滤)
- 新增 --instance 实例名(注册中心用)
- 自定义 --help 用法文本(argparse,confs 不可用时也能 --help)
- log_client: 新增 set_log_level() + push() 级别过滤
- 修复: redirect_code 配置项原加载但未使用,do_GET/do_HEAD 改为用 _config['redirect_code']
- 配置优先级: CLI > ENV > confs > 默认值
- services.yaml: redirect80 声明 --instance 和 --log-level
2026-08-10 11:45:27 +08:00
qing 01c6d0cb1f waf: CLI 标准化(--log-level/--instance + config.yaml 字段全 CLI 覆盖)
- 新增 --listen/--host (覆盖 listen)、--upstream、--tls-cert、--tls-key、--rules-dir
- 新增 --log-level debug|info|warn|error(logclient 加级别过滤)
- 新增 --instance 实例名(注册中心用)
- 自定义 --help 用法文本
- logclient: 新增 SetLevel() + Debug() + ENV LOG_LEVEL 支持
- services.yaml: waf 声明 --instance 和 --log-level
2026-08-10 11:38:08 +08:00
14 changed files with 509 additions and 80 deletions

No files matched your search

+30
View File
@@ -165,9 +165,39 @@ def rag_list():
return jsonify({"total": len(entries), "entries": entries}), 200
def _parse_args():
"""解析命令行参数(标准化:CLI > ENV > 默认值)。"""
import argparse
p = argparse.ArgumentParser(
prog="ai_review",
description="AI Review Service — 基于 DeepSeek + RAG 的内容审核微服务",
)
p.add_argument("--host", default=None,
help="绑定地址(默认读取 $AI_REVIEW_HOST,否则 127.0.0.1)")
p.add_argument("--port", type=int, default=None,
help="监听端口(默认读取 $AI_REVIEW_PORT,否则 8007)")
p.add_argument("--log-level", default="info",
choices=["debug", "info", "warn", "error"],
help="日志级别(默认 info)")
p.add_argument("--instance", default="ai_review",
help="实例名(注册中心用,默认 ai_review)")
return p.parse_args()
# ── 配置默认值(ENV 覆盖,CLI 最高优先级)───────────────────────────
ADDR = os.getenv("AI_REVIEW_HOST", "127.0.0.1")
PORT = int(os.getenv("AI_REVIEW_PORT", "8007"))
if __name__ == "__main__":
args = _parse_args()
# CLI 覆盖(最高优先级)
if args.host is not None:
ADDR = args.host
if args.port is not None:
PORT = args.port
print(f'AI Review Service listening at http://{ADDR}:{PORT}')
print(f'[ai_review] instance={args.instance} log-level={args.log_level}')
print(f'[ai_review] 配置来源: CLI > ENV > 默认值')
serve(app, host=ADDR, port=PORT)
+29
View File
@@ -18,6 +18,9 @@
*
* // 进程退出前:
* qing::LogC::instance().shutdown(); // 停止后台线程并 flush 剩余日志(幂等)
*
* // 设置日志级别(main 启动时调用,低于此级别的日志直接丢弃)
* qing::LogC::instance().set_level("warn"); // 只输出 warn/error
*/
#include <atomic>
@@ -27,6 +30,7 @@
#include <mutex>
#include <optional>
#include <string>
#include <unordered_map>
#include <vector>
#include <boost/json.hpp>
@@ -48,6 +52,17 @@ public:
std::optional<std::string> extra; // 已序列化的 JSON 字符串(存 Logs.extra TEXT)
};
// 日志级别 → 数值(大写兼容)
static const std::unordered_map<std::string, int>& level_order() {
static const std::unordered_map<std::string, int> k = {
{"debug", 0}, {"info", 1}, {"warn", 2}, {"warning", 2},
{"error", 3}, {"critical", 3},
{"DEBUG", 0}, {"INFO", 1}, {"WARN", 2}, {"WARNING", 2},
{"ERROR", 3}, {"CRITICAL", 3},
};
return k;
}
static LogC& instance() {
static LogC inst;
return inst;
@@ -55,11 +70,24 @@ public:
// ---- 公开接口 --------------------------------------------------------
// 设置日志级别阈值(debug|info|warn|error,大小写不限)
// 低于此级别的日志在 push 时直接丢弃
void set_level(const std::string& level) {
auto it = level_order().find(level);
if (it != level_order().end()) {
_level_threshold.store(it->second, std::memory_order_relaxed);
}
}
// 推送一条日志(非阻塞,立即返回)
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) {
// 日志级别过滤:低于阈值直接丢弃
auto it = level_order().find(level);
int lvl = (it != level_order().end()) ? it->second : 1;
if (lvl < _level_threshold.load(std::memory_order_relaxed)) return;
_queue.push(Entry{service, level, message,
std::move(trace_id), std::move(extra)});
}
@@ -193,6 +221,7 @@ private:
std::string _db_addr = "127.0.0.1";
int _db_port = 8005;
std::mutex _endpoint_mtx; // 保护 _db_addr/_db_port 读写(set_endpoint 与后台线程并发)
std::atomic<int> _level_threshold{1}; // 日志级别阈值,默认 INFO(1)
static constexpr size_t _BATCH_SIZE = 50; // 队列满 50 条时 flush
static constexpr double _FLUSH_INTERVAL = 2.0; // 超时 2 秒强制 flush
+44 -5
View File
@@ -406,6 +406,9 @@ static std::unordered_map<std::string, std::string> _WS_PROXY_ROUTES;
/* instance items */
static std::unique_ptr<HttpServer> _SERVE;
static std::unique_ptr<MyRSA::Generator> _KEYGEN;
/* CLI 标准化参数 */
static std::string _INSTANCE = "ca"; // --instance,注册中心用
static std::string _LOG_LEVEL = "info"; // --log-level,debug|info|warn|error
//static std::unique_ptr<Sqlite3> _SQLDB;
static RateLimiter _LOGIN_RATE;
static RateLimiter _REGISTER_RATE;
@@ -769,6 +772,11 @@ int main(int argc, char **argv) {
desc.add_options()
("help,h", "output help message")
("version,v", "output version message")
("host", po::value<std::string>(), "绑定地址(默认 127.0.0.1),覆盖 [Server] address")
("port", po::value<int>(), "监听端口(默认 8443),覆盖 [Server] port")
("config", po::value<std::string>(), "confs 拉取的配置名(默认 ca.ini)")
("log-level", po::value<std::string>(), "日志级别 debug|info|warn|error(默认 info)")
("instance", po::value<std::string>(), "实例名(注册中心用,默认 ca)")
;
@@ -815,15 +823,21 @@ int main(int argc, char **argv) {
_CONFS_HOST = ConfIni::env("CONFS_HOST", "127.0.0.1");
_CONFS_PORT = ConfIni::env_int("CONFS_PORT", 8010);
/* --config 覆盖 confs 拉取的配置名(在 confs 拉取前读取) */
std::string confs_config_name = "ca.ini";
if (vm.count("config")) {
confs_config_name = vm["config"].as<std::string>();
}
ConfIni cf;
try {
/* 从 confs 拉取 ca.ini */
/* 从 confs 拉取配置 */
std::cout << "[ca] 从 confs (" << _CONFS_HOST << ":" << _CONFS_PORT
<< ") 拉取 ca.ini ..." << std::endl;
<< ") 拉取 " << confs_config_name << " ..." << std::endl;
pmc::net::MsgClient confs_client(_CONFS_HOST, _CONFS_PORT, "ca");
json::object req;
req["_route"] = "get";
req["name"] = "ca.ini";
req["name"] = confs_config_name;
std::string resp_str = confs_client.call(
json::serialize(req), /*timeout=*/5.0,
/*retry_on_refused=*/true);
@@ -917,7 +931,28 @@ int main(int argc, char **argv) {
int env_smtp_port = ConfIni::env_int("SMTP_PORT", -1);
if (env_smtp_port > 0) _SMTP_PORT = env_smtp_port;
/* 4. 配置完整性校验 */
/* 4. CLI 参数覆盖(最高优先级) */
if (vm.count("host")) _ADDR = vm["host"].as<std::string>();
if (vm.count("port")) _PORT = vm["port"].as<int>();
if (vm.count("instance")) _INSTANCE = vm["instance"].as<std::string>();
if (vm.count("log-level")) {
_LOG_LEVEL = vm["log-level"].as<std::string>();
// 归一化 warning → warn
if (_LOG_LEVEL == "warning") _LOG_LEVEL = "warn";
// 合法性校验
static const char* valid[] = {"debug", "info", "warn", "error"};
bool ok = false;
for (const char* v : valid) {
if (_LOG_LEVEL == v) { ok = true; break; }
}
if (!ok) {
std::cerr << "[ca] 致命错误: 非法 --log-level: " << _LOG_LEVEL
<< "(合法值: debug|info|warn|error)" << std::endl;
return 1;
}
}
/* 5. 配置完整性校验 */
if (_ADDR.empty() || _PORT == -1) {
std::cerr << "[ca] 致命错误: 配置项 [Server] address/port 缺失"
"(ini 中未配置且无 LISTEN_ADDR/LISTEN_PORT 环境变量覆盖)" << std::endl;
@@ -937,8 +972,12 @@ int main(int argc, char **argv) {
DbC::init(_DB_HOST, _DB_PORT);
logs().set_endpoint(_DB_HOST, _DB_PORT);
/* 设置日志级别过滤 */
logs().set_level(_LOG_LEVEL);
std::cout << "[ca] 监听: " << _ADDR << ":" << _PORT
<< " (TLS: " << (_TLS ? "on" : "off") << ")" << std::endl;
std::cout << "[ca] 实例: " << _INSTANCE << " 日志级别: " << _LOG_LEVEL << std::endl;
std::cout << "[ca] 下游服务: db=" << _DB_HOST << ":" << _DB_PORT
<< ", captcha=" << _CAPTCHA_HOST << ":" << _CAPTCHA_PORT
<< ", smtp=" << _SMTP_HOST << ":" << _SMTP_PORT << std::endl;
@@ -984,7 +1023,7 @@ int main(int argc, char **argv) {
// 等待服务器启动完成(简单延迟,确保started日志打印后再输出提示)
std::this_thread::sleep_for(std::chrono::milliseconds(200));
std::cout << "CA " << _VER << " started" << std::endl;
logs().info("ca", std::string("CA ") + _VER + " started");
logs().info("ca", std::string("CA ") + _VER + " started (instance=" + _INSTANCE + ")");
/* 主线程:等待关闭信号,带超时轮询 */
while (!_stop_requested && !server_exited) {
+28
View File
@@ -16,6 +16,8 @@
#include <vector>
#include <chrono>
#include <cstdlib>
#include <atomic>
#include <unordered_map>
namespace captcha2 {
@@ -30,11 +32,31 @@ struct LogEntry {
class LogC {
public:
// 日志级别 → 数值(大写兼容)
static const std::unordered_map<std::string, int>& level_order() {
static const std::unordered_map<std::string, int> k = {
{"debug", 0}, {"info", 1}, {"warn", 2}, {"warning", 2},
{"error", 3}, {"critical", 3},
{"DEBUG", 0}, {"INFO", 1}, {"WARN", 2}, {"WARNING", 2},
{"ERROR", 3}, {"CRITICAL", 3},
};
return k;
}
static LogC& instance() {
static LogC inst;
return inst;
}
// 设置日志级别阈值(debug|info|warn|error,大小写不限)
// 低于此级别的日志在 push 时直接丢弃
void set_level(const std::string& level) {
auto it = level_order().find(level);
if (it != level_order().end()) {
level_threshold_.store(it->second, std::memory_order_relaxed);
}
}
void info(const std::string& svc, const std::string& msg) {
push(svc, "INFO", msg);
}
@@ -64,6 +86,11 @@ private:
~LogC() { if (queue_) queue_->shutdown(); }
void push(const std::string& svc, const std::string& level, const std::string& msg) {
// 日志级别过滤:低于阈值直接丢弃
auto it = level_order().find(level);
int lvl = (it != level_order().end()) ? it->second : 1;
if (lvl < level_threshold_.load(std::memory_order_relaxed)) return;
auto now = std::chrono::system_clock::now();
auto t = std::chrono::system_clock::to_time_t(now);
char buf[32];
@@ -105,6 +132,7 @@ private:
std::string db_host_;
int db_port_;
std::atomic<int> level_threshold_{1}; // 日志级别阈值,默认 INFO(1)
std::unique_ptr<pmc::net::AsyncQueue<LogEntry>> queue_;
};
+25 -7
View File
@@ -70,12 +70,13 @@ struct CaptchaConfig {
int rate_limit_per_min = 600; // P1-1: 全局限流仅作最后防线,主要限流由 gateway 按 IP 负责
};
// 从 confs 拉取 captcha.ini 全文,阻塞等待 confs 就绪
static std::string fetch_config_from_confs(const std::string& confs_host, int confs_port) {
// 从 confs 拉取指定名称的配置文件全文,阻塞等待 confs 就绪
static std::string fetch_config_from_confs(const std::string& confs_host, int confs_port,
const std::string& config_name) {
pmc::net::RpcClient client(confs_host, confs_port, "captcha2",
/*timeout=*/5.0, /*retry_on_refused=*/true);
try {
auto data = client.call("get", {{"name", "captcha.ini"}});
auto data = client.call("get", {{"name", config_name}});
auto* content = data.as_object().if_contains("content");
if (!content || !content->is_string()) {
std::cerr << "[captcha2] confs 返回的 captcha.ini 无 content 字段" << std::endl;
@@ -531,6 +532,9 @@ int main(int argc, char* argv[]) {
// ── 命令行参数 ──
std::string confs_host = "127.0.0.1";
int confs_port = 8010;
std::string confs_config_name = "captcha.ini"; // --config 覆盖
std::string instance = "captcha2"; // --instance 覆盖
std::string log_level = "info"; // --log-level 覆盖
{
const char* env;
if ((env = std::getenv("CONFS_HOST"))) confs_host = env;
@@ -540,9 +544,13 @@ int main(int argc, char* argv[]) {
po::options_description desc("captcha2 — pymsg 验证码服务");
desc.add_options()
("help,h", "显示帮助")
("addr,a", po::value<std::string>(), "监听地址(覆盖 confs)")
("addr,a", po::value<std::string>(), "监听地址(覆盖 confs,别名 --host)")
("host", po::value<std::string>(), "监听地址(覆盖 confs,别名 --addr/-a)")
("port,p", po::value<int>(), "监听端口(覆盖 confs)")
("workers,w", po::value<int>(), "工作线程数(覆盖 confs)");
("workers,w", po::value<int>(), "工作线程数(覆盖 confs)")
("config", po::value<std::string>(), "confs 拉取的配置名(默认 captcha.ini)")
("log-level", po::value<std::string>(), "日志级别 debug|info|warn|error,默认 info")
("instance", po::value<std::string>(), "实例名(注册中心用,默认 captcha2)");
po::variables_map vm;
po::store(po::parse_command_line(argc, argv, desc), vm);
po::notify(vm);
@@ -552,20 +560,30 @@ int main(int argc, char* argv[]) {
return 0;
}
// 预扫描 --config / --instance / --log-level / --host(confs 拉取前处理)
if (vm.count("config")) confs_config_name = vm["config"].as<std::string>();
if (vm.count("instance")) instance = vm["instance"].as<std::string>();
if (vm.count("log-level")) log_level = vm["log-level"].as<std::string>();
// ── 配置加载:confs → env → 命令行 ──
std::string ini_text = fetch_config_from_confs(confs_host, confs_port);
std::string ini_text = fetch_config_from_confs(confs_host, confs_port, confs_config_name);
merge_ini(g_config, ini_text);
apply_env_overrides(g_config);
if (vm.count("addr")) g_config.host = vm["addr"].as<std::string>();
if (vm.count("host")) g_config.host = vm["host"].as<std::string>();
if (vm.count("port")) g_config.port = vm["port"].as<int>();
if (vm.count("workers")) g_config.workers = vm["workers"].as<int>();
validate_config(g_config);
std::cerr << "[captcha2] 配置来源: confs captcha.ini @ "
g_log.set_level(log_level);
std::cerr << "[captcha2] 配置来源: confs " << confs_config_name << " @ "
<< confs_host << ":" << confs_port << std::endl;
std::cerr << "[captcha2] instance=" << instance
<< " log-level=" << log_level << std::endl;
std::cerr << "[captcha2] captcha_len=" << g_config.len
<< " ttl=" << g_config.ttl
<< " max_store=" << g_config.max_store
+7 -7
View File
@@ -21,7 +21,7 @@ services:
# --- AI 审核 ---
- name: ai_review
cmd: bash /opt/micro_servers/ai_review/_run.sh
args: []
args: ["--instance", "ai_review", "--log-level", "info"]
depends_on: [db2]
sleep_before: 2
@@ -57,7 +57,7 @@ services:
# --- 邮件服务 ---
- name: smtp
cmd: bash /opt/micro_servers/smtp/_run.sh
args: []
args: ["--instance", "smtp", "--log-level", "info"]
depends_on: []
sleep_before: 2
@@ -71,26 +71,26 @@ services:
# --- 安全链路 ---
- name: waf
cmd: bash /opt/micro_servers/waf/_run.sh
args: [] # -c config.yaml 保留在 _run.sh 作为默认值
args: ["--instance", "waf", "--log-level", "info"]
depends_on: [ca, gateway]
sleep_before: 0
- name: ca
cmd: bash /opt/micro_servers/ca/_run.sh
args: []
args: ["--instance", "ca", "--log-level", "info"]
depends_on: [db2]
sleep_before: 2
- name: gateway
cmd: bash /opt/micro_servers/gateway/_run.sh
args: []
args: ["--instance", "gateway", "--log-level", "info"]
depends_on: [db2]
sleep_before: 2
# --- 验证码 ---
- name: captcha2
cmd: bash /opt/micro_servers/captcha2/_run.sh
args: []
args: ["--instance", "captcha2", "--log-level", "info"]
depends_on: []
sleep_before: 1
@@ -104,6 +104,6 @@ services:
# --- 端口重定向(最后启动) ---
- name: redirect80
cmd: bash /opt/micro_servers/redirect80/_run.sh
args: []
args: ["--instance", "redirect80", "--log-level", "info"]
depends_on: [waf]
sleep_before: 0
+29
View File
@@ -18,6 +18,9 @@
*
* // 进程退出前:
* qing::LogC::instance().shutdown(); // 停止后台线程并 flush 剩余日志(幂等)
*
* // 设置日志级别(main 启动时调用,低于此级别的日志直接丢弃)
* qing::LogC::instance().set_level("warn"); // 只输出 warn/error
*/
#include <atomic>
@@ -27,6 +30,7 @@
#include <mutex>
#include <optional>
#include <string>
#include <unordered_map>
#include <vector>
#include <boost/json.hpp>
@@ -55,11 +59,35 @@ public:
// ---- 公开接口 --------------------------------------------------------
// 日志级别 → 数值(大写兼容)
static const std::unordered_map<std::string, int>& level_order() {
static const std::unordered_map<std::string, int> k = {
{"debug", 0}, {"info", 1}, {"warn", 2}, {"warning", 2},
{"error", 3}, {"critical", 3},
{"DEBUG", 0}, {"INFO", 1}, {"WARN", 2}, {"WARNING", 2},
{"ERROR", 3}, {"CRITICAL", 3},
};
return k;
}
// 设置日志级别阈值(debug|info|warn|error,大小写不限)
// 低于此级别的日志在 push 时直接丢弃
void set_level(const std::string& level) {
auto it = level_order().find(level);
if (it != level_order().end()) {
_level_threshold.store(it->second, std::memory_order_relaxed);
}
}
// 推送一条日志(非阻塞,立即返回)
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) {
// 日志级别过滤:低于阈值直接丢弃
auto it = level_order().find(level);
int lvl = (it != level_order().end()) ? it->second : 1;
if (lvl < _level_threshold.load(std::memory_order_relaxed)) return;
_queue.push(Entry{service, level, message,
std::move(trace_id), std::move(extra)});
}
@@ -193,6 +221,7 @@ private:
std::string _db_addr = "127.0.0.1";
int _db_port = 8005;
std::mutex _endpoint_mtx; // 保护 _db_addr/_db_port 读写(set_endpoint 与后台线程并发)
std::atomic<int> _level_threshold{1}; // 日志级别阈值,默认 INFO(1)
static constexpr size_t _BATCH_SIZE = 50; // 队列满 50 条时 flush
static constexpr double _FLUSH_INTERVAL = 2.0; // 超时 2 秒强制 flush
+29 -2
View File
@@ -109,6 +109,11 @@ static int _DB_PORT = 8005;
/* confs 引导配置(不依赖 confs 本身,环境变量 CONFS_HOST/CONFS_PORT 覆盖,默认 127.0.0.1:8010) */
static std::string _CONFS_HOST = "127.0.0.1";
static int _CONFS_PORT = 8010;
/* confs 配置名(--config 覆盖,默认 gateway.ini) */
static std::string _CONFS_CONFIG_NAME = "gateway.ini";
/* CLI 标准化参数(注册中心 / 日志级别) */
static std::string _INSTANCE = "gateway";
static std::string _LOG_LEVEL = "info";
/* instance items */
static std::unique_ptr<HttpServer> _SERVE;
@@ -164,6 +169,11 @@ int main(int argc, char **argv) {
desc.add_options()
("help,h", "output help message")
("version,v", "output version message")
("host", po::value<std::string>(), "绑定地址(覆盖 [Server] address,默认 127.0.0.1)")
("port", po::value<int>(), "监听端口(覆盖 [Server] port,默认 8445)")
("config", po::value<std::string>(), "confs 拉取的配置名(默认 gateway.ini)")
("log-level", po::value<std::string>(), "日志级别 debug|info|warn|error,默认 info")
("instance", po::value<std::string>(), "实例名(注册中心用,默认 gateway)")
;
@@ -195,6 +205,10 @@ int main(int argc, char **argv) {
return 0;
}
/* CLI 预扫描:--config 必须在 confs 拉取前处理(其余 CLI 覆盖在 ENV 之后接入) */
if (vm.count("config"))
_CONFS_CONFIG_NAME = vm["config"].as<std::string>();
/* ==================== 配置加载(两层:confs 强依赖 → 环境变量覆盖) ====================
* 与 Python 服务(dnfs/db/static)保持一致的加载模式:
* 1. 引导配置:confs 地址从 CONFS_HOST/CONFS_PORT 环境变量读取,缺省 127.0.0.1:8010
@@ -214,11 +228,11 @@ int main(int argc, char **argv) {
try {
/* 从 confs 拉取 gateway.ini */
std::cout << "[gateway] 从 confs (" << _CONFS_HOST << ":" << _CONFS_PORT
<< ") 拉取 gateway.ini ..." << std::endl;
<< ") 拉取 " << _CONFS_CONFIG_NAME << " ..." << std::endl;
pmc::net::MsgClient confs_client(_CONFS_HOST, _CONFS_PORT, "gateway");
json::object req;
req["_route"] = "get";
req["name"] = "gateway.ini";
req["name"] = _CONFS_CONFIG_NAME;
std::string resp_str = confs_client.call(
json::serialize(req), /*timeout=*/5.0,
/*retry_on_refused=*/true);
@@ -268,6 +282,16 @@ int main(int argc, char **argv) {
int env_db_port = ConfIni::env_int("DB_PORT", -1);
if (env_db_port > 0) _DB_PORT = env_db_port;
/* 3b. CLI 覆盖(最高优先级):--host / --port / --instance / --log-level */
if (vm.count("host"))
_ADDR = vm["host"].as<std::string>();
if (vm.count("port"))
_PORT = vm["port"].as<int>();
if (vm.count("instance"))
_INSTANCE = vm["instance"].as<std::string>();
if (vm.count("log-level"))
_LOG_LEVEL = vm["log-level"].as<std::string>();
/* 4. 配置完整性校验 */
if (_ADDR.empty() || _PORT == -1) {
std::cerr << "[gateway] 致命错误: 配置项 [Server] address/port 缺失"
@@ -295,9 +319,12 @@ int main(int argc, char **argv) {
/* 初始化日志推送端点 */
logs().set_endpoint(_DB_HOST, _DB_PORT);
logs().set_level(_LOG_LEVEL);
std::cout << "[gateway] 监听: " << _ADDR << ":" << _PORT
<< " (TLS: " << (_TLS ? "on" : "off") << ")" << std::endl;
std::cout << "[gateway] instance=" << _INSTANCE
<< " log-level=" << _LOG_LEVEL << std::endl;
// 根据 Server.tls 选择 HTTPS(带证书)或 HTTP(无证书)构造函数
// HTTP 模式用于 ca 前置后的内部转发链路
+21
View File
@@ -8,6 +8,7 @@
- 缓冲批量:队列满 50 条或超时 2 秒时自动 flush
- 时间戳:日志入队时即记录 ts(UTC ISO 格式),反映事件真实发生时间
- 底层复用 pymsg.AsyncRpcQueue 通用队列基础设施
- 日志级别过滤:set_level() 设定阈值,低于阈值的日志直接丢弃
用法(与旧版完全兼容):
from log_client import logs
@@ -16,6 +17,10 @@
logs.error("redirect80", "端口绑定失败", extra={"errno": 98})
logs.push("redirect80", "WARNING", "confs 不可用,使用本地配置",
trace_id="startup", extra={"fallback": "redirect80.ini"})
# 设置日志级别(main 启动时调用)
from log_client import set_log_level
set_log_level("warn") # 只输出 warn/error
"""
import json
@@ -37,6 +42,19 @@ _FLUSH_INTERVAL = 2.0
_TIMEOUT = 5
_QUEUE_MAXSIZE = 10000
# ── 日志级别过滤 ────────────────────────────────────────────────────
_LEVEL_ORDER = {"DEBUG": 0, "INFO": 1, "WARNING": 2, "WARN": 2, "ERROR": 3, "CRITICAL": 3}
_level_threshold = 1 # 默认 INFO
def set_log_level(level: str):
"""设置全局日志级别阈值。低于此级别的日志直接丢弃。"""
global _level_threshold
lv = level.upper()
if lv == "WARNING":
lv = "WARN"
_level_threshold = _LEVEL_ORDER.get(lv, 1)
def _safe_json(obj):
"""安全序列化 extra 字段,不可序列化对象转为 repr 字符串。"""
@@ -79,8 +97,11 @@ class LogClient:
trace_id: str = None, extra=None):
"""推送一条日志(非阻塞,立即返回)。
低于设定级别的日志直接丢弃。
队列满时静默丢弃最旧条目再放入新条目,保证不阻塞业务线程、不导致 OOM。
"""
if _LEVEL_ORDER.get(level.upper(), 1) < _level_threshold:
return
entry = {
'ts': _now_iso(),
'service': service,
+128 -52
View File
@@ -7,12 +7,14 @@ redirect80 — HTTP 80 端口重定向服务。
技术选型:Python 内置 http.server + 多线程混合模式,无外部 Web 框架依赖。
日志通过 log_client 异步推送到 db 的 Logs 表(fail-safe)。
配置(两层加载):
配置(三层加载):
1. confs 强依赖拉取 redirect80.ini(retry_on_refused 阻塞等待)
2. 环境变量覆盖(Docker/k8s 兼容)
confs 不可用时直接退出,不做本地回退。
3. 命令行参数(最高优先级)
配置不可用时直接退出,不做本地回退。
"""
import argparse
import configparser
import os
import sys as _sys
@@ -22,57 +24,124 @@ from socketserver import ThreadingMixIn
# ── 路径设置 ──────────────────────────────────────────────────────────
_here = os.path.dirname(os.path.abspath(__file__))
_sys.path.insert(0, os.path.join(_here, ".."))
from log_client import logs # noqa: E402
from log_client import logs, set_log_level # noqa: E402
# ── 配置加载(confs 强依赖 → 环境变量覆盖)────────────────────────
_config = {
"host": "0.0.0.0",
"port": 80,
"redirect_port": 443,
"redirect_code": 301,
}
# 1. confs 强依赖拉取配置(阻塞等待 confs 就绪,失败则退出)
_sys.path.insert(0, os.path.join(_here, "..", "pymsg"))
from pymsg import RpcClient # noqa: E402
_confs_host = os.environ.get("CONFS_HOST", "127.0.0.1")
_confs_port = int(os.environ.get("CONFS_PORT", "8010"))
_confs_client = RpcClient(
_confs_host, _confs_port, "redirect80",
retry_on_refused=True, timeout=5,
)
_data = _confs_client.call("get", {"name": "redirect80.ini"})
_ini_text = _data.get("content", "")
_ini = configparser.ConfigParser(interpolation=None)
_ini.read_string(_ini_text)
_merge_map = {
("server", "host"): "host",
("server", "port"): "port",
("server", "redirect_port"): "redirect_port",
("server", "redirect_code"): "redirect_code",
}
for (sec, key), var in _merge_map.items():
if _ini.has_option(sec, key):
val = _ini.get(sec, key)
_config[var] = type(_config[var])(val)
logs.info("redirect80", "配置已从 confs 加载")
# ── 配置加载 ─────────────────────────────────────────────────────────
def load_config(argv):
"""三层加载: confs → ENV → CLI(最高优先级)。"""
# argparse 先行:--help 在任何网络调用前处理
parser = argparse.ArgumentParser(
prog="redirect80",
description="HTTP 80 端口重定向服务",
add_help=False,
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="配置优先级: CLI > 环境变量 > confs > 默认值",
)
parser.add_argument("--host", default=None,
help="绑定地址,默认 0.0.0.0")
parser.add_argument("--port", type=int, default=None,
help="监听端口,默认 80")
parser.add_argument("--redirect-port", type=int, default=None,
help="重定向目标端口,默认 443")
parser.add_argument("--redirect-code", type=int, default=None,
help="GET/HEAD 重定向状态码,默认 301")
parser.add_argument("--log-level", default=None,
help="debug|info|warn|error,默认 info")
parser.add_argument("--instance", default=None,
help="实例名(注册中心用),默认 redirect80")
parser.add_argument("--help", "-h", action="help",
help="显示此帮助并退出")
args = parser.parse_args(argv)
# 默认值
config = {
"host": "0.0.0.0",
"port": 80,
"redirect_port": 443,
"redirect_code": 301,
"log_level": "info",
"instance": "redirect80",
}
# 1. confs 强依赖拉取配置(阻塞等待 confs 就绪,失败则退出)
confs_host = os.environ.get("CONFS_HOST", "127.0.0.1")
confs_port = int(os.environ.get("CONFS_PORT", "8010"))
try:
client = RpcClient(
confs_host, confs_port, "redirect80",
retry_on_refused=True, timeout=5,
)
data = client.call("get", {"name": "redirect80.ini"})
ini_text = data.get("content", "")
ini = configparser.ConfigParser(interpolation=None)
ini.read_string(ini_text)
_merge = {
("server", "host"): "host",
("server", "port"): "port",
("server", "redirect_port"): "redirect_port",
("server", "redirect_code"): "redirect_code",
}
for (sec, key), var in _merge.items():
if ini.has_option(sec, key):
val = ini.get(sec, key)
config[var] = type(config[var])(val)
logs.info("redirect80", "配置已从 confs 加载")
except Exception as e:
print(f"[redirect80] confs 获取失败: {e}", file=_sys.stderr)
_sys.exit(1)
# 2. 环境变量覆盖
_env_map = {
"REDIRECT80_HOST": "host",
"REDIRECT80_PORT": ("port", int),
"REDIRECT80_REDIRECT_PORT": ("redirect_port", int),
"REDIRECT80_REDIRECT_CODE": ("redirect_code", int),
}
for env_var, mapping in _env_map.items():
val = os.environ.get(env_var)
if val is not None:
if isinstance(mapping, tuple):
config[mapping[0]] = mapping[1](val)
else:
config[mapping] = val
# 3. CLI 参数(最高优先级)
if args.host is not None:
config["host"] = args.host
if args.port is not None:
config["port"] = args.port
if args.redirect_port is not None:
config["redirect_port"] = args.redirect_port
if args.redirect_code is not None:
config["redirect_code"] = args.redirect_code
if args.log_level is not None:
config["log_level"] = args.log_level
if args.instance is not None:
config["instance"] = args.instance
# log_level 合法性校验
lv = config["log_level"].lower()
if lv == "warning":
lv = "warn"
if lv not in ("debug", "info", "warn", "error"):
print(f"[redirect80] invalid log_level: {config['log_level']} "
f"(must be debug|info|warn|error)", file=_sys.stderr)
_sys.exit(1)
config["log_level"] = lv
return config
# ── 模块级配置(main() 中赋值)──────────────────────────────────────
_config = {}
# 2. 环境变量最高优先级覆盖
_env_map = {
"REDIRECT80_HOST": "host",
"REDIRECT80_PORT": ("port", int),
"REDIRECT80_REDIRECT_PORT": ("redirect_port", int),
"REDIRECT80_REDIRECT_CODE": ("redirect_code", int),
}
for env_var, mapping in _env_map.items():
val = os.environ.get(env_var)
if val is not None:
if isinstance(mapping, tuple):
_config[mapping[0]] = mapping[1](val)
else:
_config[mapping] = val
# ── 请求处理器 ──────────────────────────────────────────────────────────
@@ -110,10 +179,10 @@ class RedirectHandler(BaseHTTPRequestHandler):
f"{code} {self.command} {host} → {location}")
def do_GET(self):
self._redirect(301)
self._redirect(_config["redirect_code"])
def do_HEAD(self):
self._redirect(301)
self._redirect(_config["redirect_code"])
def do_POST(self):
self._redirect(308)
@@ -158,17 +227,24 @@ class ThreadingRedirectServer(ThreadingMixIn, HTTPServer):
def main():
global _config
_config = load_config(_sys.argv[1:])
# 设置日志级别过滤
set_log_level(_config["log_level"])
host = _config["host"]
port = int(_config["port"])
server = ThreadingRedirectServer((host, port), RedirectHandler)
logs.info("redirect80",
f"HTTP→HTTPS 重定向服务启动 "
f"服务启动 instance={_config['instance']} "
f"http://{host}:{port} → https://*:{_config['redirect_port']}")
print(f"[redirect80] 监听 http://{host}:{port} "
f"→ 重定向到 :{_config['redirect_port']}", file=_sys.stderr)
print(f"[redirect80] instance={_config['instance']} log-level={_config['log_level']} "
f"监听 http://{host}:{port} → 重定向到 :{_config['redirect_port']}",
file=_sys.stderr)
try:
server.serve_forever()
+26
View File
@@ -38,6 +38,29 @@ _FLUSH_INTERVAL = 2.0 # 超时 2 秒强制 flush
_TIMEOUT = 5 # socket 超时(秒)
_QUEUE_MAXSIZE = 10000 # 队列上限,防止后台线程异常时内存无限增长;满则丢弃最旧条目
# ── 日志级别过滤 ──
_LOG_LEVELS = {
"debug": 0, "info": 1, "warn": 2, "warning": 2,
"error": 3, "critical": 4,
}
_LOG_LEVEL_THRESHOLD = _LOG_LEVELS["info"] # 默认 info
def _normalize_level(level):
"""归一化级别名:大写转小写,warning→warn。"""
lv = (level or "info").lower()
if lv == "warning":
lv = "warn"
return lv
def set_log_level(level):
"""设置日志过滤阈值(低于该级别的日志将被丢弃)。"""
global _LOG_LEVEL_THRESHOLD
lv = _normalize_level(level)
if lv in _LOG_LEVELS:
_LOG_LEVEL_THRESHOLD = _LOG_LEVELS[lv]
def _safe_json(obj):
"""安全序列化 extra 字段,不可序列化对象转为 repr 字符串,保证不抛异常。"""
@@ -77,6 +100,9 @@ class LogClient:
队列满时静默丢弃最旧条目再放入新条目,保证不阻塞业务线程、不导致 OOM。
"""
# 级别过滤:低于当前阈值的日志直接丢弃(不进队列)
if _LOG_LEVELS.get(_normalize_level(level), 0) < _LOG_LEVEL_THRESHOLD:
return
entry = {
'ts': _now_iso(), # 入队时即生成时间戳,反映真实事件时间
'service': service,
+12 -1
View File
@@ -303,12 +303,23 @@ if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser(description="smtp — pymsg 邮箱验证码服务")
parser.add_argument("--addr", default="127.0.0.1", help="监听地址")
parser.add_argument("--addr", "--host", dest="addr", default="127.0.0.1",
help="监听地址(--host 与 --addr 等价)")
parser.add_argument("--port", type=int, default=8008, help="监听端口")
parser.add_argument("--workers", type=int, default=4,
help="回调并发线程数(0=同步串行模式)")
parser.add_argument("--log-level", default="info",
choices=["debug", "info", "warn", "error"],
help="日志级别(默认 info)")
parser.add_argument("--instance", default="smtp",
help="实例名(注册中心用,默认 smtp)")
args = parser.parse_args()
# 应用日志级别过滤(低于阈值的日志不进队列)
logs.set_log_level(args.log_level)
print(f"[smtp] instance={args.instance} log-level={args.log_level}")
print(f"[smtp] 配置来源: .env / ENV > 默认值(SMTP 账号不通过 CLI 传递)")
server = SmtpServer(args.addr, args.port, workers=args.workers)
try:
server.start()
+44
View File
@@ -30,13 +30,47 @@ const (
maxMsgSize = 64 * 1024 * 1024
)
// 日志级别数值(越小越详细)。
const (
levelDebug = 0
levelInfo = 1
levelWarn = 2
levelError = 3
)
var levelMap = map[string]int{
"debug": levelDebug,
"info": levelInfo,
"warn": levelWarn,
"warning": levelWarn,
"error": levelError,
"DEBUG": levelDebug,
"INFO": levelInfo,
"WARN": levelWarn,
"WARNING": levelWarn,
"ERROR": levelError,
}
// dbAddr 通过环境变量 LOG_DB_ADDR 配置(默认 127.0.0.1:8005),支持非标准部署场景。
var dbAddr = defaultDBAddr
// minLevel 控制最低输出级别,默认 info(debug 被过滤)。
var minLevel = levelInfo
func init() {
if v := os.Getenv("LOG_DB_ADDR"); v != "" {
dbAddr = v
}
if v := os.Getenv("LOG_LEVEL"); v != "" {
SetLevel(v)
}
}
// SetLevel 设置最低日志级别(debug|info|warn|error,大小写不敏感)。
func SetLevel(level string) {
if n, ok := levelMap[level]; ok {
minLevel = n
}
}
// Entry 单条日志。
@@ -93,10 +127,15 @@ func New() *Client {
}
// Push 推送一条日志(非阻塞,队列满时丢弃)。
// 低于 SetLevel 设定的最低级别的日志将被静默丢弃。
func Push(service, level, message string, extra map[string]any) {
if global == nil {
return
}
n, ok := levelMap[level]
if !ok || n < minLevel {
return
}
global.push(Entry{
Service: service,
Level: level,
@@ -105,6 +144,11 @@ func Push(service, level, message string, extra map[string]any) {
})
}
// Debug 推送 DEBUG 级别日志。
func Debug(service, message string, extra map[string]any) {
Push(service, "DEBUG", message, extra)
}
// Info 推送 INFO 级别日志。
func Info(service, message string, extra map[string]any) {
Push(service, "INFO", message, extra)
+57 -6
View File
@@ -42,15 +42,66 @@ type Config struct {
func main() {
var (
configFile = flag.String("c", "config.yaml", "配置文件路径")
doList = flag.Bool("list", false, "列出所有规则后退出")
doTest = flag.String("test", "", "校验单个规则文件语法后退出")
doCheck = flag.String("check", "", "用当前规则测试输入字符串(URI/path)后退出")
configFile = flag.String("c", "config.yaml", "配置文件路径")
doList = flag.Bool("list", false, "列出所有规则后退出")
doTest = flag.String("test", "", "校验单个规则文件语法后退出")
doCheck = flag.String("check", "", "用当前规则测试输入字符串(URI/path)后退出")
// CLI 标准化参数(覆盖 config.yaml)
cliListen = flag.String("listen", "", "监听地址(别名: --host),覆盖 config.yaml")
cliHost = flag.String("host", "", "监听地址(--listen 别名)")
cliUpstream = flag.String("upstream", "", "gateway 地址,覆盖 config.yaml")
cliTLSCert = flag.String("tls-cert", "", "TLS 证书文件,覆盖 config.yaml")
cliTLSKey = flag.String("tls-key", "", "TLS 私钥文件,覆盖 config.yaml")
cliRulesDir = flag.String("rules-dir", "", "规则根目录,覆盖 config.yaml")
logLevel = flag.String("log-level", "info", "日志级别: debug|info|warn|error")
instance = flag.String("instance", "waf", "实例名(注册中心用)")
)
flag.Usage = func() {
fmt.Fprintf(os.Stderr,
"waf — WAF 入口网关\n\n"+
"用法: waf [选项]\n\n"+
"选项:\n"+
" -c <file> 配置文件路径,默认 config.yaml\n"+
" --listen <addr> 监听地址(别名: --host),覆盖 config.yaml\n"+
" --upstream <url> gateway 地址,覆盖 config.yaml\n"+
" --tls-cert <file> TLS 证书文件,覆盖 config.yaml\n"+
" --tls-key <file> TLS 私钥文件,覆盖 config.yaml\n"+
" --rules-dir <dir> 规则根目录,覆盖 config.yaml\n"+
" --log-level <lv> debug|info|warn|error,默认 info\n"+
" --instance <name> 实例名,默认 waf\n"+
" -list 列出所有规则后退出\n"+
" -test <file> 校验单个规则文件语法后退出\n"+
" -check <input> 用当前规则测试输入字符串后退出\n"+
" -h, --help 显示此帮助并退出\n\n"+
"配置优先级: CLI > config.yaml\n")
}
flag.Parse()
// 日志级别
logclient.SetLevel(*logLevel)
cfg := loadConfig(*configFile)
// CLI 覆盖 config.yaml
if *cliListen != "" {
cfg.Listen = *cliListen
}
if *cliHost != "" {
cfg.Listen = *cliHost
}
if *cliUpstream != "" {
cfg.Upstream = *cliUpstream
}
if *cliTLSCert != "" {
cfg.TLSCert = *cliTLSCert
}
if *cliTLSKey != "" {
cfg.TLSKey = *cliTLSKey
}
if *cliRulesDir != "" {
cfg.RulesDir = *cliRulesDir
}
// 子命令:极客 CLI 工具
if *doTest != "" {
if err := engine.TestFile(*doTest); err != nil {
@@ -146,8 +197,8 @@ func main() {
termCh := make(chan os.Signal, 1)
signal.Notify(termCh, syscall.SIGINT, syscall.SIGTERM)
logclient.Info("waf", fmt.Sprintf("服务启动 listen=%s upstream=%s rules=%s pid=%d",
cfg.Listen, cfg.Upstream, cfg.RulesDir, os.Getpid()), nil)
logclient.Info("waf", fmt.Sprintf("服务启动 instance=%s listen=%s upstream=%s rules=%s pid=%d",
*instance, cfg.Listen, cfg.Upstream, cfg.RulesDir, os.Getpid()), nil)
errCh := make(chan error, 1)
go func() {