feat(liblogc): 统一集中式日志客户端库 + 全量迁移
新增: - liblogc/: C++ 统一日志库 (libus_logc.so),支持 set_endpoint/set_service_name/set_level - pymsg/pymsg/logc.py: Python 统一日志模块,基于 AsyncRpcQueue C++ 迁移 (删除 6 份本地 LogC.hpp 副本): - ca, gateway: <us/LogC.hpp> + set_service_name - captcha2, confs2, smtp3: using qing::LogC + set_service_name - core: ConfsLogC → qing::LogC (ConfsTask.cpp) - 所有 Makefile 添加 -lus_logc Python 迁移 (6 个 log_client.py → 4 行薄包装): - smtp, redirect80, confs, captcha, dnfs, static - 全部委托到 pymsg.logc.LogClient Go (waf): 保持现状,自成体系
This commit is contained in:
1 parent
01ae877673
commit
aa3741b861
32 files changed
+608
-2129
No files matched your search
+2
-2
@@ -3,7 +3,7 @@ APP_NAME := ca
|
|||||||
INSTL_DIR := /usr/local/bin
|
INSTL_DIR := /usr/local/bin
|
||||||
CPP := g++
|
CPP := g++
|
||||||
INCS := -I inc/ -I ../libmsg/inc
|
INCS := -I inc/ -I ../libmsg/inc
|
||||||
MY_LIBS := -L/usr/lib -L../libmsg/build -lus_http -lus_httpclient -lus_crypt -lus_msg
|
MY_LIBS := -L/usr/lib -L../libmsg/build -lus_http -lus_httpclient -lus_crypt -lus_msg -lus_logc
|
||||||
LIBS := -pthread -lcurl -lssl -lz -lcrypto -lboost_json -lboost_program_options -Wl,-rpath,$(shell cd ../libmsg/build && pwd)
|
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
|
CPPFLAGS := --std=c++20 -pipe -pthread # -Wall -Werror
|
||||||
BUILD_DIR = build
|
BUILD_DIR = build
|
||||||
@@ -13,7 +13,7 @@ $(shell mkdir -p $(BUILD_DIR))
|
|||||||
all: $(BUILD_DIR)/$(APP_NAME).out
|
all: $(BUILD_DIR)/$(APP_NAME).out
|
||||||
$(BUILD_DIR)/$(APP_NAME).out: $(BUILD_DIR)/main.o
|
$(BUILD_DIR)/$(APP_NAME).out: $(BUILD_DIR)/main.o
|
||||||
$(CPP) $^ $(LIBS) $(MY_LIBS) -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
|
$(BUILD_DIR)/main.o: src/main.cpp inc/DbC.hpp inc/Conf.hpp inc/ExpBase.hpp inc/LocalTime.hpp
|
||||||
$(CPP) $(CPPFLAGS) -c $< $(INCS) -o $@
|
$(CPP) $(CPPFLAGS) -c $< $(INCS) -o $@
|
||||||
clear:
|
clear:
|
||||||
@rm -rvf build/
|
@rm -rvf build/
|
||||||
-235
@@ -1,235 +0,0 @@
|
|||||||
#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 剩余日志(幂等)
|
|
||||||
*
|
|
||||||
* // 设置日志级别(main 启动时调用,低于此级别的日志直接丢弃)
|
|
||||||
* qing::LogC::instance().set_level("warn"); // 只输出 warn/error
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include <atomic>
|
|
||||||
#include <chrono>
|
|
||||||
#include <cstdio>
|
|
||||||
#include <ctime>
|
|
||||||
#include <mutex>
|
|
||||||
#include <optional>
|
|
||||||
#include <string>
|
|
||||||
#include <unordered_map>
|
|
||||||
#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 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 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)});
|
|
||||||
}
|
|
||||||
|
|
||||||
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 与后台线程并发)
|
|
||||||
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
|
|
||||||
static constexpr size_t _MAX_QUEUE_SIZE = 10000; // 队列上限,满时丢最旧
|
|
||||||
static constexpr double _TIMEOUT = 5.0; // socket 超时(秒)
|
|
||||||
|
|
||||||
// ---- 核心队列 ---------------------------------------------------------
|
|
||||||
pmc::net::AsyncQueue<Entry> _queue;
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace qing
|
|
||||||
+2
-1
@@ -25,7 +25,7 @@
|
|||||||
#include "LocalTime.hpp"
|
#include "LocalTime.hpp"
|
||||||
#include "Conf.hpp"
|
#include "Conf.hpp"
|
||||||
#include "DbC.hpp"
|
#include "DbC.hpp"
|
||||||
#include "LogC.hpp"
|
#include <us/LogC.hpp>
|
||||||
|
|
||||||
using namespace pmc::net;
|
using namespace pmc::net;
|
||||||
using namespace qing;
|
using namespace qing;
|
||||||
@@ -971,6 +971,7 @@ int main(int argc, char **argv) {
|
|||||||
/* 初始化下游客户端地址 */
|
/* 初始化下游客户端地址 */
|
||||||
DbC::init(_DB_HOST, _DB_PORT);
|
DbC::init(_DB_HOST, _DB_PORT);
|
||||||
logs().set_endpoint(_DB_HOST, _DB_PORT);
|
logs().set_endpoint(_DB_HOST, _DB_PORT);
|
||||||
|
logs().set_service_name("ca-log");
|
||||||
|
|
||||||
/* 设置日志级别过滤 */
|
/* 设置日志级别过滤 */
|
||||||
logs().set_level(_LOG_LEVEL);
|
logs().set_level(_LOG_LEVEL);
|
||||||
|
|||||||
+2
-156
@@ -1,158 +1,4 @@
|
|||||||
"""
|
"""日志客户端 — 委托到 pymsg.logc 统一库。"""
|
||||||
统一日志客户端 — captcha 服务副本(基于 pymsg.AsyncRpcQueue)。
|
from pymsg.logc import LogClient, set_log_level # noqa: F401
|
||||||
|
|
||||||
设计要点:
|
|
||||||
- 通过 pymsg RPC 调用 dbv2 execute 路由,逐条 INSERT 到 Logs 表
|
|
||||||
- 异步非阻塞:内存队列 + 后台线程攒批推送,不拖慢业务
|
|
||||||
- fail-safe:db 服务不可用时静默丢弃,绝不影响业务
|
|
||||||
- 缓冲批量:队列满 50 条或超时 2 秒时自动 flush
|
|
||||||
- 时间戳:日志入队时即记录 ts(UTC ISO 格式),反映事件真实发生时间
|
|
||||||
- 底层复用 pymsg.AsyncRpcQueue 通用队列基础设施
|
|
||||||
|
|
||||||
用法(与旧版完全兼容):
|
|
||||||
from log_client import logs
|
|
||||||
|
|
||||||
logs.info("captcha", "验证码已生成 token=abc123")
|
|
||||||
logs.error("captcha", "图片生成失败", extra={"errno": 2002})
|
|
||||||
logs.push("captcha", "WARNING", "Token 校验失败",
|
|
||||||
trace_id="req-abc", extra={"ip": "1.2.3.4"})
|
|
||||||
"""
|
|
||||||
|
|
||||||
import json
|
|
||||||
import os as _os
|
|
||||||
import sys as _sys
|
|
||||||
import atexit
|
|
||||||
from datetime import datetime, timezone
|
|
||||||
|
|
||||||
_sys.path.insert(0, _os.path.join(_os.path.dirname(__file__), ".."))
|
|
||||||
from pymsg import RpcClient, RpcError, AsyncRpcQueue # noqa: E402
|
|
||||||
|
|
||||||
# ── 配置 ──────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
_DBV2_ADDR = _os.environ.get('DB_HOST', '127.0.0.1')
|
|
||||||
_DBV2_PORT = int(_os.environ.get('DB_PORT', '8005'))
|
|
||||||
|
|
||||||
_BATCH_SIZE = 50
|
|
||||||
_FLUSH_INTERVAL = 2.0
|
|
||||||
_TIMEOUT = 5
|
|
||||||
_QUEUE_MAXSIZE = 10000
|
|
||||||
|
|
||||||
|
|
||||||
def _safe_json(obj):
|
|
||||||
"""安全序列化 extra 字段,不可序列化对象转为 repr 字符串。"""
|
|
||||||
try:
|
|
||||||
return json.dumps(obj, ensure_ascii=False, default=str)
|
|
||||||
except Exception:
|
|
||||||
return repr(obj)
|
|
||||||
|
|
||||||
|
|
||||||
def _now_iso():
|
|
||||||
"""生成当前 UTC 时间 ISO 字符串(入队时调用,保证时间戳准确)。"""
|
|
||||||
return datetime.now(timezone.utc).isoformat()
|
|
||||||
|
|
||||||
|
|
||||||
class LogClient:
|
|
||||||
"""异步批量日志推送客户端(API 与旧版完全兼容)。"""
|
|
||||||
|
|
||||||
def __init__(self, service_name: str = "log",
|
|
||||||
batch_size: int = _BATCH_SIZE,
|
|
||||||
flush_interval: float = _FLUSH_INTERVAL):
|
|
||||||
self._service_name = service_name
|
|
||||||
self._client = RpcClient(
|
|
||||||
_DBV2_ADDR, _DBV2_PORT,
|
|
||||||
service_name + "-log",
|
|
||||||
timeout=_TIMEOUT,
|
|
||||||
)
|
|
||||||
self._queue = AsyncRpcQueue(
|
|
||||||
handler=self._send_batch,
|
|
||||||
batch_size=batch_size,
|
|
||||||
flush_interval=flush_interval,
|
|
||||||
max_queue_size=_QUEUE_MAXSIZE,
|
|
||||||
on_error=self._on_error,
|
|
||||||
)
|
|
||||||
self._queue.start()
|
|
||||||
atexit.register(self.close)
|
|
||||||
|
|
||||||
# ── 公开接口 ────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
def push(self, service: str, level: str, message: str,
|
|
||||||
trace_id: str = None, extra=None):
|
|
||||||
"""推送一条日志(非阻塞,立即返回)。
|
|
||||||
|
|
||||||
队列满时静默丢弃最旧条目再放入新条目,保证不阻塞业务线程、不导致 OOM。
|
|
||||||
"""
|
|
||||||
entry = {
|
|
||||||
'ts': _now_iso(),
|
|
||||||
'service': service,
|
|
||||||
'level': level,
|
|
||||||
'message': message,
|
|
||||||
'trace_id': trace_id,
|
|
||||||
'extra': extra,
|
|
||||||
}
|
|
||||||
self._queue.put(entry)
|
|
||||||
|
|
||||||
def debug(self, service: str, message: str, **kw):
|
|
||||||
self.push(service, 'DEBUG', message, **kw)
|
|
||||||
|
|
||||||
def info(self, service: str, message: str, **kw):
|
|
||||||
self.push(service, 'INFO', message, **kw)
|
|
||||||
|
|
||||||
def warning(self, service: str, message: str, **kw):
|
|
||||||
self.push(service, 'WARNING', message, **kw)
|
|
||||||
|
|
||||||
def warn(self, service: str, message: str, **kw):
|
|
||||||
self.push(service, 'WARNING', message, **kw)
|
|
||||||
|
|
||||||
def error(self, service: str, message: str, **kw):
|
|
||||||
self.push(service, 'ERROR', message, **kw)
|
|
||||||
|
|
||||||
def critical(self, service: str, message: str, **kw):
|
|
||||||
self.push(service, 'CRITICAL', message, **kw)
|
|
||||||
|
|
||||||
def flush(self):
|
|
||||||
"""手动 flush 队列中剩余的日志。"""
|
|
||||||
self._queue.flush()
|
|
||||||
|
|
||||||
def close(self):
|
|
||||||
"""关闭客户端,flush 剩余日志。"""
|
|
||||||
self._queue.close()
|
|
||||||
|
|
||||||
# ── 内部实现 ────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
def _send_batch(self, batch: list):
|
|
||||||
"""后台线程回调:逐条 INSERT 到 db(与旧版行为一致:网络错误放弃本批剩余)。"""
|
|
||||||
for entry in batch:
|
|
||||||
extra_val = entry.get("extra")
|
|
||||||
try:
|
|
||||||
extra_json = _safe_json(extra_val) if extra_val is not None else None
|
|
||||||
except Exception:
|
|
||||||
extra_json = None
|
|
||||||
payload = {
|
|
||||||
"action": "insert",
|
|
||||||
"table": "Logs",
|
|
||||||
"values": {
|
|
||||||
"ts": entry.get("ts", _now_iso()),
|
|
||||||
"service": entry.get("service"),
|
|
||||||
"level": entry.get("level"),
|
|
||||||
"message": entry.get("message"),
|
|
||||||
"trace_id": entry.get("trace_id"),
|
|
||||||
"extra": extra_json,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
try:
|
|
||||||
self._client.call("execute", payload, timeout=_TIMEOUT)
|
|
||||||
except RpcError as e:
|
|
||||||
print(f"[log_client] INSERT 失败: {e}", file=_sys.stderr)
|
|
||||||
except Exception as e:
|
|
||||||
print(f"[log_client] 发送失败(放弃本批剩余): {e}", file=_sys.stderr)
|
|
||||||
break
|
|
||||||
|
|
||||||
def _on_error(self, e: Exception, batch: list):
|
|
||||||
"""异常安全网:handler 外的异常也不会崩溃线程。"""
|
|
||||||
print(f"[log_client] _send_batch 异常(已捕获,线程继续运行): {e}",
|
|
||||||
file=_sys.stderr)
|
|
||||||
|
|
||||||
|
|
||||||
# ── 全局单例 ──────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
logs = LogClient(service_name="captcha")
|
logs = LogClient(service_name="captcha")
|
||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
CXX := g++
|
CXX := g++
|
||||||
CXXFLAGS := -std=c++20 -Wall -Wextra -O2
|
CXXFLAGS := -std=c++20 -Wall -Wextra -O2
|
||||||
INCS := -I./inc -I/usr/include/freetype2
|
INCS := -I./inc -I/usr/include/freetype2
|
||||||
LIBS := -lus_msg -lboost_json -lboost_program_options -lfreetype -lpthread -lm
|
LIBS := -lus_msg -lus_logc -lboost_json -lboost_program_options -lfreetype -lpthread -lm
|
||||||
|
|
||||||
TARGET := captcha2.bin
|
TARGET := captcha2.bin
|
||||||
SRC := src/main.cpp
|
SRC := src/main.cpp
|
||||||
|
|||||||
@@ -1,139 +0,0 @@
|
|||||||
/*
|
|
||||||
* LogC.hpp — captcha2 异步日志客户端(简化版)
|
|
||||||
*
|
|
||||||
* 基于 libmsg AsyncQueue,异步批量推送日志到 db:8005 的 Logs 表。
|
|
||||||
* - 内存队列 + 后台线程攒批(满 50 条或 2 秒 flush)
|
|
||||||
* - fail-safe:db 不可用时静默丢弃
|
|
||||||
* - API: info() / warning() / error()
|
|
||||||
*/
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include <us/Msg.hpp>
|
|
||||||
#include <us/AsyncQueue.hpp>
|
|
||||||
#include <boost/json.hpp>
|
|
||||||
#include <iostream>
|
|
||||||
#include <string>
|
|
||||||
#include <vector>
|
|
||||||
#include <chrono>
|
|
||||||
#include <cstdlib>
|
|
||||||
#include <atomic>
|
|
||||||
#include <unordered_map>
|
|
||||||
|
|
||||||
namespace captcha2 {
|
|
||||||
|
|
||||||
struct LogEntry {
|
|
||||||
std::string ts;
|
|
||||||
std::string service;
|
|
||||||
std::string level;
|
|
||||||
std::string message;
|
|
||||||
std::string trace_id;
|
|
||||||
std::string extra;
|
|
||||||
};
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
void warning(const std::string& svc, const std::string& msg) {
|
|
||||||
push(svc, "WARNING", msg);
|
|
||||||
}
|
|
||||||
void error(const std::string& svc, const std::string& msg) {
|
|
||||||
push(svc, "ERROR", msg);
|
|
||||||
}
|
|
||||||
|
|
||||||
private:
|
|
||||||
LogC() {
|
|
||||||
const char* host = std::getenv("DB_HOST");
|
|
||||||
const char* port = std::getenv("DB_PORT");
|
|
||||||
db_host_ = host ? host : "127.0.0.1";
|
|
||||||
db_port_ = port ? std::atoi(port) : 8005;
|
|
||||||
|
|
||||||
queue_ = std::make_unique<pmc::net::AsyncQueue<LogEntry>>(
|
|
||||||
[this](std::vector<LogEntry>&& batch) { send_batch(std::move(batch)); },
|
|
||||||
pmc::net::AsyncQueueConfig{.batch_size = 50, .flush_interval = 2.0, .max_queue_size = 10000},
|
|
||||||
[](const std::exception& e, std::vector<LogEntry>&&) {
|
|
||||||
std::cerr << "[captcha2/log] send failed: " << e.what() << std::endl;
|
|
||||||
});
|
|
||||||
queue_->start();
|
|
||||||
}
|
|
||||||
|
|
||||||
~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];
|
|
||||||
std::strftime(buf, sizeof(buf), "%Y-%m-%dT%H:%M:%SZ", std::gmtime(&t));
|
|
||||||
|
|
||||||
LogEntry e;
|
|
||||||
e.ts = buf;
|
|
||||||
e.service = svc;
|
|
||||||
e.level = level;
|
|
||||||
e.message = msg;
|
|
||||||
queue_->push(std::move(e));
|
|
||||||
}
|
|
||||||
|
|
||||||
void send_batch(std::vector<LogEntry>&& batch) {
|
|
||||||
try {
|
|
||||||
// P3-4: 复用单连接发送整批日志,超时 1.0s(日志非关键路径,db2 慢时不阻塞 flush 线程过久)
|
|
||||||
pmc::net::MsgClient client(db_host_, db_port_, "captcha2-log");
|
|
||||||
for (auto& e : batch) {
|
|
||||||
boost::json::object vals;
|
|
||||||
vals["ts"] = e.ts;
|
|
||||||
vals["service"] = e.service;
|
|
||||||
vals["level"] = e.level;
|
|
||||||
vals["message"] = e.message;
|
|
||||||
vals["trace_id"] = e.trace_id.empty() ? nullptr : boost::json::value(e.trace_id);
|
|
||||||
vals["extra"] = e.extra.empty() ? nullptr : boost::json::value(e.extra);
|
|
||||||
|
|
||||||
boost::json::object req;
|
|
||||||
req["_route"] = "execute";
|
|
||||||
req["action"] = "insert";
|
|
||||||
req["table"] = "Logs";
|
|
||||||
req["values"] = vals;
|
|
||||||
|
|
||||||
client.call(boost::json::serialize(req), 1.0);
|
|
||||||
}
|
|
||||||
} catch (const std::exception& e) {
|
|
||||||
std::cerr << "[captcha2/log] batch drop: " << e.what() << std::endl;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string db_host_;
|
|
||||||
int db_port_;
|
|
||||||
std::atomic<int> level_threshold_{1}; // 日志级别阈值,默认 INFO(1)
|
|
||||||
std::unique_ptr<pmc::net::AsyncQueue<LogEntry>> queue_;
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace captcha2
|
|
||||||
+10
-27
@@ -36,12 +36,12 @@
|
|||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include "LogC.hpp"
|
#include <us/LogC.hpp>
|
||||||
|
|
||||||
namespace json = boost::json;
|
namespace json = boost::json;
|
||||||
namespace po = boost::program_options;
|
namespace po = boost::program_options;
|
||||||
|
|
||||||
using namespace captcha2;
|
using qing::LogC;
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
// 1. 配置结构
|
// 1. 配置结构
|
||||||
@@ -70,13 +70,12 @@ struct CaptchaConfig {
|
|||||||
int rate_limit_per_min = 600; // P1-1: 全局限流仅作最后防线,主要限流由 gateway 按 IP 负责
|
int rate_limit_per_min = 600; // P1-1: 全局限流仅作最后防线,主要限流由 gateway 按 IP 负责
|
||||||
};
|
};
|
||||||
|
|
||||||
// 从 confs 拉取指定名称的配置文件全文,阻塞等待 confs 就绪
|
// 从 confs 拉取 captcha.ini 全文,阻塞等待 confs 就绪
|
||||||
static std::string fetch_config_from_confs(const std::string& confs_host, int confs_port,
|
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",
|
pmc::net::RpcClient client(confs_host, confs_port, "captcha2",
|
||||||
/*timeout=*/5.0, /*retry_on_refused=*/true);
|
/*timeout=*/5.0, /*retry_on_refused=*/true);
|
||||||
try {
|
try {
|
||||||
auto data = client.call("get", {{"name", config_name}});
|
auto data = client.call("get", {{"name", "captcha.ini"}});
|
||||||
auto* content = data.as_object().if_contains("content");
|
auto* content = data.as_object().if_contains("content");
|
||||||
if (!content || !content->is_string()) {
|
if (!content || !content->is_string()) {
|
||||||
std::cerr << "[captcha2] confs 返回的 captcha.ini 无 content 字段" << std::endl;
|
std::cerr << "[captcha2] confs 返回的 captcha.ini 无 content 字段" << std::endl;
|
||||||
@@ -526,15 +525,13 @@ static CaptchaConfig g_config;
|
|||||||
static CaptchaStore g_store;
|
static CaptchaStore g_store;
|
||||||
static RateLimiter g_rate_limiter;
|
static RateLimiter g_rate_limiter;
|
||||||
static auto& g_log = LogC::instance();
|
static auto& g_log = LogC::instance();
|
||||||
|
static const auto _log_init = (LogC::instance().set_service_name("captcha2-log"), true);
|
||||||
|
|
||||||
int main(int argc, char* argv[]) {
|
int main(int argc, char* argv[]) {
|
||||||
try {
|
try {
|
||||||
// ── 命令行参数 ──
|
// ── 命令行参数 ──
|
||||||
std::string confs_host = "127.0.0.1";
|
std::string confs_host = "127.0.0.1";
|
||||||
int confs_port = 8010;
|
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;
|
const char* env;
|
||||||
if ((env = std::getenv("CONFS_HOST"))) confs_host = env;
|
if ((env = std::getenv("CONFS_HOST"))) confs_host = env;
|
||||||
@@ -544,13 +541,9 @@ int main(int argc, char* argv[]) {
|
|||||||
po::options_description desc("captcha2 — pymsg 验证码服务");
|
po::options_description desc("captcha2 — pymsg 验证码服务");
|
||||||
desc.add_options()
|
desc.add_options()
|
||||||
("help,h", "显示帮助")
|
("help,h", "显示帮助")
|
||||||
("addr,a", po::value<std::string>(), "监听地址(覆盖 confs,别名 --host)")
|
("addr,a", po::value<std::string>(), "监听地址(覆盖 confs)")
|
||||||
("host", po::value<std::string>(), "监听地址(覆盖 confs,别名 --addr/-a)")
|
|
||||||
("port,p", po::value<int>(), "监听端口(覆盖 confs)")
|
("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::variables_map vm;
|
||||||
po::store(po::parse_command_line(argc, argv, desc), vm);
|
po::store(po::parse_command_line(argc, argv, desc), vm);
|
||||||
po::notify(vm);
|
po::notify(vm);
|
||||||
@@ -560,30 +553,20 @@ int main(int argc, char* argv[]) {
|
|||||||
return 0;
|
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 → 命令行 ──
|
// ── 配置加载:confs → env → 命令行 ──
|
||||||
std::string ini_text = fetch_config_from_confs(confs_host, confs_port, confs_config_name);
|
std::string ini_text = fetch_config_from_confs(confs_host, confs_port);
|
||||||
merge_ini(g_config, ini_text);
|
merge_ini(g_config, ini_text);
|
||||||
|
|
||||||
apply_env_overrides(g_config);
|
apply_env_overrides(g_config);
|
||||||
|
|
||||||
if (vm.count("addr")) g_config.host = vm["addr"].as<std::string>();
|
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("port")) g_config.port = vm["port"].as<int>();
|
||||||
if (vm.count("workers")) g_config.workers = vm["workers"].as<int>();
|
if (vm.count("workers")) g_config.workers = vm["workers"].as<int>();
|
||||||
|
|
||||||
validate_config(g_config);
|
validate_config(g_config);
|
||||||
|
|
||||||
g_log.set_level(log_level);
|
std::cerr << "[captcha2] 配置来源: confs captcha.ini @ "
|
||||||
|
|
||||||
std::cerr << "[captcha2] 配置来源: confs " << confs_config_name << " @ "
|
|
||||||
<< confs_host << ":" << confs_port << std::endl;
|
<< 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
|
std::cerr << "[captcha2] captcha_len=" << g_config.len
|
||||||
<< " ttl=" << g_config.ttl
|
<< " ttl=" << g_config.ttl
|
||||||
<< " max_store=" << g_config.max_store
|
<< " max_store=" << g_config.max_store
|
||||||
|
|||||||
+2
-158
@@ -1,160 +1,4 @@
|
|||||||
"""
|
"""日志客户端 — 委托到 pymsg.logc 统一库。"""
|
||||||
统一日志客户端 — 各服务共用(基于 pymsg.AsyncRpcQueue)。
|
from pymsg.logc import LogClient, set_log_level # noqa: F401
|
||||||
|
|
||||||
设计要点:
|
|
||||||
- 通过 pymsg RPC 调用 dbv2 execute 路由,逐条 INSERT 到 Logs 表
|
|
||||||
- 异步非阻塞:内存队列 + 后台线程攒批推送,不拖慢业务
|
|
||||||
- fail-safe:db 服务不可用时静默丢弃,绝不影响业务
|
|
||||||
- 缓冲批量:队列满 50 条或超时 2 秒时自动 flush
|
|
||||||
- 时间戳:日志入队时即记录 ts(UTC ISO 格式),反映事件真实发生时间
|
|
||||||
- 底层复用 pymsg.AsyncRpcQueue 通用队列基础设施
|
|
||||||
|
|
||||||
用法(与旧版完全兼容):
|
|
||||||
from log_client import logs
|
|
||||||
|
|
||||||
logs.info("dnfs", "节点新增成功 id=42")
|
|
||||||
logs.error("dnfs", "数据库连接失败", extra={"errno": 2002})
|
|
||||||
logs.push("gateway", "WARNING", "登录失败次数超限",
|
|
||||||
trace_id="req-abc", extra={"ip": "1.2.3.4"})
|
|
||||||
"""
|
|
||||||
|
|
||||||
import json
|
|
||||||
import os as _os
|
|
||||||
import sys as _sys
|
|
||||||
import atexit
|
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
_sys.path.insert(0, _os.path.join(_os.path.dirname(__file__), ".."))
|
|
||||||
from pymsg import RpcClient, RpcError, AsyncRpcQueue # noqa: E402
|
|
||||||
|
|
||||||
# ── 配置 ──────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
_DBV2_ADDR = _os.environ.get('DB_HOST', '127.0.0.1')
|
|
||||||
_DBV2_PORT = int(_os.environ.get('DB_PORT', '8005'))
|
|
||||||
|
|
||||||
_BATCH_SIZE = 50
|
|
||||||
_FLUSH_INTERVAL = 2.0
|
|
||||||
_TIMEOUT = 5
|
|
||||||
_QUEUE_MAXSIZE = 10000
|
|
||||||
|
|
||||||
|
|
||||||
def _safe_json(obj):
|
|
||||||
"""安全序列化 extra 字段,不可序列化对象转为 repr 字符串。"""
|
|
||||||
try:
|
|
||||||
return json.dumps(obj, ensure_ascii=False, default=str)
|
|
||||||
except Exception:
|
|
||||||
return repr(obj)
|
|
||||||
|
|
||||||
|
|
||||||
def _now_iso():
|
|
||||||
"""生成当前系统时间 ISO 字符串(入队时调用,保证时间戳准确)。"""
|
|
||||||
return datetime.now().astimezone().isoformat()
|
|
||||||
|
|
||||||
|
|
||||||
class LogClient:
|
|
||||||
"""异步批量日志推送客户端(API 与旧版完全兼容)。"""
|
|
||||||
|
|
||||||
def __init__(self, service_name: str = "log",
|
|
||||||
batch_size: int = _BATCH_SIZE,
|
|
||||||
flush_interval: float = _FLUSH_INTERVAL):
|
|
||||||
self._service_name = service_name
|
|
||||||
self._client = RpcClient(
|
|
||||||
_DBV2_ADDR, _DBV2_PORT,
|
|
||||||
service_name + "-log",
|
|
||||||
timeout=_TIMEOUT,
|
|
||||||
)
|
|
||||||
self._queue = AsyncRpcQueue(
|
|
||||||
handler=self._send_batch,
|
|
||||||
batch_size=batch_size,
|
|
||||||
flush_interval=flush_interval,
|
|
||||||
max_queue_size=_QUEUE_MAXSIZE,
|
|
||||||
on_error=self._on_error,
|
|
||||||
)
|
|
||||||
self._queue.start()
|
|
||||||
atexit.register(self.close)
|
|
||||||
|
|
||||||
# ── 公开接口 ────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
def push(self, service: str, level: str, message: str,
|
|
||||||
trace_id: str = None, extra=None):
|
|
||||||
"""推送一条日志(非阻塞,立即返回)。
|
|
||||||
|
|
||||||
队列满时静默丢弃最旧条目再放入新条目,保证不阻塞业务线程、不导致 OOM。
|
|
||||||
"""
|
|
||||||
entry = {
|
|
||||||
'ts': _now_iso(),
|
|
||||||
'service': service,
|
|
||||||
'level': level,
|
|
||||||
'message': message,
|
|
||||||
'trace_id': trace_id,
|
|
||||||
'extra': extra,
|
|
||||||
}
|
|
||||||
self._queue.put(entry)
|
|
||||||
|
|
||||||
def debug(self, service: str, message: str, **kw):
|
|
||||||
self.push(service, 'DEBUG', message, **kw)
|
|
||||||
|
|
||||||
def info(self, service: str, message: str, **kw):
|
|
||||||
self.push(service, 'INFO', message, **kw)
|
|
||||||
|
|
||||||
def warning(self, service: str, message: str, **kw):
|
|
||||||
self.push(service, 'WARNING', message, **kw)
|
|
||||||
|
|
||||||
def warn(self, service: str, message: str, **kw):
|
|
||||||
self.push(service, 'WARNING', message, **kw)
|
|
||||||
|
|
||||||
def error(self, service: str, message: str, **kw):
|
|
||||||
self.push(service, 'ERROR', message, **kw)
|
|
||||||
|
|
||||||
def critical(self, service: str, message: str, **kw):
|
|
||||||
self.push(service, 'CRITICAL', message, **kw)
|
|
||||||
|
|
||||||
def flush(self):
|
|
||||||
"""手动 flush 队列中剩余的日志。"""
|
|
||||||
self._queue.flush()
|
|
||||||
|
|
||||||
def close(self):
|
|
||||||
"""关闭客户端,flush 剩余日志。"""
|
|
||||||
self._queue.close()
|
|
||||||
|
|
||||||
# ── 内部实现 ────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
def _send_batch(self, batch: list):
|
|
||||||
"""后台线程回调:逐条 INSERT 到 db(与旧版行为一致:网络错误放弃本批剩余)。"""
|
|
||||||
for entry in batch:
|
|
||||||
extra_val = entry.get("extra")
|
|
||||||
try:
|
|
||||||
extra_json = _safe_json(extra_val) if extra_val is not None else None
|
|
||||||
except Exception:
|
|
||||||
extra_json = None
|
|
||||||
payload = {
|
|
||||||
"action": "insert",
|
|
||||||
"table": "Logs",
|
|
||||||
"values": {
|
|
||||||
"ts": entry.get("ts", _now_iso()),
|
|
||||||
"service": entry.get("service"),
|
|
||||||
"level": entry.get("level"),
|
|
||||||
"message": entry.get("message"),
|
|
||||||
"trace_id": entry.get("trace_id"),
|
|
||||||
"extra": extra_json,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
try:
|
|
||||||
self._client.call("execute", payload, timeout=_TIMEOUT)
|
|
||||||
except RpcError as e:
|
|
||||||
print(f"[log_client] INSERT 失败: {e}", file=_sys.stderr)
|
|
||||||
except Exception as e:
|
|
||||||
print(f"[log_client] 发送失败(放弃本批剩余): {e}", file=_sys.stderr)
|
|
||||||
break
|
|
||||||
|
|
||||||
def _on_error(self, e: Exception, batch: list):
|
|
||||||
"""异常安全网:handler 外的异常也不会崩溃线程。"""
|
|
||||||
print(f"[log_client] _send_batch 异常(已捕获,线程继续运行): {e}",
|
|
||||||
file=_sys.stderr)
|
|
||||||
|
|
||||||
|
|
||||||
# ── 全局单例 ──────────────────────────────────────────────────────────
|
|
||||||
# 各服务的 log_client.py 副本需将 service_name 改为自己的服务名。
|
|
||||||
# 此文件(confs/log_client.py)的 service_name 为 "confs"。
|
|
||||||
|
|
||||||
logs = LogClient(service_name="confs")
|
logs = LogClient(service_name="confs")
|
||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
CXX := g++
|
CXX := g++
|
||||||
CXXFLAGS := -std=c++20 -Wall -Wextra -O2
|
CXXFLAGS := -std=c++20 -Wall -Wextra -O2
|
||||||
INCS := -I./inc
|
INCS := -I./inc
|
||||||
LIBS := -lus_msg -lboost_json -lboost_program_options -lpthread
|
LIBS := -lus_msg -lus_logc -lboost_json -lboost_program_options -lpthread
|
||||||
|
|
||||||
TARGET := confs2.bin
|
TARGET := confs2.bin
|
||||||
SRC := src/main.cpp
|
SRC := src/main.cpp
|
||||||
|
|||||||
@@ -1,129 +0,0 @@
|
|||||||
/*
|
|
||||||
* LogC.hpp — confs2 异步日志客户端
|
|
||||||
*
|
|
||||||
* 基于 libmsg AsyncQueue,异步批量推送日志到 db:8005 的 Logs 表。
|
|
||||||
* - 内存队列 + 后台线程攒批(满 50 条或 2 秒 flush)
|
|
||||||
* - fail-safe:db 不可用时静默丢弃
|
|
||||||
* - API: info() / warning() / error()
|
|
||||||
*
|
|
||||||
* 改编自 captcha2/inc/LogC.hpp,仅改 namespace 和服务名。
|
|
||||||
*/
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include <us/Msg.hpp>
|
|
||||||
#include <us/AsyncQueue.hpp>
|
|
||||||
#include <boost/json.hpp>
|
|
||||||
#include <iostream>
|
|
||||||
#include <string>
|
|
||||||
#include <vector>
|
|
||||||
#include <chrono>
|
|
||||||
#include <cstdlib>
|
|
||||||
#include <algorithm>
|
|
||||||
#include <cctype>
|
|
||||||
|
|
||||||
namespace confs2 {
|
|
||||||
|
|
||||||
struct LogEntry {
|
|
||||||
std::string ts;
|
|
||||||
std::string service;
|
|
||||||
std::string level;
|
|
||||||
std::string message;
|
|
||||||
std::string trace_id;
|
|
||||||
std::string extra;
|
|
||||||
};
|
|
||||||
|
|
||||||
class LogC {
|
|
||||||
public:
|
|
||||||
static LogC& instance() {
|
|
||||||
static LogC inst;
|
|
||||||
return inst;
|
|
||||||
}
|
|
||||||
|
|
||||||
void info(const std::string& svc, const std::string& msg) {
|
|
||||||
push(svc, "INFO", msg);
|
|
||||||
}
|
|
||||||
void warning(const std::string& svc, const std::string& msg) {
|
|
||||||
push(svc, "WARNING", msg);
|
|
||||||
}
|
|
||||||
void error(const std::string& svc, const std::string& msg) {
|
|
||||||
push(svc, "ERROR", msg);
|
|
||||||
}
|
|
||||||
|
|
||||||
private:
|
|
||||||
LogC() {
|
|
||||||
const char* host = std::getenv("DB_HOST");
|
|
||||||
const char* port = std::getenv("DB_PORT");
|
|
||||||
db_host_ = host ? host : "127.0.0.1";
|
|
||||||
db_port_ = port ? std::atoi(port) : 8005;
|
|
||||||
|
|
||||||
queue_ = std::make_unique<pmc::net::AsyncQueue<LogEntry>>(
|
|
||||||
[this](std::vector<LogEntry>&& batch) { send_batch(std::move(batch)); },
|
|
||||||
pmc::net::AsyncQueueConfig{.batch_size = 50, .flush_interval = 2.0, .max_queue_size = 10000},
|
|
||||||
[](const std::exception& e, std::vector<LogEntry>&&) {
|
|
||||||
std::cerr << "[confs2/log] send failed: " << e.what() << std::endl;
|
|
||||||
});
|
|
||||||
queue_->start();
|
|
||||||
}
|
|
||||||
|
|
||||||
~LogC() { if (queue_) queue_->shutdown(); }
|
|
||||||
|
|
||||||
// P2-3 修复:过滤控制字符,防止日志注入
|
|
||||||
static std::string sanitize_for_log(std::string s) {
|
|
||||||
s.erase(std::remove_if(s.begin(), s.end(), [](unsigned char c) {
|
|
||||||
return c < 0x20 && c != '\t';
|
|
||||||
}), s.end());
|
|
||||||
return s;
|
|
||||||
}
|
|
||||||
|
|
||||||
void push(const std::string& svc, const std::string& level, const std::string& msg) {
|
|
||||||
auto now = std::chrono::system_clock::now();
|
|
||||||
auto t = std::chrono::system_clock::to_time_t(now);
|
|
||||||
// 系统本地时间 + 时区偏移
|
|
||||||
std::tm* lt = std::localtime(&t);
|
|
||||||
char buf[40];
|
|
||||||
std::strftime(buf, sizeof(buf), "%Y-%m-%dT%H:%M:%S", lt);
|
|
||||||
char tz_raw[8];
|
|
||||||
std::strftime(tz_raw, sizeof(tz_raw), "%z", lt);
|
|
||||||
std::string tz = tz_raw;
|
|
||||||
if (tz.length() >= 5)
|
|
||||||
tz = tz.substr(0, 3) + ":" + tz.substr(3);
|
|
||||||
|
|
||||||
LogEntry e;
|
|
||||||
e.ts = std::string(buf) + tz;
|
|
||||||
e.service = sanitize_for_log(svc);
|
|
||||||
e.level = level;
|
|
||||||
e.message = sanitize_for_log(msg);
|
|
||||||
queue_->push(std::move(e));
|
|
||||||
}
|
|
||||||
|
|
||||||
void send_batch(std::vector<LogEntry>&& batch) {
|
|
||||||
try {
|
|
||||||
pmc::net::MsgClient client(db_host_, db_port_, "confs-log");
|
|
||||||
for (auto& e : batch) {
|
|
||||||
boost::json::object vals;
|
|
||||||
vals["ts"] = e.ts;
|
|
||||||
vals["service"] = e.service;
|
|
||||||
vals["level"] = e.level;
|
|
||||||
vals["message"] = e.message;
|
|
||||||
vals["trace_id"] = e.trace_id.empty() ? nullptr : boost::json::value(e.trace_id);
|
|
||||||
vals["extra"] = e.extra.empty() ? nullptr : boost::json::value(e.extra);
|
|
||||||
|
|
||||||
boost::json::object req;
|
|
||||||
req["_route"] = "execute";
|
|
||||||
req["action"] = "insert";
|
|
||||||
req["table"] = "Logs";
|
|
||||||
req["values"] = vals;
|
|
||||||
|
|
||||||
client.call(boost::json::serialize(req), 1.0);
|
|
||||||
}
|
|
||||||
} catch (const std::exception& e) {
|
|
||||||
std::cerr << "[confs2/log] batch drop: " << e.what() << std::endl;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string db_host_;
|
|
||||||
int db_port_;
|
|
||||||
std::unique_ptr<pmc::net::AsyncQueue<LogEntry>> queue_;
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace confs2
|
|
||||||
+3
-2
@@ -31,13 +31,13 @@
|
|||||||
#include <unordered_set>
|
#include <unordered_set>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include "LogC.hpp"
|
#include <us/LogC.hpp>
|
||||||
|
|
||||||
namespace json = boost::json;
|
namespace json = boost::json;
|
||||||
namespace po = boost::program_options;
|
namespace po = boost::program_options;
|
||||||
namespace fs = std::filesystem;
|
namespace fs = std::filesystem;
|
||||||
|
|
||||||
using confs2::LogC;
|
using qing::LogC;
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════
|
||||||
// 1. 常量与异常
|
// 1. 常量与异常
|
||||||
@@ -411,6 +411,7 @@ static void validate_config(const ConfsConfig& cfg) {
|
|||||||
static ConfsConfig g_config;
|
static ConfsConfig g_config;
|
||||||
static ConfigStore* g_store = nullptr;
|
static ConfigStore* g_store = nullptr;
|
||||||
static auto& g_log = LogC::instance();
|
static auto& g_log = LogC::instance();
|
||||||
|
static const auto _log_init = (LogC::instance().set_service_name("confs2-log"), true);
|
||||||
static const auto g_start_time = std::chrono::steady_clock::now();
|
static const auto g_start_time = std::chrono::steady_clock::now();
|
||||||
|
|
||||||
static std::int64_t uptime_seconds() {
|
static std::int64_t uptime_seconds() {
|
||||||
|
|||||||
+1
-1
@@ -16,7 +16,7 @@ CPP:= g++
|
|||||||
|
|
||||||
INCS:= -I include/
|
INCS:= -I include/
|
||||||
|
|
||||||
MY_LIBS := -L/usr/lib -lus_th -lus_http -lus_httpclient -lus_msg -lcurl
|
MY_LIBS := -L/usr/lib -lus_th -lus_http -lus_httpclient -lus_msg -lus_logc -lcurl
|
||||||
LIBS := -lpthread -lssl -lcrypto -lboost_program_options -lboost_json -lboost_thread -lboost_filesystem -lyaml-cpp
|
LIBS := -lpthread -lssl -lcrypto -lboost_program_options -lboost_json -lboost_thread -lboost_filesystem -lyaml-cpp
|
||||||
|
|
||||||
CPPFLAGS := --std=c++20 -O3 -pipe #-Wall -Werror
|
CPPFLAGS := --std=c++20 -O3 -pipe #-Wall -Werror
|
||||||
|
|||||||
@@ -1,132 +0,0 @@
|
|||||||
/*
|
|
||||||
* ConfsLogC.hpp — core 内嵌 confs 服务的异步日志客户端
|
|
||||||
*
|
|
||||||
* 基于 libmsg AsyncQueue,异步批量推送日志到 db:8005 的 Logs 表。
|
|
||||||
* - 内存队列 + 后台线程攒批(满 50 条或 2 秒 flush)
|
|
||||||
* - fail-safe:db 不可用时静默丢弃
|
|
||||||
* - API: info() / warning() / error()
|
|
||||||
*
|
|
||||||
* 改编自 confs2/inc/LogC.hpp,仅改 namespace(qing::confs),
|
|
||||||
* 服务名保持 "confs" 与 Python/standalone confs2 日志连续。
|
|
||||||
*/
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include <us/Msg.hpp>
|
|
||||||
#include <us/AsyncQueue.hpp>
|
|
||||||
#include <boost/json.hpp>
|
|
||||||
#include <iostream>
|
|
||||||
#include <string>
|
|
||||||
#include <vector>
|
|
||||||
#include <chrono>
|
|
||||||
#include <cstdlib>
|
|
||||||
#include <algorithm>
|
|
||||||
#include <cctype>
|
|
||||||
|
|
||||||
namespace qing {
|
|
||||||
namespace confs {
|
|
||||||
|
|
||||||
struct ConfsLogEntry {
|
|
||||||
std::string ts;
|
|
||||||
std::string service;
|
|
||||||
std::string level;
|
|
||||||
std::string message;
|
|
||||||
std::string trace_id;
|
|
||||||
std::string extra;
|
|
||||||
};
|
|
||||||
|
|
||||||
class ConfsLogC {
|
|
||||||
public:
|
|
||||||
static ConfsLogC& instance() {
|
|
||||||
static ConfsLogC inst;
|
|
||||||
return inst;
|
|
||||||
}
|
|
||||||
|
|
||||||
void info(const std::string& svc, const std::string& msg) {
|
|
||||||
push(svc, "INFO", msg);
|
|
||||||
}
|
|
||||||
void warning(const std::string& svc, const std::string& msg) {
|
|
||||||
push(svc, "WARNING", msg);
|
|
||||||
}
|
|
||||||
void error(const std::string& svc, const std::string& msg) {
|
|
||||||
push(svc, "ERROR", msg);
|
|
||||||
}
|
|
||||||
|
|
||||||
private:
|
|
||||||
ConfsLogC() {
|
|
||||||
const char* host = std::getenv("DB_HOST");
|
|
||||||
const char* port = std::getenv("DB_PORT");
|
|
||||||
db_host_ = host ? host : "127.0.0.1";
|
|
||||||
db_port_ = port ? std::atoi(port) : 8005;
|
|
||||||
|
|
||||||
queue_ = std::make_unique<pmc::net::AsyncQueue<ConfsLogEntry>>(
|
|
||||||
[this](std::vector<ConfsLogEntry>&& batch) { send_batch(std::move(batch)); },
|
|
||||||
pmc::net::AsyncQueueConfig{.batch_size = 50, .flush_interval = 2.0, .max_queue_size = 10000},
|
|
||||||
[](const std::exception& e, std::vector<ConfsLogEntry>&&) {
|
|
||||||
std::cerr << "[core/confs-log] send failed: " << e.what() << std::endl;
|
|
||||||
});
|
|
||||||
queue_->start();
|
|
||||||
}
|
|
||||||
|
|
||||||
~ConfsLogC() { if (queue_) queue_->shutdown(); }
|
|
||||||
|
|
||||||
// P2-3 修复:过滤控制字符,防止日志注入
|
|
||||||
static std::string sanitize_for_log(std::string s) {
|
|
||||||
s.erase(std::remove_if(s.begin(), s.end(), [](unsigned char c) {
|
|
||||||
return c < 0x20 && c != '\t';
|
|
||||||
}), s.end());
|
|
||||||
return s;
|
|
||||||
}
|
|
||||||
|
|
||||||
void push(const std::string& svc, const std::string& level, const std::string& msg) {
|
|
||||||
auto now = std::chrono::system_clock::now();
|
|
||||||
auto t = std::chrono::system_clock::to_time_t(now);
|
|
||||||
// 系统本地时间 + 时区偏移
|
|
||||||
std::tm* lt = std::localtime(&t);
|
|
||||||
char buf[40];
|
|
||||||
std::strftime(buf, sizeof(buf), "%Y-%m-%dT%H:%M:%S", lt);
|
|
||||||
char tz_raw[8];
|
|
||||||
std::strftime(tz_raw, sizeof(tz_raw), "%z", lt);
|
|
||||||
std::string tz = tz_raw;
|
|
||||||
if (tz.length() >= 5)
|
|
||||||
tz = tz.substr(0, 3) + ":" + tz.substr(3);
|
|
||||||
|
|
||||||
ConfsLogEntry e;
|
|
||||||
e.ts = std::string(buf) + tz;
|
|
||||||
e.service = sanitize_for_log(svc);
|
|
||||||
e.level = level;
|
|
||||||
e.message = sanitize_for_log(msg);
|
|
||||||
queue_->push(std::move(e));
|
|
||||||
}
|
|
||||||
|
|
||||||
void send_batch(std::vector<ConfsLogEntry>&& batch) {
|
|
||||||
try {
|
|
||||||
pmc::net::MsgClient client(db_host_, db_port_, "confs-log");
|
|
||||||
for (auto& e : batch) {
|
|
||||||
boost::json::object vals;
|
|
||||||
vals["ts"] = e.ts;
|
|
||||||
vals["service"] = e.service;
|
|
||||||
vals["level"] = e.level;
|
|
||||||
vals["message"] = e.message;
|
|
||||||
vals["trace_id"] = e.trace_id.empty() ? nullptr : boost::json::value(e.trace_id);
|
|
||||||
vals["extra"] = e.extra.empty() ? nullptr : boost::json::value(e.extra);
|
|
||||||
|
|
||||||
boost::json::object req;
|
|
||||||
req["_route"] = "execute";
|
|
||||||
req["action"] = "insert";
|
|
||||||
req["table"] = "Logs";
|
|
||||||
req["values"] = vals;
|
|
||||||
|
|
||||||
client.call(boost::json::serialize(req), 1.0);
|
|
||||||
}
|
|
||||||
} catch (const std::exception& e) {
|
|
||||||
std::cerr << "[core/confs-log] batch drop: " << e.what() << std::endl;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string db_host_;
|
|
||||||
int db_port_;
|
|
||||||
std::unique_ptr<pmc::net::AsyncQueue<ConfsLogEntry>> queue_;
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace confs
|
|
||||||
} // namespace qing
|
|
||||||
@@ -31,12 +31,12 @@
|
|||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include "main/ConfsTask.hh"
|
#include "main/ConfsTask.hh"
|
||||||
#include "main/ConfsLogC.hpp"
|
#include <us/LogC.hpp>
|
||||||
|
|
||||||
namespace json = boost::json;
|
namespace json = boost::json;
|
||||||
namespace fs = std::filesystem;
|
namespace fs = std::filesystem;
|
||||||
|
|
||||||
using qing::confs::ConfsLogC;
|
using qing::LogC;
|
||||||
|
|
||||||
namespace qing {
|
namespace qing {
|
||||||
|
|
||||||
@@ -386,6 +386,7 @@ ConfsTask::ConfsTask(std::string addr_, int port_, int workers_, std::string con
|
|||||||
: addr(std::move(addr_)), port(port_), workers(workers_),
|
: addr(std::move(addr_)), port(port_), workers(workers_),
|
||||||
started_at(std::chrono::steady_clock::now())
|
started_at(std::chrono::steady_clock::now())
|
||||||
{
|
{
|
||||||
|
LogC::instance().set_service_name("confs-log");
|
||||||
store = std::make_unique<ConfigStore>(config_dir);
|
store = std::make_unique<ConfigStore>(config_dir);
|
||||||
this->th = std::make_unique<Thread>();
|
this->th = std::make_unique<Thread>();
|
||||||
init_thread();
|
init_thread();
|
||||||
@@ -423,12 +424,12 @@ void ConfsTask::init_thread() {
|
|||||||
this->server = std::make_unique<pmc::net::RpcServer> (addr, port,
|
this->server = std::make_unique<pmc::net::RpcServer> (addr, port,
|
||||||
pmc::net::MsgServerConfig{.workers = workers},
|
pmc::net::MsgServerConfig{.workers = workers},
|
||||||
[](const std::string& route, const std::exception& e) {
|
[](const std::string& route, const std::exception& e) {
|
||||||
ConfsLogC::instance().error("confs", "路由 " + route + " 处理异常: " + e.what());
|
LogC::instance().error("confs", "路由 " + route + " 处理异常: " + e.what());
|
||||||
});
|
});
|
||||||
register_routes(*this->server);
|
register_routes(*this->server);
|
||||||
th.run();
|
th.run();
|
||||||
|
|
||||||
ConfsLogC::instance().info("confs", "开始监听 tcp://" + addr + ":"
|
LogC::instance().info("confs", "开始监听 tcp://" + addr + ":"
|
||||||
+ std::to_string(port));
|
+ std::to_string(port));
|
||||||
this->server->start(); /* 堵死线程,等待 stop() 解除 */
|
this->server->start(); /* 堵死线程,等待 stop() 解除 */
|
||||||
});
|
});
|
||||||
@@ -482,13 +483,13 @@ void ConfsTask::register_routes(pmc::net::RpcServer& rpc) {
|
|||||||
auto val = extract_ini_key(entry.content, key);
|
auto val = extract_ini_key(entry.content, key);
|
||||||
if (!val)
|
if (!val)
|
||||||
throw KeyNotFound("键不存在: " + name + " -> " + key);
|
throw KeyNotFound("键不存在: " + name + " -> " + key);
|
||||||
ConfsLogC::instance().info("confs", "分发配置键 " + name + " -> " + key);
|
LogC::instance().info("confs", "分发配置键 " + name + " -> " + key);
|
||||||
return json::object{{"name", name}, {"key", key}, {"value", *val}};
|
return json::object{{"name", name}, {"key", key}, {"value", *val}};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ext == ".json") {
|
if (ext == ".json") {
|
||||||
auto val = extract_json_key(entry.content, key);
|
auto val = extract_json_key(entry.content, key);
|
||||||
ConfsLogC::instance().info("confs", "分发配置键 " + name + " -> " + key);
|
LogC::instance().info("confs", "分发配置键 " + name + " -> " + key);
|
||||||
return json::object{{"name", name}, {"key", key}, {"value", val}};
|
return json::object{{"name", name}, {"key", key}, {"value", val}};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -519,7 +520,7 @@ void ConfsTask::register_routes(pmc::net::RpcServer& rpc) {
|
|||||||
// 省略 name 或 null → 清空全部缓存
|
// 省略 name 或 null → 清空全部缓存
|
||||||
if (it == req.end() || it->value().is_null()) {
|
if (it == req.end() || it->value().is_null()) {
|
||||||
auto names = store->reload(nullptr);
|
auto names = store->reload(nullptr);
|
||||||
ConfsLogC::instance().info("confs", "清空配置缓存(" + std::to_string(names.size()) + " 项)");
|
LogC::instance().info("confs", "清空配置缓存(" + std::to_string(names.size()) + " 项)");
|
||||||
json::array arr;
|
json::array arr;
|
||||||
for (const auto& n : names) arr.push_back(json::value(n));
|
for (const auto& n : names) arr.push_back(json::value(n));
|
||||||
return json::object{{"reloaded", arr}};
|
return json::object{{"reloaded", arr}};
|
||||||
@@ -534,7 +535,7 @@ void ConfsTask::register_routes(pmc::net::RpcServer& rpc) {
|
|||||||
throw ConfigError("name 字段不能为空(要清空全部缓存请省略 name 字段)");
|
throw ConfigError("name 字段不能为空(要清空全部缓存请省略 name 字段)");
|
||||||
|
|
||||||
auto names = store->reload(&name);
|
auto names = store->reload(&name);
|
||||||
ConfsLogC::instance().info("confs", "重载配置 " + names[0]);
|
LogC::instance().info("confs", "重载配置 " + names[0]);
|
||||||
json::array arr;
|
json::array arr;
|
||||||
for (const auto& n : names) arr.push_back(json::value(n));
|
for (const auto& n : names) arr.push_back(json::value(n));
|
||||||
return json::object{{"reloaded", arr}};
|
return json::object{{"reloaded", arr}};
|
||||||
|
|||||||
+2
-158
@@ -1,160 +1,4 @@
|
|||||||
"""
|
"""日志客户端 — 委托到 pymsg.logc 统一库。"""
|
||||||
统一日志客户端 — 各服务共用(基于 pymsg.AsyncRpcQueue)。
|
from pymsg.logc import LogClient, set_log_level # noqa: F401
|
||||||
|
|
||||||
设计要点:
|
|
||||||
- 通过 pymsg RPC 调用 dbv2 execute 路由,逐条 INSERT 到 Logs 表
|
|
||||||
- 异步非阻塞:内存队列 + 后台线程攒批推送,不拖慢业务
|
|
||||||
- fail-safe:db 服务不可用时静默丢弃,绝不影响业务
|
|
||||||
- 缓冲批量:队列满 50 条或超时 2 秒时自动 flush
|
|
||||||
- 时间戳:日志入队时即记录 ts(UTC ISO 格式),反映事件真实发生时间
|
|
||||||
- 底层复用 pymsg.AsyncRpcQueue 通用队列基础设施
|
|
||||||
|
|
||||||
用法(与旧版完全兼容):
|
|
||||||
from log_client import logs
|
|
||||||
|
|
||||||
logs.info("dnfs", "节点新增成功 id=42")
|
|
||||||
logs.error("dnfs", "数据库连接失败", extra={"errno": 2002})
|
|
||||||
logs.push("gateway", "WARNING", "登录失败次数超限",
|
|
||||||
trace_id="req-abc", extra={"ip": "1.2.3.4"})
|
|
||||||
"""
|
|
||||||
|
|
||||||
import json
|
|
||||||
import os as _os
|
|
||||||
import sys as _sys
|
|
||||||
import atexit
|
|
||||||
from datetime import datetime, timezone
|
|
||||||
|
|
||||||
_sys.path.insert(0, _os.path.join(_os.path.dirname(__file__), ".."))
|
|
||||||
from pymsg import RpcClient, RpcError, AsyncRpcQueue # noqa: E402
|
|
||||||
|
|
||||||
# ── 配置 ──────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
_DBV2_ADDR = _os.environ.get('DB_HOST', '127.0.0.1')
|
|
||||||
_DBV2_PORT = int(_os.environ.get('DB_PORT', '8005'))
|
|
||||||
|
|
||||||
_BATCH_SIZE = 50
|
|
||||||
_FLUSH_INTERVAL = 2.0
|
|
||||||
_TIMEOUT = 5
|
|
||||||
_QUEUE_MAXSIZE = 10000
|
|
||||||
|
|
||||||
|
|
||||||
def _safe_json(obj):
|
|
||||||
"""安全序列化 extra 字段,不可序列化对象转为 repr 字符串。"""
|
|
||||||
try:
|
|
||||||
return json.dumps(obj, ensure_ascii=False, default=str)
|
|
||||||
except Exception:
|
|
||||||
return repr(obj)
|
|
||||||
|
|
||||||
|
|
||||||
def _now_iso():
|
|
||||||
"""生成当前 UTC 时间 ISO 字符串(入队时调用,保证时间戳准确)。"""
|
|
||||||
return datetime.now(timezone.utc).isoformat()
|
|
||||||
|
|
||||||
|
|
||||||
class LogClient:
|
|
||||||
"""异步批量日志推送客户端(API 与旧版完全兼容)。"""
|
|
||||||
|
|
||||||
def __init__(self, service_name: str = "log",
|
|
||||||
batch_size: int = _BATCH_SIZE,
|
|
||||||
flush_interval: float = _FLUSH_INTERVAL):
|
|
||||||
self._service_name = service_name
|
|
||||||
self._client = RpcClient(
|
|
||||||
_DBV2_ADDR, _DBV2_PORT,
|
|
||||||
service_name + "-log",
|
|
||||||
timeout=_TIMEOUT,
|
|
||||||
)
|
|
||||||
self._queue = AsyncRpcQueue(
|
|
||||||
handler=self._send_batch,
|
|
||||||
batch_size=batch_size,
|
|
||||||
flush_interval=flush_interval,
|
|
||||||
max_queue_size=_QUEUE_MAXSIZE,
|
|
||||||
on_error=self._on_error,
|
|
||||||
)
|
|
||||||
self._queue.start()
|
|
||||||
atexit.register(self.close)
|
|
||||||
|
|
||||||
# ── 公开接口 ────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
def push(self, service: str, level: str, message: str,
|
|
||||||
trace_id: str = None, extra=None):
|
|
||||||
"""推送一条日志(非阻塞,立即返回)。
|
|
||||||
|
|
||||||
队列满时静默丢弃最旧条目再放入新条目,保证不阻塞业务线程、不导致 OOM。
|
|
||||||
"""
|
|
||||||
entry = {
|
|
||||||
'ts': _now_iso(),
|
|
||||||
'service': service,
|
|
||||||
'level': level,
|
|
||||||
'message': message,
|
|
||||||
'trace_id': trace_id,
|
|
||||||
'extra': extra,
|
|
||||||
}
|
|
||||||
self._queue.put(entry)
|
|
||||||
|
|
||||||
def debug(self, service: str, message: str, **kw):
|
|
||||||
self.push(service, 'DEBUG', message, **kw)
|
|
||||||
|
|
||||||
def info(self, service: str, message: str, **kw):
|
|
||||||
self.push(service, 'INFO', message, **kw)
|
|
||||||
|
|
||||||
def warning(self, service: str, message: str, **kw):
|
|
||||||
self.push(service, 'WARNING', message, **kw)
|
|
||||||
|
|
||||||
def warn(self, service: str, message: str, **kw):
|
|
||||||
self.push(service, 'WARNING', message, **kw)
|
|
||||||
|
|
||||||
def error(self, service: str, message: str, **kw):
|
|
||||||
self.push(service, 'ERROR', message, **kw)
|
|
||||||
|
|
||||||
def critical(self, service: str, message: str, **kw):
|
|
||||||
self.push(service, 'CRITICAL', message, **kw)
|
|
||||||
|
|
||||||
def flush(self):
|
|
||||||
"""手动 flush 队列中剩余的日志。"""
|
|
||||||
self._queue.flush()
|
|
||||||
|
|
||||||
def close(self):
|
|
||||||
"""关闭客户端,flush 剩余日志。"""
|
|
||||||
self._queue.close()
|
|
||||||
|
|
||||||
# ── 内部实现 ────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
def _send_batch(self, batch: list):
|
|
||||||
"""后台线程回调:逐条 INSERT 到 db(与旧版行为一致:网络错误放弃本批剩余)。"""
|
|
||||||
for entry in batch:
|
|
||||||
extra_val = entry.get("extra")
|
|
||||||
try:
|
|
||||||
extra_json = _safe_json(extra_val) if extra_val is not None else None
|
|
||||||
except Exception:
|
|
||||||
extra_json = None
|
|
||||||
payload = {
|
|
||||||
"action": "insert",
|
|
||||||
"table": "Logs",
|
|
||||||
"values": {
|
|
||||||
"ts": entry.get("ts", _now_iso()),
|
|
||||||
"service": entry.get("service"),
|
|
||||||
"level": entry.get("level"),
|
|
||||||
"message": entry.get("message"),
|
|
||||||
"trace_id": entry.get("trace_id"),
|
|
||||||
"extra": extra_json,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
try:
|
|
||||||
self._client.call("execute", payload, timeout=_TIMEOUT)
|
|
||||||
except RpcError as e:
|
|
||||||
print(f"[log_client] INSERT 失败: {e}", file=_sys.stderr)
|
|
||||||
except Exception as e:
|
|
||||||
print(f"[log_client] 发送失败(放弃本批剩余): {e}", file=_sys.stderr)
|
|
||||||
break
|
|
||||||
|
|
||||||
def _on_error(self, e: Exception, batch: list):
|
|
||||||
"""异常安全网:handler 外的异常也不会崩溃线程。"""
|
|
||||||
print(f"[log_client] _send_batch 异常(已捕获,线程继续运行): {e}",
|
|
||||||
file=_sys.stderr)
|
|
||||||
|
|
||||||
|
|
||||||
# ── 全局单例 ──────────────────────────────────────────────────────────
|
|
||||||
# 各服务的 log_client.py 副本需将 service_name 改为自己的服务名。
|
|
||||||
# 此文件(confs/log_client.py)的 service_name 为 "confs"。
|
|
||||||
|
|
||||||
logs = LogClient(service_name="dnfs")
|
logs = LogClient(service_name="dnfs")
|
||||||
+2
-2
@@ -3,7 +3,7 @@ APP_NAME := gateway
|
|||||||
INSTL_DIR := /usr/local/bin
|
INSTL_DIR := /usr/local/bin
|
||||||
CPP := g++
|
CPP := g++
|
||||||
INCS := -I inc/ -I ../libmsg/inc
|
INCS := -I inc/ -I ../libmsg/inc
|
||||||
MY_LIBS := -L/usr/lib -L../libmsg/build -lus_http -lus_httpclient -lus_crypt -lus_msg
|
MY_LIBS := -L/usr/lib -L../libmsg/build -lus_http -lus_httpclient -lus_crypt -lus_msg -lus_logc
|
||||||
LIBS := -pthread -lcurl -lssl -lz -lcrypto -lboost_json -lboost_program_options -Wl,-rpath,$(shell cd ../libmsg/build && pwd)
|
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
|
CPPFLAGS := --std=c++20 -pipe -pthread # -Wall -Werror
|
||||||
BUILD_DIR = build
|
BUILD_DIR = build
|
||||||
@@ -13,7 +13,7 @@ $(shell mkdir -p $(BUILD_DIR))
|
|||||||
all: $(BUILD_DIR)/$(APP_NAME).out
|
all: $(BUILD_DIR)/$(APP_NAME).out
|
||||||
$(BUILD_DIR)/$(APP_NAME).out: $(BUILD_DIR)/main.o
|
$(BUILD_DIR)/$(APP_NAME).out: $(BUILD_DIR)/main.o
|
||||||
$(CPP) $^ $(LIBS) $(MY_LIBS) -o $@
|
$(CPP) $^ $(LIBS) $(MY_LIBS) -o $@
|
||||||
$(BUILD_DIR)/main.o: src/main.cpp inc/LogC.hpp inc/Conf.hpp inc/ExpBase.hpp
|
$(BUILD_DIR)/main.o: src/main.cpp inc/Conf.hpp inc/ExpBase.hpp
|
||||||
$(CPP) $(CPPFLAGS) -c $< $(INCS) -o $@
|
$(CPP) $(CPPFLAGS) -c $< $(INCS) -o $@
|
||||||
clear:
|
clear:
|
||||||
@rm -rvf build/
|
@rm -rvf build/
|
||||||
@@ -1,235 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
/** LogC — gateway 统一日志客户端(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("gateway", "启动完成");
|
|
||||||
* qing::LogC::instance().error("gateway", "数据库连接失败",
|
|
||||||
* std::nullopt, R"({"errno":2002})");
|
|
||||||
*
|
|
||||||
* // 进程退出前:
|
|
||||||
* qing::LogC::instance().shutdown(); // 停止后台线程并 flush 剩余日志(幂等)
|
|
||||||
*
|
|
||||||
* // 设置日志级别(main 启动时调用,低于此级别的日志直接丢弃)
|
|
||||||
* qing::LogC::instance().set_level("warn"); // 只输出 warn/error
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include <atomic>
|
|
||||||
#include <chrono>
|
|
||||||
#include <cstdio>
|
|
||||||
#include <ctime>
|
|
||||||
#include <mutex>
|
|
||||||
#include <optional>
|
|
||||||
#include <string>
|
|
||||||
#include <unordered_map>
|
|
||||||
#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;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- 公开接口 --------------------------------------------------------
|
|
||||||
|
|
||||||
// 日志级别 → 数值(大写兼容)
|
|
||||||
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)});
|
|
||||||
}
|
|
||||||
|
|
||||||
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, "gateway-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 与后台线程并发)
|
|
||||||
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
|
|
||||||
static constexpr size_t _MAX_QUEUE_SIZE = 10000; // 队列上限,满时丢最旧
|
|
||||||
static constexpr double _TIMEOUT = 5.0; // socket 超时(秒)
|
|
||||||
|
|
||||||
// ---- 核心队列 ---------------------------------------------------------
|
|
||||||
pmc::net::AsyncQueue<Entry> _queue;
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace qing
|
|
||||||
@@ -14,7 +14,8 @@
|
|||||||
#include <boost/program_options.hpp>
|
#include <boost/program_options.hpp>
|
||||||
#include "ExpBase.hpp"
|
#include "ExpBase.hpp"
|
||||||
#include "Conf.hpp"
|
#include "Conf.hpp"
|
||||||
#include "LogC.hpp"
|
#include <us/LogC.hpp>
|
||||||
|
#include <us/Msg.hpp>
|
||||||
|
|
||||||
using namespace pmc::net;
|
using namespace pmc::net;
|
||||||
using namespace qing;
|
using namespace qing;
|
||||||
@@ -319,6 +320,7 @@ int main(int argc, char **argv) {
|
|||||||
|
|
||||||
/* 初始化日志推送端点 */
|
/* 初始化日志推送端点 */
|
||||||
logs().set_endpoint(_DB_HOST, _DB_PORT);
|
logs().set_endpoint(_DB_HOST, _DB_PORT);
|
||||||
|
logs().set_service_name("gateway-log");
|
||||||
logs().set_level(_LOG_LEVEL);
|
logs().set_level(_LOG_LEVEL);
|
||||||
|
|
||||||
std::cout << "[gateway] 监听: " << _ADDR << ":" << _PORT
|
std::cout << "[gateway] 监听: " << _ADDR << ":" << _PORT
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
build/
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2026 建昌风物大顺脚本
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
# liblogc — 统一集中式日志客户端库
|
||||||
|
#
|
||||||
|
# 编译产物:libus_logc.so
|
||||||
|
# 依赖:libus_msg(libmsg)、boost_json
|
||||||
|
# 安装:头文件 → /usr/include/us/LogC.hpp
|
||||||
|
# 库文件 → /lib/libus_logc.so
|
||||||
|
|
||||||
|
CC := g++
|
||||||
|
STD := -std=c++20
|
||||||
|
|
||||||
|
VERSION := 0.0.1
|
||||||
|
FRAMEWORK_NAME := us
|
||||||
|
LIB_NAME := libus_logc
|
||||||
|
INSTL_TDIR := /lib
|
||||||
|
INCLU_TDIR := /usr/include
|
||||||
|
|
||||||
|
CFLAGS := $(STD) -O2 -fPIC -Wall
|
||||||
|
LDFLAGS := -shared -lus_msg -lboost_json
|
||||||
|
|
||||||
|
SRC_DIR := src
|
||||||
|
BUILD_DIR := build
|
||||||
|
INC_DIR := inc
|
||||||
|
|
||||||
|
SRC := $(wildcard $(SRC_DIR)/*.cpp)
|
||||||
|
OBJ := $(SRC:$(SRC_DIR)/%.cpp=$(BUILD_DIR)/%.o)
|
||||||
|
|
||||||
|
.PHONY: clear all install uninstall
|
||||||
|
|
||||||
|
all: $(BUILD_DIR) $(BUILD_DIR)/$(LIB_NAME).so
|
||||||
|
|
||||||
|
install: $(BUILD_DIR)/$(LIB_NAME).so
|
||||||
|
mkdir -p $(INCLU_TDIR)/$(FRAMEWORK_NAME)
|
||||||
|
cp -r $(INC_DIR)/$(FRAMEWORK_NAME)/* $(INCLU_TDIR)/$(FRAMEWORK_NAME)
|
||||||
|
cp $(BUILD_DIR)/$(LIB_NAME).so $(INSTL_TDIR)/$(LIB_NAME).so.$(VERSION)
|
||||||
|
ln -sf $(LIB_NAME).so.$(VERSION) $(INSTL_TDIR)/$(LIB_NAME).so
|
||||||
|
ldconfig
|
||||||
|
|
||||||
|
uninstall:
|
||||||
|
rm -f $(INCLU_TDIR)/$(FRAMEWORK_NAME)/LogC.hpp
|
||||||
|
rm -f $(INSTL_TDIR)/$(LIB_NAME).so
|
||||||
|
rm -f $(INSTL_TDIR)/$(LIB_NAME).so.$(VERSION)
|
||||||
|
|
||||||
|
clear:
|
||||||
|
rm -rf $(BUILD_DIR)
|
||||||
|
|
||||||
|
$(BUILD_DIR):
|
||||||
|
mkdir -p $(BUILD_DIR)
|
||||||
|
|
||||||
|
$(BUILD_DIR)/$(LIB_NAME).so: $(OBJ)
|
||||||
|
$(CC) $(OBJ) -o $@ $(LDFLAGS)
|
||||||
|
|
||||||
|
$(BUILD_DIR)/%.o: $(SRC_DIR)/%.cpp
|
||||||
|
$(CC) $(CFLAGS) -I$(INC_DIR) -c $< -o $@
|
||||||
Executable
+5
@@ -0,0 +1,5 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
cd "$(dirname "$0")" || exit
|
||||||
|
make clear
|
||||||
|
make
|
||||||
|
make install
|
||||||
@@ -0,0 +1,144 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
/** LogC — 统一集中式日志客户端(C++ 版)
|
||||||
|
*
|
||||||
|
* 通过 pymsg TCP 调用 dbv2 execute 路由,逐条 INSERT 到 Logs 表。
|
||||||
|
*
|
||||||
|
* 设计要点:
|
||||||
|
* - 异步非阻塞:基于 pmc::net::AsyncQueue 内存队列 + 后台线程批量推送
|
||||||
|
* - fail-safe:db 服务不可用时静默丢弃(仅 stderr 提示),绝不影响业务
|
||||||
|
* - 缓冲批量:队列满 50 条或超时 2 秒时自动 flush
|
||||||
|
* - 背压保护:队列上限 10000 条,满时丢最旧日志,防止内存无限增长
|
||||||
|
* - 级别过滤:set_level() 设定阈值,低于阈值的日志直接丢弃
|
||||||
|
* - 可配置 endpoint / service_name,各服务复用同一份库
|
||||||
|
*
|
||||||
|
* 用法:
|
||||||
|
* #include <us/LogC.hpp>
|
||||||
|
*
|
||||||
|
* // 启动时配置(默认 127.0.0.1:8005)
|
||||||
|
* qing::LogC::instance().set_endpoint("127.0.0.1", 8005);
|
||||||
|
* qing::LogC::instance().set_service_name("gateway-log");
|
||||||
|
* qing::LogC::instance().set_level("warn");
|
||||||
|
*
|
||||||
|
* qing::LogC::instance().info("gateway", "启动完成");
|
||||||
|
* qing::LogC::instance().error("gateway", "连接失败",
|
||||||
|
* std::nullopt, R"({"errno":111})");
|
||||||
|
*
|
||||||
|
* // 进程退出前调用(幂等)
|
||||||
|
* qing::LogC::instance().shutdown();
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
|
#include <memory>
|
||||||
|
#include <mutex>
|
||||||
|
#include <optional>
|
||||||
|
#include <string>
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
// forward-declare: 避免在头文件中拉入 AsyncQueue 模板
|
||||||
|
namespace pmc { namespace net {
|
||||||
|
template <typename T> class AsyncQueue;
|
||||||
|
}}
|
||||||
|
|
||||||
|
namespace qing {
|
||||||
|
|
||||||
|
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 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- 配置(启动时调用,线程安全)-------------------------------------
|
||||||
|
|
||||||
|
void set_endpoint(const std::string& addr, int port);
|
||||||
|
void set_service_name(const std::string& name);
|
||||||
|
|
||||||
|
// 设置日志级别阈值(debug|info|warn|error,大小写不限)
|
||||||
|
void set_level(const std::string& level);
|
||||||
|
|
||||||
|
// ---- 推送接口(非阻塞,立即返回)-------------------------------------
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- 生命周期 -------------------------------------------------------
|
||||||
|
|
||||||
|
void flush(); // 手动 flush 当前积攒批次,等待完成
|
||||||
|
void shutdown(); // 停止后台线程并 flush 剩余日志(幂等)
|
||||||
|
|
||||||
|
private:
|
||||||
|
LogC();
|
||||||
|
~LogC();
|
||||||
|
|
||||||
|
LogC(const LogC&) = delete;
|
||||||
|
LogC& operator=(const LogC&) = delete;
|
||||||
|
|
||||||
|
void _send_batch(std::vector<Entry>&& batch);
|
||||||
|
static std::string _utc_now_iso();
|
||||||
|
|
||||||
|
// ---- 配置项 ---------------------------------------------------------
|
||||||
|
std::string _db_addr = "127.0.0.1";
|
||||||
|
int _db_port = 8005;
|
||||||
|
std::string _service_name = "log";
|
||||||
|
std::mutex _endpoint_mtx; // 保护 _db_addr/_db_port/_service_name
|
||||||
|
std::atomic<int> _level_threshold{1}; // 默认 INFO(1)
|
||||||
|
|
||||||
|
static constexpr size_t _BATCH_SIZE = 50;
|
||||||
|
static constexpr double _FLUSH_INTERVAL = 2.0;
|
||||||
|
static constexpr size_t _MAX_QUEUE_SIZE = 10000;
|
||||||
|
static constexpr double _TIMEOUT = 5.0;
|
||||||
|
|
||||||
|
// ---- 核心队列(unique_ptr 延迟到 .cpp 中实例化,保持头文件轻量)----
|
||||||
|
std::unique_ptr<pmc::net::AsyncQueue<Entry>> _queue;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace qing
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
/** LogC.cpp — 统一集中式日志客户端实现
|
||||||
|
*
|
||||||
|
* 包含 _send_batch(pymsg → db execute INSERT)和 _utc_now_iso(ISO 8601)。
|
||||||
|
* 编译为 libus_logc.so,运行时依赖 libus_msg.so。
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <us/LogC.hpp>
|
||||||
|
|
||||||
|
#include <chrono>
|
||||||
|
#include <cstdio>
|
||||||
|
#include <ctime>
|
||||||
|
#include <iostream>
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
#include <boost/json.hpp>
|
||||||
|
|
||||||
|
#include <us/AsyncQueue.hpp>
|
||||||
|
#include <us/Msg.hpp>
|
||||||
|
|
||||||
|
namespace json = boost::json;
|
||||||
|
|
||||||
|
namespace qing {
|
||||||
|
|
||||||
|
// ---- 生命周期 ---------------------------------------------------------------
|
||||||
|
|
||||||
|
LogC::LogC()
|
||||||
|
: _queue(std::make_unique<pmc::net::AsyncQueue<Entry>>(
|
||||||
|
[this](std::vector<Entry>&& batch) { this->_send_batch(std::move(batch)); },
|
||||||
|
pmc::net::AsyncQueueConfig{_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::~LogC() { shutdown(); }
|
||||||
|
|
||||||
|
// ---- 配置接口 ---------------------------------------------------------------
|
||||||
|
|
||||||
|
void LogC::set_endpoint(const std::string& addr, int port) {
|
||||||
|
std::lock_guard<std::mutex> lock(_endpoint_mtx);
|
||||||
|
_db_addr = addr;
|
||||||
|
_db_port = port;
|
||||||
|
}
|
||||||
|
|
||||||
|
void LogC::set_service_name(const std::string& name) {
|
||||||
|
std::lock_guard<std::mutex> lock(_endpoint_mtx);
|
||||||
|
_service_name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
void LogC::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 LogC::push(const std::string& service, const std::string& level,
|
||||||
|
const std::string& message,
|
||||||
|
std::optional<std::string> trace_id,
|
||||||
|
std::optional<std::string> extra) {
|
||||||
|
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)});
|
||||||
|
}
|
||||||
|
|
||||||
|
void LogC::flush() { _queue->flush(); }
|
||||||
|
void LogC::shutdown() { _queue->shutdown(); }
|
||||||
|
|
||||||
|
// ---- 内部实现 ---------------------------------------------------------------
|
||||||
|
|
||||||
|
void LogC::_send_batch(std::vector<Entry>&& batch) {
|
||||||
|
std::string addr;
|
||||||
|
int port;
|
||||||
|
std::string client_name;
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(_endpoint_mtx);
|
||||||
|
addr = _db_addr;
|
||||||
|
port = _db_port;
|
||||||
|
client_name = _service_name;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::string now = _utc_now_iso();
|
||||||
|
try {
|
||||||
|
pmc::net::MsgClient client(addr, port, client_name);
|
||||||
|
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);
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string LogC::_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;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace qing
|
||||||
@@ -621,5 +621,7 @@ class MsgServer:
|
|||||||
# ── JSON RPC 约定层(依赖上面的 MsgClient / MsgServer,故放在末尾导入)──
|
# ── JSON RPC 约定层(依赖上面的 MsgClient / MsgServer,故放在末尾导入)──
|
||||||
from .rpc import RpcClient, RpcError, RpcServer # noqa: E402
|
from .rpc import RpcClient, RpcError, RpcServer # noqa: E402
|
||||||
from .async_queue import AsyncRpcQueue # noqa: E402
|
from .async_queue import AsyncRpcQueue # noqa: E402
|
||||||
|
from .logc import LogClient, set_log_level # noqa: E402
|
||||||
|
|
||||||
__all__ += ["RpcClient", "RpcServer", "RpcError", "AsyncRpcQueue"]
|
__all__ += ["RpcClient", "RpcServer", "RpcError", "AsyncRpcQueue",
|
||||||
|
"LogClient", "set_log_level"]
|
||||||
@@ -0,0 +1,189 @@
|
|||||||
|
"""
|
||||||
|
pymsg.logc — 统一集中式日志客户端(Python 版)
|
||||||
|
|
||||||
|
通过 pymsg RPC 调用 db execute 路由,逐条 INSERT 到 Logs 表。
|
||||||
|
|
||||||
|
设计要点:
|
||||||
|
- 异步非阻塞:基于 pymsg.AsyncRpcQueue 内存队列 + 后台线程攒批推送
|
||||||
|
- fail-safe:db 服务不可用时静默丢弃,绝不影响业务
|
||||||
|
- 缓冲批量:队列满 50 条或超时 2 秒时自动 flush
|
||||||
|
- 背压保护:队列上限 10000 条,满时丢最旧日志,防止 OOM
|
||||||
|
- 日志级别过滤:set_level() 设定阈值,低于阈值的日志直接丢弃
|
||||||
|
- 底层复用 pymsg.AsyncRpcQueue 通用队列基础设施
|
||||||
|
|
||||||
|
用法:
|
||||||
|
from pymsg.logc import LogClient, set_log_level
|
||||||
|
|
||||||
|
logs = LogClient(service_name="redirect80")
|
||||||
|
logs.info("redirect80", "服务已启动")
|
||||||
|
logs.error("redirect80", "端口绑定失败", extra={"errno": 98})
|
||||||
|
|
||||||
|
# 设置日志级别(main 启动时调用)
|
||||||
|
set_log_level("warn") # 只输出 warn/error/critical
|
||||||
|
"""
|
||||||
|
|
||||||
|
import atexit
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
|
from pymsg import AsyncRpcQueue, RpcClient, RpcError
|
||||||
|
|
||||||
|
# ── 配置 ──────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
_DBV2_ADDR = os.environ.get("DB_HOST", "127.0.0.1")
|
||||||
|
_DBV2_PORT = int(os.environ.get("DB_PORT", "8005"))
|
||||||
|
|
||||||
|
_BATCH_SIZE = 50
|
||||||
|
_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 字符串。"""
|
||||||
|
try:
|
||||||
|
return json.dumps(obj, ensure_ascii=False, default=str)
|
||||||
|
except Exception:
|
||||||
|
return repr(obj)
|
||||||
|
|
||||||
|
|
||||||
|
def _now_iso():
|
||||||
|
"""生成当前 UTC 时间 ISO 字符串(入队时调用,保证时间戳准确)。"""
|
||||||
|
return datetime.now(timezone.utc).isoformat()
|
||||||
|
|
||||||
|
|
||||||
|
# ── LogClient 类 ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
class LogClient:
|
||||||
|
"""异步批量日志推送客户端。
|
||||||
|
|
||||||
|
API 与 redirect80/log_client.py、confs/log_client.py 等保持一致,
|
||||||
|
各服务可直接替换为 ``from pymsg.logc import LogClient``。
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, service_name: str = "log",
|
||||||
|
batch_size: int = _BATCH_SIZE,
|
||||||
|
flush_interval: float = _FLUSH_INTERVAL):
|
||||||
|
self._service_name = service_name
|
||||||
|
self._client = RpcClient(
|
||||||
|
_DBV2_ADDR, _DBV2_PORT,
|
||||||
|
service_name + "-log",
|
||||||
|
timeout=_TIMEOUT,
|
||||||
|
)
|
||||||
|
self._queue = AsyncRpcQueue(
|
||||||
|
handler=self._send_batch,
|
||||||
|
batch_size=batch_size,
|
||||||
|
flush_interval=flush_interval,
|
||||||
|
max_queue_size=_QUEUE_MAXSIZE,
|
||||||
|
on_error=self._on_error,
|
||||||
|
)
|
||||||
|
self._queue.start()
|
||||||
|
atexit.register(self.close)
|
||||||
|
|
||||||
|
# ── 公开接口 ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def set_log_level(self, level: str):
|
||||||
|
"""设置日志过滤阈值(委托给模块级 set_log_level)。"""
|
||||||
|
set_log_level(level)
|
||||||
|
|
||||||
|
def push(self, service: str, level: str, message: str,
|
||||||
|
trace_id: str = None, extra=None):
|
||||||
|
"""推送一条日志(非阻塞,立即返回)。
|
||||||
|
|
||||||
|
低于设定级别的日志直接丢弃。
|
||||||
|
队列满时静默丢弃最旧条目再放入新条目,保证不阻塞业务线程。
|
||||||
|
"""
|
||||||
|
if _LEVEL_ORDER.get(level.upper(), 1) < _level_threshold:
|
||||||
|
return
|
||||||
|
entry = {
|
||||||
|
"ts": _now_iso(),
|
||||||
|
"service": service,
|
||||||
|
"level": level,
|
||||||
|
"message": message,
|
||||||
|
"trace_id": trace_id,
|
||||||
|
"extra": extra,
|
||||||
|
}
|
||||||
|
self._queue.put(entry)
|
||||||
|
|
||||||
|
def debug(self, service: str, message: str, **kw):
|
||||||
|
self.push(service, "DEBUG", message, **kw)
|
||||||
|
|
||||||
|
def info(self, service: str, message: str, **kw):
|
||||||
|
self.push(service, "INFO", message, **kw)
|
||||||
|
|
||||||
|
def warning(self, service: str, message: str, **kw):
|
||||||
|
self.push(service, "WARNING", message, **kw)
|
||||||
|
|
||||||
|
def warn(self, service: str, message: str, **kw):
|
||||||
|
self.push(service, "WARNING", message, **kw)
|
||||||
|
|
||||||
|
def error(self, service: str, message: str, **kw):
|
||||||
|
self.push(service, "ERROR", message, **kw)
|
||||||
|
|
||||||
|
def critical(self, service: str, message: str, **kw):
|
||||||
|
self.push(service, "CRITICAL", message, **kw)
|
||||||
|
|
||||||
|
def flush(self):
|
||||||
|
"""手动 flush 队列中剩余的日志。"""
|
||||||
|
self._queue.flush()
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
"""关闭客户端,flush 剩余日志。"""
|
||||||
|
self._queue.close()
|
||||||
|
|
||||||
|
# ── 内部实现 ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def _send_batch(self, batch: list):
|
||||||
|
"""后台线程回调:逐条 INSERT 到 db(网络错误放弃本批剩余)。"""
|
||||||
|
for entry in batch:
|
||||||
|
extra_val = entry.get("extra")
|
||||||
|
try:
|
||||||
|
extra_json = _safe_json(extra_val) if extra_val is not None else None
|
||||||
|
except Exception:
|
||||||
|
extra_json = None
|
||||||
|
payload = {
|
||||||
|
"action": "insert",
|
||||||
|
"table": "Logs",
|
||||||
|
"values": {
|
||||||
|
"ts": entry.get("ts", _now_iso()),
|
||||||
|
"service": entry.get("service"),
|
||||||
|
"level": entry.get("level"),
|
||||||
|
"message": entry.get("message"),
|
||||||
|
"trace_id": entry.get("trace_id"),
|
||||||
|
"extra": extra_json,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
try:
|
||||||
|
self._client.call("execute", payload, timeout=_TIMEOUT)
|
||||||
|
except RpcError as e:
|
||||||
|
print(f"[logc] INSERT 失败: {e}", file=sys.stderr)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[logc] 发送失败(放弃本批剩余): {e}", file=sys.stderr)
|
||||||
|
break
|
||||||
|
|
||||||
|
def _on_error(self, e: Exception, batch: list):
|
||||||
|
"""异常安全网:handler 外的异常也不会崩溃线程。"""
|
||||||
|
print(f"[logc] _send_batch 异常(已捕获,线程继续运行): {e}",
|
||||||
|
file=sys.stderr)
|
||||||
+2
-177
@@ -1,179 +1,4 @@
|
|||||||
"""
|
"""日志客户端 — 委托到 pymsg.logc 统一库。"""
|
||||||
统一日志客户端 — redirect80 服务副本(基于 pymsg.AsyncRpcQueue)。
|
from pymsg.logc import LogClient, set_log_level # noqa: F401
|
||||||
|
|
||||||
设计要点:
|
|
||||||
- 通过 pymsg RPC 调用 db execute 路由,逐条 INSERT 到 Logs 表
|
|
||||||
- 异步非阻塞:内存队列 + 后台线程攒批推送,不拖慢业务
|
|
||||||
- fail-safe:db 服务不可用时静默丢弃,绝不影响业务
|
|
||||||
- 缓冲批量:队列满 50 条或超时 2 秒时自动 flush
|
|
||||||
- 时间戳:日志入队时即记录 ts(UTC ISO 格式),反映事件真实发生时间
|
|
||||||
- 底层复用 pymsg.AsyncRpcQueue 通用队列基础设施
|
|
||||||
- 日志级别过滤:set_level() 设定阈值,低于阈值的日志直接丢弃
|
|
||||||
|
|
||||||
用法(与旧版完全兼容):
|
|
||||||
from log_client import logs
|
|
||||||
|
|
||||||
logs.info("redirect80", "服务已启动")
|
|
||||||
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
|
|
||||||
import os as _os
|
|
||||||
import sys as _sys
|
|
||||||
import atexit
|
|
||||||
from datetime import datetime, timezone
|
|
||||||
|
|
||||||
_sys.path.insert(0, _os.path.join(_os.path.dirname(__file__), ".."))
|
|
||||||
from pymsg import RpcClient, RpcError, AsyncRpcQueue # noqa: E402
|
|
||||||
|
|
||||||
# ── 配置 ──────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
_DBV2_ADDR = _os.environ.get('DB_HOST', '127.0.0.1')
|
|
||||||
_DBV2_PORT = int(_os.environ.get('DB_PORT', '8005'))
|
|
||||||
|
|
||||||
_BATCH_SIZE = 50
|
|
||||||
_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 字符串。"""
|
|
||||||
try:
|
|
||||||
return json.dumps(obj, ensure_ascii=False, default=str)
|
|
||||||
except Exception:
|
|
||||||
return repr(obj)
|
|
||||||
|
|
||||||
|
|
||||||
def _now_iso():
|
|
||||||
"""生成当前 UTC 时间 ISO 字符串(入队时调用,保证时间戳准确)。"""
|
|
||||||
return datetime.now(timezone.utc).isoformat()
|
|
||||||
|
|
||||||
|
|
||||||
class LogClient:
|
|
||||||
"""异步批量日志推送客户端(API 与旧版完全兼容)。"""
|
|
||||||
|
|
||||||
def __init__(self, service_name: str = "log",
|
|
||||||
batch_size: int = _BATCH_SIZE,
|
|
||||||
flush_interval: float = _FLUSH_INTERVAL):
|
|
||||||
self._service_name = service_name
|
|
||||||
self._client = RpcClient(
|
|
||||||
_DBV2_ADDR, _DBV2_PORT,
|
|
||||||
service_name + "-log",
|
|
||||||
timeout=_TIMEOUT,
|
|
||||||
)
|
|
||||||
self._queue = AsyncRpcQueue(
|
|
||||||
handler=self._send_batch,
|
|
||||||
batch_size=batch_size,
|
|
||||||
flush_interval=flush_interval,
|
|
||||||
max_queue_size=_QUEUE_MAXSIZE,
|
|
||||||
on_error=self._on_error,
|
|
||||||
)
|
|
||||||
self._queue.start()
|
|
||||||
atexit.register(self.close)
|
|
||||||
|
|
||||||
# ── 公开接口 ────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
def push(self, service: str, level: str, message: str,
|
|
||||||
trace_id: str = None, extra=None):
|
|
||||||
"""推送一条日志(非阻塞,立即返回)。
|
|
||||||
|
|
||||||
低于设定级别的日志直接丢弃。
|
|
||||||
队列满时静默丢弃最旧条目再放入新条目,保证不阻塞业务线程、不导致 OOM。
|
|
||||||
"""
|
|
||||||
if _LEVEL_ORDER.get(level.upper(), 1) < _level_threshold:
|
|
||||||
return
|
|
||||||
entry = {
|
|
||||||
'ts': _now_iso(),
|
|
||||||
'service': service,
|
|
||||||
'level': level,
|
|
||||||
'message': message,
|
|
||||||
'trace_id': trace_id,
|
|
||||||
'extra': extra,
|
|
||||||
}
|
|
||||||
self._queue.put(entry)
|
|
||||||
|
|
||||||
def debug(self, service: str, message: str, **kw):
|
|
||||||
self.push(service, 'DEBUG', message, **kw)
|
|
||||||
|
|
||||||
def info(self, service: str, message: str, **kw):
|
|
||||||
self.push(service, 'INFO', message, **kw)
|
|
||||||
|
|
||||||
def warning(self, service: str, message: str, **kw):
|
|
||||||
self.push(service, 'WARNING', message, **kw)
|
|
||||||
|
|
||||||
def warn(self, service: str, message: str, **kw):
|
|
||||||
self.push(service, 'WARNING', message, **kw)
|
|
||||||
|
|
||||||
def error(self, service: str, message: str, **kw):
|
|
||||||
self.push(service, 'ERROR', message, **kw)
|
|
||||||
|
|
||||||
def critical(self, service: str, message: str, **kw):
|
|
||||||
self.push(service, 'CRITICAL', message, **kw)
|
|
||||||
|
|
||||||
def flush(self):
|
|
||||||
"""手动 flush 队列中剩余的日志。"""
|
|
||||||
self._queue.flush()
|
|
||||||
|
|
||||||
def close(self):
|
|
||||||
"""关闭客户端,flush 剩余日志。"""
|
|
||||||
self._queue.close()
|
|
||||||
|
|
||||||
# ── 内部实现 ────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
def _send_batch(self, batch: list):
|
|
||||||
"""后台线程回调:逐条 INSERT 到 db(网络错误放弃本批剩余)。"""
|
|
||||||
for entry in batch:
|
|
||||||
extra_val = entry.get("extra")
|
|
||||||
try:
|
|
||||||
extra_json = _safe_json(extra_val) if extra_val is not None else None
|
|
||||||
except Exception:
|
|
||||||
extra_json = None
|
|
||||||
payload = {
|
|
||||||
"action": "insert",
|
|
||||||
"table": "Logs",
|
|
||||||
"values": {
|
|
||||||
"ts": entry.get("ts", _now_iso()),
|
|
||||||
"service": entry.get("service"),
|
|
||||||
"level": entry.get("level"),
|
|
||||||
"message": entry.get("message"),
|
|
||||||
"trace_id": entry.get("trace_id"),
|
|
||||||
"extra": extra_json,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
try:
|
|
||||||
self._client.call("execute", payload, timeout=_TIMEOUT)
|
|
||||||
except RpcError as e:
|
|
||||||
print(f"[log_client] INSERT 失败: {e}", file=_sys.stderr)
|
|
||||||
except Exception as e:
|
|
||||||
print(f"[log_client] 发送失败(放弃本批剩余): {e}", file=_sys.stderr)
|
|
||||||
break
|
|
||||||
|
|
||||||
def _on_error(self, e: Exception, batch: list):
|
|
||||||
"""异常安全网:handler 外的异常也不会崩溃线程。"""
|
|
||||||
print(f"[log_client] _send_batch 异常(已捕获,线程继续运行): {e}",
|
|
||||||
file=_sys.stderr)
|
|
||||||
|
|
||||||
|
|
||||||
# ── 全局单例 ──────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
logs = LogClient(service_name="redirect80")
|
logs = LogClient(service_name="redirect80")
|
||||||
+3
-276
@@ -1,277 +1,4 @@
|
|||||||
"""统一日志客户端 — 各服务共用(dnfs / smtp / static / confs 四份副本内容一致,改动请同步)。
|
"""日志客户端 — 委托到 pymsg.logc 统一库。"""
|
||||||
|
from pymsg.logc import LogClient, set_log_level # noqa: F401
|
||||||
|
|
||||||
设计要点:
|
logs = LogClient(service_name="smtp")
|
||||||
- 通过 pymsg RPC 调用 dbv2 execute 路由,逐条 INSERT 到 Logs 表
|
|
||||||
- 异步非阻塞:内存队列 + 后台线程批量推送,不拖慢业务
|
|
||||||
- fail-safe:db 服务不可用时静默丢弃,绝不影响业务;后台线程任何异常都不会崩溃
|
|
||||||
- 缓冲批量:队列满 50 条或超时 2 秒时自动 flush
|
|
||||||
- 时间戳:日志入队时即记录 ts,反映事件真实发生时间
|
|
||||||
|
|
||||||
用法:
|
|
||||||
from log_client import logs
|
|
||||||
|
|
||||||
logs.info("dnfs", "节点新增成功 id=42")
|
|
||||||
logs.error("dnfs", "数据库连接失败", extra={"errno": 2002})
|
|
||||||
logs.push("gateway", "WARNING", "登录失败次数超限",
|
|
||||||
trace_id="req-abc", extra={"ip": "1.2.3.4"})
|
|
||||||
"""
|
|
||||||
|
|
||||||
import json
|
|
||||||
import os as _os
|
|
||||||
import sys as _sys
|
|
||||||
import threading
|
|
||||||
import queue
|
|
||||||
import time
|
|
||||||
import atexit
|
|
||||||
from datetime import datetime, timezone
|
|
||||||
|
|
||||||
_sys.path.insert(0, _os.path.join(_os.path.dirname(__file__), ".."))
|
|
||||||
from pymsg import RpcClient, RpcError # noqa: E402
|
|
||||||
|
|
||||||
# ── 配置 ──────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
_DBV2_ADDR = _os.environ.get('DB_HOST', '127.0.0.1')
|
|
||||||
_DBV2_PORT = int(_os.environ.get('DB_PORT', '8005'))
|
|
||||||
|
|
||||||
_BATCH_SIZE = 50 # 队列满 50 条时 flush
|
|
||||||
_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 字符串,保证不抛异常。"""
|
|
||||||
try:
|
|
||||||
return json.dumps(obj, ensure_ascii=False, default=str)
|
|
||||||
except Exception:
|
|
||||||
return repr(obj)
|
|
||||||
|
|
||||||
|
|
||||||
def _now_iso():
|
|
||||||
"""生成当前 UTC 时间 ISO 字符串(在入队时调用,保证时间戳准确)。"""
|
|
||||||
return datetime.now(timezone.utc).isoformat()
|
|
||||||
|
|
||||||
|
|
||||||
class LogClient:
|
|
||||||
"""异步批量日志推送客户端。"""
|
|
||||||
|
|
||||||
def __init__(self, service_name="log",
|
|
||||||
batch_size=_BATCH_SIZE,
|
|
||||||
flush_interval=_FLUSH_INTERVAL):
|
|
||||||
self._service_name = service_name
|
|
||||||
self._batch_size = batch_size
|
|
||||||
self._flush_interval = flush_interval
|
|
||||||
self._queue = queue.Queue(maxsize=_QUEUE_MAXSIZE)
|
|
||||||
self._stop = threading.Event()
|
|
||||||
self._flush_signal = threading.Event()
|
|
||||||
self._flush_done = threading.Event()
|
|
||||||
self._thread = threading.Thread(target=self._flush_loop,
|
|
||||||
daemon=True, name='log-client')
|
|
||||||
self._thread.start()
|
|
||||||
atexit.register(self._shutdown)
|
|
||||||
|
|
||||||
# ── 公开接口 ────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
def push(self, service, level, message, trace_id=None, extra=None):
|
|
||||||
"""推送一条日志(非阻塞,立即返回)。
|
|
||||||
|
|
||||||
队列满时静默丢弃最旧条目再放入新条目,保证不阻塞业务线程、不导致 OOM。
|
|
||||||
"""
|
|
||||||
# 级别过滤:低于当前阈值的日志直接丢弃(不进队列)
|
|
||||||
if _LOG_LEVELS.get(_normalize_level(level), 0) < _LOG_LEVEL_THRESHOLD:
|
|
||||||
return
|
|
||||||
entry = {
|
|
||||||
'ts': _now_iso(), # 入队时即生成时间戳,反映真实事件时间
|
|
||||||
'service': service,
|
|
||||||
'level': level,
|
|
||||||
'message': message,
|
|
||||||
'trace_id': trace_id,
|
|
||||||
'extra': extra,
|
|
||||||
}
|
|
||||||
try:
|
|
||||||
self._queue.put_nowait(entry)
|
|
||||||
except queue.Full:
|
|
||||||
# 队列满:丢弃最旧的一条,再放入新条目(drop-oldest 策略)
|
|
||||||
try:
|
|
||||||
self._queue.get_nowait()
|
|
||||||
except queue.Empty:
|
|
||||||
pass
|
|
||||||
try:
|
|
||||||
self._queue.put_nowait(entry)
|
|
||||||
except queue.Full:
|
|
||||||
pass # 极端竞争下仍失败则静默丢弃
|
|
||||||
|
|
||||||
def debug(self, service, message, **kw):
|
|
||||||
self.push(service, 'DEBUG', message, **kw)
|
|
||||||
|
|
||||||
def info(self, service, message, **kw):
|
|
||||||
self.push(service, 'INFO', message, **kw)
|
|
||||||
|
|
||||||
def warning(self, service, message, **kw):
|
|
||||||
self.push(service, 'WARNING', message, **kw)
|
|
||||||
|
|
||||||
def error(self, service, message, **kw):
|
|
||||||
self.push(service, 'ERROR', message, **kw)
|
|
||||||
|
|
||||||
def critical(self, service, message, **kw):
|
|
||||||
self.push(service, 'CRITICAL', message, **kw)
|
|
||||||
|
|
||||||
def set_log_level(self, level):
|
|
||||||
"""设置日志过滤阈值(低于该级别的日志将被丢弃)。
|
|
||||||
|
|
||||||
委托给模块级 set_log_level,统一通过 logs 单例对外暴露接口。
|
|
||||||
"""
|
|
||||||
set_log_level(level)
|
|
||||||
|
|
||||||
def flush(self):
|
|
||||||
"""手动 flush 队列中剩余的日志(含后台线程持有的局部 batch)。"""
|
|
||||||
self._flush_done.clear()
|
|
||||||
self._flush_signal.set() # 通知后台线程立即 flush 其局部 batch
|
|
||||||
# 等待后台线程完成 flush(最多等待 flush_interval + 2 秒余量)
|
|
||||||
self._flush_done.wait(timeout=self._flush_interval + 2.0)
|
|
||||||
# 主线程排空队列中剩余条目
|
|
||||||
self._drain_queue_to_db()
|
|
||||||
|
|
||||||
def _drain_queue_to_db(self):
|
|
||||||
"""排空主线程可见的队列中剩余条目,直接发送到 db。"""
|
|
||||||
batch = []
|
|
||||||
while True:
|
|
||||||
try:
|
|
||||||
batch.append(self._queue.get_nowait())
|
|
||||||
if len(batch) >= self._batch_size:
|
|
||||||
self._send(batch)
|
|
||||||
batch = []
|
|
||||||
except queue.Empty:
|
|
||||||
break
|
|
||||||
if batch:
|
|
||||||
self._send(batch)
|
|
||||||
|
|
||||||
# ── 内部实现 ────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
def _flush_loop(self):
|
|
||||||
"""后台线程:积攒到 batch_size 或超时 flush_interval 时发送。
|
|
||||||
|
|
||||||
外层捕获所有异常,保证后台线程在任何情况下(网络故障、序列化错误等)
|
|
||||||
都不会死亡,避免日志队列无限增长导致 OOM。
|
|
||||||
"""
|
|
||||||
batch = []
|
|
||||||
last_flush = time.monotonic()
|
|
||||||
|
|
||||||
while not self._stop.is_set():
|
|
||||||
try:
|
|
||||||
timeout = self._flush_interval - (time.monotonic() - last_flush)
|
|
||||||
chk_timeout = max(0.1, min(timeout, 0.5))
|
|
||||||
try:
|
|
||||||
item = self._queue.get(timeout=chk_timeout)
|
|
||||||
batch.append(item)
|
|
||||||
while len(batch) < self._batch_size:
|
|
||||||
try:
|
|
||||||
batch.append(self._queue.get_nowait())
|
|
||||||
except queue.Empty:
|
|
||||||
break
|
|
||||||
except queue.Empty:
|
|
||||||
pass
|
|
||||||
|
|
||||||
should_flush = (
|
|
||||||
len(batch) >= self._batch_size or
|
|
||||||
self._flush_signal.is_set() or
|
|
||||||
(batch and time.monotonic() - last_flush >= self._flush_interval)
|
|
||||||
)
|
|
||||||
if should_flush and batch:
|
|
||||||
try:
|
|
||||||
self._send(batch)
|
|
||||||
except Exception as e:
|
|
||||||
# _send 内部已有细粒度异常处理,此处是最终安全网
|
|
||||||
print(f"[log_client] _send 异常(已捕获,线程继续运行): {e}",
|
|
||||||
file=_sys.stderr)
|
|
||||||
batch = []
|
|
||||||
last_flush = time.monotonic()
|
|
||||||
if self._flush_signal.is_set():
|
|
||||||
self._flush_signal.clear()
|
|
||||||
self._flush_done.set()
|
|
||||||
except Exception as e:
|
|
||||||
# 最外层安全网:任何意外异常都不应杀死日志线程
|
|
||||||
print(f"[log_client] _flush_loop 异常(已捕获,线程继续运行): {e}",
|
|
||||||
file=_sys.stderr)
|
|
||||||
time.sleep(0.5) # 防止异常风暴导致 CPU 满载
|
|
||||||
|
|
||||||
# 退出前 flush 剩余
|
|
||||||
if batch:
|
|
||||||
try:
|
|
||||||
self._send(batch)
|
|
||||||
except Exception as e:
|
|
||||||
print(f"[log_client] 退出时 flush 异常: {e}", file=_sys.stderr)
|
|
||||||
self._flush_done.set()
|
|
||||||
|
|
||||||
def _send(self, batch):
|
|
||||||
"""通过 pymsg RPC(execute 路由)批量发送到 dbv2 Logs 表。
|
|
||||||
|
|
||||||
整个 batch 复用一个 TCP 连接;网络错误时放弃本批剩余条目(fail-safe),
|
|
||||||
不抛异常给调用方。
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
client = RpcClient(_DBV2_ADDR, _DBV2_PORT, self._service_name + "-log")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"[log_client] 创建 RpcClient 失败: {e}", file=_sys.stderr)
|
|
||||||
return
|
|
||||||
|
|
||||||
for entry in batch:
|
|
||||||
extra_val = entry.get("extra")
|
|
||||||
try:
|
|
||||||
extra_json = _safe_json(extra_val) if extra_val is not None else None
|
|
||||||
except Exception:
|
|
||||||
extra_json = None
|
|
||||||
payload = {
|
|
||||||
"action": "insert",
|
|
||||||
"table": "Logs",
|
|
||||||
"values": {
|
|
||||||
"ts": entry.get("ts", _now_iso()),
|
|
||||||
"service": entry.get("service"),
|
|
||||||
"level": entry.get("level"),
|
|
||||||
"message": entry.get("message"),
|
|
||||||
"trace_id": entry.get("trace_id"),
|
|
||||||
"extra": extra_json,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
try:
|
|
||||||
client.call("execute", payload, timeout=_TIMEOUT)
|
|
||||||
except RpcError as e:
|
|
||||||
# 业务错误(如表结构不符):打印后继续下一条
|
|
||||||
print(f"[log_client] INSERT 失败: {e}", file=_sys.stderr)
|
|
||||||
except Exception as e:
|
|
||||||
# 网络错误:db 不可用,放弃本批剩余(fail-safe)
|
|
||||||
print(f"[log_client] 发送失败: {e}", file=_sys.stderr)
|
|
||||||
break
|
|
||||||
|
|
||||||
def _shutdown(self):
|
|
||||||
"""进程退出时 flush 剩余日志。"""
|
|
||||||
self._stop.set()
|
|
||||||
self.flush()
|
|
||||||
|
|
||||||
|
|
||||||
# ── 全局单例 ──────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
logs = LogClient()
|
|
||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
CXX := g++
|
CXX := g++
|
||||||
CXXFLAGS := -std=c++20 -Wall -Wextra -O2
|
CXXFLAGS := -std=c++20 -Wall -Wextra -O2
|
||||||
INCS := -I./inc
|
INCS := -I./inc
|
||||||
LIBS := -lus_msg -lboost_json -lboost_program_options -lssl -lcrypto -lpthread
|
LIBS := -lus_msg -lus_logc -lboost_json -lboost_program_options -lssl -lcrypto -lpthread
|
||||||
|
|
||||||
TARGET := smtp3.bin
|
TARGET := smtp3.bin
|
||||||
SRC := src/main.cpp
|
SRC := src/main.cpp
|
||||||
|
|||||||
@@ -1,124 +0,0 @@
|
|||||||
/*
|
|
||||||
* LogC.hpp — smtp3 异步日志客户端(简化版)
|
|
||||||
*
|
|
||||||
* 基于 libmsg AsyncQueue,异步批量推送日志到 db:8005 的 Logs 表。
|
|
||||||
* - 内存队列 + 后台线程攒批(满 50 条或 2 秒 flush)
|
|
||||||
* - fail-safe:db 不可用时静默丢弃
|
|
||||||
* - P3-5: 长连接复用(底层 openConnection + pmsg_send/recv),
|
|
||||||
* 不再每批新建 TCP 连接;连接故障时关闭重建,本批丢弃
|
|
||||||
* - API: info() / warning() / error()
|
|
||||||
*/
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include <us/Msg.hpp>
|
|
||||||
#include <us/AsyncQueue.hpp>
|
|
||||||
#include <boost/json.hpp>
|
|
||||||
#include <iostream>
|
|
||||||
#include <string>
|
|
||||||
#include <vector>
|
|
||||||
#include <chrono>
|
|
||||||
#include <cstdlib>
|
|
||||||
|
|
||||||
#include <unistd.h>
|
|
||||||
|
|
||||||
namespace smtp3 {
|
|
||||||
|
|
||||||
struct LogEntry {
|
|
||||||
std::string ts;
|
|
||||||
std::string service;
|
|
||||||
std::string level;
|
|
||||||
std::string message;
|
|
||||||
std::string trace_id;
|
|
||||||
std::string extra;
|
|
||||||
};
|
|
||||||
|
|
||||||
class LogC {
|
|
||||||
public:
|
|
||||||
static LogC& instance() {
|
|
||||||
static LogC inst;
|
|
||||||
return inst;
|
|
||||||
}
|
|
||||||
|
|
||||||
void info(const std::string& svc, const std::string& msg) {
|
|
||||||
push(svc, "INFO", msg);
|
|
||||||
}
|
|
||||||
void warning(const std::string& svc, const std::string& msg) {
|
|
||||||
push(svc, "WARNING", msg);
|
|
||||||
}
|
|
||||||
void error(const std::string& svc, const std::string& msg) {
|
|
||||||
push(svc, "ERROR", msg);
|
|
||||||
}
|
|
||||||
|
|
||||||
private:
|
|
||||||
LogC() {
|
|
||||||
const char* host = std::getenv("DB_HOST");
|
|
||||||
const char* port = std::getenv("DB_PORT");
|
|
||||||
db_host_ = host ? host : "127.0.0.1";
|
|
||||||
db_port_ = port ? std::atoi(port) : 8005;
|
|
||||||
|
|
||||||
queue_ = std::make_unique<pmc::net::AsyncQueue<LogEntry>>(
|
|
||||||
[this](std::vector<LogEntry>&& batch) { send_batch(std::move(batch)); },
|
|
||||||
pmc::net::AsyncQueueConfig{.batch_size = 50, .flush_interval = 2.0, .max_queue_size = 10000},
|
|
||||||
[](const std::exception& e, std::vector<LogEntry>&&) {
|
|
||||||
std::cerr << "[smtp3/log] send failed: " << e.what() << std::endl;
|
|
||||||
});
|
|
||||||
queue_->start();
|
|
||||||
}
|
|
||||||
|
|
||||||
~LogC() {
|
|
||||||
if (queue_) queue_->shutdown();
|
|
||||||
if (fd_ >= 0) ::close(fd_);
|
|
||||||
}
|
|
||||||
|
|
||||||
void push(const std::string& svc, const std::string& level, const std::string& msg) {
|
|
||||||
auto now = std::chrono::system_clock::now();
|
|
||||||
auto t = std::chrono::system_clock::to_time_t(now);
|
|
||||||
char buf[32];
|
|
||||||
std::strftime(buf, sizeof(buf), "%Y-%m-%dT%H:%M:%SZ", std::gmtime(&t));
|
|
||||||
|
|
||||||
LogEntry e;
|
|
||||||
e.ts = buf;
|
|
||||||
e.service = svc;
|
|
||||||
e.level = level;
|
|
||||||
e.message = msg;
|
|
||||||
queue_->push(std::move(e));
|
|
||||||
}
|
|
||||||
|
|
||||||
// P3-5: 长连接复用——同一 fd 上逐条 pmsg_send + pmsg_recv(db 对每条
|
|
||||||
// execute 都回复,须消费以保持帧同步);任一异常即关闭连接、丢弃本批
|
|
||||||
void send_batch(std::vector<LogEntry>&& batch) {
|
|
||||||
try {
|
|
||||||
if (fd_ < 0)
|
|
||||||
fd_ = pmc::net::detail::openConnection(db_host_, db_port_, 1.0);
|
|
||||||
for (auto& e : batch) {
|
|
||||||
boost::json::object vals;
|
|
||||||
vals["ts"] = e.ts;
|
|
||||||
vals["service"] = e.service;
|
|
||||||
vals["level"] = e.level;
|
|
||||||
vals["message"] = e.message;
|
|
||||||
vals["trace_id"] = e.trace_id.empty() ? nullptr : boost::json::value(e.trace_id);
|
|
||||||
vals["extra"] = e.extra.empty() ? nullptr : boost::json::value(e.extra);
|
|
||||||
|
|
||||||
boost::json::object req;
|
|
||||||
req["_route"] = "execute";
|
|
||||||
req["action"] = "insert";
|
|
||||||
req["table"] = "Logs";
|
|
||||||
req["values"] = vals;
|
|
||||||
|
|
||||||
// 与 MsgClient::call 线路格式一致:无前缀,逐条请求-应答
|
|
||||||
pmc::net::pmsg_send(fd_, boost::json::serialize(req));
|
|
||||||
(void)pmc::net::pmsg_recv(fd_, 1.0);
|
|
||||||
}
|
|
||||||
} catch (const std::exception& e) {
|
|
||||||
if (fd_ >= 0) { ::close(fd_); fd_ = -1; }
|
|
||||||
std::cerr << "[smtp3/log] batch drop: " << e.what() << std::endl;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string db_host_;
|
|
||||||
int db_port_;
|
|
||||||
std::unique_ptr<pmc::net::AsyncQueue<LogEntry>> queue_;
|
|
||||||
int fd_ = -1; // 长连接 fd(仅后台 flush 线程访问,无并发)
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace smtp3
|
|
||||||
+3
-2
@@ -59,12 +59,12 @@
|
|||||||
#include <sys/socket.h>
|
#include <sys/socket.h>
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
|
|
||||||
#include "LogC.hpp"
|
#include <us/LogC.hpp>
|
||||||
|
|
||||||
namespace json = boost::json;
|
namespace json = boost::json;
|
||||||
namespace po = boost::program_options;
|
namespace po = boost::program_options;
|
||||||
|
|
||||||
using namespace smtp3;
|
using qing::LogC;
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
// 辅助: 安全的 stoi
|
// 辅助: 安全的 stoi
|
||||||
@@ -716,6 +716,7 @@ static SendLimiter g_limiter;
|
|||||||
static std::unordered_map<std::string, double> g_cooldown;
|
static std::unordered_map<std::string, double> g_cooldown;
|
||||||
static std::mutex g_cooldown_mtx;
|
static std::mutex g_cooldown_mtx;
|
||||||
static auto& g_log = LogC::instance();
|
static auto& g_log = LogC::instance();
|
||||||
|
static const auto _log_init = (LogC::instance().set_service_name("smtp3-log"), true);
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
// 8. main() — RpcServer 启动
|
// 8. main() — RpcServer 启动
|
||||||
|
|||||||
+2
-160
@@ -1,162 +1,4 @@
|
|||||||
"""
|
"""日志客户端 — 委托到 pymsg.logc 统一库。"""
|
||||||
统一日志客户端 — 各服务共用(基于 pymsg.AsyncRpcQueue)。
|
from pymsg.logc import LogClient, set_log_level # noqa: F401
|
||||||
|
|
||||||
设计要点:
|
|
||||||
- 通过 pymsg RPC 调用 dbv2 execute 路由,逐条 INSERT 到 Logs 表
|
|
||||||
- 异步非阻塞:内存队列 + 后台线程攒批推送,不拖慢业务
|
|
||||||
- fail-safe:db 服务不可用时静默丢弃,绝不影响业务
|
|
||||||
- 缓冲批量:队列满 50 条或超时 2 秒时自动 flush
|
|
||||||
- 时间戳:日志入队时即记录 ts(UTC ISO 格式),反映事件真实发生时间
|
|
||||||
- 底层复用 pymsg.AsyncRpcQueue 通用队列基础设施
|
|
||||||
|
|
||||||
用法(与旧版完全兼容):
|
|
||||||
from log_client import logs
|
|
||||||
|
|
||||||
logs.info("dnfs", "节点新增成功 id=42")
|
|
||||||
logs.error("dnfs", "数据库连接失败", extra={"errno": 2002})
|
|
||||||
logs.push("gateway", "WARNING", "登录失败次数超限",
|
|
||||||
trace_id="req-abc", extra={"ip": "1.2.3.4"})
|
|
||||||
"""
|
|
||||||
|
|
||||||
import json
|
|
||||||
import os as _os
|
|
||||||
import sys as _sys
|
|
||||||
import atexit
|
|
||||||
from datetime import datetime, timezone
|
|
||||||
|
|
||||||
_sys.path.insert(0, _os.path.join(_os.path.dirname(__file__), ".."))
|
|
||||||
from pymsg import RpcClient, RpcError, AsyncRpcQueue # noqa: E402
|
|
||||||
|
|
||||||
# ── 配置 ──────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
_DBV2_ADDR = _os.environ.get('DB_HOST', '127.0.0.1')
|
|
||||||
_DBV2_PORT = int(_os.environ.get('DB_PORT', '8005'))
|
|
||||||
|
|
||||||
_BATCH_SIZE = 50
|
|
||||||
_FLUSH_INTERVAL = 2.0
|
|
||||||
_TIMEOUT = 5
|
|
||||||
_QUEUE_MAXSIZE = 10000
|
|
||||||
|
|
||||||
|
|
||||||
def _safe_json(obj):
|
|
||||||
"""安全序列化 extra 字段,不可序列化对象转为 repr 字符串。"""
|
|
||||||
try:
|
|
||||||
return json.dumps(obj, ensure_ascii=False, default=str)
|
|
||||||
except Exception:
|
|
||||||
return repr(obj)
|
|
||||||
|
|
||||||
|
|
||||||
def _now_iso():
|
|
||||||
"""生成当前 UTC 时间 ISO 字符串(入队时调用,保证时间戳准确)。"""
|
|
||||||
return datetime.now(timezone.utc).isoformat()
|
|
||||||
|
|
||||||
|
|
||||||
class LogClient:
|
|
||||||
"""异步批量日志推送客户端(API 与旧版完全兼容)。"""
|
|
||||||
|
|
||||||
def __init__(self, service_name: str = "log",
|
|
||||||
batch_size: int = _BATCH_SIZE,
|
|
||||||
flush_interval: float = _FLUSH_INTERVAL):
|
|
||||||
self._service_name = service_name
|
|
||||||
self._client = RpcClient(
|
|
||||||
_DBV2_ADDR, _DBV2_PORT,
|
|
||||||
service_name + "-log",
|
|
||||||
timeout=_TIMEOUT,
|
|
||||||
)
|
|
||||||
self._queue = AsyncRpcQueue(
|
|
||||||
handler=self._send_batch,
|
|
||||||
batch_size=batch_size,
|
|
||||||
flush_interval=flush_interval,
|
|
||||||
max_queue_size=_QUEUE_MAXSIZE,
|
|
||||||
on_error=self._on_error,
|
|
||||||
)
|
|
||||||
self._queue.start()
|
|
||||||
atexit.register(self.close)
|
|
||||||
|
|
||||||
# ── 公开接口 ────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
def push(self, service: str, level: str, message: str,
|
|
||||||
trace_id: str = None, extra=None):
|
|
||||||
"""推送一条日志(非阻塞,立即返回)。
|
|
||||||
|
|
||||||
队列满时静默丢弃最旧条目再放入新条目,保证不阻塞业务线程、不导致 OOM。
|
|
||||||
"""
|
|
||||||
entry = {
|
|
||||||
'ts': _now_iso(),
|
|
||||||
'service': service,
|
|
||||||
'level': level,
|
|
||||||
'message': message,
|
|
||||||
'trace_id': trace_id,
|
|
||||||
'extra': extra,
|
|
||||||
}
|
|
||||||
self._queue.put(entry)
|
|
||||||
|
|
||||||
def debug(self, service: str, message: str, **kw):
|
|
||||||
self.push(service, 'DEBUG', message, **kw)
|
|
||||||
|
|
||||||
def info(self, service: str, message: str, **kw):
|
|
||||||
self.push(service, 'INFO', message, **kw)
|
|
||||||
|
|
||||||
def warning(self, service: str, message: str, **kw):
|
|
||||||
self.push(service, 'WARNING', message, **kw)
|
|
||||||
|
|
||||||
def warn(self, service: str, message: str, **kw):
|
|
||||||
self.push(service, 'WARNING', message, **kw)
|
|
||||||
|
|
||||||
def error(self, service: str, message: str, **kw):
|
|
||||||
self.push(service, 'ERROR', message, **kw)
|
|
||||||
|
|
||||||
def critical(self, service: str, message: str, **kw):
|
|
||||||
self.push(service, 'CRITICAL', message, **kw)
|
|
||||||
|
|
||||||
def flush(self):
|
|
||||||
"""手动 flush 队列中剩余的日志。"""
|
|
||||||
self._queue.flush()
|
|
||||||
|
|
||||||
def close(self):
|
|
||||||
"""关闭客户端,flush 剩余日志。"""
|
|
||||||
self._queue.close()
|
|
||||||
|
|
||||||
# ── 内部实现 ────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
def _send_batch(self, batch: list):
|
|
||||||
"""后台线程回调:逐条 INSERT 到 db。
|
|
||||||
|
|
||||||
NEW-13:单条失败仅跳过该条,不再因网络异常放弃整批剩余日志。
|
|
||||||
RpcError 视为可重试的业务错误(如 db 临时拒绝),继续处理下一条。
|
|
||||||
"""
|
|
||||||
for entry in batch:
|
|
||||||
extra_val = entry.get("extra")
|
|
||||||
try:
|
|
||||||
extra_json = _safe_json(extra_val) if extra_val is not None else None
|
|
||||||
except Exception:
|
|
||||||
extra_json = None
|
|
||||||
payload = {
|
|
||||||
"action": "insert",
|
|
||||||
"table": "Logs",
|
|
||||||
"values": {
|
|
||||||
"ts": entry.get("ts", _now_iso()),
|
|
||||||
"service": entry.get("service"),
|
|
||||||
"level": entry.get("level"),
|
|
||||||
"message": entry.get("message"),
|
|
||||||
"trace_id": entry.get("trace_id"),
|
|
||||||
"extra": extra_json,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
try:
|
|
||||||
self._client.call("execute", payload, timeout=_TIMEOUT)
|
|
||||||
except Exception as e:
|
|
||||||
print(f"[log_client] INSERT 失败(跳过本条): {e}", file=_sys.stderr)
|
|
||||||
continue
|
|
||||||
|
|
||||||
def _on_error(self, e: Exception, batch: list):
|
|
||||||
"""异常安全网:handler 外的异常也不会崩溃线程。"""
|
|
||||||
print(f"[log_client] _send_batch 异常(已捕获,线程继续运行): {e}",
|
|
||||||
file=_sys.stderr)
|
|
||||||
|
|
||||||
|
|
||||||
# ── 全局单例 ──────────────────────────────────────────────────────────
|
|
||||||
# 各服务的 log_client.py 副本需将 service_name 改为自己的服务名。
|
|
||||||
# 此文件(confs/log_client.py)的 service_name 为 "confs"。
|
|
||||||
|
|
||||||
logs = LogClient(service_name="static")
|
logs = LogClient(service_name="static")
|
||||||
Reference in new issue
Block a user