mirror of
https://github.com/VictoriaMetrics/VictoriaMetrics.git
synced 2024-11-23 12:31:07 +01:00
vmui: add trace analyzer (#3310)
* refactor: change structure project * refactor: change structure project * fix: add hooks for set query params * refactor: add index for pages * docs: add TESTCASES.md * refactor: restructure components * feat: add page with trace analyzer * fix: change detect trace data * Update app/vmui/packages/vmui/src/pages/TracePage/index.tsx Co-authored-by: Zakhar Bessarab <z.bessarab@victoriametrics.com> * Update app/vmui/packages/vmui/src/pages/TracePage/index.tsx Co-authored-by: Zakhar Bessarab <z.bessarab@victoriametrics.com> * fix: change descriptions on trace page * Update app/vmui/packages/vmui/src/pages/TracePage/index.tsx Co-authored-by: Zakhar Bessarab <z.bessarab@victoriametrics.com> * feat: add base components * feat: add reset styles * docs: add description about trace analyzer * feat: add styles for custom panel page * feat: add styles for predefined panels * feat: add style for TracingsView.tsx * feat: add Alerts * feat: add Tooltip.tsx * fix: correct styles * feat: add DatePicker.tsx * feat: add tables * feat: add theme provider * fix: replace using callbacks as props to handlers * fix: correct update time * fix: change TimePicker.tsx * fix: correct styles * fix: update packages * vmui: refactor code, remove material-ui * feat: add paste json for trace analyzer * vmui: update trace analyzer docs * app/vmselect/vmui: `make vmui-update` Co-authored-by: Zakhar Bessarab <z.bessarab@victoriametrics.com> Co-authored-by: Aliaksandr Valialkin <valyala@victoriametrics.com>
This commit is contained in:
parent
e79bfdf4b8
commit
519bd2af7b
@ -1644,7 +1644,9 @@ All the durations and timestamps in traces are in milliseconds.
|
||||
|
||||
Query tracing is allowed by default. It can be denied by passing `-denyQueryTracing` command-line flag to VictoriaMetrics.
|
||||
|
||||
[VMUI](#vmui) provides an UI for query tracing - just click `Trace query` checkbox and re-run the query in order to investigate its' trace.
|
||||
[VMUI](#vmui) provides an UI:
|
||||
- for query tracing - just click `Trace query` checkbox and re-run the query in order to investigate its' trace.
|
||||
- for exploring custom trace - go to the tab `Trace analyzer` and upload or paste JSON with trace information.
|
||||
|
||||
|
||||
## Cardinality limiter
|
||||
|
@ -1,12 +1,12 @@
|
||||
{
|
||||
"files": {
|
||||
"main.css": "./static/css/main.f2cbb582.css",
|
||||
"main.js": "./static/js/main.d0509bc3.js",
|
||||
"main.css": "./static/css/main.44a8ea12.css",
|
||||
"main.js": "./static/js/main.da9caa97.js",
|
||||
"static/js/27.c1ccfd29.chunk.js": "./static/js/27.c1ccfd29.chunk.js",
|
||||
"index.html": "./index.html"
|
||||
},
|
||||
"entrypoints": [
|
||||
"static/css/main.f2cbb582.css",
|
||||
"static/js/main.d0509bc3.js"
|
||||
"static/css/main.44a8ea12.css",
|
||||
"static/js/main.da9caa97.js"
|
||||
]
|
||||
}
|
@ -1 +1 @@
|
||||
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="./favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><meta name="description" content="VM-UI is a metric explorer for Victoria Metrics"/><link rel="apple-touch-icon" href="./apple-touch-icon.png"/><link rel="icon" type="image/png" sizes="32x32" href="./favicon-32x32.png"><link rel="manifest" href="./manifest.json"/><title>VM UI</title><link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin><link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono&family=Lato:wght@300;400;700&display=swap" rel="stylesheet"><script src="./dashboards/index.js" type="module"></script><script defer="defer" src="./static/js/main.d0509bc3.js"></script><link href="./static/css/main.f2cbb582.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>
|
||||
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="./favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><meta name="description" content="VM-UI is a metric explorer for Victoria Metrics"/><link rel="apple-touch-icon" href="./apple-touch-icon.png"/><link rel="icon" type="image/png" sizes="32x32" href="./favicon-32x32.png"><link rel="manifest" href="./manifest.json"/><title>VM UI</title><link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin><link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono&family=Lato:wght@300;400;700&display=swap" rel="stylesheet"><script src="./dashboards/index.js" type="module"></script><script defer="defer" src="./static/js/main.da9caa97.js"></script><link href="./static/css/main.44a8ea12.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>
|
1
app/vmselect/vmui/static/css/main.44a8ea12.css
Normal file
1
app/vmselect/vmui/static/css/main.44a8ea12.css
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
2
app/vmselect/vmui/static/js/main.da9caa97.js
Normal file
2
app/vmselect/vmui/static/js/main.da9caa97.js
Normal file
File diff suppressed because one or more lines are too long
@ -9,6 +9,7 @@ import CardinalityPanel from "./pages/CardinalityPanel";
|
||||
import TopQueries from "./pages/TopQueries";
|
||||
import ThemeProvider from "./components/Main/ThemeProvider/ThemeProvider";
|
||||
import Spinner from "./components/Main/Spinner/Spinner";
|
||||
import TracePage from "./pages/TracePage";
|
||||
|
||||
const App: FC = () => {
|
||||
|
||||
@ -45,6 +46,10 @@ const App: FC = () => {
|
||||
path={router.topQueries}
|
||||
element={<TopQueries/>}
|
||||
/>
|
||||
<Route
|
||||
path={router.trace}
|
||||
element={<TracePage/>}
|
||||
/>
|
||||
</Route>
|
||||
</Routes>
|
||||
</AppContextProvider>
|
||||
|
@ -43,6 +43,10 @@ const Header: FC = () => {
|
||||
{
|
||||
label: "Top queries",
|
||||
value: router.topQueries,
|
||||
},
|
||||
{
|
||||
label: "Trace analyzer",
|
||||
value: router.trace,
|
||||
}
|
||||
]), [appModeEnable]);
|
||||
|
||||
|
@ -4,7 +4,7 @@
|
||||
position: relative;
|
||||
display: grid;
|
||||
grid-template-columns: 20px 1fr;
|
||||
align-items: center;
|
||||
align-items: flex-start;
|
||||
gap: $padding-small;
|
||||
padding: $padding-global;
|
||||
background-color: $color-background-block;
|
||||
@ -13,7 +13,7 @@
|
||||
font-size: $font-size-medium;
|
||||
font-weight: 500;
|
||||
color: $color-text;
|
||||
line-height: 1.3;
|
||||
line-height: 20px;
|
||||
|
||||
&:after {
|
||||
position: absolute;
|
||||
@ -41,6 +41,7 @@
|
||||
|
||||
&__content {
|
||||
filter: brightness(0.6);
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
&_success {
|
||||
|
@ -279,7 +279,6 @@ export const VisibilityIcon = () => (
|
||||
</svg>
|
||||
);
|
||||
|
||||
|
||||
export const VisibilityOffIcon = () => (
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
@ -290,3 +289,14 @@ export const VisibilityOffIcon = () => (
|
||||
></path>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const CopyIcon = () => (
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path
|
||||
d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z"
|
||||
></path>
|
||||
</svg>
|
||||
);
|
||||
|
@ -16,11 +16,9 @@ $padding-modal: 22px;
|
||||
|
||||
&-content {
|
||||
padding: $padding-modal;
|
||||
max-height: 85vh;
|
||||
background: $color-white;
|
||||
box-shadow: 0 0 24px rgba($color-black, 0.07);
|
||||
border-radius: $border-radius-small;
|
||||
overflow: hidden;
|
||||
|
||||
&-header {
|
||||
display: grid;
|
||||
|
@ -64,7 +64,10 @@ const TextField: FC<TextFieldProps> = ({
|
||||
}, [fieldRef, autofocus]);
|
||||
|
||||
return <label
|
||||
className="vm-text-field"
|
||||
className={classNames({
|
||||
"vm-text-field": true,
|
||||
"vm-text-field_textarea": type === "textarea",
|
||||
})}
|
||||
data-replicated-value={value}
|
||||
>
|
||||
{startIcon && <div className="vm-text-field__icon-start">{startIcon}</div>}
|
||||
|
@ -6,7 +6,7 @@
|
||||
margin: 6px 0;
|
||||
width: 100%;
|
||||
|
||||
&::after {
|
||||
&_textarea:after {
|
||||
content: attr(data-replicated-value) " ";
|
||||
white-space: pre-wrap;
|
||||
visibility: hidden;
|
||||
|
@ -3,12 +3,14 @@ import { TracingData } from "../../api/types";
|
||||
let traceId = 0;
|
||||
|
||||
export default class Trace {
|
||||
private readonly tracing: TracingData;
|
||||
private readonly tracingChildren: Trace[];
|
||||
private readonly query: string;
|
||||
private tracing: TracingData;
|
||||
private query: string;
|
||||
private tracingChildren: Trace[];
|
||||
private readonly originalTracing: TracingData;
|
||||
private readonly id: number;
|
||||
constructor(tracingData: TracingData, query: string) {
|
||||
this.tracing = tracingData;
|
||||
this.originalTracing = JSON.parse(JSON.stringify(tracingData));
|
||||
this.query = query;
|
||||
this.id = traceId++;
|
||||
const children = tracingData.children || [];
|
||||
@ -30,4 +32,21 @@ export default class Trace {
|
||||
get duration(): number {
|
||||
return this.tracing.duration_msec;
|
||||
}
|
||||
get JSON(): string {
|
||||
return JSON.stringify(this.tracing, null, 2);
|
||||
}
|
||||
get originalJSON(): string {
|
||||
return JSON.stringify(this.originalTracing, null, 2);
|
||||
}
|
||||
setTracing (tracingData: TracingData) {
|
||||
this.tracing = tracingData;
|
||||
const children = tracingData.children || [];
|
||||
this.tracingChildren = children.map((x: TracingData) => new Trace(x, this.query));
|
||||
}
|
||||
setQuery (query: string) {
|
||||
this.query = query;
|
||||
}
|
||||
resetTracing () {
|
||||
this.tracing = this.originalTracing;
|
||||
}
|
||||
}
|
||||
|
@ -1,18 +1,38 @@
|
||||
import React, { FC } from "preact/compat";
|
||||
import React, { FC, useState } from "preact/compat";
|
||||
import Trace from "./Trace";
|
||||
import Button from "../Main/Button/Button";
|
||||
import { DeleteIcon } from "../Main/Icons";
|
||||
import { CodeIcon, DeleteIcon } from "../Main/Icons";
|
||||
import "./style.scss";
|
||||
import NestedNav from "./NestedNav/NestedNav";
|
||||
import Alert from "../Main/Alert/Alert";
|
||||
import Tooltip from "../Main/Tooltip/Tooltip";
|
||||
import Modal from "../Main/Modal/Modal";
|
||||
import JsonForm from "../../pages/TracePage/JsonForm/JsonForm";
|
||||
|
||||
interface TraceViewProps {
|
||||
traces: Trace[];
|
||||
jsonEditor?: boolean;
|
||||
onDeleteClick: (trace: Trace) => void;
|
||||
}
|
||||
|
||||
const TracingsView: FC<TraceViewProps> = ({ traces, onDeleteClick }) => {
|
||||
const TracingsView: FC<TraceViewProps> = ({ traces, jsonEditor = false, onDeleteClick }) => {
|
||||
const [openTrace, setOpenTrace] = useState<Trace | null>(null);
|
||||
|
||||
const handleCloseJson = () => {
|
||||
setOpenTrace(null);
|
||||
};
|
||||
|
||||
const handleUpdateTrace = (val: string, title: string) => {
|
||||
if (!jsonEditor || !openTrace) return;
|
||||
try {
|
||||
openTrace.setTracing(JSON.parse(val));
|
||||
openTrace.setQuery(title);
|
||||
setOpenTrace(null);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
};
|
||||
|
||||
if (!traces.length) {
|
||||
return (
|
||||
<Alert variant="info">
|
||||
@ -25,34 +45,66 @@ const TracingsView: FC<TraceViewProps> = ({ traces, onDeleteClick }) => {
|
||||
onDeleteClick(tracingData);
|
||||
};
|
||||
|
||||
return <div className="vm-tracings-view">
|
||||
{traces.map((trace: Trace) => (
|
||||
<div
|
||||
className="vm-tracings-view-trace vm-block vm-block_empty-padding"
|
||||
key={trace.idValue}
|
||||
>
|
||||
<div className="vm-tracings-view-trace-header">
|
||||
<h3 className="vm-tracings-view-trace-header-title">
|
||||
Trace for <b className="vm-tracings-view-trace-header-title__query">{trace.queryValue}</b>
|
||||
</h3>
|
||||
<Tooltip title={"Remove trace"}>
|
||||
<Button
|
||||
variant="text"
|
||||
color="error"
|
||||
startIcon={<DeleteIcon/>}
|
||||
onClick={handleDeleteClick(trace)}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<nav className="vm-tracings-view-trace__nav">
|
||||
<NestedNav
|
||||
trace={trace}
|
||||
totalMsec={trace.duration}
|
||||
/>
|
||||
</nav>
|
||||
const handleJsonClick = (tracingData: Trace) => () => {
|
||||
setOpenTrace(tracingData);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="vm-tracings-view">
|
||||
{traces.map((trace: Trace) => (
|
||||
<div
|
||||
className="vm-tracings-view-trace vm-block vm-block_empty-padding"
|
||||
key={trace.idValue}
|
||||
>
|
||||
<div className="vm-tracings-view-trace-header">
|
||||
<h3 className="vm-tracings-view-trace-header-title">
|
||||
Trace for <b className="vm-tracings-view-trace-header-title__query">{trace.queryValue}</b>
|
||||
</h3>
|
||||
<Tooltip title={"Open JSON"}>
|
||||
<Button
|
||||
variant="text"
|
||||
startIcon={<CodeIcon/>}
|
||||
onClick={handleJsonClick(trace)}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip title={"Remove trace"}>
|
||||
<Button
|
||||
variant="text"
|
||||
color="error"
|
||||
startIcon={<DeleteIcon/>}
|
||||
onClick={handleDeleteClick(trace)}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<nav className="vm-tracings-view-trace__nav">
|
||||
<NestedNav
|
||||
trace={trace}
|
||||
totalMsec={trace.duration}
|
||||
/>
|
||||
</nav>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>;
|
||||
|
||||
{openTrace && (
|
||||
<Modal
|
||||
title={openTrace.queryValue}
|
||||
onClose={handleCloseJson}
|
||||
>
|
||||
<JsonForm
|
||||
editable={jsonEditor}
|
||||
displayTitle={jsonEditor}
|
||||
defaultTile={openTrace.queryValue}
|
||||
defaultJson={openTrace.JSON}
|
||||
resetValue={openTrace.originalJSON}
|
||||
onClose={handleCloseJson}
|
||||
onUpload={handleUpdateTrace}
|
||||
/>
|
||||
</Modal>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default TracingsView;
|
||||
|
@ -13,6 +13,7 @@
|
||||
padding: $padding-small $padding-small $padding-small $padding-medium;
|
||||
|
||||
&-title {
|
||||
flex-grow: 1;
|
||||
font-size: $font-size-medium;
|
||||
margin-right: $padding-small;
|
||||
|
||||
|
@ -24,7 +24,6 @@ const JsonView: FC<JsonViewProps> = ({ data }) => {
|
||||
<div className="vm-json-view__copy">
|
||||
<Button
|
||||
variant="outlined"
|
||||
fullWidth={false}
|
||||
onClick={handlerCopy}
|
||||
>
|
||||
Copy JSON
|
||||
|
140
app/vmui/packages/vmui/src/pages/TracePage/JsonForm/JsonForm.tsx
Normal file
140
app/vmui/packages/vmui/src/pages/TracePage/JsonForm/JsonForm.tsx
Normal file
@ -0,0 +1,140 @@
|
||||
import React, { FC, useState, useMemo } from "preact/compat";
|
||||
import TextField from "../../../components/Main/TextField/TextField";
|
||||
import "./style.scss";
|
||||
import Button from "../../../components/Main/Button/Button";
|
||||
import Trace from "../../../components/TraceQuery/Trace";
|
||||
import { ErrorTypes } from "../../../types";
|
||||
import classNames from "classnames";
|
||||
import { useSnack } from "../../../contexts/Snackbar";
|
||||
import { CopyIcon, RestartIcon } from "../../../components/Main/Icons";
|
||||
|
||||
interface JsonFormProps {
|
||||
defaultJson?: string
|
||||
defaultTile?: string
|
||||
displayTitle?: boolean
|
||||
editable?: boolean
|
||||
resetValue?: string
|
||||
onUpload: (json: string, title: string) => void
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
const JsonForm: FC<JsonFormProps> = ({
|
||||
editable = false,
|
||||
defaultTile = "JSON",
|
||||
displayTitle = true,
|
||||
defaultJson = "",
|
||||
resetValue= "",
|
||||
onClose,
|
||||
onUpload,
|
||||
}) => {
|
||||
const { showInfoMessage } = useSnack();
|
||||
|
||||
const [json, setJson] = useState(defaultJson);
|
||||
const [title, setTitle] = useState(defaultTile);
|
||||
const [errorTitle, setErrorTitle] = useState("");
|
||||
const [error, setError] = useState("");
|
||||
|
||||
const errorJson = useMemo(() => {
|
||||
try {
|
||||
const resp = JSON.parse(json);
|
||||
const traceData = resp.trace || resp;
|
||||
if (!traceData.duration_msec) return ErrorTypes.traceNotFound;
|
||||
new Trace(traceData, "");
|
||||
return "";
|
||||
} catch (e) {
|
||||
return e instanceof Error ? e.message : "Unknown error";
|
||||
}
|
||||
}, [json]);
|
||||
|
||||
const handleChangeTitle = (val: string) => {
|
||||
setTitle(val);
|
||||
};
|
||||
|
||||
const handleChangeJson = (val: string) => {
|
||||
setError("");
|
||||
setJson(val);
|
||||
};
|
||||
|
||||
const handlerCopy = async () => {
|
||||
await navigator.clipboard.writeText(json);
|
||||
showInfoMessage({ text: "Formatted JSON has been copied", type: "success" });
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
setJson(resetValue);
|
||||
};
|
||||
|
||||
const handleApply = () => {
|
||||
setError(errorJson);
|
||||
const titleTrim = title.trim();
|
||||
if (!titleTrim) setErrorTitle(ErrorTypes.emptyTitle);
|
||||
if (errorJson || errorTitle) return;
|
||||
onUpload(json, title);
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames({
|
||||
"vm-json-form": true,
|
||||
"vm-json-form_one-field": !displayTitle
|
||||
})}
|
||||
>
|
||||
{displayTitle && (
|
||||
<TextField
|
||||
value={title}
|
||||
label="Title"
|
||||
error={errorTitle}
|
||||
onEnter={handleApply}
|
||||
onChange={handleChangeTitle}
|
||||
/>
|
||||
)}
|
||||
<TextField
|
||||
value={json}
|
||||
label="JSON"
|
||||
type="textarea"
|
||||
error={error}
|
||||
autofocus
|
||||
onChange={handleChangeJson}
|
||||
disabled={!editable}
|
||||
/>
|
||||
<div className="vm-json-form-footer">
|
||||
<div className="vm-json-form-footer__controls">
|
||||
<Button
|
||||
variant="outlined"
|
||||
startIcon={<CopyIcon/>}
|
||||
onClick={handlerCopy}
|
||||
>
|
||||
Copy JSON
|
||||
</Button>
|
||||
{resetValue && (
|
||||
<Button
|
||||
variant="text"
|
||||
startIcon={<RestartIcon/>}
|
||||
onClick={handleReset}
|
||||
>
|
||||
Reset JSON
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<div className="vm-json-form-footer__controls vm-json-form-footer__controls_right">
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="error"
|
||||
onClick={onClose}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={handleApply}
|
||||
>
|
||||
apply
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default JsonForm;
|
@ -0,0 +1,41 @@
|
||||
@use "src/styles/variables" as *;
|
||||
|
||||
.vm-json-form {
|
||||
display: grid;
|
||||
grid-template-rows: auto calc(90vh - 78px - ($padding-medium*3)) auto;
|
||||
gap: $padding-global;
|
||||
width: 70vw;
|
||||
max-width: 1000px;
|
||||
max-height: 900px;
|
||||
|
||||
&_one-field {
|
||||
grid-template-rows: calc(90vh - 78px - ($padding-medium*3)) auto;
|
||||
}
|
||||
|
||||
textarea {
|
||||
overflow: auto;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
&-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: $padding-small;
|
||||
|
||||
&__controls {
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: $padding-small;
|
||||
|
||||
&_right {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 90px);
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
177
app/vmui/packages/vmui/src/pages/TracePage/index.tsx
Normal file
177
app/vmui/packages/vmui/src/pages/TracePage/index.tsx
Normal file
@ -0,0 +1,177 @@
|
||||
import React, { FC, useMemo, useState } from "preact/compat";
|
||||
import { ChangeEvent } from "react";
|
||||
import Trace from "../../components/TraceQuery/Trace";
|
||||
import TracingsView from "../../components/TraceQuery/TracingsView";
|
||||
import Tooltip from "../../components/Main/Tooltip/Tooltip";
|
||||
import Button from "../../components/Main/Button/Button";
|
||||
import Alert from "../../components/Main/Alert/Alert";
|
||||
import "./style.scss";
|
||||
import { CloseIcon } from "../../components/Main/Icons";
|
||||
import Modal from "../../components/Main/Modal/Modal";
|
||||
import JsonForm from "./JsonForm/JsonForm";
|
||||
import { ErrorTypes } from "../../types";
|
||||
|
||||
const TracePage: FC = () => {
|
||||
const [openModal, setOpenModal] = useState(false);
|
||||
const [tracesState, setTracesState] = useState<Trace[]>([]);
|
||||
const [errors, setErrors] = useState<{filename: string, text: string}[]>([]);
|
||||
const hasTraces = useMemo(() => !!tracesState.length, [tracesState]);
|
||||
|
||||
const handleOpenModal = () => {
|
||||
setOpenModal(true);
|
||||
};
|
||||
|
||||
const handleCloseModal = () => {
|
||||
setOpenModal(false);
|
||||
};
|
||||
|
||||
const handleError = (e: Error, filename = "") => {
|
||||
setErrors(prev => [{ filename, text: `: ${e.message}` }, ...prev]);
|
||||
};
|
||||
|
||||
const handleOnload = (result: string, filename: string) => {
|
||||
try {
|
||||
const resp = JSON.parse(result);
|
||||
const traceData = resp.trace || resp;
|
||||
if (!traceData.duration_msec) {
|
||||
handleError(new Error(ErrorTypes.traceNotFound), filename);
|
||||
return;
|
||||
}
|
||||
const trace = new Trace(traceData, filename);
|
||||
setTracesState(prev => [trace, ...prev]);
|
||||
} catch (e) {
|
||||
if (e instanceof Error) handleError(e, filename);
|
||||
}
|
||||
};
|
||||
|
||||
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
setErrors([]);
|
||||
const files = Array.from(e.target.files || []);
|
||||
files.map(f => {
|
||||
const reader = new FileReader();
|
||||
const filename = f?.name || "";
|
||||
reader.onload = (e) => {
|
||||
const result = String(e.target?.result);
|
||||
handleOnload(result, filename);
|
||||
};
|
||||
reader.readAsText(f);
|
||||
});
|
||||
e.target.value = "";
|
||||
};
|
||||
|
||||
const handleTraceDelete = (trace: Trace) => {
|
||||
const updatedTraces = tracesState.filter((data) => data.idValue !== trace.idValue);
|
||||
setTracesState([...updatedTraces]);
|
||||
};
|
||||
|
||||
const handleCloseError = (index: number) => {
|
||||
setErrors(prev => prev.filter((e,i) => i !== index));
|
||||
};
|
||||
|
||||
const createHandlerCloseError = (index: number) => () => {
|
||||
handleCloseError(index);
|
||||
};
|
||||
|
||||
const UploadButtons = () => (
|
||||
<div className="vm-trace-page-controls">
|
||||
<Button
|
||||
variant="outlined"
|
||||
onClick={handleOpenModal}
|
||||
>
|
||||
Paste JSON
|
||||
</Button>
|
||||
<Tooltip title="The file must contain tracing information in JSON format">
|
||||
<Button>
|
||||
Upload Files
|
||||
<input
|
||||
id="json"
|
||||
type="file"
|
||||
accept="application/json"
|
||||
multiple
|
||||
title=" "
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="vm-trace-page">
|
||||
<div className="vm-trace-page-header">
|
||||
<div className="vm-trace-page-header-errors">
|
||||
{errors.map((error, i) => (
|
||||
<div
|
||||
className="vm-trace-page-header-errors-item"
|
||||
key={`${error}_${i}`}
|
||||
>
|
||||
<Alert variant="error">
|
||||
<b className="vm-trace-page-header-errors-item__filename">{error.filename}</b>
|
||||
<span>{error.text}</span>
|
||||
</Alert>
|
||||
<Button
|
||||
className="vm-trace-page-header-errors-item__close"
|
||||
startIcon={<CloseIcon/>}
|
||||
variant="text"
|
||||
color="error"
|
||||
onClick={createHandlerCloseError(i)}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div>
|
||||
{hasTraces && <UploadButtons/>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{hasTraces && (
|
||||
<div>
|
||||
<TracingsView
|
||||
jsonEditor={true}
|
||||
traces={tracesState}
|
||||
onDeleteClick={handleTraceDelete}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!hasTraces && (
|
||||
<div className="vm-trace-page-preview">
|
||||
<p className="vm-trace-page-preview__text">
|
||||
Please, upload file with JSON response content.
|
||||
{"\n"}
|
||||
The file must contain tracing information in JSON format.
|
||||
{"\n"}
|
||||
In order to use tracing please refer to the doc:
|
||||
<a
|
||||
href="https://docs.victoriametrics.com/#query-tracing"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
https://docs.victoriametrics.com/#query-tracing
|
||||
</a>
|
||||
{"\n"}
|
||||
Tracing graph will be displayed after file upload.
|
||||
</p>
|
||||
<UploadButtons/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{openModal && (
|
||||
<Modal
|
||||
title="Paste JSON"
|
||||
onClose={handleCloseModal}
|
||||
>
|
||||
<JsonForm
|
||||
editable
|
||||
displayTitle
|
||||
defaultTile={`JSON ${tracesState.length + 1}`}
|
||||
onClose={handleCloseModal}
|
||||
onUpload={handleOnload}
|
||||
/>
|
||||
</Modal>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TracePage;
|
66
app/vmui/packages/vmui/src/pages/TracePage/style.scss
Normal file
66
app/vmui/packages/vmui/src/pages/TracePage/style.scss
Normal file
@ -0,0 +1,66 @@
|
||||
@use "src/styles/variables" as *;
|
||||
|
||||
.vm-trace-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: $padding-global;
|
||||
min-height: 100%;
|
||||
|
||||
&-controls {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: $padding-global;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
&-header {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
align-items: start;
|
||||
gap: $padding-global;
|
||||
margin-bottom: $padding-medium;
|
||||
|
||||
&-errors {
|
||||
display: grid;
|
||||
align-items: flex-start;
|
||||
justify-content: stretch;
|
||||
grid-template-columns: 1fr;
|
||||
gap: $padding-medium;
|
||||
|
||||
&-item {
|
||||
position: relative;
|
||||
display: grid;
|
||||
align-items: center;
|
||||
justify-content: stretch;
|
||||
|
||||
&__filename {
|
||||
min-height: 20px;
|
||||
}
|
||||
|
||||
&__close {
|
||||
position: absolute;
|
||||
top: auto;
|
||||
right: $padding-small;
|
||||
z-index: 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-preview {
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&__text {
|
||||
margin-bottom: $padding-small;
|
||||
font-size: $font-size-medium;
|
||||
white-space: pre-line;
|
||||
text-align: center;
|
||||
line-height: 1.8;
|
||||
}
|
||||
}
|
||||
}
|
@ -3,6 +3,7 @@ const router = {
|
||||
dashboards: "/dashboards",
|
||||
cardinality: "/cardinality",
|
||||
topQueries: "/top-queries",
|
||||
trace: "/trace"
|
||||
};
|
||||
|
||||
export interface RouterOptions {
|
||||
|
@ -38,5 +38,5 @@ input[type=number]::-webkit-outer-spin-button {
|
||||
position: fixed;
|
||||
left: $padding-global;
|
||||
bottom: $padding-global;
|
||||
z-index: 9;
|
||||
z-index: 999;
|
||||
}
|
||||
|
@ -40,7 +40,9 @@ export interface InstantDataSeries {
|
||||
export enum ErrorTypes {
|
||||
emptyServer = "Please enter Server URL",
|
||||
validServer = "Please provide a valid Server URL",
|
||||
validQuery = "Please enter a valid Query and execute it"
|
||||
validQuery = "Please enter a valid Query and execute it",
|
||||
traceNotFound = "Not found the tracing information",
|
||||
emptyTitle = "Please enter title",
|
||||
}
|
||||
|
||||
export interface PanelSettings {
|
||||
|
@ -17,6 +17,7 @@ The following tip changes can be tested by building VictoriaMetrics components f
|
||||
|
||||
* FEATURE: [MetricsQL](https://docs.victoriametrics.com/MetricsQL.html): add [range_linear_regression](https://docs.victoriametrics.com/MetricsQL.html#range_linear_regression) function for calculating [simple linear regression](https://en.wikipedia.org/wiki/Simple_linear_regression) over the input time series on the selected time range. This function is useful for predictions and capacity planning. For example, `range_linear_regression(process_resident_memory_bytes)` can predict future memory usage based on the past memory usage.
|
||||
* FEATURE: [MetricsQL](https://docs.victoriametrics.com/MetricsQL.html): add [range_stddev](https://docs.victoriametrics.com/MetricsQL.html#range_stddev) and [range_stdvar](https://docs.victoriametrics.com/MetricsQL.html#range_stdvar) functions.
|
||||
* FEATURE: [vmui](https://docs.victoriametrics.com/#vmui): add the ability to upload/paste JSON to investigate the trace. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/3308) and [this pull request](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/3310).
|
||||
* FEATURE: [vmui](https://docs.victoriametrics.com/#vmui): reduce JS bundle size from 200Kb to 100Kb. See [this pull request](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/3298).
|
||||
* FEATURE: [vmui](https://docs.victoriametrics.com/#vmui): add the ability to hide results of a particular query by clicking the `eye` icon. See [this pull request](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/3359).
|
||||
|
||||
|
@ -1645,7 +1645,9 @@ All the durations and timestamps in traces are in milliseconds.
|
||||
|
||||
Query tracing is allowed by default. It can be denied by passing `-denyQueryTracing` command-line flag to VictoriaMetrics.
|
||||
|
||||
[VMUI](#vmui) provides an UI for query tracing - just click `Trace query` checkbox and re-run the query in order to investigate its' trace.
|
||||
[VMUI](#vmui) provides an UI:
|
||||
- for query tracing - just click `Trace query` checkbox and re-run the query in order to investigate its' trace.
|
||||
- for exploring custom trace - go to the tab `Trace analyzer` and upload or paste JSON with trace information.
|
||||
|
||||
|
||||
## Cardinality limiter
|
||||
|
@ -1648,7 +1648,9 @@ All the durations and timestamps in traces are in milliseconds.
|
||||
|
||||
Query tracing is allowed by default. It can be denied by passing `-denyQueryTracing` command-line flag to VictoriaMetrics.
|
||||
|
||||
[VMUI](#vmui) provides an UI for query tracing - just click `Trace query` checkbox and re-run the query in order to investigate its' trace.
|
||||
[VMUI](#vmui) provides an UI:
|
||||
- for query tracing - just click `Trace query` checkbox and re-run the query in order to investigate its' trace.
|
||||
- for exploring custom trace - go to the tab `Trace analyzer` and upload or paste JSON with trace information.
|
||||
|
||||
|
||||
## Cardinality limiter
|
||||
|
Loading…
Reference in New Issue
Block a user