@cvtoolman/huawei-lts-cli 1.0.2 → 1.0.3
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/dist/cli/commands/common.js +99 -0
- package/dist/cli/commands/context.js +40 -0
- package/dist/cli/commands/init.js +56 -1
- package/dist/cli/commands/query.js +31 -67
- package/dist/cli/commands/streams.js +81 -0
- package/dist/cli/commands/trace.js +72 -0
- package/dist/cli/index.js +18 -3
- package/dist/lts/client.js +132 -6
- package/package.json +1 -1
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.loadCheckedConfig = loadCheckedConfig;
|
|
4
|
+
exports.resolveGroupStream = resolveGroupStream;
|
|
5
|
+
exports.resolveTimeParams = resolveTimeParams;
|
|
6
|
+
exports.buildQueryExpression = buildQueryExpression;
|
|
7
|
+
exports.resolveFormat = resolveFormat;
|
|
8
|
+
exports.handleCommandError = handleCommandError;
|
|
9
|
+
const config_1 = require("../../config");
|
|
10
|
+
/** 加载配置并校验凭证,缺失时打印引导并退出 */
|
|
11
|
+
function loadCheckedConfig() {
|
|
12
|
+
const config = (0, config_1.loadConfig)();
|
|
13
|
+
if (!config.ak || !config.sk || !config.projectId) {
|
|
14
|
+
console.error('Error: Missing required credentials. Please configure them first:');
|
|
15
|
+
console.error('');
|
|
16
|
+
console.error(' Quick setup (recommended):');
|
|
17
|
+
console.error(' lts-cli init');
|
|
18
|
+
console.error('');
|
|
19
|
+
console.error(' Or set individually:');
|
|
20
|
+
console.error(' lts-cli config --set ak=<your-access-key>');
|
|
21
|
+
console.error(' lts-cli config --set sk=<your-secret-key>');
|
|
22
|
+
console.error(' lts-cli config --set projectId=<your-project-id>');
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
return config;
|
|
26
|
+
}
|
|
27
|
+
/** groupId/streamId:CLI 参数优先,其次配置默认值,都没有则报错退出 */
|
|
28
|
+
function resolveGroupStream(options, config) {
|
|
29
|
+
const groupId = options.groupId || config.groupId;
|
|
30
|
+
const streamId = options.streamId || config.streamId;
|
|
31
|
+
if (!groupId || !streamId) {
|
|
32
|
+
console.error('Error: groupId and streamId are required. Provide them via:');
|
|
33
|
+
console.error(' lts-cli config --set groupId=<your-group-id>');
|
|
34
|
+
console.error(' lts-cli config --set streamId=<your-stream-id>');
|
|
35
|
+
console.error(' or use -g <groupId> -s <streamId> flags');
|
|
36
|
+
console.error(' Tip: run "lts-cli groups" / "lts-cli streams" to discover IDs');
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
return { groupId, streamId };
|
|
40
|
+
}
|
|
41
|
+
/** 解析时间选项:--last 与 --st/--et 互斥,至少提供一组 */
|
|
42
|
+
function resolveTimeParams(options, defaultLast) {
|
|
43
|
+
const startTime = options.startTime || options.st;
|
|
44
|
+
const endTime = options.endTime || options.et;
|
|
45
|
+
const last = options.last || (!startTime && !endTime ? defaultLast : undefined);
|
|
46
|
+
if (last && (startTime || endTime)) {
|
|
47
|
+
console.error('Error: --last is mutually exclusive with --start-time/--end-time. Provide only one style.');
|
|
48
|
+
process.exit(1);
|
|
49
|
+
}
|
|
50
|
+
if (!last && (!startTime || !endTime)) {
|
|
51
|
+
console.error('Error: time range required. Use --last (e.g. --last 2h) or both --start-time and --end-time.');
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
54
|
+
return last ? { last } : { startTime, endTime };
|
|
55
|
+
}
|
|
56
|
+
/** 将 -q 表达式与便捷筛选 flag 合并为一个查询表达式(AND 连接)
|
|
57
|
+
* 字段值统一加双引号:含连字符等分词符的值(如 UUID)不加引号会匹配不到 */
|
|
58
|
+
function buildQueryExpression(options) {
|
|
59
|
+
const parts = [];
|
|
60
|
+
if (options.query) {
|
|
61
|
+
parts.push(options.query);
|
|
62
|
+
}
|
|
63
|
+
if (options.app) {
|
|
64
|
+
parts.push(`appName:"${options.app}"`);
|
|
65
|
+
}
|
|
66
|
+
if (options.content) {
|
|
67
|
+
parts.push(`content:"${options.content}"`);
|
|
68
|
+
}
|
|
69
|
+
if (options.requestId) {
|
|
70
|
+
parts.push(`REQUEST_ID:"${options.requestId}"`);
|
|
71
|
+
}
|
|
72
|
+
return parts.length > 0 ? parts.join(' AND ') : undefined;
|
|
73
|
+
}
|
|
74
|
+
/** 解析输出格式,非法值回退 pretty */
|
|
75
|
+
function resolveFormat(value) {
|
|
76
|
+
return ['json', 'table', 'pretty'].includes(value) ? value : 'pretty';
|
|
77
|
+
}
|
|
78
|
+
/** 统一错误处理:常见错误给出友好提示 */
|
|
79
|
+
function handleCommandError(error) {
|
|
80
|
+
const msg = error.message || String(error);
|
|
81
|
+
if (msg.includes('Invalid URL')) {
|
|
82
|
+
console.error('Error: Invalid endpoint URL. Make sure it starts with https://');
|
|
83
|
+
console.error(' Example: https://lts.cn-south-1.myhuaweicloud.com');
|
|
84
|
+
}
|
|
85
|
+
else if (msg.includes('Authentication failed') || msg.includes('401') || msg.includes('403')) {
|
|
86
|
+
console.error('Error: Authentication failed. Please check your AK/SK credentials:');
|
|
87
|
+
console.error(' lts-cli config --list');
|
|
88
|
+
}
|
|
89
|
+
else if (msg.includes('ENOTFOUND') || msg.includes('ECONNREFUSED')) {
|
|
90
|
+
console.error('Error: Cannot reach the LTS service. Please check your endpoint and network connection.');
|
|
91
|
+
}
|
|
92
|
+
else if (msg.includes('Request failed after retries')) {
|
|
93
|
+
console.error('Error: The service is temporarily unavailable. Please try again later.');
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
console.error('Error:', msg);
|
|
97
|
+
}
|
|
98
|
+
process.exit(1);
|
|
99
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createContextCommand = createContextCommand;
|
|
4
|
+
const commander_1 = require("commander");
|
|
5
|
+
const client_1 = require("../../lts/client");
|
|
6
|
+
const output_1 = require("../../output");
|
|
7
|
+
const common_1 = require("./common");
|
|
8
|
+
function createContextCommand() {
|
|
9
|
+
const cmd = new commander_1.Command('context');
|
|
10
|
+
cmd.description('View logs before/after a specific log entry (like "View Context" in the console)')
|
|
11
|
+
.option('-g, --group-id <groupId>', 'Log group ID (can also be set via config)')
|
|
12
|
+
.option('-s, --stream-id <streamId>', 'Log stream ID (can also be set via config)')
|
|
13
|
+
.requiredOption('--line-num <lineNum>', 'line_num of the anchor log entry')
|
|
14
|
+
.requiredOption('--time <time>', 'Timestamp (ms) of the anchor log entry')
|
|
15
|
+
.option('--before <count>', 'Number of logs before the anchor', (v) => parseInt(v, 10), 20)
|
|
16
|
+
.option('--after <count>', 'Number of logs after the anchor', (v) => parseInt(v, 10), 20)
|
|
17
|
+
.option('-f, --format <format>', 'Output format: json, table, pretty', 'pretty')
|
|
18
|
+
.action(async (options) => {
|
|
19
|
+
const config = (0, common_1.loadCheckedConfig)();
|
|
20
|
+
const { groupId, streamId } = (0, common_1.resolveGroupStream)(options, config);
|
|
21
|
+
const format = (0, common_1.resolveFormat)(options.format);
|
|
22
|
+
const client = new client_1.LTSClient(config);
|
|
23
|
+
const params = {
|
|
24
|
+
groupId,
|
|
25
|
+
streamId,
|
|
26
|
+
lineNum: options.lineNum,
|
|
27
|
+
time: options.time,
|
|
28
|
+
before: options.before,
|
|
29
|
+
after: options.after,
|
|
30
|
+
};
|
|
31
|
+
try {
|
|
32
|
+
const result = await client.queryContext(params);
|
|
33
|
+
console.log((0, output_1.formatOutput)(result, format));
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
(0, common_1.handleCommandError)(error);
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
return cmd;
|
|
40
|
+
}
|
|
@@ -67,7 +67,13 @@ const ENDPOINT_MAP = {
|
|
|
67
67
|
function createInitCommand() {
|
|
68
68
|
const cmd = new commander_1.Command('init');
|
|
69
69
|
cmd.description('Interactive setup wizard — configure credentials step by step')
|
|
70
|
-
.
|
|
70
|
+
.option('--from-url <url>', 'Import region/groupId/streamId from an LTS console URL (non-interactive)')
|
|
71
|
+
.action(async (options) => {
|
|
72
|
+
// 从控制台 URL 导入:解析 region、groupId、topicId(即 streamId),无需交互
|
|
73
|
+
if (options.fromUrl) {
|
|
74
|
+
importFromConsoleUrl(options.fromUrl);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
71
77
|
const rl = readline.createInterface({
|
|
72
78
|
input: process.stdin,
|
|
73
79
|
output: process.stdout,
|
|
@@ -154,3 +160,52 @@ function createInitCommand() {
|
|
|
154
160
|
});
|
|
155
161
|
return cmd;
|
|
156
162
|
}
|
|
163
|
+
/** 从华为云 LTS 控制台 URL 中解析 region / groupId / streamId 并写入配置 */
|
|
164
|
+
function importFromConsoleUrl(raw) {
|
|
165
|
+
let url;
|
|
166
|
+
try {
|
|
167
|
+
url = new URL(raw);
|
|
168
|
+
}
|
|
169
|
+
catch {
|
|
170
|
+
console.error('Error: Invalid URL. Please paste the full console URL, e.g.:');
|
|
171
|
+
console.error(' https://console.huaweicloud.com/lts/?region=cn-south-1#/cts/logEventsLeftMenu/events?groupId=...&topicId=...');
|
|
172
|
+
process.exit(1);
|
|
173
|
+
}
|
|
174
|
+
const region = url.searchParams.get('region') || undefined;
|
|
175
|
+
// groupId / topicId 位于 hash 段的查询参数中:#/cts/logEventsLeftMenu/events?groupId=...&topicId=...
|
|
176
|
+
const hash = url.hash || '';
|
|
177
|
+
const qIndex = hash.indexOf('?');
|
|
178
|
+
const hashParams = new URLSearchParams(qIndex >= 0 ? hash.slice(qIndex + 1) : '');
|
|
179
|
+
const groupId = hashParams.get('groupId') || url.searchParams.get('groupId') || undefined;
|
|
180
|
+
const streamId = hashParams.get('topicId') || hashParams.get('streamId') || url.searchParams.get('topicId') || undefined;
|
|
181
|
+
if (!region && !groupId && !streamId) {
|
|
182
|
+
console.error('Error: No region/groupId/topicId found in the URL. Make sure it is an LTS log search page URL.');
|
|
183
|
+
process.exit(1);
|
|
184
|
+
}
|
|
185
|
+
console.log('');
|
|
186
|
+
console.log('Importing from console URL:');
|
|
187
|
+
if (region) {
|
|
188
|
+
(0, config_1.setConfig)('region', region);
|
|
189
|
+
console.log(` region = ${region}`);
|
|
190
|
+
const autoEndpoint = ENDPOINT_MAP[region];
|
|
191
|
+
if (autoEndpoint) {
|
|
192
|
+
(0, config_1.setConfig)('endpoint', `https://${autoEndpoint}`);
|
|
193
|
+
console.log(` endpoint = https://${autoEndpoint} (derived from region)`);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
if (groupId) {
|
|
197
|
+
(0, config_1.setConfig)('groupId', groupId);
|
|
198
|
+
console.log(` groupId = ${groupId}`);
|
|
199
|
+
}
|
|
200
|
+
if (streamId) {
|
|
201
|
+
(0, config_1.setConfig)('streamId', streamId);
|
|
202
|
+
console.log(` streamId = ${streamId}`);
|
|
203
|
+
}
|
|
204
|
+
console.log('');
|
|
205
|
+
console.log('✅ Configuration saved!');
|
|
206
|
+
console.log('');
|
|
207
|
+
console.log('Next steps:');
|
|
208
|
+
console.log(' 1. Configure credentials if not done yet: lts-cli init');
|
|
209
|
+
console.log(' 2. Query logs: lts-cli query --last 2h --content "error"');
|
|
210
|
+
console.log('');
|
|
211
|
+
}
|
|
@@ -3,87 +3,70 @@ 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 config_1 = require("../../config");
|
|
7
6
|
const output_1 = require("../../output");
|
|
7
|
+
const common_1 = require("./common");
|
|
8
|
+
// commander 的处理函数会收到 (value, previous),直接传 parseInt 会把 previous 当作进制
|
|
9
|
+
function parseIntOption(value) {
|
|
10
|
+
return parseInt(value, 10);
|
|
11
|
+
}
|
|
8
12
|
function createQueryCommand() {
|
|
9
13
|
const cmd = new commander_1.Command('query');
|
|
10
14
|
cmd.description('Query LTS logs')
|
|
11
15
|
.option('-g, --group-id <groupId>', 'Log group ID (can also be set via config)')
|
|
12
16
|
.option('-s, --stream-id <streamId>', 'Log stream ID (can also be set via config)')
|
|
13
|
-
.
|
|
14
|
-
.
|
|
17
|
+
.option('--start-time <startTime>', 'Start time (ISO 8601, e.g. 2024-01-01T00:00:00Z)')
|
|
18
|
+
.option('--end-time <endTime>', 'End time (ISO 8601, e.g. 2024-01-02T00:00:00Z)')
|
|
15
19
|
.option('--st <startTime>', 'Alias for --start-time')
|
|
16
20
|
.option('--et <endTime>', 'Alias for --end-time')
|
|
21
|
+
.option('--last <duration>', 'Relative time range: 30m, 2h, 1d, 7d (mutually exclusive with --st/--et)')
|
|
17
22
|
.option('-k, --keyword <keyword>', 'Simple keyword search (legacy, use --query for advanced queries)')
|
|
18
23
|
.option('-q, --query <query>', 'Advanced query expression. Supports:\n' +
|
|
19
24
|
' - Field queries: content:error AND appName:myapp\n' +
|
|
20
25
|
' - Comparison: time>60 AND region:r\n' +
|
|
21
26
|
' - SQL syntax: SELECT * FROM log WHERE content LIKE \'%error%\'\n' +
|
|
22
27
|
' - Logical operators: AND, OR, NOT (must be surrounded by spaces)')
|
|
23
|
-
.option('
|
|
24
|
-
.option('
|
|
28
|
+
.option('--app <appName>', 'Filter by appName field (e.g. --app acerp-gateway), merged with -q via AND')
|
|
29
|
+
.option('--content <keyword>', 'Filter by log content (content:<keyword>), merged with -q via AND')
|
|
30
|
+
.option('--request-id <id>', 'Filter by REQUEST_ID field; output in chronological order')
|
|
31
|
+
.option('-l, --limit <limit>', 'Limit number of results', parseIntOption, 100)
|
|
32
|
+
.option('-o, --offset <offset>', 'Offset for pagination', parseIntOption, 0)
|
|
25
33
|
.option('-r, --reverse', 'Reverse order (newest first)')
|
|
26
34
|
.option('-f, --format <format>', 'Output format: json, table, pretty', 'pretty')
|
|
27
|
-
.option('--
|
|
35
|
+
.option('--all', 'Fetch all pages (up to 10000 logs; default is single page)')
|
|
36
|
+
.option('--no-paginate', '(deprecated) Single page is the default; kept for compatibility')
|
|
28
37
|
.action(async (options) => {
|
|
29
|
-
const config = (0,
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
console.error('');
|
|
37
|
-
console.error(' Or set individually:');
|
|
38
|
-
console.error(' lts-cli config --set ak=<your-access-key>');
|
|
39
|
-
console.error(' lts-cli config --set sk=<your-secret-key>');
|
|
40
|
-
console.error(' lts-cli config --set projectId=<your-project-id>');
|
|
41
|
-
process.exit(1);
|
|
42
|
-
}
|
|
43
|
-
// groupId & streamId: CLI args take priority over config
|
|
44
|
-
const groupId = options.groupId || config.groupId;
|
|
45
|
-
const streamId = options.streamId || config.streamId;
|
|
46
|
-
if (!groupId || !streamId) {
|
|
47
|
-
console.error('Error: groupId and streamId are required. Provide them via:');
|
|
48
|
-
console.error(' lts-cli config --set groupId=<your-group-id>');
|
|
49
|
-
console.error(' lts-cli config --set streamId=<your-stream-id>');
|
|
50
|
-
console.error(' or use -g <groupId> -s <streamId> flags');
|
|
51
|
-
process.exit(1);
|
|
52
|
-
}
|
|
53
|
-
// --st / --et are aliases for --start-time / --end-time
|
|
54
|
-
const startTime = options.startTime || options.st;
|
|
55
|
-
const endTime = options.endTime || options.et;
|
|
56
|
-
if (!startTime || !endTime) {
|
|
57
|
-
console.error('Error: --start-time and --end-time are required.');
|
|
58
|
-
process.exit(1);
|
|
59
|
-
}
|
|
60
|
-
const format = ['json', 'table', 'pretty'].includes(options.format)
|
|
61
|
-
? options.format
|
|
62
|
-
: 'pretty';
|
|
38
|
+
const config = (0, common_1.loadCheckedConfig)();
|
|
39
|
+
const { groupId, streamId } = (0, common_1.resolveGroupStream)(options, config);
|
|
40
|
+
const timeParams = (0, common_1.resolveTimeParams)(options);
|
|
41
|
+
const query = (0, common_1.buildQueryExpression)(options);
|
|
42
|
+
const format = (0, common_1.resolveFormat)(options.format);
|
|
43
|
+
// --request-id 用于链路定位,按时间正序输出
|
|
44
|
+
const reverse = options.requestId ? false : options.reverse;
|
|
63
45
|
const client = new client_1.LTSClient(config);
|
|
64
46
|
const params = {
|
|
65
47
|
groupId,
|
|
66
48
|
streamId,
|
|
67
|
-
|
|
68
|
-
endTime,
|
|
49
|
+
...timeParams,
|
|
69
50
|
keyword: options.keyword,
|
|
70
|
-
query
|
|
51
|
+
query,
|
|
71
52
|
limit: options.limit,
|
|
72
53
|
offset: options.offset,
|
|
73
|
-
reverse
|
|
54
|
+
reverse,
|
|
74
55
|
};
|
|
75
56
|
try {
|
|
76
|
-
if (options.
|
|
57
|
+
if (!options.all) {
|
|
58
|
+
// 默认单页查询,结果量由 --limit 控制
|
|
77
59
|
const result = await client.queryLogs(params);
|
|
78
60
|
console.log((0, output_1.formatOutput)(result, format));
|
|
79
61
|
}
|
|
80
62
|
else {
|
|
81
|
-
//
|
|
63
|
+
// --all:自动翻页拉取全部日志,上限 10000 条防止失控
|
|
64
|
+
const MAX_LOGS = 10000;
|
|
82
65
|
const allLogs = [];
|
|
83
66
|
let offset = options.offset || 0;
|
|
84
67
|
const limit = options.limit || 100;
|
|
85
68
|
let hasMore = true;
|
|
86
|
-
while (hasMore) {
|
|
69
|
+
while (hasMore && allLogs.length < MAX_LOGS) {
|
|
87
70
|
const pageParams = {
|
|
88
71
|
...params,
|
|
89
72
|
offset,
|
|
@@ -106,26 +89,7 @@ function createQueryCommand() {
|
|
|
106
89
|
}
|
|
107
90
|
}
|
|
108
91
|
catch (error) {
|
|
109
|
-
|
|
110
|
-
// Friendlier error messages for common issues
|
|
111
|
-
if (msg.includes('Invalid URL')) {
|
|
112
|
-
console.error('Error: Invalid endpoint URL. Make sure it starts with https://');
|
|
113
|
-
console.error(' Example: https://lts.cn-south-1.myhuaweicloud.com');
|
|
114
|
-
}
|
|
115
|
-
else if (msg.includes('Authentication failed') || msg.includes('401') || msg.includes('403')) {
|
|
116
|
-
console.error('Error: Authentication failed. Please check your AK/SK credentials:');
|
|
117
|
-
console.error(' lts-cli config --list');
|
|
118
|
-
}
|
|
119
|
-
else if (msg.includes('ENOTFOUND') || msg.includes('ECONNREFUSED')) {
|
|
120
|
-
console.error('Error: Cannot reach the LTS service. Please check your endpoint and network connection.');
|
|
121
|
-
}
|
|
122
|
-
else if (msg.includes('Request failed after retries')) {
|
|
123
|
-
console.error('Error: The service is temporarily unavailable. Please try again later.');
|
|
124
|
-
}
|
|
125
|
-
else {
|
|
126
|
-
console.error('Error querying logs:', msg);
|
|
127
|
-
}
|
|
128
|
-
process.exit(1);
|
|
92
|
+
(0, common_1.handleCommandError)(error);
|
|
129
93
|
}
|
|
130
94
|
});
|
|
131
95
|
return cmd;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.createGroupsCommand = createGroupsCommand;
|
|
7
|
+
exports.createStreamsCommand = createStreamsCommand;
|
|
8
|
+
const commander_1 = require("commander");
|
|
9
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
10
|
+
const client_1 = require("../../lts/client");
|
|
11
|
+
const common_1 = require("./common");
|
|
12
|
+
function printGroups(groups, format) {
|
|
13
|
+
if (format === 'json') {
|
|
14
|
+
console.log(JSON.stringify(groups, null, 2));
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
if (groups.length === 0) {
|
|
18
|
+
console.log(chalk_1.default.yellow('No log groups found.'));
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
console.log(chalk_1.default.bold.green(`Found ${groups.length} log groups:`));
|
|
22
|
+
console.log('');
|
|
23
|
+
for (const g of groups) {
|
|
24
|
+
console.log(` ${chalk_1.default.cyan(g.name)} ${chalk_1.default.gray(g.id)}`);
|
|
25
|
+
}
|
|
26
|
+
console.log('');
|
|
27
|
+
console.log(chalk_1.default.gray('Set default: lts-cli config --set groupId=<id>'));
|
|
28
|
+
}
|
|
29
|
+
function printStreams(streams, format) {
|
|
30
|
+
if (format === 'json') {
|
|
31
|
+
console.log(JSON.stringify(streams, null, 2));
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
if (streams.length === 0) {
|
|
35
|
+
console.log(chalk_1.default.yellow('No log streams found.'));
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
console.log(chalk_1.default.bold.green(`Found ${streams.length} log streams:`));
|
|
39
|
+
console.log('');
|
|
40
|
+
for (const s of streams) {
|
|
41
|
+
const group = s.groupId ? chalk_1.default.gray(` group: ${s.groupId}`) : '';
|
|
42
|
+
console.log(` ${chalk_1.default.cyan(s.name)} ${chalk_1.default.gray(s.id)}${group}`);
|
|
43
|
+
}
|
|
44
|
+
console.log('');
|
|
45
|
+
console.log(chalk_1.default.gray('Set default: lts-cli config --set streamId=<id>'));
|
|
46
|
+
}
|
|
47
|
+
function createGroupsCommand() {
|
|
48
|
+
const cmd = new commander_1.Command('groups');
|
|
49
|
+
cmd.description('List all LTS log groups (name + ID)')
|
|
50
|
+
.option('-f, --format <format>', 'Output format: json, pretty', 'pretty')
|
|
51
|
+
.action(async (options) => {
|
|
52
|
+
const config = (0, common_1.loadCheckedConfig)();
|
|
53
|
+
const client = new client_1.LTSClient(config);
|
|
54
|
+
try {
|
|
55
|
+
const groups = await client.listGroups();
|
|
56
|
+
printGroups(groups, options.format);
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
(0, common_1.handleCommandError)(error);
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
return cmd;
|
|
63
|
+
}
|
|
64
|
+
function createStreamsCommand() {
|
|
65
|
+
const cmd = new commander_1.Command('streams');
|
|
66
|
+
cmd.description('List LTS log streams, optionally filtered by log group name')
|
|
67
|
+
.option('-g, --group <name>', 'Filter by log group NAME (e.g. acerp)')
|
|
68
|
+
.option('-f, --format <format>', 'Output format: json, pretty', 'pretty')
|
|
69
|
+
.action(async (options) => {
|
|
70
|
+
const config = (0, common_1.loadCheckedConfig)();
|
|
71
|
+
const client = new client_1.LTSClient(config);
|
|
72
|
+
try {
|
|
73
|
+
const streams = await client.listStreams(options.group);
|
|
74
|
+
printStreams(streams, options.format);
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
(0, common_1.handleCommandError)(error);
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
return cmd;
|
|
81
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.createTraceCommand = createTraceCommand;
|
|
7
|
+
const commander_1 = require("commander");
|
|
8
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
9
|
+
const client_1 = require("../../lts/client");
|
|
10
|
+
const output_1 = require("../../output");
|
|
11
|
+
const common_1 = require("./common");
|
|
12
|
+
function createTraceCommand() {
|
|
13
|
+
const cmd = new commander_1.Command('trace');
|
|
14
|
+
cmd.description('Trace a request by REQUEST_ID — fetch the full request chain in chronological order')
|
|
15
|
+
.argument('<requestId>', 'REQUEST_ID value to trace')
|
|
16
|
+
.option('-g, --group-id <groupId>', 'Log group ID (can also be set via config)')
|
|
17
|
+
.option('-s, --stream-id <streamId>', 'Log stream ID (can also be set via config)')
|
|
18
|
+
.option('--field <name>', 'Field to match: REQUEST_ID (default), trace_id, or content (full-text fallback)', 'REQUEST_ID')
|
|
19
|
+
.option('--last <duration>', 'Relative time range: 30m, 2h, 1d, 7d (default: 24h)')
|
|
20
|
+
.option('--start-time <startTime>', 'Start time (ISO 8601)')
|
|
21
|
+
.option('--end-time <endTime>', 'End time (ISO 8601)')
|
|
22
|
+
.option('--st <startTime>', 'Alias for --start-time')
|
|
23
|
+
.option('--et <endTime>', 'Alias for --end-time')
|
|
24
|
+
.option('-l, --limit <limit>', 'Max number of logs to fetch', (v) => parseInt(v, 10), 200)
|
|
25
|
+
.option('-f, --format <format>', 'Output format: json, table, pretty', 'pretty')
|
|
26
|
+
.action(async (requestId, options) => {
|
|
27
|
+
const config = (0, common_1.loadCheckedConfig)();
|
|
28
|
+
const { groupId, streamId } = (0, common_1.resolveGroupStream)(options, config);
|
|
29
|
+
const timeParams = (0, common_1.resolveTimeParams)(options, '24h');
|
|
30
|
+
const format = (0, common_1.resolveFormat)(options.format);
|
|
31
|
+
const field = options.field;
|
|
32
|
+
// 值加双引号:含连字符的 UUID 不加引号会被分词导致匹配不到
|
|
33
|
+
const query = `${field}:"${requestId}"`;
|
|
34
|
+
const client = new client_1.LTSClient(config);
|
|
35
|
+
const params = {
|
|
36
|
+
groupId,
|
|
37
|
+
streamId,
|
|
38
|
+
...timeParams,
|
|
39
|
+
query,
|
|
40
|
+
reverse: false, // 链路按时间正序
|
|
41
|
+
};
|
|
42
|
+
try {
|
|
43
|
+
// Auto-pagination up to --limit
|
|
44
|
+
const pageSize = Math.min(options.limit || 200, 500);
|
|
45
|
+
const maxTotal = options.limit || 200;
|
|
46
|
+
const allLogs = [];
|
|
47
|
+
let offset = 0;
|
|
48
|
+
let hasMore = true;
|
|
49
|
+
while (hasMore && allLogs.length < maxTotal) {
|
|
50
|
+
const result = await client.queryLogs({ ...params, offset, limit: pageSize });
|
|
51
|
+
allLogs.push(...result.logs);
|
|
52
|
+
if (result.logs.length < pageSize) {
|
|
53
|
+
hasMore = false;
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
offset += pageSize;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
const result = { logs: allLogs.slice(0, maxTotal), total: allLogs.length };
|
|
60
|
+
let output = (0, output_1.formatOutput)(result, format);
|
|
61
|
+
// pretty 格式下高亮 requestId,便于肉眼定位
|
|
62
|
+
if (format === 'pretty' && result.logs.length > 0) {
|
|
63
|
+
output = output.split(requestId).join(chalk_1.default.yellow.bold(requestId));
|
|
64
|
+
}
|
|
65
|
+
console.log(output);
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
(0, common_1.handleCommandError)(error);
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
return cmd;
|
|
72
|
+
}
|
package/dist/cli/index.js
CHANGED
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
4
|
const commander_1 = require("commander");
|
|
5
5
|
const query_1 = require("./commands/query");
|
|
6
|
+
const trace_1 = require("./commands/trace");
|
|
7
|
+
const context_1 = require("./commands/context");
|
|
8
|
+
const streams_1 = require("./commands/streams");
|
|
6
9
|
const config_1 = require("./commands/config");
|
|
7
10
|
const init_1 = require("./commands/init");
|
|
8
11
|
const program = new commander_1.Command();
|
|
@@ -13,8 +16,12 @@ program
|
|
|
13
16
|
.addHelpText('after', `
|
|
14
17
|
Examples:
|
|
15
18
|
$ lts-cli init Setup credentials interactively
|
|
19
|
+
$ lts-cli init --from-url <url> Import region/groupId/streamId from a console URL
|
|
16
20
|
$ lts-cli config --list Show current configuration
|
|
17
|
-
$ lts-cli query --
|
|
21
|
+
$ lts-cli query --last 2h --app acerp-gateway --content "error"
|
|
22
|
+
$ lts-cli trace <requestId> Fetch full request chain by REQUEST_ID
|
|
23
|
+
$ lts-cli groups List log groups
|
|
24
|
+
$ lts-cli streams List log streams
|
|
18
25
|
|
|
19
26
|
Environment variables:
|
|
20
27
|
LTS_AK Access Key
|
|
@@ -27,6 +34,10 @@ Environment variables:
|
|
|
27
34
|
`);
|
|
28
35
|
program.addCommand((0, init_1.createInitCommand)());
|
|
29
36
|
program.addCommand((0, query_1.createQueryCommand)());
|
|
37
|
+
program.addCommand((0, trace_1.createTraceCommand)());
|
|
38
|
+
program.addCommand((0, context_1.createContextCommand)());
|
|
39
|
+
program.addCommand((0, streams_1.createGroupsCommand)());
|
|
40
|
+
program.addCommand((0, streams_1.createStreamsCommand)());
|
|
30
41
|
program.addCommand((0, config_1.createConfigCommand)());
|
|
31
42
|
// Shell completion support
|
|
32
43
|
const completionCmd = new commander_1.Command('completion')
|
|
@@ -42,7 +53,7 @@ _lts_cli_completion() {
|
|
|
42
53
|
local cur="\${words[cword]}"
|
|
43
54
|
|
|
44
55
|
if (( cword == 1 )); then
|
|
45
|
-
COMPREPLY=( $(compgen -W "query config init completion" -- "\$cur") )
|
|
56
|
+
COMPREPLY=( $(compgen -W "query trace context groups streams config init completion" -- "\$cur") )
|
|
46
57
|
return
|
|
47
58
|
fi
|
|
48
59
|
|
|
@@ -83,6 +94,10 @@ _lts_cli() {
|
|
|
83
94
|
local -a commands
|
|
84
95
|
commands=(
|
|
85
96
|
'query:Query LTS logs'
|
|
97
|
+
'trace:Trace request chain by REQUEST_ID'
|
|
98
|
+
'context:View logs before/after a log entry'
|
|
99
|
+
'groups:List log groups'
|
|
100
|
+
'streams:List log streams'
|
|
86
101
|
'config:Manage configuration'
|
|
87
102
|
'init:Interactive setup wizard'
|
|
88
103
|
'completion:Generate shell completion script'
|
|
@@ -137,7 +152,7 @@ compdef _lts_cli lts-cli`);
|
|
|
137
152
|
Register-ArgumentCompleter -CommandName lts-cli -ScriptBlock {
|
|
138
153
|
param(\$wordToComplete, \$commandAst, \$cursorPosition)
|
|
139
154
|
|
|
140
|
-
\$commands = @('query', 'config', 'init', 'completion')
|
|
155
|
+
\$commands = @('query', 'trace', 'context', 'groups', 'streams', 'config', 'init', 'completion')
|
|
141
156
|
|
|
142
157
|
\$args = \$commandAst.ToString() -split '\\s+'
|
|
143
158
|
if (\$args.Count -le 1) {
|
package/dist/lts/client.js
CHANGED
|
@@ -1,15 +1,74 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.LTSClient = void 0;
|
|
4
|
+
exports.resolveTimeRange = resolveTimeRange;
|
|
4
5
|
const LtsClient_1 = require("@huaweicloud/huaweicloud-sdk-lts/v2/LtsClient");
|
|
5
6
|
const ClientBuilder_1 = require("@huaweicloud/huaweicloud-sdk-core/ClientBuilder");
|
|
6
7
|
const BasicCredentials_1 = require("@huaweicloud/huaweicloud-sdk-core/auth/BasicCredentials");
|
|
7
8
|
const region_1 = require("@huaweicloud/huaweicloud-sdk-core/region/region");
|
|
8
9
|
const ListLogsRequest_1 = require("@huaweicloud/huaweicloud-sdk-lts/v2/model/ListLogsRequest");
|
|
9
10
|
const QueryLtsLogParams_1 = require("@huaweicloud/huaweicloud-sdk-lts/v2/model/QueryLtsLogParams");
|
|
11
|
+
const ListLogContextRequest_1 = require("@huaweicloud/huaweicloud-sdk-lts/v2/model/ListLogContextRequest");
|
|
12
|
+
const ListLogContextRequestBody_1 = require("@huaweicloud/huaweicloud-sdk-lts/v2/model/ListLogContextRequestBody");
|
|
13
|
+
const ListLogGroupsRequest_1 = require("@huaweicloud/huaweicloud-sdk-lts/v2/model/ListLogGroupsRequest");
|
|
14
|
+
const ListLogStreamsRequest_1 = require("@huaweicloud/huaweicloud-sdk-lts/v2/model/ListLogStreamsRequest");
|
|
10
15
|
function sleep(ms) {
|
|
11
16
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
12
17
|
}
|
|
18
|
+
const DURATION_REGEX = /^(\d+)([mhd])$/;
|
|
19
|
+
const DURATION_UNIT_MS = {
|
|
20
|
+
m: 60 * 1000,
|
|
21
|
+
h: 60 * 60 * 1000,
|
|
22
|
+
d: 24 * 60 * 60 * 1000,
|
|
23
|
+
};
|
|
24
|
+
/** 解析相对时间范围(如 30m/2h/1d),返回 [startMs, endMs] */
|
|
25
|
+
function resolveTimeRange(params) {
|
|
26
|
+
if (params.last) {
|
|
27
|
+
const match = DURATION_REGEX.exec(params.last.trim());
|
|
28
|
+
if (!match) {
|
|
29
|
+
throw new Error(`Invalid --last duration: "${params.last}". Expected format like 30m, 2h, 1d, 7d.`);
|
|
30
|
+
}
|
|
31
|
+
const amount = parseInt(match[1], 10);
|
|
32
|
+
const endMs = Date.now();
|
|
33
|
+
return { startMs: endMs - amount * DURATION_UNIT_MS[match[2]], endMs };
|
|
34
|
+
}
|
|
35
|
+
if (!params.startTime || !params.endTime) {
|
|
36
|
+
throw new Error('Time range required: provide --last (e.g. --last 2h) or both --start-time and --end-time.');
|
|
37
|
+
}
|
|
38
|
+
const startMs = new Date(params.startTime).getTime();
|
|
39
|
+
const endMs = new Date(params.endTime).getTime();
|
|
40
|
+
if (Number.isNaN(startMs) || Number.isNaN(endMs)) {
|
|
41
|
+
throw new Error(`Invalid time format. Use ISO 8601 (e.g. 2024-01-01T00:00:00Z).`);
|
|
42
|
+
}
|
|
43
|
+
return { startMs, endMs };
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* 解析查询表达式(对齐控制台管道语法):
|
|
47
|
+
* - "搜索语句 | SELECT ..." → keywords=搜索语句, query=SQL, is_analysis_query=true
|
|
48
|
+
* - 裸 "SELECT ..." → query=SQL, is_analysis_query=true
|
|
49
|
+
* - 其他(field:value 等) → keywords=搜索语句
|
|
50
|
+
*/
|
|
51
|
+
function parseQueryExpression(expr) {
|
|
52
|
+
const trimmed = expr.trim();
|
|
53
|
+
if (trimmed.includes('|')) {
|
|
54
|
+
const idx = trimmed.indexOf('|');
|
|
55
|
+
const searchPart = trimmed.slice(0, idx).trim();
|
|
56
|
+
const sqlPart = trimmed.slice(idx + 1).trim();
|
|
57
|
+
return {
|
|
58
|
+
keywords: searchPart && searchPart !== '*' ? searchPart : undefined,
|
|
59
|
+
query: sqlPart || undefined,
|
|
60
|
+
isAnalysisQuery: sqlPart.length > 0,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
if (/^select\s/i.test(trimmed)) {
|
|
64
|
+
return { query: trimmed, isAnalysisQuery: true };
|
|
65
|
+
}
|
|
66
|
+
return { keywords: trimmed, isAnalysisQuery: false };
|
|
67
|
+
}
|
|
68
|
+
/** 服务端默认在命中词上包裹 <HighLightTag>,输出前剥离 */
|
|
69
|
+
function stripHighlightTags(value) {
|
|
70
|
+
return value.replace(/<\/?HighLightTag>/g, '');
|
|
71
|
+
}
|
|
13
72
|
class LTSClient {
|
|
14
73
|
client;
|
|
15
74
|
config;
|
|
@@ -30,13 +89,23 @@ class LTSClient {
|
|
|
30
89
|
this.client = builder.build();
|
|
31
90
|
}
|
|
32
91
|
async queryLogs(params) {
|
|
92
|
+
const { startMs, endMs } = resolveTimeRange(params);
|
|
33
93
|
const queryParams = new QueryLtsLogParams_1.QueryLtsLogParams()
|
|
34
|
-
.withStartTime(
|
|
35
|
-
.withEndTime(
|
|
36
|
-
.withLimit(params.limit
|
|
94
|
+
.withStartTime(startMs.toString())
|
|
95
|
+
.withEndTime(endMs.toString())
|
|
96
|
+
.withLimit(Number.isFinite(params.limit) ? params.limit : 100);
|
|
37
97
|
// 优先使用 query 参数(支持SQL和结构化查询)
|
|
38
98
|
if (params.query) {
|
|
39
|
-
|
|
99
|
+
const parsed = parseQueryExpression(params.query);
|
|
100
|
+
if (parsed.keywords) {
|
|
101
|
+
queryParams.withKeywords(parsed.keywords);
|
|
102
|
+
}
|
|
103
|
+
if (parsed.query) {
|
|
104
|
+
queryParams.withQuery(parsed.query);
|
|
105
|
+
}
|
|
106
|
+
if (parsed.isAnalysisQuery) {
|
|
107
|
+
queryParams.withIsAnalysisQuery(true);
|
|
108
|
+
}
|
|
40
109
|
}
|
|
41
110
|
else if (params.keyword) {
|
|
42
111
|
// 向后兼容:简单关键词搜索
|
|
@@ -48,16 +117,73 @@ class LTSClient {
|
|
|
48
117
|
const request = new ListLogsRequest_1.ListLogsRequest(params.groupId, params.streamId, 'application/json')
|
|
49
118
|
.withBody(queryParams);
|
|
50
119
|
const response = await this.retryRequest(() => this.client.listLogs(request));
|
|
120
|
+
const raw = response;
|
|
121
|
+
// SQL 分析查询:结果在 analysisLogs 中(行为 JSON 对象)
|
|
122
|
+
if (Array.isArray(raw.analysisLogs)) {
|
|
123
|
+
const logs = raw.analysisLogs.map((row) => ({
|
|
124
|
+
logTime: '',
|
|
125
|
+
content: JSON.stringify(row),
|
|
126
|
+
labels: {},
|
|
127
|
+
}));
|
|
128
|
+
return { logs, total: logs.length };
|
|
129
|
+
}
|
|
130
|
+
const logs = (response.logs || []).map((log) => ({
|
|
131
|
+
logTime: log.line_num ?? log.lineNum ?? '',
|
|
132
|
+
content: stripHighlightTags(log.content ?? ''),
|
|
133
|
+
labels: Object.fromEntries(Object.entries(log.labels ?? {}).map(([k, v]) => [k, stripHighlightTags(v)])),
|
|
134
|
+
}));
|
|
135
|
+
return {
|
|
136
|
+
logs,
|
|
137
|
+
total: response.count ?? logs.length,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
/** 查询某条日志的上下文(前后各 N 行),对应控制台的“查看上下文” */
|
|
141
|
+
async queryContext(params) {
|
|
142
|
+
const body = new ListLogContextRequestBody_1.ListLogContextRequestBody()
|
|
143
|
+
.withLineNum(params.lineNum)
|
|
144
|
+
.withTime(params.time)
|
|
145
|
+
.withBackwardsSize(params.before ?? 20)
|
|
146
|
+
.withForwardsSize(params.after ?? 20);
|
|
147
|
+
const request = new ListLogContextRequest_1.ListLogContextRequest(params.groupId, params.streamId, 'application/json')
|
|
148
|
+
.withBody(body);
|
|
149
|
+
const response = await this.retryRequest(() => this.client.listLogContext(request));
|
|
51
150
|
const logs = (response.logs || []).map((log) => ({
|
|
52
|
-
logTime: log.lineNum ?? '',
|
|
151
|
+
logTime: log.line_num ?? log.lineNum ?? '',
|
|
53
152
|
content: log.content ?? '',
|
|
54
153
|
labels: log.labels ?? {},
|
|
55
154
|
}));
|
|
56
155
|
return {
|
|
57
156
|
logs,
|
|
58
|
-
|
|
157
|
+
// SDK 实际返回纯对象,字段为 snake_case
|
|
158
|
+
total: response.total_count ?? response.totalCount ?? logs.length,
|
|
59
159
|
};
|
|
60
160
|
}
|
|
161
|
+
/** 列出全部日志组 */
|
|
162
|
+
async listGroups() {
|
|
163
|
+
const request = new ListLogGroupsRequest_1.ListLogGroupsRequest();
|
|
164
|
+
const response = await this.retryRequest(() => this.client.listLogGroups(request));
|
|
165
|
+
// SDK 实际返回纯对象,字段为 snake_case(log_groups)
|
|
166
|
+
const groups = (response.log_groups ?? []);
|
|
167
|
+
return groups.map((g) => ({
|
|
168
|
+
id: g.log_group_id ?? '',
|
|
169
|
+
name: g.log_group_name ?? '',
|
|
170
|
+
}));
|
|
171
|
+
}
|
|
172
|
+
/** 列出日志流,可按日志组名称过滤(API 使用组名而非组 ID) */
|
|
173
|
+
async listStreams(groupName) {
|
|
174
|
+
const request = new ListLogStreamsRequest_1.ListLogStreamsRequest('application/json');
|
|
175
|
+
if (groupName) {
|
|
176
|
+
request.withLogGroupName(groupName);
|
|
177
|
+
}
|
|
178
|
+
const response = await this.retryRequest(() => this.client.listLogStreams(request));
|
|
179
|
+
// SDK 实际返回纯对象,字段为 snake_case(log_streams)
|
|
180
|
+
const streams = (response.log_streams ?? []);
|
|
181
|
+
return streams.map((s) => ({
|
|
182
|
+
id: s.log_stream_id ?? '',
|
|
183
|
+
name: s.log_stream_name ?? '',
|
|
184
|
+
groupId: s.log_group_id,
|
|
185
|
+
}));
|
|
186
|
+
}
|
|
61
187
|
async retryRequest(fn, maxRetries = 3) {
|
|
62
188
|
let lastError;
|
|
63
189
|
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|