@cvtoolman/huawei-lts-cli 1.0.3 → 2.1.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 +163 -145
- package/dist/cli/commands/common.js +12 -0
- package/dist/cli/commands/config.js +9 -2
- package/dist/cli/commands/fields.js +108 -0
- package/dist/cli/commands/init.js +25 -3
- package/dist/cli/commands/query.js +16 -35
- package/dist/cli/commands/streams.js +1 -1
- package/dist/cli/commands/trace.js +24 -20
- package/dist/cli/index.js +122 -14
- package/dist/config/store.js +7 -2
- package/dist/lts/client.js +44 -10
- package/dist/lts/pagination.js +56 -0
- package/dist/output/index.js +1 -1
- package/dist/output/table.js +2 -2
- package/package.json +12 -10
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.createQueryCommand = createQueryCommand;
|
|
4
4
|
const commander_1 = require("commander");
|
|
5
5
|
const client_1 = require("../../lts/client");
|
|
6
|
+
const pagination_1 = require("../../lts/pagination");
|
|
6
7
|
const output_1 = require("../../output");
|
|
7
8
|
const common_1 = require("./common");
|
|
8
9
|
// commander 的处理函数会收到 (value, previous),直接传 parseInt 会把 previous 当作进制
|
|
@@ -19,21 +20,18 @@ function createQueryCommand() {
|
|
|
19
20
|
.option('--st <startTime>', 'Alias for --start-time')
|
|
20
21
|
.option('--et <endTime>', 'Alias for --end-time')
|
|
21
22
|
.option('--last <duration>', 'Relative time range: 30m, 2h, 1d, 7d (mutually exclusive with --st/--et)')
|
|
22
|
-
.option('-
|
|
23
|
-
.option('-q, --query <query>', 'Advanced query expression. Supports:\n' +
|
|
23
|
+
.option('-q, --query <query>', 'Query expression. Supports:\n' +
|
|
24
24
|
' - Field queries: content:error AND appName:myapp\n' +
|
|
25
25
|
' - Comparison: time>60 AND region:r\n' +
|
|
26
|
-
' -
|
|
26
|
+
' - Pipeline analysis: error | SELECT count(*) AS c\n' +
|
|
27
27
|
' - Logical operators: AND, OR, NOT (must be surrounded by spaces)')
|
|
28
|
-
.option('--app <appName>', 'Filter by appName field
|
|
28
|
+
.option('--app <appName>', 'Filter by appName field, merged with -q via AND')
|
|
29
29
|
.option('--content <keyword>', 'Filter by log content (content:<keyword>), merged with -q via AND')
|
|
30
30
|
.option('--request-id <id>', 'Filter by REQUEST_ID field; output in chronological order')
|
|
31
|
-
.option('-l, --limit <limit>', '
|
|
32
|
-
.option('-o, --offset <offset>', 'Offset for pagination', parseIntOption, 0)
|
|
31
|
+
.option('-l, --limit <limit>', 'Page size (also the cap for --all)', parseIntOption, 100)
|
|
33
32
|
.option('-r, --reverse', 'Reverse order (newest first)')
|
|
34
33
|
.option('-f, --format <format>', 'Output format: json, table, pretty', 'pretty')
|
|
35
|
-
.option('--all', '
|
|
36
|
-
.option('--no-paginate', '(deprecated) Single page is the default; kept for compatibility')
|
|
34
|
+
.option('--all', 'Follow the cursor to the end (up to 10000 logs; default is a single page)')
|
|
37
35
|
.action(async (options) => {
|
|
38
36
|
const config = (0, common_1.loadCheckedConfig)();
|
|
39
37
|
const { groupId, streamId } = (0, common_1.resolveGroupStream)(options, config);
|
|
@@ -47,45 +45,28 @@ function createQueryCommand() {
|
|
|
47
45
|
groupId,
|
|
48
46
|
streamId,
|
|
49
47
|
...timeParams,
|
|
50
|
-
keyword: options.keyword,
|
|
51
48
|
query,
|
|
52
49
|
limit: options.limit,
|
|
53
|
-
offset: options.offset,
|
|
54
50
|
reverse,
|
|
55
51
|
};
|
|
56
52
|
try {
|
|
57
53
|
if (!options.all) {
|
|
58
54
|
// 默认单页查询,结果量由 --limit 控制
|
|
59
55
|
const result = await client.queryLogs(params);
|
|
56
|
+
(0, common_1.warnIfIncomplete)(result);
|
|
60
57
|
console.log((0, output_1.formatOutput)(result, format));
|
|
61
58
|
}
|
|
62
59
|
else {
|
|
63
|
-
// --all
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
...params,
|
|
72
|
-
offset,
|
|
73
|
-
limit,
|
|
74
|
-
};
|
|
75
|
-
const result = await client.queryLogs(pageParams);
|
|
76
|
-
allLogs.push(...result.logs);
|
|
77
|
-
if (result.logs.length < limit) {
|
|
78
|
-
hasMore = false;
|
|
79
|
-
}
|
|
80
|
-
else {
|
|
81
|
-
offset += limit;
|
|
82
|
-
}
|
|
60
|
+
// --all:沿游标读到末尾,上限 10000 条防止失控
|
|
61
|
+
const result = await (0, pagination_1.fetchAllPages)((p) => client.queryLogs(p), params, {
|
|
62
|
+
max: 10000,
|
|
63
|
+
pageSize: options.limit,
|
|
64
|
+
});
|
|
65
|
+
(0, common_1.warnIfIncomplete)(result);
|
|
66
|
+
if (result.truncated) {
|
|
67
|
+
console.error(`Warning: stopped at the ${result.logs.length}-log cap; more logs matched. Narrow the time range or add a filter.`);
|
|
83
68
|
}
|
|
84
|
-
|
|
85
|
-
logs: allLogs,
|
|
86
|
-
total: allLogs.length,
|
|
87
|
-
};
|
|
88
|
-
console.log((0, output_1.formatOutput)(result, format));
|
|
69
|
+
console.log((0, output_1.formatOutput)({ logs: result.logs, total: result.total }, format));
|
|
89
70
|
}
|
|
90
71
|
}
|
|
91
72
|
catch (error) {
|
|
@@ -64,7 +64,7 @@ function createGroupsCommand() {
|
|
|
64
64
|
function createStreamsCommand() {
|
|
65
65
|
const cmd = new commander_1.Command('streams');
|
|
66
66
|
cmd.description('List LTS log streams, optionally filtered by log group name')
|
|
67
|
-
.option('
|
|
67
|
+
.option('--group <name>', 'Filter by log group NAME (the API filters by name, not ID)')
|
|
68
68
|
.option('-f, --format <format>', 'Output format: json, pretty', 'pretty')
|
|
69
69
|
.action(async (options) => {
|
|
70
70
|
const config = (0, common_1.loadCheckedConfig)();
|
|
@@ -7,15 +7,25 @@ exports.createTraceCommand = createTraceCommand;
|
|
|
7
7
|
const commander_1 = require("commander");
|
|
8
8
|
const chalk_1 = __importDefault(require("chalk"));
|
|
9
9
|
const client_1 = require("../../lts/client");
|
|
10
|
+
const pagination_1 = require("../../lts/pagination");
|
|
10
11
|
const output_1 = require("../../output");
|
|
12
|
+
const config_1 = require("../../config");
|
|
11
13
|
const common_1 = require("./common");
|
|
14
|
+
/**
|
|
15
|
+
* 默认按全文索引检索,而不是 REQUEST_ID。
|
|
16
|
+
*
|
|
17
|
+
* 理由:`REQUEST_ID` 是某类应用的字段命名约定,不是 LTS 的通用概念。新用户的日志流
|
|
18
|
+
* 里往往没有这个字段,写死它会让 `trace` 在正确的用法下返回空。全文检索总能命中,
|
|
19
|
+
* 而用到专用字段的人可以在 config 里设一次 `traceField`。
|
|
20
|
+
*/
|
|
21
|
+
const DEFAULT_TRACE_FIELD = 'content';
|
|
12
22
|
function createTraceCommand() {
|
|
13
23
|
const cmd = new commander_1.Command('trace');
|
|
14
|
-
cmd.description('Trace a request by
|
|
15
|
-
.argument('<requestId>', '
|
|
24
|
+
cmd.description('Trace a request by ID — fetch the full request chain in chronological order')
|
|
25
|
+
.argument('<requestId>', 'Value to trace (request ID, trace ID, or any search text)')
|
|
16
26
|
.option('-g, --group-id <groupId>', 'Log group ID (can also be set via config)')
|
|
17
27
|
.option('-s, --stream-id <streamId>', 'Log stream ID (can also be set via config)')
|
|
18
|
-
.option('--field <name>',
|
|
28
|
+
.option('--field <name>', `Field to match. Defaults to config.traceField, then "${DEFAULT_TRACE_FIELD}"`)
|
|
19
29
|
.option('--last <duration>', 'Relative time range: 30m, 2h, 1d, 7d (default: 24h)')
|
|
20
30
|
.option('--start-time <startTime>', 'Start time (ISO 8601)')
|
|
21
31
|
.option('--end-time <endTime>', 'End time (ISO 8601)')
|
|
@@ -28,7 +38,7 @@ function createTraceCommand() {
|
|
|
28
38
|
const { groupId, streamId } = (0, common_1.resolveGroupStream)(options, config);
|
|
29
39
|
const timeParams = (0, common_1.resolveTimeParams)(options, '24h');
|
|
30
40
|
const format = (0, common_1.resolveFormat)(options.format);
|
|
31
|
-
const field = options.field;
|
|
41
|
+
const field = options.field || (0, config_1.getConfig)('traceField') || DEFAULT_TRACE_FIELD;
|
|
32
42
|
// 值加双引号:含连字符的 UUID 不加引号会被分词导致匹配不到
|
|
33
43
|
const query = `${field}:"${requestId}"`;
|
|
34
44
|
const client = new client_1.LTSClient(config);
|
|
@@ -40,24 +50,18 @@ function createTraceCommand() {
|
|
|
40
50
|
reverse: false, // 链路按时间正序
|
|
41
51
|
};
|
|
42
52
|
try {
|
|
43
|
-
// Auto-pagination up to --limit
|
|
44
|
-
const pageSize = Math.min(options.limit || 200, 500);
|
|
45
53
|
const maxTotal = options.limit || 200;
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
}
|
|
55
|
-
else {
|
|
56
|
-
offset += pageSize;
|
|
57
|
-
}
|
|
54
|
+
// 每页不超过 500:再大 LTS 会开始截断,且首屏延迟明显变差
|
|
55
|
+
const pageSize = Math.min(maxTotal, 500);
|
|
56
|
+
const result = await (0, pagination_1.fetchAllPages)((p) => client.queryLogs(p), params, {
|
|
57
|
+
max: maxTotal,
|
|
58
|
+
pageSize,
|
|
59
|
+
});
|
|
60
|
+
(0, common_1.warnIfIncomplete)(result);
|
|
61
|
+
if (result.truncated) {
|
|
62
|
+
console.error(`Warning: stopped at --limit ${maxTotal}; the chain may continue. Re-run with a larger --limit.`);
|
|
58
63
|
}
|
|
59
|
-
|
|
60
|
-
let output = (0, output_1.formatOutput)(result, format);
|
|
64
|
+
let output = (0, output_1.formatOutput)({ logs: result.logs, total: result.total }, format);
|
|
61
65
|
// pretty 格式下高亮 requestId,便于肉眼定位
|
|
62
66
|
if (format === 'pretty' && result.logs.length > 0) {
|
|
63
67
|
output = output.split(requestId).join(chalk_1.default.yellow.bold(requestId));
|
package/dist/cli/index.js
CHANGED
|
@@ -1,6 +1,41 @@
|
|
|
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");
|
|
6
41
|
const trace_1 = require("./commands/trace");
|
|
@@ -8,20 +43,38 @@ const context_1 = require("./commands/context");
|
|
|
8
43
|
const streams_1 = require("./commands/streams");
|
|
9
44
|
const config_1 = require("./commands/config");
|
|
10
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
|
+
}
|
|
11
63
|
const program = new commander_1.Command();
|
|
12
64
|
program
|
|
13
65
|
.name('lts-cli')
|
|
14
66
|
.description('Huawei Cloud LTS (Log Tank Service) CLI — query logs from the terminal')
|
|
15
|
-
.version(
|
|
67
|
+
.version(readVersion())
|
|
16
68
|
.addHelpText('after', `
|
|
17
69
|
Examples:
|
|
18
70
|
$ lts-cli init Setup credentials interactively
|
|
19
71
|
$ lts-cli init --from-url <url> Import region/groupId/streamId from a console URL
|
|
20
72
|
$ lts-cli config --list Show current configuration
|
|
21
|
-
$ lts-cli query --last 2h --app acerp-gateway --content "error"
|
|
22
|
-
$ lts-cli trace <requestId> Fetch full request chain by REQUEST_ID
|
|
23
73
|
$ lts-cli groups List log groups
|
|
24
|
-
$ lts-cli streams List log streams
|
|
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
|
|
25
78
|
|
|
26
79
|
Environment variables:
|
|
27
80
|
LTS_AK Access Key
|
|
@@ -38,7 +91,15 @@ program.addCommand((0, trace_1.createTraceCommand)());
|
|
|
38
91
|
program.addCommand((0, context_1.createContextCommand)());
|
|
39
92
|
program.addCommand((0, streams_1.createGroupsCommand)());
|
|
40
93
|
program.addCommand((0, streams_1.createStreamsCommand)());
|
|
94
|
+
program.addCommand((0, fields_1.createFieldsCommand)());
|
|
41
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';
|
|
42
103
|
// Shell completion support
|
|
43
104
|
const completionCmd = new commander_1.Command('completion')
|
|
44
105
|
.description('Generate shell completion script')
|
|
@@ -53,7 +114,7 @@ _lts_cli_completion() {
|
|
|
53
114
|
local cur="\${words[cword]}"
|
|
54
115
|
|
|
55
116
|
if (( cword == 1 )); then
|
|
56
|
-
COMPREPLY=( $(compgen -W "query trace context groups streams config init completion" -- "\$cur") )
|
|
117
|
+
COMPREPLY=( $(compgen -W "query trace context groups streams fields config init completion" -- "\$cur") )
|
|
57
118
|
return
|
|
58
119
|
fi
|
|
59
120
|
|
|
@@ -61,16 +122,32 @@ _lts_cli_completion() {
|
|
|
61
122
|
case "\$cmd" in
|
|
62
123
|
query)
|
|
63
124
|
case "\$prev" in
|
|
64
|
-
-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)
|
|
65
138
|
COMPREPLY=()
|
|
66
139
|
return
|
|
67
140
|
;;
|
|
68
141
|
*)
|
|
69
|
-
COMPREPLY=( $(compgen -W "
|
|
142
|
+
COMPREPLY=( $(compgen -W "${TRACE_OPTS}" -- "\$cur") )
|
|
70
143
|
return
|
|
71
144
|
;;
|
|
72
145
|
esac
|
|
73
146
|
;;
|
|
147
|
+
fields)
|
|
148
|
+
COMPREPLY=( $(compgen -W "${VALUE_OPTS}" -- "\$cur") )
|
|
149
|
+
return
|
|
150
|
+
;;
|
|
74
151
|
config)
|
|
75
152
|
COMPREPLY=( $(compgen -W "--set --get --list --unset" -- "\$cur") )
|
|
76
153
|
return
|
|
@@ -94,10 +171,11 @@ _lts_cli() {
|
|
|
94
171
|
local -a commands
|
|
95
172
|
commands=(
|
|
96
173
|
'query:Query LTS logs'
|
|
97
|
-
'trace:Trace request
|
|
174
|
+
'trace:Trace a request by ID'
|
|
98
175
|
'context:View logs before/after a log entry'
|
|
99
176
|
'groups:List log groups'
|
|
100
177
|
'streams:List log streams'
|
|
178
|
+
'fields:Show the index config of a log stream'
|
|
101
179
|
'config:Manage configuration'
|
|
102
180
|
'init:Interactive setup wizard'
|
|
103
181
|
'completion:Generate shell completion script'
|
|
@@ -124,12 +202,34 @@ _lts_cli() {
|
|
|
124
202
|
'--end-time[End time (ISO 8601)]:time:' \\
|
|
125
203
|
'--st[Alias for --start-time]:time:' \\
|
|
126
204
|
'--et[Alias for --end-time]:time:' \\
|
|
127
|
-
'
|
|
128
|
-
'-
|
|
129
|
-
'
|
|
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:' \\
|
|
130
211
|
'-r[Reverse order]' \\
|
|
131
212
|
'-f[Output format]:format:(json table pretty)' \\
|
|
132
|
-
'--
|
|
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)'
|
|
133
233
|
;;
|
|
134
234
|
config)
|
|
135
235
|
_arguments \\
|
|
@@ -152,7 +252,7 @@ compdef _lts_cli lts-cli`);
|
|
|
152
252
|
Register-ArgumentCompleter -CommandName lts-cli -ScriptBlock {
|
|
153
253
|
param(\$wordToComplete, \$commandAst, \$cursorPosition)
|
|
154
254
|
|
|
155
|
-
\$commands = @('query', 'trace', 'context', 'groups', 'streams', 'config', 'init', 'completion')
|
|
255
|
+
\$commands = @('query', 'trace', 'context', 'groups', 'streams', 'fields', 'config', 'init', 'completion')
|
|
156
256
|
|
|
157
257
|
\$args = \$commandAst.ToString() -split '\\s+'
|
|
158
258
|
if (\$args.Count -le 1) {
|
|
@@ -161,7 +261,15 @@ Register-ArgumentCompleter -CommandName lts-cli -ScriptBlock {
|
|
|
161
261
|
|
|
162
262
|
switch (\$args[1]) {
|
|
163
263
|
'query' {
|
|
164
|
-
\$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')
|
|
165
273
|
return \$opts | Where-Object { \$_ -like "\$wordToComplete*" }
|
|
166
274
|
}
|
|
167
275
|
'config' {
|
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
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.LTSClient = void 0;
|
|
4
4
|
exports.resolveTimeRange = resolveTimeRange;
|
|
5
|
+
exports.parseQueryExpression = parseQueryExpression;
|
|
5
6
|
const LtsClient_1 = require("@huaweicloud/huaweicloud-sdk-lts/v2/LtsClient");
|
|
6
7
|
const ClientBuilder_1 = require("@huaweicloud/huaweicloud-sdk-core/ClientBuilder");
|
|
7
8
|
const BasicCredentials_1 = require("@huaweicloud/huaweicloud-sdk-core/auth/BasicCredentials");
|
|
@@ -12,6 +13,8 @@ const ListLogContextRequest_1 = require("@huaweicloud/huaweicloud-sdk-lts/v2/mod
|
|
|
12
13
|
const ListLogContextRequestBody_1 = require("@huaweicloud/huaweicloud-sdk-lts/v2/model/ListLogContextRequestBody");
|
|
13
14
|
const ListLogGroupsRequest_1 = require("@huaweicloud/huaweicloud-sdk-lts/v2/model/ListLogGroupsRequest");
|
|
14
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");
|
|
15
18
|
function sleep(ms) {
|
|
16
19
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
17
20
|
}
|
|
@@ -47,6 +50,9 @@ function resolveTimeRange(params) {
|
|
|
47
50
|
* - "搜索语句 | SELECT ..." → keywords=搜索语句, query=SQL, is_analysis_query=true
|
|
48
51
|
* - 裸 "SELECT ..." → query=SQL, is_analysis_query=true
|
|
49
52
|
* - 其他(field:value 等) → keywords=搜索语句
|
|
53
|
+
*
|
|
54
|
+
* 导出仅为测试:这一段决定了用户写的字符串怎么落到 keywords/query 两个字段上,
|
|
55
|
+
* 拆错了就会静默返回全量日志。
|
|
50
56
|
*/
|
|
51
57
|
function parseQueryExpression(expr) {
|
|
52
58
|
const trimmed = expr.trim();
|
|
@@ -94,7 +100,6 @@ class LTSClient {
|
|
|
94
100
|
.withStartTime(startMs.toString())
|
|
95
101
|
.withEndTime(endMs.toString())
|
|
96
102
|
.withLimit(Number.isFinite(params.limit) ? params.limit : 100);
|
|
97
|
-
// 优先使用 query 参数(支持SQL和结构化查询)
|
|
98
103
|
if (params.query) {
|
|
99
104
|
const parsed = parseQueryExpression(params.query);
|
|
100
105
|
if (parsed.keywords) {
|
|
@@ -107,34 +112,37 @@ class LTSClient {
|
|
|
107
112
|
queryParams.withIsAnalysisQuery(true);
|
|
108
113
|
}
|
|
109
114
|
}
|
|
110
|
-
else if (params.keyword) {
|
|
111
|
-
// 向后兼容:简单关键词搜索
|
|
112
|
-
queryParams.withKeywords(params.keyword);
|
|
113
|
-
}
|
|
114
115
|
if (params.reverse !== undefined) {
|
|
115
116
|
queryParams.withIsDesc(params.reverse);
|
|
116
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
|
+
}
|
|
117
123
|
const request = new ListLogsRequest_1.ListLogsRequest(params.groupId, params.streamId, 'application/json')
|
|
118
124
|
.withBody(queryParams);
|
|
119
125
|
const response = await this.retryRequest(() => this.client.listLogs(request));
|
|
120
126
|
const raw = response;
|
|
121
|
-
// SQL 分析查询:结果在 analysisLogs 中(行为 JSON
|
|
127
|
+
// SQL 分析查询:结果在 analysisLogs 中(行为 JSON 对象,没有 line_num,无法游标翻页)
|
|
122
128
|
if (Array.isArray(raw.analysisLogs)) {
|
|
123
129
|
const logs = raw.analysisLogs.map((row) => ({
|
|
124
|
-
|
|
130
|
+
lineNum: '',
|
|
125
131
|
content: JSON.stringify(row),
|
|
126
132
|
labels: {},
|
|
127
133
|
}));
|
|
128
|
-
return { logs, total: logs.length };
|
|
134
|
+
return { logs, total: logs.length, isQueryComplete: raw.isQueryComplete };
|
|
129
135
|
}
|
|
130
136
|
const logs = (response.logs || []).map((log) => ({
|
|
131
|
-
|
|
137
|
+
lineNum: log.line_num ?? log.lineNum ?? '',
|
|
132
138
|
content: stripHighlightTags(log.content ?? ''),
|
|
133
139
|
labels: Object.fromEntries(Object.entries(log.labels ?? {}).map(([k, v]) => [k, stripHighlightTags(v)])),
|
|
134
140
|
}));
|
|
135
141
|
return {
|
|
136
142
|
logs,
|
|
137
143
|
total: response.count ?? logs.length,
|
|
144
|
+
// 服务端报告结果被截断时必须传出去,否则 AI 会拿着残缺数据下结论
|
|
145
|
+
isQueryComplete: raw.isQueryComplete,
|
|
138
146
|
};
|
|
139
147
|
}
|
|
140
148
|
/** 查询某条日志的上下文(前后各 N 行),对应控制台的“查看上下文” */
|
|
@@ -148,7 +156,7 @@ class LTSClient {
|
|
|
148
156
|
.withBody(body);
|
|
149
157
|
const response = await this.retryRequest(() => this.client.listLogContext(request));
|
|
150
158
|
const logs = (response.logs || []).map((log) => ({
|
|
151
|
-
|
|
159
|
+
lineNum: log.line_num ?? log.lineNum ?? '',
|
|
152
160
|
content: log.content ?? '',
|
|
153
161
|
labels: log.labels ?? {},
|
|
154
162
|
}));
|
|
@@ -184,6 +192,32 @@ class LTSClient {
|
|
|
184
192
|
groupId: s.log_group_id,
|
|
185
193
|
}));
|
|
186
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
|
+
})),
|
|
219
|
+
};
|
|
220
|
+
}
|
|
187
221
|
async retryRequest(fn, maxRetries = 3) {
|
|
188
222
|
let lastError;
|
|
189
223
|
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
@@ -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
|
}
|