rl-rockcli 0.0.13 → 0.0.15

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.
@@ -1,370 +0,0 @@
1
- const { DISPLAY_FIELDS, FIELD_COLOR_MAP, DEFAULT_FIELD_COLOR, TRUNCATE_FIELDS, ANSI_COLORS, GROUP_BY_FIELDS } = require('./constants');
2
- const { isBlacklisted } = require('./utils');
3
-
4
- // 分组字段显示名称
5
- const GROUP_BY_LABELS = {
6
- '@filename': 'File',
7
- '@source': 'IP',
8
- '@app_group': 'App',
9
- '@hostname': 'Machine',
10
- '_cluster': 'Cluster',
11
- };
12
-
13
- function highlightKeyword(text, keyword) {
14
- if (!keyword || !text) return text;
15
-
16
- const HIGHLIGHT_BG = '\x1b[43m';
17
- const HIGHLIGHT_FG = '\x1b[30m';
18
- const RESET = '\x1b[0m\x1b[49m';
19
-
20
- const keywords = Array.isArray(keyword) ? keyword : [keyword];
21
-
22
- let result = text;
23
- keywords.forEach(kw => {
24
- if (kw && typeof kw === 'string') {
25
- const escapedKeyword = kw.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
26
- const regex = new RegExp(`(${escapedKeyword})`, 'gi');
27
- result = result.replace(regex, `${HIGHLIGHT_BG}${HIGHLIGHT_FG}$1${RESET}`);
28
- }
29
- });
30
-
31
- return result;
32
- }
33
-
34
- function highlightFieldName(fieldName, keyword, originalColor) {
35
- if (!keyword || !fieldName) return originalColor + fieldName + '\x1b[0m';
36
-
37
- const HIGHLIGHT_BG = '\x1b[43m';
38
- const HIGHLIGHT_FG = '\x1b[30m';
39
- const RESET = '\x1b[0m\x1b[49m';
40
-
41
- const keywords = Array.isArray(keyword) ? keyword : [keyword];
42
-
43
- const shouldHighlight = keywords.some(kw => kw && typeof kw === 'string' && fieldName === kw);
44
-
45
- if (shouldHighlight) {
46
- return `${HIGHLIGHT_BG}${HIGHLIGHT_FG}${fieldName}${RESET}`;
47
- }
48
-
49
- return originalColor + fieldName + '\x1b[0m';
50
- }
51
-
52
- function truncateValue(value, fieldName, truncate) {
53
- if (truncate === 0) {
54
- return value;
55
- }
56
-
57
- if (TRUNCATE_FIELDS.includes(fieldName)) {
58
- const strValue = String(value);
59
- if (strValue.length > truncate) {
60
- return strValue.substring(0, truncate) + '...[truncated]';
61
- }
62
- }
63
- return value;
64
- }
65
-
66
- function applyHighlightAndTruncate(value, fieldName, truncate, keyword, highlight) {
67
- let strValue = typeof value === 'object' ? JSON.stringify(value) : String(value);
68
-
69
- strValue = truncateValue(strValue, fieldName, truncate);
70
-
71
- if (keyword && highlight) {
72
- strValue = highlightKeyword(strValue, keyword);
73
- }
74
-
75
- return strValue;
76
- }
77
-
78
- function getFieldColor(fieldName, isTTY) {
79
- if (!isTTY) return '';
80
- return FIELD_COLOR_MAP[fieldName] || DEFAULT_FIELD_COLOR;
81
- }
82
-
83
- function formatLogEntry(log, raw, logFormat, columns, truncate, keyword, highlight) {
84
- const isTTY = process.stdout.isTTY;
85
- const RESET = ANSI_COLORS.RESET;
86
-
87
- let columnFilter = null;
88
- if (columns) {
89
- columnFilter = columns.split(',').map(c => c.trim());
90
- }
91
-
92
- if (logFormat === 'json') {
93
- const output = {};
94
- Object.keys(log).forEach(key => {
95
- if (key.startsWith('@')) return;
96
- if (columnFilter && !columnFilter.includes(key)) return;
97
- if (log[key] === null || log[key] === undefined) return;
98
- output[key] = log[key];
99
- });
100
- console.log(JSON.stringify(output));
101
- return;
102
- }
103
-
104
- if (logFormat === 'columns') {
105
- const fieldsToDisplay = columnFilter || Object.keys(log).filter(k => !k.startsWith('@'));
106
- fieldsToDisplay.forEach(key => {
107
- if (log[key] === null || log[key] === undefined) return;
108
- const color = getFieldColor(key, isTTY);
109
- const value = applyHighlightAndTruncate(log[key], key, truncate, keyword, highlight);
110
- const highlightedKey = highlightFieldName(key, keyword && highlight ? keyword : [], color);
111
- console.log(`${highlightedKey}: ${value}`);
112
- });
113
- console.log('');
114
- return;
115
- }
116
-
117
- let formattedFields = [];
118
-
119
- if (raw) {
120
- const atFields = Object.keys(log).filter(fieldName =>
121
- fieldName.startsWith('@') &&
122
- (!columnFilter || columnFilter.includes(fieldName)) &&
123
- log[fieldName] !== null &&
124
- log[fieldName] !== undefined
125
- ).sort();
126
-
127
- const displayFields = DISPLAY_FIELDS.filter(fieldName =>
128
- !fieldName.startsWith('@') &&
129
- log.hasOwnProperty(fieldName) &&
130
- log[fieldName] !== null &&
131
- log[fieldName] !== undefined &&
132
- (!columnFilter || columnFilter.includes(fieldName))
133
- );
134
-
135
- const otherFields = Object.keys(log).filter(fieldName =>
136
- !fieldName.startsWith('@') &&
137
- !DISPLAY_FIELDS.includes(fieldName) &&
138
- (!columnFilter || columnFilter.includes(fieldName)) &&
139
- log[fieldName] !== null &&
140
- log[fieldName] !== undefined
141
- ).sort();
142
-
143
- atFields.forEach(fieldName => {
144
- const color = getFieldColor(fieldName, isTTY);
145
- const value = applyHighlightAndTruncate(log[fieldName], fieldName, truncate, keyword, highlight);
146
- const highlightedKey = highlightFieldName(fieldName, keyword && highlight ? keyword : [], color);
147
-
148
- let formattedValue = value;
149
- if (typeof value === 'string' && (value.includes(' ') || value.includes('"') || value.includes("'") || value.includes('=') || value.includes('\n') || value.includes('\r'))) {
150
- formattedValue = `"${value.replace(/"/g, '\\\"').replace(/\n/g, '\\n').replace(/\r/g, '\\r')}"`;
151
- }
152
-
153
- formattedFields.push(`${highlightedKey}=${formattedValue}${RESET}`);
154
- });
155
-
156
- displayFields.forEach(fieldName => {
157
- const color = getFieldColor(fieldName, isTTY);
158
- const value = applyHighlightAndTruncate(log[fieldName], fieldName, truncate, keyword, highlight);
159
- const highlightedKey = highlightFieldName(fieldName, keyword && highlight ? keyword : [], color);
160
-
161
- let formattedValue = value;
162
- if (typeof value === 'string' && (value.includes(' ') || value.includes('"') || value.includes("'") || value.includes('=') || value.includes('\n') || value.includes('\r'))) {
163
- formattedValue = `"${value.replace(/"/g, '\\\"').replace(/\n/g, '\\n').replace(/\r/g, '\\r')}"`;
164
- }
165
-
166
- formattedFields.push(`${highlightedKey}=${formattedValue}${RESET}`);
167
- });
168
-
169
- otherFields.forEach(fieldName => {
170
- const color = getFieldColor(fieldName, isTTY);
171
- const value = applyHighlightAndTruncate(log[fieldName], fieldName, truncate, keyword, highlight);
172
- const highlightedKey = highlightFieldName(fieldName, keyword && highlight ? keyword : [], color);
173
-
174
- let formattedValue = value;
175
- if (typeof value === 'string' && (value.includes(' ') || value.includes('"') || value.includes("'") || value.includes('=') || value.includes('\n') || value.includes('\r'))) {
176
- formattedValue = `"${value.replace(/"/g, '\\\"').replace(/\n/g, '\\n').replace(/\r/g, '\\r')}"`;
177
- }
178
-
179
- formattedFields.push(`${highlightedKey}=${formattedValue}${RESET}`);
180
- });
181
- } else {
182
- const fieldsToDisplay = columnFilter || DISPLAY_FIELDS;
183
-
184
- fieldsToDisplay.forEach(fieldName => {
185
- if (!log.hasOwnProperty(fieldName)) return;
186
- if (!raw && isBlacklisted(fieldName)) return;
187
- if (log[fieldName] === null || log[fieldName] === undefined) return;
188
-
189
- const color = getFieldColor(fieldName, isTTY);
190
- const value = applyHighlightAndTruncate(log[fieldName], fieldName, truncate, keyword, highlight);
191
- const highlightedKey = highlightFieldName(fieldName, keyword && highlight ? keyword : [], color);
192
-
193
- let formattedValue = value;
194
- if (typeof value === 'string' && (value.includes(' ') || value.includes('"') || value.includes("'") || value.includes('=') || value.includes('\n') || value.includes('\r'))) {
195
- formattedValue = `"${value.replace(/"/g, '\\\"').replace(/\n/g, '\\n').replace(/\r/g, '\\r')}"`;
196
- }
197
-
198
- formattedFields.push(`${highlightedKey}=${formattedValue}${RESET}`);
199
- });
200
- }
201
-
202
- if (formattedFields.length > 0) {
203
- console.log(formattedFields.join(' '));
204
- }
205
- }
206
-
207
- function displayLogsWithContext(logs, raw, logFormat, multilines, columns, truncate, keyword, highlight, showContextSeparators, groupByFields = null) {
208
- if (!logs || logs.length === 0) {
209
- return;
210
- }
211
-
212
- if (!showContextSeparators) {
213
- displayLogs(logs, raw, logFormat, multilines, columns, truncate, keyword, highlight, groupByFields);
214
- return;
215
- }
216
-
217
- const isTTY = process.stdout.isTTY;
218
- const CONTEXT_SEPARATOR = isTTY ? '\x1b[90m--\x1b[0m' : '--';
219
-
220
- const { areLogsConsecutive } = require('./utils');
221
- let previousLog = null;
222
- const logsToDisplay = [];
223
-
224
- logs.forEach((log) => {
225
- if (previousLog !== null && !areLogsConsecutive(previousLog, log)) {
226
- logsToDisplay.push({ _isSeparator: true });
227
- }
228
- logsToDisplay.push(log);
229
- previousLog = log;
230
- });
231
-
232
- logsToDisplay.forEach((item) => {
233
- if (item._isSeparator) {
234
- console.log(CONTEXT_SEPARATOR);
235
- } else {
236
- formatLogEntry(item, raw, logFormat, columns, truncate, keyword, highlight);
237
- }
238
- });
239
- }
240
-
241
- function displayLogs(logs, raw, logFormat, multilines, columns, truncate, keyword, highlight, groupByFields = null) {
242
- if (!logs || logs.length === 0) {
243
- return;
244
- }
245
-
246
- const isTTY = process.stdout.isTTY;
247
- const RESET = ANSI_COLORS.RESET;
248
-
249
- let columnFilter = null;
250
- if (columns) {
251
- columnFilter = columns.split(',').map(c => c.trim());
252
- }
253
-
254
- const resolvedGroupByFields = groupByFields
255
- ? groupByFields.map(field => GROUP_BY_FIELDS[field] || field)
256
- : [];
257
-
258
- let sortedLogs = logs;
259
- if (resolvedGroupByFields.length > 0) {
260
- sortedLogs = [...logs].sort((a, b) => {
261
- for (const field of resolvedGroupByFields) {
262
- const valA = a[field] || 'unknown';
263
- const valB = b[field] || 'unknown';
264
- if (valA !== valB) {
265
- return String(valA).localeCompare(String(valB));
266
- }
267
- }
268
- const timeA = parseInt(a['@timestamp']) || 0;
269
- const timeB = parseInt(b['@timestamp']) || 0;
270
- return timeA - timeB;
271
- });
272
- }
273
-
274
- let previousGroupValues = resolvedGroupByFields.map(() => null);
275
-
276
- sortedLogs.forEach((log, index) => {
277
- const currentGroupValues = resolvedGroupByFields.map(field => log[field] || 'unknown');
278
-
279
- if (resolvedGroupByFields.length > 0) {
280
- const isFirstLog = index === 0;
281
- const hasChanged = !isFirstLog && currentGroupValues.some((value, i) => value !== previousGroupValues[i]);
282
-
283
- if (hasChanged || isFirstLog) {
284
- if (!isFirstLog) {
285
- console.log('');
286
- if (isTTY) {
287
- console.log('\x1b[90m' + '─'.repeat(80) + '\x1b[0m');
288
- } else {
289
- console.log('─'.repeat(80));
290
- }
291
- }
292
-
293
- resolvedGroupByFields.forEach((field, i) => {
294
- if (isFirstLog || currentGroupValues[i] !== previousGroupValues[i]) {
295
- const label = GROUP_BY_LABELS[field] || field;
296
- const value = currentGroupValues[i];
297
- if (isTTY) {
298
- console.log('\x1b[36;1m' + `📦 ${label}: ${value}` + '\x1b[0m');
299
- } else {
300
- console.log(`${label}: ${value}`);
301
- }
302
- }
303
- });
304
- }
305
- }
306
-
307
- if (multilines && logFormat !== 'columns') {
308
- let fieldsToDisplay;
309
-
310
- if (raw) {
311
- if (columnFilter) {
312
- fieldsToDisplay = columnFilter;
313
- } else {
314
- const atFields = Object.keys(log).filter(fieldName =>
315
- fieldName.startsWith('@') &&
316
- log[fieldName] !== null &&
317
- log[fieldName] !== undefined
318
- ).sort();
319
-
320
- const displayFields = DISPLAY_FIELDS.filter(fieldName =>
321
- !fieldName.startsWith('@') &&
322
- log.hasOwnProperty(fieldName) &&
323
- log[fieldName] !== null &&
324
- log[fieldName] !== undefined
325
- );
326
-
327
- const otherFields = Object.keys(log).filter(fieldName =>
328
- !fieldName.startsWith('@') &&
329
- !DISPLAY_FIELDS.includes(fieldName) &&
330
- log[fieldName] !== null &&
331
- log[fieldName] !== undefined
332
- ).sort();
333
-
334
- fieldsToDisplay = [...atFields, ...displayFields, ...otherFields];
335
- }
336
- } else {
337
- fieldsToDisplay = columnFilter || Object.keys(log).filter(k => !k.startsWith('@'));
338
- }
339
-
340
- fieldsToDisplay.forEach(fieldName => {
341
- if (!log.hasOwnProperty(fieldName)) return;
342
- if (!raw && isBlacklisted(fieldName)) return;
343
- if (log[fieldName] === null || log[fieldName] === undefined) return;
344
-
345
- const color = getFieldColor(fieldName, isTTY);
346
- const value = applyHighlightAndTruncate(log[fieldName], fieldName, truncate, keyword, highlight);
347
- const highlightedKey = highlightFieldName(fieldName, keyword && highlight ? keyword : [], color);
348
-
349
- console.log(`${highlightedKey}=${value}${RESET}`);
350
- });
351
-
352
- console.log('\x1b[90m' + '='.repeat(80) + '\x1b[0m');
353
- } else {
354
- formatLogEntry(log, raw, logFormat, columns, truncate, keyword, highlight);
355
- }
356
-
357
- previousGroupValues = currentGroupValues;
358
- });
359
- }
360
-
361
- module.exports = {
362
- highlightKeyword,
363
- highlightFieldName,
364
- truncateValue,
365
- applyHighlightAndTruncate,
366
- getFieldColor,
367
- formatLogEntry,
368
- displayLogsWithContext,
369
- displayLogs,
370
- };
@@ -1,330 +0,0 @@
1
- // 条件引用:内网专有模块(开源版不存在)
2
- let log_provider, access_control, internal_context;
3
-
4
- try {
5
- log_provider = require('../../../lib/log_provider');
6
- } catch (e) {
7
- // 开源版:log_provider 不存在,使用空实现
8
- log_provider = null;
9
- }
10
-
11
- try {
12
- access_control = require('../../../lib/access_control');
13
- } catch (e) {
14
- // 开源版:无权限控制,使用空实现
15
- access_control = {
16
- checkLogSearchPermission: async () => ({ allowed: true }),
17
- getApiKeyFromSettings: () => null,
18
- };
19
- }
20
-
21
- try {
22
- internal_context = require('../internal/context');
23
- } catch (e) {
24
- // 开源版:无上下文查询,使用空实现
25
- internal_context = {
26
- fetchContext: async () => [],
27
- mergeAndDeduplicateLogs: (logs) => logs,
28
- };
29
- }
30
-
31
- // 通用依赖
32
- const logger = require('../../../utils/logger');
33
- const { parseTime, buildQueryString, getQueriesToRun } = require('./utils');
34
- const { displayLogsWithContext } = require('./display');
35
- const { gracefulExit } = require('../../../utils/execution_logger');
36
-
37
- // 解构赋值
38
- const { createClient: defaultCreateClient } = log_provider || {};
39
- const { checkLogSearchPermission, getApiKeyFromSettings } = access_control;
40
- const { fetchContext, mergeAndDeduplicateLogs } = internal_context;
41
-
42
- // 用于测试的依赖注入
43
- let clientFactory = null;
44
-
45
- /**
46
- * 设置自定义客户端工厂(用于测试)
47
- * @param {Function} factory - 客户端工厂函数
48
- */
49
- function setClientFactory(factory) {
50
- clientFactory = factory;
51
- }
52
-
53
- /**
54
- * 重置客户端工厂为默认值
55
- */
56
- function resetClientFactory() {
57
- clientFactory = null;
58
- }
59
-
60
- /**
61
- * 处理日志搜索命令
62
- * @param {Object} argv - 命令行参数
63
- */
64
- async function handleLogSearch(argv) {
65
- const createClient = clientFactory || defaultCreateClient;
66
- const { keyword, field, query, sandboxId, logFile, startTime, endTime, minutes, limit, offset, raw, cluster, debug, count, logFormat, multilines, columns, truncate, highlight, afterContext, beforeContext, context, groupBy, highlightSandboxId = true } = argv;
67
-
68
- // ===== 权限检查 =====
69
- const apiKey = getApiKeyFromSettings();
70
- const permissionResult = checkLogSearchPermission({
71
- apiKey,
72
- sandboxId,
73
- logFile,
74
- });
75
-
76
- if (!permissionResult.allowed) {
77
- logger.error(`❌ ${permissionResult.error}`);
78
- gracefulExit(1);
79
- }
80
-
81
- const userRole = permissionResult.role;
82
- // ===== 权限检查结束 =====
83
-
84
- const afterLines = context !== undefined ? context : (afterContext || 0);
85
- const beforeLines = context !== undefined ? context : (beforeContext || 0);
86
- const needContext = afterLines > 0 || beforeLines > 0;
87
-
88
- // 解析 groupBy 参数为数组
89
- let groupByFields = null;
90
- if (groupBy && groupBy.length > 0) {
91
- groupByFields = Array.isArray(groupBy) ? groupBy : groupBy.split(',').map(s => s.trim());
92
- }
93
-
94
- try {
95
- let actualStartTime = null;
96
- let actualEndTime = null;
97
-
98
- // 解析时间参数
99
- if (startTime || endTime) {
100
- if (startTime) {
101
- actualStartTime = parseTime(startTime);
102
- }
103
- if (endTime) {
104
- actualEndTime = parseTime(endTime);
105
- } else {
106
- actualEndTime = Date.now();
107
- }
108
- } else if (minutes) {
109
- actualStartTime = Date.now() - minutes * 60 * 1000;
110
- actualEndTime = Date.now();
111
- }
112
-
113
- if (!actualStartTime) {
114
- logger.error('❌ Error: Start time is required (use --start-time or --minutes)');
115
- gracefulExit(1);
116
- }
117
-
118
- // 规范化时间戳(末尾3位为0)
119
- actualStartTime = Math.floor(actualStartTime / 1000) * 1000;
120
- actualEndTime = Math.floor(actualEndTime / 1000) * 1000;
121
-
122
- // 构建查询字符串(传递用户角色用于权限控制)
123
- const queryString = buildQueryString({ keyword, field, query, sandboxId, logFile, role: userRole });
124
-
125
- // 获取要查询的集群和表
126
- const queriesToRun = getQueriesToRun(cluster);
127
-
128
- // 生成 trace ID 用于调试关联
129
- let traceCounter = 0;
130
- const generateTraceId = () => {
131
- traceCounter++;
132
- return String(traceCounter).padStart(2, '0');
133
- };
134
-
135
- // 并行查询所有集群和日志类型
136
- const allLogs = [];
137
-
138
- await Promise.all(queriesToRun.map(async ({ clusterName, logType, tableName }) => {
139
- const mysqlClient = createClient();
140
- const traceId = generateTraceId();
141
-
142
- try {
143
- await mysqlClient.connect();
144
-
145
- const queryOptions = {
146
- table: tableName,
147
- startTime: actualStartTime,
148
- endTime: actualEndTime,
149
- filter: queryString || null,
150
- limit: limit,
151
- offset: offset,
152
- count: count,
153
- orderBy: '@timestamp',
154
- orderDirection: 'ASC'
155
- };
156
-
157
- if (debug) {
158
- logger.debug(`🔍 Debug [${traceId}] Cluster: ${clusterName}, LogType: ${logType}, Table: ${tableName}`);
159
- logger.debug(`🔍 Debug [${traceId}] Filter: ${queryString || '(none)'}`);
160
-
161
- const conditions = [`IN_RANGE(\`@timestamp\`, ${actualStartTime}, ${actualEndTime})`];
162
- if (queryString) {
163
- const escapedQuery = queryString.replace(/'/g, "''");
164
- conditions.push(`query_string('${escapedQuery}')`);
165
- }
166
- const whereClause = conditions.join(' AND ');
167
- let sql;
168
- if (count) {
169
- sql = `SELECT count(*) as total FROM \`${tableName}\` WHERE ${whereClause}`;
170
- } else {
171
- sql = `SELECT *, \`@packId\`, \`@logGroupOrder\` FROM \`${tableName}\` WHERE ${whereClause} ORDER BY \`@timestamp\`, \`@packId\`, \`@logGroupOrder\` LIMIT ${limit} OFFSET ${offset}`;
172
- }
173
- }
174
-
175
- const logs = await mysqlClient.query(queryOptions);
176
-
177
- if (debug) {
178
- logger.debug(`📊 Debug [${traceId}] Returned rows: ${logs ? logs.length : 0}`);
179
- if (logs && logs.length > 0) {
180
- const firstLog = logs[0];
181
- const hasPackId = firstLog.hasOwnProperty('@packId');
182
- const hasLogGroupOrder = firstLog.hasOwnProperty('@logGroupOrder');
183
- logger.debug(`🔍 Debug - First row has @packId: ${hasPackId} (value: ${firstLog['@packId']})`);
184
- logger.debug(`🔍 Debug - First row has @logGroupOrder: ${hasLogGroupOrder} (value: ${firstLog['@logGroupOrder']})`);
185
- }
186
- logger.debug('');
187
- }
188
-
189
- if (logs && logs.length > 0) {
190
- logs.forEach(log => {
191
- log._cluster = clusterName;
192
- log._tableName = tableName;
193
- });
194
- allLogs.push(...logs);
195
- }
196
- } catch (error) {
197
- logger.error(`❌ Error querying cluster ${clusterName}: ${error.message}`);
198
- } finally {
199
- await mysqlClient.close();
200
- }
201
- }));
202
-
203
- // 按时间戳和 packId 排序
204
- allLogs.sort((a, b) => {
205
- const timeA = parseInt(a['@timestamp']) || 0;
206
- const timeB = parseInt(b['@timestamp']) || 0;
207
- if (timeA !== timeB) return timeA - timeB;
208
-
209
- const packIdA = parseInt(a['@packId']) || 0;
210
- const packIdB = parseInt(b['@packId']) || 0;
211
- if (packIdA !== packIdB) return packIdA - packIdB;
212
-
213
- const orderA = parseInt(a['@logGroupOrder']) || 0;
214
- const orderB = parseInt(b['@logGroupOrder']) || 0;
215
- return orderA - orderB;
216
- });
217
-
218
- // 如果需要上下文查询
219
- let finalLogs = allLogs;
220
- if (needContext && allLogs.length > 0 && !count) {
221
- if (debug) {
222
- logger.debug(`🔍 Debug - Context query enabled: before=${beforeLines}, after=${afterLines}`);
223
- logger.debug(`🔍 Debug - Matched logs count: ${allLogs.length}`);
224
- }
225
-
226
- allLogs.forEach(log => {
227
- log._isMatch = true;
228
- });
229
-
230
- const contextPromises = allLogs.map(log => {
231
- const logTableName = log._tableName;
232
- if (!logTableName) {
233
- if (debug) {
234
- logger.warn(`⚠️ Debug - Log missing _tableName, skipping context query`);
235
- }
236
- return Promise.resolve({ before: [], after: [] });
237
- }
238
- return fetchContext({
239
- log,
240
- tableName: logTableName,
241
- startTime: actualStartTime,
242
- endTime: actualEndTime,
243
- beforeLines,
244
- afterLines,
245
- debug
246
- });
247
- });
248
-
249
- const contextResults = await Promise.all(contextPromises);
250
-
251
- const allContextLogs = [];
252
- contextResults.forEach(result => {
253
- if (result.before && result.before.length > 0) {
254
- allContextLogs.push(...result.before);
255
- }
256
- if (result.after && result.after.length > 0) {
257
- allContextLogs.push(...result.after);
258
- }
259
- });
260
-
261
- if (debug) {
262
- logger.debug(`🔍 Debug - Context logs fetched: ${allContextLogs.length}`);
263
- }
264
-
265
- finalLogs = mergeAndDeduplicateLogs(allLogs, allContextLogs);
266
-
267
- if (debug) {
268
- logger.debug(`🔍 Debug - Final logs after merge: ${finalLogs.length}`);
269
- logger.debug('');
270
- }
271
- }
272
-
273
- // 显示结果
274
- if (count) {
275
- const totalCount = allLogs.reduce((sum, log) => sum + (parseInt(log.total) || 0), 0);
276
- console.log(totalCount);
277
- } else {
278
- // 提取所有需要高亮的关键词
279
- let highlightKeywords = [];
280
-
281
- // 添加 keyword 参数
282
- if (keyword) {
283
- highlightKeywords = highlightKeywords.concat(Array.isArray(keyword) ? keyword : [keyword]);
284
- }
285
-
286
- // 添加 field 参数中的值(等值匹配和比较运算符)
287
- if (field && field.length > 0) {
288
- field.forEach(f => {
289
- const greaterMatch = f.match(/^(\w+)>(.*)$/);
290
- const greaterEqualMatch = f.match(/^(\w+)>=(.*)$/);
291
- const lessMatch = f.match(/^(\w+)<(.*)$/);
292
- const lessEqualMatch = f.match(/^(\w+)<=(.*)$/);
293
- const notEqualMatch = f.match(/^(\w+)!=(.*)$/);
294
- const equalMatch = f.match(/^([^=<>!]+)=(.*)$/);
295
-
296
- if (greaterMatch || greaterEqualMatch || lessMatch || lessEqualMatch) {
297
- const match = greaterMatch || greaterEqualMatch || lessMatch || lessEqualMatch;
298
- const [, fieldName] = match;
299
- highlightKeywords.push(fieldName);
300
- } else if (notEqualMatch) {
301
- const [, fieldName, fieldValue] = notEqualMatch;
302
- highlightKeywords.push(fieldValue);
303
- } else if (equalMatch) {
304
- const [, fieldName, fieldValue] = equalMatch;
305
- highlightKeywords.push(fieldValue);
306
- }
307
- });
308
- }
309
-
310
- // 添加 sandboxId
311
- if (sandboxId && highlightSandboxId !== false) {
312
- highlightKeywords.push(sandboxId);
313
- }
314
-
315
- displayLogsWithContext(finalLogs, raw, logFormat, multilines, columns, truncate, highlightKeywords, highlight, needContext, groupByFields);
316
- }
317
- } catch (error) {
318
- logger.error(`❌ Search failed: ${error.message}`);
319
- if (error.stack) {
320
- logger.error('Stack trace:', error.stack);
321
- }
322
- gracefulExit(1);
323
- }
324
- }
325
-
326
- module.exports = {
327
- handleLogSearch,
328
- setClientFactory,
329
- resetClientFactory,
330
- };