@cvtoolman/huawei-lts-cli 1.0.3 → 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 +12 -0
- package/dist/cli/commands/config.js +9 -2
- package/dist/cli/commands/fields.js +108 -0
- package/dist/cli/commands/init.js +21 -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/profile.js +108 -0
- 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 +11 -9
|
@@ -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' {
|
|
@@ -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
|
@@ -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++) {
|