Skip to content

Commit 10599f4

Browse files
authored
new-log-viewer: Add log query support in StateContextProvider. (#80)
1 parent 31786da commit 10599f4

File tree

6 files changed

+228
-27
lines changed

6 files changed

+228
-27
lines changed

new-log-viewer/src/contexts/StateContextProvider.tsx

+40-2
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {
2121
EVENT_POSITION_ON_PAGE,
2222
FileSrcType,
2323
MainWorkerRespMessage,
24+
QueryResults,
2425
WORKER_REQ_CODE,
2526
WORKER_RESP_CODE,
2627
WorkerReq,
@@ -57,11 +58,13 @@ interface StateContextType {
5758
numPages: number,
5859
onDiskFileSizeInBytes: number,
5960
pageNum: number,
61+
queryResults: QueryResults,
6062

6163
exportLogs: () => void,
6264
loadFile: (fileSrc: FileSrcType, cursor: CursorType) => void,
6365
loadPageByAction: (navAction: NavigationAction) => void,
6466
setLogLevelFilter: (newLogLevelFilter: LogLevelFilter) => void,
67+
startQuery: (queryString: string, isRegex: boolean, isCaseSensitive: boolean) => void,
6568
}
6669
const StateContext = createContext<StateContextType>({} as StateContextType);
6770

@@ -77,11 +80,13 @@ const STATE_DEFAULT: Readonly<StateContextType> = Object.freeze({
7780
numPages: 0,
7881
onDiskFileSizeInBytes: 0,
7982
pageNum: 0,
83+
queryResults: new Map(),
8084

8185
exportLogs: () => null,
8286
loadFile: () => null,
8387
loadPageByAction: () => null,
8488
setLogLevelFilter: () => null,
89+
startQuery: () => null,
8590
});
8691

8792
interface StateContextProviderProps {
@@ -229,17 +234,18 @@ const StateContextProvider = ({children}: StateContextProviderProps) => {
229234
const {filePath, logEventNum} = useContext(UrlContext);
230235

231236
// States
237+
const [exportProgress, setExportProgress] =
238+
useState<Nullable<number>>(STATE_DEFAULT.exportProgress);
232239
const [fileName, setFileName] = useState<string>(STATE_DEFAULT.fileName);
233240
const [logData, setLogData] = useState<string>(STATE_DEFAULT.logData);
234241
const [numEvents, setNumEvents] = useState<number>(STATE_DEFAULT.numEvents);
235242
const [numPages, setNumPages] = useState<number>(STATE_DEFAULT.numPages);
236243
const [onDiskFileSizeInBytes, setOnDiskFileSizeInBytes] =
237244
useState(STATE_DEFAULT.onDiskFileSizeInBytes);
238245
const [pageNum, setPageNum] = useState<number>(STATE_DEFAULT.pageNum);
246+
const [queryResults, setQueryResults] = useState<QueryResults>(STATE_DEFAULT.queryResults);
239247
const beginLineNumToLogEventNumRef =
240248
useRef<BeginLineNumToLogEventNumMap>(STATE_DEFAULT.beginLineNumToLogEventNum);
241-
const [exportProgress, setExportProgress] =
242-
useState<Nullable<number>>(STATE_DEFAULT.exportProgress);
243249

244250
// Refs
245251
const logEventNumRef = useRef(logEventNum);
@@ -281,12 +287,42 @@ const StateContextProvider = ({children}: StateContextProviderProps) => {
281287
});
282288
break;
283289
}
290+
case WORKER_RESP_CODE.QUERY_RESULT:
291+
setQueryResults((v) => {
292+
args.results.forEach((resultsPerPage, queryPageNum) => {
293+
if (false === v.has(queryPageNum)) {
294+
v.set(queryPageNum, []);
295+
}
296+
v.get(queryPageNum)?.push(...resultsPerPage);
297+
});
298+
299+
return v;
300+
});
301+
break;
284302
default:
285303
console.error(`Unexpected ev.data: ${JSON.stringify(ev.data)}`);
286304
break;
287305
}
288306
}, [postPopUp]);
289307

308+
const startQuery = useCallback((
309+
queryString: string,
310+
isRegex: boolean,
311+
isCaseSensitive: boolean
312+
) => {
313+
setQueryResults(STATE_DEFAULT.queryResults);
314+
if (null === mainWorkerRef.current) {
315+
console.error("Unexpected null mainWorkerRef.current");
316+
317+
return;
318+
}
319+
workerPostReq(mainWorkerRef.current, WORKER_REQ_CODE.START_QUERY, {
320+
queryString: queryString,
321+
isRegex: isRegex,
322+
isCaseSensitive: isCaseSensitive,
323+
});
324+
}, []);
325+
290326
const exportLogs = useCallback(() => {
291327
if (null === mainWorkerRef.current) {
292328
console.error("Unexpected null mainWorkerRef.current");
@@ -442,11 +478,13 @@ const StateContextProvider = ({children}: StateContextProviderProps) => {
442478
numPages: numPages,
443479
onDiskFileSizeInBytes: onDiskFileSizeInBytes,
444480
pageNum: pageNum,
481+
queryResults: queryResults,
445482

446483
exportLogs: exportLogs,
447484
loadFile: loadFile,
448485
loadPageByAction: loadPageByAction,
449486
setLogLevelFilter: setLogLevelFilter,
487+
startQuery: startQuery,
450488
}}
451489
>
452490
{children}

new-log-viewer/src/services/LogFileManager/index.ts

+117-14
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
/* eslint max-lines: ["error", 400] */
12
import {
23
Decoder,
34
DecoderOptionsType,
@@ -11,11 +12,16 @@ import {
1112
CursorType,
1213
EMPTY_PAGE_RESP,
1314
FileSrcType,
15+
QueryResults,
1416
WORKER_RESP_CODE,
1517
WorkerResp,
1618
} from "../../typings/worker";
17-
import {EXPORT_LOGS_CHUNK_SIZE} from "../../utils/config";
19+
import {
20+
EXPORT_LOGS_CHUNK_SIZE,
21+
QUERY_CHUNK_SIZE,
22+
} from "../../utils/config";
1823
import {getChunkNum} from "../../utils/math";
24+
import {defer} from "../../utils/time";
1925
import {formatSizeInBytes} from "../../utils/units";
2026
import ClpIrDecoder from "../decoders/ClpIrDecoder";
2127
import JsonlDecoder from "../decoders/JsonlDecoder";
@@ -31,35 +37,43 @@ import {
3137
* Class to manage the retrieval and decoding of a given log file.
3238
*/
3339
class LogFileManager {
40+
readonly #fileName: string;
41+
42+
readonly #numEvents: number = 0;
43+
3444
readonly #pageSize: number;
3545

36-
readonly #fileName: string;
46+
#queryId: number = 0;
3747

3848
readonly #onDiskFileSizeInBytes: number;
3949

40-
#decoder: Decoder;
50+
readonly #onQueryResults: (queryResults: QueryResults) => void;
4151

42-
#numEvents: number = 0;
52+
#decoder: Decoder;
4353

4454
/**
4555
* Private constructor for LogFileManager. This is not intended to be invoked publicly.
4656
* Instead, use LogFileManager.create() to create a new instance of the class.
4757
*
48-
* @param decoder
49-
* @param fileName
50-
* @param onDiskFileSizeInBytes
51-
* @param pageSize Page size for setting up pagination.
58+
* @param params
59+
* @param params.decoder
60+
* @param params.fileName
61+
* @param params.onDiskFileSizeInBytes
62+
* @param params.pageSize Page size for setting up pagination.
63+
* @param params.onQueryResults
5264
*/
53-
constructor (
65+
constructor ({decoder, fileName, onDiskFileSizeInBytes, pageSize, onQueryResults}: {
5466
decoder: Decoder,
5567
fileName: string,
5668
onDiskFileSizeInBytes: number,
5769
pageSize: number,
58-
) {
70+
onQueryResults: (queryResults: QueryResults) => void,
71+
}) {
72+
this.#decoder = decoder;
5973
this.#fileName = fileName;
60-
this.#onDiskFileSizeInBytes = onDiskFileSizeInBytes;
6174
this.#pageSize = pageSize;
62-
this.#decoder = decoder;
75+
this.#onDiskFileSizeInBytes = onDiskFileSizeInBytes;
76+
this.#onQueryResults = onQueryResults;
6377

6478
// Build index for the entire file.
6579
const buildResult = decoder.build();
@@ -90,17 +104,26 @@ class LogFileManager {
90104
* File object.
91105
* @param pageSize Page size for setting up pagination.
92106
* @param decoderOptions Initial decoder options.
107+
* @param onQueryResults
93108
* @return A Promise that resolves to the created LogFileManager instance.
94109
*/
95110
static async create (
96111
fileSrc: FileSrcType,
97112
pageSize: number,
98-
decoderOptions: DecoderOptionsType
113+
decoderOptions: DecoderOptionsType,
114+
onQueryResults: (queryResults: QueryResults) => void,
99115
): Promise<LogFileManager> {
100116
const {fileName, fileData} = await loadFile(fileSrc);
101117
const decoder = await LogFileManager.#initDecoder(fileName, fileData, decoderOptions);
102118

103-
return new LogFileManager(decoder, fileName, fileData.length, pageSize);
119+
return new LogFileManager({
120+
decoder: decoder,
121+
fileName: fileName,
122+
onDiskFileSizeInBytes: fileData.length,
123+
pageSize: pageSize,
124+
125+
onQueryResults: onQueryResults,
126+
});
104127
}
105128

106129
/**
@@ -254,6 +277,86 @@ class LogFileManager {
254277
};
255278
}
256279

280+
/**
281+
* Creates a RegExp object based on the given query string and options, and starts querying the
282+
* first log chunk.
283+
*
284+
* @param queryString
285+
* @param isRegex
286+
* @param isCaseSensitive
287+
*/
288+
startQuery (queryString: string, isRegex: boolean, isCaseSensitive: boolean): void {
289+
this.#queryId++;
290+
291+
// If the query string is empty, or there are no logs, return
292+
if ("" === queryString || 0 === this.#numEvents) {
293+
return;
294+
}
295+
296+
// Construct query RegExp
297+
const regexPattern = isRegex ?
298+
queryString :
299+
queryString.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
300+
const regexFlags = isCaseSensitive ?
301+
"" :
302+
"i";
303+
const queryRegex = new RegExp(regexPattern, regexFlags);
304+
305+
this.#queryChunkAndScheduleNext(this.#queryId, 0, queryRegex);
306+
}
307+
308+
/**
309+
* Queries a chunk of log events, sends the results, and schedules the next chunk query if more
310+
* log events remain.
311+
*
312+
* @param queryId
313+
* @param chunkBeginIdx
314+
* @param queryRegex
315+
*/
316+
#queryChunkAndScheduleNext (
317+
queryId: number,
318+
chunkBeginIdx: number,
319+
queryRegex: RegExp
320+
): void {
321+
if (queryId !== this.#queryId) {
322+
// Current task no longer corresponds to the latest query in the LogFileManager.
323+
return;
324+
}
325+
const chunkEndIdx = Math.min(chunkBeginIdx + QUERY_CHUNK_SIZE, this.#numEvents);
326+
const results: QueryResults = new Map();
327+
const decodedEvents = this.#decoder.decodeRange(
328+
chunkBeginIdx,
329+
chunkEndIdx,
330+
null !== this.#decoder.getFilteredLogEventMap()
331+
);
332+
333+
decodedEvents?.forEach(([message, , , logEventNum]) => {
334+
const matchResult = message.match(queryRegex);
335+
if (null !== matchResult && "number" === typeof matchResult.index) {
336+
const pageNum = Math.ceil(logEventNum / this.#pageSize);
337+
if (false === results.has(pageNum)) {
338+
results.set(pageNum, []);
339+
}
340+
results.get(pageNum)?.push({
341+
logEventNum: logEventNum,
342+
message: message,
343+
matchRange: [
344+
matchResult.index,
345+
(matchResult.index + matchResult[0].length),
346+
],
347+
});
348+
}
349+
});
350+
351+
this.#onQueryResults(results);
352+
353+
if (chunkEndIdx < this.#numEvents) {
354+
defer(() => {
355+
this.#queryChunkAndScheduleNext(queryId, chunkEndIdx, queryRegex);
356+
});
357+
}
358+
}
359+
257360
/**
258361
* Gets the data that corresponds to the cursor.
259362
*

new-log-viewer/src/services/MainWorker.ts

+30-1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import dayjsUtc from "dayjs/plugin/utc";
55
import {LOG_LEVEL} from "../typings/logs";
66
import {
77
MainWorkerReqMessage,
8+
QueryResults,
89
WORKER_REQ_CODE,
910
WORKER_RESP_CODE,
1011
WorkerResp,
@@ -36,6 +37,16 @@ const postResp = <T extends WORKER_RESP_CODE>(
3637
postMessage({code, args});
3738
};
3839

40+
41+
/**
42+
* Post a response for a chunk of query results.
43+
*
44+
* @param queryResults
45+
*/
46+
const onQueryResults = (queryResults: QueryResults) => {
47+
postResp(WORKER_RESP_CODE.QUERY_RESULT, {results: queryResults});
48+
};
49+
3950
// eslint-disable-next-line no-warning-comments
4051
// TODO: Break this function up into smaller functions.
4152
// eslint-disable-next-line max-lines-per-function,max-statements
@@ -63,7 +74,8 @@ onmessage = async (ev: MessageEvent<MainWorkerReqMessage>) => {
6374
LOG_FILE_MANAGER = await LogFileManager.create(
6475
args.fileSrc,
6576
args.pageSize,
66-
args.decoderOptions
77+
args.decoderOptions,
78+
onQueryResults
6779
);
6880

6981
postResp(WORKER_RESP_CODE.LOG_FILE_INFO, {
@@ -97,6 +109,23 @@ onmessage = async (ev: MessageEvent<MainWorkerReqMessage>) => {
97109
LOG_FILE_MANAGER.loadPage(args.cursor)
98110
);
99111
break;
112+
case WORKER_REQ_CODE.START_QUERY:
113+
if (null === LOG_FILE_MANAGER) {
114+
throw new Error("Log file manager hasn't been initialized");
115+
}
116+
if (
117+
"string" !== typeof args.queryString ||
118+
"boolean" !== typeof args.isRegex ||
119+
"boolean" !== typeof args.isCaseSensitive
120+
) {
121+
throw new Error("Invalid arguments for QUERY_LOG");
122+
}
123+
LOG_FILE_MANAGER.startQuery(
124+
args.queryString,
125+
args.isRegex,
126+
args.isCaseSensitive
127+
);
128+
break;
100129
default:
101130
console.error(`Unexpected ev.data: ${JSON.stringify(ev.data)}`);
102131
break;

0 commit comments

Comments
 (0)