@cvtoolman/huawei-lts-cli 2.0.0 → 2.1.1
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/LICENSE +21 -21
- package/README.md +61 -25
- package/dist/cli/commands/common.js +21 -2
- package/dist/cli/commands/config.js +20 -6
- package/dist/cli/commands/context.js +9 -2
- package/dist/cli/commands/init.js +16 -12
- package/dist/cli/commands/query.js +18 -4
- package/dist/cli/commands/trace.js +51 -11
- package/dist/cli/index.js +208 -186
- package/dist/lts/client.js +70 -1
- package/dist/lts/pagination.js +126 -27
- package/package.json +57 -57
- package/dist/cli/options.js +0 -15
- package/dist/config/profile.js +0 -108
package/LICENSE
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2024 cvtoolman
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 cvtoolman
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
**简体中文** | [English](README_en.md)
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/@cvtoolman/huawei-lts-cli)
|
|
6
|
+
[](https://github.com/jcxy/huawei-lts-cli/actions/workflows/ci.yml)
|
|
6
7
|
[](LICENSE)
|
|
7
8
|
|
|
8
9
|
> 让你的 AI 编程助手(Claude Code、Cursor、Copilot、Qoder 等)通过自然语言查询华为云日志服务(LTS)。
|
|
@@ -60,14 +61,15 @@ npm install -g @cvtoolman/huawei-lts-cli
|
|
|
60
61
|
lts-cli init
|
|
61
62
|
```
|
|
62
63
|
|
|
63
|
-
向导会依次询问 AK、SK、Project ID、Region
|
|
64
|
+
向导会依次询问 AK、SK、Project ID、Region,一次配好,只写一个文件:
|
|
64
65
|
|
|
65
66
|
| 文件 | 内容 | 谁读 |
|
|
66
67
|
|---|---|---|
|
|
67
68
|
| `~/.lts-cli/config.json` | 凭证、默认日志组/流、`traceField`(权限 0600) | 仅 CLI |
|
|
68
|
-
| `~/.lts-cli/profile.md` | 业务词表:把「订单服务」映射到日志里的实际取值 | 仅 AI |
|
|
69
69
|
|
|
70
|
-
|
|
70
|
+
**这个目录不要整个交给 AI**,里面是明文 AK/SK。要看配置用 `lts-cli config --list`(已脱敏),不需要打开文件。
|
|
71
|
+
|
|
72
|
+
日志组有哪些、字段叫什么、值长什么样——这些事实全部靠 `lts-cli groups` / `streams` / `fields` 现查,仓库和配置里都不存第二份。
|
|
71
73
|
|
|
72
74
|
拿到控制台日志搜索页的 URL 时,可以直接导入 region / groupId / streamId:
|
|
73
75
|
|
|
@@ -77,27 +79,34 @@ lts-cli init --from-url "<控制台URL>"
|
|
|
77
79
|
|
|
78
80
|
### 3. 安装 Skill(给 AI 工具用)
|
|
79
81
|
|
|
80
|
-
|
|
82
|
+
用 [skills CLI](https://skills.sh) 一条命令装到对应工具的目录(支持 75+ 种 agent):
|
|
81
83
|
|
|
82
84
|
```bash
|
|
83
|
-
#
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
# Qoder:把 .claude/skills/ 换成 .qoder/skills/
|
|
89
|
-
# 其他工具:换成对应目录(.cursor/skills/ 等)
|
|
85
|
+
npx skills add jcxy/huawei-lts-cli -a qoder # Qoder
|
|
86
|
+
npx skills add jcxy/huawei-lts-cli -a claude-code # Claude Code
|
|
87
|
+
npx skills add jcxy/huawei-lts-cli -a cursor # Cursor
|
|
88
|
+
npx skills add jcxy/huawei-lts-cli # 交互式选 agent
|
|
90
89
|
```
|
|
91
90
|
|
|
92
|
-
|
|
91
|
+
装完重启 AI 工具:skill 在会话启动时读进上下文,已经开着的会话不会热加载。日常用 `npx skills update` 更新、`npx skills remove` 卸载。
|
|
93
92
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
93
|
+
不想用 npx 也可以手动复制(任何工具都适用):
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
git clone --depth 1 https://github.com/jcxy/huawei-lts-cli.git /tmp/huawei-lts-cli
|
|
97
|
+
cp -r /tmp/huawei-lts-cli/skills/huawei-lts-log-query <目标 skills 目录>/
|
|
98
98
|
```
|
|
99
99
|
|
|
100
|
-
|
|
100
|
+
> `npx skills` 默认用软链指向一份 git clone,更新时一起同步。用 `--copy`(Windows 无软链权限时的退路)或插件方式安装时,升级会整目录替换——如果你在里面放了业务词表,先备份。
|
|
101
|
+
|
|
102
|
+
### 4. 业务词表(可选)
|
|
103
|
+
|
|
104
|
+
AI 不知道「订单服务」在你的日志里叫什么。两条路,都不需要配置文件:
|
|
105
|
+
|
|
106
|
+
- **默认行为**:AI 用管道 SQL 实时枚举候选值(`* | SELECT appName, count(*) GROUP BY appName`),再问你挑哪一个。不落盘,也不会过期。
|
|
107
|
+
- **想省这一步**:在 skill 的 `references/vocab.md` 里写映射。这个文件默认不存在,自己建;已被 `.gitignore` 排除,不会被误提交。
|
|
108
|
+
|
|
109
|
+
### 5. 命令行直接使用(不依赖 AI)
|
|
101
110
|
|
|
102
111
|
```bash
|
|
103
112
|
# 最近 2 小时最新 50 条
|
|
@@ -144,10 +153,10 @@ lts-cli query --last 1h -f json | jq '.logs[].content'
|
|
|
144
153
|
--app <取值> appName:"<取值>",与 -q 用 AND 合并
|
|
145
154
|
--content <关键词> content:"<关键词>",与 -q 用 AND 合并
|
|
146
155
|
--request-id <id> REQUEST_ID:"<id>",按时间正序输出
|
|
147
|
-
-l, --limit <n> 每页条数(默认 100),--all
|
|
156
|
+
-l, --limit <n> 每页条数(默认 100,上限 5000),--all 时同时是总数上限
|
|
148
157
|
-r, --reverse 最新优先
|
|
149
158
|
-f, --format <fmt> pretty(默认)/ table / json
|
|
150
|
-
--all
|
|
159
|
+
--all 沿游标读到末尾(默认上限 10000 条,显式 -l 则为 -l)
|
|
151
160
|
```
|
|
152
161
|
|
|
153
162
|
`--app` / `--content` / `--request-id` 自动为值加双引号——含连字符的 UUID 不加引号会被分词导致匹配不到。
|
|
@@ -158,12 +167,21 @@ lts-cli query --last 1h -f json | jq '.logs[].content'
|
|
|
158
167
|
lts-cli trace <id> [--field <name>] [--last 24h] [-l 200] [-f fmt]
|
|
159
168
|
```
|
|
160
169
|
|
|
161
|
-
按时间正序拉出一条 ID
|
|
170
|
+
按时间正序拉出一条 ID 的完整链路。字段选择顺序是 `--field` → `config.traceField` → 运行时读一次流的索引配置:把 `REQUEST_ID` / `trace_id` / `span_id` 这类 ID 字段和全文检索 `content` 一起 OR 上。结构化日志的 ID 住在 labels 里,全文索引只覆盖正文,所以不能只靠 `content`。零命中时 stderr 会列出试过哪些字段。
|
|
171
|
+
|
|
172
|
+
```bash
|
|
173
|
+
lts-cli trace <id> --field REQUEST_ID # 临时指定
|
|
174
|
+
lts-cli config --set traceField=REQUEST_ID # 这条流长期如此才值得配
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### `lts-cli context`
|
|
162
178
|
|
|
163
179
|
```bash
|
|
164
|
-
lts-cli
|
|
180
|
+
lts-cli context --line-num <lineNum> [--time <毫秒>] [--before 20] [--after 20]
|
|
165
181
|
```
|
|
166
182
|
|
|
183
|
+
`--time` 可省:`line_num` 本身就是纳秒时间戳,前 13 位即毫秒,CLI 自己推。
|
|
184
|
+
|
|
167
185
|
## 配置说明
|
|
168
186
|
|
|
169
187
|
配置文件 `~/.lts-cli/config.json`(Windows 为 `%USERPROFILE%\.lts-cli\config.json`):
|
|
@@ -177,7 +195,7 @@ lts-cli config --set traceField=REQUEST_ID
|
|
|
177
195
|
| `endpoint` | 否 | 需包含 `https://` |
|
|
178
196
|
| `groupId` | 否 | 默认日志组 ID |
|
|
179
197
|
| `streamId` | 否 | 默认日志流 ID |
|
|
180
|
-
| `traceField` | 否 | `trace`
|
|
198
|
+
| `traceField` | 否 | `trace` 的匹配字段名;不配则自动从流索引里发现 ID 字段 |
|
|
181
199
|
|
|
182
200
|
`lts-cli config --list` 会把 `ak` / `sk` 脱敏输出,避免凭证进入终端滚屏、shell 历史或 AI 上下文。
|
|
183
201
|
|
|
@@ -216,7 +234,19 @@ lts-cli config --set traceField=REQUEST_ID
|
|
|
216
234
|
|
|
217
235
|
不在表里时用 `--set endpoint=https://...` 手动指定,或参考 [华为云 LTS 终端节点](https://support.huaweicloud.com/api-lts/lts_api_0011.html)。
|
|
218
236
|
|
|
219
|
-
##
|
|
237
|
+
## 升级说明
|
|
238
|
+
|
|
239
|
+
### 从 2.0 升级到 2.1
|
|
240
|
+
|
|
241
|
+
| 变更 | 说明 |
|
|
242
|
+
|---|---|
|
|
243
|
+
| `init` 不再生成 `~/.lts-cli/profile.md` | 业务词表改为 skill 目录下的 `references/vocab.md`。旧文件可以删,留着也没影响(CLI 从来没读过它) |
|
|
244
|
+
| SKILL.md 增加运行时枚举候选值 | 没有词表也能定位应用名 |
|
|
245
|
+
| 新增 `npx skills` 安装方式 | 见上文「安装 Skill」 |
|
|
246
|
+
|
|
247
|
+
命令行参数没有变化,脚本不需要改。
|
|
248
|
+
|
|
249
|
+
### 从 1.x 升级到 2.0
|
|
220
250
|
|
|
221
251
|
2.0 修了三个会静默出错的问题,因此有破坏性变更:
|
|
222
252
|
|
|
@@ -243,9 +273,15 @@ npm test # node:test,覆盖纯函数与游标推进
|
|
|
243
273
|
npm run typecheck # tsc --noEmit
|
|
244
274
|
```
|
|
245
275
|
|
|
246
|
-
测试使用 Node 内置的 `node:test` + `tsx`,不引入额外测试依赖。跑测试需要 Node >= 20
|
|
276
|
+
测试使用 Node 内置的 `node:test` + `tsx`,不引入额外测试依赖。跑测试需要 Node >= 22——`node --test` 的通配符支持直到 22 才落地,20 上会报 `Could not find`。CLI 本身在 Node 18 上可以正常运行,CI 的 typecheck/build 覆盖 18/20/22。
|
|
277
|
+
|
|
278
|
+
架构约定见 [CONTEXT.md](CONTEXT.md) 与 [docs/adr/](docs/adr/)。提交前请读 [CONTRIBUTING.md](CONTRIBUTING.md)——里面写了这个仓库唯一不能被破坏的原则:**同一个事实只允许有一个来源**。
|
|
279
|
+
|
|
280
|
+
## 参与
|
|
247
281
|
|
|
248
|
-
|
|
282
|
+
- 报告问题:[issue 模板](https://github.com/jcxy/huawei-lts-cli/issues/new/choose)(请勿贴真实凭证、日志组 ID 或内部业务名)
|
|
283
|
+
- 安全问题:[SECURITY.md](SECURITY.md),走私密通道,不要开公开 issue
|
|
284
|
+
- 版本变更:[CHANGELOG.md](CHANGELOG.md)
|
|
249
285
|
|
|
250
286
|
## License
|
|
251
287
|
|
|
@@ -8,6 +8,7 @@ exports.resolveFormat = resolveFormat;
|
|
|
8
8
|
exports.warnIfIncomplete = warnIfIncomplete;
|
|
9
9
|
exports.handleCommandError = handleCommandError;
|
|
10
10
|
const config_1 = require("../../config");
|
|
11
|
+
const client_1 = require("../../lts/client");
|
|
11
12
|
/** 加载配置并校验凭证,缺失时打印引导并退出 */
|
|
12
13
|
function loadCheckedConfig() {
|
|
13
14
|
const config = (0, config_1.loadConfig)();
|
|
@@ -39,7 +40,14 @@ function resolveGroupStream(options, config) {
|
|
|
39
40
|
}
|
|
40
41
|
return { groupId, streamId };
|
|
41
42
|
}
|
|
42
|
-
/**
|
|
43
|
+
/**
|
|
44
|
+
* 解析时间选项:`--last` 与 `--st/--et` 互斥,至少提供一组。
|
|
45
|
+
*
|
|
46
|
+
* 返回值一律是绝对区间(ISO 8601)。相对时长必须在这里就换算掉、只算一次:
|
|
47
|
+
* 若把 `last` 传下去,每翻一页都会重算一次 `Date.now()`,窗口随时间往前滑,
|
|
48
|
+
* 游标(line_num)一旦滑出 [start, end],服务端直接报
|
|
49
|
+
* “line_num must be between them”,`--all` 会中途失败。
|
|
50
|
+
*/
|
|
43
51
|
function resolveTimeParams(options, defaultLast) {
|
|
44
52
|
const startTime = options.startTime || options.st;
|
|
45
53
|
const endTime = options.endTime || options.et;
|
|
@@ -52,7 +60,18 @@ function resolveTimeParams(options, defaultLast) {
|
|
|
52
60
|
console.error('Error: time range required. Use --last (e.g. --last 2h) or both --start-time and --end-time.');
|
|
53
61
|
process.exit(1);
|
|
54
62
|
}
|
|
55
|
-
|
|
63
|
+
let range;
|
|
64
|
+
try {
|
|
65
|
+
range = (0, client_1.resolveTimeRange)(last ? { last } : { startTime, endTime });
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
console.error(`Error: ${error.message}`);
|
|
69
|
+
process.exit(1);
|
|
70
|
+
}
|
|
71
|
+
return {
|
|
72
|
+
startTime: new Date(range.startMs).toISOString(),
|
|
73
|
+
endTime: new Date(range.endMs).toISOString(),
|
|
74
|
+
};
|
|
56
75
|
}
|
|
57
76
|
/** 将 -q 表达式与便捷筛选 flag 合并为一个查询表达式(AND 连接)
|
|
58
77
|
* 字段值统一加双引号:含连字符等分词符的值(如 UUID)不加引号会匹配不到 */
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.maskSecret = maskSecret;
|
|
4
|
+
exports.displayValue = displayValue;
|
|
5
|
+
exports.parseAssignment = parseAssignment;
|
|
3
6
|
exports.createConfigCommand = createConfigCommand;
|
|
4
7
|
const commander_1 = require("commander");
|
|
5
8
|
const config_1 = require("../../config");
|
|
@@ -10,6 +13,17 @@ function maskSecret(value) {
|
|
|
10
13
|
return '****';
|
|
11
14
|
return `${value.slice(0, 2)}${'*'.repeat(Math.max(4, value.length - 4))}${value.slice(-2)}`;
|
|
12
15
|
}
|
|
16
|
+
/** 输出任何配置值时都经过这里:密钥只看得到首尾,其余原样。 */
|
|
17
|
+
function displayValue(key, value) {
|
|
18
|
+
return SECRET_KEYS.has(key) ? maskSecret(value) : value;
|
|
19
|
+
}
|
|
20
|
+
/** 拆 `key=value`。值里可能含 `=`(base64 密钥、URL 参数),只切第一个等号。 */
|
|
21
|
+
function parseAssignment(input) {
|
|
22
|
+
const eq = input.indexOf('=');
|
|
23
|
+
if (eq <= 0)
|
|
24
|
+
return undefined;
|
|
25
|
+
return { key: input.slice(0, eq), value: input.slice(eq + 1) };
|
|
26
|
+
}
|
|
13
27
|
function createConfigCommand() {
|
|
14
28
|
const cmd = new commander_1.Command('config');
|
|
15
29
|
cmd.description('Manage LTS CLI configuration')
|
|
@@ -19,19 +33,19 @@ function createConfigCommand() {
|
|
|
19
33
|
.option('--unset <key>', 'Remove a configuration value')
|
|
20
34
|
.action((options) => {
|
|
21
35
|
if (options.set) {
|
|
22
|
-
const
|
|
23
|
-
if (
|
|
36
|
+
const assignment = parseAssignment(options.set);
|
|
37
|
+
if (!assignment) {
|
|
24
38
|
console.error('Usage: config --set key=value');
|
|
25
39
|
process.exit(1);
|
|
26
40
|
}
|
|
27
|
-
const
|
|
41
|
+
const { key, value } = assignment;
|
|
28
42
|
(0, config_1.setConfig)(key, value);
|
|
29
|
-
console.log(`Set ${key}=${value}`);
|
|
43
|
+
console.log(`Set ${key}=${displayValue(key, value)}`);
|
|
30
44
|
}
|
|
31
45
|
else if (options.get) {
|
|
32
46
|
const value = (0, config_1.getConfig)(options.get);
|
|
33
47
|
if (value !== undefined) {
|
|
34
|
-
console.log(
|
|
48
|
+
console.log(displayValue(options.get, value));
|
|
35
49
|
}
|
|
36
50
|
else {
|
|
37
51
|
console.error(`Config key '${options.get}' not found`);
|
|
@@ -46,7 +60,7 @@ function createConfigCommand() {
|
|
|
46
60
|
}
|
|
47
61
|
else {
|
|
48
62
|
for (const [key, value] of entries) {
|
|
49
|
-
console.log(`${key}=${
|
|
63
|
+
console.log(`${key}=${displayValue(key, value)}`);
|
|
50
64
|
}
|
|
51
65
|
}
|
|
52
66
|
}
|
|
@@ -11,7 +11,7 @@ function createContextCommand() {
|
|
|
11
11
|
.option('-g, --group-id <groupId>', 'Log group ID (can also be set via config)')
|
|
12
12
|
.option('-s, --stream-id <streamId>', 'Log stream ID (can also be set via config)')
|
|
13
13
|
.requiredOption('--line-num <lineNum>', 'line_num of the anchor log entry')
|
|
14
|
-
.
|
|
14
|
+
.option('--time <time>', 'Timestamp (ms) of the anchor log entry (derived from --line-num when omitted)')
|
|
15
15
|
.option('--before <count>', 'Number of logs before the anchor', (v) => parseInt(v, 10), 20)
|
|
16
16
|
.option('--after <count>', 'Number of logs after the anchor', (v) => parseInt(v, 10), 20)
|
|
17
17
|
.option('-f, --format <format>', 'Output format: json, table, pretty', 'pretty')
|
|
@@ -19,12 +19,19 @@ function createContextCommand() {
|
|
|
19
19
|
const config = (0, common_1.loadCheckedConfig)();
|
|
20
20
|
const { groupId, streamId } = (0, common_1.resolveGroupStream)(options, config);
|
|
21
21
|
const format = (0, common_1.resolveFormat)(options.format);
|
|
22
|
+
// line_num 本身就是纳秒时间戳,前 13 位即目标日志的毫秒时间。
|
|
23
|
+
// 让用户再抄一遍 --time 是纯粹的重复劳动,还会抄错。
|
|
24
|
+
const time = options.time ?? (0, client_1.anchorTimeFromLineNum)(options.lineNum);
|
|
25
|
+
if (!time) {
|
|
26
|
+
console.error(`Cannot derive the anchor time from line_num '${options.lineNum}'. Pass --time <ms>.`);
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
22
29
|
const client = new client_1.LTSClient(config);
|
|
23
30
|
const params = {
|
|
24
31
|
groupId,
|
|
25
32
|
streamId,
|
|
26
33
|
lineNum: options.lineNum,
|
|
27
|
-
time
|
|
34
|
+
time,
|
|
28
35
|
before: options.before,
|
|
29
36
|
after: options.after,
|
|
30
37
|
};
|
|
@@ -35,24 +35,28 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.createInitCommand = createInitCommand;
|
|
37
37
|
const commander_1 = require("commander");
|
|
38
|
+
const fs = __importStar(require("fs"));
|
|
39
|
+
const path = __importStar(require("path"));
|
|
38
40
|
const readline = __importStar(require("readline"));
|
|
39
41
|
const config_1 = require("../../config");
|
|
40
|
-
const
|
|
42
|
+
const store_1 = require("../../config/store");
|
|
41
43
|
function ask(rl, question) {
|
|
42
44
|
return new Promise((resolve) => {
|
|
43
45
|
rl.question(question, (answer) => resolve(answer.trim()));
|
|
44
46
|
});
|
|
45
47
|
}
|
|
46
|
-
/**
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
48
|
+
/**
|
|
49
|
+
* 2.0 及更早版本会在凭证目录里生成一份业务词表骨架,那是把 AI 引进
|
|
50
|
+
* 明文 AK/SK 所在目录的入口。2.1 起不再生成,业务词表改由 skill 的
|
|
51
|
+
* `references/` 承载。这里只对老用户的残留文件做一次性提示,绝不代删。
|
|
52
|
+
*/
|
|
53
|
+
function reportLegacyProfile() {
|
|
54
|
+
const legacy = path.join((0, store_1.getConfigDir)(), 'profile.md');
|
|
55
|
+
if (!fs.existsSync(legacy)) {
|
|
56
|
+
return;
|
|
55
57
|
}
|
|
58
|
+
console.log(` note: ${legacy} is no longer used since 2.1 — safe to delete.`);
|
|
59
|
+
console.log(' Business vocabulary now lives in the skill: references/vocab.md');
|
|
56
60
|
}
|
|
57
61
|
const ENDPOINT_MAP = {
|
|
58
62
|
'cn-north-1': 'lts.cn-north-1.myhuaweicloud.com',
|
|
@@ -156,7 +160,7 @@ function createInitCommand() {
|
|
|
156
160
|
}
|
|
157
161
|
console.log('');
|
|
158
162
|
console.log('✅ Configuration saved!');
|
|
159
|
-
|
|
163
|
+
reportLegacyProfile();
|
|
160
164
|
console.log('');
|
|
161
165
|
console.log('Try it out:');
|
|
162
166
|
if (groupId && streamId) {
|
|
@@ -219,7 +223,7 @@ function importFromConsoleUrl(raw) {
|
|
|
219
223
|
}
|
|
220
224
|
console.log('');
|
|
221
225
|
console.log('✅ Configuration saved!');
|
|
222
|
-
|
|
226
|
+
reportLegacyProfile();
|
|
223
227
|
console.log('');
|
|
224
228
|
console.log('Next steps:');
|
|
225
229
|
console.log(' 1. Configure credentials if not done yet: lts-cli init');
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.parseLimitOption = parseLimitOption;
|
|
3
4
|
exports.createQueryCommand = createQueryCommand;
|
|
4
5
|
const commander_1 = require("commander");
|
|
5
6
|
const client_1 = require("../../lts/client");
|
|
@@ -10,6 +11,17 @@ const common_1 = require("./common");
|
|
|
10
11
|
function parseIntOption(value) {
|
|
11
12
|
return parseInt(value, 10);
|
|
12
13
|
}
|
|
14
|
+
/** --all 读不到末尾时的止损上限 */
|
|
15
|
+
const ALL_CAP = 10000;
|
|
16
|
+
/** 超限的 limit 本地就拦下:服务端会回一个 LTS.1903,还要多一次往返 */
|
|
17
|
+
function parseLimitOption(value) {
|
|
18
|
+
const limit = parseIntOption(value);
|
|
19
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > pagination_1.MAX_PAGE_SIZE) {
|
|
20
|
+
// 用 commander 的 InvalidArgumentError:它会打印一行提示,而不是整个堆栈
|
|
21
|
+
throw new commander_1.InvalidArgumentError(`--limit must be between 1 and ${pagination_1.MAX_PAGE_SIZE}`);
|
|
22
|
+
}
|
|
23
|
+
return limit;
|
|
24
|
+
}
|
|
13
25
|
function createQueryCommand() {
|
|
14
26
|
const cmd = new commander_1.Command('query');
|
|
15
27
|
cmd.description('Query LTS logs')
|
|
@@ -28,10 +40,10 @@ function createQueryCommand() {
|
|
|
28
40
|
.option('--app <appName>', 'Filter by appName field, merged with -q via AND')
|
|
29
41
|
.option('--content <keyword>', 'Filter by log content (content:<keyword>), merged with -q via AND')
|
|
30
42
|
.option('--request-id <id>', 'Filter by REQUEST_ID field; output in chronological order')
|
|
31
|
-
.option('-l, --limit <limit>',
|
|
43
|
+
.option('-l, --limit <limit>', `Page size; with --all it also caps the total (max ${pagination_1.MAX_PAGE_SIZE})`, parseLimitOption, 100)
|
|
32
44
|
.option('-r, --reverse', 'Reverse order (newest first)')
|
|
33
45
|
.option('-f, --format <format>', 'Output format: json, table, pretty', 'pretty')
|
|
34
|
-
.option('--all',
|
|
46
|
+
.option('--all', `Follow the cursor to the end (up to ${ALL_CAP} logs unless -l is given)`)
|
|
35
47
|
.action(async (options) => {
|
|
36
48
|
const config = (0, common_1.loadCheckedConfig)();
|
|
37
49
|
const { groupId, streamId } = (0, common_1.resolveGroupStream)(options, config);
|
|
@@ -57,9 +69,11 @@ function createQueryCommand() {
|
|
|
57
69
|
console.log((0, output_1.formatOutput)(result, format));
|
|
58
70
|
}
|
|
59
71
|
else {
|
|
60
|
-
// --all
|
|
72
|
+
// --all:沿游标读到末尾。显式给了 -l 就以它为上限(帮助里就是这么写的),
|
|
73
|
+
// 否则用 ALL_CAP 防止失控。
|
|
74
|
+
const max = cmd.getOptionValueSource('limit') === 'cli' ? options.limit : ALL_CAP;
|
|
61
75
|
const result = await (0, pagination_1.fetchAllPages)((p) => client.queryLogs(p), params, {
|
|
62
|
-
max
|
|
76
|
+
max,
|
|
63
77
|
pageSize: options.limit,
|
|
64
78
|
});
|
|
65
79
|
(0, common_1.warnIfIncomplete)(result);
|
|
@@ -3,6 +3,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.traceCandidateFields = traceCandidateFields;
|
|
7
|
+
exports.buildTraceExpression = buildTraceExpression;
|
|
6
8
|
exports.createTraceCommand = createTraceCommand;
|
|
7
9
|
const commander_1 = require("commander");
|
|
8
10
|
const chalk_1 = __importDefault(require("chalk"));
|
|
@@ -12,20 +14,39 @@ const output_1 = require("../../output");
|
|
|
12
14
|
const config_1 = require("../../config");
|
|
13
15
|
const common_1 = require("./common");
|
|
14
16
|
/**
|
|
15
|
-
*
|
|
17
|
+
* 默认字段的选择顺序:`--field` → `config.traceField` → 运行时发现。
|
|
16
18
|
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
19
|
+
* 以前第三步写死 `content`(全文检索)。实测不对:结构化日志的 ID 住在 labels 里
|
|
20
|
+
* (REQUEST_ID / trace_id …),而全文索引只覆盖正文,`content:"<uuid>"` 一条都
|
|
21
|
+
* 匹配不到——而且不报错,只是空结果。所以第三步改成读索引配置,把形如
|
|
22
|
+
* request/trace/span + id 的字段都 OR 上,再由 full-text 兜底。
|
|
20
23
|
*/
|
|
21
24
|
const DEFAULT_TRACE_FIELD = 'content';
|
|
25
|
+
/** 字段名能凭空进查询语句(不能含连字符,否则会被当成运算符) */
|
|
26
|
+
const SAFE_FIELD_NAME = /^[A-Za-z][A-Za-z0-9_]*$/;
|
|
27
|
+
const ID_FIELD_PATTERN = /(request|trace|span)[_-]?id$/i;
|
|
28
|
+
/** 从索引配置里挑出可能装链路 ID 的字段名(运行时发现,不维护字段表) */
|
|
29
|
+
function traceCandidateFields(index) {
|
|
30
|
+
const idFields = index.fields
|
|
31
|
+
.map((f) => f.name)
|
|
32
|
+
.filter((name) => SAFE_FIELD_NAME.test(name) && ID_FIELD_PATTERN.test(name));
|
|
33
|
+
// 全文索引用在两种情况下:有 ID 字段时做兜底,没有 ID 字段时是唯一手段
|
|
34
|
+
if (idFields.length === 0 || index.fullText.enabled) {
|
|
35
|
+
return [...idFields, DEFAULT_TRACE_FIELD];
|
|
36
|
+
}
|
|
37
|
+
return idFields;
|
|
38
|
+
}
|
|
39
|
+
/** 候选字段拼成 OR 表达式,值统一加双引号(含连字符的 UUID 不分词就匹配不到) */
|
|
40
|
+
function buildTraceExpression(fields, requestId) {
|
|
41
|
+
return fields.map((field) => `${field}:"${requestId}"`).join(' OR ');
|
|
42
|
+
}
|
|
22
43
|
function createTraceCommand() {
|
|
23
44
|
const cmd = new commander_1.Command('trace');
|
|
24
45
|
cmd.description('Trace a request by ID — fetch the full request chain in chronological order')
|
|
25
46
|
.argument('<requestId>', 'Value to trace (request ID, trace ID, or any search text)')
|
|
26
47
|
.option('-g, --group-id <groupId>', 'Log group ID (can also be set via config)')
|
|
27
48
|
.option('-s, --stream-id <streamId>', 'Log stream ID (can also be set via config)')
|
|
28
|
-
.option('--field <name>',
|
|
49
|
+
.option('--field <name>', 'Field to match. Defaults to config.traceField, then the ID fields found in the stream index')
|
|
29
50
|
.option('--last <duration>', 'Relative time range: 30m, 2h, 1d, 7d (default: 24h)')
|
|
30
51
|
.option('--start-time <startTime>', 'Start time (ISO 8601)')
|
|
31
52
|
.option('--end-time <endTime>', 'End time (ISO 8601)')
|
|
@@ -38,20 +59,23 @@ function createTraceCommand() {
|
|
|
38
59
|
const { groupId, streamId } = (0, common_1.resolveGroupStream)(options, config);
|
|
39
60
|
const timeParams = (0, common_1.resolveTimeParams)(options, '24h');
|
|
40
61
|
const format = (0, common_1.resolveFormat)(options.format);
|
|
41
|
-
const
|
|
42
|
-
// 值加双引号:含连字符的 UUID 不加引号会被分词导致匹配不到
|
|
43
|
-
const query = `${field}:"${requestId}"`;
|
|
62
|
+
const explicitField = options.field || (0, config_1.getConfig)('traceField');
|
|
44
63
|
const client = new client_1.LTSClient(config);
|
|
64
|
+
// 链路按时间正序,且不加应用过滤:一条链路本来就会横跨多个应用
|
|
45
65
|
const params = {
|
|
46
66
|
groupId,
|
|
47
67
|
streamId,
|
|
48
68
|
...timeParams,
|
|
49
|
-
|
|
50
|
-
reverse: false, // 链路按时间正序
|
|
69
|
+
reverse: false,
|
|
51
70
|
};
|
|
52
71
|
try {
|
|
72
|
+
const fields = explicitField
|
|
73
|
+
? [explicitField]
|
|
74
|
+
: await discoverTraceFields(client, groupId, streamId);
|
|
75
|
+
params.query = buildTraceExpression(fields, requestId);
|
|
53
76
|
const maxTotal = options.limit || 200;
|
|
54
|
-
//
|
|
77
|
+
// 每页 500 条:服务端单页最多 5000,但 500 的首屏延迟明显更好,
|
|
78
|
+
// 而一条链路本来就只有几十条,多跑几页也不痛不痒
|
|
55
79
|
const pageSize = Math.min(maxTotal, 500);
|
|
56
80
|
const result = await (0, pagination_1.fetchAllPages)((p) => client.queryLogs(p), params, {
|
|
57
81
|
max: maxTotal,
|
|
@@ -61,6 +85,10 @@ function createTraceCommand() {
|
|
|
61
85
|
if (result.truncated) {
|
|
62
86
|
console.error(`Warning: stopped at --limit ${maxTotal}; the chain may continue. Re-run with a larger --limit.`);
|
|
63
87
|
}
|
|
88
|
+
if (result.logs.length === 0) {
|
|
89
|
+
console.error(`No logs matched "${requestId}" in ${fields.join(', ')}. ` +
|
|
90
|
+
'Widen --last, or pick the exact field with --field <name>.');
|
|
91
|
+
}
|
|
64
92
|
let output = (0, output_1.formatOutput)({ logs: result.logs, total: result.total }, format);
|
|
65
93
|
// pretty 格式下高亮 requestId,便于肉眼定位
|
|
66
94
|
if (format === 'pretty' && result.logs.length > 0) {
|
|
@@ -74,3 +102,15 @@ function createTraceCommand() {
|
|
|
74
102
|
});
|
|
75
103
|
return cmd;
|
|
76
104
|
}
|
|
105
|
+
/**
|
|
106
|
+
* 读一次索引配置来决定搜哪些字段。读不到(权限/接口异常)时退回全文检索:
|
|
107
|
+
* 宁可退化成以前的行为,也不要让 trace 直接报错。
|
|
108
|
+
*/
|
|
109
|
+
async function discoverTraceFields(client, groupId, streamId) {
|
|
110
|
+
try {
|
|
111
|
+
return traceCandidateFields(await client.getStreamIndex(groupId, streamId));
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
return [DEFAULT_TRACE_FIELD];
|
|
115
|
+
}
|
|
116
|
+
}
|