@cvtoolman/huawei-lts-cli 1.0.2 → 2.0.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/LICENSE +1 -1
- package/README.md +140 -149
- package/dist/cli/commands/common.js +111 -0
- package/dist/cli/commands/config.js +9 -2
- package/dist/cli/commands/context.js +40 -0
- package/dist/cli/commands/fields.js +108 -0
- package/dist/cli/commands/init.js +76 -3
- package/dist/cli/commands/query.js +39 -94
- package/dist/cli/commands/streams.js +81 -0
- package/dist/cli/commands/trace.js +76 -0
- package/dist/cli/index.js +134 -11
- package/dist/config/profile.js +108 -0
- package/dist/config/store.js +7 -2
- package/dist/lts/client.js +171 -11
- package/dist/lts/pagination.js +56 -0
- package/dist/output/index.js +1 -1
- package/dist/output/table.js +2 -2
- package/package.json +11 -9
package/dist/cli/index.js
CHANGED
|
@@ -1,20 +1,80 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
"use strict";
|
|
3
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
4
|
+
if (k2 === undefined) k2 = k;
|
|
5
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
6
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
7
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
8
|
+
}
|
|
9
|
+
Object.defineProperty(o, k2, desc);
|
|
10
|
+
}) : (function(o, m, k, k2) {
|
|
11
|
+
if (k2 === undefined) k2 = k;
|
|
12
|
+
o[k2] = m[k];
|
|
13
|
+
}));
|
|
14
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
15
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
16
|
+
}) : function(o, v) {
|
|
17
|
+
o["default"] = v;
|
|
18
|
+
});
|
|
19
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
20
|
+
var ownKeys = function(o) {
|
|
21
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
22
|
+
var ar = [];
|
|
23
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
24
|
+
return ar;
|
|
25
|
+
};
|
|
26
|
+
return ownKeys(o);
|
|
27
|
+
};
|
|
28
|
+
return function (mod) {
|
|
29
|
+
if (mod && mod.__esModule) return mod;
|
|
30
|
+
var result = {};
|
|
31
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
32
|
+
__setModuleDefault(result, mod);
|
|
33
|
+
return result;
|
|
34
|
+
};
|
|
35
|
+
})();
|
|
3
36
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
37
|
+
const fs = __importStar(require("fs"));
|
|
38
|
+
const path = __importStar(require("path"));
|
|
4
39
|
const commander_1 = require("commander");
|
|
5
40
|
const query_1 = require("./commands/query");
|
|
41
|
+
const trace_1 = require("./commands/trace");
|
|
42
|
+
const context_1 = require("./commands/context");
|
|
43
|
+
const streams_1 = require("./commands/streams");
|
|
6
44
|
const config_1 = require("./commands/config");
|
|
7
45
|
const init_1 = require("./commands/init");
|
|
46
|
+
const fields_1 = require("./commands/fields");
|
|
47
|
+
/**
|
|
48
|
+
* Read the version from package.json at runtime.
|
|
49
|
+
*
|
|
50
|
+
* A hard-coded version string drifts silently from package.json — `lts-cli --version`
|
|
51
|
+
* then reports a build that was never published. Resolved relative to __dirname so it
|
|
52
|
+
* works both from src/ (tsx) and dist/ (published package).
|
|
53
|
+
*/
|
|
54
|
+
function readVersion() {
|
|
55
|
+
try {
|
|
56
|
+
const pkgPath = path.resolve(__dirname, '..', '..', 'package.json');
|
|
57
|
+
return JSON.parse(fs.readFileSync(pkgPath, 'utf-8')).version ?? '0.0.0';
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
return '0.0.0';
|
|
61
|
+
}
|
|
62
|
+
}
|
|
8
63
|
const program = new commander_1.Command();
|
|
9
64
|
program
|
|
10
65
|
.name('lts-cli')
|
|
11
66
|
.description('Huawei Cloud LTS (Log Tank Service) CLI — query logs from the terminal')
|
|
12
|
-
.version(
|
|
67
|
+
.version(readVersion())
|
|
13
68
|
.addHelpText('after', `
|
|
14
69
|
Examples:
|
|
15
70
|
$ lts-cli init Setup credentials interactively
|
|
71
|
+
$ lts-cli init --from-url <url> Import region/groupId/streamId from a console URL
|
|
16
72
|
$ lts-cli config --list Show current configuration
|
|
17
|
-
$ lts-cli
|
|
73
|
+
$ lts-cli groups List log groups
|
|
74
|
+
$ lts-cli streams List log streams in a group
|
|
75
|
+
$ lts-cli fields Show the index config of a log stream
|
|
76
|
+
$ lts-cli query --last 2h -q 'level:error' --all
|
|
77
|
+
$ lts-cli trace <requestId> Fetch the full request chain
|
|
18
78
|
|
|
19
79
|
Environment variables:
|
|
20
80
|
LTS_AK Access Key
|
|
@@ -27,7 +87,19 @@ Environment variables:
|
|
|
27
87
|
`);
|
|
28
88
|
program.addCommand((0, init_1.createInitCommand)());
|
|
29
89
|
program.addCommand((0, query_1.createQueryCommand)());
|
|
90
|
+
program.addCommand((0, trace_1.createTraceCommand)());
|
|
91
|
+
program.addCommand((0, context_1.createContextCommand)());
|
|
92
|
+
program.addCommand((0, streams_1.createGroupsCommand)());
|
|
93
|
+
program.addCommand((0, streams_1.createStreamsCommand)());
|
|
94
|
+
program.addCommand((0, fields_1.createFieldsCommand)());
|
|
30
95
|
program.addCommand((0, config_1.createConfigCommand)());
|
|
96
|
+
/**
|
|
97
|
+
* 补全列表手写在下面,和命令定义分居两处,很容易漏更新。
|
|
98
|
+
* 改任何命令的选项时请同步这三段。
|
|
99
|
+
*/
|
|
100
|
+
const QUERY_OPTS = '-g --group-id -s --stream-id --start-time --end-time --st --et --last -q --query --app --content --request-id -l --limit -r --reverse -f --format --all';
|
|
101
|
+
const TRACE_OPTS = '-g --group-id -s --stream-id --field --last --start-time --end-time --st --et -l --limit -f --format';
|
|
102
|
+
const VALUE_OPTS = '-g --group-id -s --stream-id -f --format';
|
|
31
103
|
// Shell completion support
|
|
32
104
|
const completionCmd = new commander_1.Command('completion')
|
|
33
105
|
.description('Generate shell completion script')
|
|
@@ -42,7 +114,7 @@ _lts_cli_completion() {
|
|
|
42
114
|
local cur="\${words[cword]}"
|
|
43
115
|
|
|
44
116
|
if (( cword == 1 )); then
|
|
45
|
-
COMPREPLY=( $(compgen -W "query config init completion" -- "\$cur") )
|
|
117
|
+
COMPREPLY=( $(compgen -W "query trace context groups streams fields config init completion" -- "\$cur") )
|
|
46
118
|
return
|
|
47
119
|
fi
|
|
48
120
|
|
|
@@ -50,16 +122,32 @@ _lts_cli_completion() {
|
|
|
50
122
|
case "\$cmd" in
|
|
51
123
|
query)
|
|
52
124
|
case "\$prev" in
|
|
53
|
-
-g|--group-id|-s|--stream-id|-
|
|
125
|
+
-g|--group-id|-s|--stream-id|-q|--query|--app|--content|--request-id|-l|--limit|-f|--format)
|
|
126
|
+
COMPREPLY=()
|
|
127
|
+
return
|
|
128
|
+
;;
|
|
129
|
+
*)
|
|
130
|
+
COMPREPLY=( $(compgen -W "${QUERY_OPTS}" -- "\$cur") )
|
|
131
|
+
return
|
|
132
|
+
;;
|
|
133
|
+
esac
|
|
134
|
+
;;
|
|
135
|
+
trace)
|
|
136
|
+
case "\$prev" in
|
|
137
|
+
-g|--group-id|-s|--stream-id|--field|--last|-l|--limit|-f|--format)
|
|
54
138
|
COMPREPLY=()
|
|
55
139
|
return
|
|
56
140
|
;;
|
|
57
141
|
*)
|
|
58
|
-
COMPREPLY=( $(compgen -W "
|
|
142
|
+
COMPREPLY=( $(compgen -W "${TRACE_OPTS}" -- "\$cur") )
|
|
59
143
|
return
|
|
60
144
|
;;
|
|
61
145
|
esac
|
|
62
146
|
;;
|
|
147
|
+
fields)
|
|
148
|
+
COMPREPLY=( $(compgen -W "${VALUE_OPTS}" -- "\$cur") )
|
|
149
|
+
return
|
|
150
|
+
;;
|
|
63
151
|
config)
|
|
64
152
|
COMPREPLY=( $(compgen -W "--set --get --list --unset" -- "\$cur") )
|
|
65
153
|
return
|
|
@@ -83,6 +171,11 @@ _lts_cli() {
|
|
|
83
171
|
local -a commands
|
|
84
172
|
commands=(
|
|
85
173
|
'query:Query LTS logs'
|
|
174
|
+
'trace:Trace a request by ID'
|
|
175
|
+
'context:View logs before/after a log entry'
|
|
176
|
+
'groups:List log groups'
|
|
177
|
+
'streams:List log streams'
|
|
178
|
+
'fields:Show the index config of a log stream'
|
|
86
179
|
'config:Manage configuration'
|
|
87
180
|
'init:Interactive setup wizard'
|
|
88
181
|
'completion:Generate shell completion script'
|
|
@@ -109,12 +202,34 @@ _lts_cli() {
|
|
|
109
202
|
'--end-time[End time (ISO 8601)]:time:' \\
|
|
110
203
|
'--st[Alias for --start-time]:time:' \\
|
|
111
204
|
'--et[Alias for --end-time]:time:' \\
|
|
112
|
-
'
|
|
113
|
-
'-
|
|
114
|
-
'
|
|
205
|
+
'--last[Relative range: 30m, 2h, 1d, 7d]:duration:' \\
|
|
206
|
+
'-q[Query expression]:query:' \\
|
|
207
|
+
'--app[Filter by appName]:appName:' \\
|
|
208
|
+
'--content[Filter by content]:keyword:' \\
|
|
209
|
+
'--request-id[Filter by REQUEST_ID]:id:' \\
|
|
210
|
+
'-l[Page size]:number:' \\
|
|
115
211
|
'-r[Reverse order]' \\
|
|
116
212
|
'-f[Output format]:format:(json table pretty)' \\
|
|
117
|
-
'--
|
|
213
|
+
'--all[Follow the cursor to the end]'
|
|
214
|
+
;;
|
|
215
|
+
trace)
|
|
216
|
+
_arguments \\
|
|
217
|
+
'-g[Log group ID]:group ID:' \\
|
|
218
|
+
'-s[Log stream ID]:stream ID:' \\
|
|
219
|
+
'--field[Field to match]:field:' \\
|
|
220
|
+
'--last[Relative range: 30m, 2h, 1d, 7d]:duration:' \\
|
|
221
|
+
'--start-time[Start time (ISO 8601)]:time:' \\
|
|
222
|
+
'--end-time[End time (ISO 8601)]:time:' \\
|
|
223
|
+
'--st[Alias for --start-time]:time:' \\
|
|
224
|
+
'--et[Alias for --end-time]:time:' \\
|
|
225
|
+
'-l[Max logs]:number:' \\
|
|
226
|
+
'-f[Output format]:format:(json table pretty)'
|
|
227
|
+
;;
|
|
228
|
+
fields|groups|streams)
|
|
229
|
+
_arguments \\
|
|
230
|
+
'-g[Log group ID]:group ID:' \\
|
|
231
|
+
'-s[Log stream ID]:stream ID:' \\
|
|
232
|
+
'-f[Output format]:format:(json table pretty)'
|
|
118
233
|
;;
|
|
119
234
|
config)
|
|
120
235
|
_arguments \\
|
|
@@ -137,7 +252,7 @@ compdef _lts_cli lts-cli`);
|
|
|
137
252
|
Register-ArgumentCompleter -CommandName lts-cli -ScriptBlock {
|
|
138
253
|
param(\$wordToComplete, \$commandAst, \$cursorPosition)
|
|
139
254
|
|
|
140
|
-
\$commands = @('query', 'config', 'init', 'completion')
|
|
255
|
+
\$commands = @('query', 'trace', 'context', 'groups', 'streams', 'fields', 'config', 'init', 'completion')
|
|
141
256
|
|
|
142
257
|
\$args = \$commandAst.ToString() -split '\\s+'
|
|
143
258
|
if (\$args.Count -le 1) {
|
|
@@ -146,7 +261,15 @@ Register-ArgumentCompleter -CommandName lts-cli -ScriptBlock {
|
|
|
146
261
|
|
|
147
262
|
switch (\$args[1]) {
|
|
148
263
|
'query' {
|
|
149
|
-
\$opts = @('-g', '-s', '--start-time', '--end-time', '--st', '--et', '-
|
|
264
|
+
\$opts = @('-g', '-s', '--start-time', '--end-time', '--st', '--et', '--last', '-q', '--app', '--content', '--request-id', '-l', '-r', '-f', '--all')
|
|
265
|
+
return \$opts | Where-Object { \$_ -like "\$wordToComplete*" }
|
|
266
|
+
}
|
|
267
|
+
'trace' {
|
|
268
|
+
\$opts = @('-g', '-s', '--field', '--last', '--start-time', '--end-time', '--st', '--et', '-l', '-f')
|
|
269
|
+
return \$opts | Where-Object { \$_ -like "\$wordToComplete*" }
|
|
270
|
+
}
|
|
271
|
+
'fields' {
|
|
272
|
+
\$opts = @('-g', '-s', '-f')
|
|
150
273
|
return \$opts | Where-Object { \$_ -like "\$wordToComplete*" }
|
|
151
274
|
}
|
|
152
275
|
'config' {
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.getProfilePath = getProfilePath;
|
|
37
|
+
exports.profileExists = profileExists;
|
|
38
|
+
exports.writeProfileSkeleton = writeProfileSkeleton;
|
|
39
|
+
const fs = __importStar(require("fs"));
|
|
40
|
+
const path = __importStar(require("path"));
|
|
41
|
+
const store_1 = require("./store");
|
|
42
|
+
/**
|
|
43
|
+
* `profile.md` 是给人写、给 AI 读的业务词表。CLI 从不读它。
|
|
44
|
+
*
|
|
45
|
+
* 分界线:`config.json` 存 CLI 的行为(凭证、traceField、默认 group/stream);
|
|
46
|
+
* `profile.md` 存 API 拿不到的东西(业务词 → 日志里的值、命名约定、黑话)。
|
|
47
|
+
* 字段清单、日志组/流 ID 一律不进 profile —— 那是第二份会过期的真相,
|
|
48
|
+
* 用 `lts-cli groups` / `streams` / `fields` 现查。
|
|
49
|
+
*/
|
|
50
|
+
function getProfilePath() {
|
|
51
|
+
return path.join((0, store_1.getConfigDir)(), 'profile.md');
|
|
52
|
+
}
|
|
53
|
+
function profileExists() {
|
|
54
|
+
return fs.existsSync(getProfilePath());
|
|
55
|
+
}
|
|
56
|
+
const SKELETON = `# LTS 业务词表(本机私有)
|
|
57
|
+
|
|
58
|
+
> 这份文件给人改、给 AI 读。\`lts-cli\` 本身**不读**它——CLI 的行为配置在 \`~/.lts-cli/config.json\`。
|
|
59
|
+
>
|
|
60
|
+
> 只写 API 拿不到的东西:业务词怎么落到日志里的值、团队自己的命名约定、黑话。
|
|
61
|
+
> **不要**把日志组/日志流 ID、字段清单写进来——用 \`lts-cli groups\` / \`lts-cli streams\` /
|
|
62
|
+
> \`lts-cli fields\` 现查。抄进文档的那一刻它就开始过期。
|
|
63
|
+
|
|
64
|
+
## 1. 业务称呼 → 日志里的取值
|
|
65
|
+
|
|
66
|
+
查日志前先在这里翻译,避免每次重新猜应用名。
|
|
67
|
+
|
|
68
|
+
| 业务称呼 | 日志中的取值 | 说明 |
|
|
69
|
+
| --- | --- | --- |
|
|
70
|
+
| 订单服务 | (填字段值和值) | 例如 appName 或 service_name 的取值 |
|
|
71
|
+
|
|
72
|
+
## 2. 字段含义
|
|
73
|
+
|
|
74
|
+
只写「这个字段在我们这里表示什么」。类型、是否大小写敏感、分词符请用 \`lts-cli fields\` 查。
|
|
75
|
+
|
|
76
|
+
| 字段 | 含义 |
|
|
77
|
+
| --- | --- |
|
|
78
|
+
| (字段名) | (说明) |
|
|
79
|
+
|
|
80
|
+
## 3. 命名约定与分词陷阱
|
|
81
|
+
|
|
82
|
+
- 哪些值含连字符/下划线,必须加双引号才会被当成一个词
|
|
83
|
+
- 值的大小写约定(全大写?小写?)
|
|
84
|
+
- 哪些字段看起来该有索引、实际上没建(没建索引的字段只能全文检索命中)
|
|
85
|
+
|
|
86
|
+
## 4. 团队黑话
|
|
87
|
+
|
|
88
|
+
| 说法 | 实际指什么 |
|
|
89
|
+
| --- | --- |
|
|
90
|
+
| (黑话) | (含义) |
|
|
91
|
+
|
|
92
|
+
## 5. 常用排查入口
|
|
93
|
+
|
|
94
|
+
| 想查什么 | 建议命令 |
|
|
95
|
+
| --- | --- |
|
|
96
|
+
| 某个 ID 的完整链路 | \`lts-cli trace <id>\` |
|
|
97
|
+
| 某业务最近的报错 | \`lts-cli query --last 1h -q '(字段):(值)'\` |
|
|
98
|
+
| 先看清楚这条流有什么字段 | \`lts-cli fields\` |
|
|
99
|
+
`;
|
|
100
|
+
function writeProfileSkeleton() {
|
|
101
|
+
const target = getProfilePath();
|
|
102
|
+
if (fs.existsSync(target)) {
|
|
103
|
+
return { path: target, created: false };
|
|
104
|
+
}
|
|
105
|
+
fs.mkdirSync(path.dirname(target), { recursive: true, mode: 0o700 });
|
|
106
|
+
fs.writeFileSync(target, SKELETON, { encoding: 'utf-8', mode: 0o600 });
|
|
107
|
+
return { path: target, created: true };
|
|
108
|
+
}
|
package/dist/config/store.js
CHANGED
|
@@ -36,6 +36,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
36
36
|
exports.readConfigFile = readConfigFile;
|
|
37
37
|
exports.writeConfigFile = writeConfigFile;
|
|
38
38
|
exports.getConfigFilePath = getConfigFilePath;
|
|
39
|
+
exports.getConfigDir = getConfigDir;
|
|
39
40
|
const fs = __importStar(require("fs"));
|
|
40
41
|
const path = __importStar(require("path"));
|
|
41
42
|
const os = __importStar(require("os"));
|
|
@@ -43,7 +44,7 @@ const CONFIG_DIR = path.join(os.homedir(), '.lts-cli');
|
|
|
43
44
|
const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
|
|
44
45
|
function ensureDir(dir) {
|
|
45
46
|
if (!fs.existsSync(dir)) {
|
|
46
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
47
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
47
48
|
}
|
|
48
49
|
}
|
|
49
50
|
function readConfigFile() {
|
|
@@ -60,8 +61,12 @@ function readConfigFile() {
|
|
|
60
61
|
}
|
|
61
62
|
function writeConfigFile(config) {
|
|
62
63
|
ensureDir(CONFIG_DIR);
|
|
63
|
-
|
|
64
|
+
// 配置里是明文 AK/SK:目录 0700、文件 0600,别让同机器上的其他账号读到
|
|
65
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), { encoding: 'utf-8', mode: 0o600 });
|
|
64
66
|
}
|
|
65
67
|
function getConfigFilePath() {
|
|
66
68
|
return CONFIG_FILE;
|
|
67
69
|
}
|
|
70
|
+
function getConfigDir() {
|
|
71
|
+
return CONFIG_DIR;
|
|
72
|
+
}
|
package/dist/lts/client.js
CHANGED
|
@@ -1,15 +1,80 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.LTSClient = void 0;
|
|
4
|
+
exports.resolveTimeRange = resolveTimeRange;
|
|
5
|
+
exports.parseQueryExpression = parseQueryExpression;
|
|
4
6
|
const LtsClient_1 = require("@huaweicloud/huaweicloud-sdk-lts/v2/LtsClient");
|
|
5
7
|
const ClientBuilder_1 = require("@huaweicloud/huaweicloud-sdk-core/ClientBuilder");
|
|
6
8
|
const BasicCredentials_1 = require("@huaweicloud/huaweicloud-sdk-core/auth/BasicCredentials");
|
|
7
9
|
const region_1 = require("@huaweicloud/huaweicloud-sdk-core/region/region");
|
|
8
10
|
const ListLogsRequest_1 = require("@huaweicloud/huaweicloud-sdk-lts/v2/model/ListLogsRequest");
|
|
9
11
|
const QueryLtsLogParams_1 = require("@huaweicloud/huaweicloud-sdk-lts/v2/model/QueryLtsLogParams");
|
|
12
|
+
const ListLogContextRequest_1 = require("@huaweicloud/huaweicloud-sdk-lts/v2/model/ListLogContextRequest");
|
|
13
|
+
const ListLogContextRequestBody_1 = require("@huaweicloud/huaweicloud-sdk-lts/v2/model/ListLogContextRequestBody");
|
|
14
|
+
const ListLogGroupsRequest_1 = require("@huaweicloud/huaweicloud-sdk-lts/v2/model/ListLogGroupsRequest");
|
|
15
|
+
const ListLogStreamsRequest_1 = require("@huaweicloud/huaweicloud-sdk-lts/v2/model/ListLogStreamsRequest");
|
|
16
|
+
const ListLogStreamIndexRequest_1 = require("@huaweicloud/huaweicloud-sdk-lts/v2/model/ListLogStreamIndexRequest");
|
|
17
|
+
const pagination_1 = require("./pagination");
|
|
10
18
|
function sleep(ms) {
|
|
11
19
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
12
20
|
}
|
|
21
|
+
const DURATION_REGEX = /^(\d+)([mhd])$/;
|
|
22
|
+
const DURATION_UNIT_MS = {
|
|
23
|
+
m: 60 * 1000,
|
|
24
|
+
h: 60 * 60 * 1000,
|
|
25
|
+
d: 24 * 60 * 60 * 1000,
|
|
26
|
+
};
|
|
27
|
+
/** 解析相对时间范围(如 30m/2h/1d),返回 [startMs, endMs] */
|
|
28
|
+
function resolveTimeRange(params) {
|
|
29
|
+
if (params.last) {
|
|
30
|
+
const match = DURATION_REGEX.exec(params.last.trim());
|
|
31
|
+
if (!match) {
|
|
32
|
+
throw new Error(`Invalid --last duration: "${params.last}". Expected format like 30m, 2h, 1d, 7d.`);
|
|
33
|
+
}
|
|
34
|
+
const amount = parseInt(match[1], 10);
|
|
35
|
+
const endMs = Date.now();
|
|
36
|
+
return { startMs: endMs - amount * DURATION_UNIT_MS[match[2]], endMs };
|
|
37
|
+
}
|
|
38
|
+
if (!params.startTime || !params.endTime) {
|
|
39
|
+
throw new Error('Time range required: provide --last (e.g. --last 2h) or both --start-time and --end-time.');
|
|
40
|
+
}
|
|
41
|
+
const startMs = new Date(params.startTime).getTime();
|
|
42
|
+
const endMs = new Date(params.endTime).getTime();
|
|
43
|
+
if (Number.isNaN(startMs) || Number.isNaN(endMs)) {
|
|
44
|
+
throw new Error(`Invalid time format. Use ISO 8601 (e.g. 2024-01-01T00:00:00Z).`);
|
|
45
|
+
}
|
|
46
|
+
return { startMs, endMs };
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* 解析查询表达式(对齐控制台管道语法):
|
|
50
|
+
* - "搜索语句 | SELECT ..." → keywords=搜索语句, query=SQL, is_analysis_query=true
|
|
51
|
+
* - 裸 "SELECT ..." → query=SQL, is_analysis_query=true
|
|
52
|
+
* - 其他(field:value 等) → keywords=搜索语句
|
|
53
|
+
*
|
|
54
|
+
* 导出仅为测试:这一段决定了用户写的字符串怎么落到 keywords/query 两个字段上,
|
|
55
|
+
* 拆错了就会静默返回全量日志。
|
|
56
|
+
*/
|
|
57
|
+
function parseQueryExpression(expr) {
|
|
58
|
+
const trimmed = expr.trim();
|
|
59
|
+
if (trimmed.includes('|')) {
|
|
60
|
+
const idx = trimmed.indexOf('|');
|
|
61
|
+
const searchPart = trimmed.slice(0, idx).trim();
|
|
62
|
+
const sqlPart = trimmed.slice(idx + 1).trim();
|
|
63
|
+
return {
|
|
64
|
+
keywords: searchPart && searchPart !== '*' ? searchPart : undefined,
|
|
65
|
+
query: sqlPart || undefined,
|
|
66
|
+
isAnalysisQuery: sqlPart.length > 0,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
if (/^select\s/i.test(trimmed)) {
|
|
70
|
+
return { query: trimmed, isAnalysisQuery: true };
|
|
71
|
+
}
|
|
72
|
+
return { keywords: trimmed, isAnalysisQuery: false };
|
|
73
|
+
}
|
|
74
|
+
/** 服务端默认在命中词上包裹 <HighLightTag>,输出前剥离 */
|
|
75
|
+
function stripHighlightTags(value) {
|
|
76
|
+
return value.replace(/<\/?HighLightTag>/g, '');
|
|
77
|
+
}
|
|
13
78
|
class LTSClient {
|
|
14
79
|
client;
|
|
15
80
|
config;
|
|
@@ -30,32 +95,127 @@ class LTSClient {
|
|
|
30
95
|
this.client = builder.build();
|
|
31
96
|
}
|
|
32
97
|
async queryLogs(params) {
|
|
98
|
+
const { startMs, endMs } = resolveTimeRange(params);
|
|
33
99
|
const queryParams = new QueryLtsLogParams_1.QueryLtsLogParams()
|
|
34
|
-
.withStartTime(
|
|
35
|
-
.withEndTime(
|
|
36
|
-
.withLimit(params.limit
|
|
37
|
-
// 优先使用 query 参数(支持SQL和结构化查询)
|
|
100
|
+
.withStartTime(startMs.toString())
|
|
101
|
+
.withEndTime(endMs.toString())
|
|
102
|
+
.withLimit(Number.isFinite(params.limit) ? params.limit : 100);
|
|
38
103
|
if (params.query) {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
104
|
+
const parsed = parseQueryExpression(params.query);
|
|
105
|
+
if (parsed.keywords) {
|
|
106
|
+
queryParams.withKeywords(parsed.keywords);
|
|
107
|
+
}
|
|
108
|
+
if (parsed.query) {
|
|
109
|
+
queryParams.withQuery(parsed.query);
|
|
110
|
+
}
|
|
111
|
+
if (parsed.isAnalysisQuery) {
|
|
112
|
+
queryParams.withIsAnalysisQuery(true);
|
|
113
|
+
}
|
|
44
114
|
}
|
|
45
115
|
if (params.reverse !== undefined) {
|
|
46
116
|
queryParams.withIsDesc(params.reverse);
|
|
47
117
|
}
|
|
118
|
+
// 游标翻页:首屏不带游标,后续页把上一页末条的 line_num 当作续读起点
|
|
119
|
+
if (params.lineNum) {
|
|
120
|
+
queryParams.withLineNum(params.lineNum);
|
|
121
|
+
queryParams.withSearchType(params.searchType ?? (0, pagination_1.cursorDirection)(params.reverse));
|
|
122
|
+
}
|
|
48
123
|
const request = new ListLogsRequest_1.ListLogsRequest(params.groupId, params.streamId, 'application/json')
|
|
49
124
|
.withBody(queryParams);
|
|
50
125
|
const response = await this.retryRequest(() => this.client.listLogs(request));
|
|
126
|
+
const raw = response;
|
|
127
|
+
// SQL 分析查询:结果在 analysisLogs 中(行为 JSON 对象,没有 line_num,无法游标翻页)
|
|
128
|
+
if (Array.isArray(raw.analysisLogs)) {
|
|
129
|
+
const logs = raw.analysisLogs.map((row) => ({
|
|
130
|
+
lineNum: '',
|
|
131
|
+
content: JSON.stringify(row),
|
|
132
|
+
labels: {},
|
|
133
|
+
}));
|
|
134
|
+
return { logs, total: logs.length, isQueryComplete: raw.isQueryComplete };
|
|
135
|
+
}
|
|
51
136
|
const logs = (response.logs || []).map((log) => ({
|
|
52
|
-
|
|
137
|
+
lineNum: log.line_num ?? log.lineNum ?? '',
|
|
138
|
+
content: stripHighlightTags(log.content ?? ''),
|
|
139
|
+
labels: Object.fromEntries(Object.entries(log.labels ?? {}).map(([k, v]) => [k, stripHighlightTags(v)])),
|
|
140
|
+
}));
|
|
141
|
+
return {
|
|
142
|
+
logs,
|
|
143
|
+
total: response.count ?? logs.length,
|
|
144
|
+
// 服务端报告结果被截断时必须传出去,否则 AI 会拿着残缺数据下结论
|
|
145
|
+
isQueryComplete: raw.isQueryComplete,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
/** 查询某条日志的上下文(前后各 N 行),对应控制台的“查看上下文” */
|
|
149
|
+
async queryContext(params) {
|
|
150
|
+
const body = new ListLogContextRequestBody_1.ListLogContextRequestBody()
|
|
151
|
+
.withLineNum(params.lineNum)
|
|
152
|
+
.withTime(params.time)
|
|
153
|
+
.withBackwardsSize(params.before ?? 20)
|
|
154
|
+
.withForwardsSize(params.after ?? 20);
|
|
155
|
+
const request = new ListLogContextRequest_1.ListLogContextRequest(params.groupId, params.streamId, 'application/json')
|
|
156
|
+
.withBody(body);
|
|
157
|
+
const response = await this.retryRequest(() => this.client.listLogContext(request));
|
|
158
|
+
const logs = (response.logs || []).map((log) => ({
|
|
159
|
+
lineNum: log.line_num ?? log.lineNum ?? '',
|
|
53
160
|
content: log.content ?? '',
|
|
54
161
|
labels: log.labels ?? {},
|
|
55
162
|
}));
|
|
56
163
|
return {
|
|
57
164
|
logs,
|
|
58
|
-
|
|
165
|
+
// SDK 实际返回纯对象,字段为 snake_case
|
|
166
|
+
total: response.total_count ?? response.totalCount ?? logs.length,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
/** 列出全部日志组 */
|
|
170
|
+
async listGroups() {
|
|
171
|
+
const request = new ListLogGroupsRequest_1.ListLogGroupsRequest();
|
|
172
|
+
const response = await this.retryRequest(() => this.client.listLogGroups(request));
|
|
173
|
+
// SDK 实际返回纯对象,字段为 snake_case(log_groups)
|
|
174
|
+
const groups = (response.log_groups ?? []);
|
|
175
|
+
return groups.map((g) => ({
|
|
176
|
+
id: g.log_group_id ?? '',
|
|
177
|
+
name: g.log_group_name ?? '',
|
|
178
|
+
}));
|
|
179
|
+
}
|
|
180
|
+
/** 列出日志流,可按日志组名称过滤(API 使用组名而非组 ID) */
|
|
181
|
+
async listStreams(groupName) {
|
|
182
|
+
const request = new ListLogStreamsRequest_1.ListLogStreamsRequest('application/json');
|
|
183
|
+
if (groupName) {
|
|
184
|
+
request.withLogGroupName(groupName);
|
|
185
|
+
}
|
|
186
|
+
const response = await this.retryRequest(() => this.client.listLogStreams(request));
|
|
187
|
+
// SDK 实际返回纯对象,字段为 snake_case(log_streams)
|
|
188
|
+
const streams = (response.log_streams ?? []);
|
|
189
|
+
return streams.map((s) => ({
|
|
190
|
+
id: s.log_stream_id ?? '',
|
|
191
|
+
name: s.log_stream_name ?? '',
|
|
192
|
+
groupId: s.log_group_id,
|
|
193
|
+
}));
|
|
194
|
+
}
|
|
195
|
+
/** 读取日志流的索引配置(全文索引 + 字段索引),供 `fields` 命令与 AI 运行时发现 */
|
|
196
|
+
async getStreamIndex(groupId, streamId) {
|
|
197
|
+
const request = new ListLogStreamIndexRequest_1.ListLogStreamIndexRequest(groupId, streamId, 'application/json');
|
|
198
|
+
const response = await this.retryRequest(() => this.client.listLogStreamIndex(request));
|
|
199
|
+
const raw = response;
|
|
200
|
+
// SDK 在不同版本下可能给出 class 实例(camelCase)或纯对象(snake_case),两者都认
|
|
201
|
+
const fullText = raw.fullTextIndex ?? raw.full_text_index ?? {};
|
|
202
|
+
const fields = (raw.fields ?? []);
|
|
203
|
+
return {
|
|
204
|
+
streamId: raw.logStreamId ?? raw.log_stream_id ?? streamId,
|
|
205
|
+
fullText: {
|
|
206
|
+
enabled: fullText.enable === true,
|
|
207
|
+
caseSensitive: fullText.caseSensitive ?? fullText.case_sensitive,
|
|
208
|
+
includeChinese: fullText.includeChinese ?? fullText.include_chinese,
|
|
209
|
+
tokenizer: fullText.tokenizer,
|
|
210
|
+
},
|
|
211
|
+
fields: fields.map((f) => ({
|
|
212
|
+
name: f.fieldName ?? f.field_name ?? '',
|
|
213
|
+
type: f.fieldType ?? f.field_type ?? '',
|
|
214
|
+
caseSensitive: f.caseSensitive ?? f.case_sensitive,
|
|
215
|
+
includeChinese: f.includeChinese ?? f.include_chinese,
|
|
216
|
+
tokenizer: f.tokenizer,
|
|
217
|
+
quickAnalysis: f.quickAnalysis ?? f.quick_analysis,
|
|
218
|
+
})),
|
|
59
219
|
};
|
|
60
220
|
}
|
|
61
221
|
async retryRequest(fn, maxRetries = 3) {
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.cursorDirection = cursorDirection;
|
|
4
|
+
exports.nextCursor = nextCursor;
|
|
5
|
+
exports.fetchAllPages = fetchAllPages;
|
|
6
|
+
/** 倒序(新到旧)→ 往更早读;正序 → 往更晚读。 */
|
|
7
|
+
function cursorDirection(reverse) {
|
|
8
|
+
return reverse ? 'backwards' : 'forwards';
|
|
9
|
+
}
|
|
10
|
+
/** 游标取本页最后一条的 line_num;拿不到就返回 undefined(调用方据此停止)。 */
|
|
11
|
+
function nextCursor(logs) {
|
|
12
|
+
if (logs.length === 0)
|
|
13
|
+
return undefined;
|
|
14
|
+
const last = logs[logs.length - 1].lineNum;
|
|
15
|
+
return last ? last : undefined;
|
|
16
|
+
}
|
|
17
|
+
async function fetchAllPages(fetchPage, params, options = {}) {
|
|
18
|
+
const max = options.max ?? 10000;
|
|
19
|
+
const pageSize = options.pageSize ?? params.limit ?? 100;
|
|
20
|
+
const direction = cursorDirection(params.reverse);
|
|
21
|
+
const collected = [];
|
|
22
|
+
let cursor;
|
|
23
|
+
let isQueryComplete;
|
|
24
|
+
let truncated = false;
|
|
25
|
+
while (collected.length < max) {
|
|
26
|
+
const page = await fetchPage({
|
|
27
|
+
...params,
|
|
28
|
+
limit: pageSize,
|
|
29
|
+
lineNum: cursor,
|
|
30
|
+
searchType: cursor ? direction : undefined,
|
|
31
|
+
});
|
|
32
|
+
if (page.isQueryComplete === false) {
|
|
33
|
+
isQueryComplete = false;
|
|
34
|
+
}
|
|
35
|
+
else if (isQueryComplete === undefined && page.isQueryComplete === true) {
|
|
36
|
+
isQueryComplete = true;
|
|
37
|
+
}
|
|
38
|
+
if (page.logs.length === 0)
|
|
39
|
+
break;
|
|
40
|
+
const next = nextCursor(page.logs);
|
|
41
|
+
// 服务端把同一段原样又给了一遍:整页丢弃并停止。
|
|
42
|
+
// 先判游标再入账,否则重放的一页会被当成新数据拼进结果。
|
|
43
|
+
if (cursor !== undefined && next === cursor)
|
|
44
|
+
break;
|
|
45
|
+
collected.push(...page.logs);
|
|
46
|
+
// 不满一页 = 没有更多了;拿不到游标则无法继续翻页
|
|
47
|
+
if (page.logs.length < pageSize || !next)
|
|
48
|
+
break;
|
|
49
|
+
cursor = next;
|
|
50
|
+
if (collected.length >= max) {
|
|
51
|
+
truncated = true;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
const logs = collected.slice(0, max);
|
|
55
|
+
return { logs, total: logs.length, isQueryComplete, truncated };
|
|
56
|
+
}
|
package/dist/output/index.js
CHANGED
|
@@ -27,7 +27,7 @@ function formatPretty(data) {
|
|
|
27
27
|
lines.push(chalk_1.default.bold.green(`Found ${data.total} logs:`));
|
|
28
28
|
lines.push('');
|
|
29
29
|
for (const log of data.logs) {
|
|
30
|
-
lines.push(chalk_1.default.cyan(`[${log.
|
|
30
|
+
lines.push(chalk_1.default.cyan(`[${log.lineNum}]`));
|
|
31
31
|
lines.push(chalk_1.default.white(log.content));
|
|
32
32
|
if (Object.keys(log.labels).length > 0) {
|
|
33
33
|
const labels = Object.entries(log.labels)
|
package/dist/output/table.js
CHANGED
|
@@ -10,7 +10,7 @@ function formatTable(data) {
|
|
|
10
10
|
return 'No logs found.';
|
|
11
11
|
}
|
|
12
12
|
const table = new cli_table3_1.default({
|
|
13
|
-
head: ['
|
|
13
|
+
head: ['Line', 'Content', 'Labels'],
|
|
14
14
|
colWidths: [25, 50, 30],
|
|
15
15
|
wordWrap: true,
|
|
16
16
|
});
|
|
@@ -18,7 +18,7 @@ function formatTable(data) {
|
|
|
18
18
|
const labelsStr = Object.entries(log.labels)
|
|
19
19
|
.map(([k, v]) => `${k}=${v}`)
|
|
20
20
|
.join(', ');
|
|
21
|
-
table.push([log.
|
|
21
|
+
table.push([log.lineNum, log.content, labelsStr]);
|
|
22
22
|
}
|
|
23
23
|
return table.toString();
|
|
24
24
|
}
|