@lyra-ai/toolkit 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -0
- package/bin/log-query.mjs +6 -0
- package/package.json +3 -2
- package/src/logquery.mjs +349 -0
package/README.md
CHANGED
|
@@ -45,6 +45,24 @@ http://your-host:38000
|
|
|
45
45
|
|
|
46
46
|
---
|
|
47
47
|
|
|
48
|
+
### 日志查询(log-query)
|
|
49
|
+
|
|
50
|
+
Elasticsearch 日志查询工具,支持链路追踪、时间窗口、关键词搜索、自动根因解析。首次使用运行向导配置。
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
log-query init # 向导式配置(ES 地址 + 索引 + 应用名)
|
|
54
|
+
log-query search --trace <traceId> # 按链路追踪(全链路,时间正序)
|
|
55
|
+
log-query search --at "2026-07-11 08:32:40" # 以某时刻为中心(默认 ±5 分钟)
|
|
56
|
+
log-query search --kw "NullPointerException" # 关键词搜索
|
|
57
|
+
log-query search --hours 2 --json # 回看 2 小时,JSON 输出
|
|
58
|
+
log-query search --since "..." --until "..." # 绝对时间区间
|
|
59
|
+
log-query config # 查看当前 ES 配置
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
可选参数:`--app <名>` `--level <级>` `--index <模式>` `--size <n>` `--window <分>` `--full`
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
48
66
|
### 禅道 CLI(zentao)
|
|
49
67
|
|
|
50
68
|
禅道 REST v1 的命令行工具,支持 bug 查询/创建/解决、项目管理、配置管理。
|
package/package.json
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lyra-ai/toolkit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "面向 AI agent 的零依赖开发工具集",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"toolkit": "./bin/toolkit.mjs",
|
|
8
|
-
"zentao": "./bin/zentao.mjs"
|
|
8
|
+
"zentao": "./bin/zentao.mjs",
|
|
9
|
+
"log-query": "./bin/log-query.mjs"
|
|
9
10
|
},
|
|
10
11
|
"engines": { "node": ">=18" },
|
|
11
12
|
"files": [
|
package/src/logquery.mjs
ADDED
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* log-query.mjs — ES 日志查询 CLI(零依赖,ESM)
|
|
4
|
+
*
|
|
5
|
+
* 从 ~/.toolkit/config.json 的 es 段读配置(es-url / es-index / es-app / es-user / es-pass)。
|
|
6
|
+
* 首次使用未配置时,进入向导式 init。
|
|
7
|
+
*
|
|
8
|
+
* 用法:
|
|
9
|
+
* log-query init 向导式配置
|
|
10
|
+
* log-query search --trace <id> 按链路追踪(全链路,时间正序)
|
|
11
|
+
* log-query search --at "YYYY-MM-DD HH:MM:SS" 以某时刻为中心(默认 ±5 分钟)
|
|
12
|
+
* log-query search --kw "关键词" --hours 2 回看 2 小时
|
|
13
|
+
* log-query search --since "..." --until "..." 绝对时间区间
|
|
14
|
+
*
|
|
15
|
+
* --app <名> 应用名(默认从 config 读)
|
|
16
|
+
* --level <级> 日志级别(默认 ERROR;ALL=不限)
|
|
17
|
+
* --index <模式> 索引(默认从 config 读)
|
|
18
|
+
* --size <n> 条数(trace 默认 500,其它默认 20)
|
|
19
|
+
* --window <分> --at 单侧窗口分钟数(默认 5)
|
|
20
|
+
* --json 机器可读 JSON
|
|
21
|
+
* --full 附全部命中原始文档
|
|
22
|
+
*/
|
|
23
|
+
import fs from 'node:fs';
|
|
24
|
+
import path from 'node:path';
|
|
25
|
+
import os from 'node:os';
|
|
26
|
+
import readline from 'node:readline';
|
|
27
|
+
|
|
28
|
+
const TOOLKIT_DIR = path.join(os.homedir(), '.toolkit');
|
|
29
|
+
const CONFIG_PATH = path.join(TOOLKIT_DIR, 'config.json');
|
|
30
|
+
|
|
31
|
+
// ── 配置 ──────────────────────────────────────────────
|
|
32
|
+
function loadConfig() {
|
|
33
|
+
try { return JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf-8')); }
|
|
34
|
+
catch { return {}; }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function saveConfig(config) {
|
|
38
|
+
fs.mkdirSync(TOOLKIT_DIR, { recursive: true });
|
|
39
|
+
fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function getEsConfig() {
|
|
43
|
+
const config = loadConfig();
|
|
44
|
+
return {
|
|
45
|
+
url: config.es?.url || '',
|
|
46
|
+
index: config.es?.index || 'plume_log_run_*',
|
|
47
|
+
app: config.es?.app || '',
|
|
48
|
+
user: config.es?.user || '',
|
|
49
|
+
pass: config.es?.pass || '',
|
|
50
|
+
defaults: { level: 'ERROR', window: 5, hours: 1 },
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ── 向导 ──────────────────────────────────────────────
|
|
55
|
+
async function ask(rl, prompt, def) {
|
|
56
|
+
const suffix = def ? ` (${def})` : '';
|
|
57
|
+
return new Promise(resolve => {
|
|
58
|
+
rl.question(` ${prompt}${suffix}: `, ans => {
|
|
59
|
+
resolve((ans.trim() || def || '').trim());
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export async function initWizard() {
|
|
65
|
+
console.log('\nlog-query 配置向导\n');
|
|
66
|
+
const config = loadConfig();
|
|
67
|
+
if (!config.es) config.es = {};
|
|
68
|
+
|
|
69
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
70
|
+
|
|
71
|
+
config.es.url = await ask(rl, 'ES 地址', config.es.url);
|
|
72
|
+
if (!config.es.url) { console.error('ES 地址不能为空'); rl.close(); process.exit(2); }
|
|
73
|
+
|
|
74
|
+
// 连通性测试
|
|
75
|
+
console.log(` 测试连接 ${config.es.url} ...`);
|
|
76
|
+
try {
|
|
77
|
+
const res = await fetch(`${config.es.url}/_cluster/health`, {
|
|
78
|
+
headers: config.es.user
|
|
79
|
+
? { Authorization: 'Basic ' + Buffer.from(`${config.es.user}:${config.es.pass || ''}`).toString('base64') }
|
|
80
|
+
: {},
|
|
81
|
+
signal: AbortSignal.timeout(5000),
|
|
82
|
+
});
|
|
83
|
+
if (res.ok) {
|
|
84
|
+
const j = await res.json();
|
|
85
|
+
console.log(` ✅ 连接成功 — 集群: ${j.cluster_name || '?'} 状态: ${j.status}`);
|
|
86
|
+
} else {
|
|
87
|
+
console.log(` ⚠️ 返回 ${res.status},可能需要认证`);
|
|
88
|
+
config.es.user = await ask(rl, '用户名(不需要则回车跳过)', config.es.user);
|
|
89
|
+
if (config.es.user) config.es.pass = await ask(rl, '密码', config.es.pass);
|
|
90
|
+
}
|
|
91
|
+
} catch (e) {
|
|
92
|
+
console.log(` ⚠️ 连接失败: ${e.message}(可继续配置,稍后检查网络)`);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
config.es.index = await ask(rl, '索引模式', config.es.index || 'plume_log_run_*');
|
|
96
|
+
config.es.app = await ask(rl, '默认应用名', config.es.app);
|
|
97
|
+
|
|
98
|
+
rl.close();
|
|
99
|
+
saveConfig(config);
|
|
100
|
+
console.log(`\n✅ 配置已保存到 ${CONFIG_PATH}`);
|
|
101
|
+
console.log(` 测试: log-query search --hours 1\n`);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ── 时间工具 ──────────────────────────────────────────
|
|
105
|
+
const pad = n => String(n).padStart(2, '0');
|
|
106
|
+
const compact = s => String(s || '').replace(/\s+/g, ' ').trim();
|
|
107
|
+
|
|
108
|
+
function toMs(v) {
|
|
109
|
+
if (typeof v === 'number') return v;
|
|
110
|
+
const m = /^(\d{4})-(\d{2})-(\d{2})[ T](\d{2}):(\d{2}):(\d{2})/.exec(String(v));
|
|
111
|
+
if (!m) throw new Error(`时间格式不对: ${v}`);
|
|
112
|
+
return new Date(+m[1], +m[2] - 1, +m[3], +m[4], +m[5], +m[6]).getTime();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function fmt(ms) {
|
|
116
|
+
const d = new Date(ms);
|
|
117
|
+
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ── 查询构建 ──────────────────────────────────────────
|
|
121
|
+
function buildQuery(opts) {
|
|
122
|
+
const { trace, app, level, kw, since, until, at, window = 5, hours = 1, size = 20, explicit = {} } = opts;
|
|
123
|
+
const wantTime = !trace || !!at || !!since || !!explicit.hours;
|
|
124
|
+
let gteMs = null, ltMs = null, timeFilter = null;
|
|
125
|
+
if (wantTime) {
|
|
126
|
+
if (since) { gteMs = toMs(since); ltMs = until ? toMs(until) : Date.now(); }
|
|
127
|
+
else if (at) { const c = toMs(at); gteMs = c - window * 60 * 1000; ltMs = c + window * 60 * 1000; }
|
|
128
|
+
else { const now = Date.now(); gteMs = now - hours * 3600 * 1000; ltMs = now; }
|
|
129
|
+
timeFilter = { range: { dtTime: { gte: gteMs, lte: ltMs } } };
|
|
130
|
+
}
|
|
131
|
+
const effLevel = trace ? (explicit.level ? level : null) : level;
|
|
132
|
+
const effApp = trace ? (explicit.app ? app : null) : app;
|
|
133
|
+
const must = [], filter = [];
|
|
134
|
+
if (timeFilter) filter.push(timeFilter);
|
|
135
|
+
if (trace) filter.push({ term: { traceId: trace } });
|
|
136
|
+
if (effLevel && effLevel !== 'ALL') filter.push({ term: { logLevel: effLevel } });
|
|
137
|
+
if (effApp && effApp !== 'ALL') filter.push({ term: { appName: effApp } });
|
|
138
|
+
if (kw) {
|
|
139
|
+
const w = { value: `*${kw}*`, case_insensitive: true };
|
|
140
|
+
must.push({ bool: { should: [{ wildcard: { 'content.keyword': w } }, { wildcard: { 'className.keyword': w } }], minimum_should_match: 1 } });
|
|
141
|
+
}
|
|
142
|
+
const body = { query: { bool: { must, filter } }, sort: [{ dtTime: { order: trace ? 'asc' : 'desc' } }], size };
|
|
143
|
+
return { body, gteMs, ltMs, hasTimeFilter: !!timeFilter, effApp, effLevel };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// ── 查询执行 ──────────────────────────────────────────
|
|
147
|
+
async function search(opts) {
|
|
148
|
+
const esc = getEsConfig();
|
|
149
|
+
if (!esc.url) {
|
|
150
|
+
console.error('未配置 ES 地址。运行: log-query init');
|
|
151
|
+
process.exit(2);
|
|
152
|
+
}
|
|
153
|
+
const index = opts.index || esc.index;
|
|
154
|
+
opts.app = opts.app || esc.app;
|
|
155
|
+
opts.level = opts.level || esc.defaults.level;
|
|
156
|
+
opts.window = opts.window || esc.defaults.window;
|
|
157
|
+
opts.hours = opts.hours || esc.defaults.hours;
|
|
158
|
+
|
|
159
|
+
const { body, gteMs, ltMs, hasTimeFilter, effApp, effLevel } = buildQuery(opts);
|
|
160
|
+
const headers = { 'Content-Type': 'application/json' };
|
|
161
|
+
if (esc.user) headers.Authorization = 'Basic ' + Buffer.from(`${esc.user}:${esc.pass}`).toString('base64');
|
|
162
|
+
|
|
163
|
+
const t0 = Date.now();
|
|
164
|
+
const res = await fetch(`${esc.url}/${index}/_search`, {
|
|
165
|
+
method: 'POST', headers, body: JSON.stringify(body),
|
|
166
|
+
signal: AbortSignal.timeout(30000),
|
|
167
|
+
});
|
|
168
|
+
const data = await res.json();
|
|
169
|
+
if (data.error) throw new Error('ES 错误: ' + JSON.stringify(data.error));
|
|
170
|
+
|
|
171
|
+
const hits = data.hits?.hits || [];
|
|
172
|
+
const total = typeof data.hits?.total === 'number' ? data.hits.total : data.hits?.total?.value;
|
|
173
|
+
const sources = hits.map(h => h._source);
|
|
174
|
+
const took = Date.now() - t0;
|
|
175
|
+
const meta = {
|
|
176
|
+
mode: opts.trace ? 'trace' : 'error-search',
|
|
177
|
+
index, total, returned: hits.length, elapsedMs: took,
|
|
178
|
+
timeRange: hasTimeFilter ? { gte: fmt(gteMs), lte: fmt(ltMs) } : null,
|
|
179
|
+
filters: { traceId: opts.trace || null, app: effApp || null, level: effLevel || null, kw: opts.kw || null },
|
|
180
|
+
};
|
|
181
|
+
return { sources, total, returned: hits.length, took, meta };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// ── Stack 清洗 + 错误解析 ────────────────────────────
|
|
185
|
+
const NOISE = /^\s*at (org\.springframework|sun\.|java\.lang\.reflect|com\.sun\.|org\.apache\.|javax\.|jdk\.|io\.netty|reactor\.)/;
|
|
186
|
+
|
|
187
|
+
function cleanStack(text) {
|
|
188
|
+
const lines = String(text).split('\n');
|
|
189
|
+
const out = []; let skipped = 0;
|
|
190
|
+
for (const ln of lines) {
|
|
191
|
+
if (NOISE.test(ln)) { skipped++; continue; }
|
|
192
|
+
if (skipped > 0 && out.length) { out.push(`\t... [+${skipped} framework frames]`); skipped = 0; }
|
|
193
|
+
out.push(ln);
|
|
194
|
+
}
|
|
195
|
+
if (skipped > 0 && out.length) out.push(`\t... [+${skipped} framework frames]`);
|
|
196
|
+
return out.join('\n');
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function parseError(content) {
|
|
200
|
+
if (!content) return null;
|
|
201
|
+
const text = String(content);
|
|
202
|
+
const lines = text.split('\n');
|
|
203
|
+
const firstLine = (lines[0] || '').trim();
|
|
204
|
+
let excClass = '', excMsg = '';
|
|
205
|
+
for (const ln of lines) {
|
|
206
|
+
if (/^\s*###/.test(ln)) continue;
|
|
207
|
+
const m = /([\w.$]*(?:Exception|Error)[\w.$]*):\s*(.*)$/.exec(ln.trim());
|
|
208
|
+
if (m) { excClass = m[1]; excMsg = m[2]; break; }
|
|
209
|
+
}
|
|
210
|
+
let rootClass = '', rootMsg = '';
|
|
211
|
+
for (const ln of lines) {
|
|
212
|
+
const m = /Caused by:\s*([\w.$]*(?:Exception|Error)[\w.$]*):\s*(.*)/.exec(ln);
|
|
213
|
+
if (m) { rootClass = m[1]; rootMsg = m[2]; }
|
|
214
|
+
}
|
|
215
|
+
const frames = [];
|
|
216
|
+
for (const ln of lines) {
|
|
217
|
+
const m = /^\s*at (com\.iss\.[\w.$]*)\.([\w$]+)\(([^)]+)\)/.exec(ln);
|
|
218
|
+
if (m) { const label = `${m[1]}.${m[2]}(${m[3]})`; if (!frames.includes(label)) frames.push(label); }
|
|
219
|
+
if (frames.length >= 10) break;
|
|
220
|
+
}
|
|
221
|
+
let sqlError = null;
|
|
222
|
+
const colM = /ERROR:\s*column "([\w_]+)" does not exist/.exec(text);
|
|
223
|
+
const sqlM = /SQL:\s*(SELECT [\s\S]*?)(?:\s*###|\s*\n\s*;\s)/.exec(text);
|
|
224
|
+
if (colM || sqlM) {
|
|
225
|
+
const rawSql = sqlM ? sqlM[1].replace(/\s+/g, ' ').trim() : null;
|
|
226
|
+
let table = null;
|
|
227
|
+
if (rawSql) { const tM = /\b(?:from|join|update|into)\s+([\w.]+)/i.exec(rawSql); if (tM) table = tM[1].replace(/^.*\./, ''); }
|
|
228
|
+
sqlError = { missingColumn: colM ? colM[1] : null, table, sql: rawSql ? rawSql.slice(0, 2000) : null };
|
|
229
|
+
}
|
|
230
|
+
return { firstLine, exceptionClass: excClass || null, exceptionMessage: excMsg || null, rootCauseClass: rootClass || null, rootCauseMessage: rootMsg || null, businessFrames: frames, sqlError };
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function summarize(sources) {
|
|
234
|
+
const traceMap = new Map();
|
|
235
|
+
for (const s of sources) {
|
|
236
|
+
if (!s.traceId) continue;
|
|
237
|
+
if (!traceMap.has(s.traceId)) traceMap.set(s.traceId, { traceId: s.traceId, count: 0, firstTime: s.dateTime, level: s.logLevel, message: compact(s.content).slice(0, 140) });
|
|
238
|
+
traceMap.get(s.traceId).count++;
|
|
239
|
+
}
|
|
240
|
+
const errors = sources
|
|
241
|
+
.filter(s => (s.logLevel || '').toUpperCase() === 'ERROR')
|
|
242
|
+
.map(s => ({ time: s.dateTime, app: s.appName, traceId: s.traceId, className: s.className, ...parseError(s.content) }))
|
|
243
|
+
.filter(Boolean);
|
|
244
|
+
return { errors, traceIds: [...traceMap.values()] };
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// ── CLI 入口 ──────────────────────────────────────────
|
|
248
|
+
export async function logQuery(args) {
|
|
249
|
+
const sub = args[0];
|
|
250
|
+
|
|
251
|
+
// log-query init
|
|
252
|
+
if (sub === 'init') {
|
|
253
|
+
await initWizard();
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// log-query config (查看当前 ES 配置)
|
|
258
|
+
if (sub === 'config') {
|
|
259
|
+
const esc = getEsConfig();
|
|
260
|
+
console.log('log-query ES 配置:');
|
|
261
|
+
console.log(` url: ${esc.url || '(未配置)'}`);
|
|
262
|
+
console.log(` index: ${esc.index}`);
|
|
263
|
+
console.log(` app: ${esc.app || '(无)'}`);
|
|
264
|
+
console.log(` user: ${esc.user || '(无需认证)'}`);
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// log-query search [...]
|
|
269
|
+
if (sub === 'search' || !sub) {
|
|
270
|
+
const searchArgs = sub === 'search' ? args.slice(1) : args;
|
|
271
|
+
const arg = (name, def) => { const i = searchArgs.indexOf(`--${name}`); return i >= 0 ? searchArgs[i + 1] : def; };
|
|
272
|
+
const has = name => searchArgs.includes(`--${name}`);
|
|
273
|
+
|
|
274
|
+
const trace = arg('trace');
|
|
275
|
+
const esc = getEsConfig();
|
|
276
|
+
const opts = {
|
|
277
|
+
trace,
|
|
278
|
+
kw: arg('kw'),
|
|
279
|
+
app: arg('app', esc.app),
|
|
280
|
+
level: arg('level', esc.defaults.level),
|
|
281
|
+
hours: parseFloat(arg('hours', String(esc.defaults.hours))),
|
|
282
|
+
size: parseInt(arg('size', trace ? '500' : '20'), 10),
|
|
283
|
+
index: arg('index', esc.index),
|
|
284
|
+
at: arg('at'),
|
|
285
|
+
window: parseFloat(arg('window', String(esc.defaults.window))),
|
|
286
|
+
since: arg('since'),
|
|
287
|
+
until: arg('until'),
|
|
288
|
+
explicit: { app: has('app'), level: has('level'), hours: has('hours') },
|
|
289
|
+
};
|
|
290
|
+
const jsonOut = has('json');
|
|
291
|
+
const full = has('full');
|
|
292
|
+
const cleanEnabled = !full;
|
|
293
|
+
|
|
294
|
+
const { sources, meta } = await search(opts);
|
|
295
|
+
const { errors, traceIds } = summarize(sources);
|
|
296
|
+
|
|
297
|
+
if (jsonOut) {
|
|
298
|
+
const out = { meta, errors, traceIds };
|
|
299
|
+
if (full) out.hits = sources.map(s => ({ time: s.dateTime, level: s.logLevel, app: s.appName, traceId: s.traceId, className: s.className, content: s.content }));
|
|
300
|
+
process.stdout.write(JSON.stringify(out, null, 2) + '\n');
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// 文本输出
|
|
305
|
+
const rangeTxt = meta.timeRange ? `${meta.timeRange.gte} ~ ${meta.timeRange.lte}` : '全时段(trace)';
|
|
306
|
+
console.log(`模式: ${meta.mode} 索引: ${meta.index} 时间: ${rangeTxt}`);
|
|
307
|
+
console.log(`条件: ${trace ? `trace=${trace}` : `app=${meta.filters.app} level=${meta.filters.level}`} kw=${opts.kw || '-'}`);
|
|
308
|
+
console.log(`命中: ${meta.total} 条(显示 ${meta.returned},其中 ERROR ${errors.length}) 耗时 ${meta.elapsedMs}ms`);
|
|
309
|
+
|
|
310
|
+
if (errors.length) {
|
|
311
|
+
console.log('\n❌ ERROR 摘要:');
|
|
312
|
+
for (const e of errors) {
|
|
313
|
+
console.log(` • [${e.time}] ${e.app}`);
|
|
314
|
+
console.log(` traceId: ${e.traceId}`);
|
|
315
|
+
const exc = e.exceptionClass ? `${e.exceptionClass}: ${e.exceptionMessage}` : e.firstLine;
|
|
316
|
+
console.log(` 异常: ${exc}`);
|
|
317
|
+
if (e.rootCauseClass && e.rootCauseClass !== e.exceptionClass) console.log(` 根因: ${e.rootCauseClass}: ${e.rootCauseMessage}`);
|
|
318
|
+
if (e.businessFrames?.length) {
|
|
319
|
+
console.log(` 业务帧:`);
|
|
320
|
+
for (const f of e.businessFrames.slice(0, 6)) console.log(` - ${f}`);
|
|
321
|
+
}
|
|
322
|
+
if (e.sqlError?.missingColumn) console.log(` ⚠️ 缺失列: ${e.sqlError.table ? `${e.sqlError.table}.` : ''}${e.sqlError.missingColumn}`);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if (traceIds.length && traceIds.length <= 30) {
|
|
327
|
+
console.log('\n涉及的 traceId:');
|
|
328
|
+
for (const t of traceIds) console.log(` ${t.traceId} [${t.level}] x${t.count} ${t.firstTime} ${t.message}`);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
console.log('');
|
|
332
|
+
for (const s of sources) {
|
|
333
|
+
const lvl = (s.logLevel || '').toUpperCase();
|
|
334
|
+
const star = lvl === 'ERROR' ? '❌' : lvl === 'WARN' ? '⚠️ ' : ' ';
|
|
335
|
+
const content = String(s.content || '');
|
|
336
|
+
if (content.includes('\n') && lvl === 'ERROR') {
|
|
337
|
+
const text = cleanEnabled ? cleanStack(content) : content;
|
|
338
|
+
console.log(`${star} ${s.dateTime} ${s.logLevel} | ${s.appName}\n traceId: ${s.traceId}\n class: ${s.className || '-'}`);
|
|
339
|
+
console.log(text.split('\n').map(l => ' ' + l).join('\n'));
|
|
340
|
+
} else {
|
|
341
|
+
console.log(`${star} ${s.dateTime} ${s.logLevel || '-'} | ${s.appName || '-'} | ${compact(content).slice(0, 200)}`);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
console.error(`用法: log-query init | config | search [--trace/--at/--kw/--json/...]`);
|
|
348
|
+
process.exit(2);
|
|
349
|
+
}
|