diff --git a/new-log-viewer/src/services/LogFileManager/index.ts b/new-log-viewer/src/services/LogFileManager/index.ts index 36a00e33..3767c92a 100644 --- a/new-log-viewer/src/services/LogFileManager/index.ts +++ b/new-log-viewer/src/services/LogFileManager/index.ts @@ -1,7 +1,6 @@ import { Decoder, DecoderOptionsType, - LOG_EVENT_FILE_END_IDX, } from "../../typings/decoders"; import {MAX_V8_STRING_LENGTH} from "../../typings/js"; import { @@ -52,10 +51,10 @@ class LogFileManager { this.#pageSize = pageSize; this.#decoder = decoder; - // Build index for the entire file - const buildIdxResult = decoder.buildIdx(0, LOG_EVENT_FILE_END_IDX); - if (null !== buildIdxResult && 0 < buildIdxResult.numInvalidEvents) { - console.error("Invalid events found in decoder.buildIdx():", buildIdxResult); + // Build index for the entire file. + const buildResult = decoder.build(); + if (0 < buildResult.numInvalidEvents) { + console.error("Invalid events found in decoder.build():", buildResult); } this.#numEvents = decoder.getEstimatedNumEvents(); @@ -122,13 +121,11 @@ class LogFileManager { return decoder; } - /** - * Sets options for the decoder. - * + /* Sets any formatter options that exist in the decoder's options. * @param options */ - setDecoderOptions (options: DecoderOptionsType) { - this.#decoder.setDecoderOptions(options); + setFormatterOptions (options: DecoderOptionsType) { + this.#decoder.setFormatterOptions(options); } /** @@ -144,9 +141,10 @@ class LogFileManager { logs: string, } { const endLogEventIdx = Math.min(beginLogEventIdx + EXPORT_LOGS_CHUNK_SIZE, this.#numEvents); - const results = this.#decoder.decode( + const results = this.#decoder.decodeRange( beginLogEventIdx, - endLogEventIdx + endLogEventIdx, + false, ); if (null === results) { @@ -185,7 +183,12 @@ class LogFileManager { matchingLogEventNum, } = this.#getCursorData(cursor); - const results = this.#decoder.decode(pageBeginLogEventNum - 1, pageEndLogEventNum - 1); + const results = this.#decoder.decodeRange( + pageBeginLogEventNum - 1, + pageEndLogEventNum - 1, + false + ); + if (null === results) { throw new Error("Error occurred during decoding. " + `pageBeginLogEventNum=${pageBeginLogEventNum}, ` + diff --git a/new-log-viewer/src/services/MainWorker.ts b/new-log-viewer/src/services/MainWorker.ts index a04d134c..4fe43c8d 100644 --- a/new-log-viewer/src/services/MainWorker.ts +++ b/new-log-viewer/src/services/MainWorker.ts @@ -50,7 +50,7 @@ onmessage = async (ev: MessageEvent) => { throw new Error("Log file manager hasn't been initialized"); } if ("undefined" !== typeof args.decoderOptions) { - LOG_FILE_MANAGER.setDecoderOptions(args.decoderOptions); + LOG_FILE_MANAGER.setFormatterOptions(args.decoderOptions); } let decodedEventIdx = 0; @@ -85,7 +85,7 @@ onmessage = async (ev: MessageEvent) => { throw new Error("Log file manager hasn't been initialized"); } if ("undefined" !== typeof args.decoderOptions) { - LOG_FILE_MANAGER.setDecoderOptions(args.decoderOptions); + LOG_FILE_MANAGER.setFormatterOptions(args.decoderOptions); } postResp( WORKER_RESP_CODE.PAGE_DATA, diff --git a/new-log-viewer/src/services/decoders/ClpIrDecoder.ts b/new-log-viewer/src/services/decoders/ClpIrDecoder.ts index 4cab4590..e6b439a3 100644 --- a/new-log-viewer/src/services/decoders/ClpIrDecoder.ts +++ b/new-log-viewer/src/services/decoders/ClpIrDecoder.ts @@ -4,8 +4,11 @@ import {Nullable} from "../../typings/common"; import { Decoder, DecodeResultType, + FilteredLogEventMap, + LOG_EVENT_FILE_END_IDX, LogEventCount, } from "../../typings/decoders"; +import {LogLevelFilter} from "../../typings/logs"; class ClpIrDecoder implements Decoder { @@ -31,19 +34,42 @@ class ClpIrDecoder implements Decoder { return this.#streamReader.getNumEventsBuffered(); } - buildIdx (beginIdx: number, endIdx: number): Nullable { + // eslint-disable-next-line class-methods-use-this + getFilteredLogEventMap (): FilteredLogEventMap { + // eslint-disable-next-line no-warning-comments + // TODO: Update this after log level filtering is implemented in clp-ffi-js + console.error("Not implemented."); + + return null; + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars, class-methods-use-this + setLogLevelFilter (logLevelFilter: LogLevelFilter): boolean { + // eslint-disable-next-line no-warning-comments + // TODO: Update this after log level filtering is implemented in clp-ffi-js + console.error("Not implemented."); + + return false; + } + + build (): LogEventCount { return { numInvalidEvents: 0, - numValidEvents: this.#streamReader.deserializeRange(beginIdx, endIdx), + numValidEvents: this.#streamReader.deserializeRange(0, LOG_EVENT_FILE_END_IDX), }; } // eslint-disable-next-line class-methods-use-this - setDecoderOptions (): boolean { + setFormatterOptions (): boolean { return true; } - decode (beginIdx: number, endIdx: number): Nullable { + decodeRange ( + beginIdx: number, + endIdx: number, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + useFilter: boolean + ): Nullable { return this.#streamReader.decodeRange(beginIdx, endIdx); } } diff --git a/new-log-viewer/src/services/decoders/JsonlDecoder.ts b/new-log-viewer/src/services/decoders/JsonlDecoder.ts deleted file mode 100644 index 62a5b21e..00000000 --- a/new-log-viewer/src/services/decoders/JsonlDecoder.ts +++ /dev/null @@ -1,186 +0,0 @@ -import {Nullable} from "../../typings/common"; -import { - Decoder, - DecodeResultType, - JsonlDecoderOptionsType, - LogEventCount, -} from "../../typings/decoders"; -import {Formatter} from "../../typings/formatters"; -import { - JsonObject, - JsonValue, -} from "../../typings/js"; -import { - INVALID_TIMESTAMP_VALUE, - LOG_LEVEL, -} from "../../typings/logs"; -import LogbackFormatter from "../formatters/LogbackFormatter"; - - -/** - * A decoder for JSONL (JSON lines) files that contain log events. See `JsonlDecodeOptionsType` for - * properties that are specific to log events (compared to generic JSON records). - */ -class JsonlDecoder implements Decoder { - static #textDecoder = new TextDecoder(); - - #logLevelKey: string = "level"; - - #logEvents: JsonObject[] = []; - - #invalidLogEventIdxToRawLine: Map = new Map(); - - // @ts-expect-error #fomatter is set in the constructor by `setDecoderOptions()` - #formatter: Formatter; - - /** - * @param dataArray - * @param decoderOptions - * @throws {Error} if the initial decoder options are erroneous. - */ - constructor (dataArray: Uint8Array, decoderOptions: JsonlDecoderOptionsType) { - const isOptionSet = this.setDecoderOptions(decoderOptions); - if (false === isOptionSet) { - throw new Error( - `Initial decoder options are erroneous: ${JSON.stringify(decoderOptions)}` - ); - } - this.#deserialize(dataArray); - } - - getEstimatedNumEvents (): number { - return this.#logEvents.length; - } - - buildIdx (beginIdx: number, endIdx: number): Nullable { - // This method is a dummy implementation since the actual deserialization is done in the - // constructor. - - if (0 > beginIdx || this.#logEvents.length < endIdx) { - return null; - } - - const numInvalidEvents = Array.from(this.#invalidLogEventIdxToRawLine.keys()) - .filter((eventIdx) => (beginIdx <= eventIdx && eventIdx < endIdx)) - .length; - - return { - numValidEvents: endIdx - beginIdx - numInvalidEvents, - numInvalidEvents: numInvalidEvents, - }; - } - - setDecoderOptions (options: JsonlDecoderOptionsType): boolean { - this.#formatter = new LogbackFormatter(options); - this.#logLevelKey = options.logLevelKey; - - return true; - } - - decode (beginIdx: number, endIdx: number): Nullable { - if (0 > beginIdx || this.#logEvents.length < endIdx) { - return null; - } - - // eslint-disable-next-line no-warning-comments - // TODO We could probably optimize this to avoid checking `#invalidLogEventIdxToRawLine` on - // every iteration. - const results: DecodeResultType[] = []; - for (let logEventIdx = beginIdx; logEventIdx < endIdx; logEventIdx++) { - let timestamp: number; - let message: string; - let logLevel: LOG_LEVEL; - if (this.#invalidLogEventIdxToRawLine.has(logEventIdx)) { - timestamp = INVALID_TIMESTAMP_VALUE; - message = `${this.#invalidLogEventIdxToRawLine.get(logEventIdx)}\n`; - logLevel = LOG_LEVEL.NONE; - } else { - // Explicit cast since typescript thinks `#logEvents[logEventIdx]` can be undefined, - // but it shouldn't be since we performed a bounds check at the beginning of the - // method. - const logEvent = this.#logEvents[logEventIdx] as JsonObject; - ( - {timestamp, message} = this.#formatter.formatLogEvent(logEvent) - ); - logLevel = this.#parseLogLevel(logEvent); - } - - results.push([ - message, - timestamp, - logLevel, - logEventIdx + 1, - ]); - } - - return results; - } - - /** - * Parses each line from the given data array as a JSON object and buffers it internally. If a - * line cannot be parsed as a JSON object, an error is logged and the line is skipped. - * - * @param dataArray - */ - #deserialize (dataArray: Uint8Array) { - const text = JsonlDecoder.#textDecoder.decode(dataArray); - let beginIdx = 0; - while (beginIdx < text.length) { - const endIdx = text.indexOf("\n", beginIdx); - const line = (-1 === endIdx) ? - text.substring(beginIdx) : - text.substring(beginIdx, endIdx); - - beginIdx = (-1 === endIdx) ? - text.length : - endIdx + 1; - - try { - const logEvent = JSON.parse(line) as JsonValue; - if ("object" !== typeof logEvent) { - throw new Error("Unexpected non-object."); - } - this.#logEvents.push(logEvent as JsonObject); - } catch (e) { - if (0 === line.length) { - continue; - } - console.error(e, line); - const currentLogEventIdx = this.#logEvents.length; - this.#invalidLogEventIdxToRawLine.set(currentLogEventIdx, line); - this.#logEvents.push({}); - } - } - } - - /** - * Parses the log level from the given log event. - * - * @param logEvent - * @return The parsed log level. - */ - #parseLogLevel (logEvent: JsonObject): number { - let logLevel = LOG_LEVEL.NONE; - - const parsedLogLevel = logEvent[this.#logLevelKey]; - if ("undefined" === typeof parsedLogLevel) { - console.error(`${this.#logLevelKey} doesn't exist in log event.`); - - return logLevel; - } - - const logLevelStr = "object" === typeof parsedLogLevel ? - JSON.stringify(parsedLogLevel) : - String(parsedLogLevel); - - if (false === (logLevelStr.toUpperCase() in LOG_LEVEL)) { - console.error(`${logLevelStr} doesn't match any known log level.`); - } else { - logLevel = LOG_LEVEL[logLevelStr as (keyof typeof LOG_LEVEL)]; - } - - return logLevel; - } -} - -export default JsonlDecoder; diff --git a/new-log-viewer/src/services/decoders/JsonlDecoder/index.ts b/new-log-viewer/src/services/decoders/JsonlDecoder/index.ts new file mode 100644 index 00000000..3b229fb3 --- /dev/null +++ b/new-log-viewer/src/services/decoders/JsonlDecoder/index.ts @@ -0,0 +1,243 @@ +import {Dayjs} from "dayjs"; + +import {Nullable} from "../../../typings/common"; +import { + Decoder, + DecodeResultType, + FilteredLogEventMap, + JsonlDecoderOptionsType, + LogEventCount, +} from "../../../typings/decoders"; +import {Formatter} from "../../../typings/formatters"; +import {JsonValue} from "../../../typings/js"; +import { + INVALID_TIMESTAMP_VALUE, + LOG_LEVEL, + LogEvent, + LogLevelFilter, +} from "../../../typings/logs"; +import LogbackFormatter from "../../formatters/LogbackFormatter"; +import { + convertToDayjsTimestamp, + convertToLogLevelValue, + isJsonObject, +} from "./utils"; + + +/** + * A decoder for JSONL (JSON lines) files that contain log events. See `JsonlDecoderOptionsType` for + * properties that are specific to log events (compared to generic JSON records). + */ +class JsonlDecoder implements Decoder { + static #textDecoder = new TextDecoder(); + + #dataArray: Nullable; + + #logLevelKey: string; + + #timestampKey: string; + + #logEvents: LogEvent[] = []; + + #filteredLogEventMap: FilteredLogEventMap = null; + + #invalidLogEventIdxToRawLine: Map = new Map(); + + #formatter: Formatter; + + /** + * @param dataArray + * @param decoderOptions + */ + constructor (dataArray: Uint8Array, decoderOptions: JsonlDecoderOptionsType) { + this.#dataArray = dataArray; + this.#logLevelKey = decoderOptions.logLevelKey; + this.#timestampKey = decoderOptions.timestampKey; + this.#formatter = new LogbackFormatter({formatString: decoderOptions.formatString}); + } + + getEstimatedNumEvents (): number { + return this.#logEvents.length; + } + + getFilteredLogEventMap (): FilteredLogEventMap { + return this.#filteredLogEventMap; + } + + setLogLevelFilter (logLevelFilter: LogLevelFilter): boolean { + this.#filterLogEvents(logLevelFilter); + + return true; + } + + build (): LogEventCount { + this.#deserialize(); + + const numInvalidEvents = this.#invalidLogEventIdxToRawLine.size; + + return { + numValidEvents: this.#logEvents.length - numInvalidEvents, + numInvalidEvents: numInvalidEvents, + }; + } + + setFormatterOptions (options: JsonlDecoderOptionsType): boolean { + this.#formatter = new LogbackFormatter({formatString: options.formatString}); + + return true; + } + + decodeRange ( + beginIdx: number, + endIdx: number, + useFilter: boolean, + ): Nullable { + if (useFilter && null === this.#filteredLogEventMap) { + return null; + } + + const length: number = (useFilter && null !== this.#filteredLogEventMap) ? + this.#filteredLogEventMap.length : + this.#logEvents.length; + + if (0 > beginIdx || length < endIdx) { + return null; + } + + const results: DecodeResultType[] = []; + for (let i = beginIdx; i < endIdx; i++) { + // Explicit cast since typescript thinks `#filteredLogEventMap[i]` can be undefined, but + // it shouldn't be since we performed a bounds check at the beginning of the method. + const logEventIdx: number = (useFilter && null !== this.#filteredLogEventMap) ? + (this.#filteredLogEventMap[i] as number) : + i; + + results.push(this.#decodeLogEvent(logEventIdx)); + } + + return results; + } + + /** + * Parses each line from the data array and buffers it internally. + * + * NOTE: `#dataArray` is freed after the very first run of this method. + */ + #deserialize () { + if (null === this.#dataArray) { + return; + } + + const text = JsonlDecoder.#textDecoder.decode(this.#dataArray); + let beginIdx = 0; + while (beginIdx < text.length) { + const endIdx = text.indexOf("\n", beginIdx); + const line = (-1 === endIdx) ? + text.substring(beginIdx) : + text.substring(beginIdx, endIdx); + + beginIdx = (-1 === endIdx) ? + text.length : + endIdx + 1; + + this.#parseJson(line); + } + + this.#dataArray = null; + } + + /** + * Parses a JSON line into a log event and buffers it internally. If the line isn't valid JSON, + * a default log event is buffered and the line is added to `#invalidLogEventIdxToRawLine`. + * + * @param line + */ + #parseJson (line: string) { + let fields: JsonValue; + let level: LOG_LEVEL; + let timestamp: Dayjs; + try { + fields = JSON.parse(line) as JsonValue; + if (false === isJsonObject(fields)) { + throw new Error("Unexpected non-object."); + } + level = convertToLogLevelValue(fields[this.#logLevelKey]); + timestamp = convertToDayjsTimestamp(fields[this.#timestampKey]); + } catch (e) { + if (0 === line.length) { + return; + } + console.error(e, line); + const currentLogEventIdx = this.#logEvents.length; + this.#invalidLogEventIdxToRawLine.set(currentLogEventIdx, line); + fields = {}; + level = LOG_LEVEL.NONE; + timestamp = convertToDayjsTimestamp(INVALID_TIMESTAMP_VALUE); + } + this.#logEvents.push({ + fields, + level, + timestamp, + }); + } + + /** + * Filters log events and generates `#filteredLogEventMap`. If `logLevelFilter` is `null`, + * `#filteredLogEventMap` will be set to `null`. + * + * @param logLevelFilter + */ + #filterLogEvents (logLevelFilter: LogLevelFilter) { + if (null === logLevelFilter) { + this.#filteredLogEventMap = null; + + return; + } + + const filteredLogEventMap: number[] = []; + this.#logEvents.forEach((logEvent, index) => { + if (logLevelFilter.includes(logEvent.level)) { + filteredLogEventMap.push(index); + } + }); + this.#filteredLogEventMap = filteredLogEventMap; + } + + /** + * Decodes a log event into a `DecodeResultType`. + * + * @param logEventIdx + * @return The decoded log event. + */ + #decodeLogEvent = (logEventIdx: number): DecodeResultType => { + let timestamp: number; + let message: string; + let logLevel: LOG_LEVEL; + + // eslint-disable-next-line no-warning-comments + // TODO We could probably optimize this to avoid checking `#invalidLogEventIdxToRawLine` on + // every iteration. + if (this.#invalidLogEventIdxToRawLine.has(logEventIdx)) { + timestamp = INVALID_TIMESTAMP_VALUE; + message = `${this.#invalidLogEventIdxToRawLine.get(logEventIdx)}\n`; + logLevel = LOG_LEVEL.NONE; + } else { + // Explicit cast since typescript thinks `#logEvents[logEventIdx]` can be undefined, + // but it shouldn't be since the index comes from a class-internal filter. + const logEvent = this.#logEvents[logEventIdx] as LogEvent; + logLevel = logEvent.level; + message = this.#formatter.formatLogEvent(logEvent); + timestamp = logEvent.timestamp.valueOf(); + } + + return [ + message, + timestamp, + logLevel, + logEventIdx + 1, + ]; + }; +} + + +export default JsonlDecoder; diff --git a/new-log-viewer/src/services/decoders/JsonlDecoder/utils.ts b/new-log-viewer/src/services/decoders/JsonlDecoder/utils.ts new file mode 100644 index 00000000..3ba5f2a4 --- /dev/null +++ b/new-log-viewer/src/services/decoders/JsonlDecoder/utils.ts @@ -0,0 +1,83 @@ +import dayjs, {Dayjs} from "dayjs"; + +import { + JsonObject, + JsonValue, +} from "../../../typings/js"; +import { + INVALID_TIMESTAMP_VALUE, + LOG_LEVEL, +} from "../../../typings/logs"; + + +/** + * Determines whether the given value is a `JsonObject` and applies a TypeScript narrowing + * conversion if so. + * + * @param value + * @return A TypeScript type predicate indicating whether `value` is a `JsonObject`. + */ +const isJsonObject = (value: JsonValue): value is JsonObject => { + return "object" === typeof value && null !== value && false === Array.isArray(value); +}; + +/** + * Converts a field into a log level if possible. + * + * @param field + * @return The log level or `LOG_LEVEL.NONE` if the field couldn't be converted. + */ +const convertToLogLevelValue = (field: JsonValue | undefined): LOG_LEVEL => { + let logLevelValue = LOG_LEVEL.NONE; + + if ("undefined" === typeof field || isJsonObject(field)) { + return logLevelValue; + } + + const logLevelName = String(field); + + const uppercaseLogLevelName = logLevelName.toUpperCase(); + if (uppercaseLogLevelName in LOG_LEVEL) { + logLevelValue = LOG_LEVEL[uppercaseLogLevelName as keyof typeof LOG_LEVEL]; + } + + return logLevelValue; +}; + +/** + * Converts a field into a dayjs timestamp if possible. + * + * @param field + * @return The field as a dayjs timestamp or `dayjs.utc(INVALID_TIMESTAMP_VALUE)` if: + * - the timestamp key doesn't exist in the log. + * - the timestamp's value is an unsupported type. + * - the timestamp's value is not a valid dayjs timestamp. + */ +const convertToDayjsTimestamp = (field: JsonValue | undefined): dayjs.Dayjs => { + // If the field is an invalid type, then set the timestamp to `INVALID_TIMESTAMP_VALUE`. + // NOTE: dayjs surprisingly thinks `undefined` is a valid date. See + // https://day.js.org/docs/en/parse/now#docsNav + if (("string" !== typeof field && + "number" !== typeof field) || + "undefined" === typeof field + ) { + // `INVALID_TIMESTAMP_VALUE` is a valid dayjs date. Another potential option is + // `dayjs(null)` to show "Invalid Date" in the UI. + field = INVALID_TIMESTAMP_VALUE; + } + + let dayjsTimestamp: Dayjs = dayjs.utc(field); + + // Sanitize invalid (e.g., "deadbeef") timestamps to `INVALID_TIMESTAMP_VALUE`; otherwise + // they'll show up in UI as "Invalid Date". + if (false === dayjsTimestamp.isValid()) { + dayjsTimestamp = dayjs.utc(INVALID_TIMESTAMP_VALUE); + } + + return dayjsTimestamp; +}; +export { + convertToDayjsTimestamp, + convertToLogLevelValue, + isJsonObject, +}; diff --git a/new-log-viewer/src/services/formatters/LogbackFormatter.ts b/new-log-viewer/src/services/formatters/LogbackFormatter.ts index 5d310df1..8caf91d3 100644 --- a/new-log-viewer/src/services/formatters/LogbackFormatter.ts +++ b/new-log-viewer/src/services/formatters/LogbackFormatter.ts @@ -4,10 +4,9 @@ import {Nullable} from "../../typings/common"; import { Formatter, FormatterOptionsType, - TimestampAndMessageType, } from "../../typings/formatters"; import {JsonObject} from "../../typings/js"; -import {INVALID_TIMESTAMP_VALUE} from "../../typings/logs"; +import {LogEvent} from "../../typings/logs"; /** @@ -39,14 +38,11 @@ class LogbackFormatter implements Formatter { #dateFormat: string = ""; - #timestampKey: string; - #keys: string[] = []; constructor (options: FormatterOptionsType) { // NOTE: It's safe for these values to be empty strings. this.#formatString = options.formatString; - this.#timestampKey = options.timestampKey; // Remove new line this.#formatString = this.#formatString.replace("%n", ""); @@ -59,17 +55,14 @@ class LogbackFormatter implements Formatter { * Formats the given log event. * * @param logEvent - * @return The log event's timestamp and the formatted string. + * @return The formatted log event. */ - formatLogEvent (logEvent: JsonObject): TimestampAndMessageType { - const timestamp = this.#parseTimestamp(logEvent); - let formatted = this.#formatTimestamp(timestamp, this.#formatString); - formatted = this.#formatVariables(formatted, logEvent); - - return { - timestamp: timestamp.valueOf(), - message: formatted, - }; + formatLogEvent (logEvent: LogEvent): string { + const {fields, timestamp} = logEvent; + const formatStringWithTimestamp: string = + this.#formatTimestamp(timestamp, this.#formatString); + + return this.#formatVariables(formatStringWithTimestamp, fields); } /** @@ -110,23 +103,6 @@ class LogbackFormatter implements Formatter { } } - /** - * Gets the timestamp from the log event. - * - * @param logEvent - * @return The timestamp or `INVALID_TIMESTAMP_VALUE` if: - * - the timestamp key doesn't exist in the log, or - * - the timestamp's value is not a number. - */ - #parseTimestamp (logEvent: JsonObject): dayjs.Dayjs { - let timestamp = logEvent[this.#timestampKey]; - if ("number" !== typeof timestamp && "string" !== typeof timestamp) { - timestamp = INVALID_TIMESTAMP_VALUE; - } - - return dayjs.utc(timestamp); - } - /** * Replaces the timestamp specifier in `formatString` with `timestamp`, formatted with * `#dateFormat`. diff --git a/new-log-viewer/src/typings/decoders.ts b/new-log-viewer/src/typings/decoders.ts index 53408184..d1b1eeae 100644 --- a/new-log-viewer/src/typings/decoders.ts +++ b/new-log-viewer/src/typings/decoders.ts @@ -1,4 +1,5 @@ import {Nullable} from "./common"; +import {LogLevelFilter} from "./logs"; interface LogEventCount { @@ -32,6 +33,12 @@ type DecoderOptionsType = JsonlDecoderOptionsType; */ type DecodeResultType = [string, number, number, number]; +/** + * Mapping between an index in the filtered log events collection to an index in the unfiltered log + * events collection. + */ +type FilteredLogEventMap = Nullable; + interface Decoder { /** @@ -42,33 +49,49 @@ interface Decoder { getEstimatedNumEvents(): number; /** - * When applicable, deserializes log events in the range `[beginIdx, endIdx)`. + * @return The filtered log events map. + */ + getFilteredLogEventMap(): FilteredLogEventMap; + + /** + * Sets the log level filter for the decoder. + * + * @param logLevelFilter + * @return Whether the filter was successfully set. + */ + setLogLevelFilter(logLevelFilter: LogLevelFilter): boolean + + /** + * Deserializes all log events in the file. * - * @param beginIdx - * @param endIdx End index. To deserialize to the end of the file, use `LOG_EVENT_FILE_END_IDX`. * @return Count of the successfully deserialized ("valid") log events and count of any - * un-deserializable ("invalid") log events within the range; or null if any log event in the - * range doesn't exist (e.g., the range exceeds the number of log events in the file). + * un-deserializable ("invalid") log events. */ - buildIdx(beginIdx: number, endIdx: number): Nullable; + build(): LogEventCount; /** - * Sets options for the decoder. + * Sets any formatter options that exist in the decoder's options. * * @param options * @return Whether the options were successfully set. */ - setDecoderOptions(options: DecoderOptionsType): boolean; + setFormatterOptions(options: DecoderOptionsType): boolean; /** - * Decodes the log events in the range `[beginIdx, endIdx)`. + * Decodes log events in the range `[beginIdx, endIdx)` of the filtered or unfiltered + * (depending on the value of `useFilter`) log events collection. * * @param beginIdx * @param endIdx + * @param useFilter Whether to decode from the filtered or unfiltered log events collection. * @return The decoded log events on success or null if any log event in the range doesn't exist - * (e.g., the range exceeds the number of log events in the file). + * (e.g., the range exceeds the number of log events in the collection). */ - decode(beginIdx: number, endIdx: number): Nullable; + decodeRange( + beginIdx: number, + endIdx: number, + useFilter: boolean + ): Nullable; } /** @@ -82,6 +105,7 @@ export type { Decoder, DecodeResultType, DecoderOptionsType, + FilteredLogEventMap, JsonlDecoderOptionsType, LogEventCount, }; diff --git a/new-log-viewer/src/typings/formatters.ts b/new-log-viewer/src/typings/formatters.ts index 0f559d82..87a5d067 100644 --- a/new-log-viewer/src/typings/formatters.ts +++ b/new-log-viewer/src/typings/formatters.ts @@ -1,4 +1,4 @@ -import {JsonObject} from "./js"; +import {LogEvent} from "./logs"; /** @@ -21,28 +21,19 @@ import {JsonObject} from "./js"; * inserts a newline as the last character. * - `` - Any specifier besides those above indicate a key for a kv-pair, if said kv-pair * exists in a given log event. - * @property timestampKey The key of the kv-pair that contains the authoritative timestamp for - * every log event. */ interface LogbackFormatterOptionsType { formatString: string, - timestampKey: string, } type FormatterOptionsType = LogbackFormatterOptionsType; -interface TimestampAndMessageType { - timestamp: number, - message: string, -} - interface Formatter { - formatLogEvent: (jsonObject: JsonObject) => TimestampAndMessageType + formatLogEvent: (logEvent: LogEvent) => string } export type { Formatter, FormatterOptionsType, LogbackFormatterOptionsType, - TimestampAndMessageType, }; diff --git a/new-log-viewer/src/typings/logs.ts b/new-log-viewer/src/typings/logs.ts index c3154b29..67fe98fe 100644 --- a/new-log-viewer/src/typings/logs.ts +++ b/new-log-viewer/src/typings/logs.ts @@ -1,3 +1,9 @@ +import {Dayjs} from "dayjs"; + +import {Nullable} from "./common"; +import {JsonObject} from "./js"; + + enum LOG_LEVEL { NONE = 0, TRACE, @@ -8,8 +14,20 @@ enum LOG_LEVEL { FATAL } +type LogLevelFilter = Nullable; + +interface LogEvent { + timestamp: Dayjs, + level: LOG_LEVEL, + fields: JsonObject +} + const INVALID_TIMESTAMP_VALUE = 0; +export type { + LogEvent, + LogLevelFilter, +}; export { INVALID_TIMESTAMP_VALUE, LOG_LEVEL,