优化了xml修改和表格逆生成功能

This commit is contained in:
lnk
2026-07-14 15:14:54 +08:00
parent 70c1534d96
commit 6bfaa32db4
74 changed files with 15935 additions and 1804129 deletions

View File

@@ -23,6 +23,26 @@
#include <unordered_map>
#include <unordered_set>
/*
* mq.cpp
*
* 本文件负责“外部数据输入/输出”:
* - HTTP 请求台账、ICD 列表和 XML 文件下载;
* - 动态加载 RocketMQ DLL初始化生产者/消费者;
* - 发送数据追踪请求;
* - 消费 DATATopic/TraceTopic把 jsondata/origin 落盘到 trace_data
* - RocketMQ 不可用时把待发消息写到 mq_outbox_*.jsonl便于离线排查。
*
* 数据追踪落盘结构:
* runtime\trace_data\process_N\device_x\monitor_y\
* trace_context.json 本次追踪上下文GUI 自动解析依赖它;
* sent_trace_request.json 已发送/待发送追踪请求;
* jsondata_01.txt DATA_TYPE=01 的 DATATopic 数据;
* jsondata_02.txt/03.txt 闪变等其它 DATA_TYPE
* origin_*.txt TraceTopic 原始 MMS/报告数据;
* final_sorted.xls 自动解析生成的工作簿。
*/
using json = nlohmann::json;
namespace fs = std::filesystem;
@@ -39,6 +59,7 @@ fs::path appRootDir() {
fs::path dir = executableDir();
std::wstring name = dir.filename().wstring();
std::transform(name.begin(), name.end(), name.begin(), [](wchar_t ch) { return (wchar_t)towlower(ch); });
// exe 通常在 bin 下运行runtime/source 与 bin 同级,所以从 bin 回到项目根。
if (name == L"bin" && dir.has_parent_path()) return dir.parent_path();
return dir;
}
@@ -97,11 +118,13 @@ bool setContains(const std::unordered_set<std::string>& allowed, const std::stri
std::unordered_set<std::string> parseStatusSet(const std::string& text) {
std::unordered_set<std::string> out;
try {
// 支持 GUI 默认的 JSON 数组写法,例如 "[0]"、"[1,2]"。
json j = json::parse(text);
if (j.is_array()) {
for (const auto& v : j) out.insert(jsonString(json{{"v", v}}, "v"));
}
} catch (...) {
// 也兼容用户手写 "0,1,2" 或带方括号/引号的宽松格式。
std::stringstream ss(text);
std::string part;
while (std::getline(ss, part, ',')) {
@@ -177,6 +200,7 @@ std::string httpRequest(const std::string& url,
if (!connect) throw std::runtime_error("WinHttpConnect 失败");
DWORD flags = (uc.nScheme == INTERNET_SCHEME_HTTPS) ? WINHTTP_FLAG_SECURE : 0;
// 这个项目约定body 为空走 GET不为空走 POST JSON。
const wchar_t* method = body.empty() ? L"GET" : L"POST";
request = WinHttpOpenRequest(connect, method, object.c_str(),
nullptr, WINHTTP_NO_REFERER,
@@ -300,6 +324,12 @@ std::string nowIsoMillis() {
return oss.str();
}
long long nowUnixMillis() {
return std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::system_clock::now().time_since_epoch())
.count();
}
void appendTextFile(const fs::path& path, const std::string& text) {
fs::create_directories(path.parent_path().empty() ? fs::path(".") : path.parent_path());
std::ofstream ofs(path, std::ios::binary | std::ios::app);
@@ -346,6 +376,7 @@ bool tagMatches(const std::string& expectedExpression, const std::string& actual
while (start <= expr.size()) {
size_t pos = expr.find("||", start);
std::string part = trimCopy(expr.substr(start, pos == std::string::npos ? std::string::npos : pos - start));
// RocketMQ C 接口订阅表达式可能是 "tagA || tagB",消费时也按同样规则过滤。
if (part == "*" || part == actual) return true;
if (pos == std::string::npos) break;
start = pos + 2;
@@ -369,6 +400,7 @@ long long findTimestampValue(const json& value) {
"Value_TIME", "valueTime", "timestamp", "timeStamp", "storeTimestamp", "time", "Time"
};
if (value.is_object()) {
// 不同消息体时间字段命名不统一,因此递归查找一组常见 key。
for (const auto& key : keys) {
auto it = value.find(key);
if (it != value.end()) {
@@ -507,6 +539,7 @@ bool findMonitorIdValue(const json& value, std::string& out) {
}
for (auto it = value.begin(); it != value.end(); ++it) {
if ((it.key() == "messageBody" || it.key() == "body") && it.value().is_string()) {
// RocketMQ 外层 JSON 的 messageBody 常是字符串化 JSON需要再 parse 一次。
std::string embedded = it.value().get<std::string>();
if (looksLikeJsonText(embedded)) {
try {
@@ -663,6 +696,14 @@ struct CSendResult {
long long offset;
};
/*
* RocketMqRuntime 是进程内唯一的 RocketMQ 适配器。
*
* 设计原因:
* 1. RocketMQ C DLL 通过 LoadLibrary 动态加载,运行时可能不存在;
* 2. GUI 会多次点击“初始化 MQ”需要避免重复启动/热重启导致 DLL 崩溃;
* 3. MQ 回调在 SDK 线程触发,必须用互斥锁保护会话状态,再通过 event_ 通知 GUI。
*/
class RocketMqRuntime {
struct DetachedHandles {
CProducer* producer = nullptr;
@@ -681,7 +722,9 @@ class RocketMqRuntime {
std::string xmlPath;
std::string frontType;
std::string guid;
std::unordered_set<std::string> receivedDataTypes;
long long startedAtMs = 0; // 点击“数据追踪”的时间;更早的 MQ 存量消息不得进入本次会话。
std::unordered_set<std::string> receivedDataTypes; // 同一次追踪中每个 DATA_TYPE 只保留一次。
std::unordered_set<std::string> receivedTraceReports; // 同一次追踪中每个 rpt_id 只保留第一条。
long long dataTimestamp = 0;
int originCounter = 0;
};
@@ -700,6 +743,7 @@ public:
DetachedHandles old;
{
std::lock_guard<std::mutex> lock(mu_);
// 如果生产/消费连接参数未变,只刷新配置和回调;避免重复 StartProducer/StartPushConsumer。
bool sameRuntimeConfig = cfg.producer == cfg_.producer &&
cfg.ipport == cfg_.ipport &&
cfg.producerAccessKey == cfg_.producerAccessKey &&
@@ -721,6 +765,7 @@ public:
return true;
}
if (active_ && producer_ && consumer_) {
// 连接参数变了也不在进程内重启底层 SDK提示用户重启工具后生效。
cfg_ = cfg;
event_ = std::move(event);
if (log) {
@@ -785,6 +830,7 @@ public:
SetPushConsumerLogPath(consumer_, mqLogDir.c_str());
SetPushConsumerThreadCount(consumer_, 2);
// 同一个 Topic 可能同时配置统计/实时 Tag这里合并成 RocketMQ 支持的 "A || B" 表达式。
std::map<std::string, std::string> subscriptions;
auto addSubscription = [&](const std::string& topic, const std::string& tag) {
if (topic.empty()) return;
@@ -830,8 +876,10 @@ public:
const PqLedgerMonitor& monitor,
const std::string& frontType,
const std::string& guid) {
const long long startedAtMs = nowUnixMillis();
std::lock_guard<std::mutex> lock(mu_);
cfg_ = cfg;
// 记录当前追踪目标。后续 MQ 回调会用 monitorId 或 guid 把消息路由到对应 TraceSession。
currentTraceDir_ = pqTraceDirectoryFor(cfg, device, monitor);
currentXmlPath_ = pqFindDeviceXmlPath(cfg, device);
currentMonitorId_ = monitor.id;
@@ -842,10 +890,9 @@ public:
dataTimestamp_ = 0;
originCounter_ = 0;
std::string monitorKey = normalizeMonitorId(monitor.id);
auto old = sessions_.find(monitorKey);
if (old != sessions_.end() && !old->second.guid.empty()) {
sessionsByGuid_.erase(normalizeMonitorId(old->second.guid));
}
// 一次按钮点击只允许一个活动会话。否则旧测点后续到达的消息仍会写入旧会话目录。
sessions_.clear();
sessionsByGuid_.clear();
TraceSession& session = sessions_[monitorKey];
session.monitorId = monitor.id;
session.monitorName = monitor.name;
@@ -853,11 +900,14 @@ public:
session.xmlPath = currentXmlPath_;
session.frontType = frontType;
session.guid = guid;
session.startedAtMs = startedAtMs;
session.receivedDataTypes.clear();
session.receivedTraceReports.clear();
session.dataTimestamp = 0;
session.originCounter = 0;
if (!guid.empty()) sessionsByGuid_[normalizeMonitorId(guid)] = monitorKey;
// GUI 自动解析优先读取这个上下文,避免用户手动维护 json/origin/output 路径。
json context = {
{"deviceId", device.id},
{"deviceName", device.name},
@@ -867,6 +917,8 @@ public:
{"monitorName", monitor.name},
{"guid", guid},
{"frontType", frontType},
{"startedAt", nowIsoMillis()},
{"startedAtMs", startedAtMs},
{"xmlPath", currentXmlPath_},
{"jsonDataPath", (dir / "jsondata_01.txt").u8string()},
{"jsonDataPaths", json::array({
@@ -879,10 +931,13 @@ public:
writeTextFile(dir / "trace_context.json", context.dump(2));
}
bool sendTrace(const PqToolConfig& cfg,
const std::string& payload,
std::string& err,
PqLogFn log) {
bool sendPayload(const std::string& topic,
const std::string& tag,
const std::string& messageKey,
const std::string& payload,
const std::string& label,
std::string& err,
PqLogFn log) {
std::lock_guard<std::mutex> lock(mu_);
err.clear();
if (!active_ || !producer_) {
@@ -890,14 +945,14 @@ public:
return false;
}
CMessage* msg = CreateMessage(nonEmptyCStr(cfg.topicLOG, ""));
CMessage* msg = CreateMessage(nonEmptyCStr(topic, ""));
if (!msg) {
err = "CreateMessage 返回空指针";
return false;
}
SetMessageTags(msg, nonEmptyCStr(cfg.tagLOG, ""));
SetMessageKeys(msg, nonEmptyCStr(cfg.keyLOG, ""));
SetMessageTags(msg, nonEmptyCStr(tag, ""));
SetMessageKeys(msg, nonEmptyCStr(messageKey, ""));
SetMessageBody(msg, payload.c_str());
CSendResult result{};
@@ -909,7 +964,7 @@ public:
}
if (log) {
log("RocketMQ 数据追踪消息已发送TopicLOG=" + cfg.topicLOG
log("RocketMQ " + label + "消息已发送Topic=" + topic
+ "MsgId=" + std::string(result.msgId));
}
return true;
@@ -929,6 +984,7 @@ private:
actualMonitor.clear();
reason.clear();
// 优先用消息体中的测点 ID 路由;没有测点 ID 时再用 guid/key 路由。
actualMonitor = monitorIdFromJsonText(body);
std::string normalized = normalizeMonitorId(actualMonitor);
if (!normalized.empty()) {
@@ -967,6 +1023,7 @@ private:
+ " tag=" + tag
+ " key=" + key
+ " bodyLen=" + std::to_string(body.size()) + "\n");
// 先按 Topic/Tag 判断消息类型,再按 monitorId/guid 过滤到当前追踪会话。
std::string dataFrontType = dataFrontTypeForMessageLocked(topic, tag);
bool isData = !dataFrontType.empty();
bool isTrace = topic == cfg_.traceTopic && tagMatches(cfg_.traceTag, tag);
@@ -990,10 +1047,22 @@ private:
+ " topic=" + topic + " tag=" + tag + "\n");
return 0;
}
// PushConsumer 可能在点击后继续投递积压消息。必须在任何去重、落盘和 GUI 事件之前
// 使用 Broker 存储时间做会话边界判断,保证 jsondata 与 origin 都来自本次追踪。
if (storeTs <= 0 || storeTs < session->startedAtMs) {
appendTextFile(runtimeLogPath("mq_messages.log"), nowIsoMillis()
+ " skipped " + (isData ? "DATATopic" : "TraceTopic")
+ " before trace session: storeTs=" + std::to_string(storeTs)
+ ", startedAtMs=" + std::to_string(session->startedAtMs)
+ ", monitor=" + session->monitorId
+ " topic=" + topic + " tag=" + tag + "\n");
return 0;
}
}
if (isData) {
std::string dataType = dataTypeFromJsonText(body);
std::string normalizedType = normalizeDataType(dataType);
// 同一 DATA_TYPE 重复到达时跳过,避免后来的旧数据覆盖已解析文件。
if (!session->receivedDataTypes.insert(normalizedType).second) {
appendTextFile(runtimeLogPath("mq_messages.log"), nowIsoMillis()
+ " skipped DATATopic duplicate DATA_TYPE=" + normalizedType
@@ -1012,10 +1081,22 @@ private:
if (event_) event_({"data", eventTitle("DATATopic", out.u8string()), body, out.u8string(), session->monitorId, session->traceDir});
} else if (isTrace) {
long long bodyTs = timestampFromJsonText(body);
// 如果 TraceTopic 的时间早于已经收到的 DATATopic认为是旧追踪结果避免串入本次追踪。
if (session->dataTimestamp > 0 && bodyTs > 0 && bodyTs < session->dataTimestamp) {
appendTextFile(runtimeLogPath("mq_messages.log"), nowIsoMillis() + " TraceTopic tag=" + tag + " skipped by timestamp\n");
} else {
fs::path out = nextOriginOutputPathLocked(*session, reportNameFromJsonText(body));
std::string reportName = reportNameFromJsonText(body);
std::string reportKey = lowerAscii(trimCopy(reportName));
if (reportKey.empty()) reportKey = "__unknown_report__";
// TopicAsk 的 limit=20 可能返回同一 rpt_id 的多份历史采样;本工具每次点击只取第一份。
if (!session->receivedTraceReports.insert(reportKey).second) {
appendTextFile(runtimeLogPath("mq_messages.log"), nowIsoMillis()
+ " skipped TraceTopic duplicate report=" + reportKey
+ " monitor=" + session->monitorId
+ " topic=" + topic + " tag=" + tag + "\n");
return 0;
}
fs::path out = nextOriginOutputPathLocked(*session, reportName);
writeTextFile(out, body);
appendTextFile(runtimeLogPath("mq_messages.log"), nowIsoMillis() + " TraceTopic tag=" + tag + " -> " + out.u8string() + "\n");
if (event_) event_({"trace", eventTitle("TraceTopic", out.u8string()), body, out.u8string(), session->monitorId, session->traceDir});
@@ -1104,6 +1185,7 @@ private:
bool loadLibraryLocked() {
if (module_) return true;
// 兼容不同部署目录和 DLL 命名LoadLibraryEx 用于带路径 DLL 时顺带搜索其依赖。
std::vector<std::string> candidates = {
"rocketmq-client-cpp.dll",
"rocketmq.dll",
@@ -1328,6 +1410,7 @@ RocketMqRuntime& rocketRuntime() {
PqToolConfig pqDefaultConfig() {
PqToolConfig cfg;
// 这里只覆盖需要根据 exe 位置动态计算的路径,以及 MQ 常用默认参数。
cfg.producer = "TRACE_PRODUCER";
cfg.consumer = "TRACE_CONSUMER";
cfg.dataStatTopic = "MQ_INST_1697676490574298_Bm3DLamM%LN_Topic";
@@ -1350,6 +1433,7 @@ std::string pqTraceDirectoryFor(const PqToolConfig& cfg,
const PqLedgerDevice& device,
const PqLedgerMonitor& monitor) {
fs::path root = fs::u8path(cfg.traceRootDir.empty() ? "trace_data" : cfg.traceRootDir);
// 路径中同时放名称和 id既方便人读也保证同名测点不互相覆盖。
std::string proc = safePathPart(firstNonEmpty(device.processNo, "1"), "1");
std::string dev = safePathPart(firstNonEmpty(device.name, device.id), "device");
std::string mon = safePathPart(firstNonEmpty(monitor.name, monitor.id), "monitor");
@@ -1372,6 +1456,7 @@ void pqClearTraceRecords(const PqToolConfig& cfg,
std::string pqFindDeviceXmlPath(const PqToolConfig& cfg, const PqLedgerDevice& device) {
if (!device.xmlPath.empty() && fs::exists(fs::u8path(device.xmlPath))) return device.xmlPath;
// 下载 ICD 后会把接口响应缓存到 runtime\json\icd_response.json这里用 devType 反查本地 XML。
fs::path saveDir = fs::u8path(cfg.xmlSaveDir.empty() ? "downloaded_xml" : cfg.xmlSaveDir);
auto existing = [&](const std::string& name) -> std::string {
if (name.empty()) return "";
@@ -1423,6 +1508,7 @@ bool pqFetchLedgerAndIcd(const PqToolConfig& cfg,
devices.clear();
firstXmlPath.clear();
// 1. 请求台账frontIp + terminalStatus/runFlag。
json ledgerReq;
ledgerReq["ip"] = cfg.frontIp;
try {
@@ -1438,6 +1524,7 @@ bool pqFetchLedgerAndIcd(const PqToolConfig& cfg,
std::unordered_set<std::string> terminalAllowed = parseStatusSet(cfg.terminalStatus);
std::set<std::string> devTypes;
// 2. 解析装置和 monitorData 测点,并按 terminalStatus 过滤装置。
json data = parseResponseDataArray(ledgerRaw, "台账");
for (const auto& item : data) {
PqLedgerDevice dev;
@@ -1479,6 +1566,7 @@ bool pqFetchLedgerAndIcd(const PqToolConfig& cfg,
if (log) log("台账解析完成: 装置 " + std::to_string(devices.size()) + "");
// 3. 请求 ICD 列表。icdFlag=1 时只请求本次台账中出现的 devType。
json icdReq = json::array();
if (cfg.icdFlag == "1") {
for (const auto& t : devTypes) icdReq.push_back(t);
@@ -1492,6 +1580,7 @@ bool pqFetchLedgerAndIcd(const PqToolConfig& cfg,
json icdData = parseResponseDataArray(icdRaw, "ICD");
int downloaded = 0;
std::unordered_map<std::string, std::string> devTypeToXml;
// 4. 按 filePath 下载 XML保存到 xmlSaveDir并建立 devType -> XML 路径映射。
for (const auto& model : icdData) {
std::string modelId = jsonString(model, "id");
std::string devType = jsonString(model, "devType");
@@ -1524,6 +1613,7 @@ bool pqFetchLedgerAndIcd(const PqToolConfig& cfg,
}
bool pqInitializeMq(const PqToolConfig& cfg, PqLogFn log, PqMqEventFn event) {
// 初始化时先把界面配置落盘,便于用户核对,也便于离线排查 MQ 参数。
json saved = {
{"producer", cfg.producer},
{"ipport", cfg.ipport},
@@ -1532,6 +1622,9 @@ bool pqInitializeMq(const PqToolConfig& cfg, PqLogFn log, PqMqEventFn event) {
{"topicLOG", cfg.topicLOG},
{"tagLOG", cfg.tagLOG},
{"keyLOG", cfg.keyLOG},
{"topicAsk", cfg.topicAsk},
{"tagAsk", cfg.tagAsk},
{"keyAsk", cfg.keyAsk},
{"consumer", cfg.consumer},
{"consumerIpport", cfg.consumerIpport},
{"consumerAccessKey", cfg.consumerAccessKey},
@@ -1552,6 +1645,7 @@ bool pqInitializeMq(const PqToolConfig& cfg, PqLogFn log, PqMqEventFn event) {
writeTextFile(runtimeJsonPath("mq_runtime_config.json"), saved.dump(2));
if (log) log("MQ config saved to " + runtimeJsonPath("mq_runtime_config.json").u8string());
if (!rocketRuntime().initialize(cfg, log, event) && log) {
// 这里仍返回 true让 GUI 可以继续构造追踪请求;发送失败时会进入 outbox 兜底。
log("RocketMQ runtime is not active; trace messages will be written to mq_outbox_*.jsonl.");
log("RocketMQ source is present, but a 32-bit rocketmq-client-cpp.dll or rocketmq.dll is still required.");
}
@@ -1571,6 +1665,7 @@ std::string pqBuildTracePayload(const PqToolConfig& cfg,
std::string guid = makeGuid();
int processNo = 0;
if (frontType == "cfg_3s_data") {
// 实时数据进程约定为 0统计数据使用台账中的 processNo。
processNo = 0;
} else {
try {
@@ -1580,6 +1675,7 @@ std::string pqBuildTracePayload(const PqToolConfig& cfg,
}
}
// body 是前置识别的 set_log 指令outer 是 RocketMQ 业务包装。
json body = {
{"code", "set_log"},
{"nodeId", cfg.frontInst},
@@ -1602,6 +1698,26 @@ std::string pqBuildTracePayload(const PqToolConfig& cfg,
return outer.dump();
}
std::string buildRealtimeAskPayload(const PqLedgerDevice& device,
const PqLedgerMonitor& monitor) {
const std::string guid = makeGuid();
json body = {
{"devSeries", device.id},
{"limit", 20},
{"line", monitor.id},
{"realData", true},
{"soeData", true}
};
json outer = {
{"key", guid},
{"source", "WEB"},
{"sendTime", nowIsoMillis()},
{"retryTimes", 0},
{"messageBody", body.dump()}
};
return outer.dump();
}
bool pqSendTraceRequest(const PqToolConfig& cfg,
const PqLedgerDevice& device,
const PqLedgerMonitor& monitor,
@@ -1610,6 +1726,7 @@ bool pqSendTraceRequest(const PqToolConfig& cfg,
PqMqEventFn event) {
std::string payload = pqBuildTracePayload(cfg, device, monitor, frontType);
std::string guid = guidFromJsonText(payload);
// 先设置追踪目标,确保追踪请求发出后立即到达的 MQ 回调也能正确路由。
rocketRuntime().setTraceTarget(cfg, device, monitor, frontType, guid);
fs::path traceDir = fs::u8path(pqTraceDirectoryFor(cfg, device, monitor));
fs::create_directories(traceDir);
@@ -1617,24 +1734,36 @@ bool pqSendTraceRequest(const PqToolConfig& cfg,
writeTextFile(sentPath, prettyJsonOrRaw(payload));
writeTextFile(runtimeJsonPath("last_trace_request.json"), json::parse(payload).dump(2));
if (event) event({"sent", eventTitle("Send", sentPath.u8string()), payload, sentPath.u8string(), monitor.id, traceDir.u8string()});
std::string mqError;
if (rocketRuntime().sendTrace(cfg, payload, mqError, log)) {
if (log) log("Trace target monitor=" + monitor.name + "(" + monitor.id + "), frontType=" + frontType);
return true;
auto sendOrSave = [&](const std::string& topic,
const std::string& tag,
const std::string& messageKey,
const std::string& body,
const std::string& label) {
std::string mqError;
if (rocketRuntime().sendPayload(topic, tag, messageKey, body, label, mqError, log)) return;
// RocketMQ 未初始化或发送失败时不丢请求,按目标 Topic 追加到对应 outbox。
std::string topicPart = sanitizeFilePart(topic.empty() ? label : topic);
fs::path outbox = runtimeJsonPath("mq_outbox_" + topicPart + ".jsonl");
std::ofstream ofs(outbox, std::ios::binary | std::ios::app);
if (!ofs) throw std::runtime_error("无法写入 MQ outbox: " + outbox.string());
ofs << body << "\n";
if (log) log(label + "消息发送失败(" + mqError + "),已写入 " + outbox.string());
};
// 第一条始终先发到 TopicLOG请求前置建立本次追踪日志。
sendOrSave(cfg.topicLOG, cfg.tagLOG, cfg.keyLOG, payload, "set_log");
// cfg_3s_data 紧接着向 TopicAsk 请求该装置、测点的实时/SOE 数据。
if (frontType == "cfg_3s_data") {
std::string askPayload = buildRealtimeAskPayload(device, monitor);
fs::path askPath = traceDir / "sent_realtime_request.json";
writeTextFile(askPath, prettyJsonOrRaw(askPayload));
writeTextFile(runtimeJsonPath("last_realtime_request.json"), json::parse(askPayload).dump(2));
if (event) event({"sent", eventTitle("Send Ask", askPath.u8string()), askPayload, askPath.u8string(), monitor.id, traceDir.u8string()});
sendOrSave(cfg.topicAsk, cfg.tagAsk, cfg.keyAsk, askPayload, "realtime ask");
}
std::string topicPart = sanitizeFilePart(cfg.topicLOG.empty() ? "TopicLOG" : cfg.topicLOG);
fs::path outbox = runtimeJsonPath("mq_outbox_" + topicPart + ".jsonl");
std::ofstream ofs(outbox, std::ios::binary | std::ios::app);
if (!ofs) throw std::runtime_error("无法写入 MQ outbox: " + outbox.string());
ofs << payload << "\n";
writeTextFile(runtimeJsonPath("last_trace_request.json"), json::parse(payload).dump(2));
if (log) {
log("已构造数据追踪消息TopicLOG=" + cfg.topicLOG + ", TagLOG=" + cfg.tagLOG + ", KeyLOG=" + cfg.keyLOG);
log("测点=" + monitor.name + "(" + monitor.id + "), frontType=" + frontType);
log("当前未直连 RocketMQ消息已写入 " + outbox.string());
}
if (log) log("Trace target monitor=" + monitor.name + "(" + monitor.id + "), frontType=" + frontType);
return true;
}