vmui/logs: add markdown support (#6292)

Add support for markdown format and emoji for the `_msg` field in the
"Group" view.
Add markdown rendering toggle. Disabled by default. Value is stored in
`localStorage`.
This commit is contained in:
Yury Molodov 2024-06-10 16:38:13 +02:00 committed by Aliaksandr Valialkin
parent d269a95da3
commit 2300e30ff3
No known key found for this signature in database
GPG Key ID: 52C003EE2BCDB9EB
16 changed files with 2048 additions and 21 deletions

View File

@ -11,7 +11,6 @@
"@types/lodash.debounce": "^4.0.6",
"@types/lodash.get": "^4.4.6",
"@types/lodash.throttle": "^4.1.6",
"@types/marked": "^5.0.0",
"@types/node": "^20.4.0",
"@types/qs": "^6.9.7",
"@types/react-input-mask": "^3.0.2",
@ -22,7 +21,8 @@
"lodash.debounce": "^4.0.8",
"lodash.get": "^4.4.2",
"lodash.throttle": "^4.1.1",
"marked": "^5.1.0",
"marked": "^12.0.2",
"marked-emoji": "^1.4.0",
"preact": "^10.7.1",
"qs": "^6.10.3",
"react-input-mask": "^2.0.4",
@ -4272,11 +4272,6 @@
"@types/lodash": "*"
}
},
"node_modules/@types/marked": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/@types/marked/-/marked-5.0.2.tgz",
"integrity": "sha512-OucS4KMHhFzhz27KxmWg7J+kIYqyqoW5kdIEI319hqARQQUTqhao3M/F+uFnDXD0Rg72iDDZxZNxq5gvctmLlg=="
},
"node_modules/@types/mime": {
"version": "1.3.5",
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz",
@ -13598,14 +13593,22 @@
}
},
"node_modules/marked": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/marked/-/marked-5.1.2.tgz",
"integrity": "sha512-ahRPGXJpjMjwSOlBoTMZAK7ATXkli5qCPxZ21TG44rx1KEo44bii4ekgTDQPNRQ4Kh7JMb9Ub1PVk1NxRSsorg==",
"version": "12.0.2",
"resolved": "https://registry.npmjs.org/marked/-/marked-12.0.2.tgz",
"integrity": "sha512-qXUm7e/YKFoqFPYPa3Ukg9xlI5cyAtGmyEIzMfW//m6kXwCy2Ps9DYf5ioijFKQ8qyuscrHoY04iJGctu2Kg0Q==",
"bin": {
"marked": "bin/marked.js"
},
"engines": {
"node": ">= 16"
"node": ">= 18"
}
},
"node_modules/marked-emoji": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/marked-emoji/-/marked-emoji-1.4.0.tgz",
"integrity": "sha512-/2TJfGzXpiBBq+X3akHHbTrAjZPJDwR+7FV6SyQLECnQEfaoVkrpKZJzHhPTAq3Sl/A1l2frMT0u6b38VBBlNg==",
"peerDependencies": {
"marked": ">=4 <13"
}
},
"node_modules/mdn-data": {

View File

@ -7,7 +7,6 @@
"@types/lodash.debounce": "^4.0.6",
"@types/lodash.get": "^4.4.6",
"@types/lodash.throttle": "^4.1.6",
"@types/marked": "^5.0.0",
"@types/node": "^20.4.0",
"@types/qs": "^6.9.7",
"@types/react-input-mask": "^3.0.2",
@ -18,7 +17,8 @@
"lodash.debounce": "^4.0.8",
"lodash.get": "^4.4.2",
"lodash.throttle": "^4.1.1",
"marked": "^5.1.0",
"marked": "^12.0.2",
"marked-emoji": "^1.4.0",
"preact": "^10.7.1",
"qs": "^6.10.3",
"react-input-mask": "^2.0.4",

View File

@ -4,6 +4,7 @@ import AppContextProvider from "./contexts/AppContextProvider";
import ThemeProvider from "./components/Main/ThemeProvider/ThemeProvider";
import ExploreLogs from "./pages/ExploreLogs/ExploreLogs";
import LogsLayout from "./layouts/LogsLayout/LogsLayout";
import "./constants/markedPlugins";
const AppLogs: FC = () => {
const [loadedTheme, setLoadedTheme] = useState(false);

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,5 @@
import { markedEmoji } from "marked-emoji";
import { marked } from "marked";
import emojis from "./emojis";
marked.use(markedEmoji({ emojis, renderer: (token) => token.emoji }));

View File

@ -54,7 +54,7 @@ const useGetMetricsQL = () => {
const processMarkdown = (text: string) => {
const div = document.createElement("div");
div.innerHTML = marked(text);
div.innerHTML = marked(text) as string;
const groups = div.querySelectorAll(`${CATEGORY_TAG}, ${FUNCTION_TAG}`);
return processGroups(groups);
};

View File

@ -26,6 +26,7 @@ const ExploreLogs: FC = () => {
const { logs, isLoading, error, fetchLogs } = useFetchLogs(serverUrl, query, limit);
const [queryError, setQueryError] = useState<ErrorTypes | string>("");
const [loaded, isLoaded] = useState(false);
const [markdownParsing, setMarkdownParsing] = useState(getFromStorage("LOGS_MARKDOWN") === "true");
const handleRunQuery = () => {
if (!query) {
@ -51,6 +52,11 @@ const ExploreLogs: FC = () => {
saveToStorage("LOGS_LIMIT", `${limit}`);
};
const handleChangeMarkdownParsing = (val: boolean) => {
saveToStorage("LOGS_MARKDOWN", `${val}`);
setMarkdownParsing(val);
};
useEffect(() => {
if (query) handleRunQuery();
}, [period]);
@ -65,15 +71,18 @@ const ExploreLogs: FC = () => {
query={query}
error={queryError}
limit={limit}
markdownParsing={markdownParsing}
onChange={setQuery}
onChangeLimit={handleChangeLimit}
onRun={handleRunQuery}
onChangeMarkdownParsing={handleChangeMarkdownParsing}
/>
{isLoading && <Spinner />}
{error && <Alert variant="error">{error}</Alert>}
<ExploreLogsBody
data={logs}
loaded={loaded}
markdownParsing={markdownParsing}
/>
</div>
);

View File

@ -15,10 +15,12 @@ import useBoolean from "../../../hooks/useBoolean";
import TableLogs from "./TableLogs";
import GroupLogs from "../GroupLogs/GroupLogs";
import { DATE_TIME_FORMAT } from "../../../constants/date";
import { marked } from "marked";
export interface ExploreLogBodyProps {
data: Logs[];
loaded?: boolean;
markdownParsing: boolean;
}
enum DisplayType {
@ -33,7 +35,7 @@ const tabs = [
{ label: "JSON", value: DisplayType.json, icon: <CodeIcon/> },
];
const ExploreLogsBody: FC<ExploreLogBodyProps> = ({ data, loaded }) => {
const ExploreLogsBody: FC<ExploreLogBodyProps> = ({ data, loaded, markdownParsing }) => {
const { isMobile } = useDeviceDetect();
const { timezone } = useTimeState();
const { setSearchParamsFromKeys } = useSearchParamsFromObject();
@ -46,11 +48,12 @@ const ExploreLogsBody: FC<ExploreLogBodyProps> = ({ data, loaded }) => {
...item,
_vmui_time: item._time ? dayjs(item._time).tz().format(`${DATE_TIME_FORMAT}.SSS`) : "",
_vmui_data: JSON.stringify(item, null, 2),
_vmui_markdown: marked(item._msg.replace(/```/g, "\n```\n")) as string,
})) as Logs[], [data, timezone]);
const columns = useMemo(() => {
if (!logs?.length) return [];
const hideColumns = ["_vmui_data", "_vmui_time"];
const hideColumns = ["_vmui_data", "_vmui_time", "_vmui_markdown"];
const keys = new Set<string>();
for (const item of logs) {
for (const key in item) {
@ -125,6 +128,7 @@ const ExploreLogsBody: FC<ExploreLogBodyProps> = ({ data, loaded }) => {
<GroupLogs
logs={logs}
columns={columns}
markdownParsing={markdownParsing}
/>
)}
{activeTab === DisplayType.json && (

View File

@ -6,17 +6,29 @@ import useDeviceDetect from "../../../hooks/useDeviceDetect";
import Button from "../../../components/Main/Button/Button";
import QueryEditor from "../../../components/Configurators/QueryEditor/QueryEditor";
import TextField from "../../../components/Main/TextField/TextField";
import Switch from "../../../components/Main/Switch/Switch";
export interface ExploreLogHeaderProps {
query: string;
limit: number;
error?: string;
markdownParsing: boolean;
onChange: (val: string) => void;
onChangeLimit: (val: number) => void;
onRun: () => void;
onChangeMarkdownParsing: (val: boolean) => void;
}
const ExploreLogsHeader: FC<ExploreLogHeaderProps> = ({ query, limit, error, onChange, onChangeLimit, onRun }) => {
const ExploreLogsHeader: FC<ExploreLogHeaderProps> = ({
query,
limit,
error,
markdownParsing,
onChange,
onChangeLimit,
onRun,
onChangeMarkdownParsing,
}) => {
const { isMobile } = useDeviceDetect();
const [errorLimit, setErrorLimit] = useState("");
@ -66,6 +78,14 @@ const ExploreLogsHeader: FC<ExploreLogHeaderProps> = ({ query, limit, error, onC
/>
</div>
<div className="vm-explore-logs-header-bottom">
<div className="vm-explore-logs-header-bottom-contols">
<Switch
label={"Markdown parsing"}
value={markdownParsing}
onChange={onChangeMarkdownParsing}
fullWidth={isMobile}
/>
</div>
<div className="vm-explore-logs-header-bottom-helpful">
<a
className="vm-link vm-link_with-icon"

View File

@ -25,6 +25,10 @@
justify-content: normal;
}
&-contols {
flex-grow: 1;
}
&__execute {
display: grid;
}

View File

@ -11,9 +11,10 @@ import GroupLogsItem from "./GroupLogsItem";
interface TableLogsProps {
logs: Logs[];
columns: string[];
markdownParsing: boolean;
}
const GroupLogs: FC<TableLogsProps> = ({ logs }) => {
const GroupLogs: FC<TableLogsProps> = ({ logs, markdownParsing }) => {
const copyToClipboard = useCopyToClipboard();
const [copied, setCopied] = useState<string | null>(null);
@ -77,6 +78,7 @@ const GroupLogs: FC<TableLogsProps> = ({ logs }) => {
<GroupLogsItem
key={`${value._msg}${value._time}`}
log={value}
markdownParsing={markdownParsing}
/>
))}
</div>

View File

@ -10,15 +10,16 @@ import classNames from "classnames";
interface Props {
log: Logs;
markdownParsing: boolean;
}
const GroupLogsItem: FC<Props> = ({ log }) => {
const GroupLogsItem: FC<Props> = ({ log, markdownParsing }) => {
const {
value: isOpenFields,
toggle: toggleOpenFields,
} = useBoolean(false);
const excludeKeys = ["_stream", "_msg", "_time", "_vmui_time", "_vmui_data"];
const excludeKeys = ["_stream", "_msg", "_time", "_vmui_time", "_vmui_data", "_vmui_markdown"];
const fields = Object.entries(log).filter(([key]) => !excludeKeys.includes(key));
const hasFields = fields.length > 0;
@ -71,6 +72,7 @@ const GroupLogsItem: FC<Props> = ({ log }) => {
"vm-group-logs-row-content__msg": true,
"vm-group-logs-row-content__msg_missing": !log._msg
})}
dangerouslySetInnerHTML={markdownParsing && log._vmui_markdown ? { __html: log._vmui_markdown } : undefined}
>
{log._msg || "message missing"}
</div>

View File

@ -96,6 +96,65 @@
font-style: italic;
text-align: center;
}
/* styles for markdown */
p, pre, code {
white-space: pre-wrap;
word-wrap: break-word;
word-break: normal;
}
code:not(pre code), pre {
display: inline-block;
background: $color-hover-black;
border: 1px solid $color-hover-black;
border-radius: $border-radius-small;
tab-size: 4;
font-variant-ligatures: none;
margin: calc($padding-small/4) 0;
}
p {
font-family: $font-family-global;
line-height: 1.4;
}
pre {
padding: $padding-small;
}
code {
font-size: $font-size-small;
padding: calc($padding-small / 4) calc($padding-small / 2);
}
a {
color: $color-primary;
cursor: pointer;
&:hover {
text-decoration: underline;
}
}
strong {
font-weight: bold;
}
em {
font-style: italic;
}
blockquote {
border-left: 4px solid $color-hover-black;
margin: calc($padding-small/2) $padding-small;
padding: calc($padding-small/2) $padding-small;
}
ul, ol {
list-style-position: inside;
}
/* end styles for markdown */
}
}

View File

@ -89,7 +89,7 @@ const PredefinedPanel: FC<PredefinedPanelsProps> = ({
<>
<div>
<span>Description:</span>
<div dangerouslySetInnerHTML={{ __html: marked.parse(description) }}/>
<div dangerouslySetInnerHTML={{ __html: marked(description) as string }}/>
</div>
<hr/>
</>

View File

@ -7,6 +7,7 @@ export type StorageKeys = "AUTOCOMPLETE"
| "DISABLED_DEFAULT_TIMEZONE"
| "THEME"
| "LOGS_LIMIT"
| "LOGS_MARKDOWN"
| "EXPLORE_METRICS_TIPS"
| "QUERY_HISTORY"
| "QUERY_FAVORITES"

View File

@ -19,6 +19,8 @@ according to [these docs](https://docs.victoriametrics.com/victorialogs/quicksta
## tip
* FEATURE: [web UI](https://docs.victoriametrics.com/VictoriaLogs/querying/#web-ui): add markdown support to the `Group` view. See [this pull request](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/6292).
## [v0.18.0](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v0.18.0-victorialogs)
Released at 2024-06-06