vmui: add Active Queries page (#4653)

* feat: add page to display a list of active queries (#4598)

* app/vmagent: code formatting

* fix: remove console

---------

Co-authored-by: dmitryk-dk <kozlovdmitriyy@gmail.com>
This commit is contained in:
Yury Molodov 2023-07-20 00:47:21 +02:00 committed by Aliaksandr Valialkin
parent 9d55da5d26
commit 3ad80e281f
No known key found for this signature in database
GPG key ID: A72BEC6CD3D0DED1
12 changed files with 237 additions and 11 deletions

View file

@ -266,6 +266,12 @@ func requestHandler(w http.ResponseWriter, r *http.Request) bool {
}
return true
}
if path == "/api/v1/status/active_queries" {
globalStatusActiveQueriesRequests.Inc()
httpserver.EnableCORS(w, r)
promql.WriteActiveQueries(w, r)
return true
}
if path == "/admin/tenants" {
tenantsRequests.Inc()
httpserver.EnableCORS(w, r)
@ -500,7 +506,8 @@ func selectHandler(qt *querytracer.Tracer, startTime time.Time, w http.ResponseW
return true
case "prometheus/api/v1/status/active_queries":
statusActiveQueriesRequests.Inc()
promql.WriteActiveQueries(w)
httpserver.EnableCORS(w, r)
promql.WriteActiveQueriesForTenant(at, w, r)
return true
case "prometheus/api/v1/status/top_queries":
topQueriesRequests.Inc()
@ -767,6 +774,7 @@ var (
statusTSDBRequests = metrics.NewCounter(`vm_http_requests_total{path="/select/{}/prometheus/api/v1/status/tsdb"}`)
statusTSDBErrors = metrics.NewCounter(`vm_http_request_errors_total{path="/select/{}/prometheus/api/v1/status/tsdb"}`)
globalStatusActiveQueriesRequests = metrics.NewCounter(`vm_http_requests_total{path="/api/v1/status/active_queries"}`)
statusActiveQueriesRequests = metrics.NewCounter(`vm_http_requests_total{path="/select/{}prometheus/api/v1/status/active_queries"}`)
topQueriesRequests = metrics.NewCounter(`vm_http_requests_total{path="/select/{}/prometheus/api/v1/status/top_queries"}`)

View file

@ -2,27 +2,50 @@ package promql
import (
"fmt"
"io"
"net/http"
"sort"
"sync"
"sync/atomic"
"time"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/auth"
)
// WriteActiveQueries writes active queries to w.
//
// The written active queries are sorted in descending order of their exeuction duration.
func WriteActiveQueries(w io.Writer) {
// WriteActiveQueriesForTenant writes active queries for the given (accountID, projectID) to w.
func WriteActiveQueriesForTenant(at *auth.Token, w http.ResponseWriter, r *http.Request) {
aqes := activeQueriesV.GetAll()
var dst []activeQueryEntry
for _, aqe := range aqes {
if aqe.accountID == at.AccountID && aqe.projectID == at.ProjectID {
dst = append(dst, aqe)
}
}
writeActiveQueries(w, dst)
}
// WriteActiveQueries writes active queries to w.
func WriteActiveQueries(w http.ResponseWriter, r *http.Request) {
aqes := activeQueriesV.GetAll()
writeActiveQueries(w, aqes)
}
func writeActiveQueries(w http.ResponseWriter, aqes []activeQueryEntry) {
w.Header().Set("Content-Type", "application/json")
sort.Slice(aqes, func(i, j int) bool {
return aqes[i].startTime.Sub(aqes[j].startTime) < 0
})
now := time.Now()
for _, aqe := range aqes {
fmt.Fprintf(w, `{"status":"ok","data":[`)
for i, aqe := range aqes {
d := now.Sub(aqe.startTime)
fmt.Fprintf(w, "\tduration: %.3fs, id=%016X, remote_addr=%s, accountID=%d, projectID=%d, query=%q, start=%d, end=%d, step=%d\n",
fmt.Fprintf(w, `{"duration":"%.3fs","id":"%016X","remote_addr":%s,"account_id":"%d","project_id":"%d","query":%q,"start":%d,"end":%d,"step":%d}`,
d.Seconds(), aqe.qid, aqe.quotedRemoteAddr, aqe.accountID, aqe.projectID, aqe.q, aqe.start, aqe.end, aqe.step)
if i+1 < len(aqes) {
fmt.Fprintf(w, `,`)
}
}
fmt.Fprintf(w, `]}`)
}
var activeQueriesV = newActiveQueries()

View file

@ -14,6 +14,7 @@ import PreviewIcons from "./components/Main/Icons/PreviewIcons";
import WithTemplate from "./pages/WithTemplate";
import Relabel from "./pages/Relabel";
import ExploreLogs from "./pages/ExploreLogs/ExploreLogs";
import ActiveQueries from "./pages/ActiveQueries";
const App: FC = () => {
const { REACT_APP_LOGS } = process.env;
@ -65,6 +66,10 @@ const App: FC = () => {
path={router.relabel}
element={<Relabel/>}
/>
<Route
path={router.activeQueries}
element={<ActiveQueries/>}
/>
<Route
path={router.icons}
element={<PreviewIcons/>}

View file

@ -0,0 +1,2 @@
export const getActiveQueries = (server: string): string =>
`${server}/api/v1/status/active_queries`;

View file

@ -34,6 +34,10 @@ export const defaultNavigation: NavigationItem[] = [
label: routerOptions[router.topQueries].title,
value: router.topQueries,
},
{
label: routerOptions[router.activeQueries].title,
value: router.activeQueries,
},
{
label: routerOptions[router.logs].title,
value: router.logs,

View file

@ -0,0 +1,51 @@
import { useEffect, useMemo, useState } from "preact/compat";
import { getActiveQueries } from "../../../api/active-queries";
import { useAppState } from "../../../state/common/StateContext";
import { ActiveQueriesType, ErrorTypes } from "../../../types";
import dayjs from "dayjs";
import { DATE_FULL_TIMEZONE_FORMAT } from "../../../constants/date";
interface FetchActiveQueries {
data: ActiveQueriesType[];
isLoading: boolean;
lastUpdated: string;
error?: ErrorTypes | string;
fetchData: () => Promise<void>;
}
export const useFetchActiveQueries = (): FetchActiveQueries => {
const { serverUrl } = useAppState();
const [activeQueries, setActiveQueries] = useState<ActiveQueriesType[]>([]);
const [lastUpdated, setLastUpdated] = useState<string>(dayjs().format(DATE_FULL_TIMEZONE_FORMAT));
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<ErrorTypes | string>();
const fetchUrl = useMemo(() => getActiveQueries(serverUrl), [serverUrl]);
const fetchData = async () => {
setIsLoading(true);
try {
const response = await fetch(fetchUrl);
const resp = await response.json();
setActiveQueries(resp.data);
setLastUpdated(dayjs().format("HH:mm:ss:SSS"));
if (response.ok) {
setError(undefined);
} else {
setError(`${resp.errorType}\r\n${resp?.error}`);
}
} catch (e) {
if (e instanceof Error) {
setError(`${e.name}: ${e.message}`);
}
}
setIsLoading(false);
};
useEffect(() => {
fetchData().catch(console.error);
}, [fetchUrl]);
return { data: activeQueries, lastUpdated, isLoading, error, fetchData };
};

View file

@ -0,0 +1,89 @@
import React, { FC, useMemo } from "preact/compat";
import { useFetchActiveQueries } from "./hooks/useFetchActiveQueries";
import Alert from "../../components/Main/Alert/Alert";
import Spinner from "../../components/Main/Spinner/Spinner";
import Table from "../../components/Table/Table";
import { ActiveQueriesType } from "../../types";
import dayjs from "dayjs";
import { useTimeState } from "../../state/time/TimeStateContext";
import useDeviceDetect from "../../hooks/useDeviceDetect";
import classNames from "classnames";
import Button from "../../components/Main/Button/Button";
import { RefreshIcon } from "../../components/Main/Icons";
import "./style.scss";
const ActiveQueries: FC = () => {
const { isMobile } = useDeviceDetect();
const { timezone } = useTimeState();
const { data, lastUpdated, isLoading, error, fetchData } = useFetchActiveQueries();
const activeQueries = useMemo(() => data.map(({ duration, ...item }: ActiveQueriesType) => ({
duration,
data: JSON.stringify(item, null, 2),
from: dayjs(item.start).tz().format("MMM DD, YYYY \nHH:mm:ss.SSS"),
to: dayjs(item.end).tz().format("MMM DD, YYYY \nHH:mm:ss.SSS"),
...item,
})), [data, timezone]);
const columns = useMemo(() => {
if (!activeQueries?.length) return [];
const hideColumns = ["end", "start", "data"];
const keys = new Set<string>();
for (const item of activeQueries) {
for (const key in item) {
keys.add(key);
}
}
return Array.from(keys)
.filter((col) => !hideColumns.includes(col))
.map((key) => ({
key: key as keyof ActiveQueriesType,
title: key,
}));
}, [activeQueries]);
const handleRefresh = async () => {
fetchData().catch(console.error);
};
return (
<div className="vm-active-queries">
{isLoading && <Spinner />}
<div className="vm-active-queries-header">
{!activeQueries.length && !error && <Alert variant="info">There are currently no active queries running</Alert>}
{error && <Alert variant="error">{error}</Alert>}
<div className="vm-active-queries-header-controls">
<Button
variant="contained"
onClick={handleRefresh}
startIcon={<RefreshIcon/>}
>
Update
</Button>
<div className="vm-active-queries-header__update-msg">
Last updated: {lastUpdated}
</div>
</div>
</div>
{!!activeQueries.length && (
<div
className={classNames({
"vm-block": true,
"vm-block_mobile": isMobile,
})}
>
<Table
rows={activeQueries}
columns={columns}
defaultOrderBy={"from"}
copyToClipboard={"data"}
paginationOffset={{ startIndex: 0, endIndex: Infinity }}
/>
</div>
)}
</div>
);
};
export default ActiveQueries;

View file

@ -0,0 +1,25 @@
@use "src/styles/variables" as *;
.vm-active-queries {
&-header {
display: grid;
grid-template-columns: 1fr auto;
align-items: center;
justify-content: space-between;
gap: $padding-global;
margin-bottom: $padding-global;
&-controls {
grid-column: 2;
display: grid;
gap: $padding-small;
}
&__update-msg {
white-space: nowrap;
color: $color-text-secondary;
font-size: $font-size-small;
}
}
}

View file

@ -8,6 +8,7 @@ const router = {
withTemplate: "/expand-with-exprs",
relabel: "/relabeling",
logs: "/logs",
activeQueries: "/active-queries",
icons: "/icons"
};
@ -82,6 +83,10 @@ export const routerOptions: {[key: string]: RouterOptions} = {
title: "Logs Explorer",
header: {}
},
[router.activeQueries]: {
title: "Active Queries",
header: {}
},
[router.icons]: {
title: "Icons",
header: {}

View file

@ -138,3 +138,16 @@ export interface RelabelData {
resultingLabels?: string;
steps: RelabelStep[];
}
export interface ActiveQueriesType {
duration: string;
end: number;
start: number;
id: string;
query: string;
remote_addr: string;
step: number;
from?: string;
to?: string;
data?: string;
}

View file

@ -54,6 +54,7 @@ The following `tip` changes can be tested by building VictoriaMetrics components
`-search.maxGraphiteTagValues` for limiting the number of tag values returned from [Graphite API for tag values](https://docs.victoriametrics.com/#graphite-tags-api-usage)
`-search.maxGraphiteSeries` for limiting the number of series (aka paths) returned from [Graphite API for series](https://docs.victoriametrics.com/#graphite-tags-api-usage)
See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/4339).
* FEATURE: [vmui](https://docs.victoriametrics.com/#vmui): added a new page to display a list of currently running queries. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/4598).
* BUGFIX: properly return series from [/api/v1/series](https://docs.victoriametrics.com/Single-server-VictoriaMetrics.html#prometheus-querying-api-usage) if it finds more than the `limit` series (`limit` is an optional query arg passed to this API). Previously the `limit exceeded error` error was returned in this case. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/2841#issuecomment-1560055631).
* BUGFIX: [vmui](https://docs.victoriametrics.com/#vmui): fix application routing issues and problems with manual URL changes. See [this pull request](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/4408).