mirror of
https://github.com/VictoriaMetrics/VictoriaMetrics.git
synced 2026-05-17 08:36:55 +03:00
Compare commits
16 Commits
streaming-
...
v1.91.3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7226242070 | ||
|
|
db84353809 | ||
|
|
fd6f50f883 | ||
|
|
8948af219b | ||
|
|
21a6584367 | ||
|
|
facba87494 | ||
|
|
a11badbbfd | ||
|
|
79c779b24b | ||
|
|
0c85e5f351 | ||
|
|
b250c3d7fb | ||
|
|
ac95755a7a | ||
|
|
5381e97cdd | ||
|
|
02fa89aed4 | ||
|
|
a7d24463ef | ||
|
|
ea333465e7 | ||
|
|
d0efb06250 |
2
.github/workflows/check-licenses.yml
vendored
2
.github/workflows/check-licenses.yml
vendored
@@ -17,7 +17,7 @@ jobs:
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@main
|
||||
with:
|
||||
go-version: 1.20.4
|
||||
go-version: 1.20.5
|
||||
id: go
|
||||
- name: Code checkout
|
||||
uses: actions/checkout@master
|
||||
|
||||
2
.github/workflows/codeql-analysis.yml
vendored
2
.github/workflows/codeql-analysis.yml
vendored
@@ -57,7 +57,7 @@ jobs:
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: 1.20.4
|
||||
go-version: 1.20.5
|
||||
check-latest: true
|
||||
cache: true
|
||||
if: ${{ matrix.language == 'go' }}
|
||||
|
||||
6
.github/workflows/main.yml
vendored
6
.github/workflows/main.yml
vendored
@@ -32,7 +32,7 @@ jobs:
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: 1.20.4
|
||||
go-version: 1.20.5
|
||||
check-latest: true
|
||||
cache: true
|
||||
|
||||
@@ -56,7 +56,7 @@ jobs:
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: 1.20.4
|
||||
go-version: 1.20.5
|
||||
check-latest: true
|
||||
cache: true
|
||||
|
||||
@@ -81,7 +81,7 @@ jobs:
|
||||
id: go
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: 1.20.4
|
||||
go-version: 1.20.5
|
||||
check-latest: true
|
||||
cache: true
|
||||
|
||||
|
||||
48
.github/workflows/nightly-build.yml
vendored
48
.github/workflows/nightly-build.yml
vendored
@@ -1,48 +0,0 @@
|
||||
name: nightly-build
|
||||
on:
|
||||
schedule:
|
||||
# Daily at 2:48am
|
||||
- cron: '48 2 * * *'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@main
|
||||
with:
|
||||
go-version: 1.20.4
|
||||
id: go
|
||||
|
||||
- name: Setup docker scan
|
||||
run: |
|
||||
mkdir -p ~/.docker/cli-plugins && \
|
||||
curl https://github.com/docker/scan-cli-plugin/releases/latest/download/docker-scan_linux_amd64 -L -s -S -o ~/.docker/cli-plugins/docker-scan &&\
|
||||
chmod +x ~/.docker/cli-plugins/docker-scan
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
- name: Code checkout
|
||||
uses: actions/checkout@master
|
||||
|
||||
- uses: actions/cache@v3
|
||||
with:
|
||||
path: gocache-for-docker
|
||||
key: gocache-docker-${{ runner.os }}-${{ steps.go.outputs.go-version }}-${{ hashFiles('go.mod') }}
|
||||
|
||||
- name: build & publish
|
||||
run: |
|
||||
docker scan --severity=medium --login --token "$SNYK_TOKEN" --accept-license
|
||||
LATEST_TAG=nightly PKG_TAG=nightly make publish
|
||||
env:
|
||||
SNYK_TOKEN: ${{ secrets.SNYK_AUTH_TOKEN }}
|
||||
77
.github/workflows/update-sandbox.yml
vendored
Normal file
77
.github/workflows/update-sandbox.yml
vendored
Normal file
@@ -0,0 +1,77 @@
|
||||
name: sandbox-release
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
permissions:
|
||||
contents: write
|
||||
jobs:
|
||||
deploy-sandbox:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
repository: VictoriaMetrics/ops
|
||||
ref: master
|
||||
token: ${{ secrets.VM_BOT_GH_TOKEN }}
|
||||
|
||||
- name: Import GPG key
|
||||
id: import-gpg
|
||||
uses: crazy-max/ghaction-import-gpg@v5
|
||||
with:
|
||||
gpg_private_key: ${{ secrets.VM_BOT_GPG_PRIVATE_KEY }}
|
||||
passphrase: ${{ secrets.VM_BOT_PASSPHRASE }}
|
||||
git_user_signingkey: true
|
||||
git_commit_gpgsign: true
|
||||
|
||||
- name: update image tag
|
||||
uses: fjogeleit/yaml-update-action@main
|
||||
with:
|
||||
valueFile: 'gcp-test/sandbox/manifests/benchmark-vm/vmcluster.yaml'
|
||||
commitChange: false
|
||||
createPR: false
|
||||
changes: |
|
||||
{
|
||||
"gcp-test/sandbox/manifests/benchmark-vm/vmcluster.yaml": {
|
||||
"spec.vminsert.image.tag": "${{ github.event.release.tag_name }}-enterprise-cluster",
|
||||
"spec.vmselect.image.tag": "${{ github.event.release.tag_name }}-enterprise-cluster",
|
||||
"spec.vmstorage.image.tag": "${{ github.event.release.tag_name }}-enterprise-cluster"
|
||||
},
|
||||
"gcp-test/sandbox/manifests/benchmark-vm/vmsingle.yaml": {
|
||||
"spec.image.tag": "${{ github.event.release.tag_name }}-enterprise"
|
||||
},
|
||||
"gcp-test/sandbox/manifests/monitoring/monitoring-vmagent.yaml": {
|
||||
"spec.image.tag": "${{ github.event.release.tag_name }}"
|
||||
},
|
||||
"gcp-test/sandbox/manifests/monitoring/monitoring-vmcluster.yaml": {
|
||||
"spec.vminsert.image.tag": "${{ github.event.release.tag_name }}-enterprise-cluster",
|
||||
"spec.vmselect.image.tag": "${{ github.event.release.tag_name }}-enterprise-cluster",
|
||||
"spec.vmstorage.image.tag": "${{ github.event.release.tag_name }}-enterprise-cluster"
|
||||
},
|
||||
"gcp-test/sandbox/manifests/monitoring/vmalert.yaml": {
|
||||
"spec.image.tag": "${{ github.event.release.tag_name }}-enterprise"
|
||||
}
|
||||
}
|
||||
|
||||
- name: commit changes
|
||||
run: |
|
||||
git config --global user.name "${{ steps.import-gpg.outputs.email }}"
|
||||
git config --global user.email "${{ steps.import-gpg.outputs.email }}"
|
||||
git add .
|
||||
git commit -S -m "Deploy image tag ${RELEASE_TAG} to sandbox"
|
||||
env:
|
||||
RELEASE_TAG: ${{ github.event.release.tag_name }}
|
||||
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v5
|
||||
with:
|
||||
author: ${{ github.actor }} <${{ github.actor }}@users.noreply.github.com>
|
||||
branch: release-automation
|
||||
token: ${{ secrets.VM_BOT_GH_TOKEN }}
|
||||
delete-branch: true
|
||||
title: "release ${{ github.event.release.tag_name }}"
|
||||
body: |
|
||||
Release [${{ github.event.release.tag_name }}](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/${{ github.event.release.tag_name }}) to sandbox
|
||||
|
||||
> Auto-generated by `Github Actions Bot`
|
||||
|
||||
2
Makefile
2
Makefile
@@ -31,7 +31,7 @@ all: \
|
||||
clean:
|
||||
rm -rf bin/*
|
||||
|
||||
publish: docker-scan \
|
||||
publish: package-base \
|
||||
publish-victoria-metrics \
|
||||
publish-vmagent \
|
||||
publish-vmalert \
|
||||
|
||||
@@ -587,6 +587,11 @@ func newRemoteWriteCtx(argIdx int, at *auth.Token, remoteWriteURL *url.URL, maxI
|
||||
}
|
||||
|
||||
func (rwctx *remoteWriteCtx) MustStop() {
|
||||
// sas must be stopped before rwctx is closed
|
||||
// because sas can write pending series to rwctx.pss if there are any
|
||||
sas := rwctx.sas.Swap(nil)
|
||||
sas.MustStop()
|
||||
|
||||
for _, ps := range rwctx.pss {
|
||||
ps.MustStop()
|
||||
}
|
||||
@@ -596,9 +601,6 @@ func (rwctx *remoteWriteCtx) MustStop() {
|
||||
rwctx.c.MustStop()
|
||||
rwctx.c = nil
|
||||
|
||||
sas := rwctx.sas.Swap(nil)
|
||||
sas.MustStop()
|
||||
|
||||
rwctx.fq.MustClose()
|
||||
rwctx.fq = nil
|
||||
|
||||
|
||||
@@ -1147,6 +1147,10 @@ The shortlist of configuration flags is the following:
|
||||
Optional OAuth2 scopes to use for -notifier.url. Scopes must be delimited by ';'.
|
||||
-remoteWrite.oauth2.tokenUrl string
|
||||
Optional OAuth2 tokenURL to use for -notifier.url.
|
||||
-remoteWrite.retryMaxTime duration
|
||||
The max time spent on retry attempts for the failed remote-write request. Change this value if it is expected for remoteWrite.url to be unreachable for more than -remoteWrite.retryMaxTime. See also -remoteWrite.retryMinInterval (default 30s)
|
||||
-remoteWrite.retryMinInterval duration
|
||||
The minimum delay between retry attempts. Every next retry attempt will double the delay to prevent hammering of remote database. See also -remoteWrite.retryMaxInterval (default 1s)
|
||||
-remoteWrite.sendTimeout duration
|
||||
Timeout for sending data to the configured -remoteWrite.url. (default 30s)
|
||||
-remoteWrite.showURL
|
||||
|
||||
@@ -23,6 +23,8 @@ import (
|
||||
var (
|
||||
disablePathAppend = flag.Bool("remoteWrite.disablePathAppend", false, "Whether to disable automatic appending of '/api/v1/write' path to the configured -remoteWrite.url.")
|
||||
sendTimeout = flag.Duration("remoteWrite.sendTimeout", 30*time.Second, "Timeout for sending data to the configured -remoteWrite.url.")
|
||||
retryMinInterval = flag.Duration("remoteWrite.retryMinInterval", time.Second, "The minimum delay between retry attempts. Every next retry attempt will double the delay to prevent hammering of remote database. See also -remoteWrite.retryMaxInterval")
|
||||
retryMaxTime = flag.Duration("remoteWrite.retryMaxTime", time.Second*30, "The max time spent on retry attempts for the failed remote-write request. Change this value if it is expected for remoteWrite.url to be unreachable for more than -remoteWrite.retryMaxTime. See also -remoteWrite.retryMinInterval")
|
||||
)
|
||||
|
||||
// Client is an asynchronous HTTP client for writing
|
||||
@@ -147,6 +149,7 @@ func (c *Client) run(ctx context.Context) {
|
||||
wr.Timeseries = append(wr.Timeseries, ts)
|
||||
}
|
||||
lastCtx, cancel := context.WithTimeout(context.Background(), defaultWriteTimeout)
|
||||
logger.Infof("shutting down remote write client and flushing remained %d series", len(wr.Timeseries))
|
||||
c.flush(lastCtx, wr)
|
||||
cancel()
|
||||
}
|
||||
@@ -180,9 +183,14 @@ func (c *Client) run(ctx context.Context) {
|
||||
var (
|
||||
sentRows = metrics.NewCounter(`vmalert_remotewrite_sent_rows_total`)
|
||||
sentBytes = metrics.NewCounter(`vmalert_remotewrite_sent_bytes_total`)
|
||||
sendDuration = metrics.NewFloatCounter(`vmalert_remotewrite_send_duration_seconds_total`)
|
||||
droppedRows = metrics.NewCounter(`vmalert_remotewrite_dropped_rows_total`)
|
||||
droppedBytes = metrics.NewCounter(`vmalert_remotewrite_dropped_bytes_total`)
|
||||
bufferFlushDuration = metrics.NewHistogram(`vmalert_remotewrite_flush_duration_seconds`)
|
||||
|
||||
_ = metrics.NewGauge(`vmalert_remotewrite_concurrency`, func() float64 {
|
||||
return float64(*concurrency)
|
||||
})
|
||||
)
|
||||
|
||||
// flush is a blocking function that marshals WriteRequest and sends
|
||||
@@ -203,12 +211,14 @@ func (c *Client) flush(ctx context.Context, wr *prompbmarshal.WriteRequest) {
|
||||
|
||||
b := snappy.Encode(nil, data)
|
||||
|
||||
const (
|
||||
retryCount = 5
|
||||
retryBackoff = time.Second
|
||||
)
|
||||
|
||||
for attempts := 0; attempts < retryCount; attempts++ {
|
||||
retryInterval, maxRetryInterval := *retryMinInterval, *retryMaxTime
|
||||
if retryInterval > maxRetryInterval {
|
||||
retryInterval = maxRetryInterval
|
||||
}
|
||||
timeStart := time.Now()
|
||||
defer sendDuration.Add(time.Since(timeStart).Seconds())
|
||||
L:
|
||||
for attempts := 0; ; attempts++ {
|
||||
err := c.send(ctx, b)
|
||||
if err == nil {
|
||||
sentRows.Add(len(wr.Timeseries))
|
||||
@@ -216,10 +226,10 @@ func (c *Client) flush(ctx context.Context, wr *prompbmarshal.WriteRequest) {
|
||||
return
|
||||
}
|
||||
|
||||
_, isRetriable := err.(*retriableError)
|
||||
logger.Warnf("attempt %d to send request failed: %s (retriable: %v)", attempts+1, err, isRetriable)
|
||||
_, isNotRetriable := err.(*nonRetriableError)
|
||||
logger.Warnf("attempt %d to send request failed: %s (retriable: %v)", attempts+1, err, !isNotRetriable)
|
||||
|
||||
if !isRetriable {
|
||||
if isNotRetriable {
|
||||
// exit fast if error isn't retriable
|
||||
break
|
||||
}
|
||||
@@ -227,12 +237,24 @@ func (c *Client) flush(ctx context.Context, wr *prompbmarshal.WriteRequest) {
|
||||
// check if request has been cancelled before backoff
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
break
|
||||
logger.Errorf("interrupting retry attempt %d: context cancelled", attempts+1)
|
||||
break L
|
||||
default:
|
||||
}
|
||||
|
||||
// sleeping to avoid remote db hammering
|
||||
time.Sleep(retryBackoff)
|
||||
timeLeftForRetries := maxRetryInterval - time.Since(timeStart)
|
||||
if timeLeftForRetries < 0 {
|
||||
// the max retry time has passed, so we give up
|
||||
break
|
||||
}
|
||||
|
||||
if retryInterval > timeLeftForRetries {
|
||||
retryInterval = timeLeftForRetries
|
||||
}
|
||||
// sleeping to prevent remote db hammering
|
||||
time.Sleep(retryInterval)
|
||||
retryInterval *= 2
|
||||
|
||||
}
|
||||
|
||||
droppedRows.Add(len(wr.Timeseries))
|
||||
@@ -276,22 +298,23 @@ func (c *Client) send(ctx context.Context, data []byte) error {
|
||||
case 2:
|
||||
// respond with a HTTP 2xx status code when the write is successful.
|
||||
return nil
|
||||
case 5:
|
||||
// respond with HTTP status code 5xx when the write fails and SHOULD be retried.
|
||||
return &retriableError{fmt.Errorf("unexpected response code %d for %s. Response body %q",
|
||||
resp.StatusCode, req.URL.Redacted(), body)}
|
||||
case 4:
|
||||
if resp.StatusCode != http.StatusTooManyRequests {
|
||||
// MUST NOT retry write requests on HTTP 4xx responses other than 429
|
||||
return &nonRetriableError{fmt.Errorf("unexpected response code %d for %s. Response body %q",
|
||||
resp.StatusCode, req.URL.Redacted(), body)}
|
||||
}
|
||||
fallthrough
|
||||
default:
|
||||
// respond with HTTP status code 4xx when the request is invalid, will never be able to succeed
|
||||
// and should not be retried.
|
||||
return fmt.Errorf("unexpected response code %d for %s. Response body %q",
|
||||
resp.StatusCode, req.URL.Redacted(), body)
|
||||
}
|
||||
}
|
||||
|
||||
type retriableError struct {
|
||||
type nonRetriableError struct {
|
||||
err error
|
||||
}
|
||||
|
||||
func (e *retriableError) Error() string {
|
||||
func (e *nonRetriableError) Error() string {
|
||||
return e.err.Error()
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -18,15 +19,30 @@ import (
|
||||
)
|
||||
|
||||
func TestClient_Push(t *testing.T) {
|
||||
oldMinInterval := *retryMinInterval
|
||||
*retryMinInterval = time.Millisecond * 10
|
||||
defer func() {
|
||||
*retryMinInterval = oldMinInterval
|
||||
}()
|
||||
|
||||
testSrv := newRWServer()
|
||||
cfg := Config{
|
||||
client, err := NewClient(context.Background(), Config{
|
||||
Addr: testSrv.URL,
|
||||
MaxBatchSize: 100,
|
||||
}
|
||||
client, err := NewClient(context.Background(), cfg)
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create client: %s", err)
|
||||
}
|
||||
|
||||
faultySrv := newFaultyRWServer()
|
||||
faultyClient, err := NewClient(context.Background(), Config{
|
||||
Addr: faultySrv.URL,
|
||||
MaxBatchSize: 50,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create faulty client: %s", err)
|
||||
}
|
||||
|
||||
r := rand.New(rand.NewSource(1))
|
||||
const rowsN = 1e4
|
||||
var sent int
|
||||
@@ -38,9 +54,16 @@ func TestClient_Push(t *testing.T) {
|
||||
}},
|
||||
}
|
||||
err := client.Push(s)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected err: %s", err)
|
||||
}
|
||||
if err == nil {
|
||||
sent++
|
||||
}
|
||||
err = faultyClient.Push(s)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected err: %s", err)
|
||||
}
|
||||
}
|
||||
if sent == 0 {
|
||||
t.Fatalf("0 series sent")
|
||||
@@ -48,10 +71,17 @@ func TestClient_Push(t *testing.T) {
|
||||
if err := client.Close(); err != nil {
|
||||
t.Fatalf("failed to close client: %s", err)
|
||||
}
|
||||
if err := faultyClient.Close(); err != nil {
|
||||
t.Fatalf("failed to close faulty client: %s", err)
|
||||
}
|
||||
got := testSrv.accepted()
|
||||
if got != sent {
|
||||
t.Fatalf("expected to have %d series; got %d", sent, got)
|
||||
}
|
||||
got = faultySrv.accepted()
|
||||
if got != sent {
|
||||
t.Fatalf("expected to have %d series for faulty client; got %d", sent, got)
|
||||
}
|
||||
}
|
||||
|
||||
func newRWServer() *rwServer {
|
||||
@@ -117,3 +147,42 @@ func (rw *rwServer) handler(w http.ResponseWriter, r *http.Request) {
|
||||
atomic.AddUint64(&rw.acceptedRows, uint64(len(wr.Timeseries)))
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// faultyRWServer sometimes respond with 5XX status code
|
||||
// or just closes the connection. Is used for testing retries.
|
||||
type faultyRWServer struct {
|
||||
*rwServer
|
||||
|
||||
reqsMu sync.Mutex
|
||||
reqs int
|
||||
}
|
||||
|
||||
func newFaultyRWServer() *faultyRWServer {
|
||||
rw := &faultyRWServer{
|
||||
rwServer: &rwServer{},
|
||||
}
|
||||
rw.Server = httptest.NewServer(http.HandlerFunc(rw.handler))
|
||||
return rw
|
||||
}
|
||||
|
||||
func (frw *faultyRWServer) handler(w http.ResponseWriter, r *http.Request) {
|
||||
frw.reqsMu.Lock()
|
||||
reqs := frw.reqs
|
||||
frw.reqs++
|
||||
if frw.reqs > 5 {
|
||||
frw.reqs = 0
|
||||
}
|
||||
frw.reqsMu.Unlock()
|
||||
|
||||
switch reqs {
|
||||
case 0, 1, 2, 3:
|
||||
frw.rwServer.handler(w, r)
|
||||
case 4:
|
||||
hj, _ := w.(http.Hijacker)
|
||||
conn, _, _ := hj.Hijack()
|
||||
conn.Close()
|
||||
case 5:
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
w.Write([]byte("server overloaded"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,7 +42,6 @@ func New() *Backoff {
|
||||
func (b *Backoff) Retry(ctx context.Context, cb retryableFunc) (uint64, error) {
|
||||
var attempt uint64
|
||||
for i := 0; i < b.retries; i++ {
|
||||
// @TODO we should use context to cancel retries
|
||||
err := cb()
|
||||
if err == nil {
|
||||
return attempt, nil
|
||||
@@ -55,7 +54,19 @@ func (b *Backoff) Retry(ctx context.Context, cb retryableFunc) (uint64, error) {
|
||||
backoff := float64(b.minDuration) * math.Pow(b.factor, float64(i))
|
||||
dur := time.Duration(backoff)
|
||||
logger.Errorf("got error: %s on attempt: %d; will retry in %v", err, attempt, dur)
|
||||
time.Sleep(time.Duration(backoff))
|
||||
|
||||
t := time.NewTimer(dur)
|
||||
select {
|
||||
case <-t.C:
|
||||
// duration elapsed, loop
|
||||
case <-ctx.Done():
|
||||
// context cancelled, kill the timer if it hasn't fired, and return
|
||||
// the last error we got
|
||||
if !t.Stop() {
|
||||
<-t.C
|
||||
}
|
||||
return attempt, err
|
||||
}
|
||||
}
|
||||
return attempt, fmt.Errorf("execution failed after %d retry attempts", b.retries)
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ func TestRetry_Do(t *testing.T) {
|
||||
backoffMinDuration time.Duration
|
||||
retryableFunc retryableFunc
|
||||
ctx context.Context
|
||||
withCancel bool
|
||||
cancelTimeout time.Duration
|
||||
want uint64
|
||||
wantErr bool
|
||||
}{
|
||||
@@ -79,10 +79,33 @@ func TestRetry_Do(t *testing.T) {
|
||||
want: 5,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "cancel context",
|
||||
backoffRetries: 5,
|
||||
backoffFactor: 0.1,
|
||||
backoffMinDuration: time.Millisecond * 10,
|
||||
retryableFunc: func() error {
|
||||
t := time.NewTicker(time.Millisecond * 5)
|
||||
defer t.Stop()
|
||||
for range t.C {
|
||||
return fmt.Errorf("got some error")
|
||||
}
|
||||
return nil
|
||||
},
|
||||
ctx: context.Background(),
|
||||
cancelTimeout: time.Second * 5,
|
||||
want: 3,
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
r := New()
|
||||
if tt.cancelTimeout != 0 {
|
||||
newCtx, cancelFn := context.WithTimeout(tt.ctx, tt.cancelTimeout)
|
||||
tt.ctx = newCtx
|
||||
defer cancelFn()
|
||||
}
|
||||
got, err := r.Retry(tt.ctx, tt.retryableFunc)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("Retry() error = %v, wantErr %v", err, tt.wantErr)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM golang:1.20.4 as build-web-stage
|
||||
FROM golang:1.20.5 as build-web-stage
|
||||
COPY build /build
|
||||
|
||||
WORKDIR /build
|
||||
|
||||
@@ -73,10 +73,9 @@ const HeatmapChart: FC<HeatmapChartProps> = ({
|
||||
});
|
||||
};
|
||||
const throttledSetScale = useCallback(throttle(setScale, 500), []);
|
||||
const setPlotScale = ({ u, min, max }: { u: uPlot, min: number, max: number }) => {
|
||||
const setPlotScale = ({ min, max }: { min: number, max: number }) => {
|
||||
const delta = (max - min) * 1000;
|
||||
if ((delta < limitsDurations.min) || (delta > limitsDurations.max)) return;
|
||||
u.setScale("x", { min, max });
|
||||
setXRange({ min, max });
|
||||
throttledSetScale({ min, max });
|
||||
};
|
||||
@@ -112,7 +111,7 @@ const HeatmapChart: FC<HeatmapChartProps> = ({
|
||||
const nxRange = e.deltaY < 0 ? oxRange * factor : oxRange / factor;
|
||||
const min = xVal - (zoomPos / width) * nxRange;
|
||||
const max = min + nxRange;
|
||||
u.batch(() => setPlotScale({ u, min, max }));
|
||||
u.batch(() => setPlotScale({ min, max }));
|
||||
});
|
||||
};
|
||||
|
||||
@@ -126,7 +125,6 @@ const HeatmapChart: FC<HeatmapChartProps> = ({
|
||||
e.preventDefault();
|
||||
const factor = (xRange.max - xRange.min) / 10 * (plus ? 1 : -1);
|
||||
setPlotScale({
|
||||
u: uPlotInst,
|
||||
min: xRange.min + factor,
|
||||
max: xRange.max - factor
|
||||
});
|
||||
@@ -241,7 +239,7 @@ const HeatmapChart: FC<HeatmapChartProps> = ({
|
||||
(u) => {
|
||||
const min = u.posToVal(u.select.left, "x");
|
||||
const max = u.posToVal(u.select.left + u.select.width, "x");
|
||||
setPlotScale({ u, min, max });
|
||||
setPlotScale({ min, max });
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -295,7 +293,6 @@ const HeatmapChart: FC<HeatmapChartProps> = ({
|
||||
|
||||
const zoomFactor = dur / 50 * dir;
|
||||
uPlotInst.batch(() => setPlotScale({
|
||||
u: uPlotInst,
|
||||
min: min + zoomFactor,
|
||||
max: max - zoomFactor
|
||||
}));
|
||||
|
||||
@@ -17,11 +17,11 @@ import useEventListener from "../../../../hooks/useEventListener";
|
||||
export interface ChartTooltipProps {
|
||||
id: string,
|
||||
u: uPlot,
|
||||
metrics: MetricResult[],
|
||||
series: SeriesItem[],
|
||||
yRange: number[];
|
||||
metricItem: MetricResult,
|
||||
seriesItem: SeriesItem,
|
||||
unit?: string,
|
||||
isSticky?: boolean,
|
||||
showQueryNum?: boolean,
|
||||
tooltipOffset: { left: number, top: number },
|
||||
tooltipIdx: { seriesIdx: number, dataIdx: number },
|
||||
onClose?: (id: string) => void
|
||||
@@ -31,12 +31,12 @@ const ChartTooltip: FC<ChartTooltipProps> = ({
|
||||
u,
|
||||
id,
|
||||
unit = "",
|
||||
metrics,
|
||||
series,
|
||||
yRange,
|
||||
metricItem,
|
||||
seriesItem,
|
||||
tooltipIdx,
|
||||
tooltipOffset,
|
||||
isSticky,
|
||||
showQueryNum,
|
||||
onClose
|
||||
}) => {
|
||||
const tooltipRef = useRef<HTMLDivElement>(null);
|
||||
@@ -49,21 +49,17 @@ const ChartTooltip: FC<ChartTooltipProps> = ({
|
||||
const [dataIdx, setDataIdx] = useState(tooltipIdx.dataIdx);
|
||||
|
||||
const value = get(u, ["data", seriesIdx, dataIdx], 0);
|
||||
const valueFormat = formatPrettyNumber(value, get(yRange, [0]), get(yRange, [1]));
|
||||
const valueFormat = formatPrettyNumber(value, get(u, ["scales", "1", "min"], 0), get(u, ["scales", "1", "max"], 1));
|
||||
const dataTime = u.data[0][dataIdx];
|
||||
const date = dayjs(dataTime * 1000).tz().format(DATE_FULL_TIMEZONE_FORMAT);
|
||||
|
||||
const color = series[seriesIdx]?.stroke+"";
|
||||
|
||||
const calculations = series[seriesIdx]?.calculations || {};
|
||||
|
||||
const groups = new Set(metrics.map(m => m.group));
|
||||
const showQueryNum = groups.size > 1;
|
||||
const group = metrics[seriesIdx-1]?.group || 0;
|
||||
const color = `${seriesItem?.stroke}`;
|
||||
const calculations = seriesItem?.calculations || {};
|
||||
const group = metricItem?.group || 0;
|
||||
|
||||
|
||||
const fullMetricName = useMemo(() => {
|
||||
const metric = metrics[seriesIdx-1]?.metric || {};
|
||||
const metric = metricItem?.metric || {};
|
||||
const labelNames = Object.keys(metric).filter(x => x != "__name__");
|
||||
const labels = labelNames.map(key => `${key}=${JSON.stringify(metric[key])}`);
|
||||
let metricName = metric["__name__"] || "";
|
||||
@@ -71,7 +67,7 @@ const ChartTooltip: FC<ChartTooltipProps> = ({
|
||||
metricName += "{" + labels.join(",") + "}";
|
||||
}
|
||||
return metricName;
|
||||
}, [metrics, seriesIdx]);
|
||||
}, [metricItem]);
|
||||
|
||||
const handleClose = () => {
|
||||
onClose && onClose(id);
|
||||
@@ -97,7 +93,7 @@ const ChartTooltip: FC<ChartTooltipProps> = ({
|
||||
const calcPosition = () => {
|
||||
if (!tooltipRef.current) return;
|
||||
|
||||
const topOnChart = u.valToPos((value || 0), series[seriesIdx]?.scale || "1");
|
||||
const topOnChart = u.valToPos((value || 0), seriesItem?.scale || "1");
|
||||
const leftOnChart = u.valToPos(dataTime, "x");
|
||||
const { width: tooltipWidth, height: tooltipHeight } = tooltipRef.current.getBoundingClientRect();
|
||||
const { width, height } = u.over.getBoundingClientRect();
|
||||
@@ -142,9 +138,7 @@ const ChartTooltip: FC<ChartTooltipProps> = ({
|
||||
>
|
||||
<div className="vm-chart-tooltip-header">
|
||||
<div className="vm-chart-tooltip-header__date">
|
||||
{showQueryNum && (
|
||||
<div>Query {group}</div>
|
||||
)}
|
||||
{showQueryNum && (<div>Query {group}</div>)}
|
||||
{date}
|
||||
</div>
|
||||
{isSticky && (
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import React, { FC, useState, useMemo } from "preact/compat";
|
||||
import React, { FC, useMemo } from "preact/compat";
|
||||
import { MouseEvent } from "react";
|
||||
import { LegendItemType } from "../../../../../utils/uplot/types";
|
||||
import "./style.scss";
|
||||
import classNames from "classnames";
|
||||
import Tooltip from "../../../../Main/Tooltip/Tooltip";
|
||||
import { getFreeFields } from "./helpers";
|
||||
import useCopyToClipboard from "../../../../../hooks/useCopyToClipboard";
|
||||
|
||||
@@ -15,7 +14,6 @@ interface LegendItemProps {
|
||||
|
||||
const LegendItem: FC<LegendItemProps> = ({ legend, onChange, isHeatmap }) => {
|
||||
const copyToClipboard = useCopyToClipboard();
|
||||
const [copiedValue, setCopiedValue] = useState("");
|
||||
|
||||
const freeFormFields = useMemo(() => {
|
||||
const result = getFreeFields(legend);
|
||||
@@ -25,20 +23,17 @@ const LegendItem: FC<LegendItemProps> = ({ legend, onChange, isHeatmap }) => {
|
||||
const calculations = legend.calculations;
|
||||
const showCalculations = Object.values(calculations).some(v => v);
|
||||
|
||||
const handleClickFreeField = async (val: string, id: string) => {
|
||||
const copied = await copyToClipboard(val);
|
||||
if (!copied) return;
|
||||
setCopiedValue(id);
|
||||
setTimeout(() => setCopiedValue(""), 2000);
|
||||
const handleClickFreeField = async (val: string) => {
|
||||
await copyToClipboard(val, `${val} has been copied`);
|
||||
};
|
||||
|
||||
const createHandlerClick = (legend: LegendItemType) => (e: MouseEvent<HTMLDivElement>) => {
|
||||
onChange && onChange(legend, e.ctrlKey || e.metaKey);
|
||||
};
|
||||
|
||||
const createHandlerCopy = (freeField: string, id: string) => (e: MouseEvent<HTMLDivElement>) => {
|
||||
const createHandlerCopy = (freeField: string) => (e: MouseEvent<HTMLDivElement>) => {
|
||||
e.stopPropagation();
|
||||
handleClickFreeField(freeField, id);
|
||||
handleClickFreeField(freeField);
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -62,21 +57,14 @@ const LegendItem: FC<LegendItemProps> = ({ legend, onChange, isHeatmap }) => {
|
||||
{legend.freeFormFields["__name__"]}
|
||||
{!!freeFormFields.length && <>{</>}
|
||||
{freeFormFields.map((f, i) => (
|
||||
<Tooltip
|
||||
key={f.id}
|
||||
open={copiedValue === f.id}
|
||||
title={"copied!"}
|
||||
placement="top-center"
|
||||
<span
|
||||
className="vm-legend-item-info__free-fields"
|
||||
key={f.key}
|
||||
onClick={createHandlerCopy(f.freeField)}
|
||||
title="copy to clipboard"
|
||||
>
|
||||
<span
|
||||
className="vm-legend-item-info__free-fields"
|
||||
key={f.key}
|
||||
onClick={createHandlerCopy(f.freeField, f.id)}
|
||||
title="copy to clipboard"
|
||||
>
|
||||
{f.freeField}{i + 1 < freeFormFields.length && ","}
|
||||
</span>
|
||||
</Tooltip>
|
||||
{f.freeField}{i + 1 < freeFormFields.length && ","}
|
||||
</span>
|
||||
))}
|
||||
{!!freeFormFields.length && <>}</>}
|
||||
</span>
|
||||
|
||||
@@ -1,18 +1,14 @@
|
||||
import React, { FC, useCallback, useEffect, useMemo, useRef, useState } from "preact/compat";
|
||||
import React, { FC, useCallback, useEffect, useRef, useState } from "preact/compat";
|
||||
import uPlot, {
|
||||
AlignedData as uPlotData,
|
||||
Options as uPlotOptions,
|
||||
Series as uPlotSeries,
|
||||
Range,
|
||||
Scales,
|
||||
Scale,
|
||||
} from "uplot";
|
||||
import { defaultOptions } from "../../../../utils/uplot/helpers";
|
||||
import { dragChart } from "../../../../utils/uplot/events";
|
||||
import { getAxes, getMinMaxBuffer } from "../../../../utils/uplot/axes";
|
||||
import { getAxes } from "../../../../utils/uplot/axes";
|
||||
import { MetricResult } from "../../../../api/types";
|
||||
import { dateFromSeconds, formatDateForNativeInput, limitsDurations } from "../../../../utils/time";
|
||||
import throttle from "lodash.throttle";
|
||||
import { TimeParams } from "../../../../types";
|
||||
import { YaxisState } from "../../../../state/graph/reducer";
|
||||
import "uplot/dist/uPlot.min.css";
|
||||
@@ -24,6 +20,7 @@ import { useAppState } from "../../../../state/common/StateContext";
|
||||
import { SeriesItem } from "../../../../utils/uplot/series";
|
||||
import { ElementSize } from "../../../../hooks/useElementSize";
|
||||
import useEventListener from "../../../../hooks/useEventListener";
|
||||
import { getRangeX, getRangeY, getScales } from "../../../../utils/uplot/scales";
|
||||
|
||||
export interface LineChartProps {
|
||||
metrics: MetricResult[];
|
||||
@@ -37,8 +34,6 @@ export interface LineChartProps {
|
||||
height?: number;
|
||||
}
|
||||
|
||||
enum typeChartUpdate {xRange = "xRange", yRange = "yRange"}
|
||||
|
||||
const LineChart: FC<LineChartProps> = ({
|
||||
data,
|
||||
series,
|
||||
@@ -55,7 +50,6 @@ const LineChart: FC<LineChartProps> = ({
|
||||
const uPlotRef = useRef<HTMLDivElement>(null);
|
||||
const [isPanning, setPanning] = useState(false);
|
||||
const [xRange, setXRange] = useState({ min: period.start, max: period.end });
|
||||
const [yRange, setYRange] = useState([0, 1]);
|
||||
const [uPlotInst, setUPlotInst] = useState<uPlot>();
|
||||
const [startTouchDistance, setStartTouchDistance] = useState(0);
|
||||
|
||||
@@ -63,24 +57,18 @@ const LineChart: FC<LineChartProps> = ({
|
||||
const [tooltipIdx, setTooltipIdx] = useState({ seriesIdx: -1, dataIdx: -1 });
|
||||
const [tooltipOffset, setTooltipOffset] = useState({ left: 0, top: 0 });
|
||||
const [stickyTooltips, setStickyToolTips] = useState<ChartTooltipProps[]>([]);
|
||||
const tooltipId = useMemo(() => `${tooltipIdx.seriesIdx}_${tooltipIdx.dataIdx}`, [tooltipIdx]);
|
||||
|
||||
const setScale = ({ min, max }: { min: number, max: number }): void => {
|
||||
const setPlotScale = ({ min, max }: { min: number, max: number }) => {
|
||||
const delta = (max - min) * 1000;
|
||||
if ((delta < limitsDurations.min) || (delta > limitsDurations.max)) return;
|
||||
setXRange({ min, max });
|
||||
setPeriod({
|
||||
from: dayjs(min * 1000).toDate(),
|
||||
to: dayjs(max * 1000).toDate()
|
||||
});
|
||||
};
|
||||
const throttledSetScale = useCallback(throttle(setScale, 500), []);
|
||||
const setPlotScale = ({ u, min, max }: { u: uPlot, min: number, max: number }) => {
|
||||
const delta = (max - min) * 1000;
|
||||
if ((delta < limitsDurations.min) || (delta > limitsDurations.max)) return;
|
||||
u.setScale("x", { min, max });
|
||||
setXRange({ min, max });
|
||||
throttledSetScale({ min, max });
|
||||
};
|
||||
|
||||
const onReadyChart = (u: uPlot) => {
|
||||
const onReadyChart = (u: uPlot): void => {
|
||||
const factor = 0.9;
|
||||
setTooltipOffset({
|
||||
left: parseFloat(u.over.style.left),
|
||||
@@ -111,7 +99,7 @@ const LineChart: FC<LineChartProps> = ({
|
||||
const nxRange = e.deltaY < 0 ? oxRange * factor : oxRange / factor;
|
||||
const min = xVal - (zoomPos / width) * nxRange;
|
||||
const max = min + nxRange;
|
||||
u.batch(() => setPlotScale({ u, min, max }));
|
||||
u.batch(() => setPlotScale({ min, max }));
|
||||
});
|
||||
};
|
||||
|
||||
@@ -125,33 +113,41 @@ const LineChart: FC<LineChartProps> = ({
|
||||
e.preventDefault();
|
||||
const factor = (xRange.max - xRange.min) / 10 * (plus ? 1 : -1);
|
||||
setPlotScale({
|
||||
u: uPlotInst,
|
||||
min: xRange.min + factor,
|
||||
max: xRange.max - factor
|
||||
});
|
||||
}
|
||||
}, [uPlotInst, xRange]);
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
if (!showTooltip) return;
|
||||
const id = `${tooltipIdx.seriesIdx}_${tooltipIdx.dataIdx}`;
|
||||
const props = {
|
||||
const getChartProps = useCallback(() => {
|
||||
const { seriesIdx, dataIdx } = tooltipIdx;
|
||||
const id = `${seriesIdx}_${dataIdx}`;
|
||||
const metricItem = metrics[seriesIdx-1];
|
||||
const seriesItem = series[seriesIdx] as SeriesItem;
|
||||
|
||||
const groups = new Set(metrics.map(m => m.group));
|
||||
const showQueryNum = groups.size > 1;
|
||||
|
||||
return {
|
||||
id,
|
||||
unit,
|
||||
series,
|
||||
metrics,
|
||||
yRange,
|
||||
seriesItem,
|
||||
metricItem,
|
||||
tooltipIdx,
|
||||
tooltipOffset,
|
||||
showQueryNum,
|
||||
};
|
||||
}, [uPlotInst, metrics, series, tooltipIdx, tooltipOffset, unit]);
|
||||
|
||||
if (!stickyTooltips.find(t => t.id === id)) {
|
||||
const tooltipProps = JSON.parse(JSON.stringify(props));
|
||||
setStickyToolTips(prev => [...prev, tooltipProps]);
|
||||
const handleClick = useCallback(() => {
|
||||
if (!showTooltip) return;
|
||||
const props = getChartProps();
|
||||
if (!stickyTooltips.find(t => t.id === props.id)) {
|
||||
setStickyToolTips(prev => [...prev, props as ChartTooltipProps]);
|
||||
}
|
||||
}, [metrics, series, stickyTooltips, tooltipIdx, tooltipOffset, showTooltip, unit, yRange]);
|
||||
}, [getChartProps, stickyTooltips, showTooltip]);
|
||||
|
||||
const handleUnStick = (id:string) => {
|
||||
const handleUnStick = (id: string) => {
|
||||
setStickyToolTips(prev => prev.filter(t => t.id !== id));
|
||||
};
|
||||
|
||||
@@ -165,23 +161,34 @@ const LineChart: FC<LineChartProps> = ({
|
||||
setTooltipIdx(prev => ({ ...prev, seriesIdx }));
|
||||
};
|
||||
|
||||
const getRangeX = (): Range.MinMax => [xRange.min, xRange.max];
|
||||
|
||||
const getRangeY = (u: uPlot, min = 0, max = 1, axis: string): Range.MinMax => {
|
||||
if (axis == "1") {
|
||||
setYRange([min, max]);
|
||||
}
|
||||
if (yaxis.limits.enable) return yaxis.limits.range[axis];
|
||||
return getMinMaxBuffer(min, max);
|
||||
const addSeries = (u: uPlot, series: uPlotSeries[]) => {
|
||||
series.forEach((s) => {
|
||||
u.addSeries(s);
|
||||
});
|
||||
};
|
||||
|
||||
const getScales = (): Scales => {
|
||||
const scales: { [key: string]: { range: Scale.Range } } = { x: { range: getRangeX } };
|
||||
const ranges = Object.keys(yaxis.limits.range);
|
||||
(ranges.length ? ranges : ["1"]).forEach(axis => {
|
||||
scales[axis] = { range: (u: uPlot, min = 0, max = 1) => getRangeY(u, min, max, axis) };
|
||||
const delSeries = (u: uPlot) => {
|
||||
for (let i = u.series.length - 1; i >= 0; i--) {
|
||||
u.delSeries(i);
|
||||
}
|
||||
};
|
||||
|
||||
const delHooks = (u: uPlot) => {
|
||||
Object.keys(u.hooks).forEach(hook => {
|
||||
u.hooks[hook as keyof uPlot.Hooks.Arrays] = [];
|
||||
});
|
||||
return scales;
|
||||
};
|
||||
|
||||
const handleDestroy = (u: uPlot) => {
|
||||
delSeries(u);
|
||||
delHooks(u);
|
||||
u.setData([]);
|
||||
};
|
||||
|
||||
const setSelect = (u: uPlot) => {
|
||||
const min = u.posToVal(u.select.left, "x");
|
||||
const max = u.posToVal(u.select.left + u.select.width, "x");
|
||||
setPlotScale({ min, max });
|
||||
};
|
||||
|
||||
const options: uPlotOptions = {
|
||||
@@ -189,49 +196,18 @@ const LineChart: FC<LineChartProps> = ({
|
||||
tzDate: ts => dayjs(formatDateForNativeInput(dateFromSeconds(ts))).local().toDate(),
|
||||
series,
|
||||
axes: getAxes( [{}, { scale: "1" }], unit),
|
||||
scales: { ...getScales() },
|
||||
scales: getScales(yaxis, xRange),
|
||||
width: layoutSize.width || 400,
|
||||
height: height || 500,
|
||||
plugins: [{ hooks: { ready: onReadyChart, setCursor, setSeries: seriesFocus } }],
|
||||
hooks: {
|
||||
setSelect: [
|
||||
(u) => {
|
||||
const min = u.posToVal(u.select.left, "x");
|
||||
const max = u.posToVal(u.select.left + u.select.width, "x");
|
||||
setPlotScale({ u, min, max });
|
||||
}
|
||||
]
|
||||
}
|
||||
ready: [onReadyChart],
|
||||
setSeries: [seriesFocus],
|
||||
setCursor: [setCursor],
|
||||
setSelect: [setSelect],
|
||||
destroy: [handleDestroy],
|
||||
},
|
||||
};
|
||||
|
||||
const updateChart = (type: typeChartUpdate): void => {
|
||||
if (!uPlotInst) return;
|
||||
switch (type) {
|
||||
case typeChartUpdate.xRange:
|
||||
uPlotInst.scales.x.range = getRangeX;
|
||||
break;
|
||||
case typeChartUpdate.yRange:
|
||||
Object.keys(yaxis.limits.range).forEach(axis => {
|
||||
if (!uPlotInst.scales[axis]) return;
|
||||
uPlotInst.scales[axis].range = (u: uPlot, min = 0, max = 1) => getRangeY(u, min, max, axis);
|
||||
});
|
||||
break;
|
||||
}
|
||||
if (!isPanning) uPlotInst.redraw();
|
||||
};
|
||||
|
||||
useEffect(() => setXRange({ min: period.start, max: period.end }), [period]);
|
||||
|
||||
useEffect(() => {
|
||||
setStickyToolTips([]);
|
||||
setTooltipIdx({ seriesIdx: -1, dataIdx: -1 });
|
||||
if (!uPlotRef.current) return;
|
||||
const u = new uPlot(options, data, uPlotRef.current);
|
||||
setUPlotInst(u);
|
||||
setXRange({ min: period.start, max: period.end });
|
||||
return u.destroy;
|
||||
}, [uPlotRef.current, series, layoutSize, height, isDarkTheme]);
|
||||
|
||||
const handleTouchStart = (e: TouchEvent) => {
|
||||
if (e.touches.length !== 2) return;
|
||||
e.preventDefault();
|
||||
@@ -257,19 +233,63 @@ const LineChart: FC<LineChartProps> = ({
|
||||
|
||||
const zoomFactor = dur / 50 * dir;
|
||||
uPlotInst.batch(() => setPlotScale({
|
||||
u: uPlotInst,
|
||||
min: min + zoomFactor,
|
||||
max: max - zoomFactor
|
||||
}));
|
||||
}, [uPlotInst, startTouchDistance, xRange]);
|
||||
|
||||
useEffect(() => updateChart(typeChartUpdate.xRange), [xRange]);
|
||||
useEffect(() => updateChart(typeChartUpdate.yRange), [yaxis]);
|
||||
useEffect(() => {
|
||||
setXRange({ min: period.start, max: period.end });
|
||||
}, [period]);
|
||||
|
||||
useEffect(() => {
|
||||
const show = tooltipIdx.dataIdx !== -1 && tooltipIdx.seriesIdx !== -1;
|
||||
setShowTooltip(show);
|
||||
}, [tooltipIdx, stickyTooltips]);
|
||||
setStickyToolTips([]);
|
||||
setTooltipIdx({ seriesIdx: -1, dataIdx: -1 });
|
||||
if (!uPlotRef.current) return;
|
||||
if (uPlotInst) uPlotInst.destroy();
|
||||
const u = new uPlot(options, data, uPlotRef.current);
|
||||
setUPlotInst(u);
|
||||
setXRange({ min: period.start, max: period.end });
|
||||
return u.destroy;
|
||||
}, [uPlotRef, isDarkTheme]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!uPlotInst) return;
|
||||
uPlotInst.setData(data);
|
||||
uPlotInst.redraw();
|
||||
}, [data]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!uPlotInst) return;
|
||||
delSeries(uPlotInst);
|
||||
addSeries(uPlotInst, series);
|
||||
uPlotInst.redraw();
|
||||
}, [series]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!uPlotInst) return;
|
||||
Object.keys(yaxis.limits.range).forEach(axis => {
|
||||
if (!uPlotInst.scales[axis]) return;
|
||||
uPlotInst.scales[axis].range = (u: uPlot, min = 0, max = 1) => getRangeY(u, min, max, axis, yaxis);
|
||||
});
|
||||
uPlotInst.redraw();
|
||||
}, [yaxis]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!uPlotInst) return;
|
||||
uPlotInst.scales.x.range = () => getRangeX(xRange);
|
||||
uPlotInst.redraw();
|
||||
}, [xRange]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!uPlotInst) return;
|
||||
uPlotInst.setSize({ width: layoutSize.width || 400, height: height || 500 });
|
||||
uPlotInst.redraw();
|
||||
}, [height, layoutSize]);
|
||||
|
||||
useEffect(() => {
|
||||
setShowTooltip(tooltipIdx.dataIdx !== -1 && tooltipIdx.seriesIdx !== -1);
|
||||
}, [tooltipIdx]);
|
||||
|
||||
useEventListener("click", handleClick);
|
||||
useEventListener("keydown", handleKeyDown);
|
||||
@@ -293,14 +313,8 @@ const LineChart: FC<LineChartProps> = ({
|
||||
/>
|
||||
{uPlotInst && showTooltip && (
|
||||
<ChartTooltip
|
||||
unit={unit}
|
||||
{...getChartProps()}
|
||||
u={uPlotInst}
|
||||
series={series as SeriesItem[]}
|
||||
metrics={metrics}
|
||||
yRange={yRange}
|
||||
tooltipIdx={tooltipIdx}
|
||||
tooltipOffset={tooltipOffset}
|
||||
id={tooltipId}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ import "./style.scss";
|
||||
import { ReactNode } from "react";
|
||||
import { ExoticComponent } from "react";
|
||||
import useDeviceDetect from "../../../hooks/useDeviceDetect";
|
||||
import useEventListener from "../../../hooks/useEventListener";
|
||||
|
||||
interface TooltipProps {
|
||||
children: ReactNode
|
||||
@@ -30,7 +29,6 @@ const Tooltip: FC<TooltipProps> = ({
|
||||
const popperRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const onScrollWindow = () => setIsOpen(false);
|
||||
useEventListener("scroll", onScrollWindow);
|
||||
|
||||
useEffect(() => {
|
||||
if (!popperRef.current || !isOpen) return;
|
||||
@@ -38,6 +36,11 @@ const Tooltip: FC<TooltipProps> = ({
|
||||
width: popperRef.current.clientWidth,
|
||||
height: popperRef.current.clientHeight
|
||||
});
|
||||
window.addEventListener("scroll", onScrollWindow);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("scroll", onScrollWindow);
|
||||
};
|
||||
}, [isOpen]);
|
||||
|
||||
const popperStyle = useMemo(() => {
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
import React, { FC, useCallback, useEffect, useMemo, useState } from "preact/compat";
|
||||
import React, { FC, useEffect, useMemo, useState } from "preact/compat";
|
||||
import { MetricResult } from "../../../api/types";
|
||||
import LineChart from "../../Chart/Line/LineChart/LineChart";
|
||||
import { AlignedData as uPlotData, Series as uPlotSeries } from "uplot";
|
||||
import Legend from "../../Chart/Line/Legend/Legend";
|
||||
import LegendHeatmap from "../../Chart/Heatmap/LegendHeatmap/LegendHeatmap";
|
||||
import { getHideSeries, getLegendItem, getSeriesItemContext, SeriesItem } from "../../../utils/uplot/series";
|
||||
import {
|
||||
getHideSeries,
|
||||
getLegendItem,
|
||||
getSeriesItemContext,
|
||||
SeriesItem
|
||||
} from "../../../utils/uplot/series";
|
||||
import { getLimitsYAxis, getMinMaxBuffer, getTimeSeries } from "../../../utils/uplot/axes";
|
||||
import { LegendItemType } from "../../../utils/uplot/types";
|
||||
import { TimeParams } from "../../../types";
|
||||
@@ -56,7 +61,6 @@ const GraphView: FC<GraphViewProps> = ({
|
||||
const currentStep = useMemo(() => customStep || period.step || "1s", [period.step, customStep]);
|
||||
|
||||
const data = useMemo(() => normalizeData(dataRaw, isHistogram), [isHistogram, dataRaw]);
|
||||
const getSeriesItem = useCallback(getSeriesItemContext(), [data]);
|
||||
|
||||
const [dataChart, setDataChart] = useState<uPlotData>([[]]);
|
||||
const [series, setSeries] = useState<uPlotSeries[]>([]);
|
||||
@@ -64,6 +68,10 @@ const GraphView: FC<GraphViewProps> = ({
|
||||
const [hideSeries, setHideSeries] = useState<string[]>([]);
|
||||
const [legendValue, setLegendValue] = useState<TooltipHeatmapProps | null>(null);
|
||||
|
||||
const getSeriesItem = useMemo(() => {
|
||||
return getSeriesItemContext(data, hideSeries, alias);
|
||||
}, [data, hideSeries, alias]);
|
||||
|
||||
const setLimitsYaxis = (values: {[key: string]: number[]}) => {
|
||||
const limits = getLimitsYAxis(values, !isHistogram);
|
||||
setYaxisLimits(limits);
|
||||
@@ -73,10 +81,6 @@ const GraphView: FC<GraphViewProps> = ({
|
||||
setHideSeries(getHideSeries({ hideSeries, legend, metaKey, series }));
|
||||
};
|
||||
|
||||
const handleChangeLegend = (val: TooltipHeatmapProps) => {
|
||||
setLegendValue(val);
|
||||
};
|
||||
|
||||
const prepareHistogramData = (data: (number | null)[][]) => {
|
||||
const values = data.slice(1, data.length);
|
||||
const xs: (number | null | undefined)[] = [];
|
||||
@@ -105,8 +109,9 @@ const GraphView: FC<GraphViewProps> = ({
|
||||
const tempLegend: LegendItemType[] = [];
|
||||
const tempSeries: uPlotSeries[] = [{}];
|
||||
|
||||
data?.forEach((d) => {
|
||||
const seriesItem = getSeriesItem(d, hideSeries, alias);
|
||||
data?.forEach((d, i) => {
|
||||
const seriesItem = getSeriesItem(d, i);
|
||||
|
||||
tempSeries.push(seriesItem);
|
||||
tempLegend.push(getLegendItem(seriesItem, d.group));
|
||||
const tmpValues = tempValues[d.group] || [];
|
||||
@@ -156,8 +161,8 @@ const GraphView: FC<GraphViewProps> = ({
|
||||
useEffect(() => {
|
||||
const tempLegend: LegendItemType[] = [];
|
||||
const tempSeries: uPlotSeries[] = [{}];
|
||||
data?.forEach(d => {
|
||||
const seriesItem = getSeriesItem(d, hideSeries, alias);
|
||||
data?.forEach((d, i) => {
|
||||
const seriesItem = getSeriesItem(d, i);
|
||||
tempSeries.push(seriesItem);
|
||||
tempLegend.push(getLegendItem(seriesItem, d.group));
|
||||
});
|
||||
@@ -199,7 +204,7 @@ const GraphView: FC<GraphViewProps> = ({
|
||||
setPeriod={setPeriod}
|
||||
layoutSize={containerSize}
|
||||
height={height}
|
||||
onChangeLegend={handleChangeLegend}
|
||||
onChangeLegend={setLegendValue}
|
||||
/>
|
||||
)}
|
||||
{!isHistogram && showLegend && (
|
||||
|
||||
@@ -51,6 +51,7 @@
|
||||
font-weight: bold;
|
||||
text-transform: capitalize;
|
||||
text-align: left;
|
||||
overflow-wrap: normal;
|
||||
}
|
||||
|
||||
&_gray {
|
||||
|
||||
@@ -17,7 +17,7 @@ export const dragChart = ({ e, factor = 0.85, u, setPanning, setPlotScale }: Dra
|
||||
|
||||
const clientX = isMouseEvent ? e.clientX : e.touches[0].clientX;
|
||||
const dx = xUnitsPerPx * ((clientX - leftStart) * factor);
|
||||
setPlotScale({ u, min: scXMin - dx, max: scXMax - dx });
|
||||
setPlotScale({ min: scXMin - dx, max: scXMax - dx });
|
||||
};
|
||||
const mouseUp = () => {
|
||||
setPanning(false);
|
||||
|
||||
@@ -145,17 +145,22 @@ const sortBucketsByValues = (a: MetricResult, b: MetricResult) => getUpperBound(
|
||||
|
||||
export const normalizeData = (buckets: MetricResult[], isHistogram?: boolean): MetricResult[] => {
|
||||
if (!isHistogram) return buckets;
|
||||
|
||||
const sortedBuckets = buckets.sort(sortBucketsByValues);
|
||||
const vmBuckets = convertPrometheusToVictoriaMetrics(sortedBuckets);
|
||||
const allValues = vmBuckets.map(b => b.values).flat();
|
||||
|
||||
// Compute total hits for each timestamp upfront
|
||||
const totalHitsPerTimestamp: { [timestamp: number]: number } = {};
|
||||
vmBuckets.forEach(bucket =>
|
||||
bucket.values.forEach(([timestamp, value]) => {
|
||||
totalHitsPerTimestamp[timestamp] = (totalHitsPerTimestamp[timestamp] || 0) + +value;
|
||||
})
|
||||
);
|
||||
|
||||
const result = vmBuckets.map(bucket => {
|
||||
const values = bucket.values.map((v) => {
|
||||
const totalHits = allValues
|
||||
.filter(av => av[0] === v[0])
|
||||
.reduce((bucketSum, v) => bucketSum + +v[1], 0);
|
||||
|
||||
return [v[0], `${Math.round((+v[1] / totalHits) * 100)}`];
|
||||
const values = bucket.values.map(([timestamp, value]) => {
|
||||
const totalHits = totalHitsPerTimestamp[timestamp];
|
||||
return [timestamp, `${Math.round((+value / totalHits) * 100)}`];
|
||||
});
|
||||
|
||||
return { ...bucket, values };
|
||||
|
||||
26
app/vmui/packages/vmui/src/utils/uplot/scales.ts
Normal file
26
app/vmui/packages/vmui/src/utils/uplot/scales.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import uPlot, { Range, Scale, Scales } from "uplot";
|
||||
import { getMinMaxBuffer } from "./axes";
|
||||
import { YaxisState } from "../../state/graph/reducer";
|
||||
|
||||
interface XRangeType {
|
||||
min: number,
|
||||
max: number
|
||||
}
|
||||
|
||||
export const getRangeX = (xRange: XRangeType): Range.MinMax => {
|
||||
return [xRange.min, xRange.max];
|
||||
};
|
||||
|
||||
export const getRangeY = (u: uPlot, min = 0, max = 1, axis: string, yaxis: YaxisState): Range.MinMax => {
|
||||
if (yaxis.limits.enable) return yaxis.limits.range[axis];
|
||||
return getMinMaxBuffer(min, max);
|
||||
};
|
||||
|
||||
export const getScales = (yaxis: YaxisState, xRange: XRangeType): Scales => {
|
||||
const scales: { [key: string]: { range: Scale.Range } } = { x: { range: () => getRangeX(xRange) } };
|
||||
const ranges = Object.keys(yaxis.limits.range);
|
||||
(ranges.length ? ranges : ["1"]).forEach(axis => {
|
||||
scales[axis] = { range: (u: uPlot, min = 0, max = 1) => getRangeY(u, min, max, axis, yaxis) };
|
||||
});
|
||||
return scales;
|
||||
};
|
||||
@@ -17,26 +17,34 @@ export interface SeriesItem extends Series {
|
||||
}
|
||||
}
|
||||
|
||||
export const getSeriesItemContext = () => {
|
||||
export const getSeriesItemContext = (data: MetricResult[], hideSeries: string[], alias: string[]) => {
|
||||
const colorState: {[key: string]: string} = {};
|
||||
|
||||
return (d: MetricResult, hideSeries: string[], alias: string[]): SeriesItem => {
|
||||
const label = getNameForMetric(d, alias[d.group - 1]);
|
||||
const countSavedColors = Object.keys(colorState).length;
|
||||
const hasBasicColors = countSavedColors < baseContrastColors.length;
|
||||
if (hasBasicColors) colorState[label] = colorState[label] || baseContrastColors[countSavedColors];
|
||||
|
||||
const calculations = data.map(d => {
|
||||
const values = d.values.map(v => promValueToNumber(v[1]));
|
||||
const min = getMinFromArray(values);
|
||||
const max = getMaxFromArray(values);
|
||||
const median = getMedianFromArray(values);
|
||||
const last = getLastFromArray(values);
|
||||
return {
|
||||
min: getMinFromArray(values),
|
||||
max: getMaxFromArray(values),
|
||||
median: getMedianFromArray(values),
|
||||
last: getLastFromArray(values),
|
||||
};
|
||||
});
|
||||
|
||||
const maxColors = Math.min(data.length, baseContrastColors.length);
|
||||
for (let i = 0; i < maxColors; i++) {
|
||||
const label = getNameForMetric(data[i], alias[data[i].group - 1]);
|
||||
colorState[label] = baseContrastColors[i];
|
||||
}
|
||||
|
||||
return (d: MetricResult, i: number): SeriesItem => {
|
||||
const label = getNameForMetric(d, alias[d.group - 1]);
|
||||
const color = colorState[label] || getColorFromString(label);
|
||||
const { min, max, median, last } = calculations[i];
|
||||
|
||||
return {
|
||||
label,
|
||||
freeFormFields: d.metric,
|
||||
width: 1.4,
|
||||
stroke: colorState[label] || getColorFromString(label),
|
||||
stroke: color,
|
||||
show: !includesHideSeries(label, hideSeries),
|
||||
scale: "1",
|
||||
points: {
|
||||
|
||||
@@ -12,7 +12,7 @@ export interface DragArgs {
|
||||
u: uPlot,
|
||||
factor: number,
|
||||
setPanning: (enable: boolean) => void,
|
||||
setPlotScale: ({ u, min, max }: { u: uPlot, min: number, max: number }) => void
|
||||
setPlotScale: ({ min, max }: { min: number, max: number }) => void
|
||||
}
|
||||
|
||||
export interface LegendItemType {
|
||||
|
||||
@@ -9,7 +9,7 @@ ROOT_IMAGE ?= alpine:3.18.0
|
||||
# TODO: sync it with ROOT_IMAGE when it will be fixed in the new alpine releases
|
||||
CERTS_IMAGE := alpine:3.17.3
|
||||
|
||||
GO_BUILDER_IMAGE := golang:1.20.4-alpine
|
||||
GO_BUILDER_IMAGE := golang:1.20.5-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_COMPOSE ?= docker compose
|
||||
@@ -22,9 +22,6 @@ package-base:
|
||||
--tag $(BASE_IMAGE) \
|
||||
deployment/docker/base
|
||||
|
||||
docker-scan: package-base
|
||||
docker scan --severity=medium --accept-license $(BASE_IMAGE) || (echo "❌ The build has been terminated because critical vulnerabilities were found in $(BASE_IMAGE)"; exit 1)
|
||||
|
||||
package-builder:
|
||||
(docker image ls --format '{{.Repository}}:{{.Tag}}' | grep -q '$(BUILDER_IMAGE)$$') \
|
||||
|| docker build \
|
||||
|
||||
@@ -24,8 +24,46 @@ The following tip changes can be tested by building VictoriaMetrics components f
|
||||
|
||||
## tip
|
||||
|
||||
* BUGFIX: [vmalert](https://docs.victoriametrics.com/vmalert.html): fix nil map assignment panic in runtime introduced in this [change](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/4341).
|
||||
* FEATURE: [vmagent](https://docs.victoriametrics.com/vmagent.html): Adds `enable_http2` on scrape configuration level. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/4283). Thanks to @Haleygo for [the pull request](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/4295).
|
||||
* FEATURE: [vmctl](https://docs.victoriametrics.com/vmctl.html): add verbose output for docker installations or when TTY isn't available. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/4081).
|
||||
* FEATURE: [vmctl](https://docs.victoriametrics.com/vmctl.html): interrupt backoff retries when import process is cancelled. The change makes vmctl more responsive in case of errors during the import. See [this pull request](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/4442).
|
||||
* FEATURE: [vmctl](https://docs.victoriametrics.com/vmctl.html): update backoff policy on retries to reduce probability of overloading for `source` or `destination` databases. See [this pull request](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/4442).
|
||||
* FEATURE: vmstorage: suppress "broken pipe" and "connection reset by peer" errors for search queries on vmstorage side. See [this](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/4418/commits/a6a7795b9e1f210d614a2c5f9a3016b97ded4792) and [this](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/4498/commits/830dac177f0f09032165c248943a5da0e10dfe90) commits.
|
||||
* FEATURE: [Official Grafana dashboards for VictoriaMetrics](https://grafana.com/orgs/victoriametrics): add panel for tracking rate of syscalls while writing or reading from disk via `process_io_(read|write)_syscalls_total` metrics.
|
||||
* FEATURE: accept timestamps in milliseconds at `start`, `end` and `time` query args in [Prometheus querying API](https://docs.victoriametrics.com/#prometheus-querying-api-usage). See [these docs](https://docs.victoriametrics.com/#timestamp-formats) and [this feature request](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/4459).
|
||||
* FEATURE: [vmalert](https://docs.victoriametrics.com/vmalert.html): update retry policy for pushing data to `-remoteWrite.url`. By default, vmalert will make multiple retry attempts with exponential delay. The total time spent during retry attempts shouldn't exceed `-remoteWrite.retryMaxTime` (default is 30s). When retry time is exceeded vmalert drops the data dedicated for `-remoteWrite.url`. Before, vmalert dropped data after 5 retry attempts with 1s delay between attempts (not configurable). See `-remoteWrite.retryMinInterval` and `-remoteWrite.retryMaxTime` cmd-line flags.
|
||||
* FEATURE: [vmalert](https://docs.victoriametrics.com/vmalert.html): expose `vmalert_remotewrite_send_duration_seconds_total` counter, which can be used for determining high saturation of every connection to remote storage with an alerting query `sum(rate(vmalert_remotewrite_send_duration_seconds_total[5m])) by(job, instance) > 0.9 * max(vmalert_remotewrite_concurrency) by(job, instance)`. This query triggers when a connection is saturated by more than 90%. This usually means that `-remoteWrite.concurrency` command-line flag must be increased in order to increase the number of concurrent writings into remote endpoint. See [this feature request](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/4516).
|
||||
* FEATURE: [vmauth](https://docs.victoriametrics.com/vmauth.html): expose `vmauth_user_request_duration_seconds` and `vmauth_unauthorized_user_request_duration_seconds` summary metrics for measuring requests latency per user.
|
||||
* FEATURE: [vmbackup](https://docs.victoriametrics.com/vmbackup.html): show backup progress percentage in log during backup uploading. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/4460).
|
||||
* FEATURE: [vmrestore](https://docs.victoriametrics.com/vmrestore.html): show restoring progress percentage in log during backup downloading. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/4460).
|
||||
|
||||
* BUGFIX: add the following command-line flags, which can be used for limiting Graphite API calls:
|
||||
`--search.maxGraphiteTagKeys` for limiting the number of tag keys returned from Graphite `/tags`, `/tags/autoComplete/*`, `/tags/findSeries` API.
|
||||
`--search.maxGraphiteTagValues` for limiting the number of tag values returned Graphite `/tags/<tag_name>` API.
|
||||
Remove redundant limit from [Prometheus api/v1/series](https://docs.victoriametrics.com/Single-server-VictoriaMetrics.html#prometheus-querying-api-usage). See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/4339).
|
||||
|
||||
|
||||
|
||||
## [v1.91.3](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.91.3)
|
||||
|
||||
Released at 2023-06-30
|
||||
|
||||
* SECURITY: upgrade Go builder from Go1.20.4 to Go1.20.5. See [the list of issues addressed in Go1.20.5](https://github.com/golang/go/issues?q=milestone%3AGo1.20.5+label%3ACherryPickApproved).
|
||||
|
||||
* BUGFIX: [vmagent](https://docs.victoriametrics.com/vmagent.html): fix panic on vmagent shutdown which could lead to loosing aggregation results which were not flushed to remote yet. See [this](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/4407) for details.
|
||||
* BUGFIX: [vmagent](https://docs.victoriametrics.com/vmagent.html): fixed service name detection for [consulagent service discovery](https://docs.victoriametrics.com/sd_configs.html?highlight=consulagent#consulagent_sd_configs) in case of a difference in service name and service id. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/4390) for details.
|
||||
* BUGFIX: [vmalert](https://docs.victoriametrics.com/vmalert.html): retry all errors except 4XX status codes while pushing via remote-write to the remote storage. Previously, errors like broken connection could prevent vmalert from retrying the request.
|
||||
* BUGFIX: [vmalert](https://docs.victoriametrics.com/vmalert.html): properly interrupt retry attempts on vmalert shutdown. Before, vmalert could have waited for all retries to finish for shutdown.
|
||||
* BUGFIX: [vmbackupmanager](https://docs.victoriametrics.com/vmbackupmanager.html): fix an issue with `vmbackupmanager` not being able to restore data from a backup stored in GCS. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/4420) for details.
|
||||
* BUGFIX: [storage](https://docs.victoriametrics.com/Single-server-VictoriaMetrics.html): Properly creates `parts.json` after migration from versions below `v1.90.0. It must fix errors on start-up after unclean shutdown. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/4336) for details.
|
||||
* BUGFIX: [vmui](https://docs.victoriametrics.com/#vmui): fix a memory leak issue associated with chart updates. See [this pull request](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/4455).
|
||||
|
||||
|
||||
## [v1.91.2](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.91.2)
|
||||
|
||||
Released at 2023-06-02
|
||||
|
||||
* BUGFIX: [vmalert](https://docs.victoriametrics.com/vmalert.html): fix nil map assignment panic in runtime introduced in this [change](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/4341).
|
||||
|
||||
## [v1.91.1](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.91.1)
|
||||
|
||||
|
||||
@@ -1158,6 +1158,10 @@ The shortlist of configuration flags is the following:
|
||||
Optional OAuth2 scopes to use for -notifier.url. Scopes must be delimited by ';'.
|
||||
-remoteWrite.oauth2.tokenUrl string
|
||||
Optional OAuth2 tokenURL to use for -notifier.url.
|
||||
-remoteWrite.retryMaxTime duration
|
||||
The max time spent on retry attempts for the failed remote-write request. Change this value if it is expected for remoteWrite.url to be unreachable for more than -remoteWrite.retryMaxTime. See also -remoteWrite.retryMinInterval (default 30s)
|
||||
-remoteWrite.retryMinInterval duration
|
||||
The minimum delay between retry attempts. Every next retry attempt will double the delay to prevent hammering of remote database. See also -remoteWrite.retryMaxInterval (default 1s)
|
||||
-remoteWrite.sendTimeout duration
|
||||
Timeout for sending data to the configured -remoteWrite.url. (default 30s)
|
||||
-remoteWrite.showURL
|
||||
|
||||
@@ -205,7 +205,8 @@ func runBackup(src *fslocal.FS, dst common.RemoteFS, origin common.OriginFS, con
|
||||
return nil
|
||||
}, func(elapsed time.Duration) {
|
||||
n := atomic.LoadUint64(&bytesUploaded)
|
||||
logger.Infof("uploaded %d out of %d bytes from src %s to dst %s in %s", n, uploadSize, src, dst, elapsed)
|
||||
prc := 100 * float64(n) / float64(uploadSize)
|
||||
logger.Infof("uploaded %d out of %d bytes (%.2f%%) from src %s to dst %s in %s", n, uploadSize, prc, src, dst, elapsed)
|
||||
})
|
||||
atomic.AddUint64(&bytesUploadedTotal, bytesUploaded)
|
||||
bytesUploadedTotalMetric.Set(bytesUploadedTotal)
|
||||
|
||||
@@ -180,7 +180,8 @@ func (r *Restore) Run() error {
|
||||
return nil
|
||||
}, func(elapsed time.Duration) {
|
||||
n := atomic.LoadUint64(&bytesDownloaded)
|
||||
logger.Infof("downloaded %d out of %d bytes from %s to %s in %s", n, downloadSize, src, dst, elapsed)
|
||||
prc := 100 * float64(n) / float64(downloadSize)
|
||||
logger.Infof("downloaded %d out of %d bytes (%.2f%%) from %s to %s in %s", n, downloadSize, prc, src, dst, elapsed)
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -1383,6 +1383,11 @@ func mustOpenParts(path string) []*partWrapper {
|
||||
}
|
||||
pws = append(pws, pw)
|
||||
}
|
||||
partNamesPath := filepath.Join(path, partsFilename)
|
||||
if !fs.IsPathExist(partNamesPath) {
|
||||
// create parts.json file on migration from previous versions before v1.90.0
|
||||
mustWritePartNames(pws, path)
|
||||
}
|
||||
|
||||
return pws
|
||||
}
|
||||
|
||||
@@ -206,7 +206,8 @@ func (cw *consulAgentWatcher) getServiceNames() ([]string, error) {
|
||||
return nil, fmt.Errorf("cannot parse response from %q: %w; data=%q", path, err, data)
|
||||
}
|
||||
serviceNames := make([]string, 0, len(m))
|
||||
for serviceName, service := range m {
|
||||
for _, service := range m {
|
||||
serviceName := service.Service
|
||||
if service.Datacenter != cw.watchDatacenter {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package discoveryutils
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
@@ -293,7 +294,7 @@ func doRequestWithPossibleRetry(hc *HTTPClient, req *http.Request) (*http.Respon
|
||||
if statusCode != http.StatusTooManyRequests {
|
||||
return true
|
||||
}
|
||||
} else if reqErr != net.ErrClosed && !strings.Contains(reqErr.Error(), "broken pipe") {
|
||||
} else if !errors.Is(reqErr, net.ErrClosed) && !strings.Contains(reqErr.Error(), "broken pipe") {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
|
||||
@@ -265,6 +265,12 @@ func mustOpenPartition(smallPartsPath, bigPartsPath string, s *Storage) *partiti
|
||||
smallParts := mustOpenParts(smallPartsPath, partNamesSmall)
|
||||
bigParts := mustOpenParts(bigPartsPath, partNamesBig)
|
||||
|
||||
partNamesPath := filepath.Join(smallPartsPath, partsFilename)
|
||||
if !fs.IsPathExist(partNamesPath) {
|
||||
// create parts.json file on migration from previous versions before v1.90.0
|
||||
mustWritePartNames(smallParts, bigParts, smallPartsPath)
|
||||
}
|
||||
|
||||
pt := newPartition(name, smallPartsPath, bigPartsPath, s)
|
||||
pt.smallParts = smallParts
|
||||
pt.bigParts = bigParts
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
GO_VERSION ?=1.20.4
|
||||
GO_VERSION ?=1.20.5
|
||||
SNAP_BUILDER_IMAGE := local/snap-builder:2.0.0-$(shell echo $(GO_VERSION) | tr :/ __)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user