@cvtoolman/huawei-lts-cli 1.0.3 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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('-k, --keyword <keyword>', 'Simple keyword search (legacy, use --query for advanced queries)')
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
- ' - SQL syntax: SELECT * FROM log WHERE content LIKE \'%error%\'\n' +
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 (e.g. --app acerp-gateway), merged with -q via AND')
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>', 'Limit number of results', parseIntOption, 100)
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', 'Fetch all pages (up to 10000 logs; default is single page)')
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:自动翻页拉取全部日志,上限 10000 条防止失控
64
- const MAX_LOGS = 10000;
65
- const allLogs = [];
66
- let offset = options.offset || 0;
67
- const limit = options.limit || 100;
68
- let hasMore = true;
69
- while (hasMore && allLogs.length < MAX_LOGS) {
70
- const pageParams = {
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
- const result = {
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('-g, --group <name>', 'Filter by log group NAME (e.g. acerp)')
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 REQUEST_ID — fetch the full request chain in chronological order')
15
- .argument('<requestId>', 'REQUEST_ID value to 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)')
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>', 'Field to match: REQUEST_ID (default), trace_id, or content (full-text fallback)', 'REQUEST_ID')
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
- 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
- }
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
- const result = { logs: allLogs.slice(0, maxTotal), total: allLogs.length };
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('1.0.0')
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|-k|--keyword|-l|--limit|-o|--offset|-f|--format)
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 "-g --group-id -s --stream-id --start-time --end-time --st --et -k --keyword -l --limit -o --offset -r --reverse -f --format --no-paginate" -- "\$cur") )
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 chain by REQUEST_ID'
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
- '-k[Search keyword]:keyword:' \\
128
- '-l[Limit]:number:' \\
129
- '-o[Offset]:number:' \\
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
- '--no-paginate[Disable pagination]'
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', '-k', '-l', '-o', '-r', '-f', '--no-paginate')
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' {
@@ -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
- fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), 'utf-8');
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
+ }
@@ -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
- logTime: '',
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
- logTime: log.line_num ?? log.lineNum ?? '',
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
- logTime: log.line_num ?? log.lineNum ?? '',
159
+ lineNum: log.line_num ?? log.lineNum ?? '',
152
160
  content: log.content ?? '',
153
161
  labels: log.labels ?? {},
154
162
  }));
@@ -184,6 +192,32 @@ class LTSClient {
184
192
  groupId: s.log_group_id,
185
193
  }));
186
194
  }
195
+ /** 读取日志流的索引配置(全文索引 + 字段索引),供 `fields` 命令与 AI 运行时发现 */
196
+ async getStreamIndex(groupId, streamId) {
197
+ const request = new ListLogStreamIndexRequest_1.ListLogStreamIndexRequest(groupId, streamId, 'application/json');
198
+ const response = await this.retryRequest(() => this.client.listLogStreamIndex(request));
199
+ const raw = response;
200
+ // SDK 在不同版本下可能给出 class 实例(camelCase)或纯对象(snake_case),两者都认
201
+ const fullText = raw.fullTextIndex ?? raw.full_text_index ?? {};
202
+ const fields = (raw.fields ?? []);
203
+ return {
204
+ streamId: raw.logStreamId ?? raw.log_stream_id ?? streamId,
205
+ fullText: {
206
+ enabled: fullText.enable === true,
207
+ caseSensitive: fullText.caseSensitive ?? fullText.case_sensitive,
208
+ includeChinese: fullText.includeChinese ?? fullText.include_chinese,
209
+ tokenizer: fullText.tokenizer,
210
+ },
211
+ fields: fields.map((f) => ({
212
+ name: f.fieldName ?? f.field_name ?? '',
213
+ type: f.fieldType ?? f.field_type ?? '',
214
+ caseSensitive: f.caseSensitive ?? f.case_sensitive,
215
+ includeChinese: f.includeChinese ?? f.include_chinese,
216
+ tokenizer: f.tokenizer,
217
+ quickAnalysis: f.quickAnalysis ?? f.quick_analysis,
218
+ })),
219
+ };
220
+ }
187
221
  async retryRequest(fn, maxRetries = 3) {
188
222
  let lastError;
189
223
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
@@ -0,0 +1,56 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.cursorDirection = cursorDirection;
4
+ exports.nextCursor = nextCursor;
5
+ exports.fetchAllPages = fetchAllPages;
6
+ /** 倒序(新到旧)→ 往更早读;正序 → 往更晚读。 */
7
+ function cursorDirection(reverse) {
8
+ return reverse ? 'backwards' : 'forwards';
9
+ }
10
+ /** 游标取本页最后一条的 line_num;拿不到就返回 undefined(调用方据此停止)。 */
11
+ function nextCursor(logs) {
12
+ if (logs.length === 0)
13
+ return undefined;
14
+ const last = logs[logs.length - 1].lineNum;
15
+ return last ? last : undefined;
16
+ }
17
+ async function fetchAllPages(fetchPage, params, options = {}) {
18
+ const max = options.max ?? 10000;
19
+ const pageSize = options.pageSize ?? params.limit ?? 100;
20
+ const direction = cursorDirection(params.reverse);
21
+ const collected = [];
22
+ let cursor;
23
+ let isQueryComplete;
24
+ let truncated = false;
25
+ while (collected.length < max) {
26
+ const page = await fetchPage({
27
+ ...params,
28
+ limit: pageSize,
29
+ lineNum: cursor,
30
+ searchType: cursor ? direction : undefined,
31
+ });
32
+ if (page.isQueryComplete === false) {
33
+ isQueryComplete = false;
34
+ }
35
+ else if (isQueryComplete === undefined && page.isQueryComplete === true) {
36
+ isQueryComplete = true;
37
+ }
38
+ if (page.logs.length === 0)
39
+ break;
40
+ const next = nextCursor(page.logs);
41
+ // 服务端把同一段原样又给了一遍:整页丢弃并停止。
42
+ // 先判游标再入账,否则重放的一页会被当成新数据拼进结果。
43
+ if (cursor !== undefined && next === cursor)
44
+ break;
45
+ collected.push(...page.logs);
46
+ // 不满一页 = 没有更多了;拿不到游标则无法继续翻页
47
+ if (page.logs.length < pageSize || !next)
48
+ break;
49
+ cursor = next;
50
+ if (collected.length >= max) {
51
+ truncated = true;
52
+ }
53
+ }
54
+ const logs = collected.slice(0, max);
55
+ return { logs, total: logs.length, isQueryComplete, truncated };
56
+ }
@@ -27,7 +27,7 @@ function formatPretty(data) {
27
27
  lines.push(chalk_1.default.bold.green(`Found ${data.total} logs:`));
28
28
  lines.push('');
29
29
  for (const log of data.logs) {
30
- lines.push(chalk_1.default.cyan(`[${log.logTime}]`));
30
+ lines.push(chalk_1.default.cyan(`[${log.lineNum}]`));
31
31
  lines.push(chalk_1.default.white(log.content));
32
32
  if (Object.keys(log.labels).length > 0) {
33
33
  const labels = Object.entries(log.labels)
@@ -10,7 +10,7 @@ function formatTable(data) {
10
10
  return 'No logs found.';
11
11
  }
12
12
  const table = new cli_table3_1.default({
13
- head: ['Time', 'Content', 'Labels'],
13
+ head: ['Line', 'Content', 'Labels'],
14
14
  colWidths: [25, 50, 30],
15
15
  wordWrap: true,
16
16
  });
@@ -18,7 +18,7 @@ function formatTable(data) {
18
18
  const labelsStr = Object.entries(log.labels)
19
19
  .map(([k, v]) => `${k}=${v}`)
20
20
  .join(', ');
21
- table.push([log.logTime, log.content, labelsStr]);
21
+ table.push([log.lineNum, log.content, labelsStr]);
22
22
  }
23
23
  return table.toString();
24
24
  }