@cvtoolman/huawei-lts-cli 1.0.2 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,108 @@
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.createFieldsCommand = createFieldsCommand;
7
+ const commander_1 = require("commander");
8
+ const chalk_1 = __importDefault(require("chalk"));
9
+ const cli_table3_1 = __importDefault(require("cli-table3"));
10
+ const client_1 = require("../../lts/client");
11
+ const common_1 = require("./common");
12
+ /**
13
+ * 输出日志流的索引配置。
14
+ *
15
+ * 存在的理由:字段名、字段类型、大小写是否敏感、分词符,全都是**每条日志流各自一份**
16
+ * 的配置(SDK 里是 LTSFullTextIndexInfo / LTSFieldsInfo)。任何写进文档的字段表都只对
17
+ * 某一条流成立,换个环境就是错的。所以答案必须在运行时从 API 取。
18
+ */
19
+ function boolLabel(value) {
20
+ if (value === undefined)
21
+ return '-';
22
+ return value ? 'yes' : 'no';
23
+ }
24
+ function formatJson(index) {
25
+ return JSON.stringify(index, null, 2);
26
+ }
27
+ function formatTable(index) {
28
+ const lines = [];
29
+ const summary = new cli_table3_1.default({ head: ['Full-text index', 'Value'] });
30
+ summary.push(['enabled', boolLabel(index.fullText.enabled)], ['caseSensitive', boolLabel(index.fullText.caseSensitive)], ['includeChinese', boolLabel(index.fullText.includeChinese)], ['tokenizer', index.fullText.tokenizer || '-']);
31
+ lines.push(summary.toString());
32
+ if (index.fields.length === 0) {
33
+ lines.push('');
34
+ lines.push('No field index configured — only full-text search is available on this stream.');
35
+ return lines.join('\n');
36
+ }
37
+ lines.push('');
38
+ const fields = new cli_table3_1.default({
39
+ head: ['Field', 'Type', 'Case-sensitive', 'Chinese', 'Tokenizer', 'Quick analysis'],
40
+ });
41
+ for (const f of index.fields) {
42
+ fields.push([
43
+ f.name,
44
+ f.type || '-',
45
+ boolLabel(f.caseSensitive),
46
+ boolLabel(f.includeChinese),
47
+ f.tokenizer || '-',
48
+ boolLabel(f.quickAnalysis),
49
+ ]);
50
+ }
51
+ lines.push(fields.toString());
52
+ return lines.join('\n');
53
+ }
54
+ function formatPretty(index) {
55
+ const lines = [];
56
+ lines.push(chalk_1.default.bold(`Stream ${index.streamId}`));
57
+ lines.push('');
58
+ lines.push(chalk_1.default.bold('Full-text index'));
59
+ lines.push(` enabled: ${boolLabel(index.fullText.enabled)}`);
60
+ lines.push(` caseSensitive: ${boolLabel(index.fullText.caseSensitive)}`);
61
+ lines.push(` includeChinese: ${boolLabel(index.fullText.includeChinese)}`);
62
+ lines.push(` tokenizer: ${index.fullText.tokenizer || '-'}`);
63
+ lines.push('');
64
+ if (index.fields.length === 0) {
65
+ lines.push(chalk_1.default.yellow('No field index configured — only full-text search is available.'));
66
+ return lines.join('\n');
67
+ }
68
+ lines.push(chalk_1.default.bold(`Fields (${index.fields.length})`));
69
+ for (const f of index.fields) {
70
+ const flags = [
71
+ f.caseSensitive ? 'case-sensitive' : 'case-insensitive',
72
+ f.includeChinese ? 'chinese' : 'no-chinese',
73
+ f.quickAnalysis ? 'quick-analysis' : null,
74
+ ].filter(Boolean);
75
+ lines.push(` ${chalk_1.default.cyan(f.name)} ${chalk_1.default.gray(f.type || '-')} ${chalk_1.default.gray(flags.join(', '))}`);
76
+ if (f.tokenizer) {
77
+ lines.push(chalk_1.default.gray(` tokenizer: ${f.tokenizer}`));
78
+ }
79
+ }
80
+ return lines.join('\n');
81
+ }
82
+ function createFieldsCommand() {
83
+ const cmd = new commander_1.Command('fields');
84
+ cmd
85
+ .description('Show the index config of a log stream (fields, types, case sensitivity, tokenizer)')
86
+ .option('-g, --group-id <groupId>', 'Log group ID (can also be set via config)')
87
+ .option('-s, --stream-id <streamId>', 'Log stream ID (can also be set via config)')
88
+ .option('-f, --format <format>', 'Output format: json, table, pretty', 'pretty')
89
+ .action(async (options) => {
90
+ const config = (0, common_1.loadCheckedConfig)();
91
+ const { groupId, streamId } = (0, common_1.resolveGroupStream)(options, config);
92
+ const format = (0, common_1.resolveFormat)(options.format);
93
+ const client = new client_1.LTSClient(config);
94
+ try {
95
+ const index = await client.getStreamIndex(groupId, streamId);
96
+ const output = format === 'json'
97
+ ? formatJson(index)
98
+ : format === 'table'
99
+ ? formatTable(index)
100
+ : formatPretty(index);
101
+ console.log(output);
102
+ }
103
+ catch (error) {
104
+ (0, common_1.handleCommandError)(error);
105
+ }
106
+ });
107
+ return cmd;
108
+ }
@@ -37,11 +37,23 @@ exports.createInitCommand = createInitCommand;
37
37
  const commander_1 = require("commander");
38
38
  const readline = __importStar(require("readline"));
39
39
  const config_1 = require("../../config");
40
+ const profile_1 = require("../../config/profile");
40
41
  function ask(rl, question) {
41
42
  return new Promise((resolve) => {
42
43
  rl.question(question, (answer) => resolve(answer.trim()));
43
44
  });
44
45
  }
46
+ /** 写 profile.md 骨架并告知路径。已存在则绝不覆盖——那是用户手写的内容。 */
47
+ function reportProfileSkeleton() {
48
+ const { path, created } = (0, profile_1.writeProfileSkeleton)();
49
+ if (created) {
50
+ console.log(` profile.md = ${path} (skeleton created)`);
51
+ console.log(' 业务词表:把「订单服务」这类称呼映射到日志里的实际取值');
52
+ }
53
+ else {
54
+ console.log(` profile.md = ${path} (existing, left untouched)`);
55
+ }
56
+ }
45
57
  const ENDPOINT_MAP = {
46
58
  'cn-north-1': 'lts.cn-north-1.myhuaweicloud.com',
47
59
  'cn-north-2': 'lts.cn-north-2.myhuaweicloud.com',
@@ -67,7 +79,13 @@ const ENDPOINT_MAP = {
67
79
  function createInitCommand() {
68
80
  const cmd = new commander_1.Command('init');
69
81
  cmd.description('Interactive setup wizard — configure credentials step by step')
70
- .action(async () => {
82
+ .option('--from-url <url>', 'Import region/groupId/streamId from an LTS console URL (non-interactive)')
83
+ .action(async (options) => {
84
+ // 从控制台 URL 导入:解析 region、groupId、topicId(即 streamId),无需交互
85
+ if (options.fromUrl) {
86
+ importFromConsoleUrl(options.fromUrl);
87
+ return;
88
+ }
71
89
  const rl = readline.createInterface({
72
90
  input: process.stdin,
73
91
  output: process.stdout,
@@ -138,13 +156,17 @@ function createInitCommand() {
138
156
  }
139
157
  console.log('');
140
158
  console.log('✅ Configuration saved!');
159
+ reportProfileSkeleton();
141
160
  console.log('');
142
161
  console.log('Try it out:');
143
162
  if (groupId && streamId) {
144
- console.log(' lts-cli query --start-time 2024-01-01T00:00:00Z --end-time 2024-01-02T00:00:00Z');
163
+ console.log(' lts-cli query --last 2h');
164
+ console.log(' lts-cli fields');
145
165
  }
146
166
  else {
147
- console.log(' lts-cli query -g <group-id> -s <stream-id> --start-time 2024-01-01T00:00:00Z --end-time 2024-01-02T00:00:00Z');
167
+ console.log(' lts-cli groups # find your log group ID');
168
+ console.log(' lts-cli streams --group <group-name> # find your log stream ID');
169
+ console.log(' lts-cli query -g <groupId> -s <streamId> --last 2h');
148
170
  }
149
171
  console.log('');
150
172
  }
@@ -154,3 +176,54 @@ function createInitCommand() {
154
176
  });
155
177
  return cmd;
156
178
  }
179
+ /** 从华为云 LTS 控制台 URL 中解析 region / groupId / streamId 并写入配置 */
180
+ function importFromConsoleUrl(raw) {
181
+ let url;
182
+ try {
183
+ url = new URL(raw);
184
+ }
185
+ catch {
186
+ console.error('Error: Invalid URL. Please paste the full console URL, e.g.:');
187
+ console.error(' https://console.huaweicloud.com/lts/?region=cn-south-1#/cts/logEventsLeftMenu/events?groupId=...&topicId=...');
188
+ process.exit(1);
189
+ }
190
+ const region = url.searchParams.get('region') || undefined;
191
+ // groupId / topicId 位于 hash 段的查询参数中:#/cts/logEventsLeftMenu/events?groupId=...&topicId=...
192
+ const hash = url.hash || '';
193
+ const qIndex = hash.indexOf('?');
194
+ const hashParams = new URLSearchParams(qIndex >= 0 ? hash.slice(qIndex + 1) : '');
195
+ const groupId = hashParams.get('groupId') || url.searchParams.get('groupId') || undefined;
196
+ const streamId = hashParams.get('topicId') || hashParams.get('streamId') || url.searchParams.get('topicId') || undefined;
197
+ if (!region && !groupId && !streamId) {
198
+ console.error('Error: No region/groupId/topicId found in the URL. Make sure it is an LTS log search page URL.');
199
+ process.exit(1);
200
+ }
201
+ console.log('');
202
+ console.log('Importing from console URL:');
203
+ if (region) {
204
+ (0, config_1.setConfig)('region', region);
205
+ console.log(` region = ${region}`);
206
+ const autoEndpoint = ENDPOINT_MAP[region];
207
+ if (autoEndpoint) {
208
+ (0, config_1.setConfig)('endpoint', `https://${autoEndpoint}`);
209
+ console.log(` endpoint = https://${autoEndpoint} (derived from region)`);
210
+ }
211
+ }
212
+ if (groupId) {
213
+ (0, config_1.setConfig)('groupId', groupId);
214
+ console.log(` groupId = ${groupId}`);
215
+ }
216
+ if (streamId) {
217
+ (0, config_1.setConfig)('streamId', streamId);
218
+ console.log(` streamId = ${streamId}`);
219
+ }
220
+ console.log('');
221
+ console.log('✅ Configuration saved!');
222
+ reportProfileSkeleton();
223
+ console.log('');
224
+ console.log('Next steps:');
225
+ console.log(' 1. Configure credentials if not done yet: lts-cli init');
226
+ console.log(' 2. Check which fields this stream indexes: lts-cli fields');
227
+ console.log(' 3. Query logs: lts-cli query --last 2h');
228
+ console.log('');
229
+ }
@@ -3,129 +3,74 @@ 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");
6
+ const pagination_1 = require("../../lts/pagination");
7
7
  const output_1 = require("../../output");
8
+ const common_1 = require("./common");
9
+ // commander 的处理函数会收到 (value, previous),直接传 parseInt 会把 previous 当作进制
10
+ function parseIntOption(value) {
11
+ return parseInt(value, 10);
12
+ }
8
13
  function createQueryCommand() {
9
14
  const cmd = new commander_1.Command('query');
10
15
  cmd.description('Query LTS logs')
11
16
  .option('-g, --group-id <groupId>', 'Log group ID (can also be set via config)')
12
17
  .option('-s, --stream-id <streamId>', 'Log stream ID (can also be set via config)')
13
- .requiredOption('--start-time <startTime>', 'Start time (ISO 8601, e.g. 2024-01-01T00:00:00Z)')
14
- .requiredOption('--end-time <endTime>', 'End time (ISO 8601, e.g. 2024-01-02T00:00:00Z)')
18
+ .option('--start-time <startTime>', 'Start time (ISO 8601, e.g. 2024-01-01T00:00:00Z)')
19
+ .option('--end-time <endTime>', 'End time (ISO 8601, e.g. 2024-01-02T00:00:00Z)')
15
20
  .option('--st <startTime>', 'Alias for --start-time')
16
21
  .option('--et <endTime>', 'Alias for --end-time')
17
- .option('-k, --keyword <keyword>', 'Simple keyword search (legacy, use --query for advanced queries)')
18
- .option('-q, --query <query>', 'Advanced query expression. Supports:\n' +
22
+ .option('--last <duration>', 'Relative time range: 30m, 2h, 1d, 7d (mutually exclusive with --st/--et)')
23
+ .option('-q, --query <query>', 'Query expression. Supports:\n' +
19
24
  ' - Field queries: content:error AND appName:myapp\n' +
20
25
  ' - Comparison: time>60 AND region:r\n' +
21
- ' - SQL syntax: SELECT * FROM log WHERE content LIKE \'%error%\'\n' +
26
+ ' - Pipeline analysis: error | SELECT count(*) AS c\n' +
22
27
  ' - Logical operators: AND, OR, NOT (must be surrounded by spaces)')
23
- .option('-l, --limit <limit>', 'Limit number of results', parseInt, 100)
24
- .option('-o, --offset <offset>', 'Offset for pagination', parseInt, 0)
28
+ .option('--app <appName>', 'Filter by appName field, 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>', 'Page size (also the cap for --all)', parseIntOption, 100)
25
32
  .option('-r, --reverse', 'Reverse order (newest first)')
26
33
  .option('-f, --format <format>', 'Output format: json, table, pretty', 'pretty')
27
- .option('--no-paginate', 'Disable auto-pagination (single page only)')
34
+ .option('--all', 'Follow the cursor to the end (up to 10000 logs; default is a single page)')
28
35
  .action(async (options) => {
29
- const config = (0, config_1.loadConfig)();
30
- // Validate required credentials
31
- if (!config.ak || !config.sk || !config.projectId) {
32
- console.error('Error: Missing required credentials. Please configure them first:');
33
- console.error('');
34
- console.error(' Quick setup (recommended):');
35
- console.error(' lts-cli init');
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';
36
+ const config = (0, common_1.loadCheckedConfig)();
37
+ const { groupId, streamId } = (0, common_1.resolveGroupStream)(options, config);
38
+ const timeParams = (0, common_1.resolveTimeParams)(options);
39
+ const query = (0, common_1.buildQueryExpression)(options);
40
+ const format = (0, common_1.resolveFormat)(options.format);
41
+ // --request-id 用于链路定位,按时间正序输出
42
+ const reverse = options.requestId ? false : options.reverse;
63
43
  const client = new client_1.LTSClient(config);
64
44
  const params = {
65
45
  groupId,
66
46
  streamId,
67
- startTime,
68
- endTime,
69
- keyword: options.keyword,
70
- query: options.query,
47
+ ...timeParams,
48
+ query,
71
49
  limit: options.limit,
72
- offset: options.offset,
73
- reverse: options.reverse,
50
+ reverse,
74
51
  };
75
52
  try {
76
- if (options.paginate) {
53
+ if (!options.all) {
54
+ // 默认单页查询,结果量由 --limit 控制
77
55
  const result = await client.queryLogs(params);
56
+ (0, common_1.warnIfIncomplete)(result);
78
57
  console.log((0, output_1.formatOutput)(result, format));
79
58
  }
80
59
  else {
81
- // Auto-pagination: fetch all logs
82
- const allLogs = [];
83
- let offset = options.offset || 0;
84
- const limit = options.limit || 100;
85
- let hasMore = true;
86
- while (hasMore) {
87
- const pageParams = {
88
- ...params,
89
- offset,
90
- limit,
91
- };
92
- const result = await client.queryLogs(pageParams);
93
- allLogs.push(...result.logs);
94
- if (result.logs.length < limit) {
95
- hasMore = false;
96
- }
97
- else {
98
- offset += limit;
99
- }
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.`);
100
68
  }
101
- const result = {
102
- logs: allLogs,
103
- total: allLogs.length,
104
- };
105
- console.log((0, output_1.formatOutput)(result, format));
69
+ console.log((0, output_1.formatOutput)({ logs: result.logs, total: result.total }, format));
106
70
  }
107
71
  }
108
72
  catch (error) {
109
- const msg = error.message || String(error);
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);
73
+ (0, common_1.handleCommandError)(error);
129
74
  }
130
75
  });
131
76
  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('--group <name>', 'Filter by log group NAME (the API filters by name, not ID)')
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,76 @@
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 pagination_1 = require("../../lts/pagination");
11
+ const output_1 = require("../../output");
12
+ const config_1 = require("../../config");
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';
22
+ function createTraceCommand() {
23
+ const cmd = new commander_1.Command('trace');
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)')
26
+ .option('-g, --group-id <groupId>', 'Log group ID (can also be set via config)')
27
+ .option('-s, --stream-id <streamId>', 'Log stream ID (can also be set via config)')
28
+ .option('--field <name>', `Field to match. Defaults to config.traceField, then "${DEFAULT_TRACE_FIELD}"`)
29
+ .option('--last <duration>', 'Relative time range: 30m, 2h, 1d, 7d (default: 24h)')
30
+ .option('--start-time <startTime>', 'Start time (ISO 8601)')
31
+ .option('--end-time <endTime>', 'End time (ISO 8601)')
32
+ .option('--st <startTime>', 'Alias for --start-time')
33
+ .option('--et <endTime>', 'Alias for --end-time')
34
+ .option('-l, --limit <limit>', 'Max number of logs to fetch', (v) => parseInt(v, 10), 200)
35
+ .option('-f, --format <format>', 'Output format: json, table, pretty', 'pretty')
36
+ .action(async (requestId, options) => {
37
+ const config = (0, common_1.loadCheckedConfig)();
38
+ const { groupId, streamId } = (0, common_1.resolveGroupStream)(options, config);
39
+ const timeParams = (0, common_1.resolveTimeParams)(options, '24h');
40
+ const format = (0, common_1.resolveFormat)(options.format);
41
+ const field = options.field || (0, config_1.getConfig)('traceField') || DEFAULT_TRACE_FIELD;
42
+ // 值加双引号:含连字符的 UUID 不加引号会被分词导致匹配不到
43
+ const query = `${field}:"${requestId}"`;
44
+ const client = new client_1.LTSClient(config);
45
+ const params = {
46
+ groupId,
47
+ streamId,
48
+ ...timeParams,
49
+ query,
50
+ reverse: false, // 链路按时间正序
51
+ };
52
+ try {
53
+ const maxTotal = options.limit || 200;
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.`);
63
+ }
64
+ let output = (0, output_1.formatOutput)({ logs: result.logs, total: result.total }, format);
65
+ // pretty 格式下高亮 requestId,便于肉眼定位
66
+ if (format === 'pretty' && result.logs.length > 0) {
67
+ output = output.split(requestId).join(chalk_1.default.yellow.bold(requestId));
68
+ }
69
+ console.log(output);
70
+ }
71
+ catch (error) {
72
+ (0, common_1.handleCommandError)(error);
73
+ }
74
+ });
75
+ return cmd;
76
+ }