Skip to content

Commit

Permalink
feat(formatter): Display entire log event as JSON by default and remi…
Browse files Browse the repository at this point in the history
…nd users to set format string. (y-scope#129)

Co-authored-by: Junhao Liao <[email protected]>
  • Loading branch information
davemarco and junhaoliao authored Nov 27, 2024
1 parent 72e40e4 commit 82e677e
Show file tree
Hide file tree
Showing 13 changed files with 120 additions and 24 deletions.
5 changes: 3 additions & 2 deletions src/components/CentralContainer/Sidebar/SidebarTabs/index.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {
forwardRef,
useState,
useContext,
} from "react";

import {
Expand All @@ -13,6 +13,7 @@ import InfoOutlinedIcon from "@mui/icons-material/InfoOutlined";
import SearchIcon from "@mui/icons-material/Search";
import SettingsOutlinedIcon from "@mui/icons-material/SettingsOutlined";

import {StateContext} from "../../../../contexts/StateContextProvider";
import {TAB_NAME} from "../../../../typings/tab";
import SettingsModal from "../../../modals/SettingsModal";
import FileInfoTabPanel from "./FileInfoTabPanel";
Expand Down Expand Up @@ -51,7 +52,7 @@ const SidebarTabs = forwardRef<HTMLDivElement, SidebarTabsProps>((
},
tabListRef
) => {
const [isSettingsModalOpen, setIsSettingsModalOpen] = useState<boolean>(false);
const {isSettingsModalOpen, setIsSettingsModalOpen} = useContext(StateContext);

const handleSettingsModalClose = () => {
setIsSettingsModalOpen(false);
Expand Down
26 changes: 23 additions & 3 deletions src/components/PopUps/PopUpMessageBox.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {
import React, {
useContext,
useEffect,
useRef,
Expand All @@ -8,6 +8,7 @@ import {
import {
Alert,
Box,
Button,
CircularProgress,
IconButton,
Typography,
Expand All @@ -31,14 +32,16 @@ interface PopUpMessageProps {
}

/**
* Display a pop-up message in an alert box.
* Displays a pop-up message in an alert box with an optional action button. The pop-up can
* be manually dismissed or will automatically close after the specified `timeoutMillis`.
* If `timeoutMillis` is `0`, the pop-up will remain open until manually closed.
*
* @param props
* @param props.message
* @return
*/
const PopUpMessageBox = ({message}: PopUpMessageProps) => {
const {id, level, message: messageStr, title, timeoutMillis} = message;
const {id, level, primaryAction, message: messageStr, title, timeoutMillis} = message;

const {handlePopUpMessageClose} = useContext(NotificationContext);
const [percentRemaining, setPercentRemaining] = useState<number>(100);
Expand All @@ -48,6 +51,11 @@ const PopUpMessageBox = ({message}: PopUpMessageProps) => {
handlePopUpMessageClose(id);
};

const handlePrimaryActionClick = (ev: React.MouseEvent<HTMLButtonElement>) => {
primaryAction?.onClick?.(ev);
handleCloseButtonClick();
};

useEffect(() => {
if (DO_NOT_TIMEOUT_VALUE === timeoutMillis) {
return () => {};
Expand Down Expand Up @@ -113,6 +121,18 @@ const PopUpMessageBox = ({message}: PopUpMessageProps) => {
<Typography level={"body-sm"}>
{messageStr}
</Typography>
{primaryAction && (
<Box className={"pop-up-message-box-actions-container"}>
<Button
color={color}
variant={"solid"}
{...primaryAction}
onClick={handlePrimaryActionClick}
>
{primaryAction.children}
</Button>
</Box>
)}
</div>
</Alert>
);
Expand Down
10 changes: 9 additions & 1 deletion src/components/PopUps/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,15 @@
}

.pop-up-message-box-alert-layout {
width: 300px;
display: flex;
flex-direction: column;
gap: 10px;
width: 333px;
}

.pop-up-message-box-actions-container {
display: flex;
justify-content: flex-end;
}

.pop-up-message-box-title-container {
Expand Down
3 changes: 2 additions & 1 deletion src/components/modals/SettingsModal/SettingsDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ const CONFIG_FORM_FIELDS = [
\`{<field-name>[:<formatter-name>[:<formatter-options>]]}\`, where \`field-name\` is
required, while \`formatter-name\` and \`formatter-options\` are optional. For example,
the following placeholder would format a timestamp field with name \`@timestamp\`:
\`{@timestamp:timestamp:YYYY-MM-DD HH\\:mm\\:ss.SSS}\`.`,
\`{@timestamp:timestamp:YYYY-MM-DD HH\\:mm\\:ss.SSS}\`. Leave format string blank to
display the entire log event formatted as JSON.`,
initialValue: getConfig(CONFIG_KEY.DECODER_OPTIONS).formatString,
label: "Decoder: Format string",
name: LOCAL_STORAGE_KEY.DECODER_OPTIONS_FORMAT_STRING,
Expand Down
42 changes: 36 additions & 6 deletions src/contexts/StateContextProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,22 @@ import React, {
useState,
} from "react";

import SettingsOutlinedIcon from "@mui/icons-material/SettingsOutlined";

import LogExportManager, {
EXPORT_LOG_PROGRESS_VALUE_MAX,
EXPORT_LOG_PROGRESS_VALUE_MIN,
} from "../services/LogExportManager";
import {Nullable} from "../typings/common";
import {CONFIG_KEY} from "../typings/config";
import {LogLevelFilter} from "../typings/logs";
import {DEFAULT_AUTO_DISMISS_TIMEOUT_MILLIS} from "../typings/notifications";
import {
LOG_LEVEL,
LogLevelFilter,
} from "../typings/logs";
import {
DEFAULT_AUTO_DISMISS_TIMEOUT_MILLIS,
LONG_AUTO_DISMISS_TIMEOUT_MILLIS,
} from "../typings/notifications";
import {UI_STATE} from "../typings/states";
import {SEARCH_PARAM_NAMES} from "../typings/url";
import {
Expand Down Expand Up @@ -56,8 +64,9 @@ import {

interface StateContextType {
beginLineNumToLogEventNum: BeginLineNumToLogEventNumMap,
fileName: string,
exportProgress: Nullable<number>,
fileName: string,
isSettingsModalOpen: boolean,
uiState: UI_STATE,
logData: string,
numEvents: number,
Expand All @@ -70,7 +79,8 @@ interface StateContextType {
exportLogs: () => void,
loadFile: (fileSrc: FileSrcType, cursor: CursorType) => void,
loadPageByAction: (navAction: NavigationAction) => void,
setLogLevelFilter: (newLogLevelFilter: LogLevelFilter) => void,
setIsSettingsModalOpen: (isOpen: boolean) => void,
setLogLevelFilter: (filter: LogLevelFilter) => void,
startQuery: (queryString: string, isRegex: boolean, isCaseSensitive: boolean) => void,
}
const StateContext = createContext<StateContextType>({} as StateContextType);
Expand All @@ -82,6 +92,7 @@ const STATE_DEFAULT: Readonly<StateContextType> = Object.freeze({
beginLineNumToLogEventNum: new Map<number, number>(),
exportProgress: null,
fileName: "",
isSettingsModalOpen: false,
logData: "No file is open.",
numEvents: 0,
numPages: 0,
Expand All @@ -94,6 +105,7 @@ const STATE_DEFAULT: Readonly<StateContextType> = Object.freeze({
exportLogs: () => null,
loadFile: () => null,
loadPageByAction: () => null,
setIsSettingsModalOpen: () => null,
setLogLevelFilter: () => null,
startQuery: () => null,
});
Expand Down Expand Up @@ -236,6 +248,8 @@ const StateContextProvider = ({children}: StateContextProviderProps) => {
// States
const [exportProgress, setExportProgress] =
useState<Nullable<number>>(STATE_DEFAULT.exportProgress);
const [isSettingsModalOpen, setIsSettingsModalOpen] =
useState<boolean>(STATE_DEFAULT.isSettingsModalOpen);
const [fileName, setFileName] = useState<string>(STATE_DEFAULT.fileName);
const [logData, setLogData] = useState<string>(STATE_DEFAULT.logData);
const [numEvents, setNumEvents] = useState<number>(STATE_DEFAULT.numEvents);
Expand Down Expand Up @@ -270,6 +284,20 @@ const StateContextProvider = ({children}: StateContextProviderProps) => {
}
}
break;
case WORKER_RESP_CODE.FORMAT_POPUP:
postPopUp({
level: LOG_LEVEL.INFO,
message: "Adding a format string can enhance the readability of your" +
" structured logs by customizing how fields are displayed.",
primaryAction: {
children: "Settings",
startDecorator: <SettingsOutlinedIcon/>,
onClick: () => { setIsSettingsModalOpen(true); },
},
timeoutMillis: LONG_AUTO_DISMISS_TIMEOUT_MILLIS,
title: "A format string has not been configured",
});
break;
case WORKER_RESP_CODE.LOG_FILE_INFO:
setFileName(args.fileName);
setNumEvents(args.numEvents);
Expand Down Expand Up @@ -418,14 +446,14 @@ const StateContextProvider = ({children}: StateContextProviderProps) => {
loadPageByCursor(mainWorkerRef.current, cursor);
}, []);

const setLogLevelFilter = useCallback((newLogLevelFilter: LogLevelFilter) => {
const setLogLevelFilter = useCallback((filter: LogLevelFilter) => {
if (null === mainWorkerRef.current) {
return;
}
setUiState(UI_STATE.FAST_LOADING);
workerPostReq(mainWorkerRef.current, WORKER_REQ_CODE.SET_FILTER, {
cursor: {code: CURSOR_CODE.EVENT_NUM, args: {eventNum: logEventNumRef.current ?? 1}},
logLevelFilter: newLogLevelFilter,
logLevelFilter: filter,
});
}, []);

Expand Down Expand Up @@ -510,6 +538,7 @@ const StateContextProvider = ({children}: StateContextProviderProps) => {
beginLineNumToLogEventNum: beginLineNumToLogEventNumRef.current,
exportProgress: exportProgress,
fileName: fileName,
isSettingsModalOpen: isSettingsModalOpen,
logData: logData,
numEvents: numEvents,
numPages: numPages,
Expand All @@ -522,6 +551,7 @@ const StateContextProvider = ({children}: StateContextProviderProps) => {
exportLogs: exportLogs,
loadFile: loadFile,
loadPageByAction: loadPageByAction,
setIsSettingsModalOpen: setIsSettingsModalOpen,
setLogLevelFilter: setLogLevelFilter,
startQuery: startQuery,
}}
Expand Down
10 changes: 10 additions & 0 deletions src/services/MainWorker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,14 @@ const onQueryResults = (queryProgress: number, queryResults: QueryResults) => {
postResp(WORKER_RESP_CODE.QUERY_RESULT, {progress: queryProgress, results: queryResults});
};

/**
* Sends a message to the renderer to open a pop-up which prompts user to replace the default
* format string.
*/
const postFormatPopup = () => {
postResp(WORKER_RESP_CODE.FORMAT_POPUP, null);
};

// eslint-disable-next-line no-warning-comments
// TODO: Break this function up into smaller functions.
// eslint-disable-next-line max-lines-per-function,max-statements
Expand Down Expand Up @@ -149,3 +157,5 @@ onmessage = async (ev: MessageEvent<MainWorkerReqMessage>) => {
}
}
};

export {postFormatPopup};
12 changes: 8 additions & 4 deletions src/services/decoders/ClpIrDecoder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {Formatter} from "../../typings/formatters";
import {JsonObject} from "../../typings/js";
import {LogLevelFilter} from "../../typings/logs";
import YscopeFormatter from "../formatters/YscopeFormatter";
import {postFormatPopup} from "../MainWorker";
import {
convertToDayjsTimestamp,
isJsonObject,
Expand All @@ -29,7 +30,7 @@ class ClpIrDecoder implements Decoder {

readonly #streamType: CLP_IR_STREAM_TYPE;

#formatter: Nullable<Formatter>;
#formatter: Nullable<Formatter> = null;

constructor (
streamType: CLP_IR_STREAM_TYPE,
Expand All @@ -38,9 +39,12 @@ class ClpIrDecoder implements Decoder {
) {
this.#streamType = streamType;
this.#streamReader = streamReader;
this.#formatter = (streamType === CLP_IR_STREAM_TYPE.STRUCTURED) ?
new YscopeFormatter({formatString: decoderOptions.formatString}) :
null;
if (streamType === CLP_IR_STREAM_TYPE.STRUCTURED) {
this.#formatter = new YscopeFormatter({formatString: decoderOptions.formatString});
if (0 === decoderOptions.formatString.length) {
postFormatPopup();
}
}
}

/**
Expand Down
4 changes: 4 additions & 0 deletions src/services/decoders/JsonlDecoder/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
LogLevelFilter,
} from "../../../typings/logs";
import YscopeFormatter from "../../formatters/YscopeFormatter";
import {postFormatPopup} from "../../MainWorker";
import {
convertToDayjsTimestamp,
convertToLogLevelValue,
Expand Down Expand Up @@ -54,6 +55,9 @@ class JsonlDecoder implements Decoder {
this.#logLevelKey = decoderOptions.logLevelKey;
this.#timestampKey = decoderOptions.timestampKey;
this.#formatter = new YscopeFormatter({formatString: decoderOptions.formatString});
if (0 === decoderOptions.formatString.length) {
postFormatPopup();
}
}

getEstimatedNumEvents (): number {
Expand Down
6 changes: 6 additions & 0 deletions src/services/formatters/YscopeFormatter/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
import {LogEvent} from "../../../typings/logs";
import {
getFormattedField,
jsonValueToString,
removeEscapeCharacters,
replaceDoubleBacklash,
splitFieldPlaceholder,
Expand Down Expand Up @@ -37,6 +38,11 @@ class YscopeFormatter implements Formatter {
}

formatLogEvent (logEvent: LogEvent): string {
// Empty format string is special case where formatter returns all fields as JSON.
if ("" === this.#processedFormatString) {
return jsonValueToString(logEvent.fields);
}

const formattedLogFragments: string[] = [];
let lastIndex = 0;

Expand Down
6 changes: 3 additions & 3 deletions src/typings/formatters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {LogEvent} from "./logs";


/**
* @property formatString A Yscope format string. The format string can include field-placeholders
* @property formatString A YScope format string. The format string can include field-placeholders
* to insert and format any field of a JSON log event. A field-placeholder uses the following
* syntax:
* `{<field-name>[:<formatter-name>[:<formatter-options>]]}`
Expand Down Expand Up @@ -49,14 +49,14 @@ interface YscopeFieldFormatter {
}

/**
* Type for list of currently supported Yscope field formatters.
* Type for list of currently supported YScope field formatters.
*/
type YscopeFieldFormatterMap = {
[key: string]: new (options: Nullable<string>) => YscopeFieldFormatter;
};

/**
* Parsed field placeholder from a Yscope format string.
* Parsed field placeholder from a YScope format string.
*/
type YscopeFieldPlaceholder = {
fieldNameKeys: string[],
Expand Down
9 changes: 9 additions & 0 deletions src/typings/notifications.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import {ButtonProps} from "@mui/joy";

import {LOG_LEVEL} from "./logs";


Expand All @@ -9,6 +11,7 @@ interface PopUpMessage {
message: string,
timeoutMillis: number,
title: string,
primaryAction?: ButtonProps,
}

/**
Expand All @@ -21,9 +24,15 @@ const DO_NOT_TIMEOUT_VALUE = 0;
*/
const DEFAULT_AUTO_DISMISS_TIMEOUT_MILLIS = 10_000;

/**
* A longer duration in milliseconds after which an automatic dismissal will occur.
*/
const LONG_AUTO_DISMISS_TIMEOUT_MILLIS = 20_000;


export type {PopUpMessage};
export {
DEFAULT_AUTO_DISMISS_TIMEOUT_MILLIS,
DO_NOT_TIMEOUT_VALUE,
LONG_AUTO_DISMISS_TIMEOUT_MILLIS,
};
Loading

0 comments on commit 82e677e

Please sign in to comment.