Compare commits
82 Commits
issue-8584
...
backup_doc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1d27079ec8 | ||
|
|
26ce7316a0 | ||
|
|
4c825bf31c | ||
|
|
3108376d95 | ||
|
|
494fe4403a | ||
|
|
303b425fa3 | ||
|
|
8f3efde55d | ||
|
|
f16938bba9 | ||
|
|
65ff04bc09 | ||
|
|
e2715f94af | ||
|
|
582160f566 | ||
|
|
638f9839d5 | ||
|
|
3f5bf4bd03 | ||
|
|
038419663b | ||
|
|
123f373537 | ||
|
|
57121c828f | ||
|
|
aa5edbc706 | ||
|
|
f9d8c86b0a | ||
|
|
b733fc5b83 | ||
|
|
f2eaad62dc | ||
|
|
adae788b18 | ||
|
|
a65d10fcce | ||
|
|
298f862fc0 | ||
|
|
aff1580a1d | ||
|
|
d4c0a42c1b | ||
|
|
a9736a5bfb | ||
|
|
2e4beeefb1 | ||
|
|
635c5c9feb | ||
|
|
4e1260e189 | ||
|
|
ca3910748f | ||
|
|
2f0796ff40 | ||
|
|
cdba6dbc0e | ||
|
|
f18daaeac5 | ||
|
|
a9f124388f | ||
|
|
4b2276608b | ||
|
|
b352470ae1 | ||
|
|
b3261a1b87 | ||
|
|
6d5973dcb0 | ||
|
|
74f17bb67e | ||
|
|
bf024d3dce | ||
|
|
5b87aff830 | ||
|
|
34d35869fa | ||
|
|
b1d1f1f461 | ||
|
|
98f1e32e39 | ||
|
|
edac875179 | ||
|
|
0ff1a3b154 | ||
|
|
bbe58cc37b | ||
|
|
78dca6ee6e | ||
|
|
12f26668a6 | ||
|
|
6c90843aab | ||
|
|
4aad4c64bb | ||
|
|
5630c0108e | ||
|
|
732a549cff | ||
|
|
af637bc2a2 | ||
|
|
6600916344 | ||
|
|
e9cb95c5d4 | ||
|
|
8617faa160 | ||
|
|
fe888be58c | ||
|
|
a38de1c242 | ||
|
|
8dc0f6cfab | ||
|
|
346db8a606 | ||
|
|
b277a62e94 | ||
|
|
e2535fcb28 | ||
|
|
66e7b908ec | ||
|
|
a2ba37be68 | ||
|
|
004e5ff38b | ||
|
|
4160fbecc0 | ||
|
|
44844b7fbe | ||
|
|
f60458d5fa | ||
|
|
e242dd5bf2 | ||
|
|
f863b331c1 | ||
|
|
a98779770c | ||
|
|
bcbcae2309 | ||
|
|
5b8c10d08e | ||
|
|
588fa4d90d | ||
|
|
b9c777a578 | ||
|
|
d8fe739aba | ||
|
|
e5ddf475b8 | ||
|
|
3c3c8668d6 | ||
|
|
22d1b916bf | ||
|
|
35b31f904d | ||
|
|
7c05ec42fe |
2
.github/pull_request_template.md
vendored
@@ -6,4 +6,4 @@ Please provide a brief description of the changes you made. Be as specific as po
|
||||
|
||||
The following checks are **mandatory**:
|
||||
|
||||
- [ ] My change adheres [VictoriaMetrics contributing guidelines](https://docs.victoriametrics.com/contributing/).
|
||||
- [ ] My change adheres to [VictoriaMetrics contributing guidelines](https://docs.victoriametrics.com/contributing/).
|
||||
|
||||
@@ -99,9 +99,17 @@ func RequestHandler(w http.ResponseWriter, r *http.Request) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
ctx := r.Context()
|
||||
if path == "/select/logsql/tail" {
|
||||
logsqlTailRequests.Inc()
|
||||
// Process live tailing request without timeout, since it is OK to run live tailing requests for very long time.
|
||||
// Also do not apply concurrency limit to tail requests, since these limits are intended for non-tail requests.
|
||||
logsql.ProcessLiveTailRequest(ctx, w, r)
|
||||
return true
|
||||
}
|
||||
|
||||
// Limit the number of concurrent queries, which can consume big amounts of CPU time.
|
||||
startTime := time.Now()
|
||||
ctx := r.Context()
|
||||
d := getMaxQueryDuration(r)
|
||||
ctxWithTimeout, cancel := context.WithTimeout(ctx, d)
|
||||
defer cancel()
|
||||
@@ -111,14 +119,6 @@ func RequestHandler(w http.ResponseWriter, r *http.Request) bool {
|
||||
}
|
||||
defer decRequestConcurrency()
|
||||
|
||||
if path == "/select/logsql/tail" {
|
||||
logsqlTailRequests.Inc()
|
||||
// Process live tailing request without timeout (e.g. use ctx instead of ctxWithTimeout),
|
||||
// since it is OK to run live tailing requests for very long time.
|
||||
logsql.ProcessLiveTailRequest(ctx, w, r)
|
||||
return true
|
||||
}
|
||||
|
||||
ok := processSelectRequest(ctxWithTimeout, w, r, path)
|
||||
if !ok {
|
||||
return false
|
||||
@@ -129,10 +129,7 @@ func RequestHandler(w http.ResponseWriter, r *http.Request) bool {
|
||||
case nil:
|
||||
// nothing to do
|
||||
case context.Canceled:
|
||||
remoteAddr := httpserver.GetQuotedRemoteAddr(r)
|
||||
requestURI := httpserver.GetRequestURI(r)
|
||||
logger.Infof("client has canceled the request after %.3f seconds: remoteAddr=%s, requestURI: %q",
|
||||
time.Since(startTime).Seconds(), remoteAddr, requestURI)
|
||||
// do not log canceled requests, since they are expected and legal.
|
||||
case context.DeadlineExceeded:
|
||||
err = &httpserver.ErrorWithStatusCode{
|
||||
Err: fmt.Errorf("the request couldn't be executed in %.3f seconds; possible solutions: "+
|
||||
|
||||
@@ -105,7 +105,7 @@ func main() {
|
||||
// Some workloads may need increased GOGC values. Then such values can be set via GOGC environment variable.
|
||||
// It is recommended increasing GOGC if go_memstats_gc_cpu_fraction metric exposed at /metrics page
|
||||
// exceeds 0.05 for extended periods of time.
|
||||
cgroup.SetGOGC(30)
|
||||
cgroup.SetGOGC(50)
|
||||
|
||||
// Write flags and help message to stdout, since it is easier to grep or pipe.
|
||||
flag.CommandLine.SetOutput(os.Stdout)
|
||||
@@ -443,8 +443,10 @@ func requestHandler(w http.ResponseWriter, r *http.Request) bool {
|
||||
case "/prometheus/api/v1/targets", "/api/v1/targets":
|
||||
promscrapeAPIV1TargetsRequests.Inc()
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
// https://prometheus.io/docs/prometheus/latest/querying/api/#targets
|
||||
state := r.FormValue("state")
|
||||
promscrape.WriteAPIV1Targets(w, state)
|
||||
scrapePool := r.FormValue("scrapePool")
|
||||
promscrape.WriteAPIV1Targets(w, state, scrapePool)
|
||||
return true
|
||||
case "/prometheus/target_response", "/target_response":
|
||||
promscrapeTargetResponseRequests.Inc()
|
||||
|
||||
@@ -13,10 +13,10 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/awsapi"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/encoding"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/flagutil"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/httputil"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/netutil"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/persistentqueue"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promauth"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/protoparser/protoparserutil"
|
||||
@@ -126,8 +126,7 @@ func newHTTPClient(argIdx int, remoteWriteURL, sanitizedURL string, fq *persiste
|
||||
logger.Fatalf("cannot initialize AWS Config for -remoteWrite.url=%q: %s", remoteWriteURL, err)
|
||||
}
|
||||
|
||||
tr := httputil.NewTransport(false)
|
||||
tr.DialContext = netutil.NewStatDialFunc("vmagent_remotewrite")
|
||||
tr := httputil.NewTransport(false, "vmagent_remotewrite")
|
||||
tr.TLSHandshakeTimeout = tlsHandshakeTimeout.GetOptionalArg(argIdx)
|
||||
tr.MaxConnsPerHost = 2 * concurrency
|
||||
tr.MaxIdleConnsPerHost = 2 * concurrency
|
||||
@@ -386,7 +385,7 @@ func (c *client) newRequest(url string, body []byte) (*http.Request, error) {
|
||||
h := req.Header
|
||||
h.Set("User-Agent", "vmagent")
|
||||
h.Set("Content-Type", "application/x-protobuf")
|
||||
if c.useVMProto {
|
||||
if encoding.IsZstd(body) {
|
||||
h.Set("Content-Encoding", "zstd")
|
||||
h.Set("X-VictoriaMetrics-Remote-Write-Version", "1")
|
||||
} else {
|
||||
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/persistentqueue"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/prompbmarshal"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promrelabel"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/slicesutil"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/timeutil"
|
||||
"github.com/VictoriaMetrics/metrics"
|
||||
"github.com/golang/snappy"
|
||||
@@ -197,28 +198,43 @@ func (wr *writeRequest) tryPush(src []prompbmarshal.TimeSeries) bool {
|
||||
}
|
||||
|
||||
func (wr *writeRequest) copyTimeSeries(dst, src *prompbmarshal.TimeSeries) {
|
||||
labelsDst := wr.labels
|
||||
labelsSrc := src.Labels
|
||||
|
||||
// Pre-allocate memory for labels.
|
||||
labelsLen := len(wr.labels)
|
||||
samplesDst := wr.samples
|
||||
buf := wr.buf
|
||||
for i := range src.Labels {
|
||||
labelsDst = append(labelsDst, prompbmarshal.Label{})
|
||||
dstLabel := &labelsDst[len(labelsDst)-1]
|
||||
srcLabel := &src.Labels[i]
|
||||
wr.labels = slicesutil.SetLength(wr.labels, labelsLen+len(labelsSrc))
|
||||
labelsDst := wr.labels[labelsLen:]
|
||||
|
||||
buf = append(buf, srcLabel.Name...)
|
||||
dstLabel.Name = bytesutil.ToUnsafeString(buf[len(buf)-len(srcLabel.Name):])
|
||||
buf = append(buf, srcLabel.Value...)
|
||||
dstLabel.Value = bytesutil.ToUnsafeString(buf[len(buf)-len(srcLabel.Value):])
|
||||
// Pre-allocate memory for byte slice needed for storing label names and values.
|
||||
neededBufLen := 0
|
||||
for i := range labelsSrc {
|
||||
label := &labelsSrc[i]
|
||||
neededBufLen += len(label.Name) + len(label.Value)
|
||||
}
|
||||
dst.Labels = labelsDst[labelsLen:]
|
||||
bufLen := len(wr.buf)
|
||||
wr.buf = slicesutil.SetLength(wr.buf, bufLen+neededBufLen)
|
||||
buf := wr.buf[:bufLen]
|
||||
|
||||
samplesDst = append(samplesDst, src.Samples...)
|
||||
dst.Samples = samplesDst[len(samplesDst)-len(src.Samples):]
|
||||
// Copy labels
|
||||
for i := range labelsSrc {
|
||||
dstLabel := &labelsDst[i]
|
||||
srcLabel := &labelsSrc[i]
|
||||
|
||||
wr.samples = samplesDst
|
||||
wr.labels = labelsDst
|
||||
bufLen := len(buf)
|
||||
buf = append(buf, srcLabel.Name...)
|
||||
dstLabel.Name = bytesutil.ToUnsafeString(buf[bufLen:])
|
||||
|
||||
bufLen = len(buf)
|
||||
buf = append(buf, srcLabel.Value...)
|
||||
dstLabel.Value = bytesutil.ToUnsafeString(buf[bufLen:])
|
||||
}
|
||||
wr.buf = buf
|
||||
dst.Labels = labelsDst
|
||||
|
||||
// Copy samples
|
||||
samplesLen := len(wr.samples)
|
||||
wr.samples = append(wr.samples, src.Samples...)
|
||||
dst.Samples = wr.samples[samplesLen:]
|
||||
}
|
||||
|
||||
// marshalConcurrency limits the maximum number of concurrent workers, which marshal and compress WriteRequest.
|
||||
|
||||
8
app/vmalert-tool/deployment/Dockerfile
Normal file
@@ -0,0 +1,8 @@
|
||||
ARG base_image=non-existing
|
||||
FROM $base_image
|
||||
|
||||
EXPOSE 8880
|
||||
|
||||
ENTRYPOINT ["/vmalert-tool-prod"]
|
||||
ARG src_binary=non-existing
|
||||
COPY $src_binary ./vmalert-tool-prod
|
||||
@@ -35,6 +35,8 @@ type promResponse struct {
|
||||
Stats struct {
|
||||
SeriesFetched *string `json:"seriesFetched,omitempty"`
|
||||
} `json:"stats,omitempty"`
|
||||
// IsPartial supported by VictoriaMetrics
|
||||
IsPartial *bool `json:"isPartial,omitempty"`
|
||||
}
|
||||
|
||||
// see https://prometheus.io/docs/prometheus/latest/querying/api/#instant-queries
|
||||
@@ -209,7 +211,7 @@ func parsePrometheusResponse(req *http.Request, resp *http.Response) (res Result
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
res = Result{Data: ms}
|
||||
res = Result{Data: ms, IsPartial: r.IsPartial}
|
||||
if r.Stats.SeriesFetched != nil {
|
||||
intV, err := strconv.Atoi(*r.Stats.SeriesFetched)
|
||||
if err != nil {
|
||||
|
||||
@@ -72,12 +72,14 @@ func TestVMInstantQuery(t *testing.T) {
|
||||
w.Write([]byte(`{"status":"success","data":{"resultType":"scalar","result":[1583786142, "1"]}}`))
|
||||
case 7:
|
||||
w.Write([]byte(`{"status":"success","data":{"resultType":"scalar","result":[1583786142, "1"]},"stats":{"seriesFetched": "42"}}`))
|
||||
case 8:
|
||||
w.Write([]byte(`{"status":"success", "isPartial":true, "data":{"resultType":"scalar","result":[1583786142, "1"]}}`))
|
||||
}
|
||||
})
|
||||
mux.HandleFunc("/render", func(w http.ResponseWriter, _ *http.Request) {
|
||||
c++
|
||||
switch c {
|
||||
case 8:
|
||||
case 9:
|
||||
w.Write([]byte(`[{"target":"constantLine(10)","tags":{"name":"constantLine(10)"},"datapoints":[[10,1611758343],[10,1611758373],[10,1611758403]]}]`))
|
||||
}
|
||||
})
|
||||
@@ -100,9 +102,9 @@ func TestVMInstantQuery(t *testing.T) {
|
||||
t.Fatalf("failed to parse 'time' query param %q: %s", timeParam, err)
|
||||
}
|
||||
switch c {
|
||||
case 9:
|
||||
w.Write([]byte("[]"))
|
||||
case 10:
|
||||
w.Write([]byte("[]"))
|
||||
case 11:
|
||||
w.Write([]byte(`{"status":"success","data":{"resultType":"vector","result":[{"metric":{"__name__":"total","foo":"bar"},"value":[1583786142,"13763"]},{"metric":{"__name__":"total","foo":"baz"},"value":[1583786140,"2000"]}]}}`))
|
||||
}
|
||||
})
|
||||
@@ -203,10 +205,18 @@ func TestVMInstantQuery(t *testing.T) {
|
||||
*res.SeriesFetched)
|
||||
}
|
||||
|
||||
res, _, err = pq.Query(ctx, vmQuery, ts) // 8
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected %s", err)
|
||||
}
|
||||
if res.IsPartial != nil && !*res.IsPartial {
|
||||
t.Fatalf("unexpected metric isPartial want %+v", true)
|
||||
}
|
||||
|
||||
// test graphite
|
||||
gq := s.BuildWithParams(QuerierParams{DataSourceType: string(datasourceGraphite)})
|
||||
|
||||
res, _, err = gq.Query(ctx, queryRender, ts) // 8 - graphite
|
||||
res, _, err = gq.Query(ctx, queryRender, ts) // 9 - graphite
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected %s", err)
|
||||
}
|
||||
@@ -226,9 +236,9 @@ func TestVMInstantQuery(t *testing.T) {
|
||||
vlogs := datasourceVLogs
|
||||
pq = s.BuildWithParams(QuerierParams{DataSourceType: string(vlogs), EvaluationInterval: 15 * time.Second})
|
||||
|
||||
expErr(vlogsQuery, "error parsing response") // 9
|
||||
expErr(vlogsQuery, "error parsing response") // 10
|
||||
|
||||
res, _, err = pq.Query(ctx, vlogsQuery, ts) // 10
|
||||
res, _, err = pq.Query(ctx, vlogsQuery, ts) // 11
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected %s", err)
|
||||
}
|
||||
|
||||
@@ -34,6 +34,9 @@ type Result struct {
|
||||
// If nil, then this feature is not supported by the datasource.
|
||||
// SeriesFetched is supported by VictoriaMetrics since v1.90.
|
||||
SeriesFetched *int
|
||||
// IsPartial is used by VictoriaMetrics to indicate
|
||||
// whether response data is partial.
|
||||
IsPartial *bool
|
||||
}
|
||||
|
||||
// QuerierBuilder builds Querier with given params.
|
||||
|
||||
@@ -10,8 +10,9 @@ import (
|
||||
// FakeQuerier is a mock querier that return predefined results and error message
|
||||
type FakeQuerier struct {
|
||||
sync.Mutex
|
||||
metrics []Metric
|
||||
err error
|
||||
metrics []Metric
|
||||
err error
|
||||
isPartial *bool
|
||||
}
|
||||
|
||||
// SetErr sets query error message
|
||||
@@ -21,11 +22,19 @@ func (fq *FakeQuerier) SetErr(err error) {
|
||||
fq.Unlock()
|
||||
}
|
||||
|
||||
// SetPartialResponse marks query response as partial
|
||||
func (fq *FakeQuerier) SetPartialResponse(partial bool) {
|
||||
fq.Lock()
|
||||
fq.isPartial = &partial
|
||||
fq.Unlock()
|
||||
}
|
||||
|
||||
// Reset reset querier's error message and results
|
||||
func (fq *FakeQuerier) Reset() {
|
||||
fq.Lock()
|
||||
fq.err = nil
|
||||
fq.metrics = fq.metrics[:0]
|
||||
fq.isPartial = nil
|
||||
fq.Unlock()
|
||||
}
|
||||
|
||||
@@ -57,7 +66,7 @@ func (fq *FakeQuerier) Query(_ context.Context, _ string, _ time.Time) (Result,
|
||||
cp := make([]Metric, len(fq.metrics))
|
||||
copy(cp, fq.metrics)
|
||||
req, _ := http.NewRequest(http.MethodPost, "foo.com", nil)
|
||||
return Result{Data: cp}, req, nil
|
||||
return Result{Data: cp, IsPartial: fq.isPartial}, req, nil
|
||||
}
|
||||
|
||||
// FakeQuerierWithRegistry can store different results for different query expr
|
||||
|
||||
@@ -11,7 +11,6 @@ import (
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/vmalertutil"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/flagutil"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/httputil"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/netutil"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promauth"
|
||||
)
|
||||
|
||||
@@ -83,11 +82,10 @@ func Init(extraParams url.Values) (QuerierBuilder, error) {
|
||||
if err := httputil.CheckURL(*addr); err != nil {
|
||||
return nil, fmt.Errorf("invalid -datasource.url: %w", err)
|
||||
}
|
||||
tr, err := promauth.NewTLSTransport(*tlsCertFile, *tlsKeyFile, *tlsCAFile, *tlsServerName, *tlsInsecureSkipVerify)
|
||||
tr, err := promauth.NewTLSTransport(*tlsCertFile, *tlsKeyFile, *tlsCAFile, *tlsServerName, *tlsInsecureSkipVerify, "vmalert_datasource")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create transport for -datasource.url=%q: %w", *addr, err)
|
||||
}
|
||||
tr.DialContext = netutil.NewStatDialFunc("vmalert_datasource")
|
||||
tr.DisableKeepAlives = *disableKeepAlive
|
||||
tr.MaxIdleConnsPerHost = *maxIdleConnections
|
||||
if tr.MaxIdleConns != 0 && tr.MaxIdleConns < tr.MaxIdleConnsPerHost {
|
||||
|
||||
@@ -161,7 +161,7 @@ func NewAlertManager(alertManagerURL string, fn AlertURLGenerator, authCfg proma
|
||||
if authCfg.TLSConfig != nil {
|
||||
tls = authCfg.TLSConfig
|
||||
}
|
||||
tr, err := promauth.NewTLSTransport(tls.CertFile, tls.KeyFile, tls.CAFile, tls.ServerName, tls.InsecureSkipVerify)
|
||||
tr, err := promauth.NewTLSTransport(tls.CertFile, tls.KeyFile, tls.CAFile, tls.ServerName, tls.InsecureSkipVerify, "vmalert_notifier")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create transport for alertmanager URL=%q: %w", alertManagerURL, err)
|
||||
|
||||
@@ -198,8 +198,10 @@ func NewAlertManager(alertManagerURL string, fn AlertURLGenerator, authCfg proma
|
||||
argFunc: fn,
|
||||
authCfg: aCfg,
|
||||
relabelConfigs: relabelCfg,
|
||||
client: &http.Client{Transport: tr},
|
||||
timeout: timeout,
|
||||
metrics: newNotifierMetrics(alertManagerURL),
|
||||
client: &http.Client{
|
||||
Transport: tr,
|
||||
},
|
||||
timeout: timeout,
|
||||
metrics: newNotifierMetrics(alertManagerURL),
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ import (
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/vmalertutil"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/flagutil"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/httputil"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/netutil"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promauth"
|
||||
)
|
||||
|
||||
@@ -70,12 +69,11 @@ func Init() (datasource.QuerierBuilder, error) {
|
||||
if err := httputil.CheckURL(*addr); err != nil {
|
||||
return nil, fmt.Errorf("invalid -remoteRead.url: %w", err)
|
||||
}
|
||||
tr, err := promauth.NewTLSTransport(*tlsCertFile, *tlsKeyFile, *tlsCAFile, *tlsServerName, *tlsInsecureSkipVerify)
|
||||
tr, err := promauth.NewTLSTransport(*tlsCertFile, *tlsKeyFile, *tlsCAFile, *tlsServerName, *tlsInsecureSkipVerify, "vmalert_remoteread")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create transport for -remoteRead.url=%q: %w", *addr, err)
|
||||
}
|
||||
tr.IdleConnTimeout = *idleConnectionTimeout
|
||||
tr.DialContext = netutil.NewStatDialFunc("vmalert_remoteread")
|
||||
|
||||
endpointParams, err := flagutil.ParseJSONMap(*oauth2EndpointParams)
|
||||
if err != nil {
|
||||
|
||||
@@ -93,7 +93,7 @@ func NewClient(ctx context.Context, cfg Config) (*Client, error) {
|
||||
cfg.FlushInterval = defaultFlushInterval
|
||||
}
|
||||
if cfg.Transport == nil {
|
||||
cfg.Transport = httputil.NewTransport(false)
|
||||
cfg.Transport = httputil.NewTransport(false, "vmalert_remotewrite")
|
||||
}
|
||||
cc := defaultConcurrency
|
||||
if cfg.Concurrency > 0 {
|
||||
|
||||
@@ -33,7 +33,7 @@ func NewDebugClient() (*DebugClient, error) {
|
||||
if err := httputil.CheckURL(*addr); err != nil {
|
||||
return nil, fmt.Errorf("invalid -remoteWrite.url: %w", err)
|
||||
}
|
||||
tr, err := promauth.NewTLSTransport(*tlsCertFile, *tlsKeyFile, *tlsCAFile, *tlsServerName, *tlsInsecureSkipVerify)
|
||||
tr, err := promauth.NewTLSTransport(*tlsCertFile, *tlsKeyFile, *tlsCAFile, *tlsServerName, *tlsInsecureSkipVerify, "vmalert_remotewrite_debug")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create transport for -remoteWrite.url=%q: %w", *addr, err)
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/vmalertutil"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/flagutil"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/httputil"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/netutil"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promauth"
|
||||
)
|
||||
|
||||
@@ -73,12 +72,11 @@ func Init(ctx context.Context) (*Client, error) {
|
||||
if err := httputil.CheckURL(*addr); err != nil {
|
||||
return nil, fmt.Errorf("invalid -remoteWrite.url: %w", err)
|
||||
}
|
||||
tr, err := promauth.NewTLSTransport(*tlsCertFile, *tlsKeyFile, *tlsCAFile, *tlsServerName, *tlsInsecureSkipVerify)
|
||||
tr, err := promauth.NewTLSTransport(*tlsCertFile, *tlsKeyFile, *tlsCAFile, *tlsServerName, *tlsInsecureSkipVerify, "vmalert_remotewrite")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create transport for -remoteWrite.url=%q: %w", *addr, err)
|
||||
}
|
||||
tr.IdleConnTimeout = *idleConnectionTimeout
|
||||
tr.DialContext = netutil.NewStatDialFunc("vmalert_remotewrite")
|
||||
|
||||
endpointParams, err := flagutil.ParseJSONMap(*oauth2EndpointParams)
|
||||
if err != nil {
|
||||
|
||||
@@ -207,8 +207,8 @@ func (ar *AlertingRule) logDebugf(at time.Time, a *notifier.Alert, format string
|
||||
if !ar.Debug {
|
||||
return
|
||||
}
|
||||
prefix := fmt.Sprintf("DEBUG rule %q:%q (%d) at %v: ",
|
||||
ar.GroupName, ar.Name, ar.RuleID, at.Format(time.RFC3339))
|
||||
prefix := fmt.Sprintf("DEBUG alerting rule %q, %q:%q (%d) at %v: ",
|
||||
ar.File, ar.GroupName, ar.Name, ar.RuleID, at.Format(time.RFC3339))
|
||||
|
||||
if a != nil {
|
||||
labelKeys := make([]string, len(a.Labels))
|
||||
@@ -408,8 +408,8 @@ func (ar *AlertingRule) exec(ctx context.Context, ts time.Time, limit int) ([]pr
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to execute query %q: %w", ar.Expr, err)
|
||||
}
|
||||
ar.logDebugf(ts, nil, "query returned %d samples (elapsed: %s)", curState.Samples, curState.Duration)
|
||||
|
||||
ar.logDebugf(ts, nil, "query returned %d samples (elapsed: %s, isPartial: %t)", curState.Samples, curState.Duration, isPartialResponse(res))
|
||||
qFn := func(query string) ([]datasource.Metric, error) {
|
||||
res, _, err := ar.q.Query(ctx, query, ts)
|
||||
return res.Data, err
|
||||
|
||||
@@ -22,6 +22,32 @@ import (
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promutil"
|
||||
)
|
||||
|
||||
func TestNewAlertingRule(t *testing.T) {
|
||||
f := func(group *Group, rule config.Rule, expectRule *AlertingRule) {
|
||||
t.Helper()
|
||||
|
||||
r := NewAlertingRule(&datasource.FakeQuerier{}, group, rule)
|
||||
if err := CompareRules(t, expectRule, r); err != nil {
|
||||
t.Fatalf("unexpected rule mismatch: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
f(&Group{Name: "foo"},
|
||||
config.Rule{
|
||||
Alert: "health",
|
||||
Expr: "up == 0",
|
||||
Labels: map[string]string{
|
||||
"foo": "bar",
|
||||
},
|
||||
}, &AlertingRule{
|
||||
Name: "health",
|
||||
Expr: "up == 0",
|
||||
Labels: map[string]string{
|
||||
"foo": "bar",
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func TestAlertingRuleToTimeSeries(t *testing.T) {
|
||||
timestamp := time.Now()
|
||||
|
||||
@@ -1322,3 +1348,23 @@ func TestAlertingRule_ToLabels(t *testing.T) {
|
||||
t.Fatalf("processed labels mismatch, got: %v, want: %v", ls.processed, expectedProcessedLabels)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAlertingRuleExec_Partial(t *testing.T) {
|
||||
fq := &datasource.FakeQuerier{}
|
||||
fq.Add(metricWithValueAndLabels(t, 10, "__name__", "bar"))
|
||||
fq.SetPartialResponse(true)
|
||||
|
||||
ar := newTestAlertingRule("test", 0)
|
||||
ar.Debug = true
|
||||
ar.Labels = map[string]string{"job": "test"}
|
||||
ar.q = fq
|
||||
ar.For = time.Second
|
||||
|
||||
fq.Add(metricWithValueAndLabels(t, 1, "__name__", "foo", "job", "bar"))
|
||||
|
||||
ts := time.Now()
|
||||
_, err := ar.exec(context.TODO(), ts, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -143,6 +143,7 @@ func TestUpdateWith(t *testing.T) {
|
||||
}}, []config.Rule{{
|
||||
Record: "foo",
|
||||
Expr: "min(up)",
|
||||
Debug: true,
|
||||
Labels: map[string]string{
|
||||
"baz": "bar",
|
||||
},
|
||||
|
||||
@@ -30,6 +30,7 @@ type RecordingRule struct {
|
||||
GroupID uint64
|
||||
GroupName string
|
||||
File string
|
||||
Debug bool
|
||||
|
||||
q datasource.Querier
|
||||
|
||||
@@ -91,12 +92,14 @@ func NewRecordingRule(qb datasource.QuerierBuilder, group *Group, cfg config.Rul
|
||||
GroupID: group.GetID(),
|
||||
GroupName: group.Name,
|
||||
File: group.File,
|
||||
Debug: cfg.Debug,
|
||||
q: qb.BuildWithParams(datasource.QuerierParams{
|
||||
DataSourceType: group.Type.String(),
|
||||
ApplyIntervalAsTimeFilter: setIntervalAsTimeFilter(group.Type.String(), cfg.Expr),
|
||||
EvaluationInterval: group.Interval,
|
||||
QueryParams: group.Params,
|
||||
Headers: group.Headers,
|
||||
Debug: cfg.Debug,
|
||||
}),
|
||||
}
|
||||
|
||||
@@ -169,6 +172,8 @@ func (rr *RecordingRule) exec(ctx context.Context, ts time.Time, limit int) ([]p
|
||||
return nil, curState.Err
|
||||
}
|
||||
|
||||
rr.logDebugf(ts, "query returned %d samples (elapsed: %s, isPartial: %t)", curState.Samples, curState.Duration, isPartialResponse(res))
|
||||
|
||||
qMetrics := res.Data
|
||||
numSeries := len(qMetrics)
|
||||
if limit > 0 && numSeries > limit {
|
||||
@@ -202,6 +207,17 @@ func (rr *RecordingRule) exec(ctx context.Context, ts time.Time, limit int) ([]p
|
||||
return tss, nil
|
||||
}
|
||||
|
||||
func (rr *RecordingRule) logDebugf(at time.Time, format string, args ...any) {
|
||||
if !rr.Debug {
|
||||
return
|
||||
}
|
||||
prefix := fmt.Sprintf("DEBUG recording rule %q, %q:%q (%d) at %v: ",
|
||||
rr.File, rr.GroupName, rr.Name, rr.RuleID, at.Format(time.RFC3339))
|
||||
|
||||
msg := fmt.Sprintf(format, args...)
|
||||
logger.Infof("%s", prefix+msg)
|
||||
}
|
||||
|
||||
func stringToLabels(s string) []prompbmarshal.Label {
|
||||
labels := strings.Split(s, ",")
|
||||
rLabels := make([]prompbmarshal.Label, 0, len(labels))
|
||||
|
||||
@@ -9,11 +9,34 @@ import (
|
||||
|
||||
"github.com/VictoriaMetrics/metrics"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/config"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/datasource"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/decimal"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/prompbmarshal"
|
||||
)
|
||||
|
||||
func TestNewRecordingRule(t *testing.T) {
|
||||
f := func(group *Group, rule config.Rule, expectRule *AlertingRule) {
|
||||
t.Helper()
|
||||
|
||||
r := NewAlertingRule(&datasource.FakeQuerier{}, group, rule)
|
||||
if err := CompareRules(t, expectRule, r); err != nil {
|
||||
t.Fatalf("unexpected rule mismatch: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
f(&Group{Name: "foo"},
|
||||
config.Rule{
|
||||
Alert: "health",
|
||||
Expr: "up == 0",
|
||||
Labels: map[string]string{},
|
||||
}, &AlertingRule{
|
||||
Name: "health",
|
||||
Expr: "up == 0",
|
||||
Labels: map[string]string{},
|
||||
})
|
||||
}
|
||||
|
||||
func TestRecordingRule_Exec(t *testing.T) {
|
||||
ts, _ := time.Parse(time.RFC3339, "2024-10-29T00:00:00Z")
|
||||
const defaultStep = 5 * time.Millisecond
|
||||
@@ -307,7 +330,8 @@ func TestRecordingRuleLimit_Failure(t *testing.T) {
|
||||
|
||||
fq := &datasource.FakeQuerier{}
|
||||
fq.Add(testMetrics...)
|
||||
rule := &RecordingRule{Name: "job:foo",
|
||||
rule := &RecordingRule{
|
||||
Name: "job:foo",
|
||||
state: &ruleState{entries: make([]StateEntry, 10)},
|
||||
Labels: map[string]string{
|
||||
"source": "test_limit",
|
||||
@@ -342,7 +366,8 @@ func TestRecordingRuleLimit_Success(t *testing.T) {
|
||||
|
||||
fq := &datasource.FakeQuerier{}
|
||||
fq.Add(testMetrics...)
|
||||
rule := &RecordingRule{Name: "job:foo",
|
||||
rule := &RecordingRule{
|
||||
Name: "job:foo",
|
||||
state: &ruleState{entries: make([]StateEntry, 10)},
|
||||
Labels: map[string]string{
|
||||
"source": "test_limit",
|
||||
@@ -421,3 +446,36 @@ func TestSetIntervalAsTimeFilter(t *testing.T) {
|
||||
f(`error AND _time:5m | count()`, "vlogs", false)
|
||||
f(`* | error AND _time:5m | count()`, "vlogs", false)
|
||||
}
|
||||
|
||||
func TestRecordingRuleExec_Partial(t *testing.T) {
|
||||
ts, _ := time.Parse(time.RFC3339, "2024-10-29T00:00:00Z")
|
||||
fq := &datasource.FakeQuerier{}
|
||||
|
||||
m := metricWithValueAndLabels(t, 10, "__name__", "bar")
|
||||
fq.Add(m)
|
||||
fq.SetPartialResponse(true)
|
||||
rule := &RecordingRule{
|
||||
GroupName: "Bar",
|
||||
Name: "foo",
|
||||
state: &ruleState{
|
||||
entries: make([]StateEntry, 10),
|
||||
},
|
||||
}
|
||||
rule.Debug = true
|
||||
rule.q = fq
|
||||
got, err := rule.exec(context.TODO(), ts, 0)
|
||||
want := []prompbmarshal.TimeSeries{
|
||||
newTimeSeries([]float64{10}, []int64{ts.UnixNano()}, []prompbmarshal.Label{
|
||||
{
|
||||
Name: "__name__",
|
||||
Value: "foo",
|
||||
},
|
||||
}),
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("fail to test rule %s: unexpected error: %s", rule.Name, err)
|
||||
}
|
||||
if err := compareTimeSeries(t, want, got); err != nil {
|
||||
t.Fatalf("fail to test rule %s: time series mismatch: %s", rule.Name, err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,6 +38,9 @@ func compareRecordingRules(t *testing.T, a, b *RecordingRule) error {
|
||||
if a.Expr != b.Expr {
|
||||
return fmt.Errorf("expected to have expression %q; got %q", a.Expr, b.Expr)
|
||||
}
|
||||
if a.Debug != b.Debug {
|
||||
return fmt.Errorf("expected to have debug=%t; got %t", a.Debug, b.Debug)
|
||||
}
|
||||
if !reflect.DeepEqual(a.Labels, b.Labels) {
|
||||
return fmt.Errorf("expected to have labels %#v; got %#v", a.Labels, b.Labels)
|
||||
}
|
||||
@@ -64,6 +67,9 @@ func compareAlertingRules(t *testing.T, a, b *AlertingRule) error {
|
||||
if a.Type.String() != b.Type.String() {
|
||||
return fmt.Errorf("expected to have Type %#v; got %#v", a.Type.String(), b.Type.String())
|
||||
}
|
||||
if a.Debug != b.Debug {
|
||||
return fmt.Errorf("expected to have debug=%t; got %t", a.Debug, b.Debug)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -105,3 +105,10 @@ func isSecreteHeader(str string) bool {
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func isPartialResponse(res datasource.Result) bool {
|
||||
if res.IsPartial != nil && *res.IsPartial {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -319,7 +319,7 @@ func tryProcessingRequest(w http.ResponseWriter, r *http.Request, targetURL *url
|
||||
// Timed out request must be counted as errors, since this usually means that the backend is slow.
|
||||
ui.backendErrors.Inc()
|
||||
}
|
||||
return true, false
|
||||
return false, false
|
||||
}
|
||||
if !rtbOK || !rtb.canRetry() {
|
||||
// Request body cannot be re-sent to another backend. Return the error to the client then.
|
||||
@@ -508,7 +508,7 @@ func newRoundTripper(caFileOpt, certFileOpt, keyFileOpt, serverNameOpt string, i
|
||||
return nil, fmt.Errorf("cannot initialize promauth.Config: %w", err)
|
||||
}
|
||||
|
||||
tr := httputil.NewTransport(false)
|
||||
tr := httputil.NewTransport(false, "vmauth_backend")
|
||||
tr.ResponseHeaderTimeout = *responseTimeout
|
||||
// Automatic compression must be disabled in order to fix https://github.com/VictoriaMetrics/VictoriaMetrics/issues/535
|
||||
tr.DisableCompression = true
|
||||
@@ -517,7 +517,6 @@ func newRoundTripper(caFileOpt, certFileOpt, keyFileOpt, serverNameOpt string, i
|
||||
if tr.MaxIdleConns != 0 && tr.MaxIdleConns < tr.MaxIdleConnsPerHost {
|
||||
tr.MaxIdleConns = tr.MaxIdleConnsPerHost
|
||||
}
|
||||
tr.DialContext = netutil.NewStatDialFunc("vmauth_backend")
|
||||
|
||||
rt := cfg.NewRoundTripper(tr)
|
||||
return rt, nil
|
||||
|
||||
@@ -33,7 +33,8 @@ var (
|
||||
"All created snapshots will be automatically deleted. Example: http://victoriametrics:8428/snapshot/delete")
|
||||
dst = flag.String("dst", "", "Where to put the backup on the remote storage. "+
|
||||
"Example: gs://bucket/path/to/backup, s3://bucket/path/to/backup, azblob://container/path/to/backup or fs:///path/to/local/backup/dir\n"+
|
||||
"-dst can point to the previous backup. In this case incremental backup is performed, i.e. only changed data is uploaded")
|
||||
"-dst can point to the previous backup. In this case incremental backup is performed, i.e. only changed data is uploaded\n"+
|
||||
"Note: If custom S3 endpoint is used, URL should contain only name of the bucket, while hostname of S3 server must be specified via the -customS3Endpoint command-line flag.")
|
||||
origin = flag.String("origin", "", "Optional origin directory on the remote storage with old backup for server-side copying when performing full backup. This speeds up full backups")
|
||||
concurrency = flag.Int("concurrency", 10, "The number of concurrent workers. Higher concurrency may reduce backup duration")
|
||||
maxBytesPerSecond = flagutil.NewBytes("maxBytesPerSecond", 0, "The maximum upload speed. There is no limit if it is set to 0")
|
||||
|
||||
@@ -70,7 +70,7 @@ func main() {
|
||||
return fmt.Errorf("invalid -%s: %w", otsdbAddr, err)
|
||||
}
|
||||
|
||||
tr, err := promauth.NewTLSTransport(certFile, keyFile, caFile, serverName, insecureSkipVerify)
|
||||
tr, err := promauth.NewTLSTransport(certFile, keyFile, caFile, serverName, insecureSkipVerify, "vmctl_opentsdb")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create transport for -%s=%q: %s", otsdbAddr, addr, err)
|
||||
}
|
||||
@@ -185,7 +185,7 @@ func main() {
|
||||
serverName := c.String(remoteReadServerName)
|
||||
insecureSkipVerify := c.Bool(remoteReadInsecureSkipVerify)
|
||||
|
||||
tr, err := promauth.NewTLSTransport(certFile, keyFile, caFile, serverName, insecureSkipVerify)
|
||||
tr, err := promauth.NewTLSTransport(certFile, keyFile, caFile, serverName, insecureSkipVerify, "vmctl_remoteread")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create transport for -%s=%q: %s", remoteReadSrcAddr, addr, err)
|
||||
}
|
||||
@@ -315,7 +315,7 @@ func main() {
|
||||
return fmt.Errorf("failed to create TLS Config: %s", err)
|
||||
}
|
||||
|
||||
trSrc := httputil.NewTransport(false)
|
||||
trSrc := httputil.NewTransport(false, "vmctl_src")
|
||||
trSrc.DisableKeepAlives = disableKeepAlive
|
||||
trSrc.TLSClientConfig = srcTC
|
||||
|
||||
@@ -345,7 +345,7 @@ func main() {
|
||||
return fmt.Errorf("failed to create TLS Config: %s", err)
|
||||
}
|
||||
|
||||
trDst := httputil.NewTransport(false)
|
||||
trDst := httputil.NewTransport(false, "vmctl_dst")
|
||||
trDst.DisableKeepAlives = disableKeepAlive
|
||||
trDst.TLSClientConfig = dstTC
|
||||
|
||||
@@ -455,7 +455,7 @@ func initConfigVM(c *cli.Context) (vm.Config, error) {
|
||||
serverName := c.String(vmServerName)
|
||||
insecureSkipVerify := c.Bool(vmInsecureSkipVerify)
|
||||
|
||||
tr, err := promauth.NewTLSTransport(certFile, keyFile, caFile, serverName, insecureSkipVerify)
|
||||
tr, err := promauth.NewTLSTransport(certFile, keyFile, caFile, serverName, insecureSkipVerify, "vmctl_client")
|
||||
if err != nil {
|
||||
return vm.Config{}, fmt.Errorf("failed to create transport for -%s=%q: %s", vmAddr, addr, err)
|
||||
}
|
||||
|
||||
@@ -43,60 +43,12 @@ func (pp *prometheusProcessor) run() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
bar := barpool.AddWithTemplate(fmt.Sprintf(barTpl, "Processing blocks"), len(blocks))
|
||||
if err := barpool.Start(); err != nil {
|
||||
return err
|
||||
}
|
||||
defer barpool.Stop()
|
||||
|
||||
blockReadersCh := make(chan tsdb.BlockReader)
|
||||
errCh := make(chan error, pp.cc)
|
||||
pp.im.ResetStats()
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(pp.cc)
|
||||
for i := 0; i < pp.cc; i++ {
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for br := range blockReadersCh {
|
||||
if err := pp.do(br); err != nil {
|
||||
errCh <- fmt.Errorf("read failed for block %q: %s", br.Meta().ULID, err)
|
||||
return
|
||||
}
|
||||
bar.Increment()
|
||||
}
|
||||
}()
|
||||
}
|
||||
// any error breaks the import
|
||||
for _, br := range blocks {
|
||||
select {
|
||||
case promErr := <-errCh:
|
||||
close(blockReadersCh)
|
||||
return fmt.Errorf("prometheus error: %s", promErr)
|
||||
case vmErr := <-pp.im.Errors():
|
||||
close(blockReadersCh)
|
||||
return fmt.Errorf("import process failed: %s", wrapErr(vmErr, pp.isVerbose))
|
||||
case blockReadersCh <- br:
|
||||
}
|
||||
}
|
||||
|
||||
close(blockReadersCh)
|
||||
wg.Wait()
|
||||
// wait for all buffers to flush
|
||||
pp.im.Close()
|
||||
close(errCh)
|
||||
// drain import errors channel
|
||||
for vmErr := range pp.im.Errors() {
|
||||
if vmErr.Err != nil {
|
||||
return fmt.Errorf("import process failed: %s", wrapErr(vmErr, pp.isVerbose))
|
||||
}
|
||||
}
|
||||
for err := range errCh {
|
||||
return fmt.Errorf("import process failed: %s", err)
|
||||
if err := pp.processBlocks(blocks); err != nil {
|
||||
return fmt.Errorf("migration failed: %s", err)
|
||||
}
|
||||
|
||||
log.Println("Import finished!")
|
||||
log.Print(pp.im.Stats())
|
||||
log.Println(pp.im.Stats())
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -156,3 +108,59 @@ func (pp *prometheusProcessor) do(b tsdb.BlockReader) error {
|
||||
}
|
||||
return ss.Err()
|
||||
}
|
||||
|
||||
func (pp *prometheusProcessor) processBlocks(blocks []tsdb.BlockReader) error {
|
||||
bar := barpool.AddWithTemplate(fmt.Sprintf(barTpl, "Processing blocks"), len(blocks))
|
||||
if err := barpool.Start(); err != nil {
|
||||
return err
|
||||
}
|
||||
defer barpool.Stop()
|
||||
|
||||
blockReadersCh := make(chan tsdb.BlockReader)
|
||||
errCh := make(chan error, pp.cc)
|
||||
pp.im.ResetStats()
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(pp.cc)
|
||||
for i := 0; i < pp.cc; i++ {
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for br := range blockReadersCh {
|
||||
if err := pp.do(br); err != nil {
|
||||
errCh <- fmt.Errorf("read failed for block %q: %s", br.Meta().ULID, err)
|
||||
return
|
||||
}
|
||||
bar.Increment()
|
||||
}
|
||||
}()
|
||||
}
|
||||
// any error breaks the import
|
||||
for _, br := range blocks {
|
||||
select {
|
||||
case promErr := <-errCh:
|
||||
close(blockReadersCh)
|
||||
return fmt.Errorf("prometheus error: %s", promErr)
|
||||
case vmErr := <-pp.im.Errors():
|
||||
close(blockReadersCh)
|
||||
return fmt.Errorf("import process failed: %s", wrapErr(vmErr, pp.isVerbose))
|
||||
case blockReadersCh <- br:
|
||||
}
|
||||
}
|
||||
|
||||
close(blockReadersCh)
|
||||
wg.Wait()
|
||||
// wait for all buffers to flush
|
||||
pp.im.Close()
|
||||
close(errCh)
|
||||
// drain import errors channel
|
||||
for vmErr := range pp.im.Errors() {
|
||||
if vmErr.Err != nil {
|
||||
return fmt.Errorf("import process failed: %s", wrapErr(vmErr, pp.isVerbose))
|
||||
}
|
||||
}
|
||||
for err := range errCh {
|
||||
return fmt.Errorf("import process failed: %s", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -2,167 +2,214 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/prometheus/prometheus/model/labels"
|
||||
"github.com/prometheus/prometheus/storage"
|
||||
"github.com/prometheus/prometheus/tsdb"
|
||||
"github.com/prometheus/prometheus/tsdb/chunkenc"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmctl/backoff"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmctl/barpool"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmctl/prometheus"
|
||||
remote_read_integration "github.com/VictoriaMetrics/VictoriaMetrics/app/vmctl/testdata/servers_integration_test"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmctl/vm"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmselect/promql"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmstorage"
|
||||
)
|
||||
|
||||
// If you want to run this test:
|
||||
// 1. provide test snapshot path in const testSnapshot
|
||||
// 2. define httpAddr const with your victoriametrics address
|
||||
// 3. run victoria metrics with defined address
|
||||
// 4. remove t.Skip() from Test_prometheusProcessor_run
|
||||
// 5. run tests one by one not all at one time
|
||||
|
||||
const (
|
||||
httpAddr = "http://127.0.0.1:8428/"
|
||||
testSnapshot = "./testdata/20220427T130947Z-70ba49b1093fd0bf"
|
||||
testSnapshot = "./testdata/snapshots/20250118T124506Z-59d1b952d7eaf547"
|
||||
blockData = "./testdata/snapshots/20250118T124506Z-59d1b952d7eaf547/01JHWQ445Y2P1TDYB05AEKD6MC"
|
||||
)
|
||||
|
||||
// This test simulates close process if user abort it
|
||||
func TestPrometheusProcessorRun(t *testing.T) {
|
||||
t.Skip()
|
||||
|
||||
defer func() { isSilent = false }()
|
||||
f := func(startStr, endStr string, numOfSeries int, resultExpected []vm.TimeSeries) {
|
||||
t.Helper()
|
||||
|
||||
type fields struct {
|
||||
cfg prometheus.Config
|
||||
vmCfg vm.Config
|
||||
cl func(prometheus.Config) *prometheus.Client
|
||||
im func(vm.Config) *vm.Importer
|
||||
closer func(importer *vm.Importer)
|
||||
cc int
|
||||
}
|
||||
type args struct {
|
||||
silent bool
|
||||
verbose bool
|
||||
}
|
||||
dst := remote_read_integration.NewRemoteWriteServer(t)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "simulate syscall.SIGINT",
|
||||
fields: fields{
|
||||
cfg: prometheus.Config{
|
||||
Snapshot: testSnapshot,
|
||||
Filter: prometheus.Filter{},
|
||||
},
|
||||
cl: func(cfg prometheus.Config) *prometheus.Client {
|
||||
client, err := prometheus.NewClient(cfg)
|
||||
if err != nil {
|
||||
t.Fatalf("error init prometeus client: %s", err)
|
||||
}
|
||||
return client
|
||||
},
|
||||
im: func(vmCfg vm.Config) *vm.Importer {
|
||||
importer, err := vm.NewImporter(context.Background(), vmCfg)
|
||||
if err != nil {
|
||||
t.Fatalf("error init importer: %s", err)
|
||||
}
|
||||
return importer
|
||||
},
|
||||
closer: func(importer *vm.Importer) {
|
||||
// simulate syscall.SIGINT
|
||||
time.Sleep(time.Second * 5)
|
||||
if importer != nil {
|
||||
importer.Close()
|
||||
}
|
||||
},
|
||||
vmCfg: vm.Config{Addr: httpAddr, Concurrency: 1},
|
||||
cc: 2,
|
||||
},
|
||||
args: args{
|
||||
silent: false,
|
||||
verbose: false,
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "simulate correct work",
|
||||
fields: fields{
|
||||
cfg: prometheus.Config{
|
||||
Snapshot: testSnapshot,
|
||||
Filter: prometheus.Filter{},
|
||||
},
|
||||
cl: func(cfg prometheus.Config) *prometheus.Client {
|
||||
client, err := prometheus.NewClient(cfg)
|
||||
if err != nil {
|
||||
t.Fatalf("error init prometeus client: %s", err)
|
||||
}
|
||||
return client
|
||||
},
|
||||
im: func(vmCfg vm.Config) *vm.Importer {
|
||||
importer, err := vm.NewImporter(context.Background(), vmCfg)
|
||||
if err != nil {
|
||||
t.Fatalf("error init importer: %s", err)
|
||||
}
|
||||
return importer
|
||||
},
|
||||
closer: nil,
|
||||
vmCfg: vm.Config{Addr: httpAddr, Concurrency: 5},
|
||||
cc: 2,
|
||||
},
|
||||
args: args{
|
||||
silent: true,
|
||||
verbose: false,
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
client := tt.fields.cl(tt.fields.cfg)
|
||||
importer := tt.fields.im(tt.fields.vmCfg)
|
||||
isSilent = tt.args.silent
|
||||
pp := &prometheusProcessor{
|
||||
cl: client,
|
||||
im: importer,
|
||||
cc: tt.fields.cc,
|
||||
isVerbose: tt.args.verbose,
|
||||
}
|
||||
defer func() {
|
||||
dst.Close()
|
||||
}()
|
||||
|
||||
// we should answer on prompt
|
||||
if !tt.args.silent {
|
||||
input := []byte("Y\n")
|
||||
dst.Series(resultExpected)
|
||||
dst.ExpectedSeries(resultExpected)
|
||||
|
||||
r, w, err := os.Pipe()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := fillStorage(resultExpected); err != nil {
|
||||
t.Fatalf("cannot fill storage: %s", err)
|
||||
}
|
||||
|
||||
_, err = w.Write(input)
|
||||
if err != nil {
|
||||
t.Fatalf("cannot send 'Y' to importer: %s", err)
|
||||
}
|
||||
err = w.Close()
|
||||
if err != nil {
|
||||
t.Fatalf("cannot close writer: %s", err)
|
||||
}
|
||||
isSilent = true
|
||||
defer func() { isSilent = false }()
|
||||
|
||||
stdin := os.Stdin
|
||||
// Restore stdin right after the test.
|
||||
defer func() {
|
||||
os.Stdin = stdin
|
||||
_ = r.Close()
|
||||
}()
|
||||
os.Stdin = r
|
||||
}
|
||||
bf, err := backoff.New(1, 1.8, time.Second*2)
|
||||
if err != nil {
|
||||
t.Fatalf("cannot create backoff: %s", err)
|
||||
}
|
||||
|
||||
// simulate close if needed
|
||||
if tt.fields.closer != nil {
|
||||
go tt.fields.closer(importer)
|
||||
}
|
||||
importerCfg := vm.Config{
|
||||
Addr: dst.URL(),
|
||||
Transport: nil,
|
||||
Concurrency: 1,
|
||||
Backoff: bf,
|
||||
}
|
||||
|
||||
if err := pp.run(); (err != nil) != tt.wantErr {
|
||||
t.Fatalf("run() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
ctx := context.Background()
|
||||
importer, err := vm.NewImporter(ctx, importerCfg)
|
||||
if err != nil {
|
||||
t.Fatalf("cannot create importer: %s", err)
|
||||
}
|
||||
defer importer.Close()
|
||||
|
||||
matchName := "__name__"
|
||||
matchValue := ".*"
|
||||
filter := prometheus.Filter{
|
||||
TimeMin: startStr,
|
||||
TimeMax: endStr,
|
||||
Label: matchName,
|
||||
LabelValue: matchValue,
|
||||
}
|
||||
|
||||
runnner, err := prometheus.NewClient(prometheus.Config{
|
||||
Snapshot: testSnapshot,
|
||||
Filter: filter,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("cannot create prometheus client: %s", err)
|
||||
}
|
||||
p := &prometheusProcessor{
|
||||
cl: runnner,
|
||||
im: importer,
|
||||
cc: 1,
|
||||
}
|
||||
|
||||
if err := p.run(); err != nil {
|
||||
t.Fatalf("run() error: %s", err)
|
||||
}
|
||||
|
||||
collectedTs := dst.GetCollectedTimeSeries()
|
||||
t.Logf("collected timeseries: %d; expected timeseries: %d", len(collectedTs), len(resultExpected))
|
||||
if len(collectedTs) != len(resultExpected) {
|
||||
t.Fatalf("unexpected number of collected time series; got %d; want %d", len(collectedTs), numOfSeries)
|
||||
}
|
||||
|
||||
deleted, err := deleteSeries(matchName, matchValue)
|
||||
if err != nil {
|
||||
t.Fatalf("cannot delete series: %s", err)
|
||||
}
|
||||
if deleted != numOfSeries {
|
||||
t.Fatalf("unexpected number of deleted series; got %d; want %d", deleted, numOfSeries)
|
||||
}
|
||||
}
|
||||
|
||||
processFlags()
|
||||
vmstorage.Init(promql.ResetRollupResultCacheIfNeeded)
|
||||
defer func() {
|
||||
vmstorage.Stop()
|
||||
if err := os.RemoveAll(storagePath); err != nil {
|
||||
log.Fatalf("cannot remove %q: %s", storagePath, err)
|
||||
}
|
||||
}()
|
||||
|
||||
barpool.Disable(true)
|
||||
defer func() {
|
||||
barpool.Disable(false)
|
||||
}()
|
||||
|
||||
b, err := tsdb.OpenBlock(nil, blockData, nil, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("cannot open block: %s", err)
|
||||
}
|
||||
// timestamp is equal to minTime and maxTime from meta.json
|
||||
ss, err := readBlock(b, 1737204082361, 1737204302539)
|
||||
if err != nil {
|
||||
t.Fatalf("cannot read block: %s", err)
|
||||
}
|
||||
|
||||
resultExpected, err := prepareExpectedData(ss)
|
||||
if err != nil {
|
||||
t.Fatalf("cannot prepare expected data: %s", err)
|
||||
}
|
||||
|
||||
f("2025-01-18T12:40:00Z", "2025-01-18T12:46:00Z", 2792, resultExpected)
|
||||
}
|
||||
|
||||
func readBlock(b tsdb.BlockReader, timeMin int64, timeMax int64) (storage.SeriesSet, error) {
|
||||
minTime, maxTime := b.Meta().MinTime, b.Meta().MaxTime
|
||||
|
||||
if timeMin != 0 {
|
||||
minTime = timeMin
|
||||
}
|
||||
if timeMax != 0 {
|
||||
maxTime = timeMax
|
||||
}
|
||||
|
||||
q, err := tsdb.NewBlockQuerier(b, minTime, maxTime)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
matchName := "__name__"
|
||||
matchValue := ".*"
|
||||
ctx := context.Background()
|
||||
ss := q.Select(ctx, false, nil, labels.MustNewMatcher(labels.MatchRegexp, matchName, matchValue))
|
||||
return ss, nil
|
||||
}
|
||||
|
||||
func prepareExpectedData(ss storage.SeriesSet) ([]vm.TimeSeries, error) {
|
||||
var expectedSeriesSet []vm.TimeSeries
|
||||
var it chunkenc.Iterator
|
||||
for ss.Next() {
|
||||
var name string
|
||||
var labelPairs []vm.LabelPair
|
||||
series := ss.At()
|
||||
|
||||
for _, label := range series.Labels() {
|
||||
if label.Name == "__name__" {
|
||||
name = label.Value
|
||||
continue
|
||||
}
|
||||
labelPairs = append(labelPairs, vm.LabelPair{
|
||||
Name: label.Name,
|
||||
Value: label.Value,
|
||||
})
|
||||
}
|
||||
if name == "" {
|
||||
return nil, fmt.Errorf("failed to find `__name__` label in labelset for block")
|
||||
}
|
||||
|
||||
var timestamps []int64
|
||||
var values []float64
|
||||
it = series.Iterator(it)
|
||||
for {
|
||||
typ := it.Next()
|
||||
if typ == chunkenc.ValNone {
|
||||
break
|
||||
}
|
||||
if typ != chunkenc.ValFloat {
|
||||
// Skip unsupported values
|
||||
continue
|
||||
}
|
||||
t, v := it.At()
|
||||
timestamps = append(timestamps, t)
|
||||
values = append(values, v)
|
||||
}
|
||||
if err := it.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ts := vm.TimeSeries{
|
||||
Name: name,
|
||||
LabelPairs: labelPairs,
|
||||
Timestamps: timestamps,
|
||||
Values: values,
|
||||
}
|
||||
expectedSeriesSet = append(expectedSeriesSet, ts)
|
||||
}
|
||||
return expectedSeriesSet, nil
|
||||
}
|
||||
|
||||
@@ -73,7 +73,7 @@ func TestRemoteRead(t *testing.T) {
|
||||
vmCfg: vm.Config{
|
||||
Addr: "",
|
||||
Concurrency: 1,
|
||||
Transport: httputil.NewTransport(false),
|
||||
Transport: httputil.NewTransport(false, "vmctl_test_read"),
|
||||
},
|
||||
start: "2022-09-26T11:23:05+02:00",
|
||||
end: "2022-11-26T11:24:05+02:00",
|
||||
|
||||
@@ -41,6 +41,7 @@ type RemoteWriteServer struct {
|
||||
server *httptest.Server
|
||||
series []vm.TimeSeries
|
||||
expectedSeries []vm.TimeSeries
|
||||
tss []vm.TimeSeries
|
||||
}
|
||||
|
||||
// NewRemoteWriteServer prepares test remote write server
|
||||
@@ -73,6 +74,10 @@ func (rws *RemoteWriteServer) ExpectedSeries(series []vm.TimeSeries) {
|
||||
rws.expectedSeries = append(rws.expectedSeries, series...)
|
||||
}
|
||||
|
||||
func (rws *RemoteWriteServer) GetCollectedTimeSeries() []vm.TimeSeries {
|
||||
return rws.tss
|
||||
}
|
||||
|
||||
// URL returns server url
|
||||
func (rws *RemoteWriteServer) URL() string {
|
||||
return rws.server.URL
|
||||
@@ -80,7 +85,6 @@ func (rws *RemoteWriteServer) URL() string {
|
||||
|
||||
func (rws *RemoteWriteServer) getWriteHandler(t *testing.T) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
var tss []vm.TimeSeries
|
||||
scanner := bufio.NewScanner(r.Body)
|
||||
var rows parser.Rows
|
||||
for scanner.Scan() {
|
||||
@@ -102,17 +106,11 @@ func (rws *RemoteWriteServer) getWriteHandler(t *testing.T) http.Handler {
|
||||
ts.Timestamps = append(ts.Timestamps, row.Timestamps...)
|
||||
ts.Name = nameValue
|
||||
ts.LabelPairs = labelPairs
|
||||
tss = append(tss, ts)
|
||||
rws.tss = append(rws.tss, ts)
|
||||
}
|
||||
rows.Reset()
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(tss, rws.expectedSeries) {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
t.Fatalf("datasets not equal, expected: %#v; \n got: %#v", rws.expectedSeries, tss)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
return
|
||||
})
|
||||
|
||||
BIN
app/vmctl/testdata/snapshots/20250118T124506Z-59d1b952d7eaf547/01JHWQ445Y2P1TDYB05AEKD6MC/index
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"ulid": "01JHWQ445Y2P1TDYB05AEKD6MC",
|
||||
"minTime": 1737204082361,
|
||||
"maxTime": 1737204302539,
|
||||
"stats": {
|
||||
"numSamples": 60275,
|
||||
"numSeries": 2792,
|
||||
"numChunks": 2792
|
||||
},
|
||||
"compaction": {
|
||||
"level": 1,
|
||||
"sources": [
|
||||
"01JHWQ445Y2P1TDYB05AEKD6MC"
|
||||
]
|
||||
},
|
||||
"version": 1
|
||||
}
|
||||
BIN
app/vmctl/testdata/snapshots/20250118T124506Z-59d1b952d7eaf547/01JHWQ445Y2P1TDYB05AEKD6MC/tombstones
vendored
Normal file
@@ -23,8 +23,9 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
storagePath = "TestStorage"
|
||||
retentionPeriod = "100y"
|
||||
storagePath = "TestStorage"
|
||||
retentionPeriod = "100y"
|
||||
deleteSeriesLimit = 3e3
|
||||
)
|
||||
|
||||
func TestVMNativeProcessorRun(t *testing.T) {
|
||||
@@ -66,7 +67,7 @@ func TestVMNativeProcessorRun(t *testing.T) {
|
||||
t.Fatalf("cannot add series to storage: %s", err)
|
||||
}
|
||||
|
||||
tr := httputil.NewTransport(false)
|
||||
tr := httputil.NewTransport(false, "test_client")
|
||||
tr.DisableKeepAlives = false
|
||||
|
||||
srcClient := &native.Client{
|
||||
@@ -259,7 +260,7 @@ func deleteSeries(name, value string) (int, error) {
|
||||
if err := tfs.Add([]byte(name), []byte(value), false, true); err != nil {
|
||||
return 0, fmt.Errorf("unexpected error in TagFilters.Add: %w", err)
|
||||
}
|
||||
return vmstorage.DeleteSeries(nil, []*storage.TagFilters{tfs}, 1e3)
|
||||
return vmstorage.DeleteSeries(nil, []*storage.TagFilters{tfs}, deleteSeriesLimit)
|
||||
}
|
||||
|
||||
func TestBuildMatchWithFilter_Failure(t *testing.T) {
|
||||
|
||||
@@ -320,8 +320,10 @@ func RequestHandler(w http.ResponseWriter, r *http.Request) bool {
|
||||
case "/prometheus/api/v1/targets", "/api/v1/targets":
|
||||
promscrapeAPIV1TargetsRequests.Inc()
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
// https://prometheus.io/docs/prometheus/latest/querying/api/#targets
|
||||
state := r.FormValue("state")
|
||||
promscrape.WriteAPIV1Targets(w, state)
|
||||
scrapePool := r.FormValue("scrapePool")
|
||||
promscrape.WriteAPIV1Targets(w, state, scrapePool)
|
||||
return true
|
||||
case "/prometheus/target_response", "/target_response":
|
||||
promscrapeTargetResponseRequests.Inc()
|
||||
|
||||
@@ -20,7 +20,8 @@ import (
|
||||
var (
|
||||
httpListenAddr = flag.String("httpListenAddr", ":8421", "TCP address for exporting metrics at /metrics page")
|
||||
src = flag.String("src", "", "Source path with backup on the remote storage. "+
|
||||
"Example: gs://bucket/path/to/backup, s3://bucket/path/to/backup, azblob://container/path/to/backup or fs:///path/to/local/backup")
|
||||
"Example: gs://bucket/path/to/backup, s3://bucket/path/to/backup, azblob://container/path/to/backup or fs:///path/to/local/backup\n"+
|
||||
"Note: If custom S3 endpoint is used, URL should contain only name of the bucket, while hostname of S3 server must be specified via the -customS3Endpoint command-line flag.")
|
||||
storageDataPath = flag.String("storageDataPath", "victoria-metrics-data", "Destination path where backup must be restored. "+
|
||||
"VictoriaMetrics must be stopped when restoring from backup. -storageDataPath dir can be non-empty. In this case the contents of -storageDataPath dir "+
|
||||
"is synchronized with -src contents, i.e. it works like 'rsync --delete'")
|
||||
|
||||
76
app/vmselect/vmui/assets/vendor-PQqNLyna.js
Normal file
@@ -36,8 +36,8 @@
|
||||
<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-Bc6E56oa.js"></script>
|
||||
<link rel="modulepreload" crossorigin href="./assets/vendor-DojlIpLz.js">
|
||||
<script type="module" crossorigin src="./assets/index-Clv2OTUl.js"></script>
|
||||
<link rel="modulepreload" crossorigin href="./assets/vendor-PQqNLyna.js">
|
||||
<link rel="stylesheet" crossorigin href="./assets/vendor-D1GxaB_c.css">
|
||||
<link rel="stylesheet" crossorigin href="./assets/index-u4IOGr0E.css">
|
||||
</head>
|
||||
|
||||
@@ -336,13 +336,7 @@ func RequestHandler(w http.ResponseWriter, r *http.Request) bool {
|
||||
case "/create":
|
||||
snapshotsCreateTotal.Inc()
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
snapshotPath, err := Storage.CreateSnapshot()
|
||||
if err != nil {
|
||||
err = fmt.Errorf("cannot create snapshot: %w", err)
|
||||
jsonResponseError(w, err)
|
||||
snapshotsCreateErrorsTotal.Inc()
|
||||
return true
|
||||
}
|
||||
snapshotPath := Storage.MustCreateSnapshot()
|
||||
if prometheusCompatibleResponse {
|
||||
fmt.Fprintf(w, `{"status":"success","data":{"name":%s}}`, stringsutil.JSONString(snapshotPath))
|
||||
} else {
|
||||
@@ -352,13 +346,7 @@ func RequestHandler(w http.ResponseWriter, r *http.Request) bool {
|
||||
case "/list":
|
||||
snapshotsListTotal.Inc()
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
snapshots, err := Storage.ListSnapshots()
|
||||
if err != nil {
|
||||
err = fmt.Errorf("cannot list snapshots: %w", err)
|
||||
jsonResponseError(w, err)
|
||||
snapshotsListErrorsTotal.Inc()
|
||||
return true
|
||||
}
|
||||
snapshots := Storage.MustListSnapshots()
|
||||
fmt.Fprintf(w, `{"status":"ok","snapshots":[`)
|
||||
if len(snapshots) > 0 {
|
||||
for _, snapshot := range snapshots[:len(snapshots)-1] {
|
||||
@@ -373,13 +361,7 @@ func RequestHandler(w http.ResponseWriter, r *http.Request) bool {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
snapshotName := r.FormValue("snapshot")
|
||||
|
||||
snapshots, err := Storage.ListSnapshots()
|
||||
if err != nil {
|
||||
err = fmt.Errorf("cannot list snapshots: %w", err)
|
||||
jsonResponseError(w, err)
|
||||
snapshotsDeleteErrorsTotal.Inc()
|
||||
return true
|
||||
}
|
||||
snapshots := Storage.MustListSnapshots()
|
||||
for _, snName := range snapshots {
|
||||
if snName == snapshotName {
|
||||
if err := Storage.DeleteSnapshot(snName); err != nil {
|
||||
@@ -393,19 +375,13 @@ func RequestHandler(w http.ResponseWriter, r *http.Request) bool {
|
||||
}
|
||||
}
|
||||
|
||||
err = fmt.Errorf("cannot find snapshot %q", snapshotName)
|
||||
err := fmt.Errorf("cannot find snapshot %q", snapshotName)
|
||||
jsonResponseError(w, err)
|
||||
return true
|
||||
case "/delete_all":
|
||||
snapshotsDeleteAllTotal.Inc()
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
snapshots, err := Storage.ListSnapshots()
|
||||
if err != nil {
|
||||
err = fmt.Errorf("cannot list snapshots: %w", err)
|
||||
jsonResponseError(w, err)
|
||||
snapshotsDeleteAllErrorsTotal.Inc()
|
||||
return true
|
||||
}
|
||||
snapshots := Storage.MustListSnapshots()
|
||||
for _, snapshotName := range snapshots {
|
||||
if err := Storage.DeleteSnapshot(snapshotName); err != nil {
|
||||
err = fmt.Errorf("cannot delete snapshot %q: %w", snapshotName, err)
|
||||
@@ -439,10 +415,7 @@ func initStaleSnapshotsRemover(strg *storage.Storage) {
|
||||
return
|
||||
case <-t.C:
|
||||
}
|
||||
if err := strg.DeleteStaleSnapshots(snapshotsMaxAgeDur); err != nil {
|
||||
// Use logger.Errorf instead of logger.Fatalf in the hope the error is temporary.
|
||||
logger.Errorf("cannot delete stale snapshots: %s", err)
|
||||
}
|
||||
strg.MustDeleteStaleSnapshots(snapshotsMaxAgeDur)
|
||||
}
|
||||
}()
|
||||
}
|
||||
@@ -460,11 +433,9 @@ var (
|
||||
var (
|
||||
activeForceMerges = metrics.NewCounter("vm_active_force_merges")
|
||||
|
||||
snapshotsCreateTotal = metrics.NewCounter(`vm_http_requests_total{path="/snapshot/create"}`)
|
||||
snapshotsCreateErrorsTotal = metrics.NewCounter(`vm_http_request_errors_total{path="/snapshot/create"}`)
|
||||
snapshotsCreateTotal = metrics.NewCounter(`vm_http_requests_total{path="/snapshot/create"}`)
|
||||
|
||||
snapshotsListTotal = metrics.NewCounter(`vm_http_requests_total{path="/snapshot/list"}`)
|
||||
snapshotsListErrorsTotal = metrics.NewCounter(`vm_http_request_errors_total{path="/snapshot/list"}`)
|
||||
snapshotsListTotal = metrics.NewCounter(`vm_http_requests_total{path="/snapshot/list"}`)
|
||||
|
||||
snapshotsDeleteTotal = metrics.NewCounter(`vm_http_requests_total{path="/snapshot/delete"}`)
|
||||
snapshotsDeleteErrorsTotal = metrics.NewCounter(`vm_http_request_errors_total{path="/snapshot/delete"}`)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM golang:1.24.1 AS build-web-stage
|
||||
FROM golang:1.24.2 AS build-web-stage
|
||||
COPY build /build
|
||||
|
||||
WORKDIR /build
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# All these commands must run from repository root.
|
||||
|
||||
copy-metricsql-docs:
|
||||
cp docs/MetricsQL.md app/vmui/packages/vmui/src/assets/MetricsQL.md
|
||||
cp docs/victoriametrics/MetricsQL.md app/vmui/packages/vmui/src/assets/MetricsQL.md
|
||||
|
||||
vmui-package-base-image:
|
||||
docker build -t vmui-builder-image -f app/vmui/Dockerfile-build ./app/vmui
|
||||
|
||||
@@ -6,6 +6,7 @@ import useGetMetricsQL from "../../../hooks/useGetMetricsQL";
|
||||
import { QueryContextType } from "../../../types";
|
||||
import { AUTOCOMPLETE_LIMITS } from "../../../constants/queryAutocomplete";
|
||||
import { QueryEditorAutocompleteProps } from "./QueryEditor";
|
||||
import { getExprLastPart, getValueByContext, getContext } from "./autocompleteUtils";
|
||||
|
||||
const QueryEditorAutocomplete: FC<QueryEditorAutocompleteProps> = ({
|
||||
value,
|
||||
@@ -26,11 +27,7 @@ const QueryEditorAutocomplete: FC<QueryEditorAutocompleteProps> = ({
|
||||
return { beforeCursor, afterCursor };
|
||||
}, [value, caretPosition]);
|
||||
|
||||
const exprLastPart = useMemo(() => {
|
||||
const regexpSplit = /\s(or|and|unless|default|ifnot|if|group_left|group_right)\s|}|\+|\|-|\*|\/|\^/i;
|
||||
const parts = values.beforeCursor.split(regexpSplit);
|
||||
return parts[parts.length - 1];
|
||||
}, [values]);
|
||||
const exprLastPart = useMemo(() => getExprLastPart(values.beforeCursor), [values]);
|
||||
|
||||
const metric = useMemo(() => {
|
||||
const regex1 = /\w+\((?<metricName>[^)]+)\)\s+(by|without|on|ignoring)\s*\(\w*/gi;
|
||||
@@ -54,44 +51,9 @@ const QueryEditorAutocomplete: FC<QueryEditorAutocompleteProps> = ({
|
||||
return match ? match[match.length - 1] : "";
|
||||
}, [exprLastPart]);
|
||||
|
||||
const shouldSuppressAutoSuggestion = (value: string) => {
|
||||
const pattern = /([{(),+\-*/^]|\b(?:or|and|unless|default|ifnot|if|group_left|group_right|by|without|on|ignoring)\b)/i;
|
||||
const parts = value.split(/\s+/);
|
||||
const partsCount = parts.length;
|
||||
const lastPart = parts[partsCount - 1];
|
||||
const preLastPart = parts[partsCount - 2];
|
||||
const context = useMemo(() => getContext(values.beforeCursor, metric, label), [values, metric, label]);
|
||||
|
||||
const hasEmptyPartAndQuotes = !lastPart && hasUnclosedQuotes(value);
|
||||
const suppressPreLast = (!lastPart || parts.length > 1) && !pattern.test(preLastPart);
|
||||
return hasEmptyPartAndQuotes || suppressPreLast;
|
||||
};
|
||||
|
||||
const context = useMemo(() => {
|
||||
const valueBeforeCursor = values.beforeCursor.trim();
|
||||
const endOfClosedBrackets = ["}", ")"].some(char => valueBeforeCursor.endsWith(char));
|
||||
const endOfClosedQuotes = !hasUnclosedQuotes(valueBeforeCursor) && ["`", "'", "\""].some(char => valueBeforeCursor.endsWith(char));
|
||||
if (!values.beforeCursor || endOfClosedBrackets || endOfClosedQuotes || shouldSuppressAutoSuggestion(values.beforeCursor)) {
|
||||
return QueryContextType.empty;
|
||||
}
|
||||
|
||||
const labelRegexp = /(?:by|without|on|ignoring)\s*\(\s*[^)]*$|\{[^}]*$/i;
|
||||
const patternLabelValue = `(${escapeRegexp(metric)})?{?.+${escapeRegexp(label)}(=|!=|=~|!~)"?([^"]*)$`;
|
||||
const labelValueRegexp = new RegExp(patternLabelValue, "g");
|
||||
|
||||
switch (true) {
|
||||
case labelValueRegexp.test(values.beforeCursor):
|
||||
return QueryContextType.labelValue;
|
||||
case labelRegexp.test(values.beforeCursor):
|
||||
return QueryContextType.label;
|
||||
default:
|
||||
return QueryContextType.metricsql;
|
||||
}
|
||||
}, [values, metric, label]);
|
||||
|
||||
const valueByContext = useMemo(() => {
|
||||
const wordMatch = values.beforeCursor.match(/([\w_.:]+(?![},]))$/);
|
||||
return wordMatch ? wordMatch[0] : "";
|
||||
}, [values.beforeCursor]);
|
||||
const valueByContext = useMemo(() => getValueByContext(values.beforeCursor), [values.beforeCursor]);
|
||||
|
||||
const { metrics, labels, labelValues, loading } = useFetchQueryOptions({
|
||||
valueByContext,
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { getContext, getExprLastPart, getValueByContext } from "./autocompleteUtils";
|
||||
|
||||
// Mock QueryContextType enum
|
||||
const QueryContextType = {
|
||||
empty: "empty",
|
||||
metricsql: "metricsql",
|
||||
label: "label",
|
||||
labelValue: "labelValue",
|
||||
};
|
||||
|
||||
describe("autocompleteUtils", () => {
|
||||
describe("getExprLastPart", () => {
|
||||
const tests = [
|
||||
{ input: "", expected: "" },
|
||||
{ input: "metric", expected: "metric" },
|
||||
{ input: "metric1 + metric2", expected: "metric2" },
|
||||
{ input: "rate(proc", expected: "proc" },
|
||||
{ input: "sum(rate(proc", expected: "proc" },
|
||||
{ input: "rate(metric{label=\"value\",", expected: "metric{label=\"value\"," },
|
||||
{ input: "quantile_over_time(0.9, me", expected: "me" },
|
||||
{ input: "quantile_over_time(0.9, metric{label=", expected: "metric{label=" },
|
||||
{ input: "quantile_over_time(0.9, metric{label=\"value\",", expected: "metric{label=\"value\"," },
|
||||
{ input: "quantile_over_time(0.9, metric{label=\"value\"}", expected: "" },
|
||||
{ input: "sum by (instance) (rate(proc", expected: "proc" },
|
||||
{ input: "metric{label=", expected: "metric{label=" },
|
||||
{ input: "metric{label1=\"value1\",label2=", expected: "metric{label1=\"value1\",label2=" },
|
||||
{ input: "quantile_over_time(0.9, node_cpu", expected: "node_cpu" },
|
||||
{ input: "sum(max_over_time(rate(node_cpu", expected: "node_cpu" },
|
||||
{ input: "clamp_min(metric1, m", expected: "m" },
|
||||
];
|
||||
|
||||
tests.forEach(({ input, expected }) => {
|
||||
it(`should return "${expected}" for input "${input}"`, () => {
|
||||
const result = getExprLastPart(input);
|
||||
expect(result).toBe(expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("getValueByContext", () => {
|
||||
const tests = [
|
||||
{ input: "", expected: "" },
|
||||
{ input: "metric", expected: "metric" },
|
||||
{ input: "metric1 + metric2", expected: "metric2" },
|
||||
{ input: "rate(proc", expected: "proc" },
|
||||
{ input: "sum(rate(proc", expected: "proc" },
|
||||
{ input: "quantile_over_time(0.9, node_cpu", expected: "node_cpu" },
|
||||
{ input: "clamp_min(metric1, m", expected: "m" },
|
||||
];
|
||||
|
||||
tests.forEach(({ input, expected }) => {
|
||||
it(`should return "${expected}" for input "${input}"`, () => {
|
||||
const result = getValueByContext(input);
|
||||
expect(result).toBe(expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("getContext", () => {
|
||||
const tests = [
|
||||
{ input: { beforeCursor: "" }, expected: QueryContextType.empty },
|
||||
{ input: { beforeCursor: "metric" }, expected: QueryContextType.metricsql },
|
||||
{ input: { beforeCursor: "rate(proc" }, expected: QueryContextType.metricsql },
|
||||
{ input: { beforeCursor: "sum(rate(proc" }, expected: QueryContextType.metricsql },
|
||||
{ input: { beforeCursor: "sum by (instance) (rate(proc" }, expected: QueryContextType.metricsql },
|
||||
{ input: { beforeCursor: "metric{" }, expected: QueryContextType.label },
|
||||
{ input: { beforeCursor: "metric{label=" }, expected: QueryContextType.labelValue, metric: "metric", label: "label" },
|
||||
{ input: { beforeCursor: "metric{label1=\"value1\",label2=" }, expected: QueryContextType.labelValue, metric: "metric", label: "label2" },
|
||||
{ input: { beforeCursor: "sum by (" }, expected: QueryContextType.label },
|
||||
{ input: { beforeCursor: "quantile_over_time(0.9, node_cpu" }, expected: QueryContextType.metricsql },
|
||||
{ input: { beforeCursor: "sum(max_over_time(rate(node_cpu" }, expected: QueryContextType.metricsql },
|
||||
{ input: { beforeCursor: "clamp_min(metric1, m" }, expected: QueryContextType.metricsql },
|
||||
{ input: { beforeCursor: "rate(node_cpu_seconds_total)" }, expected: QueryContextType.empty },
|
||||
];
|
||||
|
||||
tests.forEach((test) => {
|
||||
const { beforeCursor } = test.input;
|
||||
const metric = test.metric || "";
|
||||
const label = test.label || "";
|
||||
it(`should return "${test.expected}" for input "${beforeCursor}"`, () => {
|
||||
const result = getContext(beforeCursor, metric, label);
|
||||
expect(result).toBe(test.expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,112 @@
|
||||
/**
|
||||
* Utility functions for query editor autocomplete functionality
|
||||
*/
|
||||
import { QueryContextType } from "../../../types";
|
||||
import { escapeRegexp, hasUnclosedQuotes } from "../../../utils/regexp";
|
||||
|
||||
/**
|
||||
* Extracts the last part of an expression that's relevant for auto-suggestion
|
||||
* @param beforeCursor The text before the cursor position
|
||||
* @returns The relevant part of the expression for auto-suggestion
|
||||
*/
|
||||
export function getExprLastPart(beforeCursor: string): string {
|
||||
const regexpSplit =
|
||||
/\s(or|and|unless|default|ifnot|if|group_left|group_right)\s|}|\+|\|-|\*|\/|\^/i;
|
||||
const parts = beforeCursor.split(regexpSplit);
|
||||
const lastPart = parts[parts.length - 1].trim();
|
||||
|
||||
// Check if we're inside a function's parameters
|
||||
const functionRegex = /.*\(([^)]*)$/;
|
||||
const functionMatch = lastPart.match(functionRegex);
|
||||
|
||||
if (functionMatch && functionMatch[1]) {
|
||||
const params = functionMatch[1];
|
||||
|
||||
if (params.lastIndexOf("{") > params.lastIndexOf("}")) {
|
||||
const wordMatch = params.match(/([\w_.:]+)\{[^}]*$/);
|
||||
return wordMatch ? wordMatch[0] : lastPart;
|
||||
}
|
||||
|
||||
const lastCommaPos = params.lastIndexOf(",");
|
||||
if (lastCommaPos !== -1) {
|
||||
return params.substring(lastCommaPos + 1).trim();
|
||||
}
|
||||
|
||||
return params;
|
||||
}
|
||||
|
||||
return lastPart;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the current word or value at the cursor position for auto-suggestion matching
|
||||
* @param beforeCursor The text before the cursor position
|
||||
* @returns The current word or value at the cursor position
|
||||
*/
|
||||
export function getValueByContext(beforeCursor: string): string {
|
||||
const wordMatch = beforeCursor.match(/([\w_.:]+(?![},]))$/);
|
||||
return wordMatch ? wordMatch[0] : "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if auto-suggestion should be suppressed based on the query
|
||||
* @param value The query value
|
||||
* @returns Whether auto-suggestion should be suppressed
|
||||
*/
|
||||
export function shouldSuppressAutoSuggestion(value: string): boolean {
|
||||
const pattern =
|
||||
/([{(),+\-*/^]|\b(?:or|and|unless|default|ifnot|if|group_left|group_right|by|without|on|ignoring)\b)/i;
|
||||
const parts = value.split(/\s+/);
|
||||
const partsCount = parts.length;
|
||||
const lastPart = parts[partsCount - 1];
|
||||
const preLastPart = parts[partsCount - 2];
|
||||
|
||||
const hasEmptyPartAndQuotes = !lastPart && hasUnclosedQuotes(value);
|
||||
const suppressPreLast =
|
||||
(!lastPart || parts.length > 1) && !pattern.test(preLastPart);
|
||||
return hasEmptyPartAndQuotes || suppressPreLast;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the context type for auto-suggestion based on the query
|
||||
* @param beforeCursor The text before the cursor position
|
||||
* @param metric The current metric name
|
||||
* @param label The current label name
|
||||
* @returns The context type for auto-suggestion
|
||||
*/
|
||||
export function getContext(
|
||||
beforeCursor: string,
|
||||
metric: string = "",
|
||||
label: string = ""
|
||||
): QueryContextType {
|
||||
const valueBeforeCursor = beforeCursor.trim();
|
||||
const endOfClosedBrackets = ["}", ")"].some((char) =>
|
||||
valueBeforeCursor.endsWith(char)
|
||||
);
|
||||
const endOfClosedQuotes =
|
||||
!hasUnclosedQuotes(valueBeforeCursor) &&
|
||||
["`", "'", '"'].some((char) => valueBeforeCursor.endsWith(char));
|
||||
if (
|
||||
!valueBeforeCursor ||
|
||||
endOfClosedBrackets ||
|
||||
endOfClosedQuotes ||
|
||||
shouldSuppressAutoSuggestion(valueBeforeCursor)
|
||||
) {
|
||||
return QueryContextType.empty;
|
||||
}
|
||||
|
||||
const labelRegexp = /(?:by|without|on|ignoring)\s*\(\s*[^)]*$|\{[^}]*$/i;
|
||||
const patternLabelValue = `(${escapeRegexp(metric)})?{?.+${escapeRegexp(
|
||||
label
|
||||
)}(=|!=|=~|!~)"?([^"]*)$`;
|
||||
const labelValueRegexp = new RegExp(patternLabelValue, "g");
|
||||
|
||||
switch (true) {
|
||||
case labelValueRegexp.test(valueBeforeCursor):
|
||||
return QueryContextType.labelValue;
|
||||
case labelRegexp.test(valueBeforeCursor):
|
||||
return QueryContextType.label;
|
||||
default:
|
||||
return QueryContextType.metricsql;
|
||||
}
|
||||
}
|
||||
@@ -191,7 +191,7 @@ const StepConfigurator: FC = () => {
|
||||
</p>
|
||||
<p>
|
||||
Read more about <Hyperlink
|
||||
href="https://docs.victoriametrics.com/keyConcepts.html#range-query"
|
||||
href="https://docs.victoriametrics.com/keyconcepts/#range-query"
|
||||
text="Range"
|
||||
/> and <Hyperlink
|
||||
href="https://docs.victoriametrics.com/keyconcepts/#instant-query"
|
||||
|
||||
@@ -8,7 +8,7 @@ const issueLink = {
|
||||
|
||||
export const footerLinksByDefault = [
|
||||
{
|
||||
href: "https://docs.victoriametrics.com/MetricsQL.html",
|
||||
href: "https://docs.victoriametrics.com/metricsql/",
|
||||
Icon: CodeIcon,
|
||||
title: "MetricsQL",
|
||||
},
|
||||
|
||||
@@ -9,7 +9,7 @@ const CATEGORY_TAG = "h3";
|
||||
const FUNCTION_TAG = "h4";
|
||||
const DESCRIPTION_TAG = "p";
|
||||
|
||||
const docsUrl = "https://docs.victoriametrics.com/MetricsQL.html";
|
||||
const docsUrl = "https://docs.victoriametrics.com/metricsql/";
|
||||
const classLink = "vm-link vm-link_colored";
|
||||
|
||||
const prepareDescription = (text: string): string => {
|
||||
|
||||
@@ -4,13 +4,13 @@ import { useGraphState } from "../../../state/graph/GraphStateContext";
|
||||
|
||||
const last_over_time = <Hyperlink
|
||||
text="last_over_time"
|
||||
href="https://docs.victoriametrics.com/MetricsQL.html#last_over_time"
|
||||
href="https://docs.victoriametrics.com/metricsql/#last_over_time"
|
||||
underlined
|
||||
/>;
|
||||
|
||||
const instant_query = <Hyperlink
|
||||
text="instant query"
|
||||
href="https://docs.victoriametrics.com/keyConcepts.html#instant-query"
|
||||
href="https://docs.victoriametrics.com/keyconcepts/#instant-query"
|
||||
underlined
|
||||
/>;
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import CodeExample from "../../../components/Main/CodeExample/CodeExample";
|
||||
const MetricsQL = () => (
|
||||
<a
|
||||
className="vm-link vm-link_colored"
|
||||
href="https://docs.victoriametrics.com/MetricsQL.html"
|
||||
href="https://docs.victoriametrics.com/metricsql/"
|
||||
target="_blank"
|
||||
rel="help noreferrer"
|
||||
>
|
||||
|
||||
@@ -24,7 +24,7 @@ type Client struct {
|
||||
func NewClient() *Client {
|
||||
return &Client{
|
||||
httpCli: &http.Client{
|
||||
Transport: httputil.NewTransport(false),
|
||||
Transport: httputil.NewTransport(false, "apptest_client"),
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -55,6 +55,13 @@ func (c *Client) PostForm(t *testing.T, url string, data url.Values) (string, in
|
||||
return c.Post(t, url, "application/x-www-form-urlencoded", []byte(data.Encode()))
|
||||
}
|
||||
|
||||
// Delete sends a HTTP DELETE request and returns the response body and status code
|
||||
// to the caller.
|
||||
func (c *Client) Delete(t *testing.T, url string) (string, int) {
|
||||
t.Helper()
|
||||
return c.do(t, http.MethodDelete, url, "", nil)
|
||||
}
|
||||
|
||||
// do prepares a HTTP request, sends it to the server, receives the response
|
||||
// from the server, returns the response body and status code to the caller.
|
||||
func (c *Client) do(t *testing.T, method, url, contentType string, data []byte) (string, int) {
|
||||
|
||||
@@ -304,3 +304,43 @@ type MetricNamesStatsRecord struct {
|
||||
MetricName string
|
||||
QueryRequestsCount uint64
|
||||
}
|
||||
|
||||
// SnapshotCreateResponse is an in-memory reprensentation of the json response
|
||||
// returned by the /snapshot/create endpoint.
|
||||
type SnapshotCreateResponse struct {
|
||||
Status string
|
||||
Snapshot string
|
||||
}
|
||||
|
||||
// APIV1AdminTSDBSnapshotResponse is an in-memory reprensentation of the json
|
||||
// response returned by the /api/v1/admin/tsdb/snapshot endpoint.
|
||||
type APIV1AdminTSDBSnapshotResponse struct {
|
||||
Status string
|
||||
Data *SnapshotData
|
||||
}
|
||||
|
||||
// SnapshotData holds the info about the snapshot created via
|
||||
// /api/v1/admin/tsdb/snapshot endpoint.
|
||||
type SnapshotData struct {
|
||||
Name string
|
||||
}
|
||||
|
||||
// SnapshotListResponse is an in-memory reprensentation of the json response
|
||||
// returned by the /snapshot/list endpoint.
|
||||
type SnapshotListResponse struct {
|
||||
Status string
|
||||
Snapshots []string
|
||||
}
|
||||
|
||||
// SnapshotDeleteResponse is an in-memory reprensentation of the json response
|
||||
// returned by the /snapshot/delete endpoint.
|
||||
type SnapshotDeleteResponse struct {
|
||||
Status string
|
||||
Msg string
|
||||
}
|
||||
|
||||
// SnapshotDeleteAllResponse is an in-memory reprensentation of the json response
|
||||
// returned by the /snapshot/delete_all endpoint.
|
||||
type SnapshotDeleteAllResponse struct {
|
||||
Status string
|
||||
}
|
||||
|
||||
@@ -124,6 +124,23 @@ func (tc *TestCase) MustStartVminsert(instance string, flags []string) *Vminsert
|
||||
return app
|
||||
}
|
||||
|
||||
// MustStartVmagent is a test helper function that starts an instance of
|
||||
// vmagent and fails the test if the app fails to start.
|
||||
func (tc *TestCase) MustStartVmagent(instance string, flags []string, promScrapeConfigFileYAML string) *Vmagent {
|
||||
tc.t.Helper()
|
||||
|
||||
promScrapeConfigFilePath := path.Join(tc.t.TempDir(), "prometheus.yml")
|
||||
if err := os.WriteFile(promScrapeConfigFilePath, []byte(promScrapeConfigFileYAML), os.ModePerm); err != nil {
|
||||
tc.t.Fatalf("cannot init vmagent: prom config file write failed: %s", err)
|
||||
}
|
||||
app, err := StartVmagent(instance, flags, tc.cli, promScrapeConfigFilePath)
|
||||
if err != nil {
|
||||
tc.t.Fatalf("Could not start %s: %v", instance, err)
|
||||
}
|
||||
tc.addApp(instance, app)
|
||||
return app
|
||||
}
|
||||
|
||||
// Vmcluster represents a typical cluster setup: several vmstorage replicas, one
|
||||
// vminsert, and one vmselect.
|
||||
//
|
||||
|
||||
201
apptest/tests/snapshot_test.go
Normal file
@@ -0,0 +1,201 @@
|
||||
package tests
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"testing"
|
||||
|
||||
at "github.com/VictoriaMetrics/VictoriaMetrics/apptest"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
)
|
||||
|
||||
// snapshotNameRE convers years 1970-2099.
|
||||
// Corner case examples:
|
||||
// - 19700101000000-0000000000000000
|
||||
// - 20991231235959-38EECC8925ED5FFF
|
||||
var snapshotNameRE = regexp.MustCompile(`^(19[789]\d|20[0-9]{2})(0\d|1[0-2])([0-2]\d|3[01])([01]\d|2[0-3])[0-5]\d[0-5]\d-[0-9,A-F]{16}$`)
|
||||
|
||||
func TestSingleSnapshots_CreateListDelete(t *testing.T) {
|
||||
tc := at.NewTestCase(t)
|
||||
defer tc.Stop()
|
||||
|
||||
sut := tc.MustStartDefaultVmsingle()
|
||||
|
||||
// Insert some data.
|
||||
const numSamples = 1000
|
||||
samples := make([]string, numSamples)
|
||||
for i := range numSamples {
|
||||
samples[i] = fmt.Sprintf("metric_%03d %d", i, i)
|
||||
}
|
||||
sut.PrometheusAPIV1ImportPrometheus(t, samples, at.QueryOpts{})
|
||||
sut.ForceFlush(t)
|
||||
|
||||
// Create several snapshots using VictoriaMetrics and Prometheus endpoints.
|
||||
const numSnapshots = 4
|
||||
snapshots := make([]string, numSnapshots*2)
|
||||
i := 0
|
||||
for range numSnapshots {
|
||||
res := sut.SnapshotCreate(t)
|
||||
if got, want := res.Status, "ok"; got != want {
|
||||
t.Fatalf("unexpected snapshot creation status: got %q, want %q", got, want)
|
||||
}
|
||||
if !snapshotNameRE.MatchString(res.Snapshot) {
|
||||
t.Fatalf("unexpected snapshot name format: %q", res.Snapshot)
|
||||
}
|
||||
snapshots[i] = res.Snapshot
|
||||
i++
|
||||
}
|
||||
for range numSnapshots {
|
||||
res := sut.APIV1AdminTSDBSnapshot(t)
|
||||
if got, want := res.Status, "success"; got != want {
|
||||
t.Fatalf("unexpected snapshot creation status: got %q, want %q", got, want)
|
||||
}
|
||||
if !snapshotNameRE.MatchString(res.Data.Name) {
|
||||
t.Fatalf("unexpected snapshot name format: %q", res.Data.Name)
|
||||
}
|
||||
snapshots[i] = res.Data.Name
|
||||
i++
|
||||
}
|
||||
|
||||
assertSnapshotList := func(want []string) {
|
||||
gotRes := sut.SnapshotList(t)
|
||||
wantRes := &at.SnapshotListResponse{
|
||||
Status: "ok",
|
||||
Snapshots: want,
|
||||
}
|
||||
if diff := cmp.Diff(wantRes, gotRes); diff != "" {
|
||||
t.Fatalf("unexpected response (-want, +got):\n%s", diff)
|
||||
}
|
||||
}
|
||||
assertSnapshotList(snapshots)
|
||||
|
||||
// Delete non-existent snapshot.
|
||||
gotDeletedSnapshot := sut.SnapshotDelete(t, "does-not-exist")
|
||||
wantDeletedSnapshot := &at.SnapshotDeleteResponse{
|
||||
Status: "error",
|
||||
Msg: `cannot find snapshot "does-not-exist"`,
|
||||
}
|
||||
if diff := cmp.Diff(wantDeletedSnapshot, gotDeletedSnapshot); diff != "" {
|
||||
t.Fatalf("unexpected response (-want, +got):\n%s", diff)
|
||||
}
|
||||
|
||||
// Delete the first snapshot.
|
||||
gotDeletedSnapshot = sut.SnapshotDelete(t, snapshots[0])
|
||||
wantDeletedSnapshot = &at.SnapshotDeleteResponse{
|
||||
Status: "ok",
|
||||
}
|
||||
if diff := cmp.Diff(wantDeletedSnapshot, gotDeletedSnapshot); diff != "" {
|
||||
t.Fatalf("unexpected response (-want, +got):\n%s", diff)
|
||||
}
|
||||
assertSnapshotList(snapshots[1:])
|
||||
|
||||
// Delete the rest of the snapshots.
|
||||
gotDeleteAllRes := sut.SnapshotDeleteAll(t)
|
||||
wantDeleteAllRes := &at.SnapshotDeleteAllResponse{
|
||||
Status: "ok",
|
||||
}
|
||||
if diff := cmp.Diff(wantDeleteAllRes, gotDeleteAllRes); diff != "" {
|
||||
t.Fatalf("unexpected response (-want, +got):\n%s", diff)
|
||||
}
|
||||
assertSnapshotList([]string{})
|
||||
}
|
||||
|
||||
func TestClusterSnapshots_CreateListDelete(t *testing.T) {
|
||||
tc := at.NewTestCase(t)
|
||||
defer tc.Stop()
|
||||
|
||||
sut := tc.MustStartDefaultCluster().(*at.Vmcluster)
|
||||
|
||||
// Insert some data.
|
||||
const numSamples = 1000
|
||||
samples := make([]string, numSamples)
|
||||
for i := range numSamples {
|
||||
samples[i] = fmt.Sprintf("metric_%03d %d", i, i)
|
||||
}
|
||||
sut.PrometheusAPIV1ImportPrometheus(t, samples, at.QueryOpts{})
|
||||
sut.ForceFlush(t)
|
||||
|
||||
// Create several snapshots for both vmstorage replicas using
|
||||
// VictoriaMetrics endpoints only (cluster version does not have Prometheus
|
||||
// endpoint)
|
||||
createSnapshot := func(i int) string {
|
||||
t.Helper()
|
||||
|
||||
res := sut.Vmstorages[i].SnapshotCreate(t)
|
||||
if got, want := res.Status, "ok"; got != want {
|
||||
t.Fatalf("unexpected snapshot creation status: got %q, want %q", got, want)
|
||||
}
|
||||
if !snapshotNameRE.MatchString(res.Snapshot) {
|
||||
t.Fatalf("unexpected snapshot name format: %q", res.Snapshot)
|
||||
}
|
||||
return res.Snapshot
|
||||
}
|
||||
const numSnapshots = 4
|
||||
snapshots0 := make([]string, numSnapshots)
|
||||
snapshots1 := make([]string, numSnapshots)
|
||||
for i := range numSnapshots {
|
||||
snapshots0[i] = createSnapshot(0)
|
||||
snapshots1[i] = createSnapshot(1)
|
||||
}
|
||||
|
||||
assertSnapshotList := func(i int, wantNames []string) {
|
||||
t.Helper()
|
||||
got := sut.Vmstorages[i].SnapshotList(t)
|
||||
want := &at.SnapshotListResponse{
|
||||
Status: "ok",
|
||||
Snapshots: wantNames,
|
||||
}
|
||||
if diff := cmp.Diff(want, got); diff != "" {
|
||||
t.Fatalf("unexpected response (-want, +got):\n%s", diff)
|
||||
}
|
||||
}
|
||||
assertSnapshotList(0, snapshots0)
|
||||
assertSnapshotList(1, snapshots1)
|
||||
|
||||
// Delete non-existent snapshot.
|
||||
assertDeleteNonExistent := func(i int) {
|
||||
t.Helper()
|
||||
got := sut.Vmstorages[i].SnapshotDelete(t, "does-not-exist")
|
||||
want := &at.SnapshotDeleteResponse{
|
||||
Status: "error",
|
||||
Msg: `cannot find snapshot "does-not-exist"`,
|
||||
}
|
||||
if diff := cmp.Diff(want, got); diff != "" {
|
||||
t.Fatalf("unexpected response (-want, +got):\n%s", diff)
|
||||
}
|
||||
}
|
||||
assertDeleteNonExistent(0)
|
||||
assertDeleteNonExistent(1)
|
||||
|
||||
// Delete the first snapshot.
|
||||
deleteSnapshot := func(i int, snapshotName string) {
|
||||
t.Helper()
|
||||
got := sut.Vmstorages[i].SnapshotDelete(t, snapshotName)
|
||||
want := &at.SnapshotDeleteResponse{
|
||||
Status: "ok",
|
||||
}
|
||||
if diff := cmp.Diff(want, got); diff != "" {
|
||||
t.Fatalf("unexpected response (-want, +got):\n%s", diff)
|
||||
}
|
||||
}
|
||||
deleteSnapshot(0, snapshots0[0])
|
||||
assertSnapshotList(0, snapshots0[1:])
|
||||
deleteSnapshot(1, snapshots1[0])
|
||||
assertSnapshotList(1, snapshots1[1:])
|
||||
|
||||
// Delete the rest of the snapshots.
|
||||
deleteAllSnapshots := func(i int) {
|
||||
t.Helper()
|
||||
got := sut.Vmstorages[i].SnapshotDeleteAll(t)
|
||||
want := &at.SnapshotDeleteAllResponse{
|
||||
Status: "ok",
|
||||
}
|
||||
if diff := cmp.Diff(want, got); diff != "" {
|
||||
t.Fatalf("unexpected response (-want, +got):\n%s", diff)
|
||||
}
|
||||
}
|
||||
deleteAllSnapshots(0)
|
||||
assertSnapshotList(0, []string{})
|
||||
deleteAllSnapshots(1)
|
||||
assertSnapshotList(1, []string{})
|
||||
}
|
||||
54
apptest/tests/vmagent_remotewrite_test.go
Normal file
@@ -0,0 +1,54 @@
|
||||
package tests
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/apptest"
|
||||
at "github.com/VictoriaMetrics/VictoriaMetrics/apptest"
|
||||
)
|
||||
|
||||
// TestSingleVMAgentZstdRemoteWrite verifies that vmagent can successfully perform
|
||||
// a remote write to vmsingle using VM protocol (zstd).
|
||||
func TestSingleVMAgentZstdRemoteWrite(t *testing.T) {
|
||||
testSingleVMAgentRemoteWrite(t, false)
|
||||
}
|
||||
|
||||
// TestSingleVMAgentSnappyRemoteWrite verifies that vmagent can successfully perform
|
||||
// a remote write to vmsingle using Prometheus protocol (snappy).
|
||||
func TestSingleVMAgentSnappyRemoteWrite(t *testing.T) {
|
||||
testSingleVMAgentRemoteWrite(t, true)
|
||||
}
|
||||
|
||||
func testSingleVMAgentRemoteWrite(t *testing.T, forcePromProto bool) {
|
||||
tc := apptest.NewTestCase(t)
|
||||
defer tc.Stop()
|
||||
|
||||
vmsingle := tc.MustStartDefaultVmsingle()
|
||||
|
||||
vmagent := tc.MustStartVmagent("vmagent", []string{
|
||||
`-remoteWrite.flushInterval=50ms`,
|
||||
fmt.Sprintf(`-remoteWrite.forcePromProto=%v`, forcePromProto),
|
||||
fmt.Sprintf(`-remoteWrite.url=http://%s/api/v1/write`, vmsingle.HTTPAddr()),
|
||||
}, ``)
|
||||
|
||||
vmagent.APIV1ImportPrometheus(t, []string{
|
||||
"foo_bar 1 1652169600000", // 2022-05-10T08:00:00Z
|
||||
}, apptest.QueryOpts{})
|
||||
|
||||
vmsingle.ForceFlush(t)
|
||||
|
||||
tc.Assert(&at.AssertOptions{
|
||||
Msg: `unexpected metrics stored on vmagent remote write`,
|
||||
Got: func() any {
|
||||
return vmsingle.PrometheusAPIV1Series(t, `{__name__="foo_bar"}`, at.QueryOpts{
|
||||
Start: "2022-05-10T00:00:00Z",
|
||||
End: "2022-05-10T23:59:59Z",
|
||||
}).Sort()
|
||||
},
|
||||
Want: &at.PrometheusAPIV1SeriesResponse{
|
||||
Status: "success",
|
||||
Data: []map[string]string{{"__name__": "foo_bar"}},
|
||||
},
|
||||
})
|
||||
}
|
||||
107
apptest/vmagent.go
Normal file
@@ -0,0 +1,107 @@
|
||||
package apptest
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Vmagent holds the state of a vmagent app and provides vmagent-specific functions
|
||||
type Vmagent struct {
|
||||
*app
|
||||
*ServesMetrics
|
||||
|
||||
httpListenAddr string
|
||||
apiV1ImportPrometheusURL string
|
||||
}
|
||||
|
||||
// StartVmagent starts an instance of vmagent with the given flags. It also
|
||||
// sets the default flags and populates the app instance state with runtime
|
||||
// values extracted from the application log (such as httpListenAddr)
|
||||
func StartVmagent(instance string, flags []string, cli *Client, promScrapeConfigFilePath string) (*Vmagent, error) {
|
||||
extractREs := []*regexp.Regexp{
|
||||
httpListenAddrRE,
|
||||
}
|
||||
|
||||
app, stderrExtracts, err := startApp(instance, "../../bin/vmagent", flags, &appOptions{
|
||||
defaultFlags: map[string]string{
|
||||
"-httpListenAddr": "127.0.0.1:0",
|
||||
"-promscrape.config": promScrapeConfigFilePath,
|
||||
},
|
||||
extractREs: extractREs,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Vmagent{
|
||||
app: app,
|
||||
ServesMetrics: &ServesMetrics{
|
||||
metricsURL: fmt.Sprintf("http://%s/metrics", stderrExtracts[0]),
|
||||
cli: cli,
|
||||
},
|
||||
httpListenAddr: stderrExtracts[0],
|
||||
apiV1ImportPrometheusURL: fmt.Sprintf("http://%s/api/v1/import/prometheus", stderrExtracts[0]),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// APIV1ImportPrometheus is a test helper function that inserts a
|
||||
// collection of records in Prometheus text exposition format for the given
|
||||
// tenant by sending a HTTP POST request to /api/v1/import/prometheus vmagent endpoint.
|
||||
//
|
||||
// The call is blocked until the data is flushed to vmstorage or the timeout is reached.
|
||||
//
|
||||
// See https://docs.victoriametrics.com/url-examples/#apiv1importprometheus
|
||||
func (app *Vmagent) APIV1ImportPrometheus(t *testing.T, records []string, _ QueryOpts) {
|
||||
t.Helper()
|
||||
|
||||
data := []byte(strings.Join(records, "\n"))
|
||||
app.sendBlocking(t, len(records), func() {
|
||||
_, statusCode := app.cli.Post(t, app.apiV1ImportPrometheusURL, "text/plain", data)
|
||||
if statusCode != http.StatusNoContent {
|
||||
t.Fatalf("unexpected status code: got %d, want %d", statusCode, http.StatusNoContent)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// sendBlocking sends the data to vmstorage by executing `send` function and
|
||||
// waits until the data is actually sent.
|
||||
//
|
||||
// vmagent does not send the data immediately. It first puts the data into a
|
||||
// buffer. Then a background goroutine takes the data from the buffer sends it
|
||||
// to the vmstorage. This happens every 1s by default.
|
||||
//
|
||||
// Waiting is implemented a retrieving the value of `vmagent_remotewrite_requests_total`
|
||||
// metric and checking whether it is equal or greater than the wanted value.
|
||||
// If it is, then the data has been sent to vmstorage.
|
||||
//
|
||||
// Unreliable if the records are inserted concurrently.
|
||||
func (app *Vmagent) sendBlocking(t *testing.T, numRecordsToSend int, send func()) {
|
||||
t.Helper()
|
||||
|
||||
send()
|
||||
|
||||
const (
|
||||
retries = 20
|
||||
period = 100 * time.Millisecond
|
||||
)
|
||||
wantRowsSentCount := app.remoteWriteRequestsTotal(t) + numRecordsToSend
|
||||
for range retries {
|
||||
if app.remoteWriteRequestsTotal(t) >= wantRowsSentCount {
|
||||
return
|
||||
}
|
||||
time.Sleep(period)
|
||||
}
|
||||
t.Fatalf("timed out while waiting for inserted rows to be sent to vmstorage")
|
||||
}
|
||||
|
||||
func (app *Vmagent) remoteWriteRequestsTotal(t *testing.T) int {
|
||||
total := 0.0
|
||||
for _, v := range app.GetMetricsByPrefix(t, "vmagent_remotewrite_requests_total") {
|
||||
total += v
|
||||
}
|
||||
return int(total)
|
||||
}
|
||||
@@ -66,8 +66,9 @@ func StartVmsingle(instance string, flags []string, cli *Client) (*Vmsingle, err
|
||||
storageDataPath: stderrExtracts[0],
|
||||
httpListenAddr: stderrExtracts[1],
|
||||
|
||||
forceFlushURL: fmt.Sprintf("http://%s/internal/force_flush", stderrExtracts[1]),
|
||||
forceMergeURL: fmt.Sprintf("http://%s/internal/force_merge", stderrExtracts[1]),
|
||||
forceFlushURL: fmt.Sprintf("http://%s/internal/force_flush", stderrExtracts[1]),
|
||||
forceMergeURL: fmt.Sprintf("http://%s/internal/force_merge", stderrExtracts[1]),
|
||||
|
||||
influxLineWriteURL: fmt.Sprintf("http://%s/influx/write", stderrExtracts[1]),
|
||||
prometheusAPIV1ImportPrometheusURL: fmt.Sprintf("http://%s/prometheus/api/v1/import/prometheus", stderrExtracts[1]),
|
||||
prometheusAPIV1WriteURL: fmt.Sprintf("http://%s/prometheus/api/v1/write", stderrExtracts[1]),
|
||||
@@ -240,6 +241,121 @@ func (app *Vmsingle) APIV1AdminStatusMetricNamesStatsReset(t *testing.T, opts Qu
|
||||
}
|
||||
}
|
||||
|
||||
// SnapshotCreate creates a database snapshot by sending a query to the
|
||||
// /snapshot/create endpoint.
|
||||
//
|
||||
// See https://docs.victoriametrics.com/single-server-victoriametrics/#how-to-work-with-snapshots
|
||||
func (app *Vmsingle) SnapshotCreate(t *testing.T) *SnapshotCreateResponse {
|
||||
t.Helper()
|
||||
|
||||
queryURL := fmt.Sprintf("http://%s/snapshot/create", app.httpListenAddr)
|
||||
data, statusCode := app.cli.Post(t, queryURL, "", nil)
|
||||
if got, want := statusCode, http.StatusOK; got != want {
|
||||
t.Fatalf("unexpected status code: got %d, want %d, resp text=%q", got, want, data)
|
||||
}
|
||||
|
||||
var res SnapshotCreateResponse
|
||||
if err := json.Unmarshal([]byte(data), &res); err != nil {
|
||||
t.Fatalf("could not unmarshal snapshot create response: data=%q, err: %v", data, err)
|
||||
}
|
||||
|
||||
return &res
|
||||
}
|
||||
|
||||
// APIV1AdminTSDBSnapshot creates a database snapshot by sending a query to the
|
||||
// /api/v1/admin/tsdb/snapshot endpoint.
|
||||
//
|
||||
// See https://prometheus.io/docs/prometheus/latest/querying/api/#snapshot.
|
||||
func (app *Vmsingle) APIV1AdminTSDBSnapshot(t *testing.T) *APIV1AdminTSDBSnapshotResponse {
|
||||
t.Helper()
|
||||
|
||||
queryURL := fmt.Sprintf("http://%s/api/v1/admin/tsdb/snapshot", app.httpListenAddr)
|
||||
data, statusCode := app.cli.Post(t, queryURL, "", nil)
|
||||
if got, want := statusCode, http.StatusOK; got != want {
|
||||
t.Fatalf("unexpected status code: got %d, want %d, resp text=%q", got, want, data)
|
||||
}
|
||||
|
||||
var res APIV1AdminTSDBSnapshotResponse
|
||||
if err := json.Unmarshal([]byte(data), &res); err != nil {
|
||||
t.Fatalf("could not unmarshal prometheus snapshot create response: data=%q, err: %v", data, err)
|
||||
}
|
||||
|
||||
return &res
|
||||
}
|
||||
|
||||
// SnapshotList lists existing database snapshots by sending a query to the
|
||||
// /snapshot/list endpoint.
|
||||
//
|
||||
// See https://docs.victoriametrics.com/single-server-victoriametrics/#how-to-work-with-snapshots
|
||||
func (app *Vmsingle) SnapshotList(t *testing.T) *SnapshotListResponse {
|
||||
t.Helper()
|
||||
|
||||
queryURL := fmt.Sprintf("http://%s/snapshot/list", app.httpListenAddr)
|
||||
data, statusCode := app.cli.Get(t, queryURL)
|
||||
if got, want := statusCode, http.StatusOK; got != want {
|
||||
t.Fatalf("unexpected status code: got %d, want %d, resp text=%q", got, want, data)
|
||||
}
|
||||
|
||||
var res SnapshotListResponse
|
||||
if err := json.Unmarshal([]byte(data), &res); err != nil {
|
||||
t.Fatalf("could not unmarshal snapshot list response: data=%q, err: %v", data, err)
|
||||
}
|
||||
|
||||
return &res
|
||||
}
|
||||
|
||||
// SnapshotDelete deletes a snapshot by sending a query to the
|
||||
// /snapshot/delete endpoint.
|
||||
//
|
||||
// See https://docs.victoriametrics.com/single-server-victoriametrics/#how-to-work-with-snapshots
|
||||
func (app *Vmsingle) SnapshotDelete(t *testing.T, snapshotName string) *SnapshotDeleteResponse {
|
||||
t.Helper()
|
||||
|
||||
queryURL := fmt.Sprintf("http://%s/snapshot/delete?snapshot=%s", app.httpListenAddr, snapshotName)
|
||||
data, statusCode := app.cli.Delete(t, queryURL)
|
||||
wantStatusCodes := map[int]bool{
|
||||
http.StatusOK: true,
|
||||
http.StatusInternalServerError: true,
|
||||
}
|
||||
if !wantStatusCodes[statusCode] {
|
||||
t.Fatalf("unexpected status code: got %d, want %v, resp text=%q", statusCode, wantStatusCodes, data)
|
||||
}
|
||||
|
||||
var res SnapshotDeleteResponse
|
||||
if err := json.Unmarshal([]byte(data), &res); err != nil {
|
||||
t.Fatalf("could not unmarshal snapshot delete response: data=%q, err: %v", data, err)
|
||||
}
|
||||
|
||||
return &res
|
||||
}
|
||||
|
||||
// SnapshotDeleteAll deletes all snapshots by sending a query to the
|
||||
// /snapshot/delete_all endpoint.
|
||||
//
|
||||
// See https://docs.victoriametrics.com/single-server-victoriametrics/#how-to-work-with-snapshots
|
||||
func (app *Vmsingle) SnapshotDeleteAll(t *testing.T) *SnapshotDeleteAllResponse {
|
||||
t.Helper()
|
||||
|
||||
queryURL := fmt.Sprintf("http://%s/snapshot/delete_all", app.httpListenAddr)
|
||||
data, statusCode := app.cli.Get(t, queryURL)
|
||||
if got, want := statusCode, http.StatusOK; got != want {
|
||||
t.Fatalf("unexpected status code: got %d, want %d, resp text=%q", got, want, data)
|
||||
}
|
||||
|
||||
var res SnapshotDeleteAllResponse
|
||||
if err := json.Unmarshal([]byte(data), &res); err != nil {
|
||||
t.Fatalf("could not unmarshal snapshot delete all response: data=%q, err: %v", data, err)
|
||||
}
|
||||
|
||||
return &res
|
||||
}
|
||||
|
||||
// HTTPAddr returns the address at which the vmstorage process is listening
|
||||
// for http connections.
|
||||
func (app *Vmsingle) HTTPAddr() string {
|
||||
return app.httpListenAddr
|
||||
}
|
||||
|
||||
// String returns the string representation of the vmsingle app state.
|
||||
func (app *Vmsingle) String() string {
|
||||
return fmt.Sprintf("{app: %s storageDataPath: %q httpListenAddr: %q}", []any{
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package apptest
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
@@ -19,9 +20,6 @@ type Vmstorage struct {
|
||||
httpListenAddr string
|
||||
vminsertAddr string
|
||||
vmselectAddr string
|
||||
|
||||
forceFlushURL string
|
||||
forceMergeURL string
|
||||
}
|
||||
|
||||
// StartVmstorage starts an instance of vmstorage with the given flags. It also
|
||||
@@ -56,9 +54,6 @@ func StartVmstorage(instance string, flags []string, cli *Client) (*Vmstorage, e
|
||||
httpListenAddr: stderrExtracts[1],
|
||||
vminsertAddr: stderrExtracts[2],
|
||||
vmselectAddr: stderrExtracts[3],
|
||||
|
||||
forceFlushURL: fmt.Sprintf("http://%s/internal/force_flush", stderrExtracts[1]),
|
||||
forceMergeURL: fmt.Sprintf("http://%s/internal/force_merge", stderrExtracts[1]),
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -79,7 +74,8 @@ func (app *Vmstorage) VmselectAddr() string {
|
||||
func (app *Vmstorage) ForceFlush(t *testing.T) {
|
||||
t.Helper()
|
||||
|
||||
_, statusCode := app.cli.Get(t, app.forceFlushURL)
|
||||
forceFlushURL := fmt.Sprintf("http://%s/internal/force_flush", app.httpListenAddr)
|
||||
_, statusCode := app.cli.Get(t, forceFlushURL)
|
||||
if statusCode != http.StatusOK {
|
||||
t.Fatalf("unexpected status code: got %d, want %d", statusCode, http.StatusOK)
|
||||
}
|
||||
@@ -88,12 +84,102 @@ func (app *Vmstorage) ForceFlush(t *testing.T) {
|
||||
// ForceMerge is a test helper function that forces the merging of parts.
|
||||
func (app *Vmstorage) ForceMerge(t *testing.T) {
|
||||
t.Helper()
|
||||
_, statusCode := app.cli.Get(t, app.forceMergeURL)
|
||||
|
||||
forceMergeURL := fmt.Sprintf("http://%s/internal/force_merge", app.httpListenAddr)
|
||||
_, statusCode := app.cli.Get(t, forceMergeURL)
|
||||
if statusCode != http.StatusOK {
|
||||
t.Fatalf("unexpected status code: got %d, want %d", statusCode, http.StatusOK)
|
||||
}
|
||||
}
|
||||
|
||||
// SnapshotCreate creates a database snapshot by sending a query to the
|
||||
// /snapshot/create endpoint.
|
||||
//
|
||||
// See https://docs.victoriametrics.com/single-server-victoriametrics/#how-to-work-with-snapshots
|
||||
func (app *Vmstorage) SnapshotCreate(t *testing.T) *SnapshotCreateResponse {
|
||||
t.Helper()
|
||||
|
||||
queryURL := fmt.Sprintf("http://%s/snapshot/create", app.httpListenAddr)
|
||||
data, statusCode := app.cli.Post(t, queryURL, "", nil)
|
||||
if got, want := statusCode, http.StatusOK; got != want {
|
||||
t.Fatalf("unexpected status code: got %d, want %d, resp text=%q", got, want, data)
|
||||
}
|
||||
|
||||
var res SnapshotCreateResponse
|
||||
if err := json.Unmarshal([]byte(data), &res); err != nil {
|
||||
t.Fatalf("could not unmarshal snapshot create response: data=%q, err: %v", data, err)
|
||||
}
|
||||
|
||||
return &res
|
||||
}
|
||||
|
||||
// SnapshotList lists existing database snapshots by sending a query to the
|
||||
// /snapshot/list endpoint.
|
||||
//
|
||||
// See https://docs.victoriametrics.com/single-server-victoriametrics/#how-to-work-with-snapshots
|
||||
func (app *Vmstorage) SnapshotList(t *testing.T) *SnapshotListResponse {
|
||||
t.Helper()
|
||||
|
||||
queryURL := fmt.Sprintf("http://%s/snapshot/list", app.httpListenAddr)
|
||||
data, statusCode := app.cli.Get(t, queryURL)
|
||||
if got, want := statusCode, http.StatusOK; got != want {
|
||||
t.Fatalf("unexpected status code: got %d, want %d, resp text=%q", got, want, data)
|
||||
}
|
||||
|
||||
var res SnapshotListResponse
|
||||
if err := json.Unmarshal([]byte(data), &res); err != nil {
|
||||
t.Fatalf("could not unmarshal snapshot list response: data=%q, err: %v", data, err)
|
||||
}
|
||||
|
||||
return &res
|
||||
}
|
||||
|
||||
// SnapshotDelete deletes a snapshot by sending a query to the
|
||||
// /snapshot/delete endpoint.
|
||||
//
|
||||
// See https://docs.victoriametrics.com/single-server-victoriametrics/#how-to-work-with-snapshots
|
||||
func (app *Vmstorage) SnapshotDelete(t *testing.T, snapshotName string) *SnapshotDeleteResponse {
|
||||
t.Helper()
|
||||
|
||||
queryURL := fmt.Sprintf("http://%s/snapshot/delete?snapshot=%s", app.httpListenAddr, snapshotName)
|
||||
data, statusCode := app.cli.Delete(t, queryURL)
|
||||
wantStatusCodes := map[int]bool{
|
||||
http.StatusOK: true,
|
||||
http.StatusInternalServerError: true,
|
||||
}
|
||||
if !wantStatusCodes[statusCode] {
|
||||
t.Fatalf("unexpected status code: got %d, want %v, resp text=%q", statusCode, wantStatusCodes, data)
|
||||
}
|
||||
|
||||
var res SnapshotDeleteResponse
|
||||
if err := json.Unmarshal([]byte(data), &res); err != nil {
|
||||
t.Fatalf("could not unmarshal snapshot delete response: data=%q, err: %v", data, err)
|
||||
}
|
||||
|
||||
return &res
|
||||
}
|
||||
|
||||
// SnapshotDeleteAll deletes all snapshots by sending a query to the
|
||||
// /snapshot/delete_all endpoint.
|
||||
//
|
||||
// See https://docs.victoriametrics.com/single-server-victoriametrics/#how-to-work-with-snapshots
|
||||
func (app *Vmstorage) SnapshotDeleteAll(t *testing.T) *SnapshotDeleteAllResponse {
|
||||
t.Helper()
|
||||
|
||||
queryURL := fmt.Sprintf("http://%s/snapshot/delete_all", app.httpListenAddr)
|
||||
data, statusCode := app.cli.Post(t, queryURL, "", nil)
|
||||
if got, want := statusCode, http.StatusOK; got != want {
|
||||
t.Fatalf("unexpected status code: got %d, want %d, resp text=%q", got, want, data)
|
||||
}
|
||||
|
||||
var res SnapshotDeleteAllResponse
|
||||
if err := json.Unmarshal([]byte(data), &res); err != nil {
|
||||
t.Fatalf("could not unmarshal snapshot delete all response: data=%q, err: %v", data, err)
|
||||
}
|
||||
|
||||
return &res
|
||||
}
|
||||
|
||||
// String returns the string representation of the vmstorage app state.
|
||||
func (app *Vmstorage) String() string {
|
||||
return fmt.Sprintf("{app: %s storageDataPath: %q httpListenAddr: %q vminsertAddr: %q vmselectAddr: %q}", []any{
|
||||
|
||||
@@ -1690,7 +1690,9 @@
|
||||
],
|
||||
"refresh": "1m",
|
||||
"schemaVersion": 39,
|
||||
"tags": [],
|
||||
"tags": [
|
||||
"victoriametrics"
|
||||
],
|
||||
"templating": {
|
||||
"list": [
|
||||
{
|
||||
|
||||
@@ -1851,8 +1851,7 @@
|
||||
"refresh": false,
|
||||
"schemaVersion": 39,
|
||||
"tags": [
|
||||
"VictoriaMetrics",
|
||||
"monitoring"
|
||||
"victoriametrics"
|
||||
],
|
||||
"templating": {
|
||||
"list": [
|
||||
|
||||
@@ -2052,8 +2052,7 @@
|
||||
"refresh": "",
|
||||
"schemaVersion": 39,
|
||||
"tags": [
|
||||
"operator",
|
||||
"VictoriaMetrics"
|
||||
"victoriametrics"
|
||||
],
|
||||
"templating": {
|
||||
"list": [
|
||||
|
||||
@@ -3494,7 +3494,10 @@
|
||||
"preload": false,
|
||||
"refresh": "",
|
||||
"schemaVersion": 40,
|
||||
"tags": [],
|
||||
"tags": [
|
||||
"victoriametrics",
|
||||
"victorialogs"
|
||||
],
|
||||
"templating": {
|
||||
"list": [
|
||||
{
|
||||
|
||||
@@ -10061,7 +10061,9 @@
|
||||
"preload": false,
|
||||
"refresh": "",
|
||||
"schemaVersion": 40,
|
||||
"tags": [],
|
||||
"tags": [
|
||||
"victoriametrics"
|
||||
],
|
||||
"templating": {
|
||||
"list": [
|
||||
{
|
||||
|
||||
@@ -6193,8 +6193,7 @@
|
||||
"refresh": "",
|
||||
"schemaVersion": 40,
|
||||
"tags": [
|
||||
"victoriametrics",
|
||||
"vmsingle"
|
||||
"victoriametrics"
|
||||
],
|
||||
"templating": {
|
||||
"list": [
|
||||
|
||||
@@ -1691,7 +1691,9 @@
|
||||
],
|
||||
"refresh": "1m",
|
||||
"schemaVersion": 39,
|
||||
"tags": [],
|
||||
"tags": [
|
||||
"victoriametrics"
|
||||
],
|
||||
"templating": {
|
||||
"list": [
|
||||
{
|
||||
|
||||
@@ -1852,8 +1852,7 @@
|
||||
"refresh": false,
|
||||
"schemaVersion": 39,
|
||||
"tags": [
|
||||
"VictoriaMetrics",
|
||||
"monitoring"
|
||||
"victoriametrics"
|
||||
],
|
||||
"templating": {
|
||||
"list": [
|
||||
|
||||
@@ -2053,8 +2053,7 @@
|
||||
"refresh": "",
|
||||
"schemaVersion": 39,
|
||||
"tags": [
|
||||
"operator",
|
||||
"VictoriaMetrics"
|
||||
"victoriametrics"
|
||||
],
|
||||
"templating": {
|
||||
"list": [
|
||||
|
||||
@@ -3495,7 +3495,10 @@
|
||||
"preload": false,
|
||||
"refresh": "",
|
||||
"schemaVersion": 40,
|
||||
"tags": [],
|
||||
"tags": [
|
||||
"victoriametrics",
|
||||
"victorialogs"
|
||||
],
|
||||
"templating": {
|
||||
"list": [
|
||||
{
|
||||
|
||||
@@ -10062,7 +10062,9 @@
|
||||
"preload": false,
|
||||
"refresh": "",
|
||||
"schemaVersion": 40,
|
||||
"tags": [],
|
||||
"tags": [
|
||||
"victoriametrics"
|
||||
],
|
||||
"templating": {
|
||||
"list": [
|
||||
{
|
||||
|
||||
@@ -6194,8 +6194,7 @@
|
||||
"refresh": "",
|
||||
"schemaVersion": 40,
|
||||
"tags": [
|
||||
"victoriametrics",
|
||||
"vmsingle"
|
||||
"victoriametrics"
|
||||
],
|
||||
"templating": {
|
||||
"list": [
|
||||
|
||||
@@ -7647,7 +7647,6 @@
|
||||
"refresh": "",
|
||||
"schemaVersion": 40,
|
||||
"tags": [
|
||||
"vmagent",
|
||||
"victoriametrics"
|
||||
],
|
||||
"templating": {
|
||||
|
||||
@@ -3733,8 +3733,7 @@
|
||||
"refresh": "",
|
||||
"schemaVersion": 40,
|
||||
"tags": [
|
||||
"victoriametrics",
|
||||
"vmalert"
|
||||
"victoriametrics"
|
||||
],
|
||||
"templating": {
|
||||
"list": [
|
||||
|
||||
@@ -2559,7 +2559,9 @@
|
||||
"refresh": "30s",
|
||||
"revision": 1,
|
||||
"schemaVersion": 39,
|
||||
"tags": [],
|
||||
"tags": [
|
||||
"victoriametrics"
|
||||
],
|
||||
"templating": {
|
||||
"list": [
|
||||
{
|
||||
|
||||
@@ -7646,7 +7646,6 @@
|
||||
"refresh": "",
|
||||
"schemaVersion": 40,
|
||||
"tags": [
|
||||
"vmagent",
|
||||
"victoriametrics"
|
||||
],
|
||||
"templating": {
|
||||
|
||||
@@ -3732,8 +3732,7 @@
|
||||
"refresh": "",
|
||||
"schemaVersion": 40,
|
||||
"tags": [
|
||||
"victoriametrics",
|
||||
"vmalert"
|
||||
"victoriametrics"
|
||||
],
|
||||
"templating": {
|
||||
"list": [
|
||||
|
||||
@@ -2558,7 +2558,9 @@
|
||||
"refresh": "30s",
|
||||
"revision": 1,
|
||||
"schemaVersion": 39,
|
||||
"tags": [],
|
||||
"tags": [
|
||||
"victoriametrics"
|
||||
],
|
||||
"templating": {
|
||||
"list": [
|
||||
{
|
||||
|
||||
@@ -7,7 +7,7 @@ ROOT_IMAGE ?= alpine:3.21.3
|
||||
ROOT_IMAGE_SCRATCH ?= scratch
|
||||
CERTS_IMAGE := alpine:3.21.3
|
||||
|
||||
GO_BUILDER_IMAGE := golang:1.24.1-alpine
|
||||
GO_BUILDER_IMAGE := golang:1.24.2-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
|
||||
|
||||
@@ -4,7 +4,7 @@ services:
|
||||
# And forward them to --remoteWrite.url
|
||||
vmagent:
|
||||
container_name: vmagent
|
||||
image: victoriametrics/vmagent:v1.114.0
|
||||
image: victoriametrics/vmagent:v1.115.0
|
||||
depends_on:
|
||||
- "vminsert"
|
||||
ports:
|
||||
@@ -39,7 +39,7 @@ services:
|
||||
# where N is number of vmstorages (2 in this case).
|
||||
vmstorage-1:
|
||||
container_name: vmstorage-1
|
||||
image: victoriametrics/vmstorage:v1.114.0-cluster
|
||||
image: victoriametrics/vmstorage:v1.115.0-cluster
|
||||
ports:
|
||||
- 8482
|
||||
- 8400
|
||||
@@ -51,7 +51,7 @@ services:
|
||||
restart: always
|
||||
vmstorage-2:
|
||||
container_name: vmstorage-2
|
||||
image: victoriametrics/vmstorage:v1.114.0-cluster
|
||||
image: victoriametrics/vmstorage:v1.115.0-cluster
|
||||
ports:
|
||||
- 8482
|
||||
- 8400
|
||||
@@ -66,7 +66,7 @@ services:
|
||||
# pre-process them and distributes across configured vmstorage shards.
|
||||
vminsert:
|
||||
container_name: vminsert
|
||||
image: victoriametrics/vminsert:v1.114.0-cluster
|
||||
image: victoriametrics/vminsert:v1.115.0-cluster
|
||||
depends_on:
|
||||
- "vmstorage-1"
|
||||
- "vmstorage-2"
|
||||
@@ -81,7 +81,7 @@ services:
|
||||
# vmselect collects results from configured `--storageNode` shards.
|
||||
vmselect-1:
|
||||
container_name: vmselect-1
|
||||
image: victoriametrics/vmselect:v1.114.0-cluster
|
||||
image: victoriametrics/vmselect:v1.115.0-cluster
|
||||
depends_on:
|
||||
- "vmstorage-1"
|
||||
- "vmstorage-2"
|
||||
@@ -94,7 +94,7 @@ services:
|
||||
restart: always
|
||||
vmselect-2:
|
||||
container_name: vmselect-2
|
||||
image: victoriametrics/vmselect:v1.114.0-cluster
|
||||
image: victoriametrics/vmselect:v1.115.0-cluster
|
||||
depends_on:
|
||||
- "vmstorage-1"
|
||||
- "vmstorage-2"
|
||||
@@ -112,7 +112,7 @@ services:
|
||||
# It can be used as an authentication proxy.
|
||||
vmauth:
|
||||
container_name: vmauth
|
||||
image: victoriametrics/vmauth:v1.114.0
|
||||
image: victoriametrics/vmauth:v1.115.0
|
||||
depends_on:
|
||||
- "vmselect-1"
|
||||
- "vmselect-2"
|
||||
@@ -127,7 +127,7 @@ services:
|
||||
# vmalert executes alerting and recording rules
|
||||
vmalert:
|
||||
container_name: vmalert
|
||||
image: victoriametrics/vmalert:v1.114.0
|
||||
image: victoriametrics/vmalert:v1.115.0
|
||||
depends_on:
|
||||
- "vmauth"
|
||||
ports:
|
||||
|
||||
@@ -4,7 +4,7 @@ services:
|
||||
# And forward them to --remoteWrite.url
|
||||
vmagent:
|
||||
container_name: vmagent
|
||||
image: victoriametrics/vmagent:v1.114.0
|
||||
image: victoriametrics/vmagent:v1.115.0
|
||||
depends_on:
|
||||
- "victoriametrics"
|
||||
ports:
|
||||
@@ -20,7 +20,7 @@ services:
|
||||
# storing metrics and serve read requests.
|
||||
victoriametrics:
|
||||
container_name: victoriametrics
|
||||
image: victoriametrics/victoria-metrics:v1.114.0
|
||||
image: victoriametrics/victoria-metrics:v1.115.0
|
||||
ports:
|
||||
- 8428:8428
|
||||
- 8089:8089
|
||||
@@ -59,7 +59,7 @@ services:
|
||||
# vmalert executes alerting and recording rules
|
||||
vmalert:
|
||||
container_name: vmalert
|
||||
image: victoriametrics/vmalert:v1.114.0
|
||||
image: victoriametrics/vmalert:v1.115.0
|
||||
depends_on:
|
||||
- "victoriametrics"
|
||||
- "alertmanager"
|
||||
|
||||
@@ -8,7 +8,7 @@ RUN \
|
||||
cd /tmp/aws-lambda-rie && \
|
||||
CGO_ENABLED=0 go build -buildvcs=false -ldflags "-s -w" -o /aws-lambda-rie ./cmd/aws-lambda-rie
|
||||
|
||||
FROM python:3.12-bullseye
|
||||
FROM python:3.13-bullseye
|
||||
|
||||
RUN \
|
||||
apt update && \
|
||||
@@ -26,7 +26,7 @@ RUN \
|
||||
WORKDIR /var/task
|
||||
COPY --from=aws-lambda-rie /aws-lambda-rie /var/task/aws-lambda-rie
|
||||
COPY main.py /var/task/
|
||||
COPY --from=public.ecr.aws/datadog/lambda-extension:68 /opt/. /opt/
|
||||
COPY --from=public.ecr.aws/datadog/lambda-extension:73 /opt/. /opt/
|
||||
|
||||
ENTRYPOINT ["/var/task/aws-lambda-rie"]
|
||||
CMD ["/usr/local/bin/python", "-m", "awslambdaric", "main.lambda_handler"]
|
||||
|
||||
@@ -14,7 +14,7 @@ services:
|
||||
DD_API_KEY: test
|
||||
DD_DD_URL: http://dd-proxy:8427
|
||||
DD_PROFILING_ENABLED: false
|
||||
DD_ENHANCED_METRICS: false
|
||||
DD_ENHANCED_METRICS: true
|
||||
DD_LOGS_CONFIG_LOGS_DD_URL: http://dd-proxy:8427
|
||||
DD_SERVERLESS_FLUSH_STRATEGY: periodically,100
|
||||
depends_on:
|
||||
|
||||
@@ -18,7 +18,7 @@ services:
|
||||
- vlogs
|
||||
|
||||
generator:
|
||||
image: golang:1.24.1-alpine
|
||||
image: golang:1.24.2-alpine
|
||||
restart: always
|
||||
working_dir: /go/src/app
|
||||
volumes:
|
||||
|
||||
@@ -2,7 +2,7 @@ version: "3"
|
||||
|
||||
services:
|
||||
generator:
|
||||
image: golang:1.24.1-alpine
|
||||
image: golang:1.24.2-alpine
|
||||
restart: always
|
||||
working_dir: /go/src/app
|
||||
volumes:
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
- To use *vmanomaly*, part of the enterprise package, a license key is required. Obtain your key [here](https://victoriametrics.com/products/enterprise/trial/) for this tutorial or for enterprise use.
|
||||
- In the tutorial, we'll be using the following VictoriaMetrics components:
|
||||
- [VictoriaMetrics Single-Node](https://docs.victoriametrics.com/single-server-victoriametrics) (v1.114.0)
|
||||
- [vmalert](https://docs.victoriametrics.com/vmalert/) (v1.114.0)
|
||||
- [vmagent](https://docs.victoriametrics.com/vmagent/) (v1.114.0)
|
||||
- [VictoriaMetrics Single-Node](https://docs.victoriametrics.com/single-server-victoriametrics) (v1.115.0)
|
||||
- [vmalert](https://docs.victoriametrics.com/vmalert/) (v1.115.0)
|
||||
- [vmagent](https://docs.victoriametrics.com/vmagent/) (v1.115.0)
|
||||
- [Grafana](https://grafana.com/) (v.10.2.1)
|
||||
- [Docker](https://docs.docker.com/get-docker/) and [Docker Compose](https://docs.docker.com/compose/)
|
||||
- [Node exporter](https://github.com/prometheus/node_exporter#node-exporter) (v1.7.0) and [Alertmanager](https://prometheus.io/docs/alerting/latest/alertmanager/) (v0.27.0)
|
||||
@@ -315,7 +315,7 @@ Let's wrap it all up together into the `docker-compose.yml` file.
|
||||
services:
|
||||
vmagent:
|
||||
container_name: vmagent
|
||||
image: victoriametrics/vmagent:v1.114.0
|
||||
image: victoriametrics/vmagent:v1.115.0
|
||||
depends_on:
|
||||
- "victoriametrics"
|
||||
ports:
|
||||
@@ -332,7 +332,7 @@ services:
|
||||
|
||||
victoriametrics:
|
||||
container_name: victoriametrics
|
||||
image: victoriametrics/victoria-metrics:v1.114.0
|
||||
image: victoriametrics/victoria-metrics:v1.115.0
|
||||
ports:
|
||||
- 8428:8428
|
||||
volumes:
|
||||
@@ -365,7 +365,7 @@ services:
|
||||
|
||||
vmalert:
|
||||
container_name: vmalert
|
||||
image: victoriametrics/vmalert:v1.114.0
|
||||
image: victoriametrics/vmalert:v1.115.0
|
||||
depends_on:
|
||||
- "victoriametrics"
|
||||
ports:
|
||||
|
||||
@@ -49,7 +49,7 @@ See details about all supported options in the [vmgateway documentation](https:/
|
||||

|
||||
Copy the value of `Client secret`. It will be used later in Grafana configuration.<br>
|
||||
1. Go to `Clients` -> `grafana` -> `Client scopes`.<br>
|
||||
Click at `grafana-dedicated` -> `Add mapper` -> `By configuration` -> `User attribute`.<br>
|
||||
Click at `grafana-dedicated` -> `Configure a new mapper` -> `User attribute`.<br>
|
||||

|
||||

|
||||
Configure the mapper as follows<br>
|
||||
@@ -61,8 +61,13 @@ See details about all supported options in the [vmgateway documentation](https:/
|
||||
|
||||

|
||||
Click `Save`.<br>
|
||||
1. Go to `Users` -> select user to configure claims -> `Attributes`.<br>
|
||||
Specify `vm_access` as `Key`.<br>
|
||||
1. Go to `Realm settings` -> `User profile`.<br>
|
||||
Click `Create attribute`.<br>
|
||||
Specify `vm_access` as `Attribute [Name]`.<br>
|
||||

|
||||
Click `Save`.<br>
|
||||
1. Go to `Users` -> select user to configure.<br>
|
||||
Modify value of `vm_access` attribute.<br>
|
||||
For the purpose of this example, we will use 2 users:<br>
|
||||
- for the first user we will specify `{"tenant_id" : {"account_id": 0, "project_id": 0 },"extra_labels":{ "team": "admin" }}` as `Value`.
|
||||
- for the second user we will specify `{"tenant_id" : {"account_id": 0, "project_id": 1 },"extra_labels":{ "team": "dev" }}` as `Value`.
|
||||
@@ -219,44 +224,44 @@ version: '3'
|
||||
|
||||
services:
|
||||
keycloak:
|
||||
image: quay.io/keycloak/keycloak:21.0
|
||||
image: quay.io/keycloak/keycloak:26.1
|
||||
command:
|
||||
- start-dev
|
||||
ports:
|
||||
- 3001:8080
|
||||
environment:
|
||||
KEYCLOAK_ADMIN: admin
|
||||
KEYCLOAK_ADMIN_PASSWORD: change_me
|
||||
KC_BOOTSTRAP_ADMIN_USERNAME: admin
|
||||
KC_BOOTSTRAP_ADMIN_PASSWORD: change_me
|
||||
|
||||
grafana:
|
||||
image: grafana/grafana:10.4.2
|
||||
image: grafana/grafana:11.5.2
|
||||
network_mode: host
|
||||
volumes:
|
||||
- ./grafana.ini:/etc/grafana/grafana.ini
|
||||
- grafana_data:/var/lib/grafana/
|
||||
|
||||
vmsingle:
|
||||
image: victoriametrics/victoria-metrics:v1.114.0
|
||||
image: victoriametrics/victoria-metrics:v1.115.0
|
||||
command:
|
||||
- -httpListenAddr=0.0.0.0:8429
|
||||
|
||||
vmstorage:
|
||||
image: victoriametrics/vmstorage:v1.114.0-cluster
|
||||
image: victoriametrics/vmstorage:v1.115.0-cluster
|
||||
|
||||
vminsert:
|
||||
image: victoriametrics/vminsert:v1.114.0-cluster
|
||||
image: victoriametrics/vminsert:v1.115.0-cluster
|
||||
command:
|
||||
- -storageNode=vmstorage:8400
|
||||
- -httpListenAddr=0.0.0.0:8480
|
||||
|
||||
vmselect:
|
||||
image: victoriametrics/vmselect:v1.114.0-cluster
|
||||
image: victoriametrics/vmselect:v1.115.0-cluster
|
||||
command:
|
||||
- -storageNode=vmstorage:8401
|
||||
- -httpListenAddr=0.0.0.0:8481
|
||||
|
||||
vmagent:
|
||||
image: victoriametrics/vmagent:v1.114.0
|
||||
image: victoriametrics/vmagent:v1.115.0
|
||||
volumes:
|
||||
- ./scrape.yaml:/etc/vmagent/config.yaml
|
||||
command:
|
||||
@@ -265,7 +270,7 @@ services:
|
||||
- -remoteWrite.url=http://vmsingle:8429/api/v1/write
|
||||
|
||||
vmgateway-cluster:
|
||||
image: victoriametrics/vmgateway:v1.114.0-enterprise
|
||||
image: victoriametrics/vmgateway:v1.115.0-enterprise
|
||||
ports:
|
||||
- 8431:8431
|
||||
volumes:
|
||||
@@ -281,7 +286,7 @@ services:
|
||||
- -auth.oidcDiscoveryEndpoints=http://keycloak:8080/realms/master/.well-known/openid-configuration
|
||||
|
||||
vmgateway-single:
|
||||
image: victoriametrics/vmgateway:v1.114.0-enterprise
|
||||
image: victoriametrics/vmgateway:v1.115.0-enterprise
|
||||
ports:
|
||||
- 8432:8431
|
||||
volumes:
|
||||
@@ -369,9 +374,9 @@ In order to create a client for vmagent to use, follow the steps below:
|
||||

|
||||
Copy the value of `Client secret`. It will be used later in vmagent configuration.<br>
|
||||
1. Go to `Clients` -> `vmagent` -> `Client scopes`.<br>
|
||||
Click at `vmagent-dedicated` -> `Add mapper` -> `By configuration` -> `User attribute`.<br>
|
||||

|
||||

|
||||
Click at `vmagent-dedicated` -> `Configure a new mapper` -> `User attribute`.<br>
|
||||

|
||||

|
||||
Configure the mapper as follows<br>
|
||||
- `Name` as `vm_access`.
|
||||
- `Token Claim Name` as `vm_access`.
|
||||
@@ -379,13 +384,12 @@ In order to create a client for vmagent to use, follow the steps below:
|
||||
- `Claim JSON Type` as `JSON`.
|
||||
Enable `Add to ID token` and `Add to access token`.<br>
|
||||
|
||||

|
||||

|
||||
Click `Save`.<br>
|
||||
1. Go to `Service account roles` -> click on `service-account-vmagent`.<br>
|
||||

|
||||
1. Go to `Attributes` tab and add an attribute.
|
||||
Specify `vm_access` as `Key`.<br>
|
||||
Specify `{"tenant_id" : {"account_id": 0, "project_id": 0 }}` as a value.<br>
|
||||
Change `vm_access` attribute value to `{"tenant_id" : {"account_id": 0, "project_id": 0 }}`. <br>
|
||||

|
||||
Click `Save`.
|
||||
|
||||
@@ -393,7 +397,7 @@ Once iDP configuration is done, vmagent configuration needs to be updated to use
|
||||
|
||||
```yaml
|
||||
vmagent:
|
||||
image: victoriametrics/vmagent:v1.114.0
|
||||
image: victoriametrics/vmagent:v1.115.0
|
||||
volumes:
|
||||
- ./scrape.yaml:/etc/vmagent/config.yaml
|
||||
- ./vmagent-client-secret:/etc/vmagent/oauth2-client-secret
|
||||
|
||||
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 9.5 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 17 KiB |