Compare commits
2 Commits
v1.125.0
...
configwatc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d0625bb77d | ||
|
|
4163f18250 |
@@ -10,6 +10,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/configwatcher"
|
||||
"github.com/VictoriaMetrics/metrics"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmagent/csvimport"
|
||||
@@ -112,6 +113,7 @@ func main() {
|
||||
flag.Usage = usage
|
||||
envflag.Parse()
|
||||
remotewrite.InitSecretFlags()
|
||||
configwatcher.Init()
|
||||
buildinfo.Init()
|
||||
logger.Init()
|
||||
timeserieslimits.Init(*maxLabelsPerTimeseries, *maxLabelNameLen, *maxLabelValueLen)
|
||||
@@ -199,6 +201,7 @@ func main() {
|
||||
}
|
||||
protoparserutil.StopUnmarshalWorkers()
|
||||
remotewrite.Stop()
|
||||
configwatcher.Stop()
|
||||
|
||||
logger.Infof("successfully stopped vmagent in %.3f seconds", time.Since(startTime).Seconds())
|
||||
}
|
||||
|
||||
@@ -7,8 +7,8 @@ import (
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmagent/common"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmagent/remotewrite"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/auth"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/prommetadata"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/prompb"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promscrape"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/protoparser/opentelemetry/firehose"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/protoparser/opentelemetry/stream"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/protoparser/protoparserutil"
|
||||
@@ -68,7 +68,7 @@ func insertRows(at *auth.Token, tss []prompb.TimeSeries, mms []prompb.MetricMeta
|
||||
ctx.WriteRequest.Timeseries = tssDst
|
||||
|
||||
var metadataTotal int
|
||||
if prommetadata.IsEnabled() {
|
||||
if promscrape.IsMetadataEnabled() {
|
||||
var accountID, projectID uint32
|
||||
if at != nil {
|
||||
accountID = at.AccountID
|
||||
|
||||
@@ -7,8 +7,8 @@ import (
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmagent/remotewrite"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/auth"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/httpserver"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/prommetadata"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/prompb"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promscrape"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/protoparser/prometheus"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/protoparser/prometheus/stream"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/protoparser/protoparserutil"
|
||||
@@ -36,7 +36,7 @@ func InsertHandler(at *auth.Token, req *http.Request) error {
|
||||
return err
|
||||
}
|
||||
encoding := req.Header.Get("Content-Encoding")
|
||||
return stream.Parse(req.Body, defaultTimestamp, encoding, true, prommetadata.IsEnabled(), func(rows []prometheus.Row, mms []prometheus.Metadata) error {
|
||||
return stream.Parse(req.Body, defaultTimestamp, encoding, true, promscrape.IsMetadataEnabled(), func(rows []prometheus.Row, mms []prometheus.Metadata) error {
|
||||
return insertRows(at, rows, mms, extraLabels)
|
||||
}, func(s string) {
|
||||
httpserver.LogError(req, s)
|
||||
|
||||
@@ -6,8 +6,8 @@ import (
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmagent/common"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmagent/remotewrite"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/auth"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/prommetadata"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/prompb"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promscrape"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/protoparser/promremotewrite/stream"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/protoparser/protoparserutil"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/tenantmetrics"
|
||||
@@ -71,7 +71,7 @@ func insertRows(at *auth.Token, timeseries []prompb.TimeSeries, mms []prompb.Met
|
||||
ctx.WriteRequest.Timeseries = tssDst
|
||||
|
||||
var metadataTotal int
|
||||
if prommetadata.IsEnabled() {
|
||||
if promscrape.IsMetadataEnabled() {
|
||||
var accountID, projectID uint32
|
||||
if at != nil {
|
||||
accountID = at.AccountID
|
||||
|
||||
@@ -6,8 +6,8 @@ import (
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vminsert/common"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vminsert/relabel"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/httpserver"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/prommetadata"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/prompb"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promscrape"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/protoparser/prometheus"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/protoparser/prometheus/stream"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/protoparser/protoparserutil"
|
||||
@@ -30,7 +30,7 @@ func InsertHandler(req *http.Request) error {
|
||||
return err
|
||||
}
|
||||
encoding := req.Header.Get("Content-Encoding")
|
||||
return stream.Parse(req.Body, defaultTimestamp, encoding, true, prommetadata.IsEnabled(), func(rows []prometheus.Row, _ []prometheus.Metadata) error {
|
||||
return stream.Parse(req.Body, defaultTimestamp, encoding, true, promscrape.IsMetadataEnabled(), func(rows []prometheus.Row, _ []prometheus.Metadata) error {
|
||||
return insertRows(rows, extraLabels)
|
||||
}, func(s string) {
|
||||
httpserver.LogError(req, s)
|
||||
|
||||
@@ -142,12 +142,6 @@ func (s *series) summarize(aggrFunc aggrFunc, startTime, endTime, step int64, xF
|
||||
}
|
||||
|
||||
func execExpr(ec *evalConfig, query string) (nextSeriesFunc, error) {
|
||||
// Validate query length to prevent memory exhaustion
|
||||
maxLen := searchutil.GetMaxQueryLen()
|
||||
if len(query) > maxLen {
|
||||
return nil, fmt.Errorf("too long query; got %d bytes; mustn't exceed `-search.maxQueryLen=%d` bytes", len(query), maxLen)
|
||||
}
|
||||
|
||||
expr, err := graphiteql.Parse(query)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot parse %q: %w", query, err)
|
||||
|
||||
@@ -4070,9 +4070,6 @@ func TestExecExprFailure(t *testing.T) {
|
||||
|
||||
f(`holtWintersConfidenceArea(group(time("foo.baz",15),time("foo.baz",15)))`)
|
||||
f(`holtWintersConfidenceArea()`)
|
||||
|
||||
// too long query
|
||||
f(`sumSeries(` + strings.Repeat("metric.very.long.name.that.takes.space,", 500) + `metric.final)`)
|
||||
}
|
||||
|
||||
func compareSeries(ss, ssExpected []*series, expr graphiteql.Expr) error {
|
||||
|
||||
@@ -2,7 +2,6 @@ package vmselect
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"net/http"
|
||||
@@ -68,7 +67,6 @@ func Init() {
|
||||
prometheus.InitMaxUniqueTimeseries(*maxConcurrentRequests)
|
||||
|
||||
concurrencyLimitCh = make(chan struct{}, *maxConcurrentRequests)
|
||||
initVMUIConfig()
|
||||
initVMAlertProxy()
|
||||
}
|
||||
|
||||
@@ -462,11 +460,6 @@ func handleStaticAndSimpleRequests(w http.ResponseWriter, r *http.Request, path
|
||||
return true
|
||||
}
|
||||
if strings.HasPrefix(path, "/vmui/") {
|
||||
if path == "/vmui/config.json" {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
fmt.Fprint(w, vmuiConfig)
|
||||
return true
|
||||
}
|
||||
if strings.HasPrefix(path, "/vmui/static/") {
|
||||
// Allow clients caching static contents for long period of time, since it shouldn't change over time.
|
||||
// Path to static contents (such as js and css) must be changed whenever its contents is changed.
|
||||
@@ -741,34 +734,8 @@ func proxyVMAlertRequests(w http.ResponseWriter, r *http.Request) {
|
||||
var (
|
||||
vmalertProxyHost string
|
||||
vmalertProxy *nethttputil.ReverseProxy
|
||||
vmuiConfig string
|
||||
)
|
||||
|
||||
func initVMUIConfig() {
|
||||
var cfg struct {
|
||||
License struct {
|
||||
Type string `json:"type"`
|
||||
} `json:"license"`
|
||||
VMAlert struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
} `json:"vmalert"`
|
||||
}
|
||||
data, err := vmuiFiles.ReadFile("vmui/config.json")
|
||||
if err != nil {
|
||||
logger.Fatalf("cannot read vmui default config: %s", err)
|
||||
}
|
||||
err = json.Unmarshal(data, &cfg)
|
||||
if err != nil {
|
||||
logger.Fatalf("cannot parse vmui default config: %s", err)
|
||||
}
|
||||
cfg.VMAlert.Enabled = len(*vmalertProxyURL) != 0
|
||||
data, err = json.Marshal(&cfg)
|
||||
if err != nil {
|
||||
logger.Fatalf("cannot create vmui config: %s", err)
|
||||
}
|
||||
vmuiConfig = string(data)
|
||||
}
|
||||
|
||||
// initVMAlertProxy must be called after flag.Parse(), since it uses command-line flags.
|
||||
func initVMAlertProxy() {
|
||||
if len(*vmalertProxyURL) == 0 {
|
||||
|
||||
@@ -24,6 +24,7 @@ import (
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/bytesutil"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/encoding"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/fasttime"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/flagutil"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/httpserver"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/httputil"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
|
||||
@@ -37,6 +38,7 @@ var (
|
||||
latencyOffset = flag.Duration("search.latencyOffset", time.Second*30, "The time when data points become visible in query results after the collection. "+
|
||||
"It can be overridden on per-query basis via latency_offset arg. "+
|
||||
"Too small value can result in incomplete last points for query results")
|
||||
maxQueryLen = flagutil.NewBytes("search.maxQueryLen", 16*1024, "The maximum search query length in bytes")
|
||||
maxLookback = flag.Duration("search.maxLookback", 0, "Synonym to -query.lookback-delta from Prometheus. "+
|
||||
"The value is dynamically detected from interval between time series datapoints if not set. It can be overridden on per-query basis via max_lookback arg. "+
|
||||
"See also '-search.maxStalenessInterval' flag, which has the same meaning due to historical reasons")
|
||||
@@ -731,9 +733,8 @@ func QueryHandler(qt *querytracer.Tracer, startTime time.Time, w http.ResponseWr
|
||||
step = defaultStep
|
||||
}
|
||||
|
||||
maxLen := searchutil.GetMaxQueryLen()
|
||||
if len(query) > maxLen {
|
||||
return fmt.Errorf("too long query; got %d bytes; mustn't exceed `-search.maxQueryLen=%d` bytes", len(query), maxLen)
|
||||
if len(query) > maxQueryLen.IntN() {
|
||||
return fmt.Errorf("too long query; got %d bytes; mustn't exceed `-search.maxQueryLen=%d` bytes", len(query), maxQueryLen.N)
|
||||
}
|
||||
etfs, err := searchutil.GetExtraTagFilters(r)
|
||||
if err != nil {
|
||||
@@ -903,9 +904,8 @@ func queryRangeHandler(qt *querytracer.Tracer, startTime time.Time, w http.Respo
|
||||
}
|
||||
|
||||
// Validate input args.
|
||||
maxLen := searchutil.GetMaxQueryLen()
|
||||
if len(query) > maxLen {
|
||||
return fmt.Errorf("too long query; got %d bytes; mustn't exceed `-search.maxQueryLen=%d` bytes", len(query), maxLen)
|
||||
if len(query) > maxQueryLen.IntN() {
|
||||
return fmt.Errorf("too long query; got %d bytes; mustn't exceed `-search.maxQueryLen=%d` bytes", len(query), maxQueryLen.N)
|
||||
}
|
||||
if start > end {
|
||||
end = start + defaultStep
|
||||
|
||||
@@ -7,12 +7,10 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/VictoriaMetrics/metricsql"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/fasttime"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/flagutil"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/httputil"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/storage"
|
||||
"github.com/VictoriaMetrics/metricsql"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -22,7 +20,6 @@ var (
|
||||
maxStatusRequestDuration = flag.Duration("search.maxStatusRequestDuration", time.Minute*5, "The maximum duration for /api/v1/status/* requests")
|
||||
maxLabelsAPIDuration = flag.Duration("search.maxLabelsAPIDuration", time.Second*5, "The maximum duration for /api/v1/labels, /api/v1/label/.../values and /api/v1/series requests. "+
|
||||
"See also -search.maxLabelsAPISeries and -search.ignoreExtraFiltersAtLabelsAPI")
|
||||
maxQueryLen = flagutil.NewBytes("search.maxQueryLen", 16*1024, "The maximum search query length in bytes")
|
||||
)
|
||||
|
||||
// GetMaxQueryDuration returns the maximum duration for query from r.
|
||||
@@ -230,8 +227,3 @@ func toTagFilter(dst *storage.TagFilter, src *metricsql.LabelFilter) {
|
||||
dst.IsRegexp = src.IsRegexp
|
||||
dst.IsNegative = src.IsNegative
|
||||
}
|
||||
|
||||
// GetMaxQueryLen returns the current value of the search.maxQueryLen flag.
|
||||
func GetMaxQueryLen() int {
|
||||
return maxQueryLen.IntN()
|
||||
}
|
||||
|
||||
1
app/vmselect/vmui/assets/index-B7vIex3g.css
Normal file
205
app/vmselect/vmui/assets/index-SqjehVXD.js
Normal file
@@ -36,10 +36,10 @@
|
||||
<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-DY3sj68d.js"></script>
|
||||
<script type="module" crossorigin src="./assets/index-SqjehVXD.js"></script>
|
||||
<link rel="modulepreload" crossorigin href="./assets/vendor-DBOs1yKE.js">
|
||||
<link rel="stylesheet" crossorigin href="./assets/vendor-D1GxaB_c.css">
|
||||
<link rel="stylesheet" crossorigin href="./assets/index-XlRqIMog.css">
|
||||
<link rel="stylesheet" crossorigin href="./assets/index-B7vIex3g.css">
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
|
||||
@@ -683,7 +683,7 @@ func writeStorageMetrics(w io.Writer, strg *storage.Storage) {
|
||||
metrics.WriteCounterUint64(w, `vm_cache_eviction_bytes_total{type="storage/metricIDs", reason="miss_percentage"}`, m.MetricIDCacheMissEvictionBytes)
|
||||
metrics.WriteCounterUint64(w, `vm_cache_eviction_bytes_total{type="storage/metricIDs", reason="expiration"}`, m.MetricIDCacheExpireEvictionBytes)
|
||||
|
||||
metrics.WriteCounterUint64(w, `vm_deleted_metrics_total{type="indexdb"}`, m.DeletedMetricsCount)
|
||||
metrics.WriteCounterUint64(w, `vm_deleted_metrics_total{type="indexdb"}`, idbm.DeletedMetricsCount)
|
||||
|
||||
metrics.WriteCounterUint64(w, `vm_cache_collisions_total{type="storage/tsid"}`, m.TSIDCacheCollisions)
|
||||
metrics.WriteCounterUint64(w, `vm_cache_collisions_total{type="storage/metricName"}`, m.MetricNameCacheCollisions)
|
||||
|
||||
@@ -6,3 +6,12 @@ export enum AppType {
|
||||
export const APP_TYPE = import.meta.env.VITE_APP_TYPE;
|
||||
export const APP_TYPE_VM = APP_TYPE === AppType.victoriametrics;
|
||||
export const APP_TYPE_ANOMALY = APP_TYPE === AppType.vmanomaly;
|
||||
|
||||
export const isDefaultDatasourceType = (datasourceType: string): boolean => {
|
||||
switch (APP_TYPE) {
|
||||
case AppType.victoriametrics:
|
||||
return datasourceType == "prometheus" || datasourceType == "";
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useEffect, useState } from "preact/compat";
|
||||
import { ErrorTypes } from "../types";
|
||||
import { APP_TYPE_VM } from "../constants/appType";
|
||||
|
||||
const useFetchAppConfig = () => {
|
||||
const useFetchFlags = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
@@ -31,5 +31,5 @@ const useFetchAppConfig = () => {
|
||||
return { isLoading, error };
|
||||
};
|
||||
|
||||
export default useFetchAppConfig;
|
||||
export default useFetchFlags;
|
||||
|
||||
|
||||
45
app/vmui/packages/vmui/src/hooks/useFetchFlags.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { useAppDispatch, useAppState } from "../state/common/StateContext";
|
||||
import { useEffect, useState } from "preact/compat";
|
||||
import { ErrorTypes } from "../types";
|
||||
import { APP_TYPE_VM } from "../constants/appType";
|
||||
import { getUrlWithoutTenant } from "../utils/tenants";
|
||||
|
||||
const useFetchFlags = () => {
|
||||
const { serverUrl } = useAppState();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<ErrorTypes | string>("");
|
||||
|
||||
useEffect(() => {
|
||||
const fetchFlags = async () => {
|
||||
if (!serverUrl || !APP_TYPE_VM) return;
|
||||
setError("");
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
const url = getUrlWithoutTenant(serverUrl);
|
||||
const response = await fetch(`${url}/flags`);
|
||||
const data = await response.text();
|
||||
const flags = data.split("\n").filter(flag => flag.trim() !== "")
|
||||
.reduce((acc, flag) => {
|
||||
const [keyRaw, valueRaw] = flag.split("=");
|
||||
const key = keyRaw.trim().replace(/^-/, "");
|
||||
acc[key.trim()] = valueRaw ? valueRaw.trim().replace(/^"(.*)"$/, "$1") : null;
|
||||
return acc;
|
||||
}, {} as Record<string, string|null>);
|
||||
dispatch({ type: "SET_FLAGS", payload: flags });
|
||||
} catch (e) {
|
||||
setIsLoading(false);
|
||||
if (e instanceof Error) setError(`${e.name}: ${e.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
fetchFlags();
|
||||
}, [serverUrl]);
|
||||
|
||||
return { isLoading, error };
|
||||
};
|
||||
|
||||
export default useFetchFlags;
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import Header from "../Header/Header";
|
||||
import { FC, useEffect } from "preact/compat";
|
||||
import { Outlet, useSearchParams } from "react-router-dom";
|
||||
import { Outlet, useLocation, useSearchParams } from "react-router-dom";
|
||||
import qs from "qs";
|
||||
import "../MainLayout/style.scss";
|
||||
import { getAppModeEnable } from "../../utils/app-mode";
|
||||
import classNames from "classnames";
|
||||
import Footer from "../Footer/Footer";
|
||||
import { routerOptions } from "../../router";
|
||||
import useFetchDefaultTimezone from "../../hooks/useFetchDefaultTimezone";
|
||||
import useDeviceDetect from "../../hooks/useDeviceDetect";
|
||||
import ControlsAnomalyLayout from "./ControlsAnomalyLayout";
|
||||
@@ -13,10 +14,17 @@ import ControlsAnomalyLayout from "./ControlsAnomalyLayout";
|
||||
const AnomalyLayout: FC = () => {
|
||||
const appModeEnable = getAppModeEnable();
|
||||
const { isMobile } = useDeviceDetect();
|
||||
const { pathname } = useLocation();
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
|
||||
useFetchDefaultTimezone();
|
||||
|
||||
const setDocumentTitle = () => {
|
||||
const defaultTitle = "vmui for vmanomaly";
|
||||
const routeTitle = routerOptions[pathname]?.title;
|
||||
document.title = routeTitle ? `${routeTitle} - ${defaultTitle}` : defaultTitle;
|
||||
};
|
||||
|
||||
// for support old links with search params
|
||||
const redirectSearchToHashParams = () => {
|
||||
const { search, href } = window.location;
|
||||
@@ -30,6 +38,7 @@ const AnomalyLayout: FC = () => {
|
||||
if (newHref !== href) window.location.replace(newHref);
|
||||
};
|
||||
|
||||
useEffect(setDocumentTitle, [pathname]);
|
||||
useEffect(redirectSearchToHashParams, []);
|
||||
|
||||
return <section className="vm-container">
|
||||
|
||||
@@ -11,6 +11,7 @@ import { useFetchDashboards } from "../../pages/PredefinedPanels/hooks/useFetchD
|
||||
import useDeviceDetect from "../../hooks/useDeviceDetect";
|
||||
import ControlsMainLayout from "./ControlsMainLayout";
|
||||
import useFetchDefaultTimezone from "../../hooks/useFetchDefaultTimezone";
|
||||
import useFetchFlags from "../../hooks/useFetchFlags";
|
||||
import useFetchAppConfig from "../../hooks/useFetchAppConfig";
|
||||
|
||||
const MainLayout: FC = () => {
|
||||
@@ -22,6 +23,7 @@ const MainLayout: FC = () => {
|
||||
useFetchDashboards();
|
||||
useFetchDefaultTimezone();
|
||||
useFetchAppConfig();
|
||||
useFetchFlags();
|
||||
|
||||
const setDocumentTitle = () => {
|
||||
const defaultTitle = "vmui";
|
||||
|
||||
@@ -54,7 +54,7 @@
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
align-items: flex-start;
|
||||
gap: $padding-medium;
|
||||
max-width: calc(100vw - var(--scrollbar-width));
|
||||
|
||||
@@ -67,7 +67,7 @@
|
||||
width: 100%;
|
||||
font-size: 12px;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
align-items: flex-start;
|
||||
gap: $padding-medium;
|
||||
|
||||
@media (max-width: 500px) {
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { APP_TYPE, AppType } from "../constants/appType";
|
||||
|
||||
const router = {
|
||||
home: "/",
|
||||
metrics: "/metrics",
|
||||
@@ -17,6 +15,7 @@ const router = {
|
||||
rawQuery: "/raw-query",
|
||||
downsamplingDebug: "/downsampling-filters-debug",
|
||||
retentionDebug: "/retention-filters-debug",
|
||||
alerts: "/alerts",
|
||||
rules: "/rules",
|
||||
notifiers: "/notifiers",
|
||||
};
|
||||
@@ -52,23 +51,11 @@ const routerOptionsDefault = {
|
||||
},
|
||||
};
|
||||
|
||||
const getDefaultOptions = (appType: AppType) => {
|
||||
switch (appType) {
|
||||
case AppType.vmanomaly:
|
||||
return {
|
||||
title: "Anomaly exploration",
|
||||
...routerOptionsDefault,
|
||||
};
|
||||
default:
|
||||
return {
|
||||
title: "Query",
|
||||
...routerOptionsDefault,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export const routerOptions: { [key: string]: RouterOptions } = {
|
||||
[router.home]: getDefaultOptions(APP_TYPE),
|
||||
[router.home]: {
|
||||
title: "Query",
|
||||
...routerOptionsDefault,
|
||||
},
|
||||
[router.rawQuery]: {
|
||||
title: "Raw query",
|
||||
...routerOptionsDefault,
|
||||
@@ -140,7 +127,10 @@ export const routerOptions: { [key: string]: RouterOptions } = {
|
||||
title: "Icons",
|
||||
header: {},
|
||||
},
|
||||
[router.anomaly]: getDefaultOptions(AppType.vmanomaly),
|
||||
[router.anomaly]: {
|
||||
title: "Anomaly exploration",
|
||||
...routerOptionsDefault,
|
||||
},
|
||||
[router.query]: {
|
||||
title: "Query",
|
||||
...routerOptionsDefault,
|
||||
|
||||
@@ -17,7 +17,7 @@ interface NavigationConfig {
|
||||
serverUrl: string,
|
||||
isEnterpriseLicense: boolean,
|
||||
showPredefinedDashboards: boolean,
|
||||
showAlerting: boolean,
|
||||
showAlertLink: boolean,
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -43,7 +43,7 @@ const getExploreNav = () => [
|
||||
];
|
||||
|
||||
/**
|
||||
* Submenu for Alerting tab
|
||||
* Submenu for Alerts tab
|
||||
*/
|
||||
|
||||
const getAlertingNav = () => [
|
||||
@@ -57,14 +57,14 @@ const getAlertingNav = () => [
|
||||
export const getDefaultNavigation = ({
|
||||
isEnterpriseLicense,
|
||||
showPredefinedDashboards,
|
||||
showAlerting,
|
||||
showAlertLink,
|
||||
}: NavigationConfig): NavigationItem[] => [
|
||||
{ value: router.home },
|
||||
{ value: router.rawQuery },
|
||||
{ label: "Explore", submenu: getExploreNav() },
|
||||
{ label: "Tools", submenu: getToolsNav(isEnterpriseLicense) },
|
||||
{ value: router.dashboards, hide: !showPredefinedDashboards },
|
||||
{ value: "Alerting", submenu: getAlertingNav(), hide: !showAlerting },
|
||||
{ value: "Alerting", submenu: getAlertingNav(), hide: !showAlertLink },
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
@@ -9,17 +9,17 @@ import { APP_TYPE, AppType } from "../constants/appType";
|
||||
const useNavigationMenu = () => {
|
||||
const appModeEnable = getAppModeEnable();
|
||||
const { dashboardsSettings } = useDashboardsState();
|
||||
const { serverUrl, appConfig } = useAppState();
|
||||
const { serverUrl, flags, appConfig } = useAppState();
|
||||
const isEnterpriseLicense = appConfig.license?.type === "enterprise";
|
||||
const showAlerting = appConfig?.vmalert?.enabled || false;
|
||||
const showAlertLink = Boolean(flags["vmalert.proxyURL"]);
|
||||
const showPredefinedDashboards = Boolean(!appModeEnable && dashboardsSettings.length);
|
||||
|
||||
const navigationConfig = useMemo(() => ({
|
||||
serverUrl,
|
||||
isEnterpriseLicense,
|
||||
showAlerting,
|
||||
showAlertLink,
|
||||
showPredefinedDashboards
|
||||
}), [serverUrl, isEnterpriseLicense, showAlerting, showPredefinedDashboards]);
|
||||
}), [serverUrl, isEnterpriseLicense, showAlertLink, showPredefinedDashboards]);
|
||||
|
||||
|
||||
const menu = useMemo(() => {
|
||||
|
||||
@@ -10,6 +10,7 @@ export interface AppState {
|
||||
tenantId: string;
|
||||
theme: Theme;
|
||||
isDarkTheme: boolean | null;
|
||||
flags: Record<string, string | null>;
|
||||
appConfig: AppConfig
|
||||
}
|
||||
|
||||
@@ -17,6 +18,7 @@ export type Action =
|
||||
| { type: "SET_SERVER", payload: string }
|
||||
| { type: "SET_THEME", payload: Theme }
|
||||
| { type: "SET_TENANT_ID", payload: string }
|
||||
| { type: "SET_FLAGS", payload: Record<string, string | null> }
|
||||
| { type: "SET_APP_CONFIG", payload: AppConfig }
|
||||
| { type: "SET_DARK_THEME" }
|
||||
|
||||
@@ -27,6 +29,7 @@ export const initialState: AppState = {
|
||||
tenantId,
|
||||
theme: (getFromStorage("THEME") || Theme.system) as Theme,
|
||||
isDarkTheme: null,
|
||||
flags: {},
|
||||
appConfig: {}
|
||||
};
|
||||
|
||||
@@ -53,6 +56,11 @@ export function reducer(state: AppState, action: Action): AppState {
|
||||
...state,
|
||||
isDarkTheme: isDarkTheme(state.theme)
|
||||
};
|
||||
case "SET_FLAGS":
|
||||
return {
|
||||
...state,
|
||||
flags: action.payload
|
||||
};
|
||||
case "SET_APP_CONFIG":
|
||||
return {
|
||||
...state,
|
||||
|
||||
@@ -180,9 +180,6 @@ export interface AppConfig {
|
||||
license?: {
|
||||
type?: "enterprise" | "opensource";
|
||||
};
|
||||
vmalert?: {
|
||||
enabled: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export interface Group {
|
||||
|
||||
@@ -7,8 +7,7 @@ ROOT_IMAGE ?= alpine:3.22.1
|
||||
ROOT_IMAGE_SCRATCH ?= scratch
|
||||
CERTS_IMAGE := alpine:3.22.1
|
||||
|
||||
GO_BUILDER_IMAGE := golang:1.25.0
|
||||
|
||||
GO_BUILDER_IMAGE := golang:1.25.0-alpine
|
||||
BUILDER_IMAGE := local/builder:2.0.0-$(shell echo $(GO_BUILDER_IMAGE) | tr :/ __)-1
|
||||
BASE_IMAGE := local/base:1.1.4-$(shell echo $(ROOT_IMAGE) | tr :/ __)-$(shell echo $(CERTS_IMAGE) | tr :/ __)
|
||||
DOCKER ?= docker
|
||||
@@ -44,7 +43,7 @@ app-via-docker: package-builder
|
||||
$(BUILDER_IMAGE) \
|
||||
go build $(RACE) -trimpath -buildvcs=false \
|
||||
-ldflags "-extldflags '-static' $(GO_BUILDINFO)" \
|
||||
-tags 'netgo osusergo $(EXTRA_GO_BUILD_TAGS)' \
|
||||
-tags 'netgo osusergo musl $(EXTRA_GO_BUILD_TAGS)' \
|
||||
-o bin/$(APP_NAME)$(APP_SUFFIX)-prod $(PKG_PREFIX)/app/$(APP_NAME)
|
||||
|
||||
app-via-docker-windows: package-builder
|
||||
@@ -159,11 +158,11 @@ app-via-docker-pure:
|
||||
APP_SUFFIX='-pure' DOCKER_OPTS='--env CGO_ENABLED=0' $(MAKE) app-via-docker
|
||||
|
||||
app-via-docker-linux-amd64:
|
||||
EXTRA_DOCKER_ENVS='CC=x86_64-linux-gnu-gcc' \
|
||||
EXTRA_DOCKER_ENVS='CC=/opt/cross-builder/x86_64-linux-musl-cross/bin/x86_64-linux-musl-gcc' \
|
||||
CGO_ENABLED=1 GOOS=linux GOARCH=amd64 $(MAKE) app-via-docker-goos-goarch
|
||||
|
||||
app-via-docker-linux-arm64:
|
||||
EXTRA_DOCKER_ENVS='CC=aarch64-linux-gnu-gcc' \
|
||||
EXTRA_DOCKER_ENVS='CC=/opt/cross-builder/aarch64-linux-musl-cross/bin/aarch64-linux-musl-gcc' \
|
||||
CGO_ENABLED=1 GOOS=linux GOARCH=arm64 $(MAKE) app-via-docker-goos-goarch
|
||||
|
||||
app-via-docker-linux-arm:
|
||||
@@ -202,11 +201,11 @@ package-via-docker-pure:
|
||||
APP_SUFFIX='-pure' DOCKER_OPTS='--env CGO_ENABLED=0' $(MAKE) package-via-docker
|
||||
|
||||
package-via-docker-amd64:
|
||||
EXTRA_DOCKER_ENVS='CC=x86_64-linux-gnu-gcc' \
|
||||
EXTRA_DOCKER_ENVS='CC=/opt/cross-builder/x86_64-linux-musl-cross/bin/x86_64-linux-musl-gcc' \
|
||||
CGO_ENABLED=1 GOARCH=amd64 $(MAKE) package-via-docker-goarch
|
||||
|
||||
package-via-docker-arm64:
|
||||
EXTRA_DOCKER_ENVS='CC=aarch64-linux-gnu-gcc' \
|
||||
EXTRA_DOCKER_ENVS='CC=/opt/cross-builder/aarch64-linux-musl-cross/bin/aarch64-linux-musl-gcc' \
|
||||
CGO_ENABLED=1 GOARCH=arm64 $(MAKE) package-via-docker-goarch
|
||||
|
||||
package-via-docker-arm:
|
||||
|
||||
@@ -40,11 +40,7 @@ The communication scheme between components is the following:
|
||||
and recording rules results back to `vmagent`;
|
||||
* [alertmanager](#alertmanager) is configured to receive notifications from `vmalert`.
|
||||
|
||||
<picture>
|
||||
<source srcset="assets/vm-single-server-dark.png" media="(prefers-color-scheme: dark)">
|
||||
<source srcset="assets/vm-single-server-light.png" media="(prefers-color-scheme: light)">
|
||||
<img src="assets/vm-single-server-light.png" alt="VictoriaMetrics single-server deployment" width="500" >
|
||||
</picture>
|
||||
<img alt="VictoriaMetrics single-server deployment" width="500" src="assets/vm-single-server.png">
|
||||
|
||||
To access Grafana use link [http://localhost:3000](http://localhost:3000).
|
||||
|
||||
@@ -82,11 +78,7 @@ The communication scheme between components is the following:
|
||||
and recording rules to `vmagent`;
|
||||
* [alertmanager](#alertmanager) is configured to receive notifications from `vmalert`.
|
||||
|
||||
<picture>
|
||||
<source srcset="assets/vm-cluster-dark.png" media="(prefers-color-scheme: dark)">
|
||||
<source srcset="assets/vm-cluster-light.png" media="(prefers-color-scheme: light)">
|
||||
<img src="assets/vm-cluster-light.png" alt="VictoriaMetrics cluster deployment" width="500" src="assets/vm-cluster-light.png" >
|
||||
</picture>
|
||||
<img alt="VictoriaMetrics cluster deployment" width="500" src="assets/vm-cluster.png">
|
||||
|
||||
To access Grafana use link [http://localhost:3000](http://localhost:3000).
|
||||
|
||||
|
||||
BIN
deployment/docker/assets/vl-cluster.png
Normal file
|
After Width: | Height: | Size: 114 KiB |
BIN
deployment/docker/assets/vl-single-server.png
Normal file
|
After Width: | Height: | Size: 99 KiB |
|
Before Width: | Height: | Size: 156 KiB |
|
Before Width: | Height: | Size: 154 KiB |
BIN
deployment/docker/assets/vm-cluster.png
Normal file
|
After Width: | Height: | Size: 152 KiB |
|
Before Width: | Height: | Size: 93 KiB |
|
Before Width: | Height: | Size: 92 KiB |
BIN
deployment/docker/assets/vm-single-server.png
Normal file
|
After Width: | Height: | Size: 93 KiB |
@@ -1,5 +1,5 @@
|
||||
# balance load among vminsert and vmselect instances,
|
||||
# see https://docs.victoriametrics.com/victoriametrics/vmauth/#load-balancing.
|
||||
# balance load among vmselects
|
||||
# see https://docs.victoriametrics.com/victoriametrics/vmauth/#load-balancing
|
||||
# Note: if username and password are changes, please update the Grafana datasource configuration
|
||||
# and check other places where these credentials are used.
|
||||
users:
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
ARG go_builder_image=non-existing
|
||||
FROM $go_builder_image
|
||||
STOPSIGNAL SIGINT
|
||||
RUN apt update && apt install -y \
|
||||
gcc-x86-64-linux-gnu \
|
||||
gcc-aarch64-linux-gnu
|
||||
RUN apk add git gcc musl-dev make wget --no-cache && \
|
||||
mkdir /opt/cross-builder && \
|
||||
cd /opt/cross-builder && \
|
||||
for arch in aarch64 x86_64; do \
|
||||
wget \
|
||||
https://github.com/VictoriaMetrics/muslcc-mirror/releases/download/v1.0.0/${arch}-linux-musl-cross.tgz \
|
||||
-O /opt/cross-builder/${arch}-musl.tgz \
|
||||
--no-verbose && \
|
||||
tar zxf ${arch}-musl.tgz -C ./ && \
|
||||
rm /opt/cross-builder/${arch}-musl.tgz; \
|
||||
done
|
||||
|
||||
@@ -14,12 +14,10 @@ services:
|
||||
command:
|
||||
- "--promscrape.config=/etc/prometheus/prometheus.yml"
|
||||
- "--remoteWrite.url=http://vmauth:8427/insert/0/prometheus/api/v1/write"
|
||||
- "--remoteWrite.basicAuth.username=foo"
|
||||
- "--remoteWrite.basicAuth.password=bar"
|
||||
restart: always
|
||||
|
||||
grafana:
|
||||
image: grafana/grafana:12.1.1
|
||||
image: grafana/grafana:12.0.2
|
||||
depends_on:
|
||||
- "vmauth"
|
||||
ports:
|
||||
|
||||
@@ -38,7 +38,7 @@ services:
|
||||
restart: always
|
||||
|
||||
grafana:
|
||||
image: grafana/grafana:12.1.1
|
||||
image: grafana/grafana:12.0.2
|
||||
depends_on:
|
||||
- "victoriametrics"
|
||||
ports:
|
||||
|
||||
@@ -17,8 +17,7 @@ scrape_configs:
|
||||
- job_name: vminsert
|
||||
static_configs:
|
||||
- targets:
|
||||
- vminsert-1:8480
|
||||
- vminsert-2:8480
|
||||
- vminsert:8480
|
||||
- job_name: vmselect
|
||||
static_configs:
|
||||
- targets:
|
||||
|
||||
@@ -27,7 +27,7 @@ services:
|
||||
restart: always
|
||||
|
||||
grafana:
|
||||
image: grafana/grafana:12.1.1
|
||||
image: grafana/grafana:12.0.2
|
||||
depends_on:
|
||||
- "victoriametrics"
|
||||
ports:
|
||||
|
||||
@@ -58,7 +58,7 @@ services:
|
||||
- ./vmsingle/promscrape.yml:/promscrape.yml
|
||||
|
||||
grafana:
|
||||
image: grafana/grafana:12.1.1
|
||||
image: grafana/grafana:12.0.2
|
||||
depends_on: [vmsingle]
|
||||
ports:
|
||||
- 3000:3000
|
||||
|
||||
@@ -24,23 +24,15 @@ See also [LTS releases](https://docs.victoriametrics.com/victoriametrics/lts-rel
|
||||
|
||||
## tip
|
||||
|
||||
## [v1.125.0](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.125.0)
|
||||
|
||||
Released at 2025-08-29
|
||||
|
||||
* FEATURE: [vmsingle](https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/) and [vmselect](https://docs.victoriametrics.com/victoriametrics/cluster-victoriametrics/) in [VictoriaMetrics cluster](https://docs.victoriametrics.com/victoriametrics/cluster-victoriametrics/): apply `-search.maxQueryLen` limit to Graphite queries. Previously, this limit was only applied to Prometheus queries.
|
||||
* FEATURE: upgrade Go builder from Go1.24.6 to Go1.25. See [Go1.25 release notes](https://tip.golang.org/doc/go1.25).
|
||||
* FEATURE: [vmui](https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/#vmui): add export functionality for Query (Table view) and RawQuery tabs in CSV/JSON format. See [#9332](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/9332).
|
||||
* FEATURE: [vmui](https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/#vmui): replace `Alerts` tab with `Alerting` tab in vmui. The new `Alerting` tab displays vmalert groups and rules directly in vmui interface without redirecting user to vmalert's WEB UI. Links of format `.*/prometheus/vmalert/.*` will continue working by redirecting to vmalert's UI. This functionality is available only if `-vmalert.proxyURL` is set on vmselect. Some functionality of the new `Alerting` tab requires vmalert to be of the same version as vmselect, or higher.
|
||||
* FEATURE: [vmalert](https://docs.victoriametrics.com/victoriametrics/vmalert/): add `/api/v1/group?group_id=<id>` API endpoint for viewing details of a specific [rules group](https://docs.victoriametrics.com/vmalert.html#groups). The new handler is used by new `Alerting` tab in vmselect.
|
||||
* FEATURE: [vmalert](https://docs.victoriametrics.com/victoriametrics/vmalert/): add `lastError` field to `/api/v1/notifiers` response. The new field contains an error message if the last attempt to send data to the notifier failed. The error can be also viewed in `Alerting. Notifiers` tab.
|
||||
* FEATURE: [vmsingle](https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/) and `vmstorage` in [VictoriaMetrics cluster](https://docs.victoriametrics.com/victoriametrics/cluster-victoriametrics/): optimize `/api/v1/labels` and `/api/v1/label/TAG/values` requests with a single `match` or `extra_filters` filter containing timeseries metric name. See this PR [#9489](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/9489) for details.
|
||||
|
||||
* BUGFIX: [vmagent](https://docs.victoriametrics.com/victoriametrics/vmagent/): prevent remote write ingestion stop on push error for [Google Pub/Sub](https://docs.victoriametrics.com/victoriametrics/vmagent/#writing-metrics-to-pubsub) integration.
|
||||
* BUGFIX: [vmauth](https://docs.victoriametrics.com/victoriametrics/vmauth/): properly handle [mTLS authorization and routing](https://docs.victoriametrics.com/victoriametrics/vmauth/#mtls-based-request-routing). Previously it didn't work. See [#29](https://github.com/VictoriaMetrics/VictoriaLogs/issues/29).
|
||||
* BUGFIX: [MetricsQL](https://docs.victoriametrics.com/victoriametrics/metricsql/): fix `timestamp` function compatibility with Prometheus when used with sub-expressions such as `timestamp(sum(foo))`. The fix applies only when `-search.disableImplicitConversion` flag is set. See more in [#9527-comment](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/9527#issuecomment-3200646020) and [metricsql#55](https://github.com/VictoriaMetrics/metricsql/pull/55).
|
||||
* BUGFIX: [vmsingle](https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/) and `vmstorage` in [VictoriaMetrics cluster](https://docs.victoriametrics.com/victoriametrics/cluster-victoriametrics/): optimize subtract operation on uint64 sets. This should potentially improve index search with huge number of deleted series. See this issue [9602](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/9602) for details.
|
||||
* BUGFIX: all VictoriaMetrics [enterprise](https://docs.victoriametrics.com/enterprise/) components: fix support for automatic issuing of TLS certificates for HTTPS server at `-httpListenAddr` via [Let's Encrypt service](https://letsencrypt.org/). See [these docs](https://docs.victoriametrics.com/#automatic-issuing-of-tls-certificates).
|
||||
|
||||
## [v1.124.0](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.124.0)
|
||||
|
||||
|
||||
@@ -515,9 +515,9 @@ scrape_configs:
|
||||
|
||||
One of the following roles can be configured to discover targets:
|
||||
|
||||
### Dockerswarm role: services
|
||||
* `role: services`
|
||||
|
||||
The `role: services` discovers all Swarm services.
|
||||
The `services` role discovers all Swarm services.
|
||||
|
||||
Each discovered target has an [`__address__`](https://docs.victoriametrics.com/victoriametrics/relabeling/#how-to-modify-scrape-urls-in-targets) label set
|
||||
to `<ip>:<port>`, where `<ip>` is the endpoint's virtual IP, while the `<port>` is the published port of the service.
|
||||
@@ -542,9 +542,9 @@ One of the following roles can be configured to discover targets:
|
||||
* `__meta_dockerswarm_network_label_<labelname>`: each label of the network
|
||||
* `__meta_dockerswarm_network_scope`: the scope of the network
|
||||
|
||||
### Dockerswarm role: tasks
|
||||
* `role: tasks`
|
||||
|
||||
The `role: tasks` discovers all Swarm tasks.
|
||||
The `tasks` role discovers all Swarm tasks.
|
||||
|
||||
Each discovered target has an [`__address__`](https://docs.victoriametrics.com/victoriametrics/relabeling/#how-to-modify-scrape-urls-in-targets) label set
|
||||
to `<ip>:<port>`, where the `<ip>` is the node IP, while the `<port>` is the published port of the task.
|
||||
@@ -583,9 +583,9 @@ One of the following roles can be configured to discover targets:
|
||||
|
||||
The `__meta_dockerswarm_network_*` meta labels are not populated for ports which are published with `mode=host`.
|
||||
|
||||
### Dockerswarm role: nodes
|
||||
* `role: nodes`
|
||||
|
||||
The `role: nodes` is used to discover Swarm nodes.
|
||||
The `nodes` role is used to discover Swarm nodes.
|
||||
|
||||
Each discovered target has an [`__address__`](https://docs.victoriametrics.com/victoriametrics/relabeling/#how-to-modify-scrape-urls-in-targets) label set
|
||||
to `<ip>:<port>`, where `<ip>` is the node IP, while the `<port>` is the `port` value obtained from the `dockerswarm_sd_configs`.
|
||||
@@ -1072,7 +1072,7 @@ See [these examples](https://docs.victoriametrics.com/victoriametrics/scrape_con
|
||||
|
||||
One of the following `role` types can be configured to discover targets:
|
||||
|
||||
### Kubernetes role: node
|
||||
* `role: node`
|
||||
|
||||
The `role: node` discovers one target per cluster node.
|
||||
|
||||
@@ -1093,7 +1093,7 @@ One of the following `role` types can be configured to discover targets:
|
||||
|
||||
In addition, the `instance` label for the node will be set to the node name as retrieved from the API server.
|
||||
|
||||
### Kubernetes role: service
|
||||
* `role: service`
|
||||
|
||||
The `role: service` discovers Kubernetes services.
|
||||
|
||||
@@ -1120,7 +1120,7 @@ One of the following `role` types can be configured to discover targets:
|
||||
* `__meta_kubernetes_service_port_protocol`: Protocol of the service port for the target.
|
||||
* `__meta_kubernetes_service_type`: The type of the service.
|
||||
|
||||
### Kubernetes role: pod
|
||||
* `role: pod`
|
||||
|
||||
The `role: pod` discovers all pods and exposes their containers as targets.
|
||||
|
||||
@@ -1153,7 +1153,8 @@ One of the following `role` types can be configured to discover targets:
|
||||
* `__meta_kubernetes_pod_controller_kind`: Object kind of the pod controller.
|
||||
* `__meta_kubernetes_pod_controller_name`: Name of the pod controller.
|
||||
|
||||
### Kubernetes role: endpoints
|
||||
|
||||
* `role: endpoints`
|
||||
|
||||
The `role: endpoints` discovers targets from listed endpoints of a service.
|
||||
|
||||
@@ -1179,10 +1180,10 @@ One of the following `role` types can be configured to discover targets:
|
||||
* `__meta_kubernetes_endpoint_address_target_kind`: Kind of the endpoint address target.
|
||||
* `__meta_kubernetes_endpoint_address_target_name`: Name of the endpoint address target.
|
||||
|
||||
If the endpoints belong to a service, all labels of the [`role: service`](#kubernetes-role--service) are attached.
|
||||
For all targets backed by a pod, all labels of the [`role: pod`](#kubernetes-role--pod) are attached.
|
||||
If the endpoints belong to a service, all labels of the `role: service` are attached.
|
||||
For all targets backed by a pod, all labels of the `role: pod` are attached.
|
||||
|
||||
### Kubernetes role: endpointslice
|
||||
* `role: endpointslice`
|
||||
|
||||
The `role: endpointslice` discovers targets from existing endpointslices.
|
||||
|
||||
@@ -1208,10 +1209,10 @@ One of the following `role` types can be configured to discover targets:
|
||||
* `__meta_kubernetes_endpointslice_port_name`: Named port of the referenced endpoint.
|
||||
* `__meta_kubernetes_endpointslice_port_protocol`: Protocol of the referenced endpoint.
|
||||
|
||||
If the endpoints belong to a service, all labels of the [`role: service`](#kubernetes-role--service) are attached.
|
||||
For all targets backed by a pod, all labels of the [`role: pod`](#kubernetes-role--pod) are attached.
|
||||
If the endpoints belong to a service, all labels of the `role: service` are attached.
|
||||
For all targets backed by a pod, all labels of the `role: pod` are attached.
|
||||
|
||||
### Kubernetes role: ingress
|
||||
* `role: ingress`
|
||||
|
||||
The `role: ingress` discovers a target for each path of each ingress.
|
||||
|
||||
@@ -1468,9 +1469,9 @@ scrape_configs:
|
||||
# ...
|
||||
```
|
||||
|
||||
One of the following `role` types can be configured to discover OpenStack targets:
|
||||
One of the following `role` types can be configured to discover targets:
|
||||
|
||||
### OpenStack role: hypervisor
|
||||
* `role: hypervisor`
|
||||
|
||||
The `role: hypervisor` discovers one target per Nova hypervisor node.
|
||||
|
||||
@@ -1486,7 +1487,7 @@ One of the following `role` types can be configured to discover OpenStack target
|
||||
* `__meta_openstack_hypervisor_status`: the hypervisor node's status.
|
||||
* `__meta_openstack_hypervisor_type`: the hypervisor node's type.
|
||||
|
||||
### OpenStack role: instance
|
||||
* `role: instance`
|
||||
|
||||
The `role: instance` discovers one target per network interface of Nova instance.
|
||||
|
||||
|
||||
121
lib/configwatcher/watcher.go
Normal file
@@ -0,0 +1,121 @@
|
||||
package configwatcher
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/procutil"
|
||||
)
|
||||
|
||||
// TBD: migrate /-/reload handler to the package
|
||||
// TBD: print registered configs in 10s after the start
|
||||
// TBD: print what reload methods enabled
|
||||
|
||||
type handler struct {
|
||||
flag string
|
||||
handler func()
|
||||
}
|
||||
|
||||
var configCheckInterval = flag.Duration("configCheckInterval", 0, "TBD")
|
||||
|
||||
var signalHandlers []handler
|
||||
|
||||
var checkIntervalHandlers []handler
|
||||
|
||||
var mux = sync.Mutex{}
|
||||
|
||||
func RegisterHandler(flag string, handlerFn func()) {
|
||||
RegisterSignalHandler(flag, handlerFn)
|
||||
RegisterCheckIntervalHandler(flag, handlerFn)
|
||||
}
|
||||
|
||||
func RegisterSignalHandler(flag string, handlerFn func()) {
|
||||
mux.Lock()
|
||||
defer mux.Unlock()
|
||||
|
||||
signalHandlers = append(signalHandlers, handler{
|
||||
flag: flag,
|
||||
handler: handlerFn,
|
||||
})
|
||||
}
|
||||
|
||||
func RegisterCheckIntervalHandler(flag string, handlerFn func()) {
|
||||
mux.Lock()
|
||||
defer mux.Unlock()
|
||||
|
||||
checkIntervalHandlers = append(checkIntervalHandlers, handler{
|
||||
flag: flag,
|
||||
handler: handlerFn,
|
||||
})
|
||||
}
|
||||
|
||||
func UnregisterHandler(flag string) {
|
||||
mux.Lock()
|
||||
defer mux.Unlock()
|
||||
|
||||
newCheckIntervalHandlers := make([]handler, 0, len(checkIntervalHandlers))
|
||||
for _, h := range checkIntervalHandlers {
|
||||
if h.flag != flag {
|
||||
newCheckIntervalHandlers = append(newCheckIntervalHandlers, h)
|
||||
}
|
||||
}
|
||||
newSignalHandlers := make([]handler, 0, len(signalHandlers))
|
||||
for _, h := range signalHandlers {
|
||||
if h.flag != flag {
|
||||
newSignalHandlers = append(newSignalHandlers, h)
|
||||
}
|
||||
}
|
||||
|
||||
checkIntervalHandlers = newCheckIntervalHandlers
|
||||
}
|
||||
|
||||
var stopChan chan struct{}
|
||||
|
||||
func Init() {
|
||||
stopChan = make(chan struct{})
|
||||
go func() {
|
||||
sighupCh := procutil.NewSighupChan()
|
||||
|
||||
var tickerCh <-chan time.Time
|
||||
if *configCheckInterval > 0 {
|
||||
ticker := time.NewTicker(*configCheckInterval)
|
||||
tickerCh = ticker.C
|
||||
defer ticker.Stop()
|
||||
}
|
||||
for {
|
||||
|
||||
select {
|
||||
case <-sighupCh:
|
||||
mux.Lock()
|
||||
for _, h := range signalHandlers {
|
||||
h.handler()
|
||||
}
|
||||
mux.Unlock()
|
||||
case <-tickerCh:
|
||||
mux.Lock()
|
||||
for _, h := range checkIntervalHandlers {
|
||||
h.handler()
|
||||
}
|
||||
mux.Unlock()
|
||||
case <-stopChan:
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// Method for BC
|
||||
func EnableCheckInterval(dur time.Duration) {
|
||||
mux.Lock()
|
||||
defer mux.Unlock()
|
||||
|
||||
if dur > *configCheckInterval {
|
||||
*configCheckInterval = dur
|
||||
}
|
||||
}
|
||||
|
||||
// Stop stops Prometheus scraper.
|
||||
func Stop() {
|
||||
close(stopChan)
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
package prommetadata
|
||||
|
||||
import "flag"
|
||||
|
||||
var enableMetadata = flag.Bool("enableMetadata", false, "Whether to enable metadata processing for metrics scraped from targets, received via VictoriaMetrics remote write, Prometheus remote write v1 or OpenTelemetry protocol. "+
|
||||
"See also remoteWrite.maxMetadataPerBlock")
|
||||
|
||||
// IsEnabled reports whether metadata processing is enabled.
|
||||
func IsEnabled() bool {
|
||||
return *enableMetadata
|
||||
}
|
||||
|
||||
// SetEnabled sets enableMetadata to v and returns the previous value of enableMetadata.
|
||||
// This function is intended for promscrape tests.
|
||||
func SetEnabled(v bool) bool {
|
||||
prev := *enableMetadata
|
||||
*enableMetadata = v
|
||||
return prev
|
||||
}
|
||||
@@ -52,6 +52,8 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
enableMetadata = flag.Bool("enableMetadata", false, "Whether to enable metadata processing for metrics scraped from targets, received via VictoriaMetrics remote write, Prometheus remote write v1 or OpenTelemetry protocol. "+
|
||||
"See also remoteWrite.maxMetadataPerBlock")
|
||||
noStaleMarkers = flag.Bool("promscrape.noStaleMarkers", false, "Whether to disable sending Prometheus stale markers for metrics when scrape target disappears. This option may reduce memory usage if stale markers aren't needed for your setup. This option also disables populating the scrape_series_added metric. See https://prometheus.io/docs/concepts/jobs_instances/#automatically-generated-labels-and-time-series")
|
||||
seriesLimitPerTarget = flag.Int("promscrape.seriesLimitPerTarget", 0, "Optional limit on the number of unique time series a single scrape target can expose. See https://docs.victoriametrics.com/victoriametrics/vmagent/#cardinality-limiter for more info")
|
||||
strictParse = flag.Bool("promscrape.config.strictParse", true, "Whether to deny unsupported fields in -promscrape.config . Set to false in order to silently skip unsupported fields")
|
||||
@@ -88,6 +90,11 @@ var (
|
||||
|
||||
var clusterMemberID int
|
||||
|
||||
// IsMetadataEnabled returns true if metadata is enabled.
|
||||
func IsMetadataEnabled() bool {
|
||||
return *enableMetadata
|
||||
}
|
||||
|
||||
func mustInitClusterMemberID() {
|
||||
s := *clusterMemberNum
|
||||
// special case for kubernetes deployment, where pod-name formatted at some-pod-name-1
|
||||
|
||||
@@ -9,12 +9,12 @@ import (
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/configwatcher"
|
||||
"github.com/VictoriaMetrics/metrics"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/auth"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/fasttime"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/procutil"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/prompb"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promscrape/discovery/azure"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promscrape/discovery/consul"
|
||||
@@ -113,7 +113,7 @@ func runScraper(configFile string, pushData func(at *auth.Token, wr *prompb.Writ
|
||||
// Register SIGHUP handler for config reload before loadConfig.
|
||||
// This guarantees that the config will be re-read if the signal arrives just after loadConfig.
|
||||
// See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/1240
|
||||
sighupCh := procutil.NewSighupChan()
|
||||
//sighupCh := procutil.NewSighupChan()
|
||||
|
||||
logger.Infof("reading scrape configs from %q", configFile)
|
||||
cfg, err := loadConfig(configFile)
|
||||
@@ -152,61 +152,69 @@ func runScraper(configFile string, pushData func(at *auth.Token, wr *prompb.Writ
|
||||
scs.add("yandexcloud_sd_configs", *yandexcloud.SDCheckInterval, func(cfg *Config, swsPrev []*ScrapeWork) []*ScrapeWork { return cfg.getYandexCloudSDScrapeWork(swsPrev) })
|
||||
scs.add("static_configs", 0, func(cfg *Config, _ []*ScrapeWork) []*ScrapeWork { return cfg.getStaticScrapeWork() })
|
||||
|
||||
var tickerCh <-chan time.Time
|
||||
scs.updateConfig(cfg)
|
||||
|
||||
if *configCheckInterval > 0 {
|
||||
ticker := time.NewTicker(*configCheckInterval)
|
||||
tickerCh = ticker.C
|
||||
defer ticker.Stop()
|
||||
}
|
||||
for {
|
||||
scs.updateConfig(cfg)
|
||||
waitForChans:
|
||||
select {
|
||||
case <-sighupCh:
|
||||
logger.Infof("SIGHUP received; reloading Prometheus configs from %q", configFile)
|
||||
cfgNew, err := loadConfig(configFile)
|
||||
if err != nil {
|
||||
configReloadErrors.Inc()
|
||||
configSuccess.Set(0)
|
||||
logger.Errorf("cannot read %q on SIGHUP: %s; continuing with the previous config", configFile, err)
|
||||
goto waitForChans
|
||||
}
|
||||
configSuccess.Set(1)
|
||||
if !cfgNew.mustRestart(cfg) {
|
||||
logger.Infof("nothing changed in %q", configFile)
|
||||
goto waitForChans
|
||||
}
|
||||
cfg = cfgNew
|
||||
marshaledData = cfg.marshal()
|
||||
configData.Store(&marshaledData)
|
||||
configReloads.Inc()
|
||||
configTimestamp.Set(fasttime.UnixTimestamp())
|
||||
case <-tickerCh:
|
||||
// TBD print notice that deprecated -promscrape.configCheckInterval is used
|
||||
configwatcher.EnableCheckInterval(*configCheckInterval)
|
||||
|
||||
configwatcher.RegisterCheckIntervalHandler("-promscrape.config", func() {
|
||||
cfgNew, err := loadConfig(configFile)
|
||||
if err != nil {
|
||||
configReloadErrors.Inc()
|
||||
configSuccess.Set(0)
|
||||
logger.Errorf("cannot read %q: %s; continuing with the previous config", configFile, err)
|
||||
goto waitForChans
|
||||
return
|
||||
}
|
||||
configSuccess.Set(1)
|
||||
if !cfgNew.mustRestart(cfg) {
|
||||
goto waitForChans
|
||||
return
|
||||
}
|
||||
cfg = cfgNew
|
||||
marshaledData = cfg.marshal()
|
||||
configData.Store(&marshaledData)
|
||||
configReloads.Inc()
|
||||
configTimestamp.Set(fasttime.UnixTimestamp())
|
||||
case <-globalStopCh:
|
||||
cfg.mustStop()
|
||||
logger.Infof("stopping Prometheus scrapers")
|
||||
startTime := time.Now()
|
||||
scs.stop()
|
||||
logger.Infof("stopped Prometheus scrapers in %.3f seconds", time.Since(startTime).Seconds())
|
||||
|
||||
scs.updateConfig(cfg)
|
||||
})
|
||||
}
|
||||
|
||||
configwatcher.RegisterSignalHandler("-promscrape.config", func() {
|
||||
logger.Infof("SIGHUP received; reloading Prometheus configs from %q", configFile)
|
||||
cfgNew, err := loadConfig(configFile)
|
||||
if err != nil {
|
||||
configReloadErrors.Inc()
|
||||
configSuccess.Set(0)
|
||||
logger.Errorf("cannot read %q on SIGHUP: %s; continuing with the previous config", configFile, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
configSuccess.Set(1)
|
||||
if !cfgNew.mustRestart(cfg) {
|
||||
logger.Infof("nothing changed in %q", configFile)
|
||||
return
|
||||
}
|
||||
cfg = cfgNew
|
||||
marshaledData = cfg.marshal()
|
||||
configData.Store(&marshaledData)
|
||||
configReloads.Inc()
|
||||
configTimestamp.Set(fasttime.UnixTimestamp())
|
||||
|
||||
scs.updateConfig(cfg)
|
||||
})
|
||||
|
||||
go func() {
|
||||
<-globalStopCh
|
||||
|
||||
configwatcher.UnregisterHandler("-promscrape.config")
|
||||
|
||||
cfg.mustStop()
|
||||
logger.Infof("stopping Prometheus scrapers")
|
||||
startTime := time.Now()
|
||||
scs.stop()
|
||||
logger.Infof("stopped Prometheus scrapers in %.3f seconds", time.Since(startTime).Seconds())
|
||||
return
|
||||
}()
|
||||
}
|
||||
|
||||
var (
|
||||
|
||||
@@ -24,7 +24,6 @@ import (
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/leveledbytebufferpool"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promauth"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/prommetadata"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/prompb"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promrelabel"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promutil"
|
||||
@@ -521,7 +520,7 @@ func (sw *scrapeWork) processDataOneShot(scrapeTimestamp, realTimestamp int64, b
|
||||
up = 0
|
||||
scrapesFailed.Inc()
|
||||
} else {
|
||||
if prommetadata.IsEnabled() {
|
||||
if IsMetadataEnabled() {
|
||||
wc.rows, wc.metadataRows = parser.UnmarshalWithMetadata(wc.rows, wc.metadataRows, bodyString, sw.logError)
|
||||
} else {
|
||||
wc.rows.UnmarshalWithErrLogger(bodyString, sw.logError)
|
||||
@@ -617,7 +616,7 @@ func (sw *scrapeWork) processDataInStreamMode(scrapeTimestamp, realTimestamp int
|
||||
areIdenticalSeries := areIdenticalSeries(cfg, lastScrapeStr, bodyString)
|
||||
|
||||
r := body.NewReader()
|
||||
err := stream.Parse(r, scrapeTimestamp, "", false, prommetadata.IsEnabled(), func(rows []parser.Row, mms []parser.Metadata) error {
|
||||
err := stream.Parse(r, scrapeTimestamp, "", false, IsMetadataEnabled(), func(rows []parser.Row, mms []parser.Metadata) error {
|
||||
labelsLen := maxLabelsLen.Load()
|
||||
wc := writeRequestCtxPool.Get(int(labelsLen))
|
||||
defer func() {
|
||||
|
||||
@@ -11,7 +11,6 @@ import (
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/auth"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/chunkedbuffer"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/prommetadata"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/prompb"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promrelabel"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promutil"
|
||||
@@ -145,10 +144,11 @@ func TestScrapeWorkScrapeInternalSuccess(t *testing.T) {
|
||||
}
|
||||
|
||||
func testScrapeWorkScrapeInternalSuccess(t *testing.T, streamParse bool) {
|
||||
oldMetadataEnabled := prommetadata.SetEnabled(true)
|
||||
oldIsmetadataEnabled := *enableMetadata
|
||||
defer func() {
|
||||
prommetadata.SetEnabled(oldMetadataEnabled)
|
||||
*enableMetadata = oldIsmetadataEnabled
|
||||
}()
|
||||
*enableMetadata = true
|
||||
f := func(data string, cfg *ScrapeWork, dataExpected string, metaDataExpected []prompb.MetricMetadata) {
|
||||
t.Helper()
|
||||
|
||||
@@ -599,10 +599,11 @@ func testScrapeWorkScrapeInternalSuccess(t *testing.T, streamParse bool) {
|
||||
//
|
||||
// The core parsing functionality is validated separately in TestScrapeWorkScrapeInternalSuccess.
|
||||
func TestScrapeWorkScrapeInternalStreamConcurrency(t *testing.T) {
|
||||
oldMetadataEnabled := prommetadata.SetEnabled(true)
|
||||
oldIsmetadataEnabled := *enableMetadata
|
||||
defer func() {
|
||||
prommetadata.SetEnabled(oldMetadataEnabled)
|
||||
*enableMetadata = oldIsmetadataEnabled
|
||||
}()
|
||||
*enableMetadata = true
|
||||
f := func(data string, cfg *ScrapeWork, pushDataCallsExpected int64, timeseriesExpected, timeseriesExpectedDelta, metadataExpected int64) {
|
||||
t.Helper()
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/auth"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/chunkedbuffer"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/prommetadata"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/prompb"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/protoparser/protoparserutil"
|
||||
)
|
||||
@@ -131,10 +130,11 @@ func BenchmarkScrapeWorkScrapeInternalStreamBigData(b *testing.B) {
|
||||
}
|
||||
|
||||
func BenchmarkScrapeWorkScrapeInternalOneShotWithMetadata(b *testing.B) {
|
||||
oldMetadataEnabled := prommetadata.SetEnabled(true)
|
||||
oldIsmetadataEnabled := *enableMetadata
|
||||
defer func() {
|
||||
prommetadata.SetEnabled(oldMetadataEnabled)
|
||||
*enableMetadata = oldIsmetadataEnabled
|
||||
}()
|
||||
*enableMetadata = true
|
||||
data := `
|
||||
# TYPE vm_tcplistener_accepts_total counter
|
||||
# HELP vm_tcplistener_accepts_total some useless help message
|
||||
|
||||
@@ -199,6 +199,8 @@ type IndexDBMetrics struct {
|
||||
TagFiltersToMetricIDsCacheRequests uint64
|
||||
TagFiltersToMetricIDsCacheMisses uint64
|
||||
|
||||
DeletedMetricsCount uint64
|
||||
|
||||
IndexDBRefCount uint64
|
||||
|
||||
MissingTSIDsForMetricID uint64
|
||||
@@ -229,6 +231,8 @@ func (db *indexDB) scheduleToDrop() {
|
||||
// UpdateMetrics updates m with metrics from the db.
|
||||
func (db *indexDB) UpdateMetrics(m *IndexDBMetrics) {
|
||||
// global index metrics
|
||||
m.DeletedMetricsCount += uint64(db.s.getDeletedMetricIDs().Len())
|
||||
|
||||
m.IndexBlocksWithMetricIDsProcessed = indexBlocksWithMetricIDsProcessed.Load()
|
||||
m.IndexBlocksWithMetricIDsIncorrectOrder = indexBlocksWithMetricIDsIncorrectOrder.Load()
|
||||
|
||||
@@ -619,21 +623,18 @@ func (is *indexSearch) searchLabelNamesWithFiltersOnTimeRange(qt *querytracer.Tr
|
||||
}
|
||||
|
||||
func (is *indexSearch) searchLabelNamesWithFiltersOnDate(qt *querytracer.Tracer, tfss []*TagFilters, date uint64, maxLabelNames, maxMetrics int) (map[string]struct{}, error) {
|
||||
var filter *uint64set.Set
|
||||
if !isSingleMetricNameFilter(tfss) {
|
||||
filter, err := is.searchMetricIDsWithFiltersOnDate(qt, tfss, date, maxMetrics)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if filter != nil && filter.Len() <= 100e3 {
|
||||
// It is faster to obtain label names by metricIDs from the filter
|
||||
// instead of scanning the inverted index for the matching filters.
|
||||
// This should help https://github.com/VictoriaMetrics/VictoriaMetrics/issues/2978
|
||||
metricIDs := filter.AppendTo(nil)
|
||||
qt.Printf("sort %d metricIDs", len(metricIDs))
|
||||
lns := is.getLabelNamesForMetricIDs(qt, metricIDs, maxLabelNames)
|
||||
return lns, nil
|
||||
}
|
||||
filter, err := is.searchMetricIDsWithFiltersOnDate(qt, tfss, date, maxMetrics)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if filter != nil && filter.Len() <= 100e3 {
|
||||
// It is faster to obtain label names by metricIDs from the filter
|
||||
// instead of scanning the inverted index for the matching filters.
|
||||
// This should help https://github.com/VictoriaMetrics/VictoriaMetrics/issues/2978
|
||||
metricIDs := filter.AppendTo(nil)
|
||||
qt.Printf("sort %d metricIDs", len(metricIDs))
|
||||
lns := is.getLabelNamesForMetricIDs(qt, metricIDs, maxLabelNames)
|
||||
return lns, nil
|
||||
}
|
||||
|
||||
var prevLabelName []byte
|
||||
@@ -884,27 +885,23 @@ func (is *indexSearch) searchLabelValuesOnTimeRange(qt *querytracer.Tracer, labe
|
||||
}
|
||||
|
||||
func (is *indexSearch) searchLabelValuesOnDate(qt *querytracer.Tracer, labelName string, tfss []*TagFilters, date uint64, maxLabelValues, maxMetrics int) (map[string]struct{}, error) {
|
||||
filter, err := is.searchMetricIDsWithFiltersOnDate(qt, tfss, date, maxMetrics)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if filter != nil && filter.Len() <= 100e3 {
|
||||
// It is faster to obtain label values by metricIDs from the filter
|
||||
// instead of scanning the inverted index for the matching filters.
|
||||
// This should help https://github.com/VictoriaMetrics/VictoriaMetrics/issues/2978
|
||||
metricIDs := filter.AppendTo(nil)
|
||||
qt.Printf("sort %d metricIDs", len(metricIDs))
|
||||
lvs := is.getLabelValuesForMetricIDs(qt, labelName, metricIDs, maxLabelValues)
|
||||
return lvs, nil
|
||||
}
|
||||
if labelName == "__name__" {
|
||||
// __name__ label is encoded as empty string in indexdb.
|
||||
labelName = ""
|
||||
}
|
||||
useCompositeScan := labelName != "" && isSingleMetricNameFilter(tfss)
|
||||
var filter *uint64set.Set
|
||||
if !useCompositeScan {
|
||||
filter, err := is.searchMetricIDsWithFiltersOnDate(qt, tfss, date, maxMetrics)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if filter != nil && filter.Len() <= 100e3 {
|
||||
// It is faster to obtain label values by metricIDs from the filter
|
||||
// instead of scanning the inverted index for the matching filters.
|
||||
// This should help https://github.com/VictoriaMetrics/VictoriaMetrics/issues/2978
|
||||
metricIDs := filter.AppendTo(nil)
|
||||
qt.Printf("sort %d metricIDs", len(metricIDs))
|
||||
lvs := is.getLabelValuesForMetricIDs(qt, labelName, metricIDs, maxLabelValues)
|
||||
return lvs, nil
|
||||
}
|
||||
}
|
||||
|
||||
labelNameBytes := bytesutil.ToUnsafeBytes(labelName)
|
||||
if name := getCommonMetricNameForTagFilterss(tfss); len(name) > 0 && labelName != "" {
|
||||
@@ -1353,7 +1350,6 @@ func (is *indexSearch) getTSDBStatus(qt *querytracer.Tracer, tfss []*TagFilters,
|
||||
qt.Printf("no matching series for filter=%s", tfss)
|
||||
return &TSDBStatus{}, nil
|
||||
}
|
||||
|
||||
ts := &is.ts
|
||||
kb := &is.kb
|
||||
mp := &is.mp
|
||||
@@ -2194,11 +2190,6 @@ func matchTagFilters(mn *MetricName, tfs []*tagFilter, kb *bytesutil.ByteBuffer)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func isSingleMetricNameFilter(tfss []*TagFilters) bool {
|
||||
// We check if tfss contain only single filter which is __name__
|
||||
return len(tfss) == 1 && len(tfss[0].tfs) == 1 && getMetricNameFilter(tfss[0]) != nil
|
||||
}
|
||||
|
||||
func (is *indexSearch) searchMetricIDsWithFiltersOnDate(qt *querytracer.Tracer, tfss []*TagFilters, date uint64, maxMetrics int) (*uint64set.Set, error) {
|
||||
if len(tfss) == 0 {
|
||||
return nil, nil
|
||||
|
||||
@@ -1704,10 +1704,6 @@ func TestSearchTSIDWithTimeRange(t *testing.T) {
|
||||
if err := tfsMetricName.Add(nil, []byte("testMetric"), false, false); err != nil {
|
||||
t.Fatalf("cannot add filter on metric name: %s", err)
|
||||
}
|
||||
tfsComposite := NewTagFilters()
|
||||
if err := tfsComposite.Add(nil, []byte("testMetric"), false, false); err != nil {
|
||||
t.Fatalf("cannot add filter: %s", err)
|
||||
}
|
||||
|
||||
// Perform a search within a day.
|
||||
// This should return the metrics for the day
|
||||
@@ -1753,16 +1749,6 @@ func TestSearchTSIDWithTimeRange(t *testing.T) {
|
||||
t.Fatalf("unexpected labelNames; got\n%s\nwant\n%s", got, labelNames)
|
||||
}
|
||||
|
||||
// Check SearchLabelNames with filters on composite key and time range.
|
||||
lns, err = idbCurr.SearchLabelNames(nil, []*TagFilters{tfsComposite}, tr, 10000, 1e9, noDeadline)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error in SearchLabelNames(filters=%s, timeRange=%s): %s", tfs, &tr, err)
|
||||
}
|
||||
got = sortedSlice(lns)
|
||||
if !reflect.DeepEqual(got, labelNames) {
|
||||
t.Fatalf("unexpected labelNames; got\n%s\nwant\n%s", got, labelNames)
|
||||
}
|
||||
|
||||
// Check SearchLabelValues with the specified filter.
|
||||
lvs, err = idbCurr.SearchLabelValues(nil, "", []*TagFilters{tfs}, TimeRange{}, 10000, 1e9, noDeadline)
|
||||
if err != nil {
|
||||
@@ -1793,17 +1779,6 @@ func TestSearchTSIDWithTimeRange(t *testing.T) {
|
||||
t.Fatalf("unexpected labelValues; got\n%s\nwant\n%s", got, labelValues)
|
||||
}
|
||||
|
||||
// Check SearchLabelValues with filters on composite key and time range.
|
||||
lvs, err = idbCurr.SearchLabelValues(nil, "constant", []*TagFilters{tfsComposite}, tr, 10000, 1e9, noDeadline)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error in SearchLabelValues(filters=%s, timeRange=%s): %s", tfs, &tr, err)
|
||||
}
|
||||
got = sortedSlice(lvs)
|
||||
labelValues = []string{"const"}
|
||||
if !reflect.DeepEqual(got, labelValues) {
|
||||
t.Fatalf("unexpected labelValues; got\n%s\nwant\n%s", got, labelValues)
|
||||
}
|
||||
|
||||
// Perform a search across all the days, should match all metrics
|
||||
tr = TimeRange{
|
||||
MinTimestamp: int64(timestamp - msecPerDay*days),
|
||||
|
||||
@@ -655,8 +655,6 @@ type Metrics struct {
|
||||
MetricNamesUsageTrackerSizeBytes uint64
|
||||
MetricNamesUsageTrackerSizeMaxBytes uint64
|
||||
|
||||
DeletedMetricsCount uint64
|
||||
|
||||
IndexDBMetrics IndexDBMetrics
|
||||
TableMetrics TableMetrics
|
||||
}
|
||||
@@ -765,8 +763,6 @@ func (s *Storage) UpdateMetrics(m *Metrics) {
|
||||
}
|
||||
m.NextRetentionSeconds = uint64(d)
|
||||
|
||||
m.DeletedMetricsCount += uint64(s.getDeletedMetricIDs().Len())
|
||||
|
||||
idbPrev, idbCurr := s.getPrevAndCurrIndexDBs()
|
||||
defer s.putPrevAndCurrIndexDBs(idbPrev, idbCurr)
|
||||
idbCurr.UpdateMetrics(&m.IndexDBMetrics)
|
||||
|
||||
@@ -1439,55 +1439,6 @@ func TestStorageDeleteSeries_CachesAreUpdatedOrReset(t *testing.T) {
|
||||
assertDeletedMetricIDsCacheSize(3)
|
||||
}
|
||||
|
||||
func TestStorageDeleteSeriesFromPrevAndCurrIndexDB(t *testing.T) {
|
||||
defer testRemoveAll(t)
|
||||
|
||||
rng := rand.New(rand.NewSource(1))
|
||||
const numSeries = 100
|
||||
trPrev := TimeRange{
|
||||
MinTimestamp: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC).UnixMilli(),
|
||||
MaxTimestamp: time.Date(2020, 1, 1, 23, 59, 59, 999_999_999, time.UTC).UnixMilli(),
|
||||
}
|
||||
mrsPrev := testGenerateMetricRowsWithPrefix(rng, numSeries, "prev", trPrev)
|
||||
trCurr := TimeRange{
|
||||
MinTimestamp: time.Date(2020, 1, 2, 0, 0, 0, 0, time.UTC).UnixMilli(),
|
||||
MaxTimestamp: time.Date(2020, 1, 2, 23, 59, 59, 999_999_999, time.UTC).UnixMilli(),
|
||||
}
|
||||
mrsCurr := testGenerateMetricRowsWithPrefix(rng, numSeries, "curr", trCurr)
|
||||
deleteSeries := func(s *Storage, want, wantTotal int) {
|
||||
t.Helper()
|
||||
tfs := NewTagFilters()
|
||||
if err := tfs.Add(nil, []byte(".*"), false, true); err != nil {
|
||||
t.Fatalf("unexpected error in TagFilters.Add: %v", err)
|
||||
}
|
||||
got, err := s.DeleteSeries(nil, []*TagFilters{tfs}, 1e9)
|
||||
if err != nil {
|
||||
t.Fatalf("could not delete series unexpectedly: %v", err)
|
||||
}
|
||||
if got != want {
|
||||
t.Fatalf("unexpected number of deleted series: got %d, want %d", got, want)
|
||||
}
|
||||
var m Metrics
|
||||
s.UpdateMetrics(&m)
|
||||
if got, want := m.DeletedMetricsCount, uint64(wantTotal); got != want {
|
||||
t.Fatalf("unexpected number of total deleted series: got %d, want %d", got, want)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
s := MustOpenStorage(t.Name(), OpenOptions{})
|
||||
defer s.MustClose()
|
||||
s.AddRows(mrsPrev, defaultPrecisionBits)
|
||||
s.DebugFlush()
|
||||
deleteSeries(s, numSeries, numSeries)
|
||||
|
||||
s.mustRotateIndexDB(time.Now())
|
||||
|
||||
s.AddRows(mrsCurr, defaultPrecisionBits)
|
||||
s.DebugFlush()
|
||||
deleteSeries(s, numSeries, 2*numSeries)
|
||||
}
|
||||
|
||||
func TestStorageRegisterMetricNamesSerial(t *testing.T) {
|
||||
path := "TestStorageRegisterMetricNamesSerial"
|
||||
s := MustOpenStorage(path, OpenOptions{})
|
||||
|
||||
@@ -359,23 +359,12 @@ func (s *Set) Subtract(a *Set) {
|
||||
// Fast path - nothing to subtract.
|
||||
return
|
||||
}
|
||||
if s.Len() >= a.Len() {
|
||||
a.ForEach(func(part []uint64) bool {
|
||||
for _, x := range part {
|
||||
s.Del(x)
|
||||
}
|
||||
return true
|
||||
})
|
||||
} else {
|
||||
s.ForEach(func(part []uint64) bool {
|
||||
for _, x := range part {
|
||||
if a.Has(x) {
|
||||
s.Del(x)
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
a.ForEach(func(part []uint64) bool {
|
||||
for _, x := range part {
|
||||
s.Del(x)
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
// Equal returns true if s contains the same items as a.
|
||||
|
||||
@@ -7,8 +7,6 @@ import (
|
||||
"sort"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
)
|
||||
|
||||
func TestSetOps(t *testing.T) {
|
||||
@@ -760,141 +758,3 @@ func TestAddMulti(t *testing.T) {
|
||||
}
|
||||
f(a)
|
||||
}
|
||||
|
||||
func TestSubtract(t *testing.T) {
|
||||
f := func(a, b, want *Set) {
|
||||
t.Helper()
|
||||
bBefore := b.AppendTo(nil)
|
||||
a.Subtract(b)
|
||||
bAfter := b.AppendTo(nil)
|
||||
gotValues := []uint64{}
|
||||
gotValues = a.AppendTo(gotValues)
|
||||
wantValues := []uint64{}
|
||||
wantValues = want.AppendTo(wantValues)
|
||||
if diff := cmp.Diff(wantValues, gotValues); diff != "" {
|
||||
t.Fatalf("unexpected a set (-want, +got):\n%s", diff)
|
||||
}
|
||||
if diff := cmp.Diff(bBefore, bAfter); diff != "" {
|
||||
t.Fatalf("unexpected b set (-want, +got):\n%s", diff)
|
||||
}
|
||||
}
|
||||
|
||||
s := func(start, end uint64, se ...uint64) *Set {
|
||||
s := &Set{}
|
||||
for i := start; i <= end; i++ {
|
||||
s.Add(i)
|
||||
}
|
||||
if len(se)%2 != 0 {
|
||||
t.Fatalf("the number of additional starts and ends must be an even number since they go in pairs")
|
||||
}
|
||||
for i := 0; i < len(se); i += 2 {
|
||||
start := se[i]
|
||||
end := se[i+1]
|
||||
for j := start; j <= end; j++ {
|
||||
s.Add(j)
|
||||
}
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
var a, b, want *Set
|
||||
|
||||
// - no overlap
|
||||
// - a values before b values
|
||||
// - len(a) > len(b)
|
||||
a = s(1, 500_000)
|
||||
b = s(500_001, 600_000)
|
||||
want = s(1, 500_000)
|
||||
f(a, b, want)
|
||||
|
||||
// - no overlap
|
||||
// - a values after b values
|
||||
// - len(a) > len(b)
|
||||
a = s(500_001, 1_000_000)
|
||||
b = s(400_000, 500_000)
|
||||
want = s(500_001, 1_000_000)
|
||||
f(a, b, want)
|
||||
|
||||
// - no overlap
|
||||
// - a values before b values
|
||||
// - len(a) < len(b)
|
||||
a = s(400_000, 500_000)
|
||||
b = s(500_001, 1_000_000)
|
||||
want = s(400_000, 500_000)
|
||||
f(a, b, want)
|
||||
|
||||
// - no overlap
|
||||
// - a values after b values
|
||||
// - len(a) < len(b)
|
||||
a = s(500_001, 600_000)
|
||||
b = s(1, 500_000)
|
||||
want = s(500_001, 600_000)
|
||||
f(a, b, want)
|
||||
|
||||
// - overlap on the left side
|
||||
// - len(a) > len(b)
|
||||
a = s(500_000, 1_000_000)
|
||||
b = s(400_000, 600_000)
|
||||
want = s(600_001, 1_000_000)
|
||||
f(a, b, want)
|
||||
|
||||
// - overlap on the right side
|
||||
// - len(a) > len(b)
|
||||
a = s(1, 500_000)
|
||||
b = s(400_001, 600_000)
|
||||
want = s(1, 400_000)
|
||||
f(a, b, want)
|
||||
|
||||
// - overlap on the left side
|
||||
// - len(a) < len(b)
|
||||
a = s(400_000, 600_000)
|
||||
b = s(500_001, 1_000_000)
|
||||
want = s(400_000, 500_000)
|
||||
f(a, b, want)
|
||||
|
||||
// - overlap on the right side
|
||||
// - len(a) < len(b)
|
||||
a = s(400_000, 600_000)
|
||||
b = s(1, 500_000)
|
||||
want = s(500_001, 600_000)
|
||||
f(a, b, want)
|
||||
|
||||
// same
|
||||
a = s(1, 500_000)
|
||||
b = s(1, 500_000)
|
||||
want = &Set{}
|
||||
f(a, b, want)
|
||||
|
||||
// b is a subset of a
|
||||
a = s(1, 500_000)
|
||||
b = s(100_001, 400_000)
|
||||
want = s(1, 100_000, 400_001, 500_000)
|
||||
f(a, b, want)
|
||||
|
||||
// a is a subset of b
|
||||
a = s(100_001, 400_000)
|
||||
b = s(1, 500_000)
|
||||
want = &Set{}
|
||||
f(a, b, want)
|
||||
|
||||
// a with intervals
|
||||
a = s(1, 200_000, 400_000, 500_000)
|
||||
b = s(150_001, 450_000)
|
||||
want = s(1, 150_000, 450_001, 500_000)
|
||||
f(a, b, want)
|
||||
|
||||
// b with intervals
|
||||
a = s(1, 500_000)
|
||||
b = s(200_001, 300_000, 400_001, 500_000)
|
||||
want = s(1, 200_000, 300_001, 400_000)
|
||||
f(a, b, want)
|
||||
|
||||
// subtract more than once.
|
||||
a = s(1, 500_000)
|
||||
b1 := s(100_001, 200_000)
|
||||
want1 := s(1, 100_000, 200_001, 500_000)
|
||||
b2 := s(300_001, 400_000)
|
||||
want2 := s(1, 100_000, 200_001, 300_000, 400_001, 500_000)
|
||||
f(a, b1, want1)
|
||||
f(a, b2, want2)
|
||||
}
|
||||
|
||||
@@ -154,66 +154,6 @@ func benchmarkIntersect(b *testing.B, sa, sb *Set) {
|
||||
})
|
||||
}
|
||||
|
||||
func BenchmarkSubtract(b *testing.B) {
|
||||
f := func(b *testing.B, startA, itemsCountA, startB, itemsCountB uint64) {
|
||||
sa := createRangeSet(startA, int(itemsCountA))
|
||||
sb := createRangeSet(startB, int(itemsCountB))
|
||||
b.ReportAllocs()
|
||||
b.SetBytes(int64(sa.Len() + sb.Len()))
|
||||
for b.Loop() {
|
||||
saCopy := sa.Clone()
|
||||
saCopy.Subtract(sb)
|
||||
}
|
||||
}
|
||||
|
||||
start := uint64(time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC).UnixNano())
|
||||
itemsACounts := []uint64{1e3, 1e4, 1e5, 1e6, 1e7}
|
||||
itemsBCounts := []uint64{1e7, 1e6, 1e5, 1e4, 1e3}
|
||||
for i := range len(itemsACounts) {
|
||||
itemsCountA := itemsACounts[i]
|
||||
itemsCountB := itemsBCounts[i]
|
||||
|
||||
b.Run(fmt.Sprintf("-----NoOverlap-AbeforeB-A%d-B%d", itemsCountA, itemsCountB), func(b *testing.B) {
|
||||
f(b, start, itemsCountA, start+itemsCountA, itemsCountB)
|
||||
})
|
||||
b.Run(fmt.Sprintf("-----NoOverlap-AbeforeB-B%d-A%d", itemsCountB, itemsCountA), func(b *testing.B) {
|
||||
f(b, start+itemsCountA, itemsCountB, start, itemsCountA)
|
||||
})
|
||||
b.Run(fmt.Sprintf("-----NoOverlap-BbeforeA-A%d-B%d", itemsCountA, itemsCountB), func(b *testing.B) {
|
||||
f(b, start-itemsCountA, itemsCountA, start, itemsCountB)
|
||||
})
|
||||
b.Run(fmt.Sprintf("-----NoOverlap-BbeforeA-B%d-A%d", itemsCountB, itemsCountA), func(b *testing.B) {
|
||||
f(b, start, itemsCountB, start-itemsCountA, itemsCountA)
|
||||
})
|
||||
|
||||
b.Run(fmt.Sprintf("PartialOverlap-AbeforeB-A%d-B%d", itemsCountA, itemsCountB), func(b *testing.B) {
|
||||
f(b, start, itemsCountA, start+itemsCountA-itemsCountB/2, itemsCountB)
|
||||
})
|
||||
b.Run(fmt.Sprintf("PartialOverlap-AbeforeB-B%d-A%d", itemsCountB, itemsCountA), func(b *testing.B) {
|
||||
f(b, start+itemsCountA-itemsCountB/2, itemsCountB, start, itemsCountA)
|
||||
})
|
||||
b.Run(fmt.Sprintf("PartialOverlap-BbeforeA-A%d-B%d", itemsCountA, itemsCountB), func(b *testing.B) {
|
||||
f(b, start+itemsCountB/2, itemsCountA, start, itemsCountB)
|
||||
})
|
||||
b.Run(fmt.Sprintf("PartialOverlap-BbeforeA-B%d-A%d", itemsCountB, itemsCountA), func(b *testing.B) {
|
||||
f(b, start, itemsCountB, start+itemsCountB/2, itemsCountA)
|
||||
})
|
||||
|
||||
b.Run(fmt.Sprintf("---FullOverlap-AbeforeB-A%d-B%d", itemsCountA, itemsCountB), func(b *testing.B) {
|
||||
f(b, start, itemsCountA, start+itemsCountA-itemsCountB, itemsCountB)
|
||||
})
|
||||
b.Run(fmt.Sprintf("---FullOverlap-AbeforeB-B%d-A%d", itemsCountB, itemsCountA), func(b *testing.B) {
|
||||
f(b, start+itemsCountA-itemsCountB, itemsCountB, start, itemsCountA)
|
||||
})
|
||||
b.Run(fmt.Sprintf("---FullOverlap-BbeforeA-A%d-B%d", itemsCountA, itemsCountB), func(b *testing.B) {
|
||||
f(b, start+itemsCountB, itemsCountA, start, itemsCountB)
|
||||
})
|
||||
b.Run(fmt.Sprintf("---FullOverlap-BbeforeA-B%d-A%d", itemsCountB, itemsCountA), func(b *testing.B) {
|
||||
f(b, start, itemsCountB, start+itemsCountB, itemsCountA)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func createRangeSet(start uint64, itemsCount int) *Set {
|
||||
var s Set
|
||||
for i := 0; i < itemsCount; i++ {
|
||||
|
||||