mirror of
https://github.com/VictoriaMetrics/VictoriaMetrics.git
synced 2025-02-09 15:27:11 +00:00
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:
parent
9d55da5d26
commit
3ad80e281f
12 changed files with 237 additions and 11 deletions
|
@ -266,6 +266,12 @@ func requestHandler(w http.ResponseWriter, r *http.Request) bool {
|
||||||
}
|
}
|
||||||
return true
|
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" {
|
if path == "/admin/tenants" {
|
||||||
tenantsRequests.Inc()
|
tenantsRequests.Inc()
|
||||||
httpserver.EnableCORS(w, r)
|
httpserver.EnableCORS(w, r)
|
||||||
|
@ -500,7 +506,8 @@ func selectHandler(qt *querytracer.Tracer, startTime time.Time, w http.ResponseW
|
||||||
return true
|
return true
|
||||||
case "prometheus/api/v1/status/active_queries":
|
case "prometheus/api/v1/status/active_queries":
|
||||||
statusActiveQueriesRequests.Inc()
|
statusActiveQueriesRequests.Inc()
|
||||||
promql.WriteActiveQueries(w)
|
httpserver.EnableCORS(w, r)
|
||||||
|
promql.WriteActiveQueriesForTenant(at, w, r)
|
||||||
return true
|
return true
|
||||||
case "prometheus/api/v1/status/top_queries":
|
case "prometheus/api/v1/status/top_queries":
|
||||||
topQueriesRequests.Inc()
|
topQueriesRequests.Inc()
|
||||||
|
@ -767,6 +774,7 @@ var (
|
||||||
statusTSDBRequests = metrics.NewCounter(`vm_http_requests_total{path="/select/{}/prometheus/api/v1/status/tsdb"}`)
|
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"}`)
|
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"}`)
|
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"}`)
|
topQueriesRequests = metrics.NewCounter(`vm_http_requests_total{path="/select/{}/prometheus/api/v1/status/top_queries"}`)
|
||||||
|
|
|
@ -2,27 +2,50 @@ package promql
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"net/http"
|
||||||
"sort"
|
"sort"
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/VictoriaMetrics/VictoriaMetrics/lib/auth"
|
||||||
)
|
)
|
||||||
|
|
||||||
// WriteActiveQueries writes active queries to w.
|
// WriteActiveQueriesForTenant writes active queries for the given (accountID, projectID) to w.
|
||||||
//
|
func WriteActiveQueriesForTenant(at *auth.Token, w http.ResponseWriter, r *http.Request) {
|
||||||
// The written active queries are sorted in descending order of their exeuction duration.
|
|
||||||
func WriteActiveQueries(w io.Writer) {
|
|
||||||
aqes := activeQueriesV.GetAll()
|
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 {
|
sort.Slice(aqes, func(i, j int) bool {
|
||||||
return aqes[i].startTime.Sub(aqes[j].startTime) < 0
|
return aqes[i].startTime.Sub(aqes[j].startTime) < 0
|
||||||
})
|
})
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
for _, aqe := range aqes {
|
fmt.Fprintf(w, `{"status":"ok","data":[`)
|
||||||
|
for i, aqe := range aqes {
|
||||||
d := now.Sub(aqe.startTime)
|
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)
|
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()
|
var activeQueriesV = newActiveQueries()
|
||||||
|
|
|
@ -14,6 +14,7 @@ import PreviewIcons from "./components/Main/Icons/PreviewIcons";
|
||||||
import WithTemplate from "./pages/WithTemplate";
|
import WithTemplate from "./pages/WithTemplate";
|
||||||
import Relabel from "./pages/Relabel";
|
import Relabel from "./pages/Relabel";
|
||||||
import ExploreLogs from "./pages/ExploreLogs/ExploreLogs";
|
import ExploreLogs from "./pages/ExploreLogs/ExploreLogs";
|
||||||
|
import ActiveQueries from "./pages/ActiveQueries";
|
||||||
|
|
||||||
const App: FC = () => {
|
const App: FC = () => {
|
||||||
const { REACT_APP_LOGS } = process.env;
|
const { REACT_APP_LOGS } = process.env;
|
||||||
|
@ -65,6 +66,10 @@ const App: FC = () => {
|
||||||
path={router.relabel}
|
path={router.relabel}
|
||||||
element={<Relabel/>}
|
element={<Relabel/>}
|
||||||
/>
|
/>
|
||||||
|
<Route
|
||||||
|
path={router.activeQueries}
|
||||||
|
element={<ActiveQueries/>}
|
||||||
|
/>
|
||||||
<Route
|
<Route
|
||||||
path={router.icons}
|
path={router.icons}
|
||||||
element={<PreviewIcons/>}
|
element={<PreviewIcons/>}
|
||||||
|
|
2
app/vmui/packages/vmui/src/api/active-queries.ts
Normal file
2
app/vmui/packages/vmui/src/api/active-queries.ts
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
export const getActiveQueries = (server: string): string =>
|
||||||
|
`${server}/api/v1/status/active_queries`;
|
|
@ -34,6 +34,10 @@ export const defaultNavigation: NavigationItem[] = [
|
||||||
label: routerOptions[router.topQueries].title,
|
label: routerOptions[router.topQueries].title,
|
||||||
value: router.topQueries,
|
value: router.topQueries,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: routerOptions[router.activeQueries].title,
|
||||||
|
value: router.activeQueries,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: routerOptions[router.logs].title,
|
label: routerOptions[router.logs].title,
|
||||||
value: router.logs,
|
value: router.logs,
|
||||||
|
|
|
@ -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 };
|
||||||
|
};
|
89
app/vmui/packages/vmui/src/pages/ActiveQueries/index.tsx
Normal file
89
app/vmui/packages/vmui/src/pages/ActiveQueries/index.tsx
Normal 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;
|
25
app/vmui/packages/vmui/src/pages/ActiveQueries/style.scss
Normal file
25
app/vmui/packages/vmui/src/pages/ActiveQueries/style.scss
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -8,6 +8,7 @@ const router = {
|
||||||
withTemplate: "/expand-with-exprs",
|
withTemplate: "/expand-with-exprs",
|
||||||
relabel: "/relabeling",
|
relabel: "/relabeling",
|
||||||
logs: "/logs",
|
logs: "/logs",
|
||||||
|
activeQueries: "/active-queries",
|
||||||
icons: "/icons"
|
icons: "/icons"
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -82,6 +83,10 @@ export const routerOptions: {[key: string]: RouterOptions} = {
|
||||||
title: "Logs Explorer",
|
title: "Logs Explorer",
|
||||||
header: {}
|
header: {}
|
||||||
},
|
},
|
||||||
|
[router.activeQueries]: {
|
||||||
|
title: "Active Queries",
|
||||||
|
header: {}
|
||||||
|
},
|
||||||
[router.icons]: {
|
[router.icons]: {
|
||||||
title: "Icons",
|
title: "Icons",
|
||||||
header: {}
|
header: {}
|
||||||
|
|
|
@ -138,3 +138,16 @@ export interface RelabelData {
|
||||||
resultingLabels?: string;
|
resultingLabels?: string;
|
||||||
steps: RelabelStep[];
|
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;
|
||||||
|
}
|
||||||
|
|
|
@ -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.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)
|
`-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).
|
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: 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).
|
* 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).
|
||||||
|
|
Loading…
Reference in a new issue