mirror of
https://github.com/VictoriaMetrics/VictoriaMetrics.git
synced 2026-05-19 09:46:57 +03:00
Compare commits
112 Commits
v1.3.2-vic
...
debug/erro
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
005068ea5a | ||
|
|
7941877233 | ||
|
|
f303081304 | ||
|
|
a84628f701 | ||
|
|
f823a225ac | ||
|
|
79f1a37ee6 | ||
|
|
f9cd408ca9 | ||
|
|
c2811d8d11 | ||
|
|
8d981b15c9 | ||
|
|
58f09fe3f8 | ||
|
|
afd926a0b0 | ||
|
|
204c102342 | ||
|
|
c5949af9e8 | ||
|
|
5dc0413bc0 | ||
|
|
f919783de9 | ||
|
|
60f9f44150 | ||
|
|
0fcbe8fdae | ||
|
|
458b602938 | ||
|
|
471f1d0a09 | ||
|
|
7f80c1633f | ||
|
|
186b00df6b | ||
|
|
4205ae3011 | ||
|
|
491028774a | ||
|
|
565b79c9ca | ||
|
|
5478cc61c2 | ||
|
|
79c08ecac4 | ||
|
|
f47fd83e54 | ||
|
|
9c39bac565 | ||
|
|
1042f07498 | ||
|
|
79a595c6d0 | ||
|
|
40b47601d1 | ||
|
|
6bfcbe66f7 | ||
|
|
94118c63f6 | ||
|
|
9605d73809 | ||
|
|
3237c64ef3 | ||
|
|
1fbc2c0db1 | ||
|
|
71bb9fc0d0 | ||
|
|
0210f4ebd2 | ||
|
|
891ad8f202 | ||
|
|
e501640f44 | ||
|
|
21082405ec | ||
|
|
094a5ab58f | ||
|
|
bbc84fa119 | ||
|
|
9d1a72aca8 | ||
|
|
05d3db248b | ||
|
|
59d739ff0b | ||
|
|
b54d10be63 | ||
|
|
524f0e8d8b | ||
|
|
72419834af | ||
|
|
e6b7d25ab4 | ||
|
|
ac124cf5aa | ||
|
|
3d7f8377f7 | ||
|
|
4992e083f0 | ||
|
|
71a9fb16f7 | ||
|
|
7e7d029de1 | ||
|
|
983f30c326 | ||
|
|
efd8098b0b | ||
|
|
d86788e9a2 | ||
|
|
a87ad250d0 | ||
|
|
bf84de3c6b | ||
|
|
7ec8ea8301 | ||
|
|
c6f6302ca4 | ||
|
|
87100e55cc | ||
|
|
c464d4484f | ||
|
|
91f858ee1e | ||
|
|
da0d57e4b6 | ||
|
|
fa621b384e | ||
|
|
02fedb8585 | ||
|
|
04d19a2200 | ||
|
|
e612877fe7 | ||
|
|
43181b67b1 | ||
|
|
b0ed5b6174 | ||
|
|
4aeda4b267 | ||
|
|
20a2822c23 | ||
|
|
1891b74a0a | ||
|
|
0dc576d3da | ||
|
|
88861c66fe | ||
|
|
1ee5ba8d55 | ||
|
|
e0ab3fccaf | ||
|
|
2fe6640193 | ||
|
|
d1ccf205c4 | ||
|
|
b42ed019f5 | ||
|
|
5a41c7f5a5 | ||
|
|
ec193ef691 | ||
|
|
e669c87af4 | ||
|
|
87c1b2de6f | ||
|
|
bcd8d9d6c6 | ||
|
|
dbed0de650 | ||
|
|
34a730ac65 | ||
|
|
e21bdcdbc7 | ||
|
|
9db8e071c4 | ||
|
|
1627bcc6cb | ||
|
|
5033d05d55 | ||
|
|
5279faf02f | ||
|
|
564e6ea024 | ||
|
|
6b48126603 | ||
|
|
4a2192431d | ||
|
|
86bc7d5cd1 | ||
|
|
d05fadf988 | ||
|
|
e439e40e79 | ||
|
|
d6f5ba2887 | ||
|
|
94e4c4e367 | ||
|
|
aadd8d5f3a | ||
|
|
44d8e6a19d | ||
|
|
6b0ae0b79f | ||
|
|
a51a18403c | ||
|
|
de0ae735aa | ||
|
|
acbe526307 | ||
|
|
9a6ddb48df | ||
|
|
7d3e60f7f1 | ||
|
|
f54f73033b | ||
|
|
75a2e23b7e |
@@ -14,6 +14,7 @@ import (
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmselect/promql"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmstorage"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/buildinfo"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/cgroup"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/envflag"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/flagutil"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/fs"
|
||||
@@ -39,9 +40,20 @@ var (
|
||||
"The saved data survives unclean shutdowns such as OOM crash, hardware reset, SIGKILL, etc. "+
|
||||
"Bigger intervals may help increase the lifetime of flash storage with limited write cycles (e.g. Raspberry PI). "+
|
||||
"Smaller intervals increase disk IO load. Minimum supported value is 1s")
|
||||
maxIngestionRate = flag.Int("maxIngestionRate", 0, "The maximum number of samples vmsingle can receive per second. Data ingestion is paused when the limit is exceeded. "+
|
||||
"By default there are no limits on samples ingestion rate.")
|
||||
)
|
||||
|
||||
func main() {
|
||||
// VictoriaMetrics is optimized for reduced memory allocations,
|
||||
// so it can run with the reduced GOGC in order to reduce the used memory,
|
||||
// while keeping CPU usage spent in GC at low levels.
|
||||
//
|
||||
// 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)
|
||||
|
||||
// Write flags and help message to stdout, since it is easier to grep or pipe.
|
||||
flag.CommandLine.SetOutput(os.Stdout)
|
||||
flag.Usage = usage
|
||||
@@ -76,6 +88,7 @@ func main() {
|
||||
storage.SetDataFlushInterval(*inmemoryDataFlushInterval)
|
||||
vmstorage.Init(promql.ResetRollupResultCacheIfNeeded)
|
||||
vmselect.Init()
|
||||
vminsertcommon.StartIngestionRateLimiter(*maxIngestionRate)
|
||||
vminsert.Init()
|
||||
|
||||
startSelfScraper()
|
||||
@@ -97,6 +110,7 @@ func main() {
|
||||
}
|
||||
logger.Infof("successfully shut down the webservice in %.3f seconds", time.Since(startTime).Seconds())
|
||||
vminsert.Stop()
|
||||
vminsertcommon.StopIngestionRateLimiter()
|
||||
|
||||
vmstorage.Stop()
|
||||
vmselect.Stop()
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/prompbmarshal"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/protoparser/prometheus"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/storage"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/timeserieslimits"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -68,6 +69,10 @@ func selfScraper(scrapeInterval time.Duration) {
|
||||
t := &r.Tags[j]
|
||||
labels = addLabel(labels, t.Key, t.Value)
|
||||
}
|
||||
if timeserieslimits.IsExceeding(labels) {
|
||||
// Skip metric with exceeding labels.
|
||||
continue
|
||||
}
|
||||
if len(mrs) < cap(mrs) {
|
||||
mrs = mrs[:len(mrs)+1]
|
||||
} else {
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vlinsert/insertutils"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vlstorage"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/bytesutil"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/flagutil"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/httpserver"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logstorage"
|
||||
@@ -21,6 +22,11 @@ import (
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/writeconcurrencylimiter"
|
||||
)
|
||||
|
||||
var (
|
||||
datadogStreamFields = flagutil.NewArrayString("datadog.streamFields", "Datadog tags to be used as stream fields.")
|
||||
datadogIgnoreFields = flagutil.NewArrayString("datadog.ignoreFields", "Datadog tags to ignore.")
|
||||
)
|
||||
|
||||
var parserPool fastjson.ParserPool
|
||||
|
||||
// RequestHandler processes Datadog insert requests
|
||||
@@ -79,6 +85,13 @@ func datadogLogsIngestion(w http.ResponseWriter, r *http.Request) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
if len(cp.StreamFields) == 0 {
|
||||
cp.StreamFields = *datadogStreamFields
|
||||
}
|
||||
if len(cp.IgnoreFields) == 0 {
|
||||
cp.IgnoreFields = *datadogIgnoreFields
|
||||
}
|
||||
|
||||
if err := vlstorage.CanWriteData(); err != nil {
|
||||
httpserver.Errorf(w, r, "%s", err)
|
||||
return true
|
||||
@@ -105,6 +118,70 @@ var (
|
||||
v2LogsRequestDuration = metrics.NewHistogram(`vl_http_request_duration_seconds{path="/insert/datadog/api/v2/logs"}`)
|
||||
)
|
||||
|
||||
// datadog message field has two formats:
|
||||
// - regular log message with string text
|
||||
// - nested json format for serverless plugins
|
||||
// which has folowing format:
|
||||
// {"message": {"message": "text","lamdba": {"arn": "string","requestID": "string"}, "timestamp": int64} }
|
||||
//
|
||||
// See https://github.com/DataDog/datadog-lambda-extension/blob/28b90c7e4e985b72d60b5f5a5147c69c7ac693c4/bottlecap/src/logs/lambda/mod.rs#L24
|
||||
func appendMsgFields(fields []logstorage.Field, v *fastjson.Value) ([]logstorage.Field, error) {
|
||||
switch v.Type() {
|
||||
case fastjson.TypeString:
|
||||
val := v.GetStringBytes()
|
||||
fields = append(fields, logstorage.Field{
|
||||
Name: "_msg",
|
||||
Value: bytesutil.ToUnsafeString(val),
|
||||
})
|
||||
case fastjson.TypeObject:
|
||||
var firstErr error
|
||||
v.GetObject().Visit(func(k []byte, v *fastjson.Value) {
|
||||
if firstErr != nil {
|
||||
return
|
||||
}
|
||||
switch bytesutil.ToUnsafeString(k) {
|
||||
case "message":
|
||||
val := v.GetStringBytes()
|
||||
fields = append(fields, logstorage.Field{
|
||||
Name: "_msg",
|
||||
Value: bytesutil.ToUnsafeString(val),
|
||||
})
|
||||
case "status":
|
||||
val := v.GetStringBytes()
|
||||
fields = append(fields, logstorage.Field{
|
||||
Name: "status",
|
||||
Value: bytesutil.ToUnsafeString(val),
|
||||
})
|
||||
case "lamdba":
|
||||
obj, err := v.Object()
|
||||
if err != nil {
|
||||
firstErr = err
|
||||
firstErr = fmt.Errorf("unexpected lambda value type for %q:%q; want object", k, v)
|
||||
return
|
||||
}
|
||||
obj.Visit(func(k []byte, v *fastjson.Value) {
|
||||
if firstErr != nil {
|
||||
return
|
||||
}
|
||||
val, err := v.StringBytes()
|
||||
if err != nil {
|
||||
firstErr = fmt.Errorf("unexpected lambda label value type for %q:%q; want string", k, v)
|
||||
return
|
||||
}
|
||||
fields = append(fields, logstorage.Field{
|
||||
Name: bytesutil.ToUnsafeString(k),
|
||||
Value: bytesutil.ToUnsafeString(val),
|
||||
})
|
||||
})
|
||||
|
||||
}
|
||||
})
|
||||
default:
|
||||
return fields, fmt.Errorf("unsupported message type %q", v.Type().String())
|
||||
}
|
||||
return fields, nil
|
||||
}
|
||||
|
||||
// readLogsRequest parses data according to DataDog logs format
|
||||
// https://docs.datadoghq.com/api/latest/logs/#send-logs
|
||||
func readLogsRequest(ts int64, data []byte, lmp insertutils.LogMessageProcessor) error {
|
||||
@@ -129,19 +206,27 @@ func readLogsRequest(ts int64, data []byte, lmp insertutils.LogMessageProcessor)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
val, e := v.StringBytes()
|
||||
if e != nil {
|
||||
err = fmt.Errorf("unexpected label value type for %q:%q; want string", k, v)
|
||||
return
|
||||
}
|
||||
switch string(k) {
|
||||
switch bytesutil.ToUnsafeString(k) {
|
||||
case "message":
|
||||
fields = append(fields, logstorage.Field{
|
||||
Name: "_msg",
|
||||
Value: bytesutil.ToUnsafeString(val),
|
||||
})
|
||||
fields, err = appendMsgFields(fields, v)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
case "timestamp":
|
||||
val, e := v.Int64()
|
||||
if e != nil {
|
||||
err = fmt.Errorf("failed to parse timestamp for %q:%q", k, v)
|
||||
}
|
||||
if val > 0 {
|
||||
ts = val * 1e6
|
||||
}
|
||||
case "ddtags":
|
||||
// https://docs.datadoghq.com/getting_started/tagging/
|
||||
val, e := v.StringBytes()
|
||||
if e != nil {
|
||||
err = fmt.Errorf("unexpected label value type for %q:%q; want string", k, v)
|
||||
return
|
||||
}
|
||||
var pair []byte
|
||||
idx := 0
|
||||
for idx >= 0 {
|
||||
@@ -168,12 +253,20 @@ func readLogsRequest(ts int64, data []byte, lmp insertutils.LogMessageProcessor)
|
||||
}
|
||||
}
|
||||
default:
|
||||
val, e := v.StringBytes()
|
||||
if e != nil {
|
||||
err = fmt.Errorf("unexpected label value type for %q:%q; want string", k, v)
|
||||
return
|
||||
}
|
||||
fields = append(fields, logstorage.Field{
|
||||
Name: bytesutil.ToUnsafeString(k),
|
||||
Value: bytesutil.ToUnsafeString(val),
|
||||
})
|
||||
}
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
lmp.AddRow(ts, fields, nil)
|
||||
fields = fields[:0]
|
||||
}
|
||||
|
||||
@@ -54,6 +54,12 @@ func TestReadLogsRequestSuccess(t *testing.T) {
|
||||
"hostname":"127.0.0.1",
|
||||
"message":"bar",
|
||||
"service":"test"
|
||||
}, {
|
||||
"ddsource":"nginx",
|
||||
"ddtags":"tag1:value1,tag2:value2",
|
||||
"hostname":"127.0.0.1",
|
||||
"message":{"message": "nested"},
|
||||
"service":"test"
|
||||
}, {
|
||||
"ddsource":"nginx",
|
||||
"ddtags":"tag1:value1,tag2:value2",
|
||||
@@ -86,8 +92,9 @@ func TestReadLogsRequestSuccess(t *testing.T) {
|
||||
"service":"test"
|
||||
}
|
||||
]`
|
||||
rowsExpected := 6
|
||||
rowsExpected := 7
|
||||
resultExpected := `{"ddsource":"nginx","tag1":"value1","tag2":"value2","hostname":"127.0.0.1","_msg":"bar","service":"test"}
|
||||
{"ddsource":"nginx","tag1":"value1","tag2":"value2","hostname":"127.0.0.1","_msg":"nested","service":"test"}
|
||||
{"ddsource":"nginx","tag1":"value1","tag2":"value2","hostname":"127.0.0.1","_msg":"foobar","service":"test"}
|
||||
{"ddsource":"nginx","tag1":"value1","tag2":"value2","hostname":"127.0.0.1","_msg":"baz","service":"test"}
|
||||
{"ddsource":"nginx","tag1":"value1","tag2":"value2","hostname":"127.0.0.1","_msg":"xyz","service":"test"}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package vlinsert
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
@@ -34,9 +35,15 @@ func RequestHandler(w http.ResponseWriter, r *http.Request) bool {
|
||||
path = strings.TrimPrefix(path, "/insert")
|
||||
path = strings.ReplaceAll(path, "//", "/")
|
||||
|
||||
if path == "/jsonline" {
|
||||
switch path {
|
||||
case "/jsonline":
|
||||
jsonline.RequestHandler(w, r)
|
||||
return true
|
||||
case "/ready":
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(200)
|
||||
fmt.Fprintf(w, `{"status":"ok"}`)
|
||||
return true
|
||||
}
|
||||
switch {
|
||||
case strings.HasPrefix(path, "/elasticsearch/"):
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"fmt"
|
||||
"math"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"slices"
|
||||
"sort"
|
||||
"strconv"
|
||||
@@ -13,6 +14,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/VictoriaMetrics/metrics"
|
||||
"github.com/valyala/fastjson"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vlstorage"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/bytesutil"
|
||||
@@ -48,9 +50,10 @@ func ProcessFacetsRequest(ctx context.Context, w http.ResponseWriter, r *http.Re
|
||||
httpserver.Errorf(w, r, "%s", err)
|
||||
return
|
||||
}
|
||||
keepConstFields := httputils.GetBool(r, "keep_const_fields")
|
||||
|
||||
q.DropAllPipes()
|
||||
q.AddFacetsPipe(limit, maxValuesPerField, maxValueLen)
|
||||
q.AddFacetsPipe(limit, maxValuesPerField, maxValueLen, keepConstFields)
|
||||
|
||||
var mLock sync.Mutex
|
||||
m := make(map[string][]facetEntry)
|
||||
@@ -1092,18 +1095,20 @@ func parseCommonArgs(r *http.Request) (*logstorage.Query, []logstorage.TenantID,
|
||||
}
|
||||
|
||||
// Parse optional extra_filters
|
||||
extraFilters, err := getExtraFilters(r, "extra_filters")
|
||||
extraFiltersStr := r.FormValue("extra_filters")
|
||||
extraFilters, err := parseExtraFilters(extraFiltersStr)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
q.AddExtraFilters(extraFilters)
|
||||
|
||||
// Parse optional extra_stream_filters
|
||||
extraStreamFilters, err := getExtraFilters(r, "extra_stream_filters")
|
||||
extraStreamFiltersStr := r.FormValue("extra_stream_filters")
|
||||
extraStreamFilters, err := parseExtraStreamFilters(extraStreamFiltersStr)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
q.AddExtraStreamFilters(extraStreamFilters)
|
||||
q.AddExtraFilters(extraStreamFilters)
|
||||
|
||||
return q, tenantIDs, nil
|
||||
}
|
||||
@@ -1121,15 +1126,114 @@ func getTimeNsec(r *http.Request, argName string) (int64, bool, error) {
|
||||
return nsecs, true, nil
|
||||
}
|
||||
|
||||
func getExtraFilters(r *http.Request, argName string) ([]logstorage.Field, error) {
|
||||
s := r.FormValue(argName)
|
||||
func parseExtraFilters(s string) (*logstorage.Filter, error) {
|
||||
if s == "" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var p logstorage.JSONParser
|
||||
if err := p.ParseLogMessage([]byte(s)); err != nil {
|
||||
return nil, fmt.Errorf("cannot parse %s: %w", argName, err)
|
||||
if !strings.HasPrefix(s, `{"`) {
|
||||
return logstorage.ParseFilter(s)
|
||||
}
|
||||
return p.Fields, nil
|
||||
|
||||
// Extra filters in the form {"field":"value",...}.
|
||||
kvs, err := parseExtraFiltersJSON(s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
filters := make([]string, len(kvs))
|
||||
for i, kv := range kvs {
|
||||
if len(kv.values) == 1 {
|
||||
filters[i] = fmt.Sprintf("%q:=%q", kv.key, kv.values[0])
|
||||
} else {
|
||||
orValues := make([]string, len(kv.values))
|
||||
for j, v := range kv.values {
|
||||
orValues[j] = fmt.Sprintf("%q", v)
|
||||
}
|
||||
filters[i] = fmt.Sprintf("%q:in(%s)", kv.key, strings.Join(orValues, ","))
|
||||
}
|
||||
}
|
||||
s = strings.Join(filters, " ")
|
||||
return logstorage.ParseFilter(s)
|
||||
}
|
||||
|
||||
func parseExtraStreamFilters(s string) (*logstorage.Filter, error) {
|
||||
if s == "" {
|
||||
return nil, nil
|
||||
}
|
||||
if !strings.HasPrefix(s, `{"`) {
|
||||
return logstorage.ParseFilter(s)
|
||||
}
|
||||
|
||||
// Extra stream filters in the form {"field":"value",...}.
|
||||
kvs, err := parseExtraFiltersJSON(s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
filters := make([]string, len(kvs))
|
||||
for i, kv := range kvs {
|
||||
if len(kv.values) == 1 {
|
||||
filters[i] = fmt.Sprintf("%q=%q", kv.key, kv.values[0])
|
||||
} else {
|
||||
orValues := make([]string, len(kv.values))
|
||||
for j, v := range kv.values {
|
||||
orValues[j] = regexp.QuoteMeta(v)
|
||||
}
|
||||
filters[i] = fmt.Sprintf("%q=~%q", kv.key, strings.Join(orValues, "|"))
|
||||
}
|
||||
}
|
||||
s = "{" + strings.Join(filters, ",") + "}"
|
||||
return logstorage.ParseFilter(s)
|
||||
}
|
||||
|
||||
type extraFilter struct {
|
||||
key string
|
||||
values []string
|
||||
}
|
||||
|
||||
func parseExtraFiltersJSON(s string) ([]extraFilter, error) {
|
||||
v, err := fastjson.Parse(s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
o := v.GetObject()
|
||||
|
||||
var errOuter error
|
||||
var filters []extraFilter
|
||||
o.Visit(func(k []byte, v *fastjson.Value) {
|
||||
if errOuter != nil {
|
||||
return
|
||||
}
|
||||
switch v.Type() {
|
||||
case fastjson.TypeString:
|
||||
filters = append(filters, extraFilter{
|
||||
key: string(k),
|
||||
values: []string{string(v.GetStringBytes())},
|
||||
})
|
||||
case fastjson.TypeArray:
|
||||
a := v.GetArray()
|
||||
if len(a) == 0 {
|
||||
return
|
||||
}
|
||||
orValues := make([]string, len(a))
|
||||
for i, av := range a {
|
||||
ov, err := av.StringBytes()
|
||||
if err != nil {
|
||||
errOuter = fmt.Errorf("cannot obtain string item at the array for key %q; item: %s", k, av)
|
||||
return
|
||||
}
|
||||
orValues[i] = string(ov)
|
||||
}
|
||||
filters = append(filters, extraFilter{
|
||||
key: string(k),
|
||||
values: orValues,
|
||||
})
|
||||
default:
|
||||
errOuter = fmt.Errorf("unexpected type of value for key %q: %s; value: %s", k, v.Type(), v)
|
||||
}
|
||||
})
|
||||
if errOuter != nil {
|
||||
return nil, errOuter
|
||||
}
|
||||
return filters, nil
|
||||
}
|
||||
|
||||
103
app/vlselect/logsql/logsql_test.go
Normal file
103
app/vlselect/logsql/logsql_test.go
Normal file
@@ -0,0 +1,103 @@
|
||||
package logsql
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParseExtraFilters_Success(t *testing.T) {
|
||||
f := func(s, resultExpected string) {
|
||||
t.Helper()
|
||||
|
||||
f, err := parseExtraFilters(s)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error in parseExtraFilters: %s", err)
|
||||
}
|
||||
result := f.String()
|
||||
if result != resultExpected {
|
||||
t.Fatalf("unexpected result\ngot\n%s\nwant\n%s", result, resultExpected)
|
||||
}
|
||||
}
|
||||
|
||||
f("", "")
|
||||
|
||||
// JSON string
|
||||
f(`{"foo":"bar"}`, `foo:=bar`)
|
||||
f(`{"foo":["bar","baz"]}`, `foo:in(bar,baz)`)
|
||||
f(`{"z":"=b ","c":["d","e,"],"a":[],"_msg":"x"}`, `z:="=b " c:in(d,"e,") =x`)
|
||||
|
||||
// LogsQL filter
|
||||
f(`foobar`, `foobar`)
|
||||
f(`foo:bar`, `foo:bar`)
|
||||
f(`foo:(bar or baz) error _time:5m {"foo"=bar,baz="z"}`, `(foo:bar or foo:baz) error _time:5m {foo="bar",baz="z"}`)
|
||||
}
|
||||
|
||||
func TestParseExtraFilters_Failure(t *testing.T) {
|
||||
f := func(s string) {
|
||||
t.Helper()
|
||||
|
||||
_, err := parseExtraFilters(s)
|
||||
if err == nil {
|
||||
t.Fatalf("expecting non-nil error")
|
||||
}
|
||||
}
|
||||
|
||||
// Invalid JSON
|
||||
f(`{"foo"}`)
|
||||
f(`[1,2]`)
|
||||
f(`{"foo":[1]}`)
|
||||
|
||||
// Invliad LogsQL filter
|
||||
f(`foo:(bar`)
|
||||
|
||||
// excess pipe
|
||||
f(`foo | count()`)
|
||||
}
|
||||
|
||||
func TestParseExtraStreamFilters_Success(t *testing.T) {
|
||||
f := func(s, resultExpected string) {
|
||||
t.Helper()
|
||||
|
||||
f, err := parseExtraStreamFilters(s)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error in parseExtraStreamFilters: %s", err)
|
||||
}
|
||||
result := f.String()
|
||||
if result != resultExpected {
|
||||
t.Fatalf("unexpected result;\ngot\n%s\nwant\n%s", result, resultExpected)
|
||||
}
|
||||
}
|
||||
|
||||
f("", "")
|
||||
|
||||
// JSON string
|
||||
f(`{"foo":"bar"}`, `{foo="bar"}`)
|
||||
f(`{"foo":["bar","baz"]}`, `{foo=~"bar|baz"}`)
|
||||
f(`{"z":"b","c":["d","e|\""],"a":[],"_msg":"x"}`, `{z="b",c=~"d|e\\|\"",_msg="x"}`)
|
||||
|
||||
// LogsQL filter
|
||||
f(`foobar`, `foobar`)
|
||||
f(`foo:bar`, `foo:bar`)
|
||||
f(`foo:(bar or baz) error _time:5m {"foo"=bar,baz="z"}`, `(foo:bar or foo:baz) error _time:5m {foo="bar",baz="z"}`)
|
||||
}
|
||||
|
||||
func TestParseExtraStreamFilters_Failure(t *testing.T) {
|
||||
f := func(s string) {
|
||||
t.Helper()
|
||||
|
||||
_, err := parseExtraStreamFilters(s)
|
||||
if err == nil {
|
||||
t.Fatalf("expecting non-nil error")
|
||||
}
|
||||
}
|
||||
|
||||
// Invalid JSON
|
||||
f(`{"foo"}`)
|
||||
f(`[1,2]`)
|
||||
f(`{"foo":[1]}`)
|
||||
|
||||
// Invliad LogsQL filter
|
||||
f(`foo:(bar`)
|
||||
|
||||
// excess pipe
|
||||
f(`foo | count()`)
|
||||
}
|
||||
@@ -1,13 +1,13 @@
|
||||
{
|
||||
"files": {
|
||||
"main.css": "./static/css/main.d05122da.css",
|
||||
"main.js": "./static/js/main.6082e5a5.js",
|
||||
"main.css": "./static/css/main.fa83344e.css",
|
||||
"main.js": "./static/js/main.8ad2bc1f.js",
|
||||
"static/js/685.f772060c.chunk.js": "./static/js/685.f772060c.chunk.js",
|
||||
"static/media/MetricsQL.md": "./static/media/MetricsQL.a00044c91d9781cf8557.md",
|
||||
"index.html": "./index.html"
|
||||
},
|
||||
"entrypoints": [
|
||||
"static/css/main.d05122da.css",
|
||||
"static/js/main.6082e5a5.js"
|
||||
"static/css/main.fa83344e.css",
|
||||
"static/js/main.8ad2bc1f.js"
|
||||
]
|
||||
}
|
||||
@@ -1 +1 @@
|
||||
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="./favicon.svg"/><link rel="apple-touch-icon" href="./favicon.svg"/><link rel="mask-icon" href="./favicon.svg" color="#000000"><meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=5"/><meta name="theme-color" content="#000000"/><meta name="description" content="Explore your log data with VictoriaLogs UI"/><link rel="manifest" href="./manifest.json"/><title>UI for VictoriaLogs</title><meta name="twitter:card" content="summary"><meta name="twitter:title" content="UI for VictoriaLogs"><meta name="twitter:site" content="@https://victoriametrics.com/products/victorialogs/"><meta name="twitter:description" content="Explore your log data with VictoriaLogs UI"><meta name="twitter:image" content="./preview.jpg"><meta property="og:type" content="website"><meta property="og:title" content="UI for VictoriaLogs"><meta property="og:url" content="https://victoriametrics.com/products/victorialogs/"><meta property="og:description" content="Explore your log data with VictoriaLogs UI"><script defer="defer" src="./static/js/main.6082e5a5.js"></script><link href="./static/css/main.d05122da.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>
|
||||
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="./favicon.svg"/><link rel="apple-touch-icon" href="./favicon.svg"/><link rel="mask-icon" href="./favicon.svg" color="#000000"><meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=5"/><meta name="theme-color" content="#000000"/><meta name="description" content="Explore your log data with VictoriaLogs UI"/><link rel="manifest" href="./manifest.json"/><title>UI for VictoriaLogs</title><meta name="twitter:card" content="summary"><meta name="twitter:title" content="UI for VictoriaLogs"><meta name="twitter:site" content="@https://victoriametrics.com/products/victorialogs/"><meta name="twitter:description" content="Explore your log data with VictoriaLogs UI"><meta name="twitter:image" content="./preview.jpg"><meta property="og:type" content="website"><meta property="og:title" content="UI for VictoriaLogs"><meta property="og:url" content="https://victoriametrics.com/products/victorialogs/"><meta property="og:description" content="Explore your log data with VictoriaLogs UI"><script defer="defer" src="./static/js/main.8ad2bc1f.js"></script><link href="./static/css/main.fa83344e.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>
|
||||
File diff suppressed because one or more lines are too long
1
app/vlselect/vmui/static/css/main.fa83344e.css
Normal file
1
app/vlselect/vmui/static/css/main.fa83344e.css
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
2
app/vlselect/vmui/static/js/main.8ad2bc1f.js
Normal file
2
app/vlselect/vmui/static/js/main.8ad2bc1f.js
Normal file
File diff suppressed because one or more lines are too long
@@ -30,6 +30,7 @@ import (
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/auth"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/buildinfo"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/bytesutil"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/cgroup"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/envflag"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/flagutil"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/httpserver"
|
||||
@@ -45,6 +46,7 @@ import (
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/protoparser/opentelemetry/firehose"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/pushmetrics"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/stringsutil"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/timeserieslimits"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -77,6 +79,9 @@ var (
|
||||
dryRun = flag.Bool("dryRun", false, "Whether to check config files without running vmagent. The following files are checked: "+
|
||||
"-promscrape.config, -remoteWrite.relabelConfig, -remoteWrite.urlRelabelConfig, -remoteWrite.streamAggr.config . "+
|
||||
"Unknown config entries aren't allowed in -promscrape.config by default. This can be changed by passing -promscrape.config.strictParse=false command-line flag")
|
||||
maxLabelsPerTimeseries = flag.Int("maxLabelsPerTimeseries", 0, "The maximum number of labels per time series to be accepted. Series with superfluous labels are ignored. In this case the vm_rows_ignored_total{reason=\"too_many_labels\"} metric at /metrics page is incremented")
|
||||
maxLabelNameLen = flag.Int("maxLabelNameLen", 0, "The maximum length of label names in the accepted time series. Series with longer label name are ignored. In this case the vm_rows_ignored_total{reason=\"too_long_label_name\"} metric at /metrics page is incremented")
|
||||
maxLabelValueLen = flag.Int("maxLabelValueLen", 0, "The maximum length of label values in the accepted time series. Series with longer label value are ignored. In this case the vm_rows_ignored_total{reason=\"too_long_label_value\"} metric at /metrics page is incremented")
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -93,6 +98,15 @@ var (
|
||||
)
|
||||
|
||||
func main() {
|
||||
// vmagent is optimized for reduced memory allocations,
|
||||
// so it can run with the reduced GOGC in order to reduce the used memory,
|
||||
// while keeping CPU usage spent in GC at low levels.
|
||||
//
|
||||
// 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)
|
||||
|
||||
// Write flags and help message to stdout, since it is easier to grep or pipe.
|
||||
flag.CommandLine.SetOutput(os.Stdout)
|
||||
flag.Usage = usage
|
||||
@@ -100,6 +114,7 @@ func main() {
|
||||
remotewrite.InitSecretFlags()
|
||||
buildinfo.Init()
|
||||
logger.Init()
|
||||
timeserieslimits.Init(*maxLabelsPerTimeseries, *maxLabelNameLen, *maxLabelValueLen)
|
||||
|
||||
if promscrape.IsDryRun() {
|
||||
if err := promscrape.CheckConfig(); err != nil {
|
||||
|
||||
@@ -7,13 +7,10 @@ import (
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strconv"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/httpserver"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/auth"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/bloomfilter"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/bytesutil"
|
||||
@@ -21,6 +18,7 @@ import (
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/fasttime"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/flagutil"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/fs"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/httpserver"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/memory"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/persistentqueue"
|
||||
@@ -30,6 +28,7 @@ import (
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promutils"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/ratelimiter"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/streamaggr"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/timeserieslimits"
|
||||
"github.com/VictoriaMetrics/metrics"
|
||||
"github.com/cespare/xxhash/v2"
|
||||
)
|
||||
@@ -472,6 +471,15 @@ func tryPush(at *auth.Token, wr *prompbmarshal.WriteRequest, forceDropSamplesOnF
|
||||
rowsCountAfterRelabel := getRowsCount(tssBlock)
|
||||
rowsDroppedByGlobalRelabel.Add(rowsCountBeforeRelabel - rowsCountAfterRelabel)
|
||||
}
|
||||
if timeserieslimits.Enabled() {
|
||||
tmpBlock := tssBlock[:0]
|
||||
for _, ts := range tssBlock {
|
||||
if !timeserieslimits.IsExceeding(ts.Labels) {
|
||||
tmpBlock = append(tmpBlock, ts)
|
||||
}
|
||||
}
|
||||
tssBlock = tmpBlock
|
||||
}
|
||||
sortLabelsIfNeeded(tssBlock)
|
||||
tssBlock = limitSeriesCardinality(tssBlock)
|
||||
if sas.IsEnabled() {
|
||||
@@ -716,29 +724,14 @@ func logSkippedSeries(labels []prompbmarshal.Label, flagName string, flagValue i
|
||||
select {
|
||||
case <-logSkippedSeriesTicker.C:
|
||||
// Do not use logger.WithThrottler() here, since this will increase CPU usage
|
||||
// because every call to logSkippedSeries will result to a call to labelsToString.
|
||||
logger.Warnf("skip series %s because %s=%d reached", labelsToString(labels), flagName, flagValue)
|
||||
// because every call to logSkippedSeries will result to a call to prompbmarshal.LabelsToString.
|
||||
logger.Warnf("skip series %s because %s=%d reached", prompbmarshal.LabelsToString(labels), flagName, flagValue)
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
var logSkippedSeriesTicker = time.NewTicker(5 * time.Second)
|
||||
|
||||
func labelsToString(labels []prompbmarshal.Label) string {
|
||||
var b []byte
|
||||
b = append(b, '{')
|
||||
for i, label := range labels {
|
||||
b = append(b, label.Name...)
|
||||
b = append(b, '=')
|
||||
b = strconv.AppendQuote(b, label.Value)
|
||||
if i+1 < len(labels) {
|
||||
b = append(b, ',')
|
||||
}
|
||||
}
|
||||
b = append(b, '}')
|
||||
return string(b)
|
||||
}
|
||||
|
||||
var (
|
||||
globalRowsPushedBeforeRelabel = metrics.NewCounter("vmagent_remotewrite_global_rows_pushed_before_relabel_total")
|
||||
rowsDroppedByGlobalRelabel = metrics.NewCounter("vmagent_remotewrite_global_relabel_metrics_dropped_total")
|
||||
|
||||
@@ -51,9 +51,14 @@ Examples:
|
||||
Usage: `Optional external URL to template in rule's labels or annotations.`,
|
||||
Required: false,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "loggerLevel",
|
||||
Usage: `Minimum level of errors to log. Possible values: INFO, WARN, ERROR, FATAL, PANIC (default "ERROR").`,
|
||||
Required: false,
|
||||
},
|
||||
},
|
||||
Action: func(c *cli.Context) error {
|
||||
if failed := unittest.UnitTest(c.StringSlice("files"), c.Bool("disableAlertgroupLabel"), c.StringSlice("external.label"), c.String("external.url")); failed {
|
||||
if failed := unittest.UnitTest(c.StringSlice("files"), c.Bool("disableAlertgroupLabel"), c.StringSlice("external.label"), c.String("external.url"), c.String("loggerLevel")); failed {
|
||||
return fmt.Errorf("unittest failed")
|
||||
}
|
||||
return nil
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
@@ -46,17 +47,24 @@ var (
|
||||
testRemoteWritePath = "http://127.0.0.1" + httpListenAddr
|
||||
testHealthHTTPPath = "http://127.0.0.1" + httpListenAddr + "/health"
|
||||
|
||||
testLogLevel = "ERROR"
|
||||
disableAlertgroupLabel bool
|
||||
)
|
||||
|
||||
const (
|
||||
testStoragePath = "vmalert-unittest"
|
||||
testLogLevel = "ERROR"
|
||||
)
|
||||
|
||||
// UnitTest runs unittest for files
|
||||
func UnitTest(files []string, disableGroupLabel bool, externalLabels []string, externalURL string) bool {
|
||||
if err := templates.Load([]string{}, true); err != nil {
|
||||
func UnitTest(files []string, disableGroupLabel bool, externalLabels []string, externalURL, logLevel string) bool {
|
||||
if logLevel != "" {
|
||||
testLogLevel = logLevel
|
||||
}
|
||||
eu, err := url.Parse(externalURL)
|
||||
if err != nil {
|
||||
logger.Fatalf("failed to parse external URL: %w", err)
|
||||
}
|
||||
if err := templates.Load([]string{}, *eu); err != nil {
|
||||
logger.Fatalf("failed to load template: %v", err)
|
||||
}
|
||||
storagePath = filepath.Join(os.TempDir(), testStoragePath)
|
||||
|
||||
@@ -1,24 +1,14 @@
|
||||
package unittest
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/templates"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
if err := templates.Load([]string{}, true); err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
os.Exit(m.Run())
|
||||
}
|
||||
|
||||
func TestUnitTest_Failure(t *testing.T) {
|
||||
f := func(files []string) {
|
||||
t.Helper()
|
||||
|
||||
failed := UnitTest(files, false, nil, "")
|
||||
failed := UnitTest(files, false, nil, "", "")
|
||||
if !failed {
|
||||
t.Fatalf("expecting failed test")
|
||||
}
|
||||
@@ -33,7 +23,7 @@ func TestUnitTest_Success(t *testing.T) {
|
||||
f := func(disableGroupLabel bool, files []string, externalLabels []string, externalURL string) {
|
||||
t.Helper()
|
||||
|
||||
failed := UnitTest(files, disableGroupLabel, externalLabels, externalURL)
|
||||
failed := UnitTest(files, disableGroupLabel, externalLabels, externalURL, "")
|
||||
if failed {
|
||||
t.Fatalf("unexpected failed test")
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ import (
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
if err := templates.Load([]string{"testdata/templates/*good.tmpl"}, true); err != nil {
|
||||
if err := templates.Load([]string{"testdata/templates/*good.tmpl"}, url.URL{}); err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
os.Exit(m.Run())
|
||||
|
||||
@@ -7,7 +7,7 @@ groups:
|
||||
labels:
|
||||
label: bar
|
||||
annotations:
|
||||
summary: "{{ $value }"
|
||||
summary: "{{ }}"
|
||||
description: "{{$labels}}"
|
||||
- alert: UnkownAnnotationsFunction
|
||||
for: 5m
|
||||
|
||||
@@ -81,7 +81,10 @@ absolute path to all .tpl files in root.
|
||||
dryRun = flag.Bool("dryRun", false, "Whether to check only config files without running vmalert. The rules file are validated. The -rule flag must be specified.")
|
||||
)
|
||||
|
||||
var alertURLGeneratorFn notifier.AlertURLGenerator
|
||||
var (
|
||||
alertURLGeneratorFn notifier.AlertURLGenerator
|
||||
extURL *url.URL
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Write flags and help message to stdout, since it is easier to grep or pipe.
|
||||
@@ -95,9 +98,15 @@ func main() {
|
||||
buildinfo.Init()
|
||||
logger.Init()
|
||||
|
||||
err := templates.Load(*ruleTemplatesPath, true)
|
||||
var err error
|
||||
extURL, err = getExternalURL(*externalURL)
|
||||
if err != nil {
|
||||
logger.Fatalf("failed to parse %q: %s", *ruleTemplatesPath, err)
|
||||
logger.Fatalf("failed to init external.url %q: %s", *externalURL, err)
|
||||
}
|
||||
|
||||
err = templates.Load(*ruleTemplatesPath, *extURL)
|
||||
if err != nil {
|
||||
logger.Fatalf("failed to load template %q: %s", *ruleTemplatesPath, err)
|
||||
}
|
||||
|
||||
if *dryRun {
|
||||
@@ -111,12 +120,7 @@ func main() {
|
||||
return
|
||||
}
|
||||
|
||||
eu, err := getExternalURL(*externalURL)
|
||||
if err != nil {
|
||||
logger.Fatalf("failed to init `-external.url`: %s", err)
|
||||
}
|
||||
|
||||
alertURLGeneratorFn, err = getAlertURLGenerator(eu, *externalAlertSource, *validateTemplates)
|
||||
alertURLGeneratorFn, err = getAlertURLGenerator(extURL, *externalAlertSource, *validateTemplates)
|
||||
if err != nil {
|
||||
logger.Fatalf("failed to init `external.alert.source`: %s", err)
|
||||
}
|
||||
@@ -304,7 +308,7 @@ func getAlertURLGenerator(externalURL *url.URL, externalAlertSource string, vali
|
||||
}
|
||||
templated, err := alert.ExecTemplate(qFn, alert.Labels, m)
|
||||
if err != nil {
|
||||
logger.Errorf("can not exec source template %s", err)
|
||||
logger.Errorf("cannot template alert source: %s", err)
|
||||
}
|
||||
return fmt.Sprintf("%s/%s", externalURL, templated["tpl"])
|
||||
}, nil
|
||||
@@ -359,7 +363,7 @@ func configReload(ctx context.Context, m *manager, groupsCfg []config.Group, sig
|
||||
logger.Errorf("failed to reload notifier config: %s", err)
|
||||
continue
|
||||
}
|
||||
err := templates.Load(*ruleTemplatesPath, false)
|
||||
err := templates.Load(*ruleTemplatesPath, *extURL)
|
||||
if err != nil {
|
||||
setConfigError(err)
|
||||
logger.Errorf("failed to load new templates: %s", err)
|
||||
|
||||
@@ -74,7 +74,10 @@ func TestGetAlertURLGenerator(t *testing.T) {
|
||||
|
||||
func TestConfigReload(t *testing.T) {
|
||||
originalRulePath := *rulePath
|
||||
originalExternalURL := extURL
|
||||
extURL = &url.URL{}
|
||||
defer func() {
|
||||
extURL = originalExternalURL
|
||||
*rulePath = originalRulePath
|
||||
}()
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ package main
|
||||
import (
|
||||
"context"
|
||||
"math/rand"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -18,7 +19,7 @@ import (
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
if err := templates.Load([]string{"testdata/templates/*good.tmpl"}, true); err != nil {
|
||||
if err := templates.Load([]string{"testdata/templates/*good.tmpl"}, url.URL{}); err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
os.Exit(m.Run())
|
||||
|
||||
@@ -127,7 +127,7 @@ func ExecTemplate(q templates.QueryFn, annotations map[string]string, tplData Al
|
||||
|
||||
// ValidateTemplates validate annotations for possible template error, uses empty data for template population
|
||||
func ValidateTemplates(annotations map[string]string) error {
|
||||
tmpl, err := templates.Get()
|
||||
tmpl, err := templates.GetWithFuncs(nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -146,12 +146,21 @@ func templateAnnotations(annotations map[string]string, data AlertTplData, tmpl
|
||||
tData := tplData{data, externalLabels, externalURL}
|
||||
header := strings.Join(tplHeaders, "")
|
||||
for key, text := range annotations {
|
||||
// simple check to skip text without template
|
||||
if !strings.Contains(text, "{{") || !strings.Contains(text, "}}") {
|
||||
r[key] = text
|
||||
continue
|
||||
}
|
||||
|
||||
buf.Reset()
|
||||
builder.Reset()
|
||||
builder.Grow(len(header) + len(text))
|
||||
builder.WriteString(header)
|
||||
builder.WriteString(text)
|
||||
if err := templateAnnotation(&buf, builder.String(), tData, tmpl, execute); err != nil {
|
||||
// clone a new template for each parse to avoid collision
|
||||
ctmpl, _ := tmpl.Clone()
|
||||
ctmpl = ctmpl.Option("missingkey=zero")
|
||||
if err := templateAnnotation(&buf, builder.String(), tData, ctmpl, execute); err != nil {
|
||||
r[key] = text
|
||||
eg.Add(fmt.Errorf("key %q, template %q: %w", key, text, err))
|
||||
continue
|
||||
|
||||
@@ -75,7 +75,13 @@ func TestAlertExecTemplate(t *testing.T) {
|
||||
Labels: map[string]string{
|
||||
"instance": "localhost",
|
||||
},
|
||||
}, map[string]string{}, map[string]string{})
|
||||
}, map[string]string{
|
||||
"summary": "it's a test summary",
|
||||
"description": "it's a test description",
|
||||
}, map[string]string{
|
||||
"summary": "it's a test summary",
|
||||
"description": "it's a test description",
|
||||
})
|
||||
|
||||
// label-template
|
||||
f(&Alert{
|
||||
@@ -93,6 +99,19 @@ func TestAlertExecTemplate(t *testing.T) {
|
||||
"description": "It is 10000 connections for localhost for more than 5m0s",
|
||||
})
|
||||
|
||||
// label template override
|
||||
f(&Alert{
|
||||
Value: 1e4,
|
||||
}, map[string]string{
|
||||
"summary": `{{- define "default.template" -}} {{ printf "summary" }} {{- end -}} {{ template "default.template" . }}`,
|
||||
"description": `{{ template "default.template" . }}`,
|
||||
"value": `{{$value }}`,
|
||||
}, map[string]string{
|
||||
"summary": "summary",
|
||||
"description": "",
|
||||
"value": "10000",
|
||||
})
|
||||
|
||||
// expression-template
|
||||
f(&Alert{
|
||||
Expr: `vm_rows{"label"="bar"}<0`,
|
||||
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/templates"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/flagutil"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promauth"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promutils"
|
||||
@@ -93,13 +92,11 @@ var (
|
||||
func Init(gen AlertURLGenerator, extLabels map[string]string, extURL string) (func() []Notifier, error) {
|
||||
externalURL = extURL
|
||||
externalLabels = extLabels
|
||||
eu, err := url.Parse(externalURL)
|
||||
_, err := url.Parse(externalURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse external URL: %w", err)
|
||||
}
|
||||
|
||||
templates.UpdateWithFuncs(templates.FuncsWithExternalURL(eu))
|
||||
|
||||
if *blackHole {
|
||||
if len(*addrs) > 0 || *configPath != "" {
|
||||
return nil, fmt.Errorf("only one of -notifier.blackhole, -notifier.url and -notifier.config flags must be specified")
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
package notifier
|
||||
|
||||
import (
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/templates"
|
||||
"net/url"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/templates"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
if err := templates.Load([]string{"testdata/templates/*good.tmpl"}, true); err != nil {
|
||||
if err := templates.Load([]string{"testdata/templates/*good.tmpl"}, url.URL{}); err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
os.Exit(m.Run())
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math"
|
||||
"net/url"
|
||||
"os"
|
||||
"sort"
|
||||
"testing"
|
||||
@@ -26,7 +27,7 @@ func init() {
|
||||
}
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
if err := templates.Load([]string{}, true); err != nil {
|
||||
if err := templates.Load([]string{}, url.URL{}); err != nil {
|
||||
fmt.Println("failed to load template for test")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
@@ -54,10 +54,9 @@ func newTemplate() *textTpl.Template {
|
||||
}
|
||||
|
||||
// Load func loads templates from multiple globs specified in pathPatterns and either
|
||||
// sets them directly to current template if it's undefined or with overwrite=true
|
||||
// or sets replacement templates and adds templates with new names to a current
|
||||
func Load(pathPatterns []string, overwrite bool) error {
|
||||
var err error
|
||||
// sets them directly to current template if it's the first init;
|
||||
// or sets replacement templates and wait for Reload() to replace current template with replacement.
|
||||
func Load(pathPatterns []string, externalURL url.URL) error {
|
||||
tmpl := newTemplate()
|
||||
for _, tp := range pathPatterns {
|
||||
p, err := doublestar.FilepathGlob(tp)
|
||||
@@ -79,36 +78,12 @@ func Load(pathPatterns []string, overwrite bool) error {
|
||||
}
|
||||
tplMu.Lock()
|
||||
defer tplMu.Unlock()
|
||||
if masterTmpl.current == nil || overwrite {
|
||||
masterTmpl.replacement = nil
|
||||
masterTmpl.current = newTemplate()
|
||||
} else {
|
||||
masterTmpl.replacement = newTemplate()
|
||||
if err = copyTemplates(tmpl, masterTmpl.replacement, overwrite); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return copyTemplates(tmpl, masterTmpl.current, overwrite)
|
||||
}
|
||||
tmpl = tmpl.Funcs(funcsWithExternalURL(externalURL))
|
||||
|
||||
func copyTemplates(from *textTpl.Template, to *textTpl.Template, overwrite bool) error {
|
||||
if from == nil {
|
||||
return nil
|
||||
}
|
||||
if to == nil {
|
||||
to = newTemplate()
|
||||
}
|
||||
tmpl, err := from.Clone()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, t := range tmpl.Templates() {
|
||||
if to.Lookup(t.Name()) == nil || overwrite {
|
||||
to, err = to.AddParseTree(t.Name(), t.Tree)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to add template %q: %w", t.Name(), err)
|
||||
}
|
||||
}
|
||||
if masterTmpl.current == nil {
|
||||
masterTmpl.current = tmpl
|
||||
} else {
|
||||
masterTmpl.replacement = tmpl
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -153,13 +128,6 @@ func datasourceMetricsToTemplateMetrics(ms []datasource.Metric) []metric {
|
||||
// for templating functions.
|
||||
type QueryFn func(query string) ([]datasource.Metric, error)
|
||||
|
||||
// UpdateWithFuncs updates existing or sets a new function map for a template
|
||||
func UpdateWithFuncs(funcs textTpl.FuncMap) {
|
||||
tplMu.Lock()
|
||||
defer tplMu.Unlock()
|
||||
masterTmpl.current = masterTmpl.current.Funcs(funcs)
|
||||
}
|
||||
|
||||
// GetWithFuncs returns a copy of current template with additional FuncMap
|
||||
// provided with funcs argument
|
||||
func GetWithFuncs(funcs textTpl.FuncMap) (*textTpl.Template, error) {
|
||||
@@ -174,13 +142,6 @@ func GetWithFuncs(funcs textTpl.FuncMap) (*textTpl.Template, error) {
|
||||
return tmpl.Funcs(funcs), nil
|
||||
}
|
||||
|
||||
// Get returns a copy of a template
|
||||
func Get() (*textTpl.Template, error) {
|
||||
tplMu.RLock()
|
||||
defer tplMu.RUnlock()
|
||||
return masterTmpl.current.Clone()
|
||||
}
|
||||
|
||||
// FuncsWithQuery returns a function map that depends on metric data
|
||||
func FuncsWithQuery(query QueryFn) textTpl.FuncMap {
|
||||
return textTpl.FuncMap{
|
||||
@@ -198,8 +159,8 @@ func FuncsWithQuery(query QueryFn) textTpl.FuncMap {
|
||||
}
|
||||
}
|
||||
|
||||
// FuncsWithExternalURL returns a function map that depends on externalURL value
|
||||
func FuncsWithExternalURL(externalURL *url.URL) textTpl.FuncMap {
|
||||
// funcsWithExternalURL returns a function map that depends on externalURL value
|
||||
func funcsWithExternalURL(externalURL url.URL) textTpl.FuncMap {
|
||||
return textTpl.FuncMap{
|
||||
"externalURL": func() string {
|
||||
return externalURL.String()
|
||||
|
||||
@@ -2,6 +2,7 @@ package templates
|
||||
|
||||
import (
|
||||
"math"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
textTpl "text/template"
|
||||
@@ -152,7 +153,7 @@ func TestTemplatesLoad_Failure(t *testing.T) {
|
||||
f := func(pathPatterns []string, expectedErrStr string) {
|
||||
t.Helper()
|
||||
|
||||
err := Load(pathPatterns, false)
|
||||
err := Load(pathPatterns, url.URL{})
|
||||
if err == nil {
|
||||
t.Fatalf("expecting non-nil error")
|
||||
}
|
||||
@@ -171,128 +172,17 @@ func TestTemplatesLoad_Failure(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestTemplatesLoad_Success(t *testing.T) {
|
||||
f := func(initialTmpl textTemplate, pathPatterns []string, overwrite bool, expectedTmpl textTemplate) {
|
||||
f := func(pathPatterns []string, expectedTmpl textTemplate) {
|
||||
t.Helper()
|
||||
|
||||
masterTmplOrig := masterTmpl
|
||||
masterTmpl = initialTmpl
|
||||
defer func() {
|
||||
masterTmpl = masterTmplOrig
|
||||
}()
|
||||
|
||||
if err := Load(pathPatterns, overwrite); err != nil {
|
||||
if err := Load(pathPatterns, url.URL{}); err != nil {
|
||||
t.Fatalf("cannot load templates: %s", err)
|
||||
}
|
||||
|
||||
if !equalTemplates(masterTmpl.replacement, expectedTmpl.replacement) {
|
||||
t.Fatalf("unexpected replacement template\ngot\n%+v\nwant\n%+v", masterTmpl.replacement, expectedTmpl.replacement)
|
||||
}
|
||||
if !equalTemplates(masterTmpl.current, expectedTmpl.current) {
|
||||
t.Fatalf("unexpected current template\ngot\n%+v\nwant\n%+v", masterTmpl.current, expectedTmpl.current)
|
||||
}
|
||||
}
|
||||
|
||||
// non existing path undefined template override
|
||||
initialTmpl := mkTemplate(nil, nil)
|
||||
pathPatterns := []string{
|
||||
"templates/non-existing/good-*.tpl",
|
||||
"templates/absent/good-*.tpl",
|
||||
}
|
||||
overwrite := true
|
||||
expectedTmpl := mkTemplate(``, nil)
|
||||
f(initialTmpl, pathPatterns, overwrite, expectedTmpl)
|
||||
|
||||
// non existing path defined template override
|
||||
initialTmpl = mkTemplate(`
|
||||
{{- define "test.1" -}}
|
||||
{{- printf "value" -}}
|
||||
{{- end -}}
|
||||
`, nil)
|
||||
pathPatterns = []string{
|
||||
"templates/non-existing/good-*.tpl",
|
||||
"templates/absent/good-*.tpl",
|
||||
}
|
||||
overwrite = true
|
||||
expectedTmpl = mkTemplate(``, nil)
|
||||
f(initialTmpl, pathPatterns, overwrite, expectedTmpl)
|
||||
|
||||
// existing path undefined template override
|
||||
initialTmpl = mkTemplate(nil, nil)
|
||||
pathPatterns = []string{
|
||||
"templates/other/nested/good0-*.tpl",
|
||||
"templates/test/good0-*.tpl",
|
||||
}
|
||||
overwrite = false
|
||||
expectedTmpl = mkTemplate(`
|
||||
{{- define "good0-test.tpl" -}}{{- end -}}
|
||||
{{- define "test.0" -}}
|
||||
{{ printf "Hello %s!" externalURL }}
|
||||
{{- end -}}
|
||||
{{- define "test.1" -}}
|
||||
{{ printf "Hello %s!" externalURL }}
|
||||
{{- end -}}
|
||||
{{- define "test.2" -}}
|
||||
{{ printf "Hello %s!" externalURL }}
|
||||
{{- end -}}
|
||||
{{- define "test.3" -}}
|
||||
{{ printf "Hello %s!" externalURL }}
|
||||
{{- end -}}
|
||||
`, nil)
|
||||
f(initialTmpl, pathPatterns, overwrite, expectedTmpl)
|
||||
|
||||
// existing path defined template override
|
||||
initialTmpl = mkTemplate(`
|
||||
{{- define "test.1" -}}
|
||||
{{ printf "Hello %s!" "world" }}
|
||||
{{- end -}}
|
||||
`, nil)
|
||||
pathPatterns = []string{
|
||||
"templates/other/nested/good0-*.tpl",
|
||||
"templates/test/good0-*.tpl",
|
||||
}
|
||||
overwrite = false
|
||||
expectedTmpl = mkTemplate(`
|
||||
{{- define "good0-test.tpl" -}}{{- end -}}
|
||||
{{- define "test.0" -}}
|
||||
{{ printf "Hello %s!" externalURL }}
|
||||
{{- end -}}
|
||||
{{- define "test.1" -}}
|
||||
{{ printf "Hello %s!" "world" }}
|
||||
{{- end -}}
|
||||
{{- define "test.2" -}}
|
||||
{{ printf "Hello %s!" externalURL }}
|
||||
{{- end -}}
|
||||
{{- define "test.3" -}}
|
||||
{{ printf "Hello %s!" externalURL }}
|
||||
{{- end -}}
|
||||
`, `
|
||||
{{- define "good0-test.tpl" -}}{{- end -}}
|
||||
{{- define "test.0" -}}
|
||||
{{ printf "Hello %s!" externalURL }}
|
||||
{{- end -}}
|
||||
{{- define "test.1" -}}
|
||||
{{ printf "Hello %s!" externalURL }}
|
||||
{{- end -}}
|
||||
{{- define "test.2" -}}
|
||||
{{ printf "Hello %s!" externalURL }}
|
||||
{{- end -}}
|
||||
{{- define "test.3" -}}
|
||||
{{ printf "Hello %s!" externalURL }}
|
||||
{{- end -}}
|
||||
`)
|
||||
f(initialTmpl, pathPatterns, overwrite, expectedTmpl)
|
||||
}
|
||||
|
||||
func TestTemplatesReload(t *testing.T) {
|
||||
f := func(initialTmpl, expectedTmpl textTemplate) {
|
||||
t.Helper()
|
||||
|
||||
masterTmplOrig := masterTmpl
|
||||
masterTmpl = initialTmpl
|
||||
defer func() {
|
||||
masterTmpl = masterTmplOrig
|
||||
}()
|
||||
|
||||
Reload()
|
||||
|
||||
if !equalTemplates(masterTmpl.replacement, expectedTmpl.replacement) {
|
||||
@@ -303,46 +193,47 @@ func TestTemplatesReload(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// empty current and replacement templates
|
||||
f(mkTemplate(nil, nil), mkTemplate(nil, nil))
|
||||
// non existing path
|
||||
pathPatterns := []string{
|
||||
"templates/non-existing/good-*.tpl",
|
||||
"templates/absent/good-*.tpl",
|
||||
}
|
||||
expectedTmpl := mkTemplate(``, nil)
|
||||
f(pathPatterns, expectedTmpl)
|
||||
|
||||
// empty current template only
|
||||
f(mkTemplate(`
|
||||
{{- define "test.1" -}}
|
||||
{{- printf "value" -}}
|
||||
{{- end -}}
|
||||
`, nil), mkTemplate(`
|
||||
{{- define "test.1" -}}
|
||||
{{- printf "value" -}}
|
||||
{{- end -}}
|
||||
`, nil))
|
||||
|
||||
// empty replacement template only
|
||||
f(mkTemplate(nil, `
|
||||
{{- define "test.1" -}}
|
||||
{{- printf "value" -}}
|
||||
{{- end -}}
|
||||
`), mkTemplate(`
|
||||
{{- define "test.1" -}}
|
||||
{{- printf "value" -}}
|
||||
{{- end -}}
|
||||
`, nil))
|
||||
|
||||
// defined both templates
|
||||
f(mkTemplate(`
|
||||
// existing path
|
||||
pathPatterns = []string{
|
||||
"templates/test/good0-*.tpl",
|
||||
}
|
||||
expectedTmpl = mkTemplate(`
|
||||
{{- define "good0-test.tpl" -}}{{- end -}}
|
||||
{{- define "test.0" -}}
|
||||
{{- printf "value" -}}
|
||||
{{ printf "Hello %s!" externalURL }}
|
||||
{{- end -}}
|
||||
{{- define "test.2" -}}
|
||||
{{ printf "Hello %s!" externalURL }}
|
||||
{{- end -}}
|
||||
{{- define "test.3" -}}
|
||||
{{ printf "Hello %s!" externalURL }}
|
||||
{{- end -}}
|
||||
`, nil)
|
||||
f(pathPatterns, expectedTmpl)
|
||||
|
||||
// existing path defined template override
|
||||
pathPatterns = []string{
|
||||
"templates/other/nested/good0-*.tpl",
|
||||
}
|
||||
expectedTmpl = mkTemplate(`
|
||||
{{- define "good0-test.tpl" -}}{{- end -}}
|
||||
{{- define "test.0" -}}
|
||||
{{ printf "Hello %s!" externalURL }}
|
||||
{{- end -}}
|
||||
{{- define "test.1" -}}
|
||||
{{- printf "before" -}}
|
||||
{{ printf "Hello %s!" externalURL }}
|
||||
{{- end -}}
|
||||
`, `
|
||||
{{- define "test.1" -}}
|
||||
{{- printf "after" -}}
|
||||
{{- define "test.3" -}}
|
||||
{{ printf "Hello %s!" externalURL }}
|
||||
{{- end -}}
|
||||
`), mkTemplate(`
|
||||
{{- define "test.1" -}}
|
||||
{{- printf "after" -}}
|
||||
{{- end -}}
|
||||
`, nil))
|
||||
`, nil)
|
||||
f(pathPatterns, expectedTmpl)
|
||||
}
|
||||
|
||||
@@ -2,11 +2,12 @@ package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/config"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/datasource"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/rule"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestRecordingToApi(t *testing.T) {
|
||||
|
||||
@@ -783,10 +783,11 @@ func parseAuthConfig(data []byte) (*AuthConfig, error) {
|
||||
|
||||
func parseAuthConfigUsers(ac *AuthConfig) (map[string]*UserInfo, error) {
|
||||
uis := ac.Users
|
||||
if len(uis) == 0 && ac.UnauthorizedUser == nil {
|
||||
return nil, fmt.Errorf("Missing `users` or `unauthorized_user` sections")
|
||||
}
|
||||
byAuthToken := make(map[string]*UserInfo, len(uis))
|
||||
if len(uis) == 0 && ac.UnauthorizedUser == nil {
|
||||
// fast path for empty configuration
|
||||
return byAuthToken, nil
|
||||
}
|
||||
for i := range uis {
|
||||
ui := &uis[i]
|
||||
ats, err := getAuthTokens(ui.AuthToken, ui.BearerToken, ui.Username, ui.Password)
|
||||
|
||||
@@ -24,16 +24,10 @@ func TestParseAuthConfigFailure(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// Empty config
|
||||
f(``)
|
||||
|
||||
// Invalid entry
|
||||
f(`foobar`)
|
||||
f(`foobar: baz`)
|
||||
|
||||
// Empty users
|
||||
f(`users: []`)
|
||||
|
||||
// Missing url_prefix
|
||||
f(`
|
||||
users:
|
||||
@@ -302,6 +296,12 @@ func TestParseAuthConfigSuccess(t *testing.T) {
|
||||
|
||||
insecureSkipVerifyTrue := true
|
||||
|
||||
// Empty config
|
||||
f(``, map[string]*UserInfo{})
|
||||
|
||||
// Empty users
|
||||
f(`users: []`, map[string]*UserInfo{})
|
||||
|
||||
// Single user
|
||||
f(`
|
||||
users:
|
||||
|
||||
@@ -213,7 +213,7 @@ func processRequest(w http.ResponseWriter, r *http.Request, ui *UserInfo) {
|
||||
missingRouteRequests.Inc()
|
||||
var di string
|
||||
if ui.DumpRequestOnErrors {
|
||||
di = debugInfo(u, r.Header)
|
||||
di = debugInfo(u, r)
|
||||
}
|
||||
httpserver.Errorf(w, r, "missing route for %q%s", u.String(), di)
|
||||
return
|
||||
@@ -668,13 +668,13 @@ func (rtb *readTrackingBody) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func debugInfo(u *url.URL, h http.Header) string {
|
||||
func debugInfo(u *url.URL, r *http.Request) string {
|
||||
s := &strings.Builder{}
|
||||
fmt.Fprintf(s, " (host: %q; ", u.Host)
|
||||
fmt.Fprintf(s, " (host: %q; ", r.Host)
|
||||
fmt.Fprintf(s, "path: %q; ", u.Path)
|
||||
fmt.Fprintf(s, "args: %q; ", u.Query().Encode())
|
||||
fmt.Fprint(s, "headers:")
|
||||
_ = h.WriteSubset(s, nil)
|
||||
_ = r.Header.WriteSubset(s, nil)
|
||||
fmt.Fprint(s, ")")
|
||||
return s.String()
|
||||
}
|
||||
|
||||
@@ -4,13 +4,46 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/VictoriaMetrics/metrics"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vminsert/relabel"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmstorage"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/bytesutil"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/httpserver"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/prompbmarshal"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/ratelimiter"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/slicesutil"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/storage"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/timeserieslimits"
|
||||
)
|
||||
|
||||
// StartIngestionRateLimiter starts ingestion rate limiter.
|
||||
//
|
||||
// Ingestion rate limiter must be started before Init() call.
|
||||
//
|
||||
// StopIngestionRateLimiter must be called before Stop() call in order to unblock all the callers
|
||||
// to ingestion rate limiter. Otherwise deadlock may occur at Stop() call.
|
||||
func StartIngestionRateLimiter(maxIngestionRate int) {
|
||||
if maxIngestionRate <= 0 {
|
||||
return
|
||||
}
|
||||
ingestionRateLimitReached := metrics.NewCounter(`vm_max_ingestion_rate_limit_reached_total`)
|
||||
ingestionRateLimiterStopCh = make(chan struct{})
|
||||
ingestionRateLimiter = ratelimiter.New(int64(maxIngestionRate), ingestionRateLimitReached, ingestionRateLimiterStopCh)
|
||||
}
|
||||
|
||||
// StopIngestionRateLimiter stops ingestion rate limiter.
|
||||
func StopIngestionRateLimiter() {
|
||||
if ingestionRateLimiterStopCh == nil {
|
||||
return
|
||||
}
|
||||
close(ingestionRateLimiterStopCh)
|
||||
ingestionRateLimiterStopCh = nil
|
||||
}
|
||||
|
||||
var (
|
||||
ingestionRateLimiter *ratelimiter.RateLimiter
|
||||
ingestionRateLimiterStopCh chan struct{}
|
||||
)
|
||||
|
||||
// InsertCtx contains common bits for data points insertion.
|
||||
@@ -59,7 +92,27 @@ func (ctx *InsertCtx) marshalMetricNameRaw(prefix []byte, labels []prompbmarshal
|
||||
return metricNameRaw[:len(metricNameRaw):len(metricNameRaw)]
|
||||
}
|
||||
|
||||
// TryPrepareLabels prepares context labels to the ingestion
|
||||
//
|
||||
// It returns false if timeseries should be skipped
|
||||
func (ctx *InsertCtx) TryPrepareLabels(hasRelabeling bool) bool {
|
||||
if hasRelabeling {
|
||||
ctx.ApplyRelabeling()
|
||||
}
|
||||
if len(ctx.Labels) == 0 {
|
||||
return false
|
||||
}
|
||||
if timeserieslimits.Enabled() && timeserieslimits.IsExceeding(ctx.Labels) {
|
||||
return false
|
||||
}
|
||||
ctx.sortLabelsIfNeeded()
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// WriteDataPoint writes (timestamp, value) with the given prefix and labels into ctx buffer.
|
||||
//
|
||||
// caller should invoke TryPrepareLabels before using this function if needed
|
||||
func (ctx *InsertCtx) WriteDataPoint(prefix []byte, labels []prompbmarshal.Label, timestamp int64, value float64) error {
|
||||
metricNameRaw := ctx.marshalMetricNameRaw(prefix, labels)
|
||||
return ctx.addRow(metricNameRaw, timestamp, value)
|
||||
@@ -67,6 +120,8 @@ func (ctx *InsertCtx) WriteDataPoint(prefix []byte, labels []prompbmarshal.Label
|
||||
|
||||
// WriteDataPointExt writes (timestamp, value) with the given metricNameRaw and labels into ctx buffer.
|
||||
//
|
||||
// caller must invoke TryPrepareLabels before using this function
|
||||
//
|
||||
// It returns metricNameRaw for the given labels if len(metricNameRaw) == 0.
|
||||
func (ctx *InsertCtx) WriteDataPointExt(metricNameRaw []byte, labels []prompbmarshal.Label, timestamp int64, value float64) ([]byte, error) {
|
||||
if len(metricNameRaw) == 0 {
|
||||
@@ -149,9 +204,12 @@ func (ctx *InsertCtx) FlushBufs() error {
|
||||
}
|
||||
matchIdxsPool.Put(matchIdxs)
|
||||
}
|
||||
ingestionRateLimiter.Register(len(ctx.mrs))
|
||||
|
||||
// There is no need in limiting the number of concurrent calls to vmstorage.AddRows() here,
|
||||
// since the number of concurrent FlushBufs() calls should be already limited via writeconcurrencylimiter
|
||||
// used at every stream.Parse() call under lib/protoparser/*
|
||||
|
||||
err := vmstorage.AddRows(ctx.mrs)
|
||||
ctx.Reset(0)
|
||||
if err == nil {
|
||||
|
||||
@@ -12,8 +12,8 @@ var sortLabels = flag.Bool("sortLabels", false, `Whether to sort labels for inco
|
||||
`For example, if m{k1="v1",k2="v2"} may be sent as m{k2="v2",k1="v1"}. `+
|
||||
`Enabled sorting for labels can slow down ingestion performance a bit`)
|
||||
|
||||
// SortLabelsIfNeeded sorts labels if -sortLabels command-line flag is set
|
||||
func (ctx *InsertCtx) SortLabelsIfNeeded() {
|
||||
// sortLabelsIfNeeded sorts labels if -sortLabels command-line flag is set
|
||||
func (ctx *InsertCtx) sortLabelsIfNeeded() {
|
||||
if *sortLabels {
|
||||
sort.Sort(&ctx.Labels)
|
||||
}
|
||||
|
||||
@@ -46,14 +46,9 @@ func insertRows(rows []parser.Row, extraLabels []prompbmarshal.Label) error {
|
||||
label := &extraLabels[j]
|
||||
ctx.AddLabel(label.Name, label.Value)
|
||||
}
|
||||
if hasRelabeling {
|
||||
ctx.ApplyRelabeling()
|
||||
}
|
||||
if len(ctx.Labels) == 0 {
|
||||
// Skip metric without labels.
|
||||
if !ctx.TryPrepareLabels(hasRelabeling) {
|
||||
continue
|
||||
}
|
||||
ctx.SortLabelsIfNeeded()
|
||||
if err := ctx.WriteDataPoint(nil, ctx.Labels, r.Timestamp, r.Value); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -60,14 +60,9 @@ func insertRows(sketches []*datadogsketches.Sketch, extraLabels []prompbmarshal.
|
||||
label := &extraLabels[j]
|
||||
ctx.AddLabel(label.Name, label.Value)
|
||||
}
|
||||
if hasRelabeling {
|
||||
ctx.ApplyRelabeling()
|
||||
}
|
||||
if len(ctx.Labels) == 0 {
|
||||
// Skip metric without labels.
|
||||
if !ctx.TryPrepareLabels(hasRelabeling) {
|
||||
continue
|
||||
}
|
||||
ctx.SortLabelsIfNeeded()
|
||||
var metricNameRaw []byte
|
||||
var err error
|
||||
for _, p := range m.Points {
|
||||
|
||||
@@ -63,14 +63,9 @@ func insertRows(series []datadogv1.Series, extraLabels []prompbmarshal.Label) er
|
||||
label := &extraLabels[j]
|
||||
ctx.AddLabel(label.Name, label.Value)
|
||||
}
|
||||
if hasRelabeling {
|
||||
ctx.ApplyRelabeling()
|
||||
}
|
||||
if len(ctx.Labels) == 0 {
|
||||
// Skip metric without labels.
|
||||
if !ctx.TryPrepareLabels(hasRelabeling) {
|
||||
continue
|
||||
}
|
||||
ctx.SortLabelsIfNeeded()
|
||||
var metricNameRaw []byte
|
||||
var err error
|
||||
for _, pt := range ss.Points {
|
||||
|
||||
@@ -66,14 +66,9 @@ func insertRows(series []datadogv2.Series, extraLabels []prompbmarshal.Label) er
|
||||
label := &extraLabels[j]
|
||||
ctx.AddLabel(label.Name, label.Value)
|
||||
}
|
||||
if hasRelabeling {
|
||||
ctx.ApplyRelabeling()
|
||||
}
|
||||
if len(ctx.Labels) == 0 {
|
||||
// Skip metric without labels.
|
||||
if !ctx.TryPrepareLabels(hasRelabeling) {
|
||||
continue
|
||||
}
|
||||
ctx.SortLabelsIfNeeded()
|
||||
var metricNameRaw []byte
|
||||
var err error
|
||||
for _, pt := range ss.Points {
|
||||
|
||||
@@ -36,14 +36,9 @@ func insertRows(rows []parser.Row) error {
|
||||
tag := &r.Tags[j]
|
||||
ctx.AddLabel(tag.Key, tag.Value)
|
||||
}
|
||||
if hasRelabeling {
|
||||
ctx.ApplyRelabeling()
|
||||
}
|
||||
if len(ctx.Labels) == 0 {
|
||||
// Skip metric without labels.
|
||||
if !ctx.TryPrepareLabels(hasRelabeling) {
|
||||
continue
|
||||
}
|
||||
ctx.SortLabelsIfNeeded()
|
||||
if err := ctx.WriteDataPoint(nil, ctx.Labels, r.Timestamp, r.Value); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
parser "github.com/VictoriaMetrics/VictoriaMetrics/lib/protoparser/influx"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/protoparser/influx/stream"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/storage"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/timeserieslimits"
|
||||
"github.com/VictoriaMetrics/metrics"
|
||||
)
|
||||
|
||||
@@ -69,6 +70,7 @@ func insertRows(db string, rows []parser.Row, extraLabels []prompbmarshal.Label)
|
||||
ic.Reset(rowsLen)
|
||||
rowsTotal := 0
|
||||
hasRelabeling := relabel.HasRelabeling()
|
||||
hasLimitsEnabled := timeserieslimits.Enabled()
|
||||
for i := range rows {
|
||||
r := &rows[i]
|
||||
rowsTotal += len(r.Fields)
|
||||
@@ -108,18 +110,17 @@ func insertRows(db string, rows []parser.Row, extraLabels []prompbmarshal.Label)
|
||||
metricGroup := bytesutil.ToUnsafeString(ctx.metricGroupBuf)
|
||||
ic.Labels = append(ic.Labels[:0], ctx.originLabels...)
|
||||
ic.AddLabel("", metricGroup)
|
||||
ic.ApplyRelabeling()
|
||||
if len(ic.Labels) == 0 {
|
||||
// Skip metric without labels.
|
||||
if !ic.TryPrepareLabels(true) {
|
||||
continue
|
||||
}
|
||||
ic.SortLabelsIfNeeded()
|
||||
if err := ic.WriteDataPoint(nil, ic.Labels, r.Timestamp, f.Value); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ic.SortLabelsIfNeeded()
|
||||
if !ic.TryPrepareLabels(false) {
|
||||
continue
|
||||
}
|
||||
ctx.metricNameBuf = storage.MarshalMetricNameRaw(ctx.metricNameBuf[:0], ic.Labels)
|
||||
labelsLen := len(ic.Labels)
|
||||
for j := range r.Fields {
|
||||
@@ -130,9 +131,12 @@ func insertRows(db string, rows []parser.Row, extraLabels []prompbmarshal.Label)
|
||||
metricGroup := bytesutil.ToUnsafeString(ctx.metricGroupBuf)
|
||||
ic.Labels = ic.Labels[:labelsLen]
|
||||
ic.AddLabel("", metricGroup)
|
||||
if len(ic.Labels) == 0 {
|
||||
// Skip metric without labels.
|
||||
continue
|
||||
if hasLimitsEnabled {
|
||||
// special case for optimisation above
|
||||
// check only __name__ label value limits
|
||||
if timeserieslimits.IsExceeding(ic.Labels[len(ic.Labels)-1:]) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if err := ic.WriteDataPoint(ctx.metricNameBuf, ic.Labels[len(ic.Labels)-1:], r.Timestamp, f.Value); err != nil {
|
||||
return err
|
||||
|
||||
@@ -41,8 +41,8 @@ import (
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promscrape"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/protoparser/common"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/protoparser/opentelemetry/firehose"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/storage"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/stringsutil"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/timeserieslimits"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -67,8 +67,9 @@ var (
|
||||
"at -opentsdbHTTPListenAddr . See https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt")
|
||||
configAuthKey = flagutil.NewPassword("configAuthKey", "Authorization key for accessing /config page. It must be passed via authKey query arg. It overrides -httpAuth.*")
|
||||
reloadAuthKey = flagutil.NewPassword("reloadAuthKey", "Auth key for /-/reload http endpoint. It must be passed via authKey query arg. It overrides httpAuth.* settings.")
|
||||
maxLabelsPerTimeseries = flag.Int("maxLabelsPerTimeseries", 30, "The maximum number of labels accepted per time series. Superfluous labels are dropped. In this case the vm_metrics_with_dropped_labels_total metric at /metrics page is incremented")
|
||||
maxLabelValueLen = flag.Int("maxLabelValueLen", 4*1024, "The maximum length of label values in the accepted time series. Longer label values are truncated. In this case the vm_too_long_label_values_total metric at /metrics page is incremented")
|
||||
maxLabelsPerTimeseries = flag.Int("maxLabelsPerTimeseries", 40, "The maximum number of labels per time series to be accepted. Series with superfluous labels are ignored. In this case the vm_rows_ignored_total{reason=\"too_many_labels\"} metric at /metrics page is incremented")
|
||||
maxLabelNameLen = flag.Int("maxLabelNameLen", 256, "The maximum length of label name in the accepted time series. Series with longer label name are ignored. In this case the vm_rows_ignored_total{reason=\"too_long_label_name\"} metric at /metrics page is incremented")
|
||||
maxLabelValueLen = flag.Int("maxLabelValueLen", 4*1024, "The maximum length of label values in the accepted time series. Series with longer label value are ignored. In this case the vm_rows_ignored_total{reason=\"too_long_label_value\"} metric at /metrics page is incremented")
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -87,8 +88,6 @@ var staticServer = http.FileServer(http.FS(staticFiles))
|
||||
func Init() {
|
||||
relabel.Init()
|
||||
vminsertCommon.InitStreamAggr()
|
||||
storage.SetMaxLabelsPerTimeseries(*maxLabelsPerTimeseries)
|
||||
storage.SetMaxLabelValueLen(*maxLabelValueLen)
|
||||
common.StartUnmarshalWorkers()
|
||||
if len(*graphiteListenAddr) > 0 {
|
||||
graphiteServer = graphiteserver.MustStart(*graphiteListenAddr, *graphiteUseProxyProtocol, graphite.InsertHandler)
|
||||
@@ -105,6 +104,7 @@ func Init() {
|
||||
promscrape.Init(func(_ *auth.Token, wr *prompbmarshal.WriteRequest) {
|
||||
prompush.Push(wr)
|
||||
})
|
||||
timeserieslimits.Init(*maxLabelsPerTimeseries, *maxLabelNameLen, *maxLabelValueLen)
|
||||
}
|
||||
|
||||
// Stop stops vminsert.
|
||||
@@ -439,14 +439,4 @@ var (
|
||||
promscrapeStatusConfigRequests = metrics.NewCounter(`vm_http_requests_total{path="/api/v1/status/config"}`)
|
||||
|
||||
promscrapeConfigReloadRequests = metrics.NewCounter(`vm_http_requests_total{path="/-/reload"}`)
|
||||
|
||||
_ = metrics.NewGauge(`vm_metrics_with_dropped_labels_total`, func() float64 {
|
||||
return float64(storage.MetricsWithDroppedLabels.Load())
|
||||
})
|
||||
_ = metrics.NewGauge(`vm_too_long_label_names_total`, func() float64 {
|
||||
return float64(storage.TooLongLabelNames.Load())
|
||||
})
|
||||
_ = metrics.NewGauge(`vm_too_long_label_values_total`, func() float64 {
|
||||
return float64(storage.TooLongLabelValues.Load())
|
||||
})
|
||||
)
|
||||
|
||||
@@ -55,14 +55,9 @@ func insertRows(block *stream.Block, extraLabels []prompbmarshal.Label) error {
|
||||
label := &extraLabels[j]
|
||||
ic.AddLabel(label.Name, label.Value)
|
||||
}
|
||||
if hasRelabeling {
|
||||
ic.ApplyRelabeling()
|
||||
}
|
||||
if len(ic.Labels) == 0 {
|
||||
// Skip metric without labels.
|
||||
if !ic.TryPrepareLabels(hasRelabeling) {
|
||||
return nil
|
||||
}
|
||||
ic.SortLabelsIfNeeded()
|
||||
ctx.metricNameBuf = storage.MarshalMetricNameRaw(ctx.metricNameBuf[:0], ic.Labels)
|
||||
values := block.Values
|
||||
timestamps := block.Timestamps
|
||||
@@ -71,7 +66,9 @@ func insertRows(block *stream.Block, extraLabels []prompbmarshal.Label) error {
|
||||
}
|
||||
for j, value := range values {
|
||||
timestamp := timestamps[j]
|
||||
if err := ic.WriteDataPoint(ctx.metricNameBuf, nil, timestamp, value); err != nil {
|
||||
// TODO: @f41gh7 looks like it's better to use WriteDataPointExt
|
||||
// since metricName never changes inside insertRows call
|
||||
if err := ic.WriteDataPoint(ctx.metricNameBuf, ic.Labels, timestamp, value); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,14 +58,9 @@ func insertRows(rows []newrelic.Row, extraLabels []prompbmarshal.Label) error {
|
||||
label := &extraLabels[k]
|
||||
ctx.AddLabel(label.Name, label.Value)
|
||||
}
|
||||
if hasRelabeling {
|
||||
ctx.ApplyRelabeling()
|
||||
}
|
||||
if len(ctx.Labels) == 0 {
|
||||
// Skip metric without labels.
|
||||
if !ctx.TryPrepareLabels(hasRelabeling) {
|
||||
continue
|
||||
}
|
||||
ctx.SortLabelsIfNeeded()
|
||||
if err := ctx.WriteDataPoint(nil, ctx.Labels, r.Timestamp, s.Value); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -59,14 +59,9 @@ func insertRows(tss []prompbmarshal.TimeSeries, extraLabels []prompbmarshal.Labe
|
||||
for _, label := range extraLabels {
|
||||
ctx.AddLabel(label.Name, label.Value)
|
||||
}
|
||||
if hasRelabeling {
|
||||
ctx.ApplyRelabeling()
|
||||
}
|
||||
if len(ctx.Labels) == 0 {
|
||||
// Skip metric without labels.
|
||||
if !ctx.TryPrepareLabels(hasRelabeling) {
|
||||
continue
|
||||
}
|
||||
ctx.SortLabelsIfNeeded()
|
||||
var metricNameRaw []byte
|
||||
var err error
|
||||
samples := ts.Samples
|
||||
|
||||
@@ -36,14 +36,9 @@ func insertRows(rows []parser.Row) error {
|
||||
tag := &r.Tags[j]
|
||||
ctx.AddLabel(tag.Key, tag.Value)
|
||||
}
|
||||
if hasRelabeling {
|
||||
ctx.ApplyRelabeling()
|
||||
}
|
||||
if len(ctx.Labels) == 0 {
|
||||
// Skip metric without labels.
|
||||
if !ctx.TryPrepareLabels(hasRelabeling) {
|
||||
continue
|
||||
}
|
||||
ctx.SortLabelsIfNeeded()
|
||||
if err := ctx.WriteDataPoint(nil, ctx.Labels, r.Timestamp, r.Value); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -54,14 +54,9 @@ func insertRows(rows []parser.Row, extraLabels []prompbmarshal.Label) error {
|
||||
label := &extraLabels[j]
|
||||
ctx.AddLabel(label.Name, label.Value)
|
||||
}
|
||||
if hasRelabeling {
|
||||
ctx.ApplyRelabeling()
|
||||
}
|
||||
if len(ctx.Labels) == 0 {
|
||||
// Skip metric without labels.
|
||||
if !ctx.TryPrepareLabels(hasRelabeling) {
|
||||
continue
|
||||
}
|
||||
ctx.SortLabelsIfNeeded()
|
||||
if err := ctx.WriteDataPoint(nil, ctx.Labels, r.Timestamp, r.Value); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -54,14 +54,9 @@ func insertRows(rows []parser.Row, extraLabels []prompbmarshal.Label) error {
|
||||
label := &extraLabels[j]
|
||||
ctx.AddLabel(label.Name, label.Value)
|
||||
}
|
||||
if hasRelabeling {
|
||||
ctx.ApplyRelabeling()
|
||||
}
|
||||
if len(ctx.Labels) == 0 {
|
||||
// Skip metric without labels.
|
||||
if !ctx.TryPrepareLabels(hasRelabeling) {
|
||||
continue
|
||||
}
|
||||
ctx.SortLabelsIfNeeded()
|
||||
if err := ctx.WriteDataPoint(nil, ctx.Labels, r.Timestamp, r.Value); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -57,12 +57,9 @@ func push(ctx *common.InsertCtx, tss []prompbmarshal.TimeSeries) {
|
||||
label := &ts.Labels[j]
|
||||
ctx.AddLabel(label.Name, label.Value)
|
||||
}
|
||||
ctx.ApplyRelabeling()
|
||||
if len(ctx.Labels) == 0 {
|
||||
// Skip metric without labels.
|
||||
if !ctx.TryPrepareLabels(false) {
|
||||
continue
|
||||
}
|
||||
ctx.SortLabelsIfNeeded()
|
||||
var metricNameRaw []byte
|
||||
var err error
|
||||
for i := range ts.Samples {
|
||||
|
||||
@@ -52,14 +52,10 @@ func insertRows(timeseries []prompb.TimeSeries, extraLabels []prompbmarshal.Labe
|
||||
label := &extraLabels[j]
|
||||
ctx.AddLabel(label.Name, label.Value)
|
||||
}
|
||||
if hasRelabeling {
|
||||
ctx.ApplyRelabeling()
|
||||
}
|
||||
if len(ctx.Labels) == 0 {
|
||||
// Skip metric without labels.
|
||||
|
||||
if !ctx.TryPrepareLabels(hasRelabeling) {
|
||||
continue
|
||||
}
|
||||
ctx.SortLabelsIfNeeded()
|
||||
var metricNameRaw []byte
|
||||
var err error
|
||||
samples := ts.Samples
|
||||
|
||||
@@ -58,14 +58,9 @@ func insertRows(rows []parser.Row, extraLabels []prompbmarshal.Label) error {
|
||||
label := &extraLabels[j]
|
||||
ic.AddLabel(label.Name, label.Value)
|
||||
}
|
||||
if hasRelabeling {
|
||||
ic.ApplyRelabeling()
|
||||
}
|
||||
if len(ic.Labels) == 0 {
|
||||
// Skip metric without labels.
|
||||
if !ic.TryPrepareLabels(hasRelabeling) {
|
||||
continue
|
||||
}
|
||||
ic.SortLabelsIfNeeded()
|
||||
ctx.metricNameBuf = storage.MarshalMetricNameRaw(ctx.metricNameBuf[:0], ic.Labels)
|
||||
values := r.Values
|
||||
timestamps := r.Timestamps
|
||||
|
||||
@@ -699,8 +699,13 @@ func (rc *rollupConfig) doInternal(dstValues []float64, tsm *timeseriesMap, valu
|
||||
// Extend dstValues in order to remove mallocs below.
|
||||
dstValues = decimal.ExtendFloat64sCapacity(dstValues, len(rc.Timestamps))
|
||||
|
||||
scrapeInterval := getScrapeInterval(timestamps, rc.Step)
|
||||
maxPrevInterval := getMaxPrevInterval(scrapeInterval)
|
||||
// Use step as the scrape interval for instant queries (when start == end).
|
||||
maxPrevInterval := rc.Step
|
||||
if rc.Start < rc.End {
|
||||
scrapeInterval := getScrapeInterval(timestamps, rc.Step)
|
||||
maxPrevInterval = getMaxPrevInterval(scrapeInterval)
|
||||
}
|
||||
|
||||
if rc.LookbackDelta > 0 && maxPrevInterval > rc.LookbackDelta {
|
||||
maxPrevInterval = rc.LookbackDelta
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
{
|
||||
"files": {
|
||||
"main.css": "./static/css/main.b1929c64.css",
|
||||
"main.js": "./static/js/main.a7d57628.js",
|
||||
"main.css": "./static/css/main.876c56b7.css",
|
||||
"main.js": "./static/js/main.caf36c39.js",
|
||||
"static/js/685.f772060c.chunk.js": "./static/js/685.f772060c.chunk.js",
|
||||
"static/media/MetricsQL.md": "./static/media/MetricsQL.a00044c91d9781cf8557.md",
|
||||
"index.html": "./index.html"
|
||||
},
|
||||
"entrypoints": [
|
||||
"static/css/main.b1929c64.css",
|
||||
"static/js/main.a7d57628.js"
|
||||
"static/css/main.876c56b7.css",
|
||||
"static/js/main.caf36c39.js"
|
||||
]
|
||||
}
|
||||
@@ -1 +1 @@
|
||||
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="./favicon.svg"/><link rel="apple-touch-icon" href="./favicon.svg"/><link rel="mask-icon" href="./favicon.svg" color="#000000"><meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=5"/><meta name="theme-color" content="#000000"/><meta name="description" content="Explore and troubleshoot your VictoriaMetrics data"/><link rel="manifest" href="./manifest.json"/><title>vmui</title><script src="./dashboards/index.js" type="module"></script><meta name="twitter:card" content="summary"><meta name="twitter:title" content="UI for VictoriaMetrics"><meta name="twitter:site" content="@https://victoriametrics.com/"><meta name="twitter:description" content="Explore and troubleshoot your VictoriaMetrics data"><meta name="twitter:image" content="./preview.jpg"><meta property="og:type" content="website"><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 defer="defer" src="./static/js/main.a7d57628.js"></script><link href="./static/css/main.b1929c64.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>
|
||||
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="./favicon.svg"/><link rel="apple-touch-icon" href="./favicon.svg"/><link rel="mask-icon" href="./favicon.svg" color="#000000"><meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=5"/><meta name="theme-color" content="#000000"/><meta name="description" content="Explore and troubleshoot your VictoriaMetrics data"/><link rel="manifest" href="./manifest.json"/><title>vmui</title><script src="./dashboards/index.js" type="module"></script><meta name="twitter:card" content="summary"><meta name="twitter:title" content="UI for VictoriaMetrics"><meta name="twitter:site" content="@https://victoriametrics.com/"><meta name="twitter:description" content="Explore and troubleshoot your VictoriaMetrics data"><meta name="twitter:image" content="./preview.jpg"><meta property="og:type" content="website"><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 defer="defer" src="./static/js/main.caf36c39.js"></script><link href="./static/css/main.876c56b7.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>
|
||||
1
app/vmselect/vmui/static/css/main.876c56b7.css
Normal file
1
app/vmselect/vmui/static/css/main.876c56b7.css
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
2
app/vmselect/vmui/static/js/main.caf36c39.js
Normal file
2
app/vmselect/vmui/static/js/main.caf36c39.js
Normal file
File diff suppressed because one or more lines are too long
@@ -6,7 +6,7 @@ COPY web/ /build/
|
||||
RUN GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -o web-amd64 github.com/VictoriMetrics/vmui/ && \
|
||||
GOOS=windows GOARCH=amd64 CGO_ENABLED=0 go build -o web-windows github.com/VictoriMetrics/vmui/
|
||||
|
||||
FROM alpine:3.20.3
|
||||
FROM alpine:3.21.0
|
||||
USER root
|
||||
|
||||
COPY --from=build-web-stage /build/web-amd64 /app/web
|
||||
|
||||
@@ -48,3 +48,11 @@ export interface LogHits {
|
||||
[key: string]: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface ReportMetaData {
|
||||
id: number;
|
||||
title: string;
|
||||
endpoint: string;
|
||||
comment: string;
|
||||
params: Record<string, string>;
|
||||
}
|
||||
|
||||
@@ -43,7 +43,8 @@ const QueryEditor: FC<QueryEditorProps> = ({
|
||||
const { isMobile } = useDeviceDetect();
|
||||
|
||||
const [openAutocomplete, setOpenAutocomplete] = useState(false);
|
||||
const [caretPosition, setCaretPosition] = useState<[number, number]>([0, 0]);
|
||||
const [caretPositionAutocomplete, setCaretPositionAutocomplete] = useState<[number, number]>([0, 0]);
|
||||
const [caretPositionInput, setCaretPositionInput] = useState<[number, number]>([0, 0]);
|
||||
const autocompleteAnchorEl = useRef<HTMLInputElement>(null);
|
||||
|
||||
const [showAutocomplete, setShowAutocomplete] = useState(autocomplete);
|
||||
@@ -66,7 +67,7 @@ const QueryEditor: FC<QueryEditorProps> = ({
|
||||
|
||||
const handleSelect = (val: string, caretPosition: number) => {
|
||||
onChange(val);
|
||||
setCaretPosition([caretPosition, caretPosition]);
|
||||
setCaretPositionInput([caretPosition, caretPosition]);
|
||||
};
|
||||
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
@@ -108,7 +109,7 @@ const QueryEditor: FC<QueryEditorProps> = ({
|
||||
};
|
||||
|
||||
const handleChangeCaret = (val: [number, number]) => {
|
||||
setCaretPosition(prev => prev[0] === val[0] && prev[1] === val[1] ? prev : val);
|
||||
setCaretPositionAutocomplete(prev => prev[0] === val[0] && prev[1] === val[1] ? prev : val);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
@@ -118,7 +119,7 @@ const QueryEditor: FC<QueryEditorProps> = ({
|
||||
useEffect(() => {
|
||||
setShowAutocomplete(false);
|
||||
debouncedSetShowAutocomplete(true);
|
||||
}, [caretPosition]);
|
||||
}, [caretPositionAutocomplete]);
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -137,13 +138,13 @@ const QueryEditor: FC<QueryEditorProps> = ({
|
||||
onChangeCaret={handleChangeCaret}
|
||||
disabled={disabled}
|
||||
inputmode={"search"}
|
||||
caretPosition={caretPosition}
|
||||
caretPosition={caretPositionInput}
|
||||
/>
|
||||
{showAutocomplete && autocomplete && (
|
||||
<QueryEditorAutocomplete
|
||||
value={value}
|
||||
anchorEl={autocompleteAnchorEl}
|
||||
caretPosition={caretPosition}
|
||||
caretPosition={caretPositionAutocomplete}
|
||||
hasHelperText={Boolean(warning || error)}
|
||||
includeFunctions={includeFunctions}
|
||||
onSelect={handleSelect}
|
||||
|
||||
@@ -38,6 +38,10 @@
|
||||
align-items: flex-start;
|
||||
gap: $padding-small;
|
||||
|
||||
ul {
|
||||
list-style-position: inside;
|
||||
}
|
||||
|
||||
button {
|
||||
color: inherit;
|
||||
min-height: 29px;
|
||||
|
||||
@@ -19,6 +19,10 @@ const Accordion: FC<AccordionProps> = ({
|
||||
const [isOpen, setIsOpen] = useState(defaultExpanded);
|
||||
|
||||
const toggleOpen = () => {
|
||||
const selection = window.getSelection();
|
||||
if (selection && selection.toString()) {
|
||||
return; // If the text is selected, cancel the execution of toggle.
|
||||
}
|
||||
setIsOpen(prev => !prev);
|
||||
};
|
||||
|
||||
|
||||
@@ -46,6 +46,8 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
align-self: flex-start;
|
||||
min-height: 24px;
|
||||
}
|
||||
|
||||
&__content {
|
||||
|
||||
@@ -570,3 +570,14 @@ export const SpinnerIcon = () => (
|
||||
</path>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const CommentIcon = () => (
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path
|
||||
d="M21.99 4c0-1.1-.89-2-1.99-2H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h14l4 4zM18 14H6v-2h12zm0-3H6V9h12zm0-3H6V6h12z"
|
||||
></path>
|
||||
</svg>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
import React, { FC } from "preact/compat";
|
||||
import useBoolean from "../../../hooks/useBoolean";
|
||||
import classNames from "classnames";
|
||||
import TextField from "../TextField/TextField";
|
||||
import "./style.scss";
|
||||
import { marked } from "marked";
|
||||
|
||||
interface Props {
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
}
|
||||
|
||||
const tabs = [
|
||||
{ title: "Write", value: false },
|
||||
{ title: "Preview", value: true },
|
||||
];
|
||||
|
||||
const MarkdownEditor: FC<Props> = ({ value, onChange }) => {
|
||||
const {
|
||||
value: markdownPreview,
|
||||
setTrue: setMarkdownPreviewTrue,
|
||||
setFalse: setMarkdownPreviewFalse,
|
||||
} = useBoolean(false);
|
||||
|
||||
return (
|
||||
<div className="vm-markdown-editor">
|
||||
<div className="vm-markdown-editor-header">
|
||||
<div className="vm-markdown-editor-header-tabs">
|
||||
{tabs.map(({ title, value }) => (
|
||||
<div
|
||||
key={title}
|
||||
className={classNames({
|
||||
"vm-markdown-editor-header-tabs__tab": true,
|
||||
"vm-markdown-editor-header-tabs__tab_active": markdownPreview === value,
|
||||
})}
|
||||
onClick={value ? setMarkdownPreviewTrue : setMarkdownPreviewFalse}
|
||||
>
|
||||
{title}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<span className="vm-markdown-editor-header__info">
|
||||
Markdown is supported
|
||||
</span>
|
||||
</div>
|
||||
{markdownPreview ? (
|
||||
<div
|
||||
className="vm-markdown-editor-preview vm-markdown"
|
||||
dangerouslySetInnerHTML={{ __html: marked(value) as string }}
|
||||
/>
|
||||
) : (
|
||||
<TextField
|
||||
type="textarea"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MarkdownEditor;
|
||||
@@ -0,0 +1,75 @@
|
||||
@use "src/styles/variables" as *;
|
||||
|
||||
.vm-markdown-editor {
|
||||
margin-top: 6px;
|
||||
padding: 0 6px;
|
||||
border-radius: $border-radius-small;
|
||||
border: $border-divider;
|
||||
overflow: hidden;
|
||||
|
||||
&-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background-color: $color-hover-black;
|
||||
padding-right: $padding-global;
|
||||
border-bottom: $border-divider;
|
||||
margin: -1px -7px 6px;
|
||||
|
||||
&-tabs {
|
||||
display: flex;
|
||||
|
||||
&__tab {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: -1px;
|
||||
padding: $padding-small $padding-large;
|
||||
min-height: 40px;
|
||||
color: $color-text-secondary;
|
||||
transition: color 0.3s;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: $color-text;
|
||||
}
|
||||
|
||||
&_active {
|
||||
position: relative;
|
||||
color: $color-text;
|
||||
background-color: $color-background-body;
|
||||
border-top-right-radius: $border-radius-small;
|
||||
border-top-left-radius: $border-radius-small;
|
||||
z-index: 1;
|
||||
|
||||
&:first-child {
|
||||
border-right: $border-divider;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-right: $border-divider;
|
||||
border-left: $border-divider;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__info {
|
||||
margin-left: auto;
|
||||
margin-right: 0;
|
||||
color: $color-text-secondary;
|
||||
font-size: $font-size-small;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
&-preview {
|
||||
padding: $padding-small;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
&-preview,
|
||||
textarea {
|
||||
min-height: 200px;
|
||||
resize: vertical;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
import React, {
|
||||
FC,
|
||||
useEffect,
|
||||
useState,
|
||||
useRef,
|
||||
useMemo,
|
||||
FormEvent,
|
||||
@@ -65,7 +64,6 @@ const TextField: FC<TextFieldProps> = ({
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
const fieldRef = useMemo(() => type === "textarea" ? textareaRef : inputRef, [type]);
|
||||
const [selectionPos, setSelectionPos] = useState<[start: number, end: number]>([0, 0]);
|
||||
|
||||
const inputClasses = classNames({
|
||||
"vm-text-field__input": true,
|
||||
@@ -77,8 +75,9 @@ const TextField: FC<TextFieldProps> = ({
|
||||
});
|
||||
|
||||
const updateCaretPosition = (target: HTMLInputElement | HTMLTextAreaElement) => {
|
||||
if (!onChangeCaret) return;
|
||||
const { selectionStart, selectionEnd } = target;
|
||||
setSelectionPos([selectionStart || 0, selectionEnd || 0]);
|
||||
onChangeCaret && onChangeCaret([selectionStart || 0, selectionEnd || 0]);
|
||||
};
|
||||
|
||||
const handleMouseUp = (e: MouseEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
||||
@@ -127,14 +126,6 @@ const TextField: FC<TextFieldProps> = ({
|
||||
fieldRef?.current?.focus && fieldRef.current.focus();
|
||||
}, [fieldRef, autofocus]);
|
||||
|
||||
useEffect(() => {
|
||||
onChangeCaret && onChangeCaret(selectionPos);
|
||||
}, [selectionPos]);
|
||||
|
||||
useEffect(() => {
|
||||
setSelectionRange(selectionPos);
|
||||
}, [value]);
|
||||
|
||||
useEffect(() => {
|
||||
caretPosition && setSelectionRange(caretPosition);
|
||||
}, [caretPosition]);
|
||||
|
||||
@@ -16,17 +16,20 @@ const UploadJsonButtons: FC<Props> = ({ onOpenModal, onChange }) => (
|
||||
>
|
||||
Paste JSON
|
||||
</Button>
|
||||
<Button>
|
||||
Upload Files
|
||||
<div className="vm-upload-json-buttons__upload">
|
||||
<Button>
|
||||
Upload Files
|
||||
</Button>
|
||||
<input
|
||||
id="json"
|
||||
name="json"
|
||||
type="file"
|
||||
accept="application/json"
|
||||
multiple
|
||||
title=" "
|
||||
onChange={onChange}
|
||||
/>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
|
||||
@@ -6,4 +6,8 @@
|
||||
gap: $padding-global;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&__upload {
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,17 +3,17 @@ import Alert from "../components/Main/Alert/Alert";
|
||||
import useDeviceDetect from "../hooks/useDeviceDetect";
|
||||
import classNames from "classnames";
|
||||
import { CloseIcon } from "../components/Main/Icons";
|
||||
import { ReactNode } from "react";
|
||||
|
||||
export interface SnackModel {
|
||||
message?: string;
|
||||
open?: boolean;
|
||||
key?: number;
|
||||
variant?: "success" | "error" | "info" | "warning";
|
||||
interface SnackbarItem {
|
||||
text: string | ReactNode,
|
||||
type: "success" | "error" | "info" | "warning"
|
||||
timeout?: number
|
||||
}
|
||||
|
||||
type SnackbarItem = undefined | {
|
||||
text: string,
|
||||
type: "success" | "error" | "info" | "warning"
|
||||
export interface SnackModel extends SnackbarItem {
|
||||
open?: boolean;
|
||||
key?: number;
|
||||
}
|
||||
|
||||
type SnackbarContextType = {
|
||||
@@ -31,26 +31,25 @@ export const useSnack = (): SnackbarContextType => useContext(SnackbarContext);
|
||||
export const SnackbarProvider: FC = ({ children }) => {
|
||||
const { isMobile } = useDeviceDetect();
|
||||
|
||||
const [snack, setSnack] = useState<SnackModel>({});
|
||||
const [snack, setSnack] = useState<SnackModel>({ text: "", type: "info" });
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const [infoMessage, setInfoMessage] = useState<SnackbarItem>(undefined);
|
||||
const [infoMessage, setInfoMessage] = useState<SnackbarItem | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!infoMessage) return;
|
||||
setSnack({
|
||||
message: infoMessage.text,
|
||||
variant: infoMessage.type,
|
||||
...infoMessage,
|
||||
key: Date.now()
|
||||
});
|
||||
setOpen(true);
|
||||
const timeout = setTimeout(handleClose, 4000);
|
||||
const timeout = setTimeout(handleClose, infoMessage.timeout || 4000);
|
||||
|
||||
return () => clearTimeout(timeout);
|
||||
}, [infoMessage]);
|
||||
|
||||
const handleClose = () => {
|
||||
setInfoMessage(undefined);
|
||||
setInfoMessage(null);
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
@@ -61,9 +60,9 @@ export const SnackbarProvider: FC = ({ children }) => {
|
||||
"vm-snackbar_mobile": isMobile,
|
||||
})}
|
||||
>
|
||||
<Alert variant={snack.variant}>
|
||||
<Alert variant={snack.type}>
|
||||
<div className="vm-snackbar-content">
|
||||
<span>{snack.message}</span>
|
||||
<span>{snack.text}</span>
|
||||
<div
|
||||
className="vm-snackbar-content__close"
|
||||
onClick={handleClose}
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
import { useSnack } from "../contexts/Snackbar";
|
||||
|
||||
type CopyFn = (text: string, msgInfo?: string) => Promise<boolean> // Return success
|
||||
|
||||
const useCopyToClipboard = (): CopyFn => {
|
||||
const { showInfoMessage } = useSnack();
|
||||
|
||||
return async (text, msgInfo) => {
|
||||
if (!navigator?.clipboard) {
|
||||
showInfoMessage({ text: "Clipboard not supported", type: "error" });
|
||||
console.warn("Clipboard not supported");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Try to save to clipboard then save it in the state if worked
|
||||
try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
if (msgInfo) {
|
||||
showInfoMessage({ text: msgInfo, type: "success" });
|
||||
}
|
||||
return true;
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
showInfoMessage({ text: `${error.name}: ${error.message}`, type: "error" });
|
||||
}
|
||||
console.warn("Copy failed", error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
export default useCopyToClipboard;
|
||||
71
app/vmui/packages/vmui/src/hooks/useCopyToClipboard.tsx
Normal file
71
app/vmui/packages/vmui/src/hooks/useCopyToClipboard.tsx
Normal file
@@ -0,0 +1,71 @@
|
||||
import React from "preact/compat";
|
||||
import { useSnack } from "../contexts/Snackbar";
|
||||
|
||||
type CopyFn = (text: string, msgInfo?: string) => Promise<boolean> // Return success
|
||||
|
||||
const useCopyToClipboard = (): CopyFn => {
|
||||
const { showInfoMessage } = useSnack();
|
||||
|
||||
return async (text, msgInfo) => {
|
||||
if (!navigator?.clipboard) {
|
||||
showInfoMessage({ text: <DebugInfoClipboardApi/>, type: "error", timeout: 20000 });
|
||||
return false;
|
||||
}
|
||||
|
||||
// Try to save to clipboard then save it in the state if worked
|
||||
try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
if (msgInfo) {
|
||||
showInfoMessage({ text: msgInfo, type: "success" });
|
||||
}
|
||||
return true;
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
showInfoMessage({ text: `${error.name}: ${error.message}`, type: "error" });
|
||||
}
|
||||
console.warn("Copy failed", error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
export default useCopyToClipboard;
|
||||
|
||||
const DebugInfoClipboardApi = () => (
|
||||
<div className="vm-snackbar-details">
|
||||
<p className="vm-snackbar-details__title">Clipboard not supported</p>
|
||||
{!window.isSecureContext ? (
|
||||
<p className="vm-snackbar-details__msg">
|
||||
<p>This page is not running in a secure context (HTTPS).</p>
|
||||
<p>Clipboard operations require a secure context.</p>
|
||||
<a
|
||||
className="vm-link vm-link_underlined vm-link_colored"
|
||||
href="https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Learn more about secure contexts
|
||||
</a>
|
||||
</p>
|
||||
) : (
|
||||
<p className="vm-snackbar-details__msg">
|
||||
<p>Common reasons:</p>
|
||||
<ul>
|
||||
<li>Browser restrictions</li>
|
||||
<li>Insecure connection (HTTP)</li>
|
||||
<li>Permissions not granted</li>
|
||||
</ul>
|
||||
<p>
|
||||
For detailed information, visit the <a
|
||||
className="vm-link vm-link_underlined vm-link_colored"
|
||||
href="https://developer.mozilla.org/en-US/docs/Web/API/Clipboard_API#security_considerations"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Clipboard API documentation
|
||||
</a>
|
||||
</p>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { FC, useCallback, useEffect, useMemo, useRef, useState } from "preact/compat";
|
||||
import React, { FC, useCallback, useEffect, useRef, useState } from "preact/compat";
|
||||
import { DownloadIcon } from "../../../components/Main/Icons";
|
||||
import Button from "../../../components/Main/Button/Button";
|
||||
import Tooltip from "../../../components/Main/Tooltip/Tooltip";
|
||||
@@ -12,32 +12,65 @@ import TextField from "../../../components/Main/TextField/TextField";
|
||||
import { useQueryState } from "../../../state/query/QueryStateContext";
|
||||
import { ErrorTypes } from "../../../types";
|
||||
import Alert from "../../../components/Main/Alert/Alert";
|
||||
import qs from "qs";
|
||||
import Popper from "../../../components/Main/Popper/Popper";
|
||||
import helperText from "./helperText";
|
||||
import { Link } from "react-router-dom";
|
||||
import router from "../../../router";
|
||||
import { parseLineToJSON } from "../../../utils/json";
|
||||
import { ExportMetricResult, ReportMetaData } from "../../../api/types";
|
||||
import { getApiEndpoint } from "../../../utils/url";
|
||||
import MarkdownEditor from "../../../components/Main/MarkdownEditor/MarkdownEditor";
|
||||
|
||||
export enum ReportType {
|
||||
QUERY_DATA,
|
||||
RAW_DATA,
|
||||
}
|
||||
|
||||
type Props = {
|
||||
fetchUrl?: string[];
|
||||
reportType?: ReportType
|
||||
}
|
||||
|
||||
const getDefaultReportName = () => `vmui_report_${dayjs().utc().format(DATE_FILENAME_FORMAT)}`;
|
||||
type MetaData = {
|
||||
id: number;
|
||||
url: URL;
|
||||
title: string;
|
||||
comment: string;
|
||||
}
|
||||
|
||||
const DownloadReport: FC<Props> = ({ fetchUrl }) => {
|
||||
const getDefaultTitle = (type: ReportType) => {
|
||||
switch (type) {
|
||||
case ReportType.RAW_DATA:
|
||||
return "Raw report";
|
||||
default:
|
||||
return "Report";
|
||||
}
|
||||
};
|
||||
|
||||
const getDefaultFilename = (title: string) => {
|
||||
const timestamp = dayjs().utc().format(DATE_FILENAME_FORMAT);
|
||||
return `vmui_${title.toLowerCase().replace(/ /g, "_")}_${timestamp}`;
|
||||
};
|
||||
|
||||
const DownloadReport: FC<Props> = ({ fetchUrl, reportType = ReportType.QUERY_DATA }) => {
|
||||
const { query } = useQueryState();
|
||||
|
||||
const [filename, setFilename] = useState(getDefaultReportName());
|
||||
const defaultTitle = getDefaultTitle(reportType);
|
||||
const defaultFilename = getDefaultFilename(defaultTitle);
|
||||
|
||||
const [title, setTitle] = useState(defaultTitle);
|
||||
const [filename, setFilename] = useState(defaultFilename);
|
||||
const [comment, setComment] = useState("");
|
||||
const [trace, setTrace] = useState(true);
|
||||
const [trace, setTrace] = useState(reportType === ReportType.QUERY_DATA);
|
||||
const [error, setError] = useState<ErrorTypes | string>();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const titleRef = useRef<HTMLDivElement>(null);
|
||||
const filenameRef = useRef<HTMLDivElement>(null);
|
||||
const commentRef = useRef<HTMLDivElement>(null);
|
||||
const traceRef = useRef<HTMLDivElement>(null);
|
||||
const generateRef = useRef<HTMLDivElement>(null);
|
||||
const helperRefs = [filenameRef, commentRef, traceRef, generateRef];
|
||||
const helperRefs = [filenameRef, titleRef, commentRef, traceRef, generateRef];
|
||||
const [stepHelper, setStepHelper] = useState(0);
|
||||
|
||||
const {
|
||||
@@ -52,13 +85,17 @@ const DownloadReport: FC<Props> = ({ fetchUrl }) => {
|
||||
setFalse: handleCloseHelper,
|
||||
} = useBoolean(false);
|
||||
|
||||
const fetchUrlReport = useMemo(() => {
|
||||
const getFetchUrlReport = useCallback(() => {
|
||||
if (!fetchUrl) return;
|
||||
return fetchUrl.map((str, i) => {
|
||||
const url = new URL(str);
|
||||
trace ? url.searchParams.set("trace", "1") : url.searchParams.delete("trace");
|
||||
return { id: i, url: url };
|
||||
});
|
||||
try {
|
||||
return fetchUrl.map((str, i) => {
|
||||
const url = new URL(str);
|
||||
trace ? url.searchParams.set("trace", "1") : url.searchParams.delete("trace");
|
||||
return { id: i, url: url };
|
||||
});
|
||||
} catch (e) {
|
||||
setError(String(e));
|
||||
}
|
||||
}, [fetchUrl, trace]);
|
||||
|
||||
const generateFile = useCallback((data: unknown) => {
|
||||
@@ -68,7 +105,7 @@ const DownloadReport: FC<Props> = ({ fetchUrl }) => {
|
||||
|
||||
const link = document.createElement("a");
|
||||
link.href = href;
|
||||
link.download = `${filename || getDefaultReportName()}.json`;
|
||||
link.download = `${filename || defaultFilename}.json`;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
|
||||
@@ -77,9 +114,63 @@ const DownloadReport: FC<Props> = ({ fetchUrl }) => {
|
||||
handleClose();
|
||||
}, [filename]);
|
||||
|
||||
const getMetaData = ({ id, url, comment, title }: MetaData): ReportMetaData => {
|
||||
return {
|
||||
id,
|
||||
title: title || defaultTitle,
|
||||
comment,
|
||||
endpoint: getApiEndpoint(url.pathname) || "",
|
||||
params: Object.fromEntries(url.searchParams)
|
||||
};
|
||||
};
|
||||
|
||||
const processJsonLineResponse = async (response: Response, metaData: MetaData) => {
|
||||
const result: { metric: { [p: string]: string }, values: number[][] }[] = [];
|
||||
const text = await response.text();
|
||||
|
||||
if (response.ok) {
|
||||
const lines = text.split("\n").filter(line => line);
|
||||
lines.forEach((line: string) => {
|
||||
const jsonLine = parseLineToJSON(line) as (ExportMetricResult | null);
|
||||
if (!jsonLine) return;
|
||||
result.push({
|
||||
metric: jsonLine.metric,
|
||||
values: jsonLine.values.map((value, index) => [(jsonLine.timestamps[index] / 1000), value]),
|
||||
});
|
||||
});
|
||||
} else {
|
||||
setError(String(text));
|
||||
}
|
||||
|
||||
return { data: { result, resultType: "matrix" }, vmui: getMetaData(metaData) };
|
||||
};
|
||||
|
||||
const processJsonResponse = async (response: Response, metaData: MetaData) => {
|
||||
const resp = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
resp.vmui = getMetaData(metaData);
|
||||
return resp;
|
||||
} else {
|
||||
const errorType = resp.errorType ? `${resp.errorType}\r\n` : "";
|
||||
setError(`${errorType}${resp?.error || resp?.message || "unknown error"}`);
|
||||
}
|
||||
};
|
||||
|
||||
const processResponse = async (response: Response, metaData: MetaData) => {
|
||||
switch (reportType) {
|
||||
case ReportType.RAW_DATA:
|
||||
return await processJsonLineResponse(response, metaData);
|
||||
default:
|
||||
return await processJsonResponse(response, metaData);
|
||||
}
|
||||
};
|
||||
|
||||
const handleGenerateReport = useCallback(async () => {
|
||||
const fetchUrlReport = getFetchUrlReport();
|
||||
|
||||
if (!fetchUrlReport) {
|
||||
setError(ErrorTypes.validQuery);
|
||||
setError(prev => !prev ? ErrorTypes.validQuery : prev);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -88,20 +179,12 @@ const DownloadReport: FC<Props> = ({ fetchUrl }) => {
|
||||
|
||||
try {
|
||||
const result = [];
|
||||
for await (const { url, id } of fetchUrlReport) {
|
||||
for await (const fetchOps of fetchUrlReport) {
|
||||
if (!fetchOps) continue;
|
||||
const { url, id } = fetchOps;
|
||||
const response = await fetch(url);
|
||||
const resp = await response.json();
|
||||
if (response.ok) {
|
||||
resp.vmui = {
|
||||
id,
|
||||
comment,
|
||||
params: qs.parse(new URL(url).search.replace(/^\?/, ""))
|
||||
};
|
||||
result.push(resp);
|
||||
} else {
|
||||
const errorType = resp.errorType ? `${resp.errorType}\r\n` : "";
|
||||
setError(`${errorType}${resp?.error || resp?.message || "unknown error"}`);
|
||||
}
|
||||
const data = await processResponse(response, { id, url, comment, title });
|
||||
result.push(data);
|
||||
}
|
||||
result.length && generateFile(result);
|
||||
} catch (e) {
|
||||
@@ -111,15 +194,20 @@ const DownloadReport: FC<Props> = ({ fetchUrl }) => {
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [fetchUrlReport, comment, generateFile, query]);
|
||||
}, [getFetchUrlReport, comment, generateFile, query, title]);
|
||||
|
||||
const handleChangeHelp = (step: number) => () => {
|
||||
setStepHelper(prevStep => prevStep + step);
|
||||
const findNextRef = (index: number): number => {
|
||||
const nextIndex = index + step;
|
||||
if (helperRefs[nextIndex]?.current) return nextIndex;
|
||||
return findNextRef(nextIndex);
|
||||
};
|
||||
setStepHelper(findNextRef);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setError("");
|
||||
setFilename(getDefaultReportName());
|
||||
setFilename(defaultFilename);
|
||||
setComment("");
|
||||
}, [openModal]);
|
||||
|
||||
@@ -155,31 +243,41 @@ const DownloadReport: FC<Props> = ({ fetchUrl }) => {
|
||||
<div className="vm-download-report">
|
||||
<div className="vm-download-report-settings">
|
||||
<div ref={filenameRef}>
|
||||
<div className="vm-download-report-settings__title">Filename</div>
|
||||
<TextField
|
||||
label="Filename"
|
||||
value={filename}
|
||||
onChange={setFilename}
|
||||
/>
|
||||
</div>
|
||||
<div ref={commentRef}>
|
||||
<div ref={titleRef}>
|
||||
<div className="vm-download-report-settings__title">Report title</div>
|
||||
<TextField
|
||||
type="textarea"
|
||||
label="Comment"
|
||||
value={title}
|
||||
onChange={setTitle}
|
||||
/>
|
||||
</div>
|
||||
<div ref={commentRef}>
|
||||
<div className="vm-download-report-settings__title">Comment</div>
|
||||
<MarkdownEditor
|
||||
value={comment}
|
||||
onChange={setComment}
|
||||
/>
|
||||
</div>
|
||||
<div ref={traceRef}>
|
||||
<Checkbox
|
||||
checked={trace}
|
||||
onChange={setTrace}
|
||||
label={"Include query trace"}
|
||||
/>
|
||||
</div>
|
||||
<Alert variant="info">
|
||||
If confused with the query results,
|
||||
try viewing the raw samples for selected series in <RawQueryLink/> tab.
|
||||
</Alert>
|
||||
{reportType === ReportType.QUERY_DATA && (
|
||||
<>
|
||||
<div ref={traceRef}>
|
||||
<Checkbox
|
||||
checked={trace}
|
||||
onChange={setTrace}
|
||||
label={"Include query trace"}
|
||||
/>
|
||||
</div>
|
||||
<Alert variant="info">
|
||||
If confused with the query results,
|
||||
try viewing the raw samples for selected series in <RawQueryLink/> tab.
|
||||
</Alert>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{error && <Alert variant="error">{error}</Alert>}
|
||||
<div className="vm-download-report__buttons">
|
||||
|
||||
@@ -11,6 +11,18 @@ const filename = (
|
||||
</>
|
||||
);
|
||||
|
||||
const tittle = (
|
||||
<>
|
||||
<p>Title - specify the title that will be displayed on the <Link
|
||||
to={router.queryAnalyzer}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="vm-link vm-link_underlined"
|
||||
>{routerOptions[router.queryAnalyzer].title}</Link> page.</p>
|
||||
<p>This helps identify your report in the interface.</p>
|
||||
</>
|
||||
);
|
||||
|
||||
const comment = (
|
||||
<>
|
||||
<p>Comment (optional) - add a comment to your report.</p>
|
||||
@@ -39,6 +51,7 @@ const generate = (
|
||||
|
||||
export default [
|
||||
filename,
|
||||
tittle,
|
||||
comment,
|
||||
trace,
|
||||
generate,
|
||||
|
||||
@@ -9,10 +9,15 @@
|
||||
|
||||
&-settings {
|
||||
display: grid;
|
||||
gap: $padding-global;
|
||||
gap: $padding-large;
|
||||
|
||||
textarea {
|
||||
min-height: 200px;
|
||||
&__title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-right: $padding-global;
|
||||
font-size: $font-size;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,7 +39,7 @@
|
||||
line-height: 1.3;
|
||||
|
||||
p {
|
||||
margin-bottom: calc($padding-small/2);
|
||||
margin-bottom: calc($padding-small / 2);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { FC } from "preact/compat";
|
||||
import Hyperlink from "../../../components/Main/Hyperlink/Hyperlink";
|
||||
import { useGraphState } from "../../../state/graph/GraphStateContext";
|
||||
|
||||
const last_over_time = <Hyperlink
|
||||
text="last_over_time"
|
||||
@@ -13,15 +14,19 @@ const instant_query = <Hyperlink
|
||||
underlined
|
||||
/>;
|
||||
|
||||
const InstantQueryTip: FC = () => (
|
||||
<div>
|
||||
<p>
|
||||
This tab shows {instant_query} results for the last 5 minutes ending at the selected time range.
|
||||
</p>
|
||||
<p>
|
||||
Please wrap the query into {last_over_time} if you need results over arbitrary lookbehind interval.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
const InstantQueryTip: FC = () => {
|
||||
const { customStep } = useGraphState();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p>
|
||||
This tab shows {instant_query} results for the last {customStep || "5m"} (defined by the <code>step</code>) ending at the selected time range.
|
||||
</p>
|
||||
<p>
|
||||
Please wrap the query into {last_over_time} if you need results over arbitrary lookbehind interval.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default InstantQueryTip;
|
||||
|
||||
@@ -1,13 +1,20 @@
|
||||
import React, { FC, useMemo } from "preact/compat";
|
||||
import { DataAnalyzerType } from "../index";
|
||||
import Button from "../../../components/Main/Button/Button";
|
||||
import { ClockIcon, InfoIcon, TimelineIcon } from "../../../components/Main/Icons";
|
||||
import useBoolean from "../../../hooks/useBoolean";
|
||||
import Modal from "../../../components/Main/Modal/Modal";
|
||||
import {
|
||||
ClockIcon,
|
||||
CommentIcon,
|
||||
InfoIcon,
|
||||
TimelineIcon
|
||||
} from "../../../components/Main/Icons";
|
||||
import { TimeParams } from "../../../types";
|
||||
import "./style.scss";
|
||||
import dayjs from "dayjs";
|
||||
import { DATE_TIME_FORMAT } from "../../../constants/date";
|
||||
import useBoolean from "../../../hooks/useBoolean";
|
||||
import Modal from "../../../components/Main/Modal/Modal";
|
||||
import { marked } from "marked";
|
||||
import Button from "../../../components/Main/Button/Button";
|
||||
import get from "lodash.get";
|
||||
|
||||
type Props = {
|
||||
data: DataAnalyzerType[];
|
||||
@@ -15,8 +22,23 @@ type Props = {
|
||||
}
|
||||
|
||||
const QueryAnalyzerInfo: FC<Props> = ({ data, period }) => {
|
||||
const dataWithStats = useMemo(() => data.filter(d => d.stats && d.data.resultType === "matrix"), [data]);
|
||||
const comment = useMemo(() => data.find(d => d?.vmui?.comment)?.vmui?.comment, [data]);
|
||||
const dataWithStats = useMemo(() => data.filter(d => d.vmui || d.stats), [data]);
|
||||
const title = dataWithStats.find(d => d?.vmui?.title)?.vmui?.title || "Report";
|
||||
const comment = dataWithStats.find(d => d?.vmui?.comment)?.vmui?.comment;
|
||||
|
||||
const table = useMemo(() => {
|
||||
return [
|
||||
"vmui.endpoint",
|
||||
...new Set(dataWithStats.flatMap(d => [
|
||||
...Object.keys(d.vmui?.params || []).map(key => `vmui.params.${key}`),
|
||||
...Object.keys(d.stats || []).map(key => `stats.${key}`),
|
||||
"isPartial"
|
||||
]))
|
||||
].map(key => ({
|
||||
column: key.split(".").pop(),
|
||||
values: dataWithStats.map(data => get(data, key, "-"))
|
||||
})).filter(({ values }) => values.length && values.every(v => v !== "-"));
|
||||
}, [dataWithStats]);
|
||||
|
||||
const timeRange = useMemo(() => {
|
||||
if (!period) return "";
|
||||
@@ -34,59 +56,80 @@ const QueryAnalyzerInfo: FC<Props> = ({ data, period }) => {
|
||||
return (
|
||||
<>
|
||||
<div className="vm-query-analyzer-info-header">
|
||||
<Button
|
||||
startIcon={<InfoIcon/>}
|
||||
variant="outlined"
|
||||
color="warning"
|
||||
onClick={handleOpenModal}
|
||||
>
|
||||
Show report info
|
||||
</Button>
|
||||
{period && (
|
||||
<>
|
||||
<div className="vm-query-analyzer-info-header__period">
|
||||
<TimelineIcon/> step: {period.step}
|
||||
</div>
|
||||
<div className="vm-query-analyzer-info-header__period">
|
||||
<ClockIcon/> {timeRange}
|
||||
</div>
|
||||
</>
|
||||
<h1 className="vm-query-analyzer-info-header__title">{title}</h1>
|
||||
{timeRange && (
|
||||
<div className="vm-query-analyzer-info-header__timerange">
|
||||
<ClockIcon/> {timeRange}
|
||||
</div>
|
||||
)}
|
||||
{period?.step && (
|
||||
<div className="vm-query-analyzer-info-header__timerange">
|
||||
<TimelineIcon/> step {period.step}
|
||||
</div>
|
||||
)}
|
||||
{(comment || !!table.length) && (
|
||||
<div className="vm-query-analyzer-info-header__info">
|
||||
<Button
|
||||
startIcon={<InfoIcon/>}
|
||||
variant="outlined"
|
||||
color="warning"
|
||||
onClick={handleOpenModal}
|
||||
>
|
||||
Show stats{comment && " & comments"}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{openModal && (
|
||||
<Modal
|
||||
title="Report info"
|
||||
title={title}
|
||||
onClose={handleCloseModal}
|
||||
>
|
||||
<div className="vm-query-analyzer-info">
|
||||
{comment && (
|
||||
<div className="vm-query-analyzer-info-item vm-query-analyzer-info-item_comment">
|
||||
<div className="vm-query-analyzer-info-item__title">Comment:</div>
|
||||
<div className="vm-query-analyzer-info-item__text">{comment}</div>
|
||||
<div className="vm-query-analyzer-info__modal">
|
||||
{!!table.length && (
|
||||
<div className="vm-query-analyzer-info-stats">
|
||||
<div className="vm-query-analyzer-info-comment-header">
|
||||
<InfoIcon/>
|
||||
Stats
|
||||
</div>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
{table.map(({ column }) => (
|
||||
<th key={column}>
|
||||
{column}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
{table[0]?.values.map((_, rowIndex) => (
|
||||
<tr key={rowIndex}>
|
||||
{table.map(({ values }, j) => (
|
||||
<td key={j}>
|
||||
{values[rowIndex]}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
{dataWithStats.map((d, i) => (
|
||||
<div
|
||||
className="vm-query-analyzer-info-item"
|
||||
key={i}
|
||||
>
|
||||
<div className="vm-query-analyzer-info-item__title">
|
||||
{dataWithStats.length > 1 ? `Query ${i + 1}:` : "Stats:"}
|
||||
</div>
|
||||
<div className="vm-query-analyzer-info-item__text">
|
||||
{Object.entries(d.stats || {}).map(([key, value]) => (
|
||||
<div key={key}>
|
||||
{key}: {value ?? "-"}
|
||||
</div>
|
||||
))}
|
||||
isPartial: {String(d.isPartial ?? "-")}
|
||||
|
||||
{comment && (
|
||||
<div className="vm-query-analyzer-info-comment">
|
||||
<div className="vm-query-analyzer-info-comment-header">
|
||||
<CommentIcon/>
|
||||
Comments
|
||||
</div>
|
||||
<div
|
||||
className="vm-query-analyzer-info-comment-body vm-markdown"
|
||||
dangerouslySetInnerHTML={{ __html: (marked(comment) as string) || comment }}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
<div className="vm-query-analyzer-info-type">
|
||||
{dataWithStats[0]?.vmui?.params ? "The report was created using vmui" : "The report was created manually"}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Modal>
|
||||
)}
|
||||
|
||||
@@ -1,47 +1,115 @@
|
||||
@use "src/styles/variables" as *;
|
||||
|
||||
.vm-query-analyzer-info-header {
|
||||
display: flex;
|
||||
gap: $padding-global;
|
||||
.vm-query-analyzer-info {
|
||||
|
||||
&__period {
|
||||
&-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
gap: $padding-small;
|
||||
border: $border-divider;
|
||||
border-radius: $border-radius-small;
|
||||
padding: 6px $padding-global;
|
||||
|
||||
svg {
|
||||
width: calc($font-size-small + 1px);
|
||||
color: $color-primary;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.vm-query-analyzer-info {
|
||||
display: grid;
|
||||
gap: $padding-large;
|
||||
min-width: 300px;
|
||||
|
||||
&-type {
|
||||
text-align: center;
|
||||
font-style: italic;
|
||||
color: $color-text-secondary;
|
||||
}
|
||||
|
||||
&-item {
|
||||
display: grid;
|
||||
padding-bottom: $padding-large;
|
||||
border-bottom: $border-divider;
|
||||
line-height: 130%;
|
||||
font-size: $font-size-small;
|
||||
background-color: $color-background-body;
|
||||
z-index: 1;
|
||||
|
||||
&__title {
|
||||
font-weight: bold;
|
||||
font-size: $font-size-large;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
&__text {
|
||||
white-space: pre-wrap;
|
||||
&__timerange {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: calc($padding-small / 2);
|
||||
border: $border-divider;
|
||||
border-radius: $border-radius-small;
|
||||
padding: calc($padding-small / 2) $padding-small;
|
||||
font-size: $font-size-small;
|
||||
|
||||
svg {
|
||||
width: calc($font-size-small + 1px);
|
||||
color: $color-primary;
|
||||
}
|
||||
}
|
||||
|
||||
&__info {
|
||||
margin-left: auto;
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&__modal {
|
||||
width: min(800px, 90vw);
|
||||
}
|
||||
|
||||
&-comment {
|
||||
position: relative;
|
||||
max-width: 800px;
|
||||
border-radius: $border-radius-medium;
|
||||
border: $border-divider;
|
||||
font-size: $font-size-small;
|
||||
|
||||
&-header {
|
||||
display: grid;
|
||||
grid-template-columns: 16px 1fr;
|
||||
align-items: center;
|
||||
gap: $padding-small;
|
||||
padding: $padding-small;
|
||||
border-bottom: $border-divider;
|
||||
background-color: $color-hover-black;
|
||||
font-weight: 500;
|
||||
z-index: 1;
|
||||
|
||||
svg {
|
||||
color: $color-primary;
|
||||
}
|
||||
}
|
||||
|
||||
&-body {
|
||||
padding: $padding-small;
|
||||
max-height: 60vh;
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
||||
|
||||
&-stats {
|
||||
border-radius: $border-radius-medium;
|
||||
border: $border-divider;
|
||||
font-size: $font-size-small;
|
||||
margin-bottom: $padding-global;
|
||||
overflow: hidden;
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
td, th {
|
||||
padding: $padding-small;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
tr {
|
||||
border-bottom: $border-divider;
|
||||
}
|
||||
|
||||
thead {
|
||||
th {
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
tbody {
|
||||
tr {
|
||||
transition: background-color 0.3s;
|
||||
|
||||
&:hover {
|
||||
background-color: $color-hover-black;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,8 +9,9 @@ import useBoolean from "../../hooks/useBoolean";
|
||||
import UploadJsonButtons from "../../components/UploadJsonButtons/UploadJsonButtons";
|
||||
import JsonForm from "./JsonForm/JsonForm";
|
||||
import "../TracePage/style.scss";
|
||||
import "./style.scss";
|
||||
import QueryAnalyzerView from "./QueryAnalyzerView/QueryAnalyzerView";
|
||||
import { InstantMetricResult, MetricResult, TracingData } from "../../api/types";
|
||||
import { InstantMetricResult, MetricResult, ReportMetaData, TracingData } from "../../api/types";
|
||||
import QueryAnalyzerInfo from "./QueryAnalyzerInfo/QueryAnalyzerInfo";
|
||||
import { TimeParams } from "../../types";
|
||||
import { dateFromSeconds, formatDateToUTC, humanizeSeconds } from "../../utils/time";
|
||||
@@ -21,15 +22,8 @@ export type DataAnalyzerType = {
|
||||
resultType: "vector" | "matrix";
|
||||
result: MetricResult[] | InstantMetricResult[]
|
||||
};
|
||||
stats?: {
|
||||
seriesFetched?: string;
|
||||
executionTimeMsec?: number
|
||||
};
|
||||
vmui?: {
|
||||
id: number;
|
||||
comment: string;
|
||||
params: Record<string, string>;
|
||||
};
|
||||
stats?: Record<string, string>;
|
||||
vmui?: ReportMetaData;
|
||||
status: string;
|
||||
trace?: TracingData;
|
||||
isPartial?: boolean;
|
||||
@@ -92,10 +86,12 @@ const QueryAnalyzer: FC = () => {
|
||||
setData(response);
|
||||
} else {
|
||||
setError("Invalid structure - JSON does not match the expected format");
|
||||
setData([]);
|
||||
}
|
||||
} catch (e) {
|
||||
if (e instanceof Error) {
|
||||
setError(`${e.name}: ${e.message}`);
|
||||
setData([]);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -129,33 +125,16 @@ const QueryAnalyzer: FC = () => {
|
||||
}, [files]);
|
||||
|
||||
return (
|
||||
<div className="vm-trace-page">
|
||||
<div className="vm-query-analyzer">
|
||||
{hasData && (
|
||||
<div className="vm-trace-page-header">
|
||||
<div className="vm-trace-page-header-errors">
|
||||
<QueryAnalyzerInfo
|
||||
data={data}
|
||||
period={period}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<UploadJsonButtons
|
||||
onOpenModal={handleOpenModal}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<div className="vm-trace-page-header-errors-item vm-trace-page-header-errors-item_margin-bottom">
|
||||
<Alert variant="error">{error}</Alert>
|
||||
<Button
|
||||
className="vm-trace-page-header-errors-item__close"
|
||||
startIcon={<CloseIcon/>}
|
||||
variant="text"
|
||||
color="error"
|
||||
onClick={handleCloseError}
|
||||
<div className="vm-query-analyzer-header">
|
||||
<QueryAnalyzerInfo
|
||||
data={data}
|
||||
period={period}
|
||||
/>
|
||||
<UploadJsonButtons
|
||||
onOpenModal={handleOpenModal}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
@@ -185,6 +164,19 @@ const QueryAnalyzer: FC = () => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<div className="vm-query-analyzer-error">
|
||||
<Alert variant="error">{error}</Alert>
|
||||
<Button
|
||||
className="vm-query-analyzer-error__close"
|
||||
startIcon={<CloseIcon/>}
|
||||
variant="text"
|
||||
color="error"
|
||||
onClick={handleCloseError}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{openModal && (
|
||||
<Modal
|
||||
title="Paste JSON"
|
||||
|
||||
29
app/vmui/packages/vmui/src/pages/QueryAnalyzer/style.scss
Normal file
29
app/vmui/packages/vmui/src/pages/QueryAnalyzer/style.scss
Normal file
@@ -0,0 +1,29 @@
|
||||
@use "src/styles/variables" as *;
|
||||
|
||||
.vm-query-analyzer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
padding: $padding-medium 0;
|
||||
}
|
||||
|
||||
&-header {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
gap: $padding-global;
|
||||
margin-bottom: $padding-global;
|
||||
}
|
||||
|
||||
&-error {
|
||||
position: relative;
|
||||
margin: $padding-global 0;
|
||||
|
||||
&__close {
|
||||
position: absolute;
|
||||
top: $padding-small;
|
||||
right: $padding-small;
|
||||
z-index: 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import { useAppState } from "../../../state/common/StateContext";
|
||||
import { useCustomPanelState } from "../../../state/customPanel/CustomPanelStateContext";
|
||||
import { isValidHttpUrl } from "../../../utils/url";
|
||||
import { getExportDataUrl } from "../../../api/query-range";
|
||||
import { parseLineToJSON } from "../../../utils/json";
|
||||
|
||||
interface FetchQueryParams {
|
||||
hideQuery?: number[];
|
||||
@@ -24,14 +25,6 @@ interface FetchQueryReturn {
|
||||
abortFetch: () => void
|
||||
}
|
||||
|
||||
const parseLineToJSON = (line: string): ExportMetricResult | null => {
|
||||
try {
|
||||
return JSON.parse(line);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export const useFetchExport = ({ hideQuery, showAllSeries }: FetchQueryParams): FetchQueryReturn => {
|
||||
const { query } = useQueryState();
|
||||
const { period } = useTimeState();
|
||||
@@ -62,7 +55,7 @@ export const useFetchExport = ({ hideQuery, showAllSeries }: FetchQueryParams):
|
||||
}
|
||||
}, [serverUrl, period, hideQuery, reduceMemUsage]);
|
||||
|
||||
const fetchData = useCallback(async ( { fetchUrl, stateSeriesLimits, showAllSeries }: {
|
||||
const fetchData = useCallback(async ({ fetchUrl, stateSeriesLimits, showAllSeries }: {
|
||||
fetchUrl: string[];
|
||||
stateSeriesLimits: SeriesLimits;
|
||||
showAllSeries?: boolean;
|
||||
@@ -99,12 +92,12 @@ export const useFetchExport = ({ hideQuery, showAllSeries }: FetchQueryParams):
|
||||
const lines = text.split("\n").filter(line => line);
|
||||
const lineLimited = lines.slice(0, freeTempSize).sort();
|
||||
lineLimited.forEach((line: string) => {
|
||||
const jsonLine = parseLineToJSON(line);
|
||||
const jsonLine = parseLineToJSON(line) as (ExportMetricResult | null);
|
||||
if (!jsonLine) return;
|
||||
tempData.push({
|
||||
group: counter,
|
||||
metric: jsonLine.metric,
|
||||
values: jsonLine.values.map((value, index) => [(jsonLine.timestamps[index]/1000), value]),
|
||||
values: jsonLine.values.map((value, index) => [(jsonLine.timestamps[index] / 1000), value]),
|
||||
} as MetricBase);
|
||||
});
|
||||
totalLength += lines.length;
|
||||
@@ -119,7 +112,7 @@ export const useFetchExport = ({ hideQuery, showAllSeries }: FetchQueryParams):
|
||||
} catch (e) {
|
||||
setIsLoading(false);
|
||||
if (e instanceof Error && e.name !== "AbortError") {
|
||||
setError(error);
|
||||
setError(String(e));
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ import { DisplayType } from "../../types";
|
||||
import Hyperlink from "../../components/Main/Hyperlink/Hyperlink";
|
||||
import { CloseIcon } from "../../components/Main/Icons";
|
||||
import Button from "../../components/Main/Button/Button";
|
||||
import DownloadReport, { ReportType } from "../CustomPanel/DownloadReport/DownloadReport";
|
||||
|
||||
const RawSamplesLink = () => (
|
||||
<Hyperlink
|
||||
@@ -65,6 +66,7 @@ const RawQueryPage: FC = () => {
|
||||
queryErrors,
|
||||
setQueryErrors,
|
||||
abortFetch,
|
||||
fetchUrl,
|
||||
} = useFetchExport({ hideQuery, showAllSeries });
|
||||
|
||||
const controlsRef = useRef<HTMLDivElement>(null);
|
||||
@@ -106,12 +108,22 @@ const RawQueryPage: FC = () => {
|
||||
{showPageDescription && (
|
||||
<Alert variant="info">
|
||||
<div className="vm-explore-metrics-header-description">
|
||||
<p>
|
||||
This page provides a dedicated view for querying and displaying <RawSamplesLink/> from VictoriaMetrics.
|
||||
It expects only <TimeSeriesSelectorLink/> as a query argument.
|
||||
Users often assume that the <QueryDataLink/> returns data exactly as stored,
|
||||
but data samples and timestamps may be modified by the API.
|
||||
</p>
|
||||
<ul>
|
||||
<li>
|
||||
This page provides a dedicated view for querying and displaying <RawSamplesLink/> from VictoriaMetrics.
|
||||
</li>
|
||||
<li>
|
||||
It expects only <TimeSeriesSelectorLink/> as a query argument.
|
||||
</li>
|
||||
<li>
|
||||
Deduplication can only be disabled if it was previously enabled on the server
|
||||
(<code>-dedup.minScrapeInterval</code>).
|
||||
</li>
|
||||
<li>
|
||||
Users often assume that the <QueryDataLink/> returns data exactly as stored,
|
||||
but data samples and timestamps may be modified by the API.
|
||||
</li>
|
||||
</ul>
|
||||
<Button
|
||||
variant="text"
|
||||
size="small"
|
||||
@@ -146,6 +158,12 @@ const RawQueryPage: FC = () => {
|
||||
<div className="vm-custom-panel-body-header__tabs">
|
||||
<DisplayTypeSwitch tabFilter={(tab) => (tab.value !== DisplayType.table)}/>
|
||||
</div>
|
||||
{data && (
|
||||
<DownloadReport
|
||||
fetchUrl={fetchUrl}
|
||||
reportType={ReportType.RAW_DATA}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<CustomPanelTabs
|
||||
graphData={data}
|
||||
|
||||
@@ -61,7 +61,8 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
justify-content: flex-end;
|
||||
min-height: calc(($vh * 50) - var(--scrollbar-height));
|
||||
|
||||
&__text {
|
||||
margin-bottom: $padding-global;
|
||||
|
||||
139
app/vmui/packages/vmui/src/styles/components/markdown.scss
Normal file
139
app/vmui/packages/vmui/src/styles/components/markdown.scss
Normal file
@@ -0,0 +1,139 @@
|
||||
@use "src/styles/variables" as *;
|
||||
|
||||
.vm-markdown {
|
||||
display: block;
|
||||
line-height: 1.5;
|
||||
|
||||
pre,
|
||||
code {
|
||||
font-size: 1em;
|
||||
font-family: $font-family-monospace
|
||||
}
|
||||
|
||||
pre {
|
||||
padding: .5em;
|
||||
line-height: 1.25;
|
||||
overflow-x: scroll;
|
||||
}
|
||||
|
||||
a,
|
||||
a:visited {
|
||||
color: $color-primary;
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
body {
|
||||
line-height: 1.85;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 1em;
|
||||
margin-bottom: 1.3em;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
margin: .5em 0 0.25em;
|
||||
font-weight: inherit;
|
||||
line-height: 1.42;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 1.8em;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.6em;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.4em;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
h5 {
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
h6 {
|
||||
font-size: 0.8em;
|
||||
}
|
||||
|
||||
small {
|
||||
font-size: 0.8em;
|
||||
}
|
||||
|
||||
img,
|
||||
canvas,
|
||||
iframe,
|
||||
video,
|
||||
svg,
|
||||
select,
|
||||
textarea {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
pre {
|
||||
background-color: $color-hover-black;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
border-left: 3px solid rgba($color-black, 0.2);
|
||||
padding-left: 1em;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
ul, ol {
|
||||
margin: 0.3em 0;
|
||||
list-style-position: inside;
|
||||
|
||||
li {
|
||||
margin-bottom: 0.3em;
|
||||
|
||||
ul, ol {
|
||||
margin-left: 1em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
input[type="checkbox"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
th,
|
||||
td {
|
||||
padding: 0.2em 0.4em;
|
||||
}
|
||||
|
||||
hr {
|
||||
border-top: $border-divider;
|
||||
}
|
||||
|
||||
strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
em {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
del {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
code:not(pre code) {
|
||||
display: inline;
|
||||
vertical-align: middle;
|
||||
background: $color-hover-black;
|
||||
border-radius: $border-radius-small;
|
||||
border: $border-divider;
|
||||
tab-size: 4;
|
||||
font-variant-ligatures: none;
|
||||
padding: 0.12em 0.4em;
|
||||
font-size: 0.9em;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
@@ -57,14 +57,51 @@ input[type=number]::-webkit-outer-spin-button {
|
||||
&-content {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
align-items: center;
|
||||
gap: $padding-large;
|
||||
align-items: flex-start;
|
||||
line-height: 24px;
|
||||
|
||||
&__close {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
padding: 4px;
|
||||
color: inherit;
|
||||
opacity: 0.8;
|
||||
border-radius: $border-radius-small;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: $color-hover-black;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-details {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 0;
|
||||
|
||||
&__title {
|
||||
font-weight: 600;
|
||||
padding-bottom: calc($padding-small/2);
|
||||
margin-bottom: calc($padding-small/2);
|
||||
border-bottom: 1px solid inherit;
|
||||
}
|
||||
|
||||
&__msg {
|
||||
display: grid;
|
||||
line-height: 150%;
|
||||
white-space: pre-line;
|
||||
text-wrap: pretty;
|
||||
overflow-wrap: anywhere;
|
||||
|
||||
ul, ol {
|
||||
list-style-position: inside;
|
||||
margin-bottom: $padding-small;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -50,10 +50,6 @@ button {
|
||||
background: none;
|
||||
}
|
||||
|
||||
strong {
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
input[type='file'] {
|
||||
opacity: 0;
|
||||
cursor: pointer;
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
@forward "./components/table";
|
||||
@forward "./components/link";
|
||||
@forward "./components/dynamic-number";
|
||||
@forward "./components/markdown";
|
||||
|
||||
:root {
|
||||
/* base palette */
|
||||
|
||||
7
app/vmui/packages/vmui/src/utils/json.ts
Normal file
7
app/vmui/packages/vmui/src/utils/json.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export const parseLineToJSON = (line: string) => {
|
||||
try {
|
||||
return JSON.parse(line);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
@@ -25,3 +25,13 @@ export const isEqualURLSearchParams = (params1: URLSearchParams, params2: URLSea
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
export const getApiEndpoint = (url: string): string | null => {
|
||||
try {
|
||||
const match = url.match(/\/api\/v1\/[^?]+/);
|
||||
return match ? match[0] : null;
|
||||
} catch (error) {
|
||||
console.error("Invalid URL:", error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -15,10 +15,11 @@ import (
|
||||
|
||||
// Regular expressions for runtime information to extract from the app logs.
|
||||
var (
|
||||
storageDataPathRE = regexp.MustCompile(`successfully opened storage "(.*)"`)
|
||||
httpListenAddrRE = regexp.MustCompile(`started server at http://(.*:\d{1,5})/`)
|
||||
vminsertAddrRE = regexp.MustCompile(`accepting vminsert conns at (.*:\d{1,5})$`)
|
||||
vmselectAddrRE = regexp.MustCompile(`accepting vmselect conns at (.*:\d{1,5})$`)
|
||||
storageDataPathRE = regexp.MustCompile(`successfully opened storage "(.*)"`)
|
||||
httpListenAddrRE = regexp.MustCompile(`started server at http://(.*:\d{1,5})/`)
|
||||
vminsertAddrRE = regexp.MustCompile(`accepting vminsert conns at (.*:\d{1,5})$`)
|
||||
vminsertClusterNativeAddrRE = regexp.MustCompile(`started TCP clusternative server at "(.*:\d{1,5})"`)
|
||||
vmselectAddrRE = regexp.MustCompile(`accepting vmselect conns at (.*:\d{1,5})$`)
|
||||
)
|
||||
|
||||
// app represents an instance of some VictoriaMetrics server (such as vmstorage,
|
||||
|
||||
@@ -128,7 +128,7 @@ func (app *ServesMetrics) GetMetric(t *testing.T, metricName string) float64 {
|
||||
return res
|
||||
}
|
||||
}
|
||||
t.Fatalf("metic not found: %s", metricName)
|
||||
t.Fatalf("metric not found: %s", metricName)
|
||||
return 0
|
||||
}
|
||||
|
||||
|
||||
@@ -133,7 +133,7 @@ func NewSample(t *testing.T, timeStr string, value float64) *Sample {
|
||||
if err != nil {
|
||||
t.Fatalf("could not parse RFC3339 time %q: %v", timeStr, err)
|
||||
}
|
||||
return &Sample{parsedTime.Unix(), value}
|
||||
return &Sample{parsedTime.UnixMilli(), value}
|
||||
}
|
||||
|
||||
// UnmarshalJSON populates the sample fields from a JSON string.
|
||||
@@ -149,7 +149,7 @@ func (s *Sample) UnmarshalJSON(b []byte) error {
|
||||
if got, want := len(raw), 2; got != want {
|
||||
return fmt.Errorf("unexpected number of fields: got %d, want %d (raw sample: %s)", got, want, string(b))
|
||||
}
|
||||
s.Timestamp = int64(ts)
|
||||
s.Timestamp = int64(ts * 1000)
|
||||
var err error
|
||||
s.Value, err = strconv.ParseFloat(v, 64)
|
||||
if err != nil {
|
||||
|
||||
@@ -134,8 +134,21 @@ func (c *vmcluster) ForceFlush(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// MustStartDefaultCluster is a typical cluster configuration suitable for most
|
||||
// tests.
|
||||
// MustStartDefaultCluster starts a typical cluster configuration with default
|
||||
// flags.
|
||||
func (tc *TestCase) MustStartDefaultCluster() PrometheusWriteQuerier {
|
||||
tc.t.Helper()
|
||||
|
||||
return tc.MustStartCluster(&ClusterOptions{
|
||||
Vmstorage1Instance: "vmstorage1",
|
||||
Vmstorage2Instance: "vmstorage2",
|
||||
VminsertInstance: "vminsert",
|
||||
VmselectInstance: "vmselect",
|
||||
})
|
||||
}
|
||||
|
||||
// ClusterOptions holds the params for simple cluster configuration suitable for
|
||||
// most tests.
|
||||
//
|
||||
// The cluster consists of two vmstorages, one vminsert and one vmselect, no
|
||||
// data replication.
|
||||
@@ -145,23 +158,42 @@ func (c *vmcluster) ForceFlush(t *testing.T) {
|
||||
// vmselect) but instead just need a typical cluster configuration to verify
|
||||
// some business logic (such as API surface, or MetricsQL). Such cluster
|
||||
// tests usually come paired with corresponding vmsingle tests.
|
||||
func (tc *TestCase) MustStartDefaultCluster() PrometheusWriteQuerier {
|
||||
type ClusterOptions struct {
|
||||
Vmstorage1Instance string
|
||||
Vmstorage1Flags []string
|
||||
Vmstorage2Instance string
|
||||
Vmstorage2Flags []string
|
||||
VminsertInstance string
|
||||
VminsertFlags []string
|
||||
VmselectInstance string
|
||||
VmselectFlags []string
|
||||
}
|
||||
|
||||
// MustStartCluster starts a typical cluster configuration with custom flags.
|
||||
func (tc *TestCase) MustStartCluster(opts *ClusterOptions) PrometheusWriteQuerier {
|
||||
tc.t.Helper()
|
||||
|
||||
vmstorage1 := tc.MustStartVmstorage("vmstorage-1", []string{
|
||||
"-storageDataPath=" + tc.Dir() + "/vmstorage-1",
|
||||
opts.Vmstorage1Flags = append(opts.Vmstorage1Flags, []string{
|
||||
"-storageDataPath=" + tc.Dir() + "/" + opts.Vmstorage1Instance,
|
||||
"-retentionPeriod=100y",
|
||||
})
|
||||
vmstorage2 := tc.MustStartVmstorage("vmstorage-2", []string{
|
||||
"-storageDataPath=" + tc.Dir() + "/vmstorage-2",
|
||||
}...)
|
||||
vmstorage1 := tc.MustStartVmstorage(opts.Vmstorage1Instance, opts.Vmstorage1Flags)
|
||||
|
||||
opts.Vmstorage2Flags = append(opts.Vmstorage2Flags, []string{
|
||||
"-storageDataPath=" + tc.Dir() + "/" + opts.Vmstorage2Instance,
|
||||
"-retentionPeriod=100y",
|
||||
})
|
||||
vminsert := tc.MustStartVminsert("vminsert", []string{
|
||||
}...)
|
||||
vmstorage2 := tc.MustStartVmstorage(opts.Vmstorage2Instance, opts.Vmstorage2Flags)
|
||||
|
||||
opts.VminsertFlags = append(opts.VminsertFlags, []string{
|
||||
"-storageNode=" + vmstorage1.VminsertAddr() + "," + vmstorage2.VminsertAddr(),
|
||||
})
|
||||
vmselect := tc.MustStartVmselect("vmselect", []string{
|
||||
}...)
|
||||
vminsert := tc.MustStartVminsert(opts.VminsertInstance, opts.VminsertFlags)
|
||||
|
||||
opts.VmselectFlags = append(opts.VmselectFlags, []string{
|
||||
"-storageNode=" + vmstorage1.VmselectAddr() + "," + vmstorage2.VmselectAddr(),
|
||||
})
|
||||
}...)
|
||||
vmselect := tc.MustStartVmselect(opts.VmselectInstance, opts.VmselectFlags)
|
||||
|
||||
return &vmcluster{vminsert, vmselect, []*Vmstorage{vmstorage1, vmstorage2}}
|
||||
}
|
||||
|
||||
@@ -3,9 +3,10 @@ package tests
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/apptest"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/google/go-cmp/cmp/cmpopts"
|
||||
|
||||
at "github.com/VictoriaMetrics/VictoriaMetrics/apptest"
|
||||
)
|
||||
|
||||
// Data used in examples in
|
||||
@@ -30,7 +31,7 @@ var docData = []string{
|
||||
// TestSingleKeyConceptsQuery verifies cases from https://docs.victoriametrics.com/keyconcepts/#query-data
|
||||
// for vm-single.
|
||||
func TestSingleKeyConceptsQuery(t *testing.T) {
|
||||
tc := apptest.NewTestCase(t)
|
||||
tc := at.NewTestCase(t)
|
||||
defer tc.Stop()
|
||||
|
||||
sut := tc.MustStartDefaultVmsingle()
|
||||
@@ -41,7 +42,7 @@ func TestSingleKeyConceptsQuery(t *testing.T) {
|
||||
// TestClusterKeyConceptsQueryData verifies cases from https://docs.victoriametrics.com/keyconcepts/#query-data
|
||||
// for vm-cluster.
|
||||
func TestClusterKeyConceptsQueryData(t *testing.T) {
|
||||
tc := apptest.NewTestCase(t)
|
||||
tc := at.NewTestCase(t)
|
||||
defer tc.Stop()
|
||||
|
||||
sut := tc.MustStartDefaultCluster()
|
||||
@@ -50,10 +51,10 @@ func TestClusterKeyConceptsQueryData(t *testing.T) {
|
||||
}
|
||||
|
||||
// testClusterKeyConceptsQuery verifies cases from https://docs.victoriametrics.com/keyconcepts/#query-data
|
||||
func testKeyConceptsQueryData(t *testing.T, sut apptest.PrometheusWriteQuerier) {
|
||||
func testKeyConceptsQueryData(t *testing.T, sut at.PrometheusWriteQuerier) {
|
||||
|
||||
// Insert example data from documentation.
|
||||
sut.PrometheusAPIV1ImportPrometheus(t, docData, apptest.QueryOpts{})
|
||||
sut.PrometheusAPIV1ImportPrometheus(t, docData, at.QueryOpts{})
|
||||
sut.ForceFlush(t)
|
||||
|
||||
testInstantQuery(t, sut)
|
||||
@@ -64,14 +65,14 @@ func testKeyConceptsQueryData(t *testing.T, sut apptest.PrometheusWriteQuerier)
|
||||
// testInstantQuery verifies the statements made in the `Instant query` section
|
||||
// of the VictoriaMetrics documentation. See:
|
||||
// https://docs.victoriametrics.com/keyconcepts/#instant-query
|
||||
func testInstantQuery(t *testing.T, q apptest.PrometheusQuerier) {
|
||||
func testInstantQuery(t *testing.T, q at.PrometheusQuerier) {
|
||||
// Get the value of the foo_bar time series at 2022-05-10T08:03:00Z with the
|
||||
// step of 5m and timeout 5s. There is no sample at exactly this timestamp.
|
||||
// Therefore, VictoriaMetrics will search for the nearest sample within the
|
||||
// [time-5m..time] interval.
|
||||
got := q.PrometheusAPIV1Query(t, "foo_bar", apptest.QueryOpts{Time: "2022-05-10T08:03:00.000Z", Step: "5m"})
|
||||
want := apptest.NewPrometheusAPIV1QueryResponse(t, `{"data":{"result":[{"metric":{"__name__":"foo_bar"},"value":[1652169780,"3"]}]}}`)
|
||||
opt := cmpopts.IgnoreFields(apptest.PrometheusAPIV1QueryResponse{}, "Status", "Data.ResultType")
|
||||
got := q.PrometheusAPIV1Query(t, "foo_bar", at.QueryOpts{Time: "2022-05-10T08:03:00.000Z", Step: "5m"})
|
||||
want := at.NewPrometheusAPIV1QueryResponse(t, `{"data":{"result":[{"metric":{"__name__":"foo_bar"},"value":[1652169780,"3"]}]}}`)
|
||||
opt := cmpopts.IgnoreFields(at.PrometheusAPIV1QueryResponse{}, "Status", "Data.ResultType")
|
||||
if diff := cmp.Diff(want, got, opt); diff != "" {
|
||||
t.Errorf("unexpected response (-want, +got):\n%s", diff)
|
||||
}
|
||||
@@ -81,7 +82,7 @@ func testInstantQuery(t *testing.T, q apptest.PrometheusQuerier) {
|
||||
// Therefore, VictoriaMetrics will search for the nearest sample within the
|
||||
// [time-1m..time] interval. Since the nearest sample is 2m away and the
|
||||
// step is 1m, then the VictoriaMetrics must return empty response.
|
||||
got = q.PrometheusAPIV1Query(t, "foo_bar", apptest.QueryOpts{Time: "2022-05-10T08:18:00.000Z", Step: "1m"})
|
||||
got = q.PrometheusAPIV1Query(t, "foo_bar", at.QueryOpts{Time: "2022-05-10T08:18:00.000Z", Step: "1m"})
|
||||
if len(got.Data.Result) > 0 {
|
||||
t.Errorf("unexpected response: got non-empty result, want empty result:\n%v", got)
|
||||
}
|
||||
@@ -90,14 +91,14 @@ func testInstantQuery(t *testing.T, q apptest.PrometheusQuerier) {
|
||||
// testRangeQuery verifies the statements made in the `Range query` section of
|
||||
// the VictoriaMetrics documentation. See:
|
||||
// https://docs.victoriametrics.com/keyconcepts/#range-query
|
||||
func testRangeQuery(t *testing.T, q apptest.PrometheusQuerier) {
|
||||
f := func(start, end, step string, wantSamples []*apptest.Sample) {
|
||||
func testRangeQuery(t *testing.T, q at.PrometheusQuerier) {
|
||||
f := func(start, end, step string, wantSamples []*at.Sample) {
|
||||
t.Helper()
|
||||
|
||||
got := q.PrometheusAPIV1QueryRange(t, "foo_bar", apptest.QueryOpts{Start: start, End: end, Step: step})
|
||||
want := apptest.NewPrometheusAPIV1QueryResponse(t, `{"data": {"result": [{"metric": {"__name__": "foo_bar"}, "values": []}]}}`)
|
||||
got := q.PrometheusAPIV1QueryRange(t, "foo_bar", at.QueryOpts{Start: start, End: end, Step: step})
|
||||
want := at.NewPrometheusAPIV1QueryResponse(t, `{"data": {"result": [{"metric": {"__name__": "foo_bar"}, "values": []}]}}`)
|
||||
want.Data.Result[0].Samples = wantSamples
|
||||
opt := cmpopts.IgnoreFields(apptest.PrometheusAPIV1QueryResponse{}, "Status", "Data.ResultType")
|
||||
opt := cmpopts.IgnoreFields(at.PrometheusAPIV1QueryResponse{}, "Status", "Data.ResultType")
|
||||
if diff := cmp.Diff(want, got, opt); diff != "" {
|
||||
t.Errorf("unexpected response (-want, +got):\n%s", diff)
|
||||
}
|
||||
@@ -106,66 +107,68 @@ func testRangeQuery(t *testing.T, q apptest.PrometheusQuerier) {
|
||||
// Verify the statement that the query result for
|
||||
// [2022-05-10T07:59:00Z..2022-05-10T08:17:00Z] time range and 1m step will
|
||||
// contain 17 points.
|
||||
f("2022-05-10T07:59:00.000Z", "2022-05-10T08:17:00.000Z", "1m", []*apptest.Sample{
|
||||
f("2022-05-10T07:59:00.000Z", "2022-05-10T08:17:00.000Z", "1m", []*at.Sample{
|
||||
// Sample for 2022-05-10T07:59:00Z is missing because the time series has
|
||||
// samples only starting from 8:00.
|
||||
apptest.NewSample(t, "2022-05-10T08:00:00Z", 1),
|
||||
apptest.NewSample(t, "2022-05-10T08:01:00Z", 2),
|
||||
apptest.NewSample(t, "2022-05-10T08:02:00Z", 3),
|
||||
apptest.NewSample(t, "2022-05-10T08:03:00Z", 3),
|
||||
apptest.NewSample(t, "2022-05-10T08:04:00Z", 5),
|
||||
apptest.NewSample(t, "2022-05-10T08:05:00Z", 5),
|
||||
apptest.NewSample(t, "2022-05-10T08:06:00Z", 5.5),
|
||||
apptest.NewSample(t, "2022-05-10T08:07:00Z", 5.5),
|
||||
apptest.NewSample(t, "2022-05-10T08:08:00Z", 4),
|
||||
apptest.NewSample(t, "2022-05-10T08:09:00Z", 4),
|
||||
at.NewSample(t, "2022-05-10T08:00:00Z", 1),
|
||||
at.NewSample(t, "2022-05-10T08:01:00Z", 2),
|
||||
at.NewSample(t, "2022-05-10T08:02:00Z", 3),
|
||||
at.NewSample(t, "2022-05-10T08:03:00Z", 3),
|
||||
at.NewSample(t, "2022-05-10T08:04:00Z", 5),
|
||||
at.NewSample(t, "2022-05-10T08:05:00Z", 5),
|
||||
at.NewSample(t, "2022-05-10T08:06:00Z", 5.5),
|
||||
at.NewSample(t, "2022-05-10T08:07:00Z", 5.5),
|
||||
at.NewSample(t, "2022-05-10T08:08:00Z", 4),
|
||||
at.NewSample(t, "2022-05-10T08:09:00Z", 4),
|
||||
// Sample for 2022-05-10T08:10:00Z is missing because there is no sample
|
||||
// within the [8:10 - 1m .. 8:10] interval.
|
||||
apptest.NewSample(t, "2022-05-10T08:11:00Z", 3.5),
|
||||
apptest.NewSample(t, "2022-05-10T08:12:00Z", 3.25),
|
||||
apptest.NewSample(t, "2022-05-10T08:13:00Z", 3),
|
||||
apptest.NewSample(t, "2022-05-10T08:14:00Z", 2),
|
||||
apptest.NewSample(t, "2022-05-10T08:15:00Z", 1),
|
||||
apptest.NewSample(t, "2022-05-10T08:16:00Z", 4),
|
||||
apptest.NewSample(t, "2022-05-10T08:17:00Z", 4),
|
||||
at.NewSample(t, "2022-05-10T08:11:00Z", 3.5),
|
||||
at.NewSample(t, "2022-05-10T08:12:00Z", 3.25),
|
||||
at.NewSample(t, "2022-05-10T08:13:00Z", 3),
|
||||
at.NewSample(t, "2022-05-10T08:14:00Z", 2),
|
||||
at.NewSample(t, "2022-05-10T08:15:00Z", 1),
|
||||
at.NewSample(t, "2022-05-10T08:16:00Z", 4),
|
||||
at.NewSample(t, "2022-05-10T08:17:00Z", 4),
|
||||
})
|
||||
|
||||
// Verify the statement that a query is executed at start, start+step,
|
||||
// start+2*step, …, step+N*step timestamps, where N is the whole number
|
||||
// of steps that fit between start and end.
|
||||
f("2022-05-10T08:00:01.000Z", "2022-05-10T08:02:00.000Z", "1m", []*apptest.Sample{
|
||||
apptest.NewSample(t, "2022-05-10T08:00:01Z", 1),
|
||||
apptest.NewSample(t, "2022-05-10T08:01:01Z", 2),
|
||||
f("2022-05-10T08:00:01.000Z", "2022-05-10T08:02:00.000Z", "1m", []*at.Sample{
|
||||
at.NewSample(t, "2022-05-10T08:00:01Z", 1),
|
||||
at.NewSample(t, "2022-05-10T08:01:01Z", 2),
|
||||
})
|
||||
|
||||
// Verify the statement that a query is executed at start, start+step,
|
||||
// start+2*step, …, end timestamps, when end = start + N*step.
|
||||
f("2022-05-10T08:00:00.000Z", "2022-05-10T08:02:00.000Z", "1m", []*apptest.Sample{
|
||||
apptest.NewSample(t, "2022-05-10T08:00:00Z", 1),
|
||||
apptest.NewSample(t, "2022-05-10T08:01:00Z", 2),
|
||||
apptest.NewSample(t, "2022-05-10T08:02:00Z", 3),
|
||||
f("2022-05-10T08:00:00.000Z", "2022-05-10T08:02:00.000Z", "1m", []*at.Sample{
|
||||
at.NewSample(t, "2022-05-10T08:00:00Z", 1),
|
||||
at.NewSample(t, "2022-05-10T08:01:00Z", 2),
|
||||
at.NewSample(t, "2022-05-10T08:02:00Z", 3),
|
||||
})
|
||||
|
||||
// If the step isn’t set, then it defaults to 5m (5 minutes).
|
||||
f("2022-05-10T07:59:00.000Z", "2022-05-10T08:17:00.000Z", "", []*apptest.Sample{
|
||||
f("2022-05-10T07:59:00.000Z", "2022-05-10T08:17:00.000Z", "", []*at.Sample{
|
||||
// Sample for 2022-05-10T07:59:00Z is missing because the time series has
|
||||
// samples only starting from 8:00.
|
||||
apptest.NewSample(t, "2022-05-10T08:04:00Z", 5),
|
||||
apptest.NewSample(t, "2022-05-10T08:09:00Z", 4),
|
||||
apptest.NewSample(t, "2022-05-10T08:14:00Z", 2),
|
||||
at.NewSample(t, "2022-05-10T08:04:00Z", 5),
|
||||
at.NewSample(t, "2022-05-10T08:09:00Z", 4),
|
||||
at.NewSample(t, "2022-05-10T08:14:00Z", 2),
|
||||
})
|
||||
}
|
||||
|
||||
// testRangeQueryIsEquivalentToManyInstantQueries verifies the statement made in
|
||||
// the `Range query` section of the VictoriaMetrics documentation that a range
|
||||
// query is actually an instant query executed 1 + (start-end)/step times on the
|
||||
// time range from start to end. See:
|
||||
// https://docs.victoriametrics.com/keyconcepts/#range-query
|
||||
func testRangeQueryIsEquivalentToManyInstantQueries(t *testing.T, q apptest.PrometheusQuerier) {
|
||||
f := func(timestamp string, want *apptest.Sample) {
|
||||
// time range from start to end. The only difference is that instant queries
|
||||
// will not procude ephemeral points.
|
||||
//
|
||||
// See: https://docs.victoriametrics.com/keyconcepts/#range-query
|
||||
func testRangeQueryIsEquivalentToManyInstantQueries(t *testing.T, q at.PrometheusQuerier) {
|
||||
f := func(timestamp string, want *at.Sample) {
|
||||
t.Helper()
|
||||
|
||||
gotInstant := q.PrometheusAPIV1Query(t, "foo_bar", apptest.QueryOpts{Time: timestamp, Step: "1m"})
|
||||
gotInstant := q.PrometheusAPIV1Query(t, "foo_bar", at.QueryOpts{Time: timestamp, Step: "1m"})
|
||||
if want == nil {
|
||||
if got, want := len(gotInstant.Data.Result), 0; got != want {
|
||||
t.Errorf("unexpected instant result size: got %d, want %d", got, want)
|
||||
@@ -178,7 +181,7 @@ func testRangeQueryIsEquivalentToManyInstantQueries(t *testing.T, q apptest.Prom
|
||||
}
|
||||
}
|
||||
|
||||
rangeRes := q.PrometheusAPIV1QueryRange(t, "foo_bar", apptest.QueryOpts{
|
||||
rangeRes := q.PrometheusAPIV1QueryRange(t, "foo_bar", at.QueryOpts{
|
||||
Start: "2022-05-10T07:59:00.000Z",
|
||||
End: "2022-05-10T08:17:00.000Z",
|
||||
Step: "1m",
|
||||
@@ -189,13 +192,13 @@ func testRangeQueryIsEquivalentToManyInstantQueries(t *testing.T, q apptest.Prom
|
||||
f("2022-05-10T08:00:00.000Z", rangeSamples[0])
|
||||
f("2022-05-10T08:01:00.000Z", rangeSamples[1])
|
||||
f("2022-05-10T08:02:00.000Z", rangeSamples[2])
|
||||
f("2022-05-10T08:03:00.000Z", rangeSamples[3])
|
||||
f("2022-05-10T08:03:00.000Z", nil)
|
||||
f("2022-05-10T08:04:00.000Z", rangeSamples[4])
|
||||
f("2022-05-10T08:05:00.000Z", rangeSamples[5])
|
||||
f("2022-05-10T08:05:00.000Z", nil)
|
||||
f("2022-05-10T08:06:00.000Z", rangeSamples[6])
|
||||
f("2022-05-10T08:07:00.000Z", rangeSamples[7])
|
||||
f("2022-05-10T08:08:00.000Z", rangeSamples[8])
|
||||
f("2022-05-10T08:09:00.000Z", rangeSamples[9])
|
||||
f("2022-05-10T08:09:00.000Z", nil)
|
||||
f("2022-05-10T08:10:00.000Z", nil)
|
||||
f("2022-05-10T08:11:00.000Z", rangeSamples[10])
|
||||
f("2022-05-10T08:12:00.000Z", rangeSamples[11])
|
||||
@@ -203,5 +206,164 @@ func testRangeQueryIsEquivalentToManyInstantQueries(t *testing.T, q apptest.Prom
|
||||
f("2022-05-10T08:14:00.000Z", rangeSamples[13])
|
||||
f("2022-05-10T08:15:00.000Z", rangeSamples[14])
|
||||
f("2022-05-10T08:16:00.000Z", rangeSamples[15])
|
||||
f("2022-05-10T08:17:00.000Z", rangeSamples[16])
|
||||
f("2022-05-10T08:17:00.000Z", nil)
|
||||
}
|
||||
|
||||
func TestSingleMillisecondPrecisionInInstantQueries(t *testing.T) {
|
||||
tc := at.NewTestCase(t)
|
||||
defer tc.Stop()
|
||||
|
||||
sut := tc.MustStartDefaultVmsingle()
|
||||
|
||||
testMillisecondPrecisionInInstantQueries(tc, sut)
|
||||
}
|
||||
|
||||
func TestClusterMillisecondPrecisionInInstantQueries(t *testing.T) {
|
||||
tc := at.NewTestCase(t)
|
||||
defer tc.Stop()
|
||||
|
||||
sut := tc.MustStartDefaultCluster()
|
||||
|
||||
testMillisecondPrecisionInInstantQueries(tc, sut)
|
||||
}
|
||||
|
||||
func testMillisecondPrecisionInInstantQueries(tc *at.TestCase, sut at.PrometheusWriteQuerier) {
|
||||
t := tc.T()
|
||||
|
||||
type opts struct {
|
||||
query string
|
||||
qtime string
|
||||
step string
|
||||
wantMetric map[string]string
|
||||
wantSample *at.Sample
|
||||
wantSamples []*at.Sample
|
||||
}
|
||||
f := func(sut at.PrometheusQuerier, opts *opts) {
|
||||
t.Helper()
|
||||
wantResult := []*at.QueryResult{}
|
||||
if opts.wantMetric != nil && (opts.wantSample != nil || len(opts.wantSamples) > 0) {
|
||||
wantResult = append(wantResult, &at.QueryResult{
|
||||
Metric: opts.wantMetric,
|
||||
Sample: opts.wantSample,
|
||||
Samples: opts.wantSamples,
|
||||
})
|
||||
}
|
||||
tc.Assert(&at.AssertOptions{
|
||||
Msg: "unexpected /api/v1/query response",
|
||||
Got: func() any {
|
||||
return sut.PrometheusAPIV1Query(t, opts.query, at.QueryOpts{
|
||||
Time: opts.qtime,
|
||||
Step: opts.step,
|
||||
})
|
||||
},
|
||||
Want: &at.PrometheusAPIV1QueryResponse{Data: &at.QueryData{Result: wantResult}},
|
||||
CmpOpts: []cmp.Option{
|
||||
cmpopts.IgnoreFields(at.PrometheusAPIV1QueryResponse{}, "Status", "Data.ResultType"),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
sut.PrometheusAPIV1ImportPrometheus(t, []string{
|
||||
`series1{label="foo"} 10 1707123456700`, // 2024-02-05T08:57:36.700Z
|
||||
`series1{label="foo"} 20 1707123456800`, // 2024-02-05T08:57:36.800Z
|
||||
}, at.QueryOpts{})
|
||||
sut.ForceFlush(t)
|
||||
|
||||
// Verify that both points were created correctly. Fetch both points by
|
||||
// passing a duration into the instant query: the difference between the two
|
||||
// timestamps, plus 1ms (to include both points), so 101ms:
|
||||
f(sut, &opts{
|
||||
query: "series1[101ms]",
|
||||
qtime: "1707123456800", // 2024-02-05T08:57:36.800Z
|
||||
wantMetric: map[string]string{"__name__": "series1", "label": "foo"},
|
||||
wantSamples: []*at.Sample{
|
||||
{Timestamp: 1707123456700, Value: 10},
|
||||
{Timestamp: 1707123456800, Value: 20},
|
||||
},
|
||||
})
|
||||
|
||||
// Search the first point at its own timestamp with step 1ms.
|
||||
f(sut, &opts{
|
||||
query: "series1",
|
||||
qtime: "1707123456700", // 2024-02-05T08:57:36.700Z
|
||||
step: "1ms",
|
||||
wantMetric: map[string]string{"__name__": "series1", "label": "foo"},
|
||||
wantSample: &at.Sample{Timestamp: 1707123456700, Value: 10},
|
||||
})
|
||||
|
||||
// Search the first point at 1ms past its own timestamp.
|
||||
// Expect empty result because the search interval (qtime-step, qtime]
|
||||
// excludes the timestamp of the first point.
|
||||
f(sut, &opts{
|
||||
query: "series1",
|
||||
qtime: "1707123456701", // 2024-02-05T08:57:36.701Z
|
||||
step: "1ms",
|
||||
})
|
||||
|
||||
// Fetch the last point at its timestamp with step 1ms.
|
||||
f(sut, &opts{
|
||||
query: "series1",
|
||||
qtime: "1707123456800", // 2024-02-05T08:57:36.800Z
|
||||
step: "1ms",
|
||||
wantMetric: map[string]string{"__name__": "series1", "label": "foo"},
|
||||
wantSample: &at.Sample{Timestamp: 1707123456800, Value: 20},
|
||||
})
|
||||
|
||||
// Fetch the last point at its timestamp with step 1ms.
|
||||
// Expect empty result because the search interval (qtime-step, qtime]
|
||||
// excludes the timestamp of the first point.
|
||||
f(sut, &opts{
|
||||
query: "series1",
|
||||
qtime: "1707123456801", // 2024-02-05T08:57:36.801Z
|
||||
step: "1ms",
|
||||
// wantMetric: map[string]string{"__name__": "series1", "label": "foo"},
|
||||
// wantSample: &at.Sample{Timestamp: 1707123456801, Value: 20},
|
||||
})
|
||||
|
||||
// Insert samples with different dates. The difference in ms between the two
|
||||
// timestamps is 4236579304.
|
||||
sut.PrometheusAPIV1ImportPrometheus(t, []string{
|
||||
`series2{label="foo"} 10 1638564958042`, // 2021-12-03T20:55:58.042Z
|
||||
`series2{label="foo"} 20 1642801537346`, // 2022-01-21T21:45:37.346Z
|
||||
}, at.QueryOpts{})
|
||||
sut.ForceFlush(t)
|
||||
|
||||
// Both Prometheus and VictoriaMetrics exclude the leftmost millisecond,
|
||||
// thus the following queries must return only one sample.
|
||||
f(sut, &opts{
|
||||
query: "series2[4236579304ms]",
|
||||
qtime: "1642801537346",
|
||||
step: "1ms",
|
||||
wantMetric: map[string]string{"__name__": "series2", "label": "foo"},
|
||||
wantSamples: []*at.Sample{
|
||||
{Timestamp: 1642801537346, Value: 20},
|
||||
},
|
||||
})
|
||||
f(sut, &opts{
|
||||
query: "count_over_time(series2[4236579304ms])",
|
||||
qtime: "1642801537346", // 2022-01-21T21:45:37.346Z
|
||||
step: "1ms",
|
||||
wantMetric: map[string]string{"label": "foo"},
|
||||
wantSample: &at.Sample{Timestamp: 1642801537346, Value: 1},
|
||||
})
|
||||
|
||||
// Adding 1ms to the duration (4236579305ms) causes queries to return 2
|
||||
// samples.
|
||||
f(sut, &opts{
|
||||
query: "series2[4236579305ms]",
|
||||
qtime: "1642801537346",
|
||||
step: "1ms",
|
||||
wantMetric: map[string]string{"__name__": "series2", "label": "foo"},
|
||||
wantSamples: []*at.Sample{
|
||||
{Timestamp: 1638564958042, Value: 10}, // 2021-12-03T20:55:58.042Z
|
||||
{Timestamp: 1642801537346, Value: 20},
|
||||
},
|
||||
})
|
||||
f(sut, &opts{
|
||||
query: "count_over_time(series2[4236579305ms])",
|
||||
qtime: "1642801537346", // 2022-01-21T21:45:37.346Z
|
||||
step: "1ms",
|
||||
wantMetric: map[string]string{"label": "foo"},
|
||||
wantSample: &at.Sample{Timestamp: 1642801537346, Value: 2},
|
||||
})
|
||||
}
|
||||
|
||||
33
apptest/tests/maxingestionrate_test.go
Normal file
33
apptest/tests/maxingestionrate_test.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package tests
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/apptest"
|
||||
)
|
||||
|
||||
// Data used in tests
|
||||
var testData = []string{
|
||||
"foo_bar 1.00",
|
||||
"foo_bar 2.00",
|
||||
}
|
||||
|
||||
func TestSingleMaxIngestionRateIncrementsMetric(t *testing.T) {
|
||||
tc := apptest.NewTestCase(t)
|
||||
defer tc.Stop()
|
||||
sut := tc.MustStartVmsingle("vmsingle", []string{"-maxIngestionRate=1"})
|
||||
sut.PrometheusAPIV1ImportPrometheus(t, testData, apptest.QueryOpts{})
|
||||
if got := sut.GetMetric(t, "vm_max_ingestion_rate_limit_reached_total"); got <= 0 {
|
||||
t.Fatalf("Unexpected vm_max_ingestion_rate_limit_reached_total: got %f, want >0", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSingleMaxIngestionRateDoesNotIncrementMetric(t *testing.T) {
|
||||
tc := apptest.NewTestCase(t)
|
||||
defer tc.Stop()
|
||||
sut := tc.MustStartVmsingle("vmsingle", []string{"-maxIngestionRate=15"})
|
||||
sut.PrometheusAPIV1ImportPrometheus(t, testData, apptest.QueryOpts{})
|
||||
if got, want := sut.GetMetric(t, "vm_max_ingestion_rate_limit_reached_total"), 0.0; got != want {
|
||||
t.Fatalf("Unexpected vm_max_ingestion_rate_limit_reached_total: got %f, want >0", got)
|
||||
}
|
||||
}
|
||||
204
apptest/tests/relabeling_test.go
Normal file
204
apptest/tests/relabeling_test.go
Normal file
@@ -0,0 +1,204 @@
|
||||
package tests
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/google/go-cmp/cmp/cmpopts"
|
||||
|
||||
at "github.com/VictoriaMetrics/VictoriaMetrics/apptest"
|
||||
pb "github.com/VictoriaMetrics/VictoriaMetrics/lib/prompbmarshal"
|
||||
)
|
||||
|
||||
func TestSingleIngestionWithRelabeling(t *testing.T) {
|
||||
tc := at.NewTestCase(t)
|
||||
defer tc.Stop()
|
||||
const relabelFileName = "relabel_config.yaml"
|
||||
relabelingRules := `
|
||||
# add 4 labels in order to call memory allocation
|
||||
- replacement: value1
|
||||
target_label: label1
|
||||
- replacement: value2
|
||||
target_label: label2
|
||||
- replacement: value3
|
||||
target_label: label3
|
||||
- replacement: value4
|
||||
target_label: label4
|
||||
|
||||
# drop specific timeseries by name prefix
|
||||
- action: drop
|
||||
if: '{__name__=~"^must_drop.+"}'
|
||||
|
||||
# strip prefix from metric name
|
||||
# and write it into special label
|
||||
- source_labels: [__name__]
|
||||
regex: '^(.+)_(.+)'
|
||||
replacement: $1
|
||||
target_label: ingestion_protocol
|
||||
- source_labels: [__name__]
|
||||
regex: '^(.+)_(.+)'
|
||||
replacement: $2
|
||||
target_label: __name__
|
||||
`
|
||||
relabelFilePath := fmt.Sprintf("%s/%s", t.TempDir(), relabelFileName)
|
||||
if err := os.WriteFile(relabelFilePath, []byte(relabelingRules), os.ModePerm); err != nil {
|
||||
t.Fatalf("cannot create file=%q: %s", relabelFilePath, err)
|
||||
}
|
||||
sut := tc.MustStartVmsingle("relabeling-ingest",
|
||||
[]string{fmt.Sprintf(`-relabelConfig=%s`, relabelFilePath),
|
||||
`-retentionPeriod=100y`})
|
||||
|
||||
type opts struct {
|
||||
query string
|
||||
qtime string
|
||||
step string
|
||||
wantMetrics []map[string]string
|
||||
wantSamples []*at.Sample
|
||||
}
|
||||
f := func(sut at.PrometheusQuerier, opts *opts) {
|
||||
t.Helper()
|
||||
wantResult := []*at.QueryResult{}
|
||||
for idx, wm := range opts.wantMetrics {
|
||||
wantResult = append(wantResult, &at.QueryResult{
|
||||
Metric: wm,
|
||||
Samples: []*at.Sample{opts.wantSamples[idx]},
|
||||
})
|
||||
|
||||
}
|
||||
tc.Assert(&at.AssertOptions{
|
||||
Msg: "unexpected /api/v1/query response",
|
||||
Got: func() any {
|
||||
return sut.PrometheusAPIV1Query(t, opts.query, at.QueryOpts{
|
||||
Time: opts.qtime,
|
||||
Step: opts.step,
|
||||
})
|
||||
},
|
||||
Want: &at.PrometheusAPIV1QueryResponse{Data: &at.QueryData{Result: wantResult}},
|
||||
CmpOpts: []cmp.Option{
|
||||
cmpopts.IgnoreFields(at.PrometheusAPIV1QueryResponse{}, "Status", "Data.ResultType"),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
sut.PrometheusAPIV1ImportPrometheus(t, []string{
|
||||
`importprometheus_series{label="foo"} 10 1707123456700`, // 2024-02-05T08:57:36.700Z
|
||||
`must_drop_series{label="foo"} 20 1707123456800`, // 2024-02-05T08:57:36.800Z
|
||||
}, at.QueryOpts{})
|
||||
sut.ForceFlush(t)
|
||||
f(sut, &opts{
|
||||
query: `{label="foo"}[120ms]`,
|
||||
qtime: "1707123456800", // 2024-02-05T08:57:36.800Z
|
||||
wantMetrics: []map[string]string{
|
||||
{
|
||||
"__name__": "series",
|
||||
"label": "foo",
|
||||
"label1": "value1",
|
||||
"label2": "value2",
|
||||
"label3": "value3",
|
||||
"label4": "value4",
|
||||
"ingestion_protocol": "importprometheus",
|
||||
},
|
||||
},
|
||||
wantSamples: []*at.Sample{
|
||||
{Timestamp: 1707123456700, Value: 10},
|
||||
},
|
||||
})
|
||||
|
||||
// write influx with multi field set series1 and series2 in order to test
|
||||
// memory optimisation at vminsert side
|
||||
sut.InfluxWrite(t, []string{
|
||||
`influxline,label=foo1 series1=10,series2=30 1707123456700`, // 2024-02-05T08:57:36.700Z
|
||||
`must_drop,label=foo1 series1=20,series2=40 1707123456800`, // 2024-02-05T08:57:36.800Z
|
||||
}, at.QueryOpts{})
|
||||
sut.ForceFlush(t)
|
||||
f(sut, &opts{
|
||||
query: `{label="foo1"}[120ms]`,
|
||||
qtime: "1707123456800", // 2024-02-05T08:57:36.800Z
|
||||
wantMetrics: []map[string]string{
|
||||
{
|
||||
"__name__": "series1",
|
||||
"label": "foo1",
|
||||
"label1": "value1",
|
||||
"label2": "value2",
|
||||
"label3": "value3",
|
||||
"label4": "value4",
|
||||
"ingestion_protocol": "influxline",
|
||||
},
|
||||
{
|
||||
"__name__": "series2",
|
||||
"label": "foo1",
|
||||
"label1": "value1",
|
||||
"label2": "value2",
|
||||
"label3": "value3",
|
||||
"label4": "value4",
|
||||
"ingestion_protocol": "influxline"},
|
||||
},
|
||||
wantSamples: []*at.Sample{
|
||||
{Timestamp: 1707123456700, Value: 10},
|
||||
{Timestamp: 1707123456700, Value: 30},
|
||||
},
|
||||
})
|
||||
|
||||
pbData := []pb.TimeSeries{
|
||||
{
|
||||
Labels: []pb.Label{
|
||||
{
|
||||
Name: "__name__",
|
||||
Value: "prometheusrw_series",
|
||||
},
|
||||
{
|
||||
Name: "label",
|
||||
Value: "foo2",
|
||||
},
|
||||
},
|
||||
Samples: []pb.Sample{
|
||||
{
|
||||
Value: 10,
|
||||
Timestamp: 1707123456700, // 2024-02-05T08:57:36.700Z
|
||||
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Labels: []pb.Label{
|
||||
{
|
||||
Name: "__name__",
|
||||
Value: "must_drop_series",
|
||||
},
|
||||
{
|
||||
Name: "label",
|
||||
Value: "foo2",
|
||||
},
|
||||
},
|
||||
Samples: []pb.Sample{
|
||||
{
|
||||
Value: 20,
|
||||
Timestamp: 1707123456800, // 2024-02-05T08:57:36.800Z
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
sut.PrometheusAPIV1Write(t, pbData, at.QueryOpts{})
|
||||
sut.ForceFlush(t)
|
||||
f(sut, &opts{
|
||||
query: `{label="foo2"}[120ms]`,
|
||||
qtime: "1707123456800", // 2024-02-05T08:57:36.800Z
|
||||
wantMetrics: []map[string]string{
|
||||
{
|
||||
"__name__": "series",
|
||||
"label": "foo2",
|
||||
"label1": "value1",
|
||||
"label2": "value2",
|
||||
"label3": "value3",
|
||||
"label4": "value4",
|
||||
"ingestion_protocol": "prometheusrw",
|
||||
},
|
||||
},
|
||||
wantSamples: []*at.Sample{
|
||||
{Timestamp: 1707123456700, Value: 10}, // 2024-02-05T08:57:36.700Z
|
||||
},
|
||||
})
|
||||
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user