mirror of
https://github.com/VictoriaMetrics/VictoriaMetrics.git
synced 2026-05-31 07:41:10 +03:00
Compare commits
141 Commits
v1.3.2-vic
...
fix-panel-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1cc1d45503 | ||
|
|
d064e14933 | ||
|
|
77b0fcfdd9 | ||
|
|
ee7fe11fd2 | ||
|
|
4c26fb6fe5 | ||
|
|
fc135094b3 | ||
|
|
5d42f21abd | ||
|
|
28eeabded1 | ||
|
|
b6910cfff7 | ||
|
|
8938ef398c | ||
|
|
df2b75fa81 | ||
|
|
857734c66c | ||
|
|
bedc0c0f8f | ||
|
|
5a41bdf329 | ||
|
|
bf5d0dd245 | ||
|
|
1cec37b0f5 | ||
|
|
c40c25b03c | ||
|
|
82badc3dd5 | ||
|
|
43ded688f7 | ||
|
|
661420fe85 | ||
|
|
7aab967447 | ||
|
|
afb07034ed | ||
|
|
44d2205136 | ||
|
|
b226318f9e | ||
|
|
30999204c9 | ||
|
|
ffddfa1f94 | ||
|
|
fc336bbf20 | ||
|
|
e0b2c1c4f5 | ||
|
|
5afbee5f6f | ||
|
|
51459196f9 | ||
|
|
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 |
16
README.md
16
README.md
@@ -1,12 +1,14 @@
|
||||
# VictoriaMetrics
|
||||
|
||||
[](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/latest)
|
||||
[](https://hub.docker.com/r/victoriametrics/victoria-metrics)
|
||||
[](https://slack.victoriametrics.com/)
|
||||
[](https://github.com/VictoriaMetrics/VictoriaMetrics/blob/master/LICENSE)
|
||||
[](https://goreportcard.com/report/github.com/VictoriaMetrics/VictoriaMetrics)
|
||||
[](https://github.com/VictoriaMetrics/VictoriaMetrics/actions)
|
||||
[](https://codecov.io/gh/VictoriaMetrics/VictoriaMetrics)
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
<picture>
|
||||
<source srcset="docs/logo_white.webp" media="(prefers-color-scheme: dark)">
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -614,7 +614,7 @@ func (ar *AlertingRule) alertToTimeSeries(a *notifier.Alert, timestamp int64) []
|
||||
}
|
||||
|
||||
func alertToTimeSeries(a *notifier.Alert, timestamp int64) prompbmarshal.TimeSeries {
|
||||
var labels []prompbmarshal.Label
|
||||
labels := make([]prompbmarshal.Label, 0, len(a.Labels)+2)
|
||||
for k, v := range a.Labels {
|
||||
labels = append(labels, prompbmarshal.Label{
|
||||
Name: k,
|
||||
@@ -634,7 +634,7 @@ func alertToTimeSeries(a *notifier.Alert, timestamp int64) prompbmarshal.TimeSer
|
||||
// alertForToTimeSeries returns a time series that represents
|
||||
// state of active alerts, where value is time when alert become active
|
||||
func alertForToTimeSeries(a *notifier.Alert, timestamp int64) prompbmarshal.TimeSeries {
|
||||
var labels []prompbmarshal.Label
|
||||
labels := make([]prompbmarshal.Label, 0, len(a.Labels)+1)
|
||||
for k, v := range a.Labels {
|
||||
labels = append(labels, prompbmarshal.Label{
|
||||
Name: k,
|
||||
@@ -650,21 +650,24 @@ func alertForToTimeSeries(a *notifier.Alert, timestamp int64) prompbmarshal.Time
|
||||
// for alerts which changed their state from Pending to Inactive or Firing.
|
||||
func pendingAlertStaleTimeSeries(ls map[string]string, timestamp int64, includeAlertForState bool) []prompbmarshal.TimeSeries {
|
||||
var result []prompbmarshal.TimeSeries
|
||||
var baseLabels []prompbmarshal.Label
|
||||
baseLabels := make([]prompbmarshal.Label, 0, len(ls)+1)
|
||||
for k, v := range ls {
|
||||
baseLabels = append(baseLabels, prompbmarshal.Label{
|
||||
Name: k,
|
||||
Value: v,
|
||||
})
|
||||
}
|
||||
|
||||
alertsLabels := make([]prompbmarshal.Label, 0, len(ls)+2)
|
||||
alertsLabels = append(alertsLabels, baseLabels...)
|
||||
// __name__ already been dropped, no need to check duplication
|
||||
alertsLabels := append(baseLabels, prompbmarshal.Label{Name: "__name__", Value: alertMetricName})
|
||||
alertsLabels = append(alertsLabels, prompbmarshal.Label{Name: "__name__", Value: alertMetricName})
|
||||
alertsLabels = append(alertsLabels, prompbmarshal.Label{Name: alertStateLabel, Value: notifier.StatePending.String()})
|
||||
result = append(result, newTimeSeries([]float64{decimal.StaleNaN}, []int64{timestamp}, alertsLabels))
|
||||
|
||||
if includeAlertForState {
|
||||
alertsForStateLabels := append(baseLabels, prompbmarshal.Label{Name: "__name__", Value: alertForStateMetricName})
|
||||
result = append(result, newTimeSeries([]float64{decimal.StaleNaN}, []int64{timestamp}, alertsForStateLabels))
|
||||
baseLabels = append(baseLabels, prompbmarshal.Label{Name: "__name__", Value: alertForStateMetricName})
|
||||
result = append(result, newTimeSeries([]float64{decimal.StaleNaN}, []int64{timestamp}, baseLabels))
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -672,22 +675,25 @@ func pendingAlertStaleTimeSeries(ls map[string]string, timestamp int64, includeA
|
||||
// firingAlertStaleTimeSeries returns stale `ALERTS` and `ALERTS_FOR_STATE` time series
|
||||
// for alerts which changed their state from Firing to Inactive.
|
||||
func firingAlertStaleTimeSeries(ls map[string]string, timestamp int64) []prompbmarshal.TimeSeries {
|
||||
var baseLabels []prompbmarshal.Label
|
||||
baseLabels := make([]prompbmarshal.Label, 0, len(ls)+1)
|
||||
for k, v := range ls {
|
||||
baseLabels = append(baseLabels, prompbmarshal.Label{
|
||||
Name: k,
|
||||
Value: v,
|
||||
})
|
||||
}
|
||||
|
||||
alertsLabels := make([]prompbmarshal.Label, 0, len(ls)+2)
|
||||
alertsLabels = append(alertsLabels, baseLabels...)
|
||||
// __name__ already been dropped, no need to check duplication
|
||||
alertsLabels := append(baseLabels, prompbmarshal.Label{Name: "__name__", Value: alertMetricName})
|
||||
alertsLabels = append(alertsLabels, prompbmarshal.Label{Name: "__name__", Value: alertMetricName})
|
||||
alertsLabels = append(alertsLabels, prompbmarshal.Label{Name: alertStateLabel, Value: notifier.StateFiring.String()})
|
||||
|
||||
alertsForStateLabels := append(baseLabels, prompbmarshal.Label{Name: "__name__", Value: alertForStateMetricName})
|
||||
baseLabels = append(baseLabels, prompbmarshal.Label{Name: "__name__", Value: alertForStateMetricName})
|
||||
|
||||
return []prompbmarshal.TimeSeries{
|
||||
newTimeSeries([]float64{decimal.StaleNaN}, []int64{timestamp}, alertsLabels),
|
||||
newTimeSeries([]float64{decimal.StaleNaN}, []int64{timestamp}, alertsForStateLabels),
|
||||
newTimeSeries([]float64{decimal.StaleNaN}, []int64{timestamp}, baseLabels),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -252,10 +252,14 @@ func TestAlertingRule_Exec(t *testing.T) {
|
||||
},
|
||||
map[int][]prompbmarshal.TimeSeries{
|
||||
0: {
|
||||
{Labels: []prompbmarshal.Label{{Name: "__name__", Value: alertMetricName}, {Name: "alertname", Value: "empty_labels"}, {Name: "alertstate", Value: "firing"}},
|
||||
Samples: []prompbmarshal.Sample{{Value: 1, Timestamp: ts.UnixNano() / 1e6}}},
|
||||
{Labels: []prompbmarshal.Label{{Name: "__name__", Value: alertForStateMetricName}, {Name: "alertname", Value: "empty_labels"}},
|
||||
Samples: []prompbmarshal.Sample{{Value: float64(ts.Unix()), Timestamp: ts.UnixNano() / 1e6}}},
|
||||
{
|
||||
Labels: []prompbmarshal.Label{{Name: "__name__", Value: alertMetricName}, {Name: "alertname", Value: "empty_labels"}, {Name: "alertstate", Value: "firing"}},
|
||||
Samples: []prompbmarshal.Sample{{Value: 1, Timestamp: ts.UnixNano() / 1e6}},
|
||||
},
|
||||
{
|
||||
Labels: []prompbmarshal.Label{{Name: "__name__", Value: alertForStateMetricName}, {Name: "alertname", Value: "empty_labels"}},
|
||||
Samples: []prompbmarshal.Sample{{Value: float64(ts.Unix()), Timestamp: ts.UnixNano() / 1e6}},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
@@ -273,22 +277,34 @@ func TestAlertingRule_Exec(t *testing.T) {
|
||||
4: {{labels: []string{"name", "foo"}, alert: ¬ifier.Alert{State: notifier.StateInactive}}},
|
||||
}, map[int][]prompbmarshal.TimeSeries{
|
||||
0: {
|
||||
{Labels: []prompbmarshal.Label{{Name: "__name__", Value: alertMetricName}, {Name: "alertname", Value: "single-firing=>inactive=>firing=>inactive=>inactive"}, {Name: "alertstate", Value: "firing"}, {Name: "name", Value: "foo"}},
|
||||
Samples: []prompbmarshal.Sample{{Value: 1, Timestamp: ts.UnixNano() / 1e6}}},
|
||||
{Labels: []prompbmarshal.Label{{Name: "__name__", Value: alertForStateMetricName}, {Name: "alertname", Value: "single-firing=>inactive=>firing=>inactive=>inactive"}, {Name: "name", Value: "foo"}},
|
||||
Samples: []prompbmarshal.Sample{{Value: float64(ts.Unix()), Timestamp: ts.UnixNano() / 1e6}}},
|
||||
{
|
||||
Labels: []prompbmarshal.Label{{Name: "__name__", Value: alertMetricName}, {Name: "alertname", Value: "single-firing=>inactive=>firing=>inactive=>inactive"}, {Name: "alertstate", Value: "firing"}, {Name: "name", Value: "foo"}},
|
||||
Samples: []prompbmarshal.Sample{{Value: 1, Timestamp: ts.UnixNano() / 1e6}},
|
||||
},
|
||||
{
|
||||
Labels: []prompbmarshal.Label{{Name: "__name__", Value: alertForStateMetricName}, {Name: "alertname", Value: "single-firing=>inactive=>firing=>inactive=>inactive"}, {Name: "name", Value: "foo"}},
|
||||
Samples: []prompbmarshal.Sample{{Value: float64(ts.Unix()), Timestamp: ts.UnixNano() / 1e6}},
|
||||
},
|
||||
},
|
||||
1: {
|
||||
{Labels: []prompbmarshal.Label{{Name: "__name__", Value: alertMetricName}, {Name: "alertname", Value: "single-firing=>inactive=>firing=>inactive=>inactive"}, {Name: "alertstate", Value: "firing"}, {Name: "name", Value: "foo"}},
|
||||
Samples: []prompbmarshal.Sample{{Value: decimal.StaleNaN, Timestamp: ts.Add(defaultStep).UnixNano() / 1e6}}},
|
||||
{Labels: []prompbmarshal.Label{{Name: "__name__", Value: alertForStateMetricName}, {Name: "alertname", Value: "single-firing=>inactive=>firing=>inactive=>inactive"}, {Name: "name", Value: "foo"}},
|
||||
Samples: []prompbmarshal.Sample{{Value: decimal.StaleNaN, Timestamp: ts.Add(defaultStep).UnixNano() / 1e6}}},
|
||||
{
|
||||
Labels: []prompbmarshal.Label{{Name: "__name__", Value: alertMetricName}, {Name: "alertname", Value: "single-firing=>inactive=>firing=>inactive=>inactive"}, {Name: "alertstate", Value: "firing"}, {Name: "name", Value: "foo"}},
|
||||
Samples: []prompbmarshal.Sample{{Value: decimal.StaleNaN, Timestamp: ts.Add(defaultStep).UnixNano() / 1e6}},
|
||||
},
|
||||
{
|
||||
Labels: []prompbmarshal.Label{{Name: "__name__", Value: alertForStateMetricName}, {Name: "alertname", Value: "single-firing=>inactive=>firing=>inactive=>inactive"}, {Name: "name", Value: "foo"}},
|
||||
Samples: []prompbmarshal.Sample{{Value: decimal.StaleNaN, Timestamp: ts.Add(defaultStep).UnixNano() / 1e6}},
|
||||
},
|
||||
},
|
||||
2: {
|
||||
{Labels: []prompbmarshal.Label{{Name: "__name__", Value: alertMetricName}, {Name: "alertname", Value: "single-firing=>inactive=>firing=>inactive=>inactive"}, {Name: "alertstate", Value: "firing"}, {Name: "name", Value: "foo"}},
|
||||
Samples: []prompbmarshal.Sample{{Value: 1, Timestamp: ts.Add(2*defaultStep).UnixNano() / 1e6}}},
|
||||
{Labels: []prompbmarshal.Label{{Name: "__name__", Value: alertForStateMetricName}, {Name: "alertname", Value: "single-firing=>inactive=>firing=>inactive=>inactive"}, {Name: "name", Value: "foo"}},
|
||||
Samples: []prompbmarshal.Sample{{Value: float64(ts.Add(2 * defaultStep).Unix()), Timestamp: ts.Add(2*defaultStep).UnixNano() / 1e6}}},
|
||||
{
|
||||
Labels: []prompbmarshal.Label{{Name: "__name__", Value: alertMetricName}, {Name: "alertname", Value: "single-firing=>inactive=>firing=>inactive=>inactive"}, {Name: "alertstate", Value: "firing"}, {Name: "name", Value: "foo"}},
|
||||
Samples: []prompbmarshal.Sample{{Value: 1, Timestamp: ts.Add(2*defaultStep).UnixNano() / 1e6}},
|
||||
},
|
||||
{
|
||||
Labels: []prompbmarshal.Label{{Name: "__name__", Value: alertForStateMetricName}, {Name: "alertname", Value: "single-firing=>inactive=>firing=>inactive=>inactive"}, {Name: "name", Value: "foo"}},
|
||||
Samples: []prompbmarshal.Sample{{Value: float64(ts.Add(2 * defaultStep).Unix()), Timestamp: ts.Add(2*defaultStep).UnixNano() / 1e6}},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
@@ -344,34 +360,54 @@ func TestAlertingRule_Exec(t *testing.T) {
|
||||
},
|
||||
}, map[int][]prompbmarshal.TimeSeries{
|
||||
0: {
|
||||
{Labels: []prompbmarshal.Label{{Name: "__name__", Value: alertMetricName}, {Name: "alertname", Value: "multiple-steps-firing"}, {Name: "alertstate", Value: "firing"}, {Name: "name", Value: "foo"}},
|
||||
Samples: []prompbmarshal.Sample{{Value: 1, Timestamp: ts.UnixNano() / 1e6}}},
|
||||
{Labels: []prompbmarshal.Label{{Name: "__name__", Value: alertForStateMetricName}, {Name: "alertname", Value: "multiple-steps-firing"}, {Name: "name", Value: "foo"}},
|
||||
Samples: []prompbmarshal.Sample{{Value: float64(ts.Unix()), Timestamp: ts.UnixNano() / 1e6}}},
|
||||
{
|
||||
Labels: []prompbmarshal.Label{{Name: "__name__", Value: alertMetricName}, {Name: "alertname", Value: "multiple-steps-firing"}, {Name: "alertstate", Value: "firing"}, {Name: "name", Value: "foo"}},
|
||||
Samples: []prompbmarshal.Sample{{Value: 1, Timestamp: ts.UnixNano() / 1e6}},
|
||||
},
|
||||
{
|
||||
Labels: []prompbmarshal.Label{{Name: "__name__", Value: alertForStateMetricName}, {Name: "alertname", Value: "multiple-steps-firing"}, {Name: "name", Value: "foo"}},
|
||||
Samples: []prompbmarshal.Sample{{Value: float64(ts.Unix()), Timestamp: ts.UnixNano() / 1e6}},
|
||||
},
|
||||
},
|
||||
1: {
|
||||
// stale time series for foo, `firing -> inactive`
|
||||
{Labels: []prompbmarshal.Label{{Name: "__name__", Value: alertMetricName}, {Name: "alertname", Value: "multiple-steps-firing"}, {Name: "alertstate", Value: "firing"}, {Name: "name", Value: "foo"}},
|
||||
Samples: []prompbmarshal.Sample{{Value: decimal.StaleNaN, Timestamp: ts.Add(defaultStep).UnixNano() / 1e6}}},
|
||||
{Labels: []prompbmarshal.Label{{Name: "__name__", Value: alertForStateMetricName}, {Name: "alertname", Value: "multiple-steps-firing"}, {Name: "name", Value: "foo"}},
|
||||
Samples: []prompbmarshal.Sample{{Value: decimal.StaleNaN, Timestamp: ts.Add(defaultStep).UnixNano() / 1e6}}},
|
||||
{
|
||||
Labels: []prompbmarshal.Label{{Name: "__name__", Value: alertMetricName}, {Name: "alertname", Value: "multiple-steps-firing"}, {Name: "alertstate", Value: "firing"}, {Name: "name", Value: "foo"}},
|
||||
Samples: []prompbmarshal.Sample{{Value: decimal.StaleNaN, Timestamp: ts.Add(defaultStep).UnixNano() / 1e6}},
|
||||
},
|
||||
{
|
||||
Labels: []prompbmarshal.Label{{Name: "__name__", Value: alertForStateMetricName}, {Name: "alertname", Value: "multiple-steps-firing"}, {Name: "name", Value: "foo"}},
|
||||
Samples: []prompbmarshal.Sample{{Value: decimal.StaleNaN, Timestamp: ts.Add(defaultStep).UnixNano() / 1e6}},
|
||||
},
|
||||
// new time series for foo1
|
||||
{Labels: []prompbmarshal.Label{{Name: "__name__", Value: alertMetricName}, {Name: "alertname", Value: "multiple-steps-firing"}, {Name: "alertstate", Value: "firing"}, {Name: "name", Value: "foo1"}},
|
||||
Samples: []prompbmarshal.Sample{{Value: 1, Timestamp: ts.Add(defaultStep).UnixNano() / 1e6}}},
|
||||
{Labels: []prompbmarshal.Label{{Name: "__name__", Value: alertForStateMetricName}, {Name: "alertname", Value: "multiple-steps-firing"}, {Name: "name", Value: "foo1"}},
|
||||
Samples: []prompbmarshal.Sample{{Value: float64(ts.Add(defaultStep).Unix()), Timestamp: ts.Add(defaultStep).UnixNano() / 1e6}}},
|
||||
{
|
||||
Labels: []prompbmarshal.Label{{Name: "__name__", Value: alertMetricName}, {Name: "alertname", Value: "multiple-steps-firing"}, {Name: "alertstate", Value: "firing"}, {Name: "name", Value: "foo1"}},
|
||||
Samples: []prompbmarshal.Sample{{Value: 1, Timestamp: ts.Add(defaultStep).UnixNano() / 1e6}},
|
||||
},
|
||||
{
|
||||
Labels: []prompbmarshal.Label{{Name: "__name__", Value: alertForStateMetricName}, {Name: "alertname", Value: "multiple-steps-firing"}, {Name: "name", Value: "foo1"}},
|
||||
Samples: []prompbmarshal.Sample{{Value: float64(ts.Add(defaultStep).Unix()), Timestamp: ts.Add(defaultStep).UnixNano() / 1e6}},
|
||||
},
|
||||
},
|
||||
2: {
|
||||
// stale time series for foo1
|
||||
{Labels: []prompbmarshal.Label{{Name: "__name__", Value: alertMetricName}, {Name: "alertname", Value: "multiple-steps-firing"}, {Name: "alertstate", Value: "firing"}, {Name: "name", Value: "foo1"}},
|
||||
Samples: []prompbmarshal.Sample{{Value: decimal.StaleNaN, Timestamp: ts.Add(2*defaultStep).UnixNano() / 1e6}}},
|
||||
{Labels: []prompbmarshal.Label{{Name: "__name__", Value: alertForStateMetricName}, {Name: "alertname", Value: "multiple-steps-firing"}, {Name: "name", Value: "foo1"}},
|
||||
Samples: []prompbmarshal.Sample{{Value: decimal.StaleNaN, Timestamp: ts.Add(2*defaultStep).UnixNano() / 1e6}}},
|
||||
{
|
||||
Labels: []prompbmarshal.Label{{Name: "__name__", Value: alertMetricName}, {Name: "alertname", Value: "multiple-steps-firing"}, {Name: "alertstate", Value: "firing"}, {Name: "name", Value: "foo1"}},
|
||||
Samples: []prompbmarshal.Sample{{Value: decimal.StaleNaN, Timestamp: ts.Add(2*defaultStep).UnixNano() / 1e6}},
|
||||
},
|
||||
{
|
||||
Labels: []prompbmarshal.Label{{Name: "__name__", Value: alertForStateMetricName}, {Name: "alertname", Value: "multiple-steps-firing"}, {Name: "name", Value: "foo1"}},
|
||||
Samples: []prompbmarshal.Sample{{Value: decimal.StaleNaN, Timestamp: ts.Add(2*defaultStep).UnixNano() / 1e6}},
|
||||
},
|
||||
// new time series for foo2
|
||||
{Labels: []prompbmarshal.Label{{Name: "__name__", Value: alertMetricName}, {Name: "alertname", Value: "multiple-steps-firing"}, {Name: "alertstate", Value: "firing"}, {Name: "name", Value: "foo2"}},
|
||||
Samples: []prompbmarshal.Sample{{Value: 1, Timestamp: ts.Add(2*defaultStep).UnixNano() / 1e6}}},
|
||||
{Labels: []prompbmarshal.Label{{Name: "__name__", Value: alertForStateMetricName}, {Name: "alertname", Value: "multiple-steps-firing"}, {Name: "name", Value: "foo2"}},
|
||||
Samples: []prompbmarshal.Sample{{Value: float64(ts.Add(2 * defaultStep).Unix()), Timestamp: ts.Add(2*defaultStep).UnixNano() / 1e6}}},
|
||||
{
|
||||
Labels: []prompbmarshal.Label{{Name: "__name__", Value: alertMetricName}, {Name: "alertname", Value: "multiple-steps-firing"}, {Name: "alertstate", Value: "firing"}, {Name: "name", Value: "foo2"}},
|
||||
Samples: []prompbmarshal.Sample{{Value: 1, Timestamp: ts.Add(2*defaultStep).UnixNano() / 1e6}},
|
||||
},
|
||||
{
|
||||
Labels: []prompbmarshal.Label{{Name: "__name__", Value: alertForStateMetricName}, {Name: "alertname", Value: "multiple-steps-firing"}, {Name: "name", Value: "foo2"}},
|
||||
Samples: []prompbmarshal.Sample{{Value: float64(ts.Add(2 * defaultStep).Unix()), Timestamp: ts.Add(2*defaultStep).UnixNano() / 1e6}},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
@@ -389,50 +425,72 @@ func TestAlertingRule_Exec(t *testing.T) {
|
||||
1: {{labels: []string{"name", "foo"}, alert: ¬ifier.Alert{State: notifier.StateFiring}}},
|
||||
}, map[int][]prompbmarshal.TimeSeries{
|
||||
0: {
|
||||
{Labels: []prompbmarshal.Label{{Name: "__name__", Value: alertMetricName}, {Name: "alertname", Value: "for-fired"}, {Name: "alertstate", Value: "pending"}, {Name: "name", Value: "foo"}},
|
||||
Samples: []prompbmarshal.Sample{{Value: 1, Timestamp: ts.UnixNano() / 1e6}}},
|
||||
{Labels: []prompbmarshal.Label{{Name: "__name__", Value: alertForStateMetricName}, {Name: "alertname", Value: "for-fired"}, {Name: "name", Value: "foo"}},
|
||||
Samples: []prompbmarshal.Sample{{Value: float64(ts.Unix()), Timestamp: ts.UnixNano() / 1e6}}},
|
||||
{
|
||||
Labels: []prompbmarshal.Label{{Name: "__name__", Value: alertMetricName}, {Name: "alertname", Value: "for-fired"}, {Name: "alertstate", Value: "pending"}, {Name: "name", Value: "foo"}},
|
||||
Samples: []prompbmarshal.Sample{{Value: 1, Timestamp: ts.UnixNano() / 1e6}},
|
||||
},
|
||||
{
|
||||
Labels: []prompbmarshal.Label{{Name: "__name__", Value: alertForStateMetricName}, {Name: "alertname", Value: "for-fired"}, {Name: "name", Value: "foo"}},
|
||||
Samples: []prompbmarshal.Sample{{Value: float64(ts.Unix()), Timestamp: ts.UnixNano() / 1e6}},
|
||||
},
|
||||
},
|
||||
1: {
|
||||
// stale time series for `pending -> firing`
|
||||
{Labels: []prompbmarshal.Label{{Name: "__name__", Value: alertMetricName}, {Name: "alertname", Value: "for-fired"}, {Name: "alertstate", Value: "pending"}, {Name: "name", Value: "foo"}},
|
||||
Samples: []prompbmarshal.Sample{{Value: decimal.StaleNaN, Timestamp: ts.Add(defaultStep).UnixNano() / 1e6}}},
|
||||
{Labels: []prompbmarshal.Label{{Name: "__name__", Value: alertMetricName}, {Name: "alertname", Value: "for-fired"}, {Name: "alertstate", Value: "firing"}, {Name: "name", Value: "foo"}},
|
||||
Samples: []prompbmarshal.Sample{{Value: 1, Timestamp: ts.Add(defaultStep).UnixNano() / 1e6}}},
|
||||
{Labels: []prompbmarshal.Label{{Name: "__name__", Value: alertForStateMetricName}, {Name: "alertname", Value: "for-fired"}, {Name: "name", Value: "foo"}},
|
||||
Samples: []prompbmarshal.Sample{{Value: float64(ts.Add(defaultStep).Unix()), Timestamp: ts.Add(defaultStep).UnixNano() / 1e6}}},
|
||||
{
|
||||
Labels: []prompbmarshal.Label{{Name: "__name__", Value: alertMetricName}, {Name: "alertname", Value: "for-fired"}, {Name: "alertstate", Value: "pending"}, {Name: "name", Value: "foo"}},
|
||||
Samples: []prompbmarshal.Sample{{Value: decimal.StaleNaN, Timestamp: ts.Add(defaultStep).UnixNano() / 1e6}},
|
||||
},
|
||||
{
|
||||
Labels: []prompbmarshal.Label{{Name: "__name__", Value: alertMetricName}, {Name: "alertname", Value: "for-fired"}, {Name: "alertstate", Value: "firing"}, {Name: "name", Value: "foo"}},
|
||||
Samples: []prompbmarshal.Sample{{Value: 1, Timestamp: ts.Add(defaultStep).UnixNano() / 1e6}},
|
||||
},
|
||||
{
|
||||
Labels: []prompbmarshal.Label{{Name: "__name__", Value: alertForStateMetricName}, {Name: "alertname", Value: "for-fired"}, {Name: "name", Value: "foo"}},
|
||||
Samples: []prompbmarshal.Sample{{Value: float64(ts.Add(defaultStep).Unix()), Timestamp: ts.Add(defaultStep).UnixNano() / 1e6}},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
f(newTestAlertingRule("for-pending=>empty", time.Second), [][]datasource.Metric{
|
||||
{metricWithLabels(t, "name", "foo")},
|
||||
{metricWithLabels(t, "name", "foo")},
|
||||
{metricWithLabels(t, "name", "foo", "a1", "b1", "a2", "b2", "a3", "b3")},
|
||||
{metricWithLabels(t, "name", "foo", "a1", "b1", "a2", "b2", "a3", "b3")},
|
||||
// empty step to delete pending alerts
|
||||
{},
|
||||
}, map[int][]testAlert{
|
||||
0: {{labels: []string{"name", "foo"}, alert: ¬ifier.Alert{State: notifier.StatePending}}},
|
||||
1: {{labels: []string{"name", "foo"}, alert: ¬ifier.Alert{State: notifier.StatePending}}},
|
||||
0: {{labels: []string{"name", "foo", "a1", "b1", "a2", "b2", "a3", "b3"}, alert: ¬ifier.Alert{State: notifier.StatePending}}},
|
||||
1: {{labels: []string{"name", "foo", "a1", "b1", "a2", "b2", "a3", "b3"}, alert: ¬ifier.Alert{State: notifier.StatePending}}},
|
||||
2: {},
|
||||
}, map[int][]prompbmarshal.TimeSeries{
|
||||
0: {
|
||||
{Labels: []prompbmarshal.Label{{Name: "__name__", Value: alertMetricName}, {Name: "alertname", Value: "for-pending=>empty"}, {Name: "alertstate", Value: "pending"}, {Name: "name", Value: "foo"}},
|
||||
Samples: []prompbmarshal.Sample{{Value: 1, Timestamp: ts.UnixNano() / 1e6}}},
|
||||
{Labels: []prompbmarshal.Label{{Name: "__name__", Value: alertForStateMetricName}, {Name: "alertname", Value: "for-pending=>empty"}, {Name: "name", Value: "foo"}},
|
||||
Samples: []prompbmarshal.Sample{{Value: float64(ts.Unix()), Timestamp: ts.UnixNano() / 1e6}}},
|
||||
{
|
||||
Labels: []prompbmarshal.Label{{Name: "__name__", Value: alertMetricName}, {Name: "a1", Value: "b1"}, {Name: "a2", Value: "b2"}, {Name: "a3", Value: "b3"}, {Name: "alertname", Value: "for-pending=>empty"}, {Name: "alertstate", Value: "pending"}, {Name: "name", Value: "foo"}},
|
||||
Samples: []prompbmarshal.Sample{{Value: 1, Timestamp: ts.UnixNano() / 1e6}},
|
||||
},
|
||||
{
|
||||
Labels: []prompbmarshal.Label{{Name: "__name__", Value: alertForStateMetricName}, {Name: "a1", Value: "b1"}, {Name: "a2", Value: "b2"}, {Name: "a3", Value: "b3"}, {Name: "alertname", Value: "for-pending=>empty"}, {Name: "name", Value: "foo"}},
|
||||
Samples: []prompbmarshal.Sample{{Value: float64(ts.Unix()), Timestamp: ts.UnixNano() / 1e6}},
|
||||
},
|
||||
},
|
||||
1: {
|
||||
{Labels: []prompbmarshal.Label{{Name: "__name__", Value: alertMetricName}, {Name: "alertname", Value: "for-pending=>empty"}, {Name: "alertstate", Value: "pending"}, {Name: "name", Value: "foo"}},
|
||||
Samples: []prompbmarshal.Sample{{Value: 1, Timestamp: ts.Add(defaultStep).UnixNano() / 1e6}}},
|
||||
{Labels: []prompbmarshal.Label{{Name: "__name__", Value: alertForStateMetricName}, {Name: "alertname", Value: "for-pending=>empty"}, {Name: "name", Value: "foo"}},
|
||||
Samples: []prompbmarshal.Sample{{Value: float64(ts.Unix()), Timestamp: ts.Add(defaultStep).UnixNano() / 1e6}}},
|
||||
{
|
||||
Labels: []prompbmarshal.Label{{Name: "__name__", Value: alertMetricName}, {Name: "a1", Value: "b1"}, {Name: "a2", Value: "b2"}, {Name: "a3", Value: "b3"}, {Name: "alertname", Value: "for-pending=>empty"}, {Name: "alertstate", Value: "pending"}, {Name: "name", Value: "foo"}},
|
||||
Samples: []prompbmarshal.Sample{{Value: 1, Timestamp: ts.Add(defaultStep).UnixNano() / 1e6}},
|
||||
},
|
||||
{
|
||||
Labels: []prompbmarshal.Label{{Name: "__name__", Value: alertForStateMetricName}, {Name: "a1", Value: "b1"}, {Name: "a2", Value: "b2"}, {Name: "a3", Value: "b3"}, {Name: "alertname", Value: "for-pending=>empty"}, {Name: "name", Value: "foo"}},
|
||||
Samples: []prompbmarshal.Sample{{Value: float64(ts.Unix()), Timestamp: ts.Add(defaultStep).UnixNano() / 1e6}},
|
||||
},
|
||||
},
|
||||
// stale time series for `pending -> inactive`
|
||||
2: {
|
||||
{Labels: []prompbmarshal.Label{{Name: "__name__", Value: alertMetricName}, {Name: "alertname", Value: "for-pending=>empty"}, {Name: "alertstate", Value: "pending"}, {Name: "name", Value: "foo"}},
|
||||
Samples: []prompbmarshal.Sample{{Value: decimal.StaleNaN, Timestamp: ts.Add(2*defaultStep).UnixNano() / 1e6}}},
|
||||
{Labels: []prompbmarshal.Label{{Name: "__name__", Value: alertForStateMetricName}, {Name: "alertname", Value: "for-pending=>empty"}, {Name: "name", Value: "foo"}},
|
||||
Samples: []prompbmarshal.Sample{{Value: decimal.StaleNaN, Timestamp: ts.Add(2*defaultStep).UnixNano() / 1e6}}},
|
||||
{
|
||||
Labels: []prompbmarshal.Label{{Name: "__name__", Value: alertMetricName}, {Name: "a1", Value: "b1"}, {Name: "a2", Value: "b2"}, {Name: "a3", Value: "b3"}, {Name: "alertname", Value: "for-pending=>empty"}, {Name: "alertstate", Value: "pending"}, {Name: "name", Value: "foo"}},
|
||||
Samples: []prompbmarshal.Sample{{Value: decimal.StaleNaN, Timestamp: ts.Add(2*defaultStep).UnixNano() / 1e6}},
|
||||
},
|
||||
{
|
||||
Labels: []prompbmarshal.Label{{Name: "__name__", Value: alertForStateMetricName}, {Name: "a1", Value: "b1"}, {Name: "a2", Value: "b2"}, {Name: "a3", Value: "b3"}, {Name: "alertname", Value: "for-pending=>empty"}, {Name: "name", Value: "foo"}},
|
||||
Samples: []prompbmarshal.Sample{{Value: decimal.StaleNaN, Timestamp: ts.Add(2*defaultStep).UnixNano() / 1e6}},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -7,11 +7,13 @@ import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"math"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
@@ -348,6 +350,7 @@ func (up *URLPrefix) discoverBackendAddrsIfNeeded() {
|
||||
hostToAddrs := make(map[string][]string)
|
||||
for _, bu := range up.busOriginal {
|
||||
host := bu.Hostname()
|
||||
port := bu.Port()
|
||||
if hostToAddrs[host] != nil {
|
||||
// ips for the given host have been already discovered
|
||||
continue
|
||||
@@ -364,7 +367,11 @@ func (up *URLPrefix) discoverBackendAddrsIfNeeded() {
|
||||
} else {
|
||||
resolvedAddrs = make([]string, len(addrs))
|
||||
for i, addr := range addrs {
|
||||
resolvedAddrs[i] = fmt.Sprintf("%s:%d", addr.Target, addr.Port)
|
||||
hostPort := port
|
||||
if hostPort == "" && addr.Port > 0 {
|
||||
hostPort = strconv.FormatUint(uint64(addr.Port), 10)
|
||||
}
|
||||
resolvedAddrs[i] = net.JoinHostPort(addr.Target, hostPort)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -375,7 +382,7 @@ func (up *URLPrefix) discoverBackendAddrsIfNeeded() {
|
||||
} else {
|
||||
resolvedAddrs = make([]string, len(addrs))
|
||||
for i, addr := range addrs {
|
||||
resolvedAddrs[i] = addr.String()
|
||||
resolvedAddrs[i] = net.JoinHostPort(addr.String(), port)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -389,17 +396,9 @@ func (up *URLPrefix) discoverBackendAddrsIfNeeded() {
|
||||
var busNew []*backendURL
|
||||
for _, bu := range up.busOriginal {
|
||||
host := bu.Hostname()
|
||||
port := bu.Port()
|
||||
for _, addr := range hostToAddrs[host] {
|
||||
buCopy := *bu
|
||||
buCopy.Host = addr
|
||||
if port != "" {
|
||||
if n := strings.IndexByte(buCopy.Host, ':'); n >= 0 {
|
||||
// Drop the discovered port and substitute it the port specified in bu.
|
||||
buCopy.Host = buCopy.Host[:n]
|
||||
}
|
||||
buCopy.Host += ":" + port
|
||||
}
|
||||
busNew = append(busNew, &backendURL{
|
||||
url: &buCopy,
|
||||
})
|
||||
@@ -783,10 +782,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)
|
||||
|
||||
@@ -3,12 +3,14 @@ package main
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
"gopkg.in/yaml.v2"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/netutil"
|
||||
)
|
||||
|
||||
func TestParseAuthConfigFailure(t *testing.T) {
|
||||
@@ -24,16 +26,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 +298,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:
|
||||
@@ -799,6 +801,75 @@ func TestBrokenBackend(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestDiscoverBackendIPsWithIPV6(t *testing.T) {
|
||||
f := func(actualUrl, expectedUrl string) {
|
||||
t.Helper()
|
||||
up := mustParseURL(actualUrl)
|
||||
up.discoverBackendIPs = true
|
||||
up.loadBalancingPolicy = "least_loaded"
|
||||
|
||||
up.discoverBackendAddrsIfNeeded()
|
||||
pbus := up.bus.Load()
|
||||
bus := *pbus
|
||||
|
||||
if len(bus) != 1 {
|
||||
t.Fatalf("expected url list to be of size 1; got %d instead", len(bus))
|
||||
}
|
||||
|
||||
got := bus[0].url.Host
|
||||
if got != expectedUrl {
|
||||
t.Fatalf(`expected url to be %q; got %q instead`, expectedUrl, bus[0].url.Host)
|
||||
}
|
||||
}
|
||||
|
||||
// Discover backendURL with SRV hostnames
|
||||
customResolver := &fakeResolver{
|
||||
Resolver: &net.Resolver{},
|
||||
// SRV records must return hostname
|
||||
// not an IP address
|
||||
lookupSRVResults: map[string][]*net.SRV{
|
||||
"_vmselect._tcp.selectwithport.": {
|
||||
{
|
||||
Target: "vmselect.local",
|
||||
Port: 8481,
|
||||
},
|
||||
},
|
||||
"_vmselect._tcp.selectwoport.": {
|
||||
{
|
||||
Target: "vmselect.local",
|
||||
},
|
||||
},
|
||||
},
|
||||
lookupIPAddrResults: map[string][]net.IPAddr{
|
||||
"vminsert.local": {
|
||||
{
|
||||
IP: net.ParseIP("10.0.10.13"),
|
||||
},
|
||||
},
|
||||
"ipv6.vminsert.local": {
|
||||
{
|
||||
IP: net.ParseIP("2607:f8b0:400a:80b::200e"),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
origResolver := netutil.Resolver
|
||||
netutil.Resolver = customResolver
|
||||
defer func() {
|
||||
netutil.Resolver = origResolver
|
||||
}()
|
||||
f("http://srv+_vmselect._tcp.selectwithport.:8080", "vmselect.local:8080")
|
||||
f("http://srv+_vmselect._tcp.selectwithport.:", "vmselect.local:8481")
|
||||
f("http://srv+_vmselect._tcp.selectwoport.:8080", "vmselect.local:8080")
|
||||
f("http://srv+_vmselect._tcp.selectwoport.", "vmselect.local:")
|
||||
|
||||
f("http://vminsert.local:8080", "10.0.10.13:8080")
|
||||
f("http://vminsert.local", "10.0.10.13:")
|
||||
f("http://ipv6.vminsert.local:8080", "[2607:f8b0:400a:80b::200e]:8080")
|
||||
f("http://ipv6.vminsert.local", "[2607:f8b0:400a:80b::200e]:")
|
||||
|
||||
}
|
||||
|
||||
func getRegexs(paths []string) []*Regex {
|
||||
var sps []*Regex
|
||||
for _, path := range paths {
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -180,11 +180,7 @@ func (c *Client) Explore() ([]*Series, error) {
|
||||
log.Printf("skip measurement %q since it has no fields", s.Measurement)
|
||||
continue
|
||||
}
|
||||
tags, ok := measurementTags[s.Measurement]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("failed to find tags of measurement %s", s.Measurement)
|
||||
}
|
||||
emptyTags := getEmptyTags(tags, s.LabelPairs)
|
||||
emptyTags := getEmptyTags(measurementTags[s.Measurement], s.LabelPairs)
|
||||
for _, field := range fields {
|
||||
is := &Series{
|
||||
Measurement: s.Measurement,
|
||||
@@ -201,11 +197,16 @@ func (c *Client) Explore() ([]*Series, error) {
|
||||
// getEmptyTags returns tags of a measurement that are missing in a specific series.
|
||||
// Tags represent all tags of a measurement. LabelPairs represent tags of a specific series.
|
||||
func getEmptyTags(tags map[string]struct{}, LabelPairs []LabelPair) []string {
|
||||
if len(tags) == 0 {
|
||||
// fast path: the measurement does not contain any tag
|
||||
return nil
|
||||
}
|
||||
|
||||
labelMap := make(map[string]struct{})
|
||||
for _, pair := range LabelPairs {
|
||||
labelMap[pair.Name] = struct{}{}
|
||||
}
|
||||
result := make([]string, 0, len(labelMap)-len(LabelPairs))
|
||||
var result []string
|
||||
for tag := range tags {
|
||||
if _, ok := labelMap[tag]; !ok {
|
||||
result = append(result, tag)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,23 @@ 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 {
|
||||
// special case for optimisations below
|
||||
// do not call TryPrepareLabels
|
||||
// manually apply sort and limits on demand
|
||||
ic.SortLabelsIfNeeded()
|
||||
if hasLimitsEnabled {
|
||||
if timeserieslimits.IsExceeding(ic.Labels) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
ctx.metricNameBuf = storage.MarshalMetricNameRaw(ctx.metricNameBuf[:0], ic.Labels)
|
||||
labelsLen := len(ic.Labels)
|
||||
for j := range r.Fields {
|
||||
@@ -130,9 +137,10 @@ 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 {
|
||||
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
|
||||
|
||||
@@ -46,6 +46,8 @@ var (
|
||||
"so there is no need in spending additional CPU time on its handling. Staleness markers may exist only in data obtained from Prometheus scrape targets")
|
||||
minWindowForInstantRollupOptimization = flag.Duration("search.minWindowForInstantRollupOptimization", time.Hour*3, "Enable cache-based optimization for repeated queries "+
|
||||
"to /api/v1/query (aka instant queries), which contain rollup functions with lookbehind window exceeding the given value")
|
||||
maxBinaryOpPushdownLabelValues = flag.Int("search.maxBinaryOpPushdownLabelValues", 100, "The maximum number of values for a label in the first expression that can be extracted as a common label filter and pushed down to the second expression in a binary operation. "+
|
||||
"A larger value makes the pushed-down filter more complex but fewer time series will be returned. This flag is useful when selective label contains numerous values, for example `instance`, and storage resources are abundant.")
|
||||
)
|
||||
|
||||
// The minimum number of points per timeseries for enabling time rounding.
|
||||
@@ -582,7 +584,7 @@ func getCommonLabelFilters(tss []*timeseries) []metricsql.LabelFilter {
|
||||
}
|
||||
continue
|
||||
}
|
||||
if len(vc.values) > 100 {
|
||||
if len(vc.values) > *maxBinaryOpPushdownLabelValues {
|
||||
// Too many unique values found for the given tag.
|
||||
// Do not make a filter on such values, since it may slow down
|
||||
// search for matching time series.
|
||||
|
||||
@@ -6,8 +6,6 @@ import (
|
||||
"math"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmselect/netstorage"
|
||||
@@ -16,7 +14,6 @@ import (
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/querytracer"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/storage"
|
||||
"github.com/VictoriaMetrics/metrics"
|
||||
"github.com/VictoriaMetrics/metricsql"
|
||||
)
|
||||
|
||||
@@ -270,7 +267,7 @@ func getReverseCmpOp(op string) string {
|
||||
}
|
||||
|
||||
func parsePromQLWithCache(q string) (metricsql.Expr, error) {
|
||||
pcv := parseCacheV.Get(q)
|
||||
pcv := parseCacheV.get(q)
|
||||
if pcv == nil {
|
||||
e, err := metricsql.Parse(q)
|
||||
if err == nil {
|
||||
@@ -284,7 +281,7 @@ func parsePromQLWithCache(q string) (metricsql.Expr, error) {
|
||||
e: e,
|
||||
err: err,
|
||||
}
|
||||
parseCacheV.Put(q, pcv)
|
||||
parseCacheV.put(q, pcv)
|
||||
}
|
||||
if pcv.err != nil {
|
||||
return nil, pcv.err
|
||||
@@ -328,80 +325,3 @@ func escapeDots(s string) string {
|
||||
}
|
||||
return string(result)
|
||||
}
|
||||
|
||||
var parseCacheV = func() *parseCache {
|
||||
pc := &parseCache{
|
||||
m: make(map[string]*parseCacheValue),
|
||||
}
|
||||
metrics.NewGauge(`vm_cache_requests_total{type="promql/parse"}`, func() float64 {
|
||||
return float64(pc.Requests())
|
||||
})
|
||||
metrics.NewGauge(`vm_cache_misses_total{type="promql/parse"}`, func() float64 {
|
||||
return float64(pc.Misses())
|
||||
})
|
||||
metrics.NewGauge(`vm_cache_entries{type="promql/parse"}`, func() float64 {
|
||||
return float64(pc.Len())
|
||||
})
|
||||
return pc
|
||||
}()
|
||||
|
||||
const parseCacheMaxLen = 10e3
|
||||
|
||||
type parseCacheValue struct {
|
||||
e metricsql.Expr
|
||||
err error
|
||||
}
|
||||
|
||||
type parseCache struct {
|
||||
requests atomic.Uint64
|
||||
misses atomic.Uint64
|
||||
|
||||
m map[string]*parseCacheValue
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
func (pc *parseCache) Requests() uint64 {
|
||||
return pc.requests.Load()
|
||||
}
|
||||
|
||||
func (pc *parseCache) Misses() uint64 {
|
||||
return pc.misses.Load()
|
||||
}
|
||||
|
||||
func (pc *parseCache) Len() uint64 {
|
||||
pc.mu.RLock()
|
||||
n := len(pc.m)
|
||||
pc.mu.RUnlock()
|
||||
return uint64(n)
|
||||
}
|
||||
|
||||
func (pc *parseCache) Get(q string) *parseCacheValue {
|
||||
pc.requests.Add(1)
|
||||
|
||||
pc.mu.RLock()
|
||||
pcv := pc.m[q]
|
||||
pc.mu.RUnlock()
|
||||
|
||||
if pcv == nil {
|
||||
pc.misses.Add(1)
|
||||
}
|
||||
return pcv
|
||||
}
|
||||
|
||||
func (pc *parseCache) Put(q string, pcv *parseCacheValue) {
|
||||
pc.mu.Lock()
|
||||
overflow := len(pc.m) - parseCacheMaxLen
|
||||
if overflow > 0 {
|
||||
// Remove 10% of items from the cache.
|
||||
overflow = int(float64(len(pc.m)) * 0.1)
|
||||
for k := range pc.m {
|
||||
delete(pc.m, k)
|
||||
overflow--
|
||||
if overflow <= 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
pc.m[q] = pcv
|
||||
pc.mu.Unlock()
|
||||
}
|
||||
|
||||
142
app/vmselect/promql/parse_cache.go
Normal file
142
app/vmselect/promql/parse_cache.go
Normal file
@@ -0,0 +1,142 @@
|
||||
// Cache for metricsql expressions
|
||||
// Based on the fastcache idea of locking buckets in order to avoid whole cache locks.
|
||||
// See: https://github.com/VictoriaMetrics/fastcache
|
||||
package promql
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/VictoriaMetrics/metrics"
|
||||
"github.com/VictoriaMetrics/metricsql"
|
||||
|
||||
xxhash "github.com/cespare/xxhash/v2"
|
||||
)
|
||||
|
||||
var parseCacheV = func() *parseCache {
|
||||
pc := newParseCache()
|
||||
metrics.NewGauge(`vm_cache_requests_total{type="promql/parse"}`, func() float64 {
|
||||
return float64(pc.requests())
|
||||
})
|
||||
metrics.NewGauge(`vm_cache_misses_total{type="promql/parse"}`, func() float64 {
|
||||
return float64(pc.misses())
|
||||
})
|
||||
metrics.NewGauge(`vm_cache_entries{type="promql/parse"}`, func() float64 {
|
||||
return float64(pc.len())
|
||||
})
|
||||
return pc
|
||||
}()
|
||||
|
||||
const (
|
||||
parseBucketCount = 128
|
||||
|
||||
parseCacheMaxLen int = 10e3
|
||||
|
||||
parseBucketMaxLen int = parseCacheMaxLen / parseBucketCount
|
||||
|
||||
parseBucketFreePercent float64 = 0.1
|
||||
)
|
||||
|
||||
type parseCacheValue struct {
|
||||
e metricsql.Expr
|
||||
err error
|
||||
}
|
||||
|
||||
type parseBucket struct {
|
||||
m map[string]*parseCacheValue
|
||||
mu sync.RWMutex
|
||||
requests atomic.Uint64
|
||||
misses atomic.Uint64
|
||||
}
|
||||
|
||||
type parseCache struct {
|
||||
buckets [parseBucketCount]parseBucket
|
||||
}
|
||||
|
||||
func newParseCache() *parseCache {
|
||||
pc := new(parseCache)
|
||||
for i := 0; i < parseBucketCount; i++ {
|
||||
pc.buckets[i] = newParseBucket()
|
||||
}
|
||||
return pc
|
||||
}
|
||||
|
||||
func (pc *parseCache) put(q string, pcv *parseCacheValue) {
|
||||
h := xxhash.Sum64String(q)
|
||||
idx := h % parseBucketCount
|
||||
pc.buckets[idx].put(q, pcv)
|
||||
}
|
||||
|
||||
func (pc *parseCache) get(q string) *parseCacheValue {
|
||||
h := xxhash.Sum64String(q)
|
||||
idx := h % parseBucketCount
|
||||
return pc.buckets[idx].get(q)
|
||||
}
|
||||
|
||||
func (pc *parseCache) requests() uint64 {
|
||||
var n uint64
|
||||
for i := 0; i < parseBucketCount; i++ {
|
||||
n += pc.buckets[i].requests.Load()
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
func (pc *parseCache) misses() uint64 {
|
||||
var n uint64
|
||||
for i := 0; i < parseBucketCount; i++ {
|
||||
n += pc.buckets[i].misses.Load()
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
func (pc *parseCache) len() uint64 {
|
||||
var n uint64
|
||||
for i := 0; i < parseBucketCount; i++ {
|
||||
n += pc.buckets[i].len()
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
func newParseBucket() parseBucket {
|
||||
return parseBucket{
|
||||
m: make(map[string]*parseCacheValue, parseBucketMaxLen),
|
||||
}
|
||||
}
|
||||
|
||||
func (pb *parseBucket) len() uint64 {
|
||||
pb.mu.RLock()
|
||||
n := len(pb.m)
|
||||
pb.mu.RUnlock()
|
||||
return uint64(n)
|
||||
}
|
||||
|
||||
func (pb *parseBucket) get(q string) *parseCacheValue {
|
||||
pb.requests.Add(1)
|
||||
|
||||
pb.mu.RLock()
|
||||
pcv := pb.m[q]
|
||||
pb.mu.RUnlock()
|
||||
|
||||
if pcv == nil {
|
||||
pb.misses.Add(1)
|
||||
}
|
||||
return pcv
|
||||
}
|
||||
|
||||
func (pb *parseBucket) put(q string, pcv *parseCacheValue) {
|
||||
pb.mu.Lock()
|
||||
overflow := len(pb.m) - parseBucketMaxLen
|
||||
if overflow > 0 {
|
||||
// Remove parseBucketDeletePercent*100 % of items from the bucket.
|
||||
overflow = int(float64(len(pb.m)) * parseBucketFreePercent)
|
||||
for k := range pb.m {
|
||||
delete(pb.m, k)
|
||||
overflow--
|
||||
if overflow <= 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
pb.m[q] = pcv
|
||||
pb.mu.Unlock()
|
||||
}
|
||||
129
app/vmselect/promql/parse_cache_test.go
Normal file
129
app/vmselect/promql/parse_cache_test.go
Normal file
@@ -0,0 +1,129 @@
|
||||
package promql
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/VictoriaMetrics/metricsql"
|
||||
)
|
||||
|
||||
func testGetParseCacheValue(q string) *parseCacheValue {
|
||||
e, err := metricsql.Parse(q)
|
||||
return &parseCacheValue{
|
||||
e: e,
|
||||
err: err,
|
||||
}
|
||||
}
|
||||
|
||||
func testGenerateQueries(items int) []string {
|
||||
queries := make([]string, items)
|
||||
for i := 0; i < items; i++ {
|
||||
queries[i] = fmt.Sprintf(`node_time_seconds{instance="node%d", job="job%d"}`, i, i)
|
||||
}
|
||||
return queries
|
||||
}
|
||||
|
||||
func TestParseCache(t *testing.T) {
|
||||
pc := newParseCache()
|
||||
if pc.len() != 0 || pc.misses() != 0 || pc.requests() != 0 {
|
||||
t.Errorf("unexpected pc.Len()=%d, pc.Misses()=%d, pc.Requests()=%d; expected all to be zero.", pc.len(), pc.misses(), pc.requests())
|
||||
}
|
||||
|
||||
q1 := `foo{bar="baz"}`
|
||||
v1 := testGetParseCacheValue(q1)
|
||||
|
||||
q2 := `foo1{bar1="baz1"}`
|
||||
v2 := testGetParseCacheValue(q2)
|
||||
|
||||
pc.put(q1, v1)
|
||||
if pc.len() != 1 {
|
||||
t.Errorf("unexpected value obtained; got %d; want %d", pc.len(), 1)
|
||||
}
|
||||
|
||||
if res := pc.get(q2); res != nil {
|
||||
t.Errorf("unexpected non-empty value obtained from cache: %d ", res)
|
||||
}
|
||||
if pc.len() != 1 {
|
||||
t.Errorf("unexpected value obtained; got %d; want %d", pc.len(), 1)
|
||||
}
|
||||
if miss := pc.misses(); miss != 1 {
|
||||
t.Errorf("unexpected value obtained; got %d; want %d", miss, 1)
|
||||
}
|
||||
if req := pc.requests(); req != 1 {
|
||||
t.Errorf("unexpected value obtained; got %d; want %d", req, 1)
|
||||
}
|
||||
|
||||
pc.put(q2, v2)
|
||||
if pc.len() != 2 {
|
||||
t.Errorf("unexpected value obtained; got %d; want %d", pc.len(), 2)
|
||||
}
|
||||
|
||||
if res := pc.get(q1); res != v1 {
|
||||
t.Errorf("unexpected value obtained; got %v; want %v", res, v1)
|
||||
}
|
||||
|
||||
if res := pc.get(q2); res != v2 {
|
||||
t.Errorf("unexpected value obtained; got %v; want %v", res, v2)
|
||||
}
|
||||
|
||||
pc.put(q2, v2)
|
||||
if pc.len() != 2 {
|
||||
t.Errorf("unexpected value obtained; got %d; want %d", pc.len(), 2)
|
||||
}
|
||||
if miss := pc.misses(); miss != 1 {
|
||||
t.Errorf("unexpected value obtained; got %d; want %d", miss, 1)
|
||||
}
|
||||
if req := pc.requests(); req != 3 {
|
||||
t.Errorf("unexpected value obtained; got %d; want %d", req, 3)
|
||||
}
|
||||
|
||||
if res := pc.get(q2); res != v2 {
|
||||
t.Errorf("unexpected value obtained; got %v; want %v", res, v2)
|
||||
}
|
||||
if pc.len() != 2 {
|
||||
t.Errorf("unexpected value obtained; got %d; want %d", pc.len(), 2)
|
||||
}
|
||||
if miss := pc.misses(); miss != 1 {
|
||||
t.Errorf("unexpected value obtained; got %d; want %d", miss, 1)
|
||||
}
|
||||
if req := pc.requests(); req != 4 {
|
||||
t.Errorf("unexpected value obtained; got %d; want %d", req, 4)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseCacheBucketOverflow(t *testing.T) {
|
||||
b := newParseBucket()
|
||||
var expectedLen uint64
|
||||
|
||||
// +2 for overflow and clean up
|
||||
queries := testGenerateQueries(parseBucketMaxLen + 2)
|
||||
|
||||
// Same value for all keys
|
||||
v := testGetParseCacheValue(queries[0])
|
||||
|
||||
// Fill bucket
|
||||
for i := 0; i < parseBucketMaxLen; i++ {
|
||||
b.put(queries[i], v)
|
||||
}
|
||||
expectedLen = uint64(parseBucketMaxLen)
|
||||
if b.len() != expectedLen {
|
||||
t.Errorf("unexpected value obtained; got %v; want %v", b.len(), expectedLen)
|
||||
}
|
||||
|
||||
// Overflow bucket
|
||||
expectedLen = uint64(parseBucketMaxLen + 1)
|
||||
b.put(queries[parseBucketMaxLen], v)
|
||||
if b.len() != uint64(expectedLen) {
|
||||
t.Errorf("unexpected value obtained; got %v; want %v", b.len(), expectedLen)
|
||||
}
|
||||
|
||||
// Clean up;
|
||||
oldLen := b.len()
|
||||
overflow := int(float64(oldLen) * parseBucketFreePercent)
|
||||
expectedLen = oldLen - uint64(overflow) + 1 // +1 for new entry
|
||||
|
||||
b.put(queries[parseBucketMaxLen+1], v)
|
||||
if b.len() != expectedLen {
|
||||
t.Errorf("unexpected value obtained; got %v; want %v", b.len(), expectedLen)
|
||||
}
|
||||
}
|
||||
235
app/vmselect/promql/parse_cache_timing_test.go
Normal file
235
app/vmselect/promql/parse_cache_timing_test.go
Normal file
@@ -0,0 +1,235 @@
|
||||
package promql
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func BenchmarkCachePutNoOverFlow(b *testing.B) {
|
||||
const items int = (parseCacheMaxLen / 2)
|
||||
pc := newParseCache()
|
||||
|
||||
queries := testGenerateQueries(items)
|
||||
v := testGetParseCacheValue(queries[0])
|
||||
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
b.RunParallel(func(pb *testing.PB) {
|
||||
for pb.Next() {
|
||||
for i := 0; i < items; i++ {
|
||||
pc.put(queries[i], v)
|
||||
}
|
||||
}
|
||||
})
|
||||
if pc.len() != uint64(items) {
|
||||
b.Errorf("unexpected value obtained; got %d; want %d", pc.len(), items)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkCacheGetNoOverflow(b *testing.B) {
|
||||
const items int = parseCacheMaxLen / 2
|
||||
pc := newParseCache()
|
||||
|
||||
queries := testGenerateQueries(items)
|
||||
v := testGetParseCacheValue(queries[0])
|
||||
|
||||
for i := 0; i < len(queries); i++ {
|
||||
pc.put(queries[i], v)
|
||||
}
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
b.RunParallel(func(pb *testing.PB) {
|
||||
for pb.Next() {
|
||||
for i := 0; i < items; i++ {
|
||||
if v := pc.get(queries[i]); v == nil {
|
||||
b.Errorf("unexpected nil value obtained from cache for query: %s ", queries[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func BenchmarkCachePutGetNoOverflow(b *testing.B) {
|
||||
const items int = parseCacheMaxLen / 2
|
||||
pc := newParseCache()
|
||||
|
||||
queries := testGenerateQueries(items)
|
||||
v := testGetParseCacheValue(queries[0])
|
||||
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
b.RunParallel(func(pb *testing.PB) {
|
||||
for pb.Next() {
|
||||
for i := 0; i < items; i++ {
|
||||
pc.put(queries[i], v)
|
||||
if res := pc.get(queries[i]); res == nil {
|
||||
b.Errorf("unexpected nil value obtained from cache for query: %s ", queries[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
if pc.len() != uint64(items) {
|
||||
b.Errorf("unexpected value obtained; got %d; want %d", pc.len(), items)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkCachePutOverflow(b *testing.B) {
|
||||
const items int = parseCacheMaxLen + (parseCacheMaxLen / 2)
|
||||
c := newParseCache()
|
||||
|
||||
queries := testGenerateQueries(items)
|
||||
v := testGetParseCacheValue(queries[0])
|
||||
|
||||
for i := 0; i < parseCacheMaxLen; i++ {
|
||||
c.put(queries[i], v)
|
||||
}
|
||||
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
b.RunParallel(func(pb *testing.PB) {
|
||||
for pb.Next() {
|
||||
for i := parseCacheMaxLen; i < items; i++ {
|
||||
c.put(queries[i], v)
|
||||
}
|
||||
}
|
||||
})
|
||||
maxElemnts := uint64(parseCacheMaxLen + parseBucketCount)
|
||||
if c.len() > maxElemnts {
|
||||
b.Errorf("cache length is more than expected; got %d, expected %d", c.len(), maxElemnts)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkCachePutGetOverflow(b *testing.B) {
|
||||
const items int = parseCacheMaxLen + (parseCacheMaxLen / 2)
|
||||
c := newParseCache()
|
||||
|
||||
queries := testGenerateQueries(items)
|
||||
v := testGetParseCacheValue(queries[0])
|
||||
|
||||
for i := 0; i < parseCacheMaxLen; i++ {
|
||||
c.put(queries[i], v)
|
||||
}
|
||||
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
b.RunParallel(func(pb *testing.PB) {
|
||||
for pb.Next() {
|
||||
for i := parseCacheMaxLen; i < items; i++ {
|
||||
c.put(queries[i], v)
|
||||
c.get(queries[i])
|
||||
}
|
||||
}
|
||||
})
|
||||
maxElemnts := uint64(parseCacheMaxLen + parseBucketCount)
|
||||
if c.len() > maxElemnts {
|
||||
b.Errorf("cache length is more than expected; got %d, expected %d", c.len(), maxElemnts)
|
||||
}
|
||||
}
|
||||
|
||||
var testSimpleQueries = []string{
|
||||
`m{a="b"}`,
|
||||
`{a="b"}`,
|
||||
`m{c="d",a="b"}`,
|
||||
`{a="b",c="d"}`,
|
||||
`m1{a="foo"}`,
|
||||
`m2{a="bar"}`,
|
||||
`m1{b="foo"}`,
|
||||
`m2{b="bar"}`,
|
||||
`m1{a="foo",b="bar"}`,
|
||||
`m2{b="bar",c="x"}`,
|
||||
`{b="bar"}`,
|
||||
}
|
||||
|
||||
func BenchmarkParsePromQLWithCacheSimple(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
for j := 0; j < len(testSimpleQueries); j++ {
|
||||
_, err := parsePromQLWithCache(testSimpleQueries[j])
|
||||
if err != nil {
|
||||
b.Errorf("unexpected error: %s", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkParsePromQLWithCacheSimpleParallel(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
b.RunParallel(func(pb *testing.PB) {
|
||||
for pb.Next() {
|
||||
for i := 0; i < len(testSimpleQueries); i++ {
|
||||
_, err := parsePromQLWithCache(testSimpleQueries[i])
|
||||
if err != nil {
|
||||
b.Errorf("unexpected error: %s", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
var testComplexQueries = []string{
|
||||
`sort_desc(label_set(2, "foo", "bar") * ignoring(a) (label_set(time(), "foo", "bar") or label_set(10, "foo", "qwert")))`,
|
||||
`sum(a.b{c="d.e",x=~"a.b.+[.a]",y!~"aaa.bb|cc.dd"}) + avg_over_time(1,sum({x=~"aa.bb"}))`,
|
||||
`sort((label_set(time() offset 100s, "foo", "bar"), label_set(time()+10, "foo", "baz") offset 50s) offset 400s)`,
|
||||
`sort(label_map((
|
||||
label_set(time(), "label", "v1"),
|
||||
label_set(time()+100, "label", "v2"),
|
||||
label_set(time()+200, "label", "v3"),
|
||||
label_set(time()+300, "x", "y"),
|
||||
label_set(time()+400, "label", "v4"),
|
||||
), "label", "v1", "foo", "v2", "bar", "", "qwe", "v4", ""))`,
|
||||
`sort(labels_equal((
|
||||
label_set(10, "instance", "qwe", "host", "rty"),
|
||||
label_set(20, "instance", "qwe", "host", "qwe"),
|
||||
label_set(30, "aaa", "bbb", "instance", "foo", "host", "foo"),
|
||||
), "instance", "host"))`,
|
||||
`with (
|
||||
x = (
|
||||
label_set(time() > 1500, "foo", "123.456", "__name__", "aaa"),
|
||||
label_set(-time(), "foo", "bar", "__name__", "bbb"),
|
||||
label_set(-time(), "__name__", "bxs"),
|
||||
label_set(-time(), "foo", "45", "bar", "xs"),
|
||||
)
|
||||
)
|
||||
sort(x + label_value(x, "foo"))`,
|
||||
`label_replace(
|
||||
label_replace(
|
||||
label_replace(time(), "__name__", "x${1}y", "foo", ".*"),
|
||||
"xxx", "foo${1}bar(${1})", "__name__", "(.+)"),
|
||||
"xxx", "AA$1", "xxx", "foox(.+)"
|
||||
)`,
|
||||
`sort_desc(union(
|
||||
label_set(time() > 1400, "__name__", "x", "foo", "bar"),
|
||||
label_set(time() < 1700, "__name__", "y", "foo", "baz")) default 123)`,
|
||||
`sort(histogram_quantile(0.6,
|
||||
label_set(90, "foo", "bar", "le", "10")
|
||||
or label_set(100, "foo", "bar", "le", "30")
|
||||
or label_set(300, "foo", "bar", "le", "+Inf")
|
||||
or label_set(200, "tag", "xx", "le", "10")
|
||||
or label_set(300, "tag", "xx", "le", "30")
|
||||
))`,
|
||||
}
|
||||
|
||||
func BenchmarkParsePromQLWithCacheComplex(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
for j := 0; j < len(testComplexQueries); j++ {
|
||||
_, err := parsePromQLWithCache(testComplexQueries[j])
|
||||
if err != nil {
|
||||
b.Errorf("unexpected error: %s", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkParsePromQLWithCacheComplexParallel(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
b.RunParallel(func(pb *testing.PB) {
|
||||
for pb.Next() {
|
||||
for i := 0; i < len(testComplexQueries); i++ {
|
||||
_, err := parsePromQLWithCache(testComplexQueries[i])
|
||||
if err != nil {
|
||||
b.Errorf("unexpected error: %s", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -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,16 @@ export interface LogHits {
|
||||
[key: string]: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface ReportMetaData {
|
||||
id: number;
|
||||
title: string;
|
||||
endpoint: string;
|
||||
comment: string;
|
||||
params: Record<string, string>;
|
||||
}
|
||||
|
||||
export interface LogsFiledValues {
|
||||
value: string;
|
||||
hits: number;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
import React, { FC, useCallback, useEffect, useMemo, useState } from "preact/compat";
|
||||
import Autocomplete, { AutocompleteOptions } from "../../../Main/Autocomplete/Autocomplete";
|
||||
import { AUTOCOMPLETE_LIMITS } from "../../../../constants/queryAutocomplete";
|
||||
import { QueryEditorAutocompleteProps } from "../QueryEditor";
|
||||
import { getContextData, splitLogicalParts } from "./parser";
|
||||
import { ContextType, LogicalPart, LogicalPartType } from "./types";
|
||||
import { useFetchLogsQLOptions } from "./useFetchLogsQLOptions";
|
||||
import { pipeList } from "./pipes";
|
||||
|
||||
const LogsQueryEditorAutocomplete: FC<QueryEditorAutocompleteProps> = ({
|
||||
value,
|
||||
anchorEl,
|
||||
caretPosition,
|
||||
hasHelperText,
|
||||
onSelect,
|
||||
onFoundOptions
|
||||
}) => {
|
||||
const [offsetPos, setOffsetPos] = useState({ top: 0, left: 0 });
|
||||
|
||||
const fullValue = useMemo(() => {
|
||||
if (caretPosition[0] !== caretPosition[1]) return { valueBeforeCursor: value, valueAfterCursor: "" };
|
||||
const valueBeforeCursor = value.substring(0, caretPosition[0]);
|
||||
const valueAfterCursor = value.substring(caretPosition[1]);
|
||||
return { valueBeforeCursor, valueAfterCursor };
|
||||
}, [value, caretPosition]);
|
||||
|
||||
const logicalParts = useMemo(() => {
|
||||
return splitLogicalParts(value);
|
||||
}, [value]);
|
||||
|
||||
const contextData = useMemo(() => {
|
||||
if (caretPosition[0] !== caretPosition[1]) return;
|
||||
const part = logicalParts.find(p => caretPosition[0] >= p.position[0] && caretPosition[0] <= p.position[1]);
|
||||
if (!part) return;
|
||||
const cursorStartPosition = caretPosition[0] - part.position[0];
|
||||
return {
|
||||
...part,
|
||||
...getContextData(part, cursorStartPosition)
|
||||
};
|
||||
}, [logicalParts, caretPosition]);
|
||||
|
||||
const { fieldNames, fieldValues, loading } = useFetchLogsQLOptions(contextData);
|
||||
|
||||
const options = useMemo(() => {
|
||||
switch (contextData?.contextType) {
|
||||
case ContextType.FilterName:
|
||||
case ContextType.FilterUnknown:
|
||||
return fieldNames;
|
||||
case ContextType.FilterValue:
|
||||
return fieldValues;
|
||||
case ContextType.PipeName:
|
||||
return pipeList;
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
}, [contextData, fieldNames, fieldValues]);
|
||||
|
||||
const getUpdatedValue = (insertValue: string, logicalParts: LogicalPart[], id?: number) => {
|
||||
return logicalParts.reduce((acc, part) => {
|
||||
const value = part.id === id ? insertValue : part.value;
|
||||
const separator = part.type === LogicalPartType.Pipe ? " | " : " ";
|
||||
return `${acc}${separator}${value}`;
|
||||
}, "").trim();
|
||||
};
|
||||
|
||||
const getModifyInsert = (insert: string, contextType: ContextType, value = "", insertType?: string) => {
|
||||
let modifiedInsert = insert;
|
||||
|
||||
if (insertType === ContextType.FilterName) {
|
||||
modifiedInsert += ":";
|
||||
} else if (contextType === ContextType.FilterValue) {
|
||||
const insertWithQuotes = value.startsWith("_stream:") ? modifiedInsert : `"${modifiedInsert}"`;
|
||||
modifiedInsert = `${contextData?.filterName || ""}:${insertWithQuotes}`;
|
||||
}
|
||||
|
||||
return modifiedInsert;
|
||||
};
|
||||
|
||||
const handleSelect = useCallback((insert: string, item: AutocompleteOptions) => {
|
||||
const {
|
||||
id,
|
||||
contextType = ContextType.FilterUnknown,
|
||||
value = "",
|
||||
position = [0, 0]
|
||||
} = contextData || {};
|
||||
|
||||
const insertValue = getModifyInsert(insert, contextType, value, item.type);
|
||||
const newValue = getUpdatedValue(insertValue, logicalParts, id);
|
||||
const updatedPosition = (position[0] || 1) + insertValue.length + (item.type === ContextType.PipeName ? 1 : 0);
|
||||
|
||||
onSelect(newValue, updatedPosition);
|
||||
}, [contextData, logicalParts]);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (!anchorEl.current) {
|
||||
setOffsetPos({ top: 0, left: 0 });
|
||||
return;
|
||||
}
|
||||
|
||||
const element = anchorEl.current.querySelector("textarea") || anchorEl.current;
|
||||
const style = window.getComputedStyle(element);
|
||||
const fontSize = `${style.getPropertyValue("font-size")}`;
|
||||
const fontFamily = `${style.getPropertyValue("font-family")}`;
|
||||
const lineHeight = parseInt(`${style.getPropertyValue("line-height")}`);
|
||||
|
||||
const span = document.createElement("div");
|
||||
span.style.font = `${fontSize} ${fontFamily}`;
|
||||
span.style.padding = style.getPropertyValue("padding");
|
||||
span.style.lineHeight = `${lineHeight}px`;
|
||||
span.style.width = `${element.offsetWidth}px`;
|
||||
span.style.maxWidth = `${element.offsetWidth}px`;
|
||||
span.style.whiteSpace = style.getPropertyValue("white-space");
|
||||
span.style.overflowWrap = style.getPropertyValue("overflow-wrap");
|
||||
|
||||
const marker = document.createElement("span");
|
||||
span.appendChild(document.createTextNode(fullValue.valueBeforeCursor || ""));
|
||||
span.appendChild(marker);
|
||||
span.appendChild(document.createTextNode(fullValue.valueAfterCursor || ""));
|
||||
document.body.appendChild(span);
|
||||
|
||||
const spanRect = span.getBoundingClientRect();
|
||||
const markerRect = marker.getBoundingClientRect();
|
||||
|
||||
const leftOffset = markerRect.left - spanRect.left;
|
||||
const topOffset = markerRect.bottom - spanRect.bottom - (hasHelperText ? lineHeight : 0);
|
||||
setOffsetPos({ top: topOffset, left: leftOffset });
|
||||
|
||||
span.remove();
|
||||
marker.remove();
|
||||
}, [anchorEl, caretPosition, hasHelperText, fullValue]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Autocomplete
|
||||
loading={loading}
|
||||
disabledFullScreen
|
||||
value={contextData?.valueContext || ""}
|
||||
options={options}
|
||||
anchor={anchorEl}
|
||||
minLength={0}
|
||||
offset={offsetPos}
|
||||
onSelect={handleSelect}
|
||||
onFoundOptions={onFoundOptions}
|
||||
maxDisplayResults={{
|
||||
limit: AUTOCOMPLETE_LIMITS.displayResults,
|
||||
message: "Please, specify the query more precisely."
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default LogsQueryEditorAutocomplete;
|
||||
@@ -0,0 +1,117 @@
|
||||
import { ContextData, ContextType, LogicalPart, LogicalPartPosition, LogicalPartType } from "./types";
|
||||
import { pipeList } from "./pipes";
|
||||
|
||||
const BUILDER_OPERATORS = ["AND", "OR", "NOT"];
|
||||
const PIPE_NAMES = pipeList.map(p => p.value);
|
||||
|
||||
export const splitLogicalParts = (expr: string) => {
|
||||
// Replace spaces around the colon (:) with just the colon, removing the spaces
|
||||
const input = expr; //.replace(/\s*:\s*/g, ":");
|
||||
const parts: LogicalPart[] = [];
|
||||
let currentPart = "";
|
||||
let isPipePart = false;
|
||||
|
||||
const quotes = ["'", "\"", "`"];
|
||||
let insideQuotes = false;
|
||||
let expectedQuote = "";
|
||||
|
||||
const openBrackets = ["(", "[", "{"];
|
||||
const closeBrackets = [")", "]", "}"];
|
||||
const brackets = [...openBrackets, ...closeBrackets];
|
||||
let insideBrackets = 0;
|
||||
|
||||
let startIndex = 0;
|
||||
|
||||
for (let i = 0; i < input.length; i++) {
|
||||
const char = input[i];
|
||||
|
||||
// Check if the current character is a quote
|
||||
if (quotes.includes(char)) {
|
||||
const isClosedQuote: boolean = insideQuotes && (char === expectedQuote);
|
||||
insideQuotes = !isClosedQuote;
|
||||
expectedQuote = isClosedQuote ? "" : char;
|
||||
}
|
||||
|
||||
// Check if the current character is a bracket
|
||||
if (!insideQuotes && brackets.includes(char)) {
|
||||
const dir = openBrackets.includes(char) ? 1 : -1;
|
||||
insideBrackets += dir;
|
||||
}
|
||||
|
||||
// Check if the current character is a pipe
|
||||
if ((!insideQuotes && !insideBrackets && char === "|")) {
|
||||
isPipePart = true;
|
||||
const countStartSpaces = currentPart.match(/^ */)?.[0].length || 0;
|
||||
const countEndSpaces = currentPart.match(/ *$/)?.[0].length || 0;
|
||||
pushPart(currentPart, true, [startIndex + countStartSpaces, i - countEndSpaces - 1], parts);
|
||||
currentPart = "";
|
||||
startIndex = i + 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if the current character is a space
|
||||
if (!isPipePart && !insideQuotes && !insideBrackets && char === " ") {
|
||||
const nextStr = input.slice(i).replace(/^\s*/, "");
|
||||
const prevStr = input.slice(0, i).replace(/\s*$/, "");
|
||||
if (!nextStr.startsWith(":") && !prevStr.endsWith(":")) {
|
||||
pushPart(currentPart, false, [startIndex, i - 1], parts);
|
||||
currentPart = "";
|
||||
startIndex = i + 1;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
currentPart += char;
|
||||
}
|
||||
|
||||
// push the last part
|
||||
pushPart(currentPart, isPipePart, [startIndex, input.length], parts);
|
||||
|
||||
return parts;
|
||||
};
|
||||
|
||||
const pushPart = (currentPart: string, isPipePart: boolean, position: LogicalPartPosition, parts: LogicalPart[]) => {
|
||||
const trimmedPart = currentPart.trim();
|
||||
if (!trimmedPart) return;
|
||||
const isOperator = BUILDER_OPERATORS.includes(trimmedPart.toUpperCase());
|
||||
parts.push({
|
||||
id: parts.length,
|
||||
value: trimmedPart,
|
||||
position,
|
||||
type: isPipePart
|
||||
? LogicalPartType.Pipe
|
||||
: isOperator ? LogicalPartType.Operator : LogicalPartType.Filter,
|
||||
});
|
||||
};
|
||||
|
||||
export const getContextData = (part: LogicalPart, cursorPos: number) => {
|
||||
const valueBeforeCursor = part.value.substring(0, cursorPos);
|
||||
const valueAfterCursor = part.value.substring(cursorPos);
|
||||
|
||||
const metaData: ContextData = {
|
||||
valueBeforeCursor,
|
||||
valueAfterCursor,
|
||||
valueContext: part.value,
|
||||
contextType: ContextType.Unknown,
|
||||
};
|
||||
|
||||
if (part.type === LogicalPartType.Filter) {
|
||||
const noColon = !valueBeforeCursor.includes(":") && !valueAfterCursor.includes(":");
|
||||
if (noColon) {
|
||||
metaData.contextType = ContextType.FilterUnknown;
|
||||
} else if (valueBeforeCursor.includes(":")) {
|
||||
const [filterName, filterValue] = valueBeforeCursor.split(":");
|
||||
metaData.contextType = ContextType.FilterValue;
|
||||
metaData.filterName = filterName;
|
||||
metaData.valueContext = filterValue;
|
||||
} else {
|
||||
metaData.contextType = ContextType.FilterName;
|
||||
}
|
||||
} else if (part.type === LogicalPartType.Pipe) {
|
||||
const valueStartWithPipe = PIPE_NAMES.some(p => part.value.startsWith(p));
|
||||
metaData.contextType = valueStartWithPipe ? ContextType.PipeValue : ContextType.PipeName;
|
||||
}
|
||||
|
||||
metaData.valueContext = metaData.valueContext.replace(/^["']|["']$/g, "");
|
||||
return metaData;
|
||||
};
|
||||
@@ -0,0 +1,130 @@
|
||||
import React from "react";
|
||||
import { ContextType } from "./types";
|
||||
import { FunctionIcon } from "../../../Main/Icons";
|
||||
|
||||
const docsUrl = "https://docs.victoriametrics.com/victorialogs/logsql";
|
||||
const classLink = "vm-link vm-link_colored";
|
||||
|
||||
const prepareDescription = (text: string): string => {
|
||||
const replaceClass = `$1 target="_blank" class="${classLink}" $2`;
|
||||
const replaceHref = `$1 $2${docsUrl}#`;
|
||||
return text
|
||||
.replace(/(<a) (href=")#/gm, replaceHref)
|
||||
.replace(/(<a) (href="[^"]+")/gm, replaceClass);
|
||||
};
|
||||
|
||||
export const pipeList = [
|
||||
{
|
||||
"value": "copy",
|
||||
"description": "<a href=\"#copy-pipe\"><code>copy</code></a> copies <a href=\"https://docs.victoriametrics.com/victorialogs/keyconcepts/#data-model\">log fields</a>."
|
||||
},
|
||||
{
|
||||
"value": "delete",
|
||||
"description": "<a href=\"#delete-pipe\"><code>delete</code></a> deletes <a href=\"https://docs.victoriametrics.com/victorialogs/keyconcepts/#data-model\">log fields</a>."
|
||||
},
|
||||
{
|
||||
"value": "drop_empty_fields",
|
||||
"description": "<a href=\"#drop_empty_fields-pipe\"><code>drop_empty_fields</code></a> drops <a href=\"https://docs.victoriametrics.com/victorialogs/keyconcepts/#data-model\">log fields</a> with empty values."
|
||||
},
|
||||
{
|
||||
"value": "extract",
|
||||
"description": "<a href=\"#extract-pipe\"><code>extract</code></a> extracts the specified text into the given log fields."
|
||||
},
|
||||
{
|
||||
"value": "extract_regexp",
|
||||
"description": "<a href=\"#extract_regexp-pipe\"><code>extract_regexp</code></a> extracts the specified text into the given log fields via <a href=\"https://github.com/google/re2/wiki/Syntax\" rel=\"external\" target=\"_blank\">RE2 regular expressions</a>."
|
||||
},
|
||||
{
|
||||
"value": "field_names",
|
||||
"description": "<a href=\"#field_names-pipe\"><code>field_names</code></a> returns all the names of <a href=\"https://docs.victoriametrics.com/victorialogs/keyconcepts/#data-model\">log fields</a>."
|
||||
},
|
||||
{
|
||||
"value": "field_values",
|
||||
"description": "<a href=\"#field_values-pipe\"><code>field_values</code></a> returns all the values for the given <a href=\"https://docs.victoriametrics.com/victorialogs/keyconcepts/#data-model\">log field</a>."
|
||||
},
|
||||
{
|
||||
"value": "fields",
|
||||
"description": "<a href=\"#fields-pipe\"><code>fields</code></a> selects the given set of <a href=\"https://docs.victoriametrics.com/victorialogs/keyconcepts/#data-model\">log fields</a>."
|
||||
},
|
||||
{
|
||||
"value": "filter",
|
||||
"description": "<a href=\"#filter-pipe\"><code>filter</code></a> applies additional <a href=\"#filters\">filters</a> to results."
|
||||
},
|
||||
{
|
||||
"value": "format",
|
||||
"description": "<a href=\"#format-pipe\"><code>format</code></a> formats output field from input <a href=\"https://docs.victoriametrics.com/victorialogs/keyconcepts/#data-model\">log fields</a>."
|
||||
},
|
||||
{
|
||||
"value": "limit",
|
||||
"description": "<a href=\"#limit-pipe\"><code>limit</code></a> limits the number selected logs."
|
||||
},
|
||||
{
|
||||
"value": "math",
|
||||
"description": "<a href=\"#math-pipe\"><code>math</code></a> performs mathematical calculations over <a href=\"https://docs.victoriametrics.com/victorialogs/keyconcepts/#data-model\">log fields</a>."
|
||||
},
|
||||
{
|
||||
"value": "offset",
|
||||
"description": "<a href=\"#offset-pipe\"><code>offset</code></a> skips the given number of selected logs."
|
||||
},
|
||||
{
|
||||
"value": "pack_json",
|
||||
"description": "<a href=\"#pack_json-pipe\"><code>pack_json</code></a> packs <a href=\"https://docs.victoriametrics.com/victorialogs/keyconcepts/#data-model\">log fields</a> into JSON object."
|
||||
},
|
||||
{
|
||||
"value": "pack_logfmt",
|
||||
"description": "<a href=\"#pack_logfmt-pipe\"><code>pack_logfmt</code></a> packs <a href=\"https://docs.victoriametrics.com/victorialogs/keyconcepts/#data-model\">log fields</a> into <a href=\"https://brandur.org/logfmt\" rel=\"external\" target=\"_blank\">logfmt</a> message."
|
||||
},
|
||||
{
|
||||
"value": "rename",
|
||||
"description": "<a href=\"#rename-pipe\"><code>rename</code></a> renames <a href=\"https://docs.victoriametrics.com/victorialogs/keyconcepts/#data-model\">log fields</a>."
|
||||
},
|
||||
{
|
||||
"value": "replace",
|
||||
"description": "<a href=\"#replace-pipe\"><code>replace</code></a> replaces substrings in the specified <a href=\"https://docs.victoriametrics.com/victorialogs/keyconcepts/#data-model\">log fields</a>."
|
||||
},
|
||||
{
|
||||
"value": "replace_regexp",
|
||||
"description": "<a href=\"#replace_regexp-pipe\"><code>replace_regexp</code></a> updates <a href=\"https://docs.victoriametrics.com/victorialogs/keyconcepts/#data-model\">log fields</a> with regular expressions."
|
||||
},
|
||||
{
|
||||
"value": "sort",
|
||||
"description": "<a href=\"#sort-pipe\"><code>sort</code></a> sorts logs by the given <a href=\"https://docs.victoriametrics.com/victorialogs/keyconcepts/#data-model\">fields</a>."
|
||||
},
|
||||
{
|
||||
"value": "stats",
|
||||
"description": "<a href=\"#stats-pipe\"><code>stats</code></a> calculates various stats over the selected logs."
|
||||
},
|
||||
{
|
||||
"value": "stream_context",
|
||||
"description": "<a href=\"#stream_context-pipe\"><code>stream_context</code></a> allows selecting surrounding logs in front and after the matching logs\nper each <a href=\"/victorialogs/keyconcepts/#stream-fields\">log stream</a>."
|
||||
},
|
||||
{
|
||||
"value": "top",
|
||||
"description": "<a href=\"#top-pipe\"><code>top</code></a> returns top <code>N</code> field sets with the maximum number of matching logs."
|
||||
},
|
||||
{
|
||||
"value": "uniq",
|
||||
"description": "<a href=\"#uniq-pipe\"><code>uniq</code></a> returns unique log entires."
|
||||
},
|
||||
{
|
||||
"value": "unpack_json",
|
||||
"description": "<a href=\"#unpack_json-pipe\"><code>unpack_json</code></a> unpacks JSON messages from <a href=\"https://docs.victoriametrics.com/victorialogs/keyconcepts/#data-model\">log fields</a>."
|
||||
},
|
||||
{
|
||||
"value": "unpack_logfmt",
|
||||
"description": "<a href=\"#unpack_logfmt-pipe\"><code>unpack_logfmt</code></a> unpacks <a href=\"https://brandur.org/logfmt\" rel=\"external\" target=\"_blank\">logfmt</a> messages from <a href=\"https://docs.victoriametrics.com/victorialogs/keyconcepts/#data-model\">log fields</a>."
|
||||
},
|
||||
{
|
||||
"value": "unpack_syslog",
|
||||
"description": "<a href=\"#unpack_syslog-pipe\"><code>unpack_syslog</code></a> unpacks <a href=\"https://en.wikipedia.org/wiki/Syslog\" rel=\"external\" target=\"_blank\">syslog</a> messages from <a href=\"https://docs.victoriametrics.com/victorialogs/keyconcepts/#data-model\">log fields</a>."
|
||||
},
|
||||
{
|
||||
"value": "unroll",
|
||||
"description": "<a href=\"#unroll-pipe\"><code>unroll</code></a> unrolls JSON arrays from <a href=\"https://docs.victoriametrics.com/victorialogs/keyconcepts/#data-model\">log fields</a>."
|
||||
}
|
||||
].map(item => ({
|
||||
...item,
|
||||
type: ContextType.PipeName,
|
||||
icon: <FunctionIcon/>,
|
||||
description: prepareDescription(item.description),
|
||||
}));
|
||||
@@ -0,0 +1,31 @@
|
||||
export enum LogicalPartType {
|
||||
Filter = "Filter",
|
||||
Pipe = "Pipe",
|
||||
Operator = "Operator",
|
||||
}
|
||||
|
||||
export type LogicalPartPosition = [start: number, end: number];
|
||||
|
||||
export interface LogicalPart {
|
||||
id: number;
|
||||
value: string;
|
||||
type: LogicalPartType;
|
||||
position: LogicalPartPosition;
|
||||
}
|
||||
|
||||
export interface ContextData {
|
||||
valueBeforeCursor: string;
|
||||
valueAfterCursor: string;
|
||||
contextType: ContextType;
|
||||
valueContext: string;
|
||||
filterName?: string;
|
||||
}
|
||||
|
||||
export enum ContextType {
|
||||
FilterName = "FilterName",
|
||||
FilterUnknown = "FilterUnknown",
|
||||
FilterValue = "FilterValue",
|
||||
PipeName = "Pipes",
|
||||
PipeValue = "PipeValue",
|
||||
Unknown = "Unknown",
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
import React, { useEffect, useState, useRef, Dispatch, SetStateAction } from "preact/compat";
|
||||
import dayjs from "dayjs";
|
||||
import { ContextData, ContextType } from "./types";
|
||||
import { FunctionIcon, LabelIcon, MetricIcon, ValueIcon } from "../../../Main/Icons";
|
||||
import { AutocompleteOptions } from "../../../Main/Autocomplete/Autocomplete";
|
||||
import { useAppState } from "../../../../state/common/StateContext";
|
||||
import { useTimeState } from "../../../../state/time/TimeStateContext";
|
||||
import { useCallback } from "react";
|
||||
import { AUTOCOMPLETE_LIMITS } from "../../../../constants/queryAutocomplete";
|
||||
import { LogsFiledValues } from "../../../../api/types";
|
||||
import { useLogsDispatch, useLogsState } from "../../../../state/logsPanel/LogsStateContext";
|
||||
|
||||
type FetchDataArgs = {
|
||||
urlSuffix: string;
|
||||
setter: Dispatch<SetStateAction<AutocompleteOptions[]>>
|
||||
type: ContextType;
|
||||
params?: URLSearchParams;
|
||||
}
|
||||
|
||||
const icons = {
|
||||
[ContextType.FilterName]: <MetricIcon/>,
|
||||
[ContextType.FilterUnknown]: <MetricIcon/>,
|
||||
[ContextType.FilterValue]: <ValueIcon/>,
|
||||
[ContextType.PipeName]: <FunctionIcon/>,
|
||||
[ContextType.PipeValue]: <LabelIcon/>,
|
||||
[ContextType.Unknown]: <ValueIcon/>
|
||||
};
|
||||
|
||||
export const useFetchLogsQLOptions = (contextData?: ContextData) => {
|
||||
const { serverUrl } = useAppState();
|
||||
const { period: { start, end } } = useTimeState();
|
||||
const { autocompleteCache } = useLogsState();
|
||||
const dispatch = useLogsDispatch();
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const [fieldNames, setFieldNames] = useState<AutocompleteOptions[]>([]);
|
||||
const [fieldValues, setFieldValues] = useState<AutocompleteOptions[]>([]);
|
||||
|
||||
const abortControllerRef = useRef(new AbortController());
|
||||
|
||||
const getQueryParams = useCallback((params?: Record<string, string>) => {
|
||||
const startDay = dayjs(start * 1000).startOf("day").valueOf() / 1000;
|
||||
const endDay = dayjs(end * 1000).endOf("day").valueOf() / 1000;
|
||||
|
||||
return new URLSearchParams({
|
||||
...(params || {}),
|
||||
limit: `${AUTOCOMPLETE_LIMITS.queryLimit}`,
|
||||
start: `${startDay}`,
|
||||
end: `${endDay}`
|
||||
});
|
||||
}, [start, end]);
|
||||
|
||||
const processData = (values: LogsFiledValues[], type: ContextType): AutocompleteOptions[] => {
|
||||
return values.map(v => ({
|
||||
value: v.value,
|
||||
type: `${type}`,
|
||||
icon: icons[type]
|
||||
}));
|
||||
};
|
||||
|
||||
const fetchData = async ({ urlSuffix, setter, type, params }: FetchDataArgs) => {
|
||||
// if (!value && type === TypeData.metric) return;
|
||||
abortControllerRef.current.abort();
|
||||
abortControllerRef.current = new AbortController();
|
||||
const { signal } = abortControllerRef.current;
|
||||
const key = `${urlSuffix}?${params?.toString()}`;
|
||||
setLoading(true);
|
||||
try {
|
||||
const cachedData = autocompleteCache.get(key);
|
||||
if (cachedData) {
|
||||
setter(processData(cachedData, type));
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
const response = await fetch(`${serverUrl}/select/logsql/${urlSuffix}?${params}`, { signal });
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
const value = (data?.values || []) as LogsFiledValues[];
|
||||
setter(value ? processData(value, type) : []);
|
||||
dispatch({ type: "SET_AUTOCOMPLETE_CACHE", payload: { key, value } });
|
||||
}
|
||||
setLoading(false);
|
||||
} catch (e) {
|
||||
if (e instanceof Error && e.name !== "AbortError") {
|
||||
dispatch({ type: "SET_AUTOCOMPLETE_CACHE", payload: { key, value: [] } });
|
||||
setLoading(false);
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// fetch field names
|
||||
useEffect(() => {
|
||||
const validContexts = [ContextType.FilterName, ContextType.FilterUnknown];
|
||||
const isInvalidContext = !validContexts.includes(contextData?.contextType || ContextType.Unknown);
|
||||
if (!serverUrl || isInvalidContext) {
|
||||
return;
|
||||
}
|
||||
|
||||
setFieldNames([]);
|
||||
|
||||
fetchData({
|
||||
urlSuffix: "field_names",
|
||||
setter: setFieldNames,
|
||||
type: ContextType.FilterName,
|
||||
params: getQueryParams({ query: "*" })
|
||||
});
|
||||
|
||||
return () => abortControllerRef.current?.abort();
|
||||
}, [serverUrl, contextData]);
|
||||
|
||||
// fetch field values
|
||||
useEffect(() => {
|
||||
const isInvalidContext = contextData?.contextType !== ContextType.FilterValue;
|
||||
if (!serverUrl || isInvalidContext || !contextData?.filterName) {
|
||||
return;
|
||||
}
|
||||
|
||||
setFieldValues([]);
|
||||
|
||||
fetchData({
|
||||
urlSuffix: "field_values",
|
||||
setter: setFieldValues,
|
||||
type: ContextType.FilterValue,
|
||||
params: getQueryParams({ query: "*", field: contextData.filterName })
|
||||
});
|
||||
|
||||
return () => abortControllerRef.current?.abort();
|
||||
}, [serverUrl, contextData]);
|
||||
|
||||
return {
|
||||
fieldNames,
|
||||
fieldValues,
|
||||
loading,
|
||||
};
|
||||
};
|
||||
@@ -2,7 +2,6 @@ import React, { FC, useEffect, useRef, useState } from "preact/compat";
|
||||
import { KeyboardEvent } from "react";
|
||||
import { ErrorTypes } from "../../../types";
|
||||
import TextField from "../../Main/TextField/TextField";
|
||||
import QueryEditorAutocomplete from "./QueryEditorAutocomplete";
|
||||
import "./style.scss";
|
||||
import { QueryStats } from "../../../api/types";
|
||||
import { partialWarning, seriesFetchedWarning } from "./warningText";
|
||||
@@ -11,6 +10,16 @@ import useDeviceDetect from "../../../hooks/useDeviceDetect";
|
||||
import { useQueryState } from "../../../state/query/QueryStateContext";
|
||||
import debounce from "lodash.debounce";
|
||||
|
||||
export interface QueryEditorAutocompleteProps {
|
||||
value: string;
|
||||
anchorEl: React.RefObject<HTMLInputElement>;
|
||||
caretPosition: [number, number]; // [start, end]
|
||||
hasHelperText: boolean;
|
||||
includeFunctions: boolean;
|
||||
onSelect: (val: string, caretPosition: number) => void;
|
||||
onFoundOptions: (val: AutocompleteOptions[]) => void;
|
||||
}
|
||||
|
||||
export interface QueryEditorProps {
|
||||
onChange: (query: string) => void;
|
||||
onEnter: () => void;
|
||||
@@ -19,6 +28,7 @@ export interface QueryEditorProps {
|
||||
value: string;
|
||||
oneLiner?: boolean;
|
||||
autocomplete: boolean;
|
||||
autocompleteEl?: FC<QueryEditorAutocompleteProps>;
|
||||
error?: ErrorTypes | string;
|
||||
stats?: QueryStats;
|
||||
label: string;
|
||||
@@ -33,6 +43,7 @@ const QueryEditor: FC<QueryEditorProps> = ({
|
||||
onArrowUp,
|
||||
onArrowDown,
|
||||
autocomplete,
|
||||
autocompleteEl: AutocompleteEl,
|
||||
error,
|
||||
stats,
|
||||
label,
|
||||
@@ -43,10 +54,11 @@ 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);
|
||||
const [showAutocomplete, setShowAutocomplete] = useState(!!AutocompleteEl);
|
||||
const debouncedSetShowAutocomplete = useRef(debounce(setShowAutocomplete, 500)).current;
|
||||
|
||||
const warning = [
|
||||
@@ -66,7 +78,7 @@ const QueryEditor: FC<QueryEditorProps> = ({
|
||||
|
||||
const handleSelect = (val: string, caretPosition: number) => {
|
||||
onChange(val);
|
||||
setCaretPosition([caretPosition, caretPosition]);
|
||||
setCaretPositionInput([caretPosition, caretPosition]);
|
||||
};
|
||||
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
@@ -108,17 +120,17 @@ 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(() => {
|
||||
setOpenAutocomplete(autocomplete);
|
||||
setOpenAutocomplete(!!AutocompleteEl);
|
||||
}, [autocompleteQuick]);
|
||||
|
||||
useEffect(() => {
|
||||
setShowAutocomplete(false);
|
||||
debouncedSetShowAutocomplete(true);
|
||||
}, [caretPosition]);
|
||||
}, [caretPositionAutocomplete]);
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -137,13 +149,13 @@ const QueryEditor: FC<QueryEditorProps> = ({
|
||||
onChangeCaret={handleChangeCaret}
|
||||
disabled={disabled}
|
||||
inputmode={"search"}
|
||||
caretPosition={caretPosition}
|
||||
caretPosition={caretPositionInput}
|
||||
/>
|
||||
{showAutocomplete && autocomplete && (
|
||||
<QueryEditorAutocomplete
|
||||
{showAutocomplete && autocomplete && AutocompleteEl && (
|
||||
<AutocompleteEl
|
||||
value={value}
|
||||
anchorEl={autocompleteAnchorEl}
|
||||
caretPosition={caretPosition}
|
||||
caretPosition={caretPositionAutocomplete}
|
||||
hasHelperText={Boolean(warning || error)}
|
||||
includeFunctions={includeFunctions}
|
||||
onSelect={handleSelect}
|
||||
|
||||
@@ -1,20 +1,11 @@
|
||||
import React, { FC, useState, useEffect, useMemo, useCallback } from "preact/compat";
|
||||
import Autocomplete, { AutocompleteOptions } from "../../Main/Autocomplete/Autocomplete";
|
||||
import Autocomplete from "../../Main/Autocomplete/Autocomplete";
|
||||
import { useFetchQueryOptions } from "../../../hooks/useFetchQueryOptions";
|
||||
import { escapeRegexp, hasUnclosedQuotes } from "../../../utils/regexp";
|
||||
import useGetMetricsQL from "../../../hooks/useGetMetricsQL";
|
||||
import { QueryContextType } from "../../../types";
|
||||
import { AUTOCOMPLETE_LIMITS } from "../../../constants/queryAutocomplete";
|
||||
|
||||
interface QueryEditorAutocompleteProps {
|
||||
value: string;
|
||||
anchorEl: React.RefObject<HTMLElement>;
|
||||
caretPosition: [number, number]; // [start, end]
|
||||
hasHelperText: boolean;
|
||||
includeFunctions: boolean;
|
||||
onSelect: (val: string, caretPosition: number) => void;
|
||||
onFoundOptions: (val: AutocompleteOptions[]) => void;
|
||||
}
|
||||
import { QueryEditorAutocompleteProps } from "./QueryEditor";
|
||||
|
||||
const QueryEditorAutocomplete: FC<QueryEditorAutocompleteProps> = ({
|
||||
value,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -28,7 +28,7 @@ interface AutocompleteProps {
|
||||
offset?: {top: number, left: number}
|
||||
maxDisplayResults?: {limit: number, message?: string}
|
||||
loading?: boolean;
|
||||
onSelect: (val: string) => void
|
||||
onSelect: (val: string, item: AutocompleteOptions) => void
|
||||
onOpenAutocomplete?: (val: boolean) => void
|
||||
onFoundOptions?: (val: AutocompleteOptions[]) => void
|
||||
onChangeWrapperRef?: (elementRef: React.RefObject<HTMLElement>) => void
|
||||
@@ -97,9 +97,9 @@ const Autocomplete: FC<AutocompleteProps> = ({
|
||||
return noOptionsText && !foundOptions.length;
|
||||
}, [noOptionsText,foundOptions]);
|
||||
|
||||
const createHandlerSelect = (item: string) => () => {
|
||||
const createHandlerSelect = (item: AutocompleteOptions) => () => {
|
||||
if (disabled) return;
|
||||
onSelect(item);
|
||||
onSelect(item.value, item);
|
||||
if (!selected) handleCloseAutocomplete();
|
||||
};
|
||||
|
||||
@@ -141,7 +141,7 @@ const Autocomplete: FC<AutocompleteProps> = ({
|
||||
|
||||
if (key === "Enter") {
|
||||
const item = foundOptions[focusOption.index];
|
||||
item && onSelect(item.value);
|
||||
item && onSelect(item.value, item);
|
||||
if (!selected) handleCloseAutocomplete();
|
||||
}
|
||||
|
||||
@@ -206,7 +206,7 @@ const Autocomplete: FC<AutocompleteProps> = ({
|
||||
})}
|
||||
id={`$autocomplete$${option.value}`}
|
||||
key={`${i}${option.value}`}
|
||||
onClick={createHandlerSelect(option.value)}
|
||||
onClick={createHandlerSelect(option)}
|
||||
onMouseEnter={createHandlerMouseEnter(i)}
|
||||
onMouseLeave={handlerMouseLeave}
|
||||
>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -25,6 +25,7 @@ import { QueryStats } from "../../../api/types";
|
||||
import { usePrettifyQuery } from "./hooks/usePrettifyQuery";
|
||||
import QueryHistory from "../QueryHistory/QueryHistory";
|
||||
import AnomalyConfig from "../../../components/ExploreAnomaly/AnomalyConfig";
|
||||
import QueryEditorAutocomplete from "../../../components/Configurators/QueryEditor/QueryEditorAutocomplete";
|
||||
|
||||
export interface QueryConfiguratorProps {
|
||||
queryErrors: string[];
|
||||
@@ -216,6 +217,7 @@ const QueryConfigurator: FC<QueryConfiguratorProps> = ({
|
||||
<QueryEditor
|
||||
value={stateQuery[i]}
|
||||
autocomplete={!hideButtons?.autocomplete && (autocomplete || autocompleteQuick)}
|
||||
autocompleteEl={QueryEditorAutocomplete}
|
||||
error={queryErrors[i]}
|
||||
stats={stats[i]}
|
||||
onArrowUp={createHandlerArrow(-1, i)}
|
||||
|
||||
@@ -6,6 +6,9 @@ import useDeviceDetect from "../../../hooks/useDeviceDetect";
|
||||
import Button from "../../../components/Main/Button/Button";
|
||||
import QueryEditor from "../../../components/Configurators/QueryEditor/QueryEditor";
|
||||
import TextField from "../../../components/Main/TextField/TextField";
|
||||
import LogsQueryEditorAutocomplete from "../../../components/Configurators/QueryEditor/LogsQL/LogsQueryEditorAutocomplete";
|
||||
import { useQueryDispatch, useQueryState } from "../../../state/query/QueryStateContext";
|
||||
import Switch from "../../../components/Main/Switch/Switch";
|
||||
|
||||
export interface ExploreLogHeaderProps {
|
||||
query: string;
|
||||
@@ -27,6 +30,8 @@ const ExploreLogsHeader: FC<ExploreLogHeaderProps> = ({
|
||||
onRun,
|
||||
}) => {
|
||||
const { isMobile } = useDeviceDetect();
|
||||
const { autocomplete } = useQueryState();
|
||||
const queryDispatch = useQueryDispatch();
|
||||
|
||||
const [errorLimit, setErrorLimit] = useState("");
|
||||
const [limitInput, setLimitInput] = useState(limit);
|
||||
@@ -42,6 +47,10 @@ const ExploreLogsHeader: FC<ExploreLogHeaderProps> = ({
|
||||
}
|
||||
};
|
||||
|
||||
const onChangeAutocomplete = () => {
|
||||
queryDispatch({ type: "TOGGLE_AUTOCOMPLETE" });
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setLimitInput(limit);
|
||||
}, [limit]);
|
||||
@@ -57,7 +66,8 @@ const ExploreLogsHeader: FC<ExploreLogHeaderProps> = ({
|
||||
<div className="vm-explore-logs-header-top">
|
||||
<QueryEditor
|
||||
value={query}
|
||||
autocomplete={false}
|
||||
autocomplete={autocomplete}
|
||||
autocompleteEl={LogsQueryEditorAutocomplete}
|
||||
onArrowUp={() => null}
|
||||
onArrowDown={() => null}
|
||||
onEnter={onRun}
|
||||
@@ -75,7 +85,14 @@ const ExploreLogsHeader: FC<ExploreLogHeaderProps> = ({
|
||||
/>
|
||||
</div>
|
||||
<div className="vm-explore-logs-header-bottom">
|
||||
<div className="vm-explore-logs-header-bottom-contols"></div>
|
||||
<div className="vm-explore-logs-header-bottom-contols">
|
||||
<Switch
|
||||
label={"Autocomplete"}
|
||||
value={autocomplete}
|
||||
onChange={onChangeAutocomplete}
|
||||
fullWidth={isMobile}
|
||||
/>
|
||||
</div>
|
||||
<div className="vm-explore-logs-header-bottom-helpful">
|
||||
<a
|
||||
className="vm-link vm-link_with-icon"
|
||||
|
||||
@@ -26,6 +26,9 @@
|
||||
}
|
||||
|
||||
&-contols {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user