Compare commits

...

1 Commits

Author SHA1 Message Date
Hui Wang
bcfb172697 init 2026-08-14 21:29:09 +08:00
20 changed files with 529 additions and 266 deletions

View File

@@ -1,5 +1,7 @@
groups:
- name: alertmanager.rules
labels:
team: wow
rules:
- alert: AlertmanagerConfigInconsistent
annotations:
@@ -12,4 +14,12 @@ groups:
count by(namespace,service) (count_values by(namespace,service) ("config_hash", alertmanager_config_hash{job="alertmanager-main",namespace="openshift-monitoring"})) != 1
for: 5m
labels:
severity: critical
severity: critical
- name: g1
interval: 30s
labels:
team: wow
rules:
- alert: a1
expr: up>0
for: 5m

View File

@@ -0,0 +1,46 @@
package remotewrite
import (
"context"
"fmt"
"testing"
"time"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/prompb"
)
func BenchmarkClientFlushEndToEnd(b *testing.B) {
srv := newRWServer()
defer srv.Close()
client, err := NewClient(context.Background(), Config{
Addr: srv.URL,
MaxBatchSize: 10000,
Concurrency: 1,
MaxQueueSize: 100000,
FlushInterval: time.Hour,
})
if err != nil {
b.Fatalf("failed to create client: %s", err)
}
defer client.Close()
const tsCount = 100000
tss := make([]prompb.TimeSeries, tsCount)
for i := range tss {
tss[i] = prompb.TimeSeries{
Labels: []prompb.Label{{Name: "__name__", Value: fmt.Sprintf("metric_%d", i)}},
Samples: []prompb.Sample{{Value: float64(i), Timestamp: 1000}},
}
}
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
// WriteRequest is stack-allocated each iter; re-assigning tss is just a slice header copy
wr := prompb.WriteRequest{Timeseries: tss}
client.flush(context.Background(), &wr)
// flush calls wr.Reset() via defer; restore the slice for next iteration
wr.Timeseries = tss
}
}

View File

@@ -22,6 +22,7 @@ import (
"github.com/VictoriaMetrics/VictoriaMetrics/lib/httputil"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/procutil"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/vmalertapi"
)
var reloadAuthKey = flagutil.NewPassword("reloadAuthKey", "Auth key for /-/reload http endpoint. It must be passed via authKey query arg. It overrides -httpAuth.*")
@@ -281,16 +282,8 @@ func (rh *requestHandler) getAlert(r *http.Request) (*rule.ApiAlert, *httpserver
return a, nil
}
type listGroupsResponse struct {
Status string `json:"status"`
Page int `json:"page,omitempty"`
TotalPages int `json:"total_pages,omitempty"`
TotalGroups int `json:"total_groups,omitempty"`
TotalRules int `json:"total_rules,omitempty"`
Data struct {
Groups []*rule.ApiGroup `json:"groups"`
} `json:"data"`
}
// listGroupsResponse is shared with lib/vmalertproxy, which merges responses from multiple vmalerts.
type listGroupsResponse = vmalertapi.ListGroupsResponse[*rule.ApiGroup]
type groupsFilter struct {
groupNames []string
@@ -596,12 +589,8 @@ func (rh *requestHandler) listGroups(rf *rulesFilter) ([]byte, *httpserver.Error
return b, nil
}
type listAlertsResponse struct {
Status string `json:"status"`
Data struct {
Alerts []*rule.ApiAlert `json:"alerts"`
} `json:"data"`
}
// listAlertsResponse is shared with lib/vmalertproxy, which merges responses from multiple vmalerts.
type listAlertsResponse = vmalertapi.ListAlertsResponse[*rule.ApiAlert]
func (rh *requestHandler) groupAlerts() []rule.GroupAlerts {
rh.m.groupsMu.RLock()
@@ -665,12 +654,8 @@ func (rh *requestHandler) listAlerts(af *alertsFilter) ([]byte, *httpserver.Erro
return b, nil
}
type listNotifiersResponse struct {
Status string `json:"status"`
Data struct {
Notifiers []*notifier.ApiNotifier `json:"notifiers"`
} `json:"data"`
}
// listNotifiersResponse is shared with lib/vmalertproxy, which merges responses from multiple vmalerts.
type listNotifiersResponse = vmalertapi.ListNotifiersResponse[*notifier.ApiNotifier]
func (rh *requestHandler) listNotifiers() ([]byte, *httpserver.ErrorWithStatusCode) {
targets := notifier.GetTargets()

View File

@@ -38,9 +38,15 @@ var (
logSlowQueryDuration = flag.Duration("search.logSlowQueryDuration", 5*time.Second, "Log queries with execution time exceeding this value. Zero disables slow query logging. "+
"See also -search.logQueryMemoryUsage")
vmalertProxyURL = flag.String("vmalert.proxyURL", "", "Optional URL for proxying requests to vmalert. For example, if -vmalert.proxyURL=http://vmalert:8880 , "+
vmalertProxyURL = flagutil.NewArrayString("vmalert.proxyURL", "Optional URL for proxying requests to vmalert. For example, if -vmalert.proxyURL=http://vmalert:8880 , "+
"then alerting API requests such as /api/v1/rules from Grafana will be proxied to http://vmalert:8880/api/v1/rules . "+
"If multiple URLs are set, then alerting API requests are sent to all of them and the responses are merged. Every returned group, alert and notifier target "+
"is marked with the `__vmalert_source` label containing the name of the vmalert it came from - see -vmalert.proxyName. "+
"Unavailable vmalerts do not fail the request - they are reported via the `warnings` field in the response. "+
"See https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/#vmalert")
vmalertProxyName = flagutil.NewArrayString("vmalert.proxyName", "Optional name for the vmalert at the corresponding -vmalert.proxyURL. "+
"It is used as a value for the `__vmalert_source` label and for routing requests to a particular vmalert via `vmalert_source` query arg. "+
"By default the name is set to `vmalert_proxy_N`, where N is the one-based position of the corresponding -vmalert.proxyURL")
)
var slowQueries = metrics.NewCounter(`vm_slow_queries_total`)
@@ -56,10 +62,8 @@ func Init(vmselectMaxConcurrentRequests int, vmselectMaxQueueDuration time.Durat
maxQueueDuration = vmselectMaxQueueDuration
concurrencyLimitCh = make(chan struct{}, maxConcurrentRequests)
vmalertproxy.Init(*vmalertProxyURL, *vmalertProxyName)
initVMUIConfig()
vmalertproxy.Init(*vmalertProxyURL)
}
// InitSecretFlags manages the secret flags for this pkg and must be called by app-level initSecretFlags.
@@ -527,7 +531,7 @@ func handleStaticAndSimpleRequests(w http.ResponseWriter, r *http.Request, path
}
if strings.HasPrefix(path, "/vmalert/") {
vmalertRequests.Inc()
if len(*vmalertProxyURL) == 0 {
if !vmalertproxy.Enabled() {
w.WriteHeader(http.StatusBadRequest)
w.Header().Set("Content-Type", "application/json")
fmt.Fprintf(w, "%s", `{"status":"error","msg":"the '-vmalert.proxyURL' command-line must be configured; `+
@@ -571,7 +575,7 @@ func handleStaticAndSimpleRequests(w http.ResponseWriter, r *http.Request, path
return true
case "/api/v1/rules", "/rules":
rulesRequests.Inc()
if len(*vmalertProxyURL) > 0 {
if vmalertproxy.Enabled() {
vmalertproxy.HandleRequest(w, r, path)
return true
}
@@ -581,7 +585,7 @@ func handleStaticAndSimpleRequests(w http.ResponseWriter, r *http.Request, path
return true
case "/api/v1/alerts", "/alerts":
alertsRequests.Inc()
if len(*vmalertProxyURL) > 0 {
if vmalertproxy.Enabled() {
vmalertproxy.HandleRequest(w, r, path)
return true
}
@@ -591,7 +595,7 @@ func handleStaticAndSimpleRequests(w http.ResponseWriter, r *http.Request, path
return true
case "/api/v1/notifiers", "/notifiers":
notifiersRequests.Inc()
if len(*vmalertProxyURL) > 0 {
if vmalertproxy.Enabled() {
vmalertproxy.HandleRequest(w, r, path)
return true
}
@@ -749,6 +753,9 @@ func initVMUIConfig() {
} `json:"license"`
VMAlert struct {
Enabled bool `json:"enabled"`
// Sources contains names of the vmalerts at -vmalert.proxyURL.
// vmui uses them for filtering rules by the vmalert they come from.
Sources []string `json:"sources,omitempty"`
} `json:"vmalert"`
}
data, err := vmuiFiles.ReadFile("vmui/config.json")
@@ -764,7 +771,11 @@ func initVMUIConfig() {
// buildinfo.ShortVersion() may return empty result for builds without tags
cfg.Version = buildinfo.Version
}
cfg.VMAlert.Enabled = len(*vmalertProxyURL) != 0
cfg.VMAlert.Enabled = vmalertproxy.Enabled()
if names := vmalertproxy.SourceNames(); len(names) > 1 {
// A single vmalert needs no filtering by source.
cfg.VMAlert.Sources = names
}
data, err = json.Marshal(&cfg)
if err != nil {
logger.Fatalf("cannot create vmui config: %s", err)

View File

@@ -0,0 +1,5 @@
<svg width="48" height="48" fill="#020202" xmlns="http://www.w3.org/2000/svg">
<path d="M24.5475 0C10.3246.0265251 1.11379 3.06365 4.40623 6.10077c0 0 12.32997 11.23333 16.58217 14.84083.8131.6896 2.1728 1.1936 3.5191 1.2201h.1199c1.3463-.0265 2.706-.5305 3.5191-1.2201 4.2522-3.5942 16.5422-14.84083 16.5422-14.84083C48.0478 3.06365 38.8636.0265251 24.6674 0"/>
<path d="M28.1579 27.0159c-.8131.6896-2.1728 1.1936-3.5191 1.2201h-.12c-1.3463-.0265-2.7059-.5305-3.519-1.2201-2.9725-2.5067-13.35639-11.87-17.26201-15.3979v5.4112c0 .5968.22661 1.3793.6265 1.7506C7.00358 21.1936 17.2675 30.5437 20.9731 33.6737c.8132.6896 2.1728 1.1936 3.5191 1.2201h.12c1.3463-.0265 2.7059-.5305 3.519-1.2201 3.679-3.13 13.9429-12.4536 16.6089-14.8939.4132-.3713.6265-1.1538.6265-1.7506V11.618c-3.9323 3.5411-14.3162 12.931-17.2354 15.3979h.0267Z"/>
<path d="M28.1579 39.748c-.8131.6897-2.1728 1.1937-3.5191 1.2202h-.12c-1.3463-.0265-2.7059-.5305-3.519-1.2202-2.9725-2.4933-13.35639-11.8567-17.26201-15.3978v5.4111c0 .5969.22661 1.3793.6265 1.7507C7.00358 33.9258 17.2675 43.2759 20.9731 46.4058c.8132.6897 2.1728 1.1937 3.5191 1.2202h.12c1.3463-.0265 2.7059-.5305 3.519-1.2202 3.679-3.1299 13.9429-12.4535 16.6089-14.8938.4132-.3714.6265-1.1538.6265-1.7507v-5.4111c-3.9323 3.5411-14.3162 12.931-17.2354 15.3978h.0267Z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

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

File diff suppressed because one or more lines are too long

View File

@@ -1 +0,0 @@
<svg width="48" height="48" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M24.5475 0C10.3246.0265251 1.11379 3.06365 4.40623 6.10077c0 0 12.32997 11.23333 16.58217 14.84083.8131.6896 2.1728 1.1936 3.5191 1.2201h.1199c1.3463-.0265 2.706-.5305 3.5191-1.2201 4.2522-3.5942 16.5422-14.84083 16.5422-14.84083C48.0478 3.06365 38.8636.0265251 24.6674 0" fill="#020202"/><path d="M28.1579 27.0159c-.8131.6896-2.1728 1.1936-3.5191 1.2201h-.12c-1.3463-.0265-2.7059-.5305-3.519-1.2201-2.9725-2.5067-13.35639-11.87-17.26201-15.3979v5.4112c0 .5968.22661 1.3793.6265 1.7506C7.00358 21.1936 17.2675 30.5437 20.9731 33.6737c.8132.6896 2.1728 1.1936 3.5191 1.2201h.12c1.3463-.0265 2.7059-.5305 3.519-1.2201 3.679-3.13 13.9429-12.4536 16.6089-14.8939.4132-.3713.6265-1.1538.6265-1.7506V11.618c-3.9323 3.5411-14.3162 12.931-17.2354 15.3979h.0267Z" fill="#020202"/><path d="M28.1579 39.748c-.8131.6897-2.1728 1.1937-3.5191 1.2202h-.12c-1.3463-.0265-2.7059-.5305-3.519-1.2202-2.9725-2.4933-13.35639-11.8567-17.26201-15.3978v5.4111c0 .5969.22661 1.3793.6265 1.7507C7.00358 33.9258 17.2675 43.2759 20.9731 46.4058c.8132.6897 2.1728 1.1937 3.5191 1.2202h.12c1.3463-.0265 2.7059-.5305 3.519-1.2202 3.679-3.1299 13.9429-12.4535 16.6089-14.8938.4132-.3714.6265-1.1538.6265-1.7507v-5.4111c-3.9323 3.5411-14.3162 12.931-17.2354 15.3978h.0267Z" fill="#020202"/></svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -2,9 +2,9 @@
<html lang="en">
<head>
<meta charset="utf-8"/>
<link rel="icon" href="./favicon.svg"/>
<link rel="apple-touch-icon" href="./favicon.svg"/>
<link rel="mask-icon" href="./favicon.svg" color="#000000">
<link id="favicon" rel="icon" href="./assets/favicon.svg" />
<link rel="apple-touch-icon" href="./assets/favicon.svg" />
<link id="mask-icon" rel="mask-icon" href="./assets/favicon.svg" color="#000000">
<meta name="robots" content="noindex">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=5"/>
@@ -37,11 +37,11 @@
<meta property="og:title" content="UI for VictoriaMetrics">
<meta property="og:url" content="https://victoriametrics.com/">
<meta property="og:description" content="Explore and troubleshoot your VictoriaMetrics data">
<script type="module" crossorigin src="./assets/index-B1dXK3k7.js"></script>
<script type="module" crossorigin src="./assets/index-CLkMlXOw.js"></script>
<link rel="modulepreload" crossorigin href="./assets/rolldown-runtime-CNC7AqOf.js">
<link rel="modulepreload" crossorigin href="./assets/vendor-DwJYpOdw.js">
<link rel="stylesheet" crossorigin href="./assets/vendor-CnsZ1jie.css">
<link rel="stylesheet" crossorigin href="./assets/index-BJqoElx2.css">
<link rel="stylesheet" crossorigin href="./assets/index-B7GLMMVh.css">
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>

View File

@@ -3,7 +3,7 @@
"name": "vmui",
"icons": [
{
"src": "favicon.svg",
"src": "./assets/favicon.svg",
"sizes": "any",
"type": "image/svg+xml"
}

View File

@@ -1,5 +1,10 @@
export const getGroupsUrl = (server: string, search: string, type: string, states: string[], maxGroups: number): string => {
return `${server}/vmalert/api/v1/rules?datasource_type=prometheus&search=${encodeURIComponent(search)}&type=${encodeURIComponent(type)}&state=${states.map(encodeURIComponent).join(",")}&group_limit=${maxGroups}&extended_states=true`;
// vmalertSourceParam routes the request to a single vmalert from -vmalert.proxyURL.
// An empty source means that the request is sent to all the configured vmalerts.
const vmalertSourceParam = (source: string): string =>
source ? `&vmalert_source=${encodeURIComponent(source)}` : "";
export const getGroupsUrl = (server: string, search: string, type: string, states: string[], maxGroups: number, source: string): string => {
return `${server}/vmalert/api/v1/rules?datasource_type=prometheus&search=${encodeURIComponent(search)}&type=${encodeURIComponent(type)}&state=${states.map(encodeURIComponent).join(",")}&group_limit=${maxGroups}&extended_states=true${vmalertSourceParam(source)}`;
};
export const getItemUrl = (

View File

@@ -11,9 +11,12 @@ interface RulesHeaderProps {
allRuleTypes: string[];
allStates: string[];
states: string[];
sources: string[];
allSources: string[];
search: string;
onChangeRuleType: (input: string) => void;
onChangeStates: (input: string) => void;
onChangeSource: (input: string) => void;
onChangeSearch: (input: string) => void;
}
@@ -22,9 +25,12 @@ const RulesHeader = ({
allRuleTypes,
allStates,
states,
sources,
allSources,
search,
onChangeRuleType,
onChangeStates,
onChangeSource,
onChangeSearch,
}: RulesHeaderProps) => {
const noStateText = useMemo(
@@ -67,6 +73,19 @@ const RulesHeader = ({
searchable
/>
</div>
{allSources.length > 1 && (
<div className="vm-explore-alerts-header__vmalert_source">
<Select
value={sources}
list={allSources}
label="vmalert source"
placeholder="Please select vmalert source"
onChange={onChangeSource}
includeAll
searchable
/>
</div>
)}
<div className="vm-explore-alerts-header-search">
<TextField
label="Search"

View File

@@ -21,6 +21,10 @@
min-width: 150px;
}
&__vmalert_source {
min-width: 180px;
}
&-search {
flex-grow: 1;
.vm-text-field__input {

View File

@@ -17,11 +17,13 @@ import { getQueryStringValue } from "../../utils/query-string";
import { getChanges } from "./helpers";
import debounce from "lodash.debounce";
import { getStates } from "../../components/ExploreAlerts/helpers";
import { useAppState } from "../../state/common/StateContext";
const defaultRuleType = getQueryStringValue("type", "") as string;
const defaultStatesStr = getQueryStringValue("states", "") as string;
const defaultStates = defaultStatesStr.split("&").filter((s) => s) as string[];
const defaultSearchInput = getQueryStringValue("search", "") as string;
const defaultSource = getQueryStringValue("vmalert_source", "") as string;
const TYPE_STATES: Record<string, string[]> = {
alert: ["inactive", "firing", "nomatch", "pending", "unhealthy"],
record: ["unhealthy", "nomatch", "ok"],
@@ -36,8 +38,10 @@ const ExploreRules: FC = () => {
const [searchInput, setSearchInput] = useState(defaultSearchInput);
const [ruleType, setRuleType] = useState(defaultRuleType);
const [states, setStates] = useState(defaultStates);
const [source, setSource] = useState(defaultSource);
const [modalOpen, setModalOpen] = useState(false);
const [searchParams, setSearchParams] = useSearchParams();
const { appConfig } = useAppState();
useEffect(() => {
setModalOpen(!!groupId);
@@ -47,6 +51,7 @@ const ExploreRules: FC = () => {
type: ruleType,
states: states.join("&"),
search: searchInput,
vmalert_source: source,
group_id: groupId,
alert_id: alertId,
rule_id: ruleId,
@@ -113,19 +118,28 @@ const ExploreRules: FC = () => {
[ruleType]
);
const selectedRuleTypes = [ruleType].filter(Boolean);
// allSources is set by vmselect only when more than a single -vmalert.proxyURL is configured.
const allSources = useMemo(() => appConfig?.vmalert?.sources || [], [appConfig]);
const selectedSources = [source].filter(Boolean);
useEffect(() => {
if (!states.every(v => allStates.includes(v))) {
setStates([]);
}
}, [states, allStates]);
useEffect(() => {
if (source && allSources.length && !allSources.includes(source)) {
setSource("");
}
}, [source, allSources]);
const pageNumInt: number = Math.max(1, parseInt(pageNum, 10) || 1);
const {
groups,
isLoading,
error,
warnings,
pageInfo,
} = useFetchGroups({ blockFetch: modalOpen, search: searchInput, ruleType, states, pageNum: pageNumInt, onPageChange });
} = useFetchGroups({ blockFetch: modalOpen, search: searchInput, ruleType, states, source, pageNum: pageNumInt, onPageChange });
const handleChangeStates = useCallback((title: string) => {
const newParams = new URLSearchParams(searchParams);
@@ -143,6 +157,14 @@ const ExploreRules: FC = () => {
setRuleType(changes.length && changes.length !== allRuleTypes.length ? changes[0] : "");
}, [ruleType, searchParams]);
const handleChangeSource = useCallback((title: string) => {
const newParams = new URLSearchParams(searchParams);
newParams.set("page_num", "1");
setSearchParams(newParams);
const changes = getChanges(title, selectedSources);
setSource(changes.length && changes.length !== allSources.length ? changes[0] : "");
}, [source, searchParams, allSources]);
return (
<>
{modalOpen && getModal()}
@@ -153,11 +175,20 @@ const ExploreRules: FC = () => {
allRuleTypes={allRuleTypes}
states={states}
allStates={allStates}
sources={selectedSources}
allSources={allSources}
search={searchInput}
onChangeRuleType={handleChangeRuleType}
onChangeStates={handleChangeStates}
onChangeSource={handleChangeSource}
onChangeSearch={debounce(handleChangeSearch, 500)}
/>
{warnings.map((warning) => (
<Alert
key={warning}
variant="warning"
>{warning}</Alert>
))}
<Pagination
page={pageInfo.page}
totalPages={pageInfo.total_pages}

View File

@@ -8,6 +8,9 @@ interface FetchGroupsReturn {
groups: Group[];
isLoading: boolean;
error?: ErrorTypes | string;
// warnings is non-empty when some of the vmalerts at -vmalert.proxyURL are unavailable.
// The rest of vmalerts still return their groups in this case.
warnings: string[];
pageInfo: PageInfo;
}
@@ -16,6 +19,7 @@ interface FetchGroupsProps {
search: string;
ruleType: string;
states: string[];
source: string;
pageNum: number;
onPageChange: (num: number) => () => void;
}
@@ -29,7 +33,7 @@ interface PageInfo {
const MAX_GROUPS = 100;
export const useFetchGroups = ({ blockFetch, pageNum, search, ruleType, states, onPageChange }: FetchGroupsProps): FetchGroupsReturn => {
export const useFetchGroups = ({ blockFetch, pageNum, search, ruleType, states, source, onPageChange }: FetchGroupsProps): FetchGroupsReturn => {
const { serverUrl } = useAppState();
const { period } = useTimeState();
@@ -42,10 +46,11 @@ export const useFetchGroups = ({ blockFetch, pageNum, search, ruleType, states,
total_rules: 0,
});
const [error, setError] = useState<ErrorTypes | string>();
const [warnings, setWarnings] = useState<string[]>([]);
const fetchUrl = useMemo(
() => getGroupsUrl(serverUrl, search, ruleType, states, MAX_GROUPS),
[serverUrl, search, ruleType, states],
() => getGroupsUrl(serverUrl, search, ruleType, states, MAX_GROUPS, source),
[serverUrl, search, ruleType, states, source],
);
const loaded = !!groups.length || !blockFetch;
@@ -66,6 +71,7 @@ export const useFetchGroups = ({ blockFetch, pageNum, search, ruleType, states,
total_groups: resp.total_groups || 0,
total_rules: resp.total_rules || 0,
});
setWarnings((resp.warnings || []) as string[]);
setError(undefined);
} else if (response.status === 400 && resp?.error?.includes("exceeds total amount of pages")) {
onPageChange(1)();
@@ -83,5 +89,5 @@ export const useFetchGroups = ({ blockFetch, pageNum, search, ruleType, states,
fetchData().catch(console.error);
}, [fetchUrl, period, loaded, pageNum]);
return { groups, isLoading, error, pageInfo };
return { groups, isLoading, error, warnings, pageInfo };
};

View File

@@ -6,6 +6,7 @@ interface rulesQueryProps {
type?: string;
states?: string;
search?: string;
vmalert_source?: string;
rule_id: string;
group_id: string;
alert_id: string;
@@ -15,6 +16,7 @@ export const useRulesSetQueryParams = ({
type,
states,
search,
vmalert_source,
rule_id,
alert_id,
group_id,
@@ -26,6 +28,7 @@ export const useRulesSetQueryParams = ({
type,
states,
search,
vmalert_source,
alert_id,
rule_id,
group_id,
@@ -38,6 +41,7 @@ export const useRulesSetQueryParams = ({
type,
states,
search,
vmalert_source,
rule_id,
group_id,
alert_id,

View File

@@ -184,6 +184,9 @@ export interface AppConfig {
};
vmalert?: {
enabled: boolean;
// sources contains names of the vmalerts configured via -vmalert.proxyURL.
// It is set only if more than a single vmalert is configured.
sources?: string[];
};
version?: string;
}

View File

@@ -1,30 +1,107 @@
package vmalertproxy
import (
"fmt"
"net/http"
"net/http/httputil"
nethttputil "net/http/httputil"
"net/url"
"strings"
"github.com/VictoriaMetrics/metrics"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/httputil"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
)
// Init initializes proxying requests to the given proxyURL when calling HandleRequest.
// SourceLabel is the meta-label added to every group, alert and notifier target
// returned by HandleRequest when more than a single -vmalert.proxyURL is configured.
//
// Init must be called after flag.Parse(), since it uses command-line flags.
func Init(proxyURL string) {
if len(proxyURL) == 0 {
return
}
pu, err := url.Parse(proxyURL)
if err != nil {
logger.Fatalf("cannot parse -vmalert.proxyURL=%q: %s", proxyURL, err)
}
vmalertProxyHost = pu.Host
vmalertProxy = httputil.NewSingleHostReverseProxy(pu)
// It contains the name of the vmalert, which returned the corresponding entity.
const SourceLabel = "__vmalert_source"
// SourceQueryArg is the name of the query arg, which routes the request
// to a single vmalert with the given name instead of fanning it out to all
// the configured -vmalert.proxyURL.
const SourceQueryArg = "vmalert_source"
type backend struct {
// name is the value of SourceLabel added to entities returned by this backend.
name string
// u is the parsed -vmalert.proxyURL.
u *url.URL
rp *nethttputil.ReverseProxy
requests *metrics.Counter
errors *metrics.Counter
}
// HandleRequest proxies the given request path to vmalert at proxyURL passed to Init().
var backends []*backend
// Init initializes proxying requests to the given proxyURLs when calling HandleRequest.
//
// proxyNames contains optional names for the vmalert at the corresponding proxyURLs.
// Missing names are set to `vmalert_proxy_N`, where N is the one-based index of the proxyURL.
//
// Init must be called after flag.Parse(), since it uses command-line flags.
func Init(proxyURLs, proxyNames []string) {
backends = nil
names := make(map[string]struct{}, len(proxyURLs))
for i, proxyURL := range proxyURLs {
if len(proxyURL) == 0 {
continue
}
pu, err := url.Parse(proxyURL)
if err != nil {
logger.Fatalf("cannot parse -vmalert.proxyURL=%q: %s", proxyURL, err)
}
name := fmt.Sprintf("vmalert_proxy_%d", i+1)
if i < len(proxyNames) && len(proxyNames[i]) > 0 {
name = proxyNames[i]
}
if _, ok := names[name]; ok {
logger.Fatalf("duplicate name %q for -vmalert.proxyURL #%d; every vmalert must have unique name at -vmalert.proxyName", name, i+1)
}
names[name] = struct{}{}
backends = append(backends, &backend{
name: name,
u: pu,
rp: nethttputil.NewSingleHostReverseProxy(pu),
requests: metrics.GetOrCreateCounter(fmt.Sprintf(`vm_vmalert_proxy_requests_total{source=%q}`, name)),
errors: metrics.GetOrCreateCounter(fmt.Sprintf(`vm_vmalert_proxy_request_errors_total{source=%q}`, name)),
})
}
if len(proxyNames) > len(proxyURLs) {
logger.Fatalf("-vmalert.proxyName cannot contain more items (%d) than -vmalert.proxyURL (%d)", len(proxyNames), len(proxyURLs))
}
}
// Enabled returns true if at least a single -vmalert.proxyURL is configured.
func Enabled() bool {
return len(backends) > 0
}
// SourceNames returns names of the configured vmalerts in the order of -vmalert.proxyURL.
//
// The returned names are used as values for the SourceLabel meta-label and for the SourceQueryArg query arg.
func SourceNames() []string {
names := make([]string, len(backends))
for i, b := range backends {
names[i] = b.name
}
return names
}
// HandleRequest proxies the given request path to vmalert at proxyURLs passed to Init().
//
// If multiple proxyURLs are configured, then requests to the alerting API are sent to all of them
// and the responses are merged. Every returned entity is marked with the SourceLabel meta-label,
// so it is possible to determine the vmalert it came from. Unavailable vmalerts don't fail
// the whole request - they are reported via the `warnings` field of the response instead.
//
// Requests to a non-mergeable path (such as vmalert UI) are proxied to the first configured vmalert.
// Pass SourceQueryArg query arg for proxying the request to the given vmalert only.
func HandleRequest(w http.ResponseWriter, r *http.Request, path string) {
defer func() {
err := recover()
@@ -36,11 +113,59 @@ func HandleRequest(w http.ResponseWriter, r *http.Request, path string) {
// Forward other panics to the caller.
panic(err)
}()
bs := backends
if len(bs) == 0 {
writeAPIError(w, http.StatusBadRequest, "the '-vmalert.proxyURL' command-line flag must be configured; "+
"see https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/#vmalert")
return
}
if name := r.URL.Query().Get(SourceQueryArg); len(name) > 0 {
b := getBackendByName(name)
if b == nil {
writeAPIError(w, http.StatusBadRequest, fmt.Sprintf("unknown %s=%q; make sure it matches -vmalert.proxyName", SourceQueryArg, name))
return
}
b.proxyRequest(w, r, path)
return
}
if len(bs) == 1 {
bs[0].proxyRequest(w, r, path)
return
}
if handleFanOut(w, r, path, bs) {
return
}
// The path cannot be merged across multiple vmalerts (e.g. vmalert UI and its static assets).
// Proxy it to the first configured vmalert.
bs[0].proxyRequest(w, r, path)
}
func getBackendByName(name string) *backend {
for _, b := range backends {
if b.name == name {
return b
}
}
return nil
}
// proxyRequest proxies r to b as is.
func (b *backend) proxyRequest(w http.ResponseWriter, r *http.Request, path string) {
b.requests.Inc()
req := r.Clone(r.Context())
req.URL.Path = path
req.Host = vmalertProxyHost
req.Host = b.u.Host
if strings.HasPrefix(r.Header.Get(`User-Agent`), `Grafana`) {
q := req.URL.Query()
changed := false
if q.Has(SourceQueryArg) {
// SourceQueryArg is consumed by HandleRequest - do not pass it to vmalert.
q.Del(SourceQueryArg)
changed = true
}
if isGrafanaRequest(r) {
// Grafana currently supports only Prometheus-style alerts. If other alert types
// (e.g. logs or traces) are returned, it may fail with "Error loading alerts".
//
@@ -53,16 +178,21 @@ func HandleRequest(w http.ResponseWriter, r *http.Request, path string) {
// See:
// - https://github.com/VictoriaMetrics/victoriametrics-datasource/issues/329#issuecomment-3847585443
// - https://github.com/VictoriaMetrics/victoriametrics-datasource/issues/59
q := req.URL.Query()
q.Set("datasource_type", "prometheus")
changed = true
}
if changed {
req.URL.RawQuery = q.Encode()
req.RequestURI = ""
}
vmalertProxy.ServeHTTP(w, req)
b.rp.ServeHTTP(w, req)
}
var (
vmalertProxyHost string
vmalertProxy *httputil.ReverseProxy
)
func isGrafanaRequest(r *http.Request) bool {
return strings.HasPrefix(r.Header.Get(`User-Agent`), `Grafana`)
}
var proxyClient = &http.Client{
Transport: httputil.NewTransport(false, "vm_vmalert_proxy_client"),
}