diff --git a/app/vmagent/remotewrite/relabel.go b/app/vmagent/remotewrite/relabel.go index 359c87e92..547f0982a 100644 --- a/app/vmagent/remotewrite/relabel.go +++ b/app/vmagent/remotewrite/relabel.go @@ -46,11 +46,11 @@ func loadRelabelConfigs() (*relabelConfigs, error) { } rcs.global = global } - if len(*relabelConfigPaths) > (len(*remoteWriteURLs) + len(*remoteWriteMultitenantURLs)) { - return nil, fmt.Errorf("too many -remoteWrite.urlRelabelConfig args: %d; it mustn't exceed the number of -remoteWrite.url or -remoteWrite.multitenantURL args: %d", - len(*relabelConfigPaths), (len(*remoteWriteURLs) + len(*remoteWriteMultitenantURLs))) + if len(*relabelConfigPaths) > len(*remoteWriteURLs) { + return nil, fmt.Errorf("too many -remoteWrite.urlRelabelConfig args: %d; it mustn't exceed the number of -remoteWrite.url args: %d", + len(*relabelConfigPaths), (len(*remoteWriteURLs))) } - rcs.perURL = make([]*promrelabel.ParsedConfigs, (len(*remoteWriteURLs) + len(*remoteWriteMultitenantURLs))) + rcs.perURL = make([]*promrelabel.ParsedConfigs, len(*remoteWriteURLs)) for i, path := range *relabelConfigPaths { if len(path) == 0 { // Skip empty relabel config. diff --git a/app/vmagent/remotewrite/remotewrite.go b/app/vmagent/remotewrite/remotewrite.go index 13be88c5a..78dfad8fb 100644 --- a/app/vmagent/remotewrite/remotewrite.go +++ b/app/vmagent/remotewrite/remotewrite.go @@ -29,7 +29,6 @@ import ( "github.com/VictoriaMetrics/VictoriaMetrics/lib/promutils" "github.com/VictoriaMetrics/VictoriaMetrics/lib/ratelimiter" "github.com/VictoriaMetrics/VictoriaMetrics/lib/streamaggr" - "github.com/VictoriaMetrics/VictoriaMetrics/lib/tenantmetrics" "github.com/VictoriaMetrics/metrics" "github.com/cespare/xxhash/v2" ) @@ -39,10 +38,6 @@ var ( "or Prometheus remote_write protocol. Example url: http://:8428/api/v1/write . "+ "Pass multiple -remoteWrite.url options in order to replicate the collected data to multiple remote storage systems. "+ "The data can be sharded among the configured remote storage systems if -remoteWrite.shardByURL flag is set") - remoteWriteMultitenantURLs = flagutil.NewArrayString("remoteWrite.multitenantURL", "Base path for multitenant remote storage URL to write data to. "+ - "See https://docs.victoriametrics.com/vmagent/#multitenancy for details. Example url: http://:8480 . "+ - "Pass multiple -remoteWrite.multitenantURL flags in order to replicate data to multiple remote storage systems. "+ - "This flag is deprecated in favor of -enableMultitenantHandlers . See https://docs.victoriametrics.com/vmagent/#multitenancy") enableMultitenantHandlers = flag.Bool("enableMultitenantHandlers", false, "Whether to process incoming data via multitenant insert handlers according to "+ "https://docs.victoriametrics.com/cluster-victoriametrics/#url-format . By default incoming data is processed via single-node insert handlers "+ "according to https://docs.victoriametrics.com/#how-to-import-time-series-data ."+ @@ -118,14 +113,10 @@ var ( ) var ( - // rwctxsDefault contains statically populated entries when -remoteWrite.url is specified. - rwctxsDefault []*remoteWriteCtx + // rwctxs contains statically populated entries when -remoteWrite.url is specified. + rwctxs []*remoteWriteCtx - // rwctxsMap contains dynamically populated entries when -remoteWrite.multitenantURL is specified. - rwctxsMap = make(map[tenantmetrics.TenantID][]*remoteWriteCtx) - rwctxsMapLock sync.Mutex - - // Data without tenant id is written to defaultAuthToken if -remoteWrite.multitenantURL is specified. + // Data without tenant id is written to defaultAuthToken if -enableMultitenantHandlers is specified. defaultAuthToken = &auth.Token{} // ErrQueueFullHTTPRetry must be returned when TryPush() returns false. @@ -140,9 +131,9 @@ var ( disableOnDiskQueueAll bool ) -// MultitenancyEnabled returns true if -enableMultitenantHandlers or -remoteWrite.multitenantURL is specified. +// MultitenancyEnabled returns true if -enableMultitenantHandlers is specified. func MultitenancyEnabled() bool { - return *enableMultitenantHandlers || len(*remoteWriteMultitenantURLs) > 0 + return *enableMultitenantHandlers } // Contains the current relabelConfigs. @@ -173,11 +164,8 @@ var ( // // Stop must be called for graceful shutdown. func Init() { - if len(*remoteWriteURLs) == 0 && len(*remoteWriteMultitenantURLs) == 0 { - logger.Fatalf("at least one `-remoteWrite.url` or `-remoteWrite.multitenantURL` command-line flag must be set") - } - if len(*remoteWriteURLs) > 0 && len(*remoteWriteMultitenantURLs) > 0 { - logger.Fatalf("cannot set both `-remoteWrite.url` and `-remoteWrite.multitenantURL` command-line flags") + if len(*remoteWriteURLs) == 0 { + logger.Fatalf("at least one `-remoteWrite.url` command-line flag must be set") } if *maxHourlySeries > 0 { hourlySeriesLimiter = bloomfilter.NewLimiter(*maxHourlySeries, time.Hour) @@ -228,7 +216,7 @@ func Init() { relabelConfigTimestamp.Set(fasttime.UnixTimestamp()) if len(*remoteWriteURLs) > 0 { - rwctxsDefault = newRemoteWriteCtxs(nil, *remoteWriteURLs) + rwctxs = newRemoteWriteCtxs(nil, *remoteWriteURLs) } disableOnDiskQueueAll = true @@ -261,12 +249,6 @@ func dropDanglingQueues() { if *keepDanglingQueues { return } - if len(*remoteWriteMultitenantURLs) > 0 { - // Do not drop dangling queues for *remoteWriteMultitenantURLs, since it is impossible to determine - // unused queues for multitenant urls - they are created on demand when new sample for the given - // tenant is pushed to remote storage. - return - } // Remove dangling persistent queues, if any. // This is required for the case when the number of queues has been changed or URL have been changed. // See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/4014 @@ -274,8 +256,8 @@ func dropDanglingQueues() { // In case if there were many persistent queues with identical *remoteWriteURLs // the queue with the last index will be dropped. // See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/6140 - existingQueues := make(map[string]struct{}, len(rwctxsDefault)) - for _, rwctx := range rwctxsDefault { + existingQueues := make(map[string]struct{}, len(rwctxs)) + for _, rwctx := range rwctxs { existingQueues[rwctx.fq.Dirname()] = struct{}{} } @@ -292,7 +274,7 @@ func dropDanglingQueues() { } } if removed > 0 { - logger.Infof("removed %d dangling queues from %q, active queues: %d", removed, *tmpDataPath, len(rwctxsDefault)) + logger.Infof("removed %d dangling queues from %q, active queues: %d", removed, *tmpDataPath, len(rwctxs)) } } @@ -320,18 +302,6 @@ var ( ) func reloadStreamAggrConfigs() { - if len(*remoteWriteMultitenantURLs) > 0 { - rwctxsMapLock.Lock() - for _, rwctxs := range rwctxsMap { - reinitStreamAggr(rwctxs) - } - rwctxsMapLock.Unlock() - } else { - reinitStreamAggr(rwctxsDefault) - } -} - -func reinitStreamAggr(rwctxs []*remoteWriteCtx) { for _, rwctx := range rwctxs { rwctx.reinitStreamAggr() } @@ -411,18 +381,10 @@ func Stop() { close(configReloaderStopCh) configReloaderWG.Wait() - for _, rwctx := range rwctxsDefault { + for _, rwctx := range rwctxs { rwctx.MustStop() } - rwctxsDefault = nil - - // There is no need in locking rwctxsMapLock here, since nobody should call TryPush during the Stop call. - for _, rwctxs := range rwctxsMap { - for _, rwctx := range rwctxs { - rwctx.MustStop() - } - } - rwctxsMap = nil + rwctxs = nil if sl := hourlySeriesLimiter; sl != nil { sl.MustStop() @@ -432,20 +394,14 @@ func Stop() { } } -// PushDropSamplesOnFailure pushes wr to the configured remote storage systems set via -remoteWrite.url and -remoteWrite.multitenantURL -// -// If at is nil, then the data is pushed to the configured -remoteWrite.url. -// If at isn't nil, the data is pushed to the configured -remoteWrite.multitenantURL. +// PushDropSamplesOnFailure pushes wr to the configured remote storage systems set via -remoteWrite.url // // PushDropSamplesOnFailure can modify wr contents. func PushDropSamplesOnFailure(at *auth.Token, wr *prompbmarshal.WriteRequest) { _ = tryPush(at, wr, true) } -// TryPush tries sending wr to the configured remote storage systems set via -remoteWrite.url and -remoteWrite.multitenantURL -// -// If at is nil, then the data is pushed to the configured -remoteWrite.url. -// If at isn't nil, the data is pushed to the configured -remoteWrite.multitenantURL. +// TryPush tries sending wr to the configured remote storage systems set via -remoteWrite.url // // TryPush can modify wr contents, so the caller must re-initialize wr before calling TryPush() after unsuccessful attempt. // TryPush may send partial data from wr on unsuccessful attempt, so repeated call for the same wr may send the data multiple times. @@ -464,28 +420,11 @@ func tryPush(at *auth.Token, wr *prompbmarshal.WriteRequest, forceDropSamplesOnF } var tenantRctx *relabelCtx - var rwctxs []*remoteWriteCtx - if at == nil { - rwctxs = rwctxsDefault - } else if len(*remoteWriteMultitenantURLs) == 0 { + if at != nil { // Convert at to (vm_account_id, vm_project_id) labels. tenantRctx = getRelabelCtx() defer putRelabelCtx(tenantRctx) - rwctxs = rwctxsDefault - } else { - rwctxsMapLock.Lock() - tenantID := tenantmetrics.TenantID{ - AccountID: at.AccountID, - ProjectID: at.ProjectID, - } - rwctxs = rwctxsMap[tenantID] - if rwctxs == nil { - rwctxs = newRemoteWriteCtxs(at, *remoteWriteMultitenantURLs) - rwctxsMap[tenantID] = rwctxs - } - rwctxsMapLock.Unlock() } - rowsCount := getRowsCount(tss) // Quick check whether writes to configured remote storage systems are blocked. @@ -552,14 +491,14 @@ func tryPush(at *auth.Token, wr *prompbmarshal.WriteRequest, forceDropSamplesOnF } sortLabelsIfNeeded(tssBlock) tssBlock = limitSeriesCardinality(tssBlock) - if !tryPushBlockToRemoteStorages(rwctxs, tssBlock, forceDropSamplesOnFailure) { + if !tryPushBlockToRemoteStorages(tssBlock, forceDropSamplesOnFailure) { return false } } return true } -func tryPushBlockToRemoteStorages(rwctxs []*remoteWriteCtx, tssBlock []prompbmarshal.TimeSeries, forceDropSamplesOnFailure bool) bool { +func tryPushBlockToRemoteStorages(tssBlock []prompbmarshal.TimeSeries, forceDropSamplesOnFailure bool) bool { if len(tssBlock) == 0 { // Nothing to push return true @@ -578,7 +517,7 @@ func tryPushBlockToRemoteStorages(rwctxs []*remoteWriteCtx, tssBlock []prompbmar if replicas <= 0 { replicas = 1 } - return tryShardingBlockAmongRemoteStorages(rwctxs, tssBlock, replicas, forceDropSamplesOnFailure) + return tryShardingBlockAmongRemoteStorages(tssBlock, replicas, forceDropSamplesOnFailure) } // Replicate tssBlock samples among rwctxs. @@ -599,7 +538,7 @@ func tryPushBlockToRemoteStorages(rwctxs []*remoteWriteCtx, tssBlock []prompbmar return !anyPushFailed.Load() } -func tryShardingBlockAmongRemoteStorages(rwctxs []*remoteWriteCtx, tssBlock []prompbmarshal.TimeSeries, replicas int, forceDropSamplesOnFailure bool) bool { +func tryShardingBlockAmongRemoteStorages(tssBlock []prompbmarshal.TimeSeries, replicas int, forceDropSamplesOnFailure bool) bool { x := getTSSShards(len(rwctxs)) defer putTSSShards(x) diff --git a/app/vmui/packages/vmui/src/components/ExploreAnomaly/AnomalyConfig.tsx b/app/vmui/packages/vmui/src/components/ExploreAnomaly/AnomalyConfig.tsx new file mode 100644 index 000000000..ca9a90fc5 --- /dev/null +++ b/app/vmui/packages/vmui/src/components/ExploreAnomaly/AnomalyConfig.tsx @@ -0,0 +1,120 @@ +import React, { FC, useState } from "preact/compat"; +import Button from "../Main/Button/Button"; +import TextField from "../Main/TextField/TextField"; +import Modal from "../Main/Modal/Modal"; +import Spinner from "../Main/Spinner/Spinner"; +import { DownloadIcon, ErrorIcon } from "../Main/Icons"; +import useBoolean from "../../hooks/useBoolean"; +import useDeviceDetect from "../../hooks/useDeviceDetect"; +import { useAppState } from "../../state/common/StateContext"; +import classNames from "classnames"; +import "./style.scss"; + +const AnomalyConfig: FC = () => { + const { serverUrl } = useAppState(); + const { isMobile } = useDeviceDetect(); + + const { + value: isModalOpen, + setTrue: setOpenModal, + setFalse: setCloseModal, + } = useBoolean(false); + + const [isLoading, setIsLoading] = useState(false); + const [textConfig, setTextConfig] = useState(""); + const [downloadUrl, setDownloadUrl] = useState(""); + const [error, setError] = useState(""); + + const fetchConfig = async () => { + setIsLoading(true); + try { + const url = `${serverUrl}/api/vmanomaly/config.yaml`; + const response = await fetch(url); + if (!response.ok) { + setError(` ${response.status} ${response.statusText}`); + } else { + const blob = await response.blob(); + const yamlAsString = await blob.text(); + setTextConfig(yamlAsString); + setDownloadUrl(URL.createObjectURL(blob)); + } + } catch (error) { + console.error(error); + setError(String(error)); + } + setIsLoading(false); + }; + + const handleOpenModal = () => { + setOpenModal(); + setError(""); + URL.revokeObjectURL(downloadUrl); + setTextConfig(""); + setDownloadUrl(""); + fetchConfig(); + }; + + return ( + <> + + {isModalOpen && ( + +
+ {isLoading && ( + + )} + {!isLoading && error && ( +
+
+

Cannot download config

+

{error}

+
+ )} + {!isLoading && textConfig && ( + + )} +
+ {downloadUrl && ( + + + + )} +
+
+
+ )} + + ); +}; + +export default AnomalyConfig; diff --git a/app/vmui/packages/vmui/src/components/ExploreAnomaly/style.scss b/app/vmui/packages/vmui/src/components/ExploreAnomaly/style.scss new file mode 100644 index 000000000..72a43e314 --- /dev/null +++ b/app/vmui/packages/vmui/src/components/ExploreAnomaly/style.scss @@ -0,0 +1,61 @@ +@use "src/styles/variables" as *; + +.vm-anomaly-config { + display: grid; + grid-template-rows: calc(($vh * 70) - 78px - ($padding-medium*3)) auto; + gap: $padding-global; + min-width: 400px; + max-width: 80vw; + min-height: 300px; + + &_mobile { + width: 100%; + max-width: none; + min-height: 100%; + grid-template-rows: calc(($vh * 100) - 78px - ($padding-global*3)) auto; + } + + textarea { + overflow: auto; + width: 100%; + height: 100%; + max-height: 900px; + } + + &-error { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + width: 100%; + gap: $padding-small; + text-align: center; + + &__icon { + display: flex; + align-items: center; + justify-content: center; + width: 30px; + height: 30px; + margin-bottom: $padding-small; + color: $color-error; + } + + &__title { + font-size: $font-size-medium; + font-weight: bold; + } + + &__text { + max-width: 700px; + line-height: 1.3; + } + } + + &-footer { + display: flex; + align-items: center; + justify-content: flex-end; + gap: $padding-small; + } +} diff --git a/app/vmui/packages/vmui/src/components/Main/Spinner/Spinner.tsx b/app/vmui/packages/vmui/src/components/Main/Spinner/Spinner.tsx index 05e997e36..e19549a17 100644 --- a/app/vmui/packages/vmui/src/components/Main/Spinner/Spinner.tsx +++ b/app/vmui/packages/vmui/src/components/Main/Spinner/Spinner.tsx @@ -1,4 +1,5 @@ -import React, { CSSProperties, FC } from "preact/compat"; +import React, { FC } from "preact/compat"; +import { CSSProperties } from "react"; import "./style.scss"; import classNames from "classnames"; import { useAppState } from "../../../state/common/StateContext"; @@ -8,7 +9,7 @@ interface SpinnerProps { message?: string } -const Spinner: FC = ({ containerStyles = {}, message }) => { +const Spinner: FC = ({ containerStyles, message }) => { const { isDarkTheme } = useAppState(); return ( @@ -17,7 +18,7 @@ const Spinner: FC = ({ containerStyles = {}, message }) => { "vm-spinner": true, "vm-spinner_dark": isDarkTheme, })} - style={containerStyles && {}} + style={containerStyles} >
diff --git a/app/vmui/packages/vmui/src/pages/CustomPanel/QueryConfigurator/QueryConfigurator.tsx b/app/vmui/packages/vmui/src/pages/CustomPanel/QueryConfigurator/QueryConfigurator.tsx index bd687041e..6ca90a921 100644 --- a/app/vmui/packages/vmui/src/pages/CustomPanel/QueryConfigurator/QueryConfigurator.tsx +++ b/app/vmui/packages/vmui/src/pages/CustomPanel/QueryConfigurator/QueryConfigurator.tsx @@ -24,6 +24,7 @@ import useDeviceDetect from "../../../hooks/useDeviceDetect"; import { QueryStats } from "../../../api/types"; import { usePrettifyQuery } from "./hooks/usePrettifyQuery"; import QueryHistory from "../QueryHistory/QueryHistory"; +import AnomalyConfig from "../../../components/ExploreAnomaly/AnomalyConfig"; export interface QueryConfiguratorProps { queryErrors: string[]; @@ -37,6 +38,7 @@ export interface QueryConfiguratorProps { prettify?: boolean; autocomplete?: boolean; traceQuery?: boolean; + anomalyConfig?: boolean; } } @@ -253,6 +255,7 @@ const QueryConfigurator: FC = ({
+ {hideButtons?.anomalyConfig && } {!hideButtons?.addQuery && stateQuery.length < MAX_QUERY_FIELDS && (