vmui: Integrate WITH template playground (#3831)

* feat: add WithTemplate page

* app/vmselect/prometheus: enable json mode for expand with expr API

* app/vmselect/prometheus: enable CORS and add content type

* feat: add api for expand with templates

* fix: remove console from useExpandWithExprs

* app/vmselect/prometheus: fix escaping

* vmui:  integrate WITH template

* app/vmctl: check content type instead of form param

* fix: add content-type for fetch with-exprs

* fix: add a header to the server's response that allows the "Content-Type" header

* app/vmctl: added comment and cleanup

* app/vmctl: use format query param

---------

Co-authored-by: dmitryk-dk <kozlovdmitriyy@gmail.com>
This commit is contained in:
Yury Molodov 2023-04-25 10:40:01 +02:00 committed by GitHub
parent e2053baf32
commit 68e31a6000
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 664 additions and 73 deletions

View File

@ -469,6 +469,7 @@ func RequestHandler(w http.ResponseWriter, r *http.Request) bool {
return true
case "/expand-with-exprs":
expandWithExprsRequests.Inc()
httpserver.EnableCORS(w, r)
prometheus.ExpandWithExprs(w, r)
return true
case "/api/v1/rules", "/rules":

View File

@ -1,4 +1,5 @@
{% import (
"fmt"
"github.com/VictoriaMetrics/metricsql"
) %}
@ -243,3 +244,31 @@ WITH (
</pre>
{% endfunc %}
{% stripspace %}
{% func ExpandWithExprsJSONResponse(q string) %}
{%= expandWithExprsJSON(q) %}
{% endfunc %}
{% func expandWithExprsJSON(q string) %}
{% if len(q) == 0 %}
{
"status": "error",
"error": "query string cannot be empty"
}
{% return %}
{% endif %}
{
{% code expr, err := metricsql.Parse(q) %}
{% if err != nil %}
"status": "error",
"error": {%q= fmt.Sprintf("Cannot parse query: %s", err) %}
{% else %}
{% code expr = metricsql.Optimize(expr) %}
"status": "success",
"expr": {%q= string(expr.AppendString(nil)) %}
{% endif %}
}
{% endfunc %}
{% endstripspace %}

View File

@ -6,127 +6,128 @@ package prometheus
//line app/vmselect/prometheus/expand-with-exprs.qtpl:1
import (
"fmt"
"github.com/VictoriaMetrics/metricsql"
)
// ExpandWithExprsResponse returns a webpage, which expands with templates in q MetricsQL.
//line app/vmselect/prometheus/expand-with-exprs.qtpl:8
//line app/vmselect/prometheus/expand-with-exprs.qtpl:9
import (
qtio422016 "io"
qt422016 "github.com/valyala/quicktemplate"
)
//line app/vmselect/prometheus/expand-with-exprs.qtpl:8
//line app/vmselect/prometheus/expand-with-exprs.qtpl:9
var (
_ = qtio422016.Copy
_ = qt422016.AcquireByteBuffer
)
//line app/vmselect/prometheus/expand-with-exprs.qtpl:8
//line app/vmselect/prometheus/expand-with-exprs.qtpl:9
func StreamExpandWithExprsResponse(qw422016 *qt422016.Writer, q string) {
//line app/vmselect/prometheus/expand-with-exprs.qtpl:8
//line app/vmselect/prometheus/expand-with-exprs.qtpl:9
qw422016.N().S(`<html><head><title>Expand WITH expressions</title><style>p { font-weight: bold }textarea { margin: 1em }</style></head><body><div><form method="get"><div><p><a href="https://docs.victoriametrics.com/MetricsQL.html">MetricsQL</a> query with optional WITH expressions:</p><textarea name="query" style="height: 15em; width: 90%">`)
//line app/vmselect/prometheus/expand-with-exprs.qtpl:25
//line app/vmselect/prometheus/expand-with-exprs.qtpl:26
qw422016.E().S(q)
//line app/vmselect/prometheus/expand-with-exprs.qtpl:25
//line app/vmselect/prometheus/expand-with-exprs.qtpl:26
qw422016.N().S(`</textarea><br/><input type="submit" value="Expand" /><p><a href="https://docs.victoriametrics.com/MetricsQL.html">MetricsQL</a> query after expanding WITH expressions and applying other optimizations:</p><textarea style="height: 5em; width: 90%" readonly="readonly">`)
//line app/vmselect/prometheus/expand-with-exprs.qtpl:31
//line app/vmselect/prometheus/expand-with-exprs.qtpl:32
streamexpandWithExprs(qw422016, q)
//line app/vmselect/prometheus/expand-with-exprs.qtpl:31
//line app/vmselect/prometheus/expand-with-exprs.qtpl:32
qw422016.N().S(`</textarea></div></form></div><div>`)
//line app/vmselect/prometheus/expand-with-exprs.qtpl:36
//line app/vmselect/prometheus/expand-with-exprs.qtpl:37
streamwithExprsTutorial(qw422016)
//line app/vmselect/prometheus/expand-with-exprs.qtpl:36
//line app/vmselect/prometheus/expand-with-exprs.qtpl:37
qw422016.N().S(`</div></body></html>`)
//line app/vmselect/prometheus/expand-with-exprs.qtpl:40
//line app/vmselect/prometheus/expand-with-exprs.qtpl:41
}
//line app/vmselect/prometheus/expand-with-exprs.qtpl:40
//line app/vmselect/prometheus/expand-with-exprs.qtpl:41
func WriteExpandWithExprsResponse(qq422016 qtio422016.Writer, q string) {
//line app/vmselect/prometheus/expand-with-exprs.qtpl:40
//line app/vmselect/prometheus/expand-with-exprs.qtpl:41
qw422016 := qt422016.AcquireWriter(qq422016)
//line app/vmselect/prometheus/expand-with-exprs.qtpl:40
//line app/vmselect/prometheus/expand-with-exprs.qtpl:41
StreamExpandWithExprsResponse(qw422016, q)
//line app/vmselect/prometheus/expand-with-exprs.qtpl:40
//line app/vmselect/prometheus/expand-with-exprs.qtpl:41
qt422016.ReleaseWriter(qw422016)
//line app/vmselect/prometheus/expand-with-exprs.qtpl:40
//line app/vmselect/prometheus/expand-with-exprs.qtpl:41
}
//line app/vmselect/prometheus/expand-with-exprs.qtpl:40
//line app/vmselect/prometheus/expand-with-exprs.qtpl:41
func ExpandWithExprsResponse(q string) string {
//line app/vmselect/prometheus/expand-with-exprs.qtpl:40
//line app/vmselect/prometheus/expand-with-exprs.qtpl:41
qb422016 := qt422016.AcquireByteBuffer()
//line app/vmselect/prometheus/expand-with-exprs.qtpl:40
//line app/vmselect/prometheus/expand-with-exprs.qtpl:41
WriteExpandWithExprsResponse(qb422016, q)
//line app/vmselect/prometheus/expand-with-exprs.qtpl:40
//line app/vmselect/prometheus/expand-with-exprs.qtpl:41
qs422016 := string(qb422016.B)
//line app/vmselect/prometheus/expand-with-exprs.qtpl:40
//line app/vmselect/prometheus/expand-with-exprs.qtpl:41
qt422016.ReleaseByteBuffer(qb422016)
//line app/vmselect/prometheus/expand-with-exprs.qtpl:40
//line app/vmselect/prometheus/expand-with-exprs.qtpl:41
return qs422016
//line app/vmselect/prometheus/expand-with-exprs.qtpl:40
//line app/vmselect/prometheus/expand-with-exprs.qtpl:41
}
//line app/vmselect/prometheus/expand-with-exprs.qtpl:42
func streamexpandWithExprs(qw422016 *qt422016.Writer, q string) {
//line app/vmselect/prometheus/expand-with-exprs.qtpl:43
if len(q) == 0 {
func streamexpandWithExprs(qw422016 *qt422016.Writer, q string) {
//line app/vmselect/prometheus/expand-with-exprs.qtpl:44
return
if len(q) == 0 {
//line app/vmselect/prometheus/expand-with-exprs.qtpl:45
return
//line app/vmselect/prometheus/expand-with-exprs.qtpl:46
}
//line app/vmselect/prometheus/expand-with-exprs.qtpl:47
//line app/vmselect/prometheus/expand-with-exprs.qtpl:48
expr, err := metricsql.Parse(q)
//line app/vmselect/prometheus/expand-with-exprs.qtpl:48
if err != nil {
//line app/vmselect/prometheus/expand-with-exprs.qtpl:48
qw422016.N().S(`Cannot parse query:`)
//line app/vmselect/prometheus/expand-with-exprs.qtpl:49
qw422016.E().V(err)
if err != nil {
//line app/vmselect/prometheus/expand-with-exprs.qtpl:49
qw422016.N().S(`Cannot parse query:`)
//line app/vmselect/prometheus/expand-with-exprs.qtpl:50
} else {
qw422016.E().V(err)
//line app/vmselect/prometheus/expand-with-exprs.qtpl:51
} else {
//line app/vmselect/prometheus/expand-with-exprs.qtpl:52
expr = metricsql.Optimize(expr)
//line app/vmselect/prometheus/expand-with-exprs.qtpl:52
qw422016.E().Z(expr.AppendString(nil))
//line app/vmselect/prometheus/expand-with-exprs.qtpl:53
qw422016.E().Z(expr.AppendString(nil))
//line app/vmselect/prometheus/expand-with-exprs.qtpl:54
}
//line app/vmselect/prometheus/expand-with-exprs.qtpl:54
//line app/vmselect/prometheus/expand-with-exprs.qtpl:55
}
//line app/vmselect/prometheus/expand-with-exprs.qtpl:54
//line app/vmselect/prometheus/expand-with-exprs.qtpl:55
func writeexpandWithExprs(qq422016 qtio422016.Writer, q string) {
//line app/vmselect/prometheus/expand-with-exprs.qtpl:54
//line app/vmselect/prometheus/expand-with-exprs.qtpl:55
qw422016 := qt422016.AcquireWriter(qq422016)
//line app/vmselect/prometheus/expand-with-exprs.qtpl:54
//line app/vmselect/prometheus/expand-with-exprs.qtpl:55
streamexpandWithExprs(qw422016, q)
//line app/vmselect/prometheus/expand-with-exprs.qtpl:54
//line app/vmselect/prometheus/expand-with-exprs.qtpl:55
qt422016.ReleaseWriter(qw422016)
//line app/vmselect/prometheus/expand-with-exprs.qtpl:54
//line app/vmselect/prometheus/expand-with-exprs.qtpl:55
}
//line app/vmselect/prometheus/expand-with-exprs.qtpl:54
//line app/vmselect/prometheus/expand-with-exprs.qtpl:55
func expandWithExprs(q string) string {
//line app/vmselect/prometheus/expand-with-exprs.qtpl:54
//line app/vmselect/prometheus/expand-with-exprs.qtpl:55
qb422016 := qt422016.AcquireByteBuffer()
//line app/vmselect/prometheus/expand-with-exprs.qtpl:54
//line app/vmselect/prometheus/expand-with-exprs.qtpl:55
writeexpandWithExprs(qb422016, q)
//line app/vmselect/prometheus/expand-with-exprs.qtpl:54
//line app/vmselect/prometheus/expand-with-exprs.qtpl:55
qs422016 := string(qb422016.B)
//line app/vmselect/prometheus/expand-with-exprs.qtpl:54
//line app/vmselect/prometheus/expand-with-exprs.qtpl:55
qt422016.ReleaseByteBuffer(qb422016)
//line app/vmselect/prometheus/expand-with-exprs.qtpl:54
//line app/vmselect/prometheus/expand-with-exprs.qtpl:55
return qs422016
//line app/vmselect/prometheus/expand-with-exprs.qtpl:54
//line app/vmselect/prometheus/expand-with-exprs.qtpl:55
}
//line app/vmselect/prometheus/expand-with-exprs.qtpl:58
//line app/vmselect/prometheus/expand-with-exprs.qtpl:59
func streamwithExprsTutorial(qw422016 *qt422016.Writer) {
//line app/vmselect/prometheus/expand-with-exprs.qtpl:58
//line app/vmselect/prometheus/expand-with-exprs.qtpl:59
qw422016.N().S(`
<h3>Tutorial for WITH expressions in <a href="https://docs.victoriametrics.com/MetricsQL.html">MetricsQL</a></h3>
@ -315,31 +316,127 @@ WITH (
</pre>
`)
//line app/vmselect/prometheus/expand-with-exprs.qtpl:245
//line app/vmselect/prometheus/expand-with-exprs.qtpl:246
}
//line app/vmselect/prometheus/expand-with-exprs.qtpl:245
//line app/vmselect/prometheus/expand-with-exprs.qtpl:246
func writewithExprsTutorial(qq422016 qtio422016.Writer) {
//line app/vmselect/prometheus/expand-with-exprs.qtpl:245
//line app/vmselect/prometheus/expand-with-exprs.qtpl:246
qw422016 := qt422016.AcquireWriter(qq422016)
//line app/vmselect/prometheus/expand-with-exprs.qtpl:245
//line app/vmselect/prometheus/expand-with-exprs.qtpl:246
streamwithExprsTutorial(qw422016)
//line app/vmselect/prometheus/expand-with-exprs.qtpl:245
//line app/vmselect/prometheus/expand-with-exprs.qtpl:246
qt422016.ReleaseWriter(qw422016)
//line app/vmselect/prometheus/expand-with-exprs.qtpl:245
//line app/vmselect/prometheus/expand-with-exprs.qtpl:246
}
//line app/vmselect/prometheus/expand-with-exprs.qtpl:245
//line app/vmselect/prometheus/expand-with-exprs.qtpl:246
func withExprsTutorial() string {
//line app/vmselect/prometheus/expand-with-exprs.qtpl:245
//line app/vmselect/prometheus/expand-with-exprs.qtpl:246
qb422016 := qt422016.AcquireByteBuffer()
//line app/vmselect/prometheus/expand-with-exprs.qtpl:245
//line app/vmselect/prometheus/expand-with-exprs.qtpl:246
writewithExprsTutorial(qb422016)
//line app/vmselect/prometheus/expand-with-exprs.qtpl:245
//line app/vmselect/prometheus/expand-with-exprs.qtpl:246
qs422016 := string(qb422016.B)
//line app/vmselect/prometheus/expand-with-exprs.qtpl:245
//line app/vmselect/prometheus/expand-with-exprs.qtpl:246
qt422016.ReleaseByteBuffer(qb422016)
//line app/vmselect/prometheus/expand-with-exprs.qtpl:245
//line app/vmselect/prometheus/expand-with-exprs.qtpl:246
return qs422016
//line app/vmselect/prometheus/expand-with-exprs.qtpl:245
//line app/vmselect/prometheus/expand-with-exprs.qtpl:246
}
//line app/vmselect/prometheus/expand-with-exprs.qtpl:249
func StreamExpandWithExprsJSONResponse(qw422016 *qt422016.Writer, q string) {
//line app/vmselect/prometheus/expand-with-exprs.qtpl:250
streamexpandWithExprsJSON(qw422016, q)
//line app/vmselect/prometheus/expand-with-exprs.qtpl:251
}
//line app/vmselect/prometheus/expand-with-exprs.qtpl:251
func WriteExpandWithExprsJSONResponse(qq422016 qtio422016.Writer, q string) {
//line app/vmselect/prometheus/expand-with-exprs.qtpl:251
qw422016 := qt422016.AcquireWriter(qq422016)
//line app/vmselect/prometheus/expand-with-exprs.qtpl:251
StreamExpandWithExprsJSONResponse(qw422016, q)
//line app/vmselect/prometheus/expand-with-exprs.qtpl:251
qt422016.ReleaseWriter(qw422016)
//line app/vmselect/prometheus/expand-with-exprs.qtpl:251
}
//line app/vmselect/prometheus/expand-with-exprs.qtpl:251
func ExpandWithExprsJSONResponse(q string) string {
//line app/vmselect/prometheus/expand-with-exprs.qtpl:251
qb422016 := qt422016.AcquireByteBuffer()
//line app/vmselect/prometheus/expand-with-exprs.qtpl:251
WriteExpandWithExprsJSONResponse(qb422016, q)
//line app/vmselect/prometheus/expand-with-exprs.qtpl:251
qs422016 := string(qb422016.B)
//line app/vmselect/prometheus/expand-with-exprs.qtpl:251
qt422016.ReleaseByteBuffer(qb422016)
//line app/vmselect/prometheus/expand-with-exprs.qtpl:251
return qs422016
//line app/vmselect/prometheus/expand-with-exprs.qtpl:251
}
//line app/vmselect/prometheus/expand-with-exprs.qtpl:253
func streamexpandWithExprsJSON(qw422016 *qt422016.Writer, q string) {
//line app/vmselect/prometheus/expand-with-exprs.qtpl:254
if len(q) == 0 {
//line app/vmselect/prometheus/expand-with-exprs.qtpl:254
qw422016.N().S(`{"status": "error","error": "query string cannot be empty"}`)
//line app/vmselect/prometheus/expand-with-exprs.qtpl:259
return
//line app/vmselect/prometheus/expand-with-exprs.qtpl:260
}
//line app/vmselect/prometheus/expand-with-exprs.qtpl:260
qw422016.N().S(`{`)
//line app/vmselect/prometheus/expand-with-exprs.qtpl:263
expr, err := metricsql.Parse(q)
//line app/vmselect/prometheus/expand-with-exprs.qtpl:264
if err != nil {
//line app/vmselect/prometheus/expand-with-exprs.qtpl:264
qw422016.N().S(`"status": "error","error":`)
//line app/vmselect/prometheus/expand-with-exprs.qtpl:266
qw422016.N().Q(fmt.Sprintf("Cannot parse query: %s", err))
//line app/vmselect/prometheus/expand-with-exprs.qtpl:267
} else {
//line app/vmselect/prometheus/expand-with-exprs.qtpl:268
expr = metricsql.Optimize(expr)
//line app/vmselect/prometheus/expand-with-exprs.qtpl:268
qw422016.N().S(`"status": "success","expr":`)
//line app/vmselect/prometheus/expand-with-exprs.qtpl:270
qw422016.N().Q(string(expr.AppendString(nil)))
//line app/vmselect/prometheus/expand-with-exprs.qtpl:271
}
//line app/vmselect/prometheus/expand-with-exprs.qtpl:271
qw422016.N().S(`}`)
//line app/vmselect/prometheus/expand-with-exprs.qtpl:273
}
//line app/vmselect/prometheus/expand-with-exprs.qtpl:273
func writeexpandWithExprsJSON(qq422016 qtio422016.Writer, q string) {
//line app/vmselect/prometheus/expand-with-exprs.qtpl:273
qw422016 := qt422016.AcquireWriter(qq422016)
//line app/vmselect/prometheus/expand-with-exprs.qtpl:273
streamexpandWithExprsJSON(qw422016, q)
//line app/vmselect/prometheus/expand-with-exprs.qtpl:273
qt422016.ReleaseWriter(qw422016)
//line app/vmselect/prometheus/expand-with-exprs.qtpl:273
}
//line app/vmselect/prometheus/expand-with-exprs.qtpl:273
func expandWithExprsJSON(q string) string {
//line app/vmselect/prometheus/expand-with-exprs.qtpl:273
qb422016 := qt422016.AcquireByteBuffer()
//line app/vmselect/prometheus/expand-with-exprs.qtpl:273
writeexpandWithExprsJSON(qb422016, q)
//line app/vmselect/prometheus/expand-with-exprs.qtpl:273
qs422016 := string(qb422016.B)
//line app/vmselect/prometheus/expand-with-exprs.qtpl:273
qt422016.ReleaseByteBuffer(qb422016)
//line app/vmselect/prometheus/expand-with-exprs.qtpl:273
return qs422016
//line app/vmselect/prometheus/expand-with-exprs.qtpl:273
}

View File

@ -60,10 +60,17 @@ const defaultStep = 5 * 60 * 1000
// ExpandWithExprs handles the request to /expand-with-exprs
func ExpandWithExprs(w http.ResponseWriter, r *http.Request) {
query := r.FormValue("query")
format := r.FormValue("format")
bw := bufferedwriter.Get(w)
defer bufferedwriter.Put(bw)
if format == "json" {
w.Header().Set("Content-Type", "application/json")
WriteExpandWithExprsJSONResponse(bw, query)
} else {
WriteExpandWithExprsResponse(bw, query)
}
_ = bw.Flush()
}

View File

@ -11,6 +11,7 @@ import ThemeProvider from "./components/Main/ThemeProvider/ThemeProvider";
import TracePage from "./pages/TracePage";
import ExploreMetrics from "./pages/ExploreMetrics";
import PreviewIcons from "./components/Main/Icons/PreviewIcons";
import WithTemplate from "./pages/WithTemplate";
const App: FC = () => {
@ -51,6 +52,10 @@ const App: FC = () => {
path={router.dashboards}
element={<DashboardsLayout/>}
/>
<Route
path={router.withTemplate}
element={<WithTemplate/>}
/>
<Route
path={router.icons}
element={<PreviewIcons/>}

View File

@ -0,0 +1,2 @@
export const getExpandWithExprUrl = (server: string, query: string): string =>
`${server}/expand-with-exprs?query=${query}&format=json`;

View File

@ -44,10 +44,19 @@ const HeaderNav: FC<HeaderNavProps> = ({ color, background, direction }) => {
},
]
},
{
label: "Tools",
submenu: [
{
label: routerOptions[router.trace].title,
value: router.trace,
},
{
label: routerOptions[router.withTemplate].title,
value: router.withTemplate,
},
]
},
{
label: routerOptions[router.dashboards].title,
value: router.dashboards,

View File

@ -0,0 +1,45 @@
import React, { FC, useEffect } from "preact/compat";
import "./style.scss";
import { useState } from "react";
import Tooltip from "../Tooltip/Tooltip";
import Button from "../Button/Button";
import { CopyIcon } from "../Icons";
enum CopyState { copy = "Copy", copied = "Copied" }
const CodeExample: FC<{code: string}> = ({ code }) => {
const [tooltip, setTooltip] = useState(CopyState.copy);
const handlerCopy = () => {
navigator.clipboard.writeText(code);
setTooltip(CopyState.copied);
};
useEffect(() => {
let timeout: NodeJS.Timeout | null = null;
if (tooltip === CopyState.copied) {
timeout = setTimeout(() => setTooltip(CopyState.copy), 1000);
}
return () => {
timeout && clearTimeout(timeout);
};
}, [tooltip]);
return (
<code className="vm-code-example">
{code}
<div className="vm-code-example__copy">
<Tooltip title={tooltip}>
<Button
size="small"
variant="text"
onClick={handlerCopy}
startIcon={<CopyIcon/>}
/>
</Tooltip>
</div>
</code>
);
};
export default CodeExample;

View File

@ -0,0 +1,17 @@
@use "src/styles/variables" as *;
.vm-code-example {
position: relative;
display: block;
padding: $padding-global;
white-space: pre-wrap;
border-radius: $border-radius-small;
background-color: rgba($color-black, 0.05);
overflow: auto;
&__copy {
position: absolute;
right: 10px;
top: 10px;
}
}

View File

@ -21,7 +21,7 @@ import {
const spinnerMessage = `Please wait while cardinality stats is calculated.
This may take some time if the db contains big number of time series.`;
const Index: FC = () => {
const CardinalityPanel: FC = () => {
const { isMobile } = useDeviceDetect();
const [searchParams, setSearchParams] = useSearchParams();
@ -88,4 +88,4 @@ const Index: FC = () => {
);
};
export default Index;
export default CardinalityPanel;

View File

@ -19,9 +19,8 @@ import classNames from "classnames";
const exampleDuration = "30ms, 15s, 3d4h, 1y2w";
const Index: FC = () => {
const TopQueries: FC = () => {
const { isMobile } = useDeviceDetect();
const { data, error, loading } = useFetchTopQueries();
const { topN, maxLifetime } = useTopQueriesState();
const topQueriesDispatch = useTopQueriesDispatch();
@ -180,4 +179,4 @@ const Index: FC = () => {
);
};
export default Index;
export default TopQueries;

View File

@ -0,0 +1,218 @@
import React, { FC } from "preact/compat";
import "./style.scss";
import CodeExample from "../../../components/Main/CodeExample/CodeExample";
const MetricsQL = () => (
<a
className="vm-link vm-link_colored"
href="https://docs.victoriametrics.com/MetricsQL.html"
target="_blank"
rel="help noreferrer"
>
MetricsQL
</a>
);
const NodeExporterFull = () => (
<a
className="vm-link vm-link_colored"
href="https://grafana.com/grafana/dashboards/1860-node-exporter-full/"
target="_blank"
rel="help noreferrer"
>
Node Exporter Full
</a>
);
const WithTemplateTutorial: FC = () => (
<section className="vm-with-template-tutorial">
<h2 className="vm-with-template-tutorial__title">
Tutorial for WITH expressions in <MetricsQL/>
</h2>
<div className="vm-with-template-tutorial-section">
<p className="vm-with-template-tutorial-section__text">Let&apos;s look at the following real query from <NodeExporterFull/> dashboard:</p>
<CodeExample
code= {`(
(
node_memory_MemTotal_bytes{instance=~"$node:$port", job=~"$job"}
-
node_memory_MemFree_bytes{instance=~"$node:$port", job=~"$job"}
)
/
node_memory_MemTotal_bytes{instance=~"$node:$port", job=~"$job"}
) * 100`}
/>
<p className="vm-with-template-tutorial-section__text">
It is clear the query calculates the percentage of used memory for the given $node, $port and $job.
Isn&apos;t it? :)
</p>
</div>
<div className="vm-with-template-tutorial-section">
<p className="vm-with-template-tutorial-section__text">
What&apos;s wrong with this query?
Copy-pasted label filters for distinct timeseries which makes it easy
to mistype these filters during modification. Let&apos;s simplify the query with WITH expressions:
</p>
<CodeExample
code={`WITH (
commonFilters = {instance=~"$node:$port",job=~"$job"}
)
(
node_memory_MemTotal_bytes{commonFilters}
-
node_memory_MemFree_bytes{commonFilters}
)
/
node_memory_MemTotal_bytes{commonFilters} * 100`}
/>
</div>
<div className="vm-with-template-tutorial-section">
<p className="vm-with-template-tutorial-section__text">
Now label filters are located in a single place instead of three distinct places.
The query mentions node_memory_MemTotal_bytes metric twice and {"{commonFilters}"} three times.
WITH expressions may improve this:
</p>
<CodeExample
code={`WITH (
my_resource_utilization(free, limit, filters) = (limit{filters} - free{filters}) / limit{filters} * 100
)
my_resource_utilization(
node_memory_MemFree_bytes,
node_memory_MemTotal_bytes,
{instance=~"$node:$port",job=~"$job"},
)`}
/>
<p className="vm-with-template-tutorial-section__text">
Now the template function my_resource_utilization() may be used
for monitoring arbitrary resources - memory, CPU, network, storage, you name it.
</p>
</div>
<div className="vm-with-template-tutorial-section">
<p className="vm-with-template-tutorial-section__text">
Let&apos;s take another nice query from <NodeExporterFull/> dashboard:
</p>
<CodeExample
code={`(
(
(
count(
count(node_cpu_seconds_total{instance=~"$node:$port",job=~"$job"}) by (cpu)
)
)
-
avg(
sum by (mode) (rate(node_cpu_seconds_total{mode='idle',instance=~"$node:$port",job=~"$job"}[5m]))
)
)
*
100
)
/
count(
count(node_cpu_seconds_total{instance=~"$node:$port",job=~"$job"}) by (cpu)
)`}
/>
<p className="vm-with-template-tutorial-section__text">
Do you understand what does this mess do? Is it manageable? :)
WITH expressions are happy to help in a few iterations.
</p>
</div>
<div className="vm-with-template-tutorial-section">
<p className="vm-with-template-tutorial-section__text">
1. Extract common filters used in multiple places into a commonFilters variable:
</p>
<CodeExample
code={`WITH (
commonFilters = {instance=~"$node:$port",job=~"$job"}
)
(
(
(
count(
count(node_cpu_seconds_total{commonFilters}) by (cpu)
)
)
-
avg(
sum by (mode) (rate(node_cpu_seconds_total{mode='idle',commonFilters}[5m]))
)
)
*
100
)
/
count(
count(node_cpu_seconds_total{commonFilters}) by (cpu)
)`}
/>
</div>
<div className="vm-with-template-tutorial-section">
<p className="vm-with-template-tutorial-section__text">
2. Extract &#34;count(count(...) by (cpu))&#34; into cpuCount variable:
</p>
<CodeExample
code={`WITH (
commonFilters = {instance=~"$node:$port",job=~"$job"},
cpuCount = count(count(node_cpu_seconds_total{commonFilters}) by (cpu))
)
(
(
cpuCount
-
avg(
sum by (mode) (rate(node_cpu_seconds_total{mode='idle',commonFilters}[5m]))
)
)
*
100
) / cpuCount`}
/>
</div>
<div className="vm-with-template-tutorial-section">
<p className="vm-with-template-tutorial-section__text">
3. Extract rate(...) part into cpuIdle variable,
since it is clear now that this part calculates the number of idle CPUs:
</p>
<CodeExample
code={`WITH (
commonFilters = {instance=~"$node:$port",job=~"$job"},
cpuCount = count(count(node_cpu_seconds_total{commonFilters}) by (cpu)),
cpuIdle = sum(rate(node_cpu_seconds_total{mode='idle',commonFilters}[5m]))
)
((cpuCount - cpuIdle) * 100) / cpuCount`}
/>
</div>
<div className="vm-with-template-tutorial-section">
<p className="vm-with-template-tutorial-section__text">
4. Put node_cpu_seconds_total{"{commonFilters}"} into its own varialbe with the name cpuSeconds:
</p>
<CodeExample
code={`WITH (
cpuSeconds = node_cpu_seconds_total{instance=~"$node:$port",job=~"$job"},
cpuCount = count(count(cpuSeconds) by (cpu)),
cpuIdle = sum(rate(cpuSeconds{mode='idle'}[5m]))
)
((cpuCount - cpuIdle) * 100) / cpuCount`}
/>
<p className="vm-with-template-tutorial-section__text">
Now the query became more clear comparing to the initial query.
</p>
</div>
<div className="vm-with-template-tutorial-section">
<p className="vm-with-template-tutorial-section__text">
WITH expressions may be nested and may be put anywhere. Try expanding the following query:
</p>
<CodeExample
code= {`WITH (
f(a, b) = WITH (
f1(x) = b-x,
f2(x) = x+x
) f1(a)*f2(b)
) f(foo, with(x=bar) x)`}
/>
</div>
</section>
);
export default WithTemplateTutorial;

View File

@ -0,0 +1,22 @@
@use "src/styles/variables" as *;
.vm-with-template-tutorial {
display: grid;
gap: $padding-large;
&__title {
font-size: $font-size-large;
font-weight: bold;
}
&-section {
display: grid;
gap: $padding-global;
&__text {
font-size: $font-size-medium;
line-height: 130%;
max-width: 720px;
}
}
}

View File

@ -0,0 +1,37 @@
import { useAppState } from "../../../state/common/StateContext";
import { useState } from "react";
import { ErrorTypes } from "../../../types";
import { getExpandWithExprUrl } from "../../../api/expand-with-exprs";
export const useExpandWithExprs = () => {
const { serverUrl } = useAppState();
const [data, setData] = useState("");
const [loading, setLoading] = useState(false);
const [error, setError] = useState<ErrorTypes | string>();
const fetchData = async (query: string) => {
const fetchUrl = getExpandWithExprUrl(serverUrl, query);
setLoading(true);
try {
const response = await fetch(fetchUrl);
const resp = await response.json();
setData(resp?.expr || "");
setError(String(resp.error || ""));
} catch (e) {
if (e instanceof Error && e.name !== "AbortError") {
setError(`${e.name}: ${e.message}`);
}
}
setLoading(false);
};
return {
data,
error,
loading,
expand: fetchData
};
};

View File

@ -0,0 +1,63 @@
import React, { FC } from "preact/compat";
import "./style.scss";
import TextField from "../../components/Main/TextField/TextField";
import { useState } from "react";
import Button from "../../components/Main/Button/Button";
import { PlayIcon } from "../../components/Main/Icons";
import WithTemplateTutorial from "./WithTemplateTutorial/WithTemplateTutorial";
import { useExpandWithExprs } from "./hooks/useExpandWithExprs";
import Spinner from "../../components/Main/Spinner/Spinner";
const WithTemplate: FC = () => {
const { data, loading, error, expand } = useExpandWithExprs();
const [expr, setExpr] = useState("");
const handleChangeInput = (val: string) => {
setExpr(val);
};
const handleRunQuery = () => {
expand(expr);
};
return (
<section className="vm-with-template">
{loading && <Spinner />}
<div className="vm-with-template-body vm-block">
<div className="vm-with-template-body__expr">
<TextField
type="textarea"
label="MetricsQL query with optional WITH expressions"
value={expr}
error={error}
autofocus
onChange={handleChangeInput}
/>
</div>
<div className="vm-with-template-body__result">
<TextField
type="textarea"
label="MetricsQL query after expanding WITH expressions and applying other optimizations"
value={data}
disabled
/>
</div>
<div className="vm-with-template-body-top">
<Button
variant="contained"
onClick={handleRunQuery}
startIcon={<PlayIcon/>}
>
Expand
</Button>
</div>
</div>
<div className="vm-block">
<WithTemplateTutorial/>
</div>
</section>
);
};
export default WithTemplate;

View File

@ -0,0 +1,34 @@
@use "src/styles/variables" as *;
.vm-with-template {
display: grid;
gap: $padding-medium;
&-body {
display: grid;
gap: $padding-global;
align-items: flex-start;
width: 100%;
&-top {
display: flex;
gap: $padding-small;
align-items: center;
justify-content: flex-end;
}
&__expr textarea {
min-height: 200px;
}
&__result textarea {
min-height: 60px;
}
textarea {
overflow: auto;
width: 100%;
height: 100%;
}
}
}

View File

@ -5,7 +5,8 @@ const router = {
cardinality: "/cardinality",
topQueries: "/top-queries",
trace: "/trace",
icons: "/icons"
withTemplate: "/expand-with-exprs",
icons: "/icons" // for dev
};
export interface RouterOptionsHeader {
@ -65,6 +66,10 @@ export const routerOptions: {[key: string]: RouterOptions} = {
title: "Dashboards",
...routerOptionsDefault,
},
[router.withTemplate]: {
title: "WITH templates",
header: {}
},
[router.icons]: {
title: "Icons",
header: {}

View File

@ -160,6 +160,7 @@ Released at 2023-02-24
* FEATURE: [vmui](https://docs.victoriametrics.com/#vmui): show `median` instead of `avg` in graph tooltip and line legend, since `median` is more tolerant against spikes. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/3706).
* FEATURE: add `-search.maxSeriesPerAggrFunc` command-line flag, which can be used for limiting the number of time series [MetricsQL aggregate functions](https://docs.victoriametrics.com/MetricsQL.html#aggregate-functions) can return in a single query. This flag can be useful for preventing OOMs when [count_values](https://docs.victoriametrics.com/MetricsQL.html#count_values) function is improperly used.
* FEATURE: [vmui](https://docs.victoriametrics.com/#vmui): small UX improvements for mobile view. See [this feature request](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/3707) and [this pull request](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/3848).
* FEATURE: [vmui](https://docs.victoriametrics.com/#vmui): integrate WITH template playground. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/3811).
* FEATURE: add `-search.logQueryMemoryUsage` command-line flag for logging queries, which need more memory than specified by this command-line flag. See [this feature request](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/3553). Thanks to @michal-kralik for the idea and the intial implementation.
* FEATURE: allow setting zero value for `-search.latencyOffset` command-line flag. This may be needed in [some cases](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/2061#issuecomment-1299109836). Previously the minimum supported value for `-search.latencyOffset` command-line flag was `1s`.