mirror of
https://github.com/VictoriaMetrics/VictoriaMetrics.git
synced 2026-06-06 02:21:58 +03:00
Compare commits
5 Commits
dependabot
...
alert-to-t
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
72005d7e62 | ||
|
|
8d68214f88 | ||
|
|
a901523ab4 | ||
|
|
59032c839f | ||
|
|
5673cd3ac8 |
33
app/vmui/packages/vmui/package-lock.json
generated
33
app/vmui/packages/vmui/package-lock.json
generated
@@ -146,6 +146,7 @@
|
||||
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.29.0",
|
||||
"@babel/generator": "^7.29.0",
|
||||
@@ -548,6 +549,7 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=20.19.0"
|
||||
},
|
||||
@@ -596,31 +598,11 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=20.19.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/core": {
|
||||
"version": "1.9.1",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.1.tgz",
|
||||
"integrity": "sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@emnapi/wasi-threads": "1.2.0",
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/runtime": {
|
||||
"version": "1.9.1",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.1.tgz",
|
||||
"integrity": "sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/wasi-threads": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.0.tgz",
|
||||
@@ -2421,6 +2403,7 @@
|
||||
"integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
@@ -2763,6 +2746,7 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.9.0",
|
||||
"caniuse-lite": "^1.0.30001759",
|
||||
@@ -3592,6 +3576,7 @@
|
||||
"integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.8.0",
|
||||
"@eslint-community/regexpp": "^4.12.1",
|
||||
@@ -5821,6 +5806,7 @@
|
||||
"resolved": "https://registry.npmjs.org/preact/-/preact-10.29.0.tgz",
|
||||
"integrity": "sha512-wSAGyk2bYR1c7t3SZ3jHcM6xy0lcBcDel6lODcs9ME6Th++Dx2KU+6D3HD8wMMKGA8Wpw7OMd3/4RGzYRpzwRg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/preact"
|
||||
@@ -6595,8 +6581,7 @@
|
||||
"version": "0.27.0",
|
||||
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
|
||||
"integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/semver": {
|
||||
"version": "6.3.1",
|
||||
@@ -7231,6 +7216,7 @@
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
@@ -7334,6 +7320,7 @@
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.7.tgz",
|
||||
"integrity": "sha512-P1PbweD+2/udplnThz3btF4cf6AgPky7kk23RtHUkJIU5BIxwPprhRGmOAHs6FTI7UiGbTNrgNP6jSYD6JaRnw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"lightningcss": "^1.32.0",
|
||||
"picomatch": "^4.0.4",
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
AlertingRuleIcon,
|
||||
RecordingRuleIcon,
|
||||
DetailsIcon,
|
||||
ChartIcon,
|
||||
} from "../../Main/Icons";
|
||||
import Button from "../../Main/Button/Button";
|
||||
|
||||
@@ -26,9 +27,10 @@ interface ItemHeaderControlsProps {
|
||||
id?: string;
|
||||
name: string;
|
||||
onClose?: () => void;
|
||||
topQueriesUrl?: string;
|
||||
}
|
||||
|
||||
const ItemHeader: FC<ItemHeaderControlsProps> = ({ name, id, groupId, entity, type, states, onClose, classes }) => {
|
||||
const ItemHeader: FC<ItemHeaderControlsProps> = ({ name, id, groupId, entity, type, states, onClose, classes, topQueriesUrl }) => {
|
||||
const { isMobile } = useDeviceDetect();
|
||||
const { serverUrl } = useAppState();
|
||||
const navigate = useNavigate();
|
||||
@@ -108,6 +110,19 @@ const ItemHeader: FC<ItemHeaderControlsProps> = ({ name, id, groupId, entity, ty
|
||||
<div className="vm-explore-alerts-item-header__name">{name}</div>
|
||||
</div>
|
||||
<div className="vm-explore-alerts-controls">
|
||||
{topQueriesUrl && (
|
||||
<Tooltip title="Top query">
|
||||
<a
|
||||
href={`#${topQueriesUrl}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="vm-explore-alerts-item-header__top-queries-link"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<ChartIcon />
|
||||
</a>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Badges
|
||||
align="end"
|
||||
items={badgesItems}
|
||||
|
||||
@@ -76,4 +76,22 @@
|
||||
.vm-explore-alerts-controls {
|
||||
display: flex;
|
||||
column-gap: $padding-global;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.vm-explore-alerts-item-header__top-queries-link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: $color-text-secondary;
|
||||
text-decoration: none;
|
||||
|
||||
svg {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
fill: currentColor;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: $color-primary;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,14 +4,24 @@ import Accordion from "../../Main/Accordion/Accordion";
|
||||
import "./style.scss";
|
||||
import { Rule as APIRule } from "../../../types";
|
||||
import BaseRule from "../BaseRule";
|
||||
import Tooltip from "../../Main/Tooltip/Tooltip";
|
||||
import { ChartIcon } from "../../Main/Icons";
|
||||
import router from "../../../router";
|
||||
|
||||
interface RuleProps {
|
||||
states: Record<string, number>;
|
||||
rule: APIRule;
|
||||
topQueriesSet?: Set<string>;
|
||||
}
|
||||
|
||||
const Rule: FC<RuleProps> = ({ states, rule }) => {
|
||||
const normalizeQuery = (q: string): string => q.replace(/\s+/g, " ").trim();
|
||||
|
||||
const Rule: FC<RuleProps> = ({ states, rule, topQueriesSet }) => {
|
||||
const state = Object.keys(states).length > 0 ? Object.keys(states)[0] : "ok";
|
||||
const isInTopQueries = topQueriesSet ? topQueriesSet.has(normalizeQuery(rule.query)) : false;
|
||||
|
||||
const topQueriesUrl = `${router.topQueries}?topN=100&maxLifetime=10m&query=${encodeURIComponent(rule.query)}`;
|
||||
|
||||
return (
|
||||
<div className={`vm-explore-alerts-rule vm-badge-item ${state.replace(" ", "-")}`}>
|
||||
<Accordion
|
||||
@@ -23,6 +33,7 @@ const Rule: FC<RuleProps> = ({ states, rule }) => {
|
||||
states={states}
|
||||
id={rule.id}
|
||||
name={rule.name}
|
||||
topQueriesUrl={isInTopQueries ? topQueriesUrl : undefined}
|
||||
/>}
|
||||
>
|
||||
<BaseRule item={rule} />
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import Spinner from "../../components/Main/Spinner/Spinner";
|
||||
import Alert from "../../components/Main/Alert/Alert";
|
||||
import { useFetchItem } from "./hooks/useFetchItem";
|
||||
import { useFetchGroup } from "./hooks/useFetchGroup";
|
||||
import "./style.scss";
|
||||
import { Alert as APIAlert } from "../../types";
|
||||
import { Alert as APIAlert, Group as APIGroup } from "../../types";
|
||||
import ItemHeader from "../../components/ExploreAlerts/ItemHeader";
|
||||
import BaseAlert from "../../components/ExploreAlerts/BaseAlert";
|
||||
import Modal from "../../components/Main/Modal/Modal";
|
||||
@@ -21,6 +22,9 @@ const ExploreAlert = ({ groupId, id, mode, onClose }: ExploreAlertProps) => {
|
||||
error,
|
||||
} = useFetchItem<APIAlert>({ groupId, id, mode });
|
||||
|
||||
const { group } = useFetchGroup<APIGroup>({ id: groupId });
|
||||
const enrichedItem = item && group ? { ...item, group_interval: group.interval } : item;
|
||||
|
||||
if (isLoading) return (
|
||||
<Spinner />
|
||||
);
|
||||
@@ -51,7 +55,7 @@ const ExploreAlert = ({ groupId, id, mode, onClose }: ExploreAlertProps) => {
|
||||
onClose={onClose}
|
||||
>
|
||||
<div className="vm-explore-alerts">
|
||||
{item && (<BaseAlert item={item} />) || (
|
||||
{enrichedItem && (<BaseAlert item={enrichedItem} />) || (
|
||||
<Alert variant="info">{noItemFound}</Alert>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import Spinner from "../../components/Main/Spinner/Spinner";
|
||||
import Alert from "../../components/Main/Alert/Alert";
|
||||
import { useFetchItem } from "./hooks/useFetchItem";
|
||||
import { useFetchGroup } from "./hooks/useFetchGroup";
|
||||
import "./style.scss";
|
||||
import { Rule as APIRule } from "../../types";
|
||||
import { Rule as APIRule, Group as APIGroup } from "../../types";
|
||||
import ItemHeader from "../../components/ExploreAlerts/ItemHeader";
|
||||
import BaseRule from "../../components/ExploreAlerts/BaseRule";
|
||||
import Modal from "../../components/Main/Modal/Modal";
|
||||
@@ -22,6 +23,10 @@ const ExploreRule = ({ groupId, id, mode, onClose }: ExploreRuleProps) => {
|
||||
error,
|
||||
} = useFetchItem<APIRule>({ groupId, id, mode });
|
||||
|
||||
const { group } = useFetchGroup<APIGroup>({ id: groupId });
|
||||
console.log(group);
|
||||
const enrichedItem = item && group ? { ...item, group_interval: group.interval } : item;
|
||||
|
||||
if (isLoading) return (
|
||||
<Spinner />
|
||||
);
|
||||
@@ -49,7 +54,7 @@ const ExploreRule = ({ groupId, id, mode, onClose }: ExploreRuleProps) => {
|
||||
onClose={onClose}
|
||||
>
|
||||
<div className="vm-explore-alerts">
|
||||
{item && (<BaseRule item={item} />) || (
|
||||
{enrichedItem && (<BaseRule item={enrichedItem} />) || (
|
||||
<Alert variant="info">{noItemFound}</Alert>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -17,6 +17,7 @@ import { getQueryStringValue } from "../../utils/query-string";
|
||||
import { getChanges } from "./helpers";
|
||||
import debounce from "lodash.debounce";
|
||||
import { getStates } from "../../components/ExploreAlerts/helpers";
|
||||
import { useTopQueriesSet } from "./hooks/useTopQueriesSet";
|
||||
|
||||
const defaultRuleType = getQueryStringValue("type", "") as string;
|
||||
const defaultStatesStr = getQueryStringValue("states", "") as string;
|
||||
@@ -119,6 +120,8 @@ const ExploreRules: FC = () => {
|
||||
}
|
||||
}, [states, allStates]);
|
||||
|
||||
const topQueriesSet = useTopQueriesSet();
|
||||
|
||||
const pageNumInt: number = Math.max(1, parseInt(pageNum, 10) || 1);
|
||||
const {
|
||||
groups,
|
||||
@@ -187,6 +190,7 @@ const ExploreRules: FC = () => {
|
||||
key={`rule-${rule.id}`}
|
||||
rule={rule}
|
||||
states={getStates(rule)}
|
||||
topQueriesSet={topQueriesSet}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useTimeState } from "../../../state/time/TimeStateContext";
|
||||
import { useEffect, useMemo, useState } from "preact/compat";
|
||||
import { getGroupUrl } from "../../../api/explore-alerts";
|
||||
import { useAppState } from "../../../state/common/StateContext";
|
||||
import { ErrorTypes } from "../../../types";
|
||||
import { ErrorTypes, Group } from "../../../types";
|
||||
|
||||
interface FetchGroupReturn<T> {
|
||||
group?: T;
|
||||
@@ -38,7 +38,9 @@ export const useFetchGroup = <T>({
|
||||
case "application/json": {
|
||||
const resp = await response.json();
|
||||
if (response.ok) {
|
||||
setGroup(resp as T);
|
||||
const data = resp as Group;
|
||||
data.rules?.forEach(rule => { rule.group_interval = data.interval; });
|
||||
setGroup(data as unknown as T);
|
||||
setError(undefined);
|
||||
} else {
|
||||
setError(`${resp.errorType}\r\n${resp?.error}`);
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import { useEffect, useMemo, useState } from "preact/compat";
|
||||
import { useAppState } from "../../../state/common/StateContext";
|
||||
import { getTopQueries } from "../../../api/top-queries";
|
||||
import { TopQueriesData } from "../../../types";
|
||||
|
||||
const TOP_N = 100;
|
||||
const MAX_LIFETIME = "10m";
|
||||
|
||||
const normalizeQuery = (q: string): string => q.replace(/\s+/g, " ").trim();
|
||||
|
||||
export const useTopQueriesSet = (): Set<string> => {
|
||||
const { serverUrl } = useAppState();
|
||||
const [querySet, setQuerySet] = useState<Set<string>>(new Set());
|
||||
|
||||
const fetchUrl = useMemo(() => getTopQueries(serverUrl, TOP_N, MAX_LIFETIME), [serverUrl]);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
const response = await fetch(fetchUrl);
|
||||
if (!response.ok) return;
|
||||
const data: TopQueriesData = await response.json();
|
||||
const queries = new Set<string>();
|
||||
const lists = [data.topByCount, data.topByAvgDuration, data.topBySumDuration, data.topByAvgMemoryUsage];
|
||||
for (const list of lists) {
|
||||
if (Array.isArray(list)) {
|
||||
for (const item of list) {
|
||||
queries.add(normalizeQuery(item.query));
|
||||
}
|
||||
}
|
||||
}
|
||||
setQuerySet(queries);
|
||||
} catch {
|
||||
// silently ignore errors - top queries is an optional enhancement
|
||||
}
|
||||
};
|
||||
|
||||
fetchData();
|
||||
}, [fetchUrl]);
|
||||
|
||||
return querySet;
|
||||
};
|
||||
@@ -13,6 +13,7 @@ export interface TopQueryPanelProps {
|
||||
title?: string,
|
||||
columns: {title?: string, key: (keyof TopQuery), sortBy?: (keyof TopQuery)}[],
|
||||
defaultOrderBy?: keyof TopQuery,
|
||||
highlightQuery?: string,
|
||||
}
|
||||
const tabs = ["table", "JSON"].map((t, i) => ({
|
||||
value: String(i),
|
||||
@@ -20,7 +21,7 @@ const tabs = ["table", "JSON"].map((t, i) => ({
|
||||
icon: i === 0 ? <TableIcon /> : <CodeIcon />
|
||||
}));
|
||||
|
||||
const TopQueryPanel: FC<TopQueryPanelProps> = ({ rows, title, columns, defaultOrderBy }) => {
|
||||
const TopQueryPanel: FC<TopQueryPanelProps> = ({ rows, title, columns, defaultOrderBy, highlightQuery }) => {
|
||||
const { isMobile } = useDeviceDetect();
|
||||
const [activeTab, setActiveTab] = useState(0);
|
||||
|
||||
@@ -69,6 +70,7 @@ const TopQueryPanel: FC<TopQueryPanelProps> = ({ rows, title, columns, defaultOr
|
||||
rows={rows}
|
||||
columns={columns}
|
||||
defaultOrderBy={defaultOrderBy}
|
||||
highlightQuery={highlightQuery}
|
||||
/>
|
||||
)}
|
||||
{activeTab === 1 && <JsonView data={rows} />}
|
||||
|
||||
@@ -9,12 +9,16 @@ import Tooltip from "../../../components/Main/Tooltip/Tooltip";
|
||||
import { Link } from "react-router-dom";
|
||||
import useCopyToClipboard from "../../../hooks/useCopyToClipboard";
|
||||
|
||||
const TopQueryTable:FC<TopQueryPanelProps> = ({ rows, columns, defaultOrderBy }) => {
|
||||
const normalizeQuery = (q: string): string => q.replace(/\s+/g, " ").trim();
|
||||
|
||||
const TopQueryTable:FC<TopQueryPanelProps> = ({ rows, columns, defaultOrderBy, highlightQuery }) => {
|
||||
const copyToClipboard = useCopyToClipboard();
|
||||
|
||||
const [orderBy, setOrderBy] = useState<keyof TopQuery>(defaultOrderBy || "count");
|
||||
const [orderDir, setOrderDir] = useState<"asc" | "desc">("desc");
|
||||
|
||||
const normalizedHighlight = useMemo(() => highlightQuery ? normalizeQuery(highlightQuery) : "", [highlightQuery]);
|
||||
|
||||
const sortedList = useMemo(() => stableSort(rows, getComparator(orderDir, orderBy)),
|
||||
[rows, orderBy, orderDir]);
|
||||
|
||||
@@ -59,9 +63,11 @@ const TopQueryTable:FC<TopQueryPanelProps> = ({ rows, columns, defaultOrderBy })
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="vm-table-body">
|
||||
{sortedList.map((row, rowIndex) => (
|
||||
{sortedList.map((row, rowIndex) => {
|
||||
const isHighlighted = normalizedHighlight && normalizeQuery(row.query) === normalizedHighlight;
|
||||
return (
|
||||
<tr
|
||||
className="vm-table__row"
|
||||
className={classNames({ "vm-table__row": true, "vm-table__row_highlighted": !!isHighlighted })}
|
||||
key={rowIndex}
|
||||
>
|
||||
{columns.map((col) => (
|
||||
@@ -103,7 +109,8 @@ const TopQueryTable:FC<TopQueryPanelProps> = ({ rows, columns, defaultOrderBy })
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
|
||||
@@ -15,6 +15,7 @@ import "./style.scss";
|
||||
import useDeviceDetect from "../../hooks/useDeviceDetect";
|
||||
import classNames from "classnames";
|
||||
import useStateSearchParams from "../../hooks/useStateSearchParams";
|
||||
import { getQueryStringValue } from "../../utils/query-string";
|
||||
|
||||
const exampleDuration = "30ms, 15s, 3d4h, 1y2w";
|
||||
|
||||
@@ -23,6 +24,7 @@ const TopQueries: FC = () => {
|
||||
|
||||
const [topN, setTopN] = useStateSearchParams(10, "topN");
|
||||
const [maxLifetime, setMaxLifetime] = useStateSearchParams("10m", "maxLifetime");
|
||||
const highlightQuery = getQueryStringValue("query", "") as string;
|
||||
|
||||
const { data, error, loading, fetch } = useFetchTopQueries({ topN, maxLifetime });
|
||||
|
||||
@@ -157,6 +159,7 @@ const TopQueries: FC = () => {
|
||||
{ key: "count" }
|
||||
]}
|
||||
defaultOrderBy={"sumDurationSeconds"}
|
||||
highlightQuery={highlightQuery}
|
||||
/>
|
||||
<TopQueryPanel
|
||||
rows={data.topByAvgDuration}
|
||||
@@ -168,6 +171,7 @@ const TopQueries: FC = () => {
|
||||
{ key: "count" }
|
||||
]}
|
||||
defaultOrderBy={"avgDurationSeconds"}
|
||||
highlightQuery={highlightQuery}
|
||||
/>
|
||||
<TopQueryPanel
|
||||
rows={data.topByCount}
|
||||
@@ -177,6 +181,7 @@ const TopQueries: FC = () => {
|
||||
{ key: "timeRange", sortBy: "timeRangeSeconds", title: "query time interval" },
|
||||
{ key: "count" }
|
||||
]}
|
||||
highlightQuery={highlightQuery}
|
||||
/>
|
||||
<TopQueryPanel
|
||||
rows={data.topByAvgMemoryUsage}
|
||||
@@ -188,6 +193,7 @@ const TopQueries: FC = () => {
|
||||
{ key: "count" }
|
||||
]}
|
||||
defaultOrderBy={"avgMemoryBytes"}
|
||||
highlightQuery={highlightQuery}
|
||||
/>
|
||||
</div>
|
||||
</>)}
|
||||
|
||||
@@ -23,6 +23,16 @@
|
||||
&_selected {
|
||||
background-color: rgba($color-dodger-blue, 0.05);
|
||||
}
|
||||
|
||||
&_highlighted {
|
||||
font-weight: bold;
|
||||
background-color: rgba($color-dodger-blue, 0.08);
|
||||
box-shadow: inset 3px 0 0 $color-dodger-blue;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba($color-dodger-blue, 0.14);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-cell {
|
||||
|
||||
15
ceconfig.yaml
Normal file
15
ceconfig.yaml
Normal file
@@ -0,0 +1,15 @@
|
||||
streams:
|
||||
- name: 'global_by_metric_name'
|
||||
group: "__name__"
|
||||
|
||||
- name: 'global_by_instance'
|
||||
group: "instance"
|
||||
|
||||
- name: 'eu_region_by_instance'
|
||||
filter: '{region="eu-central-1"}'
|
||||
group: "instance"
|
||||
|
||||
# TODO:
|
||||
# - window duration
|
||||
# - do not expose as metric below threashold
|
||||
# -
|
||||
22
task.md
Normal file
22
task.md
Normal file
@@ -0,0 +1,22 @@
|
||||
Implement cardinality estimator.
|
||||
|
||||
Place absolute all code in app/cestimator.
|
||||
|
||||
It should accept a config via -config in yaml format.
|
||||
Example of configuration:
|
||||
|
||||
```yaml
|
||||
estimators:
|
||||
- stream: foo # required
|
||||
filter: 'as promql' #optional
|
||||
group: 'label name' # optional
|
||||
```
|
||||
|
||||
For each estimator in config it should create hll counter using https://github.com/axiomhq/hyperloglog lib.
|
||||
If a group parameter is defined than create a hll counter per group.
|
||||
|
||||
The app should accept data in Prometheus remote write protocol. Reuse existing solutions.
|
||||
|
||||
expose cardinality on /metrics endpoint in format:
|
||||
|
||||
cardinality_estimate{stream="foo",group="label name"} 123
|
||||
Reference in New Issue
Block a user