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 GitHub
parent 0b7c47a40c
commit 84088e5a2d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 2048 additions and 21 deletions

View file

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

View file

@ -7,7 +7,6 @@
"@types/lodash.debounce": "^4.0.6", "@types/lodash.debounce": "^4.0.6",
"@types/lodash.get": "^4.4.6", "@types/lodash.get": "^4.4.6",
"@types/lodash.throttle": "^4.1.6", "@types/lodash.throttle": "^4.1.6",
"@types/marked": "^5.0.0",
"@types/node": "^20.4.0", "@types/node": "^20.4.0",
"@types/qs": "^6.9.7", "@types/qs": "^6.9.7",
"@types/react-input-mask": "^3.0.2", "@types/react-input-mask": "^3.0.2",
@ -18,7 +17,8 @@
"lodash.debounce": "^4.0.8", "lodash.debounce": "^4.0.8",
"lodash.get": "^4.4.2", "lodash.get": "^4.4.2",
"lodash.throttle": "^4.1.1", "lodash.throttle": "^4.1.1",
"marked": "^5.1.0", "marked": "^12.0.2",
"marked-emoji": "^1.4.0",
"preact": "^10.7.1", "preact": "^10.7.1",
"qs": "^6.10.3", "qs": "^6.10.3",
"react-input-mask": "^2.0.4", "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 ThemeProvider from "./components/Main/ThemeProvider/ThemeProvider";
import ExploreLogs from "./pages/ExploreLogs/ExploreLogs"; import ExploreLogs from "./pages/ExploreLogs/ExploreLogs";
import LogsLayout from "./layouts/LogsLayout/LogsLayout"; import LogsLayout from "./layouts/LogsLayout/LogsLayout";
import "./constants/markedPlugins";
const AppLogs: FC = () => { const AppLogs: FC = () => {
const [loadedTheme, setLoadedTheme] = useState(false); 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 processMarkdown = (text: string) => {
const div = document.createElement("div"); const div = document.createElement("div");
div.innerHTML = marked(text); div.innerHTML = marked(text) as string;
const groups = div.querySelectorAll(`${CATEGORY_TAG}, ${FUNCTION_TAG}`); const groups = div.querySelectorAll(`${CATEGORY_TAG}, ${FUNCTION_TAG}`);
return processGroups(groups); return processGroups(groups);
}; };

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -96,6 +96,65 @@
font-style: italic; font-style: italic;
text-align: center; 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> <div>
<span>Description:</span> <span>Description:</span>
<div dangerouslySetInnerHTML={{ __html: marked.parse(description) }}/> <div dangerouslySetInnerHTML={{ __html: marked(description) as string }}/>
</div> </div>
<hr/> <hr/>
</> </>

View file

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

View file

@ -19,6 +19,8 @@ according to [these docs](https://docs.victoriametrics.com/victorialogs/quicksta
## tip ## 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) ## [v0.18.0](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v0.18.0-victorialogs)
Released at 2024-06-06 Released at 2024-06-06