Merge branch 'public-single-node' into pmm-6401-read-prometheus-data-files

This commit is contained in:
Aliaksandr Valialkin 2022-09-30 13:21:21 +03:00
commit 3a15bc761b
No known key found for this signature in database
GPG key ID: A72BEC6CD3D0DED1
39 changed files with 1851 additions and 1216 deletions

View file

@ -3,7 +3,6 @@ package remotewrite
import (
"flag"
"fmt"
"regexp"
"strings"
"sync"
@ -118,9 +117,9 @@ func (rctx *relabelCtx) applyRelabeling(tss []prompbmarshal.TimeSeries, extraLab
for j := range tmpLabels {
label := &tmpLabels[j]
if label.Name == "__name__" {
label.Value = unsupportedPromChars.ReplaceAllString(label.Value, "_")
label.Value = promrelabel.SanitizeName(label.Value)
} else {
label.Name = unsupportedPromChars.ReplaceAllString(label.Name, "_")
label.Name = promrelabel.SanitizeName(label.Name)
}
}
}
@ -138,9 +137,6 @@ func (rctx *relabelCtx) applyRelabeling(tss []prompbmarshal.TimeSeries, extraLab
return tssDst
}
// See https://prometheus.io/docs/concepts/data_model/#metric-names-and-labels
var unsupportedPromChars = regexp.MustCompile(`[^a-zA-Z0-9_:]`)
type relabelCtx struct {
// pool for labels, which are used during the relabeling.
labels []prompbmarshal.Label

View file

@ -120,7 +120,7 @@ name: <string>
# params:
# nocache: ["1"] # disable caching for vmselect
# denyPartialResponse: ["true"] # fail if one or more vmstorage nodes returned an error
# extra_label: ["env=dev"] # apply additional label filter "env=dev" for all requests
# extra_label: ["env=dev"] # apply additional label filter "env=dev" for all requests
# see more details at https://docs.victoriametrics.com#prometheus-querying-api-enhancements
params:
[ <string>: [<string>, ...]]
@ -131,7 +131,7 @@ params:
# headers:
# - "CustomHeader: foo"
# - "CustomHeader2: bar"
# Headers set via this param have priority over headers set via `-datasource.headers` flag.
# Headers set via this param have priority over headers set via `-datasource.headers` flag.
headers:
[ <string>, ...]
@ -185,8 +185,8 @@ expr: <string>
[ for: <duration> | default = 0s ]
# Whether to print debug information into logs.
# Information includes alerts state changes and requests sent to the datasource.
# Please note, that if rule's query params contain sensitive
# Information includes alerts state changes and requests sent to the datasource.
# Please note, that if rule's query params contain sensitive
# information - it will be printed to logs.
# Is applicable to alerting rules only.
[ debug: <bool> | default = false ]
@ -200,9 +200,9 @@ annotations:
[ <labelname>: <tmpl_string> ]
```
#### Templating
#### Templating
It is allowed to use [Go templating](https://golang.org/pkg/text/template/) in annotations to format data, iterate over
It is allowed to use [Go templating](https://golang.org/pkg/text/template/) in annotations to format data, iterate over
or execute expressions.
The following variables are available in templating:
@ -224,7 +224,7 @@ and [reusable templates](#reusable-templates).
#### Reusable templates
Like in Alertmanager you can define [reusable templates](https://prometheus.io/docs/prometheus/latest/configuration/template_examples/#defining-reusable-templates)
to share same templates across annotations. Just define the templates in a file and
to share same templates across annotations. Just define the templates in a file and
set the path via `-rule.templates` flag.
For example, template `grafana.filter` can be defined as following:
@ -418,8 +418,8 @@ To avoid recording rules results and alerts state duplication in VictoriaMetrics
don't forget to configure [deduplication](https://docs.victoriametrics.com/Single-server-VictoriaMetrics.html#deduplication).
The recommended value for `-dedup.minScrapeInterval` must be greater or equal to vmalert's `evaluation_interval`.
If you observe inconsistent or "jumping" values in series produced by vmalert, try disabling `-datasource.queryTimeAlignment`
command line flag. Because of alignment, two or more vmalert HA pairs will produce results with the same timestamps.
But due of backfilling (data delivered to the datasource with some delay) values of such results may differ,
command line flag. Because of alignment, two or more vmalert HA pairs will produce results with the same timestamps.
But due of backfilling (data delivered to the datasource with some delay) values of such results may differ,
which would affect deduplication logic and result into "jumping" datapoints.
Alertmanager will automatically deduplicate alerts with identical labels, so ensure that
@ -439,8 +439,8 @@ This ability allows to aggregate data. For example, the following rule will calc
metric `http_requests` on the `5m` interval:
```yaml
- record: http_requests:avg5m
expr: avg_over_time(http_requests[5m])
- record: http_requests:avg5m
expr: avg_over_time(http_requests[5m])
```
Every time this rule will be evaluated, `vmalert` will backfill its results as a new time series `http_requests:avg5m`
@ -454,9 +454,9 @@ This ability allows to downsample data. For example, the following config will e
groups:
- name: my_group
interval: 5m
rules:
rules:
- record: http_requests:avg5m
expr: avg_over_time(http_requests[5m])
expr: avg_over_time(http_requests[5m])
```
Ability of `vmalert` to be configured with different `datasource.url` and `remoteWrite.url` allows
@ -474,7 +474,7 @@ or reducing resolution) and push results to "cold" cluster.
```
./bin/vmalert -rule=downsampling-rules.yml \ # Path to the file with rules configuration. Supports wildcard
-datasource.url=http://raw-cluster-vmselect:8481/select/0/prometheus # vmselect addr for executing recordi ng rules expressions
-datasource.url=http://raw-cluster-vmselect:8481/select/0/prometheus # vmselect addr for executing recording rules expressions
-remoteWrite.url=http://aggregated-cluster-vminsert:8480/insert/0/prometheus # vminsert addr to persist recording rules results
```
@ -496,7 +496,7 @@ we recommend using [vmagent](https://docs.victoriametrics.com/vmagent.html) as f
In this topology, `vmalert` is configured to persist rule results to `vmagent`. And `vmagent`
is configured to fan-out received data to two or more destinations.
Using `vmagent` as a proxy provides additional benefits such as
Using `vmagent` as a proxy provides additional benefits such as
[data persisting when storage is unreachable](https://docs.victoriametrics.com/vmagent.html#replication-and-high-availability),
or time series modification via [relabeling](https://docs.victoriametrics.com/vmagent.html#relabeling).
@ -657,7 +657,7 @@ Note that too small value passed to `-search.latencyOffset` may lead to incomple
Try the following recommendations in such cases:
* Always configure group's `evaluationInterval` to be bigger or equal to `scrape_interval` at which metrics
* Always configure group's `evaluationInterval` to be bigger or equal to `scrape_interval` at which metrics
are delivered to the datasource;
* If you know in advance, that data in datasource is delayed - try changing vmalert's `-datasource.lookback`
command-line flag to add a time shift for evaluations;
@ -671,7 +671,7 @@ after multiple consecutive evaluations, and at each evaluation their expression
becomes false, then alert's state resets to the initial state.
If `-remoteWrite.url` command-line flag is configured, vmalert will persist alert's state in form of time series
`ALERTS` and `ALERTS_FOR_STATE` to the specified destination. Such time series can be then queried via
`ALERTS` and `ALERTS_FOR_STATE` to the specified destination. Such time series can be then queried via
[vmui](https://docs.victoriametrics.com/Single-server-VictoriaMetrics.html#vmui) or Grafana to track how alerts state
changed in time.
@ -680,9 +680,9 @@ on `/vmalert/groups` page and check the `Last updates` section:
<img alt="vmalert state" src="vmalert_state.png">
Rows in the section represent ordered rule evaluations and their results. The column `curl` contains an example of
Rows in the section represent ordered rule evaluations and their results. The column `curl` contains an example of
HTTP request sent by vmalert to the `-datasource.url` during evaluation. If specific state shows that there were
no samples returned and curl command returns data - then it is very likely there was no data in datasource on the
no samples returned and curl command returns data - then it is very likely there was no data in datasource on the
moment when rule was evaluated.
vmalert also alows configuring more detailed logging for specific rule. Just set `debug: true` in rule's configuration
@ -762,11 +762,11 @@ The shortlist of configuration flags is the following:
-datasource.maxIdleConnections int
Defines the number of idle (keep-alive connections) to each configured datasource. Consider setting this value equal to the value: groups_total * group.concurrency. Too low a value may result in a high number of sockets in TIME_WAIT state. (default 100)
-datasource.oauth2.clientID string
Optional OAuth2 clientID to use for -datasource.url.
Optional OAuth2 clientID to use for -datasource.url.
-datasource.oauth2.clientSecret string
Optional OAuth2 clientSecret to use for -datasource.url.
-datasource.oauth2.clientSecretFile string
Optional OAuth2 clientSecretFile to use for -datasource.url.
Optional OAuth2 clientSecretFile to use for -datasource.url.
-datasource.oauth2.scopes string
Optional OAuth2 scopes to use for -datasource.url. Scopes must be delimited by ';'
-datasource.oauth2.tokenUrl string
@ -960,7 +960,7 @@ The shortlist of configuration flags is the following:
-remoteRead.oauth2.scopes string
Optional OAuth2 scopes to use for -remoteRead.url. Scopes must be delimited by ';'.
-remoteRead.oauth2.tokenUrl string
Optional OAuth2 tokenURL to use for -remoteRead.url.
Optional OAuth2 tokenURL to use for -remoteRead.url.
-remoteRead.showURL
Whether to show -remoteRead.url in the exported metrics. It is hidden by default, since it can contain sensitive info such as auth key
-remoteRead.tlsCAFile string
@ -1121,7 +1121,7 @@ and [DNS](https://prometheus.io/docs/prometheus/latest/configuration/configurati
For example:
```
static_configs:
static_configs:
- targets:
- localhost:9093
- localhost:9095
@ -1130,7 +1130,7 @@ consul_sd_configs:
- server: localhost:8500
services:
- alertmanager
dns_sd_configs:
- names:
- my.domain.com

View file

@ -165,11 +165,15 @@ func (ar *AlertingRule) logDebugf(at time.Time, a *notifier.Alert, format string
}
type labelSet struct {
// origin labels from series
// used for templating
// origin labels extracted from received time series
// plus extra labels (group labels, service labels like alertNameLabel).
// in case of conflicts, origin labels from time series preferred.
// used for templating annotations
origin map[string]string
// processed labels with additional data
// used as Alert labels
// processed labels includes origin labels
// plus extra labels (group labels, service labels like alertNameLabel).
// in case of conflicts, extra labels are preferred.
// used as labels attached to notifier.Alert and ALERTS series written to remote storage.
processed map[string]string
}
@ -177,7 +181,7 @@ type labelSet struct {
// to labelSet which contains original and processed labels.
func (ar *AlertingRule) toLabels(m datasource.Metric, qFn templates.QueryFn) (*labelSet, error) {
ls := &labelSet{
origin: make(map[string]string, len(m.Labels)),
origin: make(map[string]string),
processed: make(map[string]string),
}
for _, l := range m.Labels {
@ -199,14 +203,23 @@ func (ar *AlertingRule) toLabels(m datasource.Metric, qFn templates.QueryFn) (*l
}
for k, v := range extraLabels {
ls.processed[k] = v
if _, ok := ls.origin[k]; !ok {
ls.origin[k] = v
}
}
// set additional labels to identify group and rule name
if ar.Name != "" {
ls.processed[alertNameLabel] = ar.Name
if _, ok := ls.origin[alertNameLabel]; !ok {
ls.origin[alertNameLabel] = ar.Name
}
}
if !*disableAlertGroupLabel && ar.GroupName != "" {
ls.processed[alertGroupNameLabel] = ar.GroupName
if _, ok := ls.origin[alertGroupNameLabel]; !ok {
ls.origin[alertGroupNameLabel] = ar.GroupName
}
}
return ls, nil
}

View file

@ -700,14 +700,26 @@ func TestAlertingRule_Template(t *testing.T) {
expAlerts map[uint64]*notifier.Alert
}{
{
newTestRuleWithLabels("common", "region", "east"),
&AlertingRule{
Name: "common",
Labels: map[string]string{
"region": "east",
},
Annotations: map[string]string{
"summary": `{{ $labels.alertname }}: Too high connection number for "{{ $labels.instance }}"`,
},
alerts: make(map[uint64]*notifier.Alert),
state: newRuleState(),
},
[]datasource.Metric{
metricWithValueAndLabels(t, 1, "instance", "foo"),
metricWithValueAndLabels(t, 1, "instance", "bar"),
},
map[uint64]*notifier.Alert{
hash(map[string]string{alertNameLabel: "common", "region": "east", "instance": "foo"}): {
Annotations: map[string]string{},
Annotations: map[string]string{
"summary": `common: Too high connection number for "foo"`,
},
Labels: map[string]string{
alertNameLabel: "common",
"region": "east",
@ -715,7 +727,9 @@ func TestAlertingRule_Template(t *testing.T) {
},
},
hash(map[string]string{alertNameLabel: "common", "region": "east", "instance": "bar"}): {
Annotations: map[string]string{},
Annotations: map[string]string{
"summary": `common: Too high connection number for "bar"`,
},
Labels: map[string]string{
alertNameLabel: "common",
"region": "east",

View file

@ -9,11 +9,12 @@ groups:
denyPartialResponse: ["true"]
rules:
- alert: Conns
expr: sum(vm_tcplistener_conns) by(instance) > 1
expr: vm_tcplistener_conns > 0
for: 3m
debug: true
annotations:
summary: Too high connection number for {{$labels.instance}}
labels: "Available labels: {{ $labels }}"
summary: Too high connection number for {{ $labels.instance }}
{{ with printf "sum(vm_tcplistener_conns{instance=%q})" .Labels.instance | query }}
{{ . | first | value }}
{{ end }}

View file

@ -6,13 +6,12 @@ import (
"strconv"
"strings"
"time"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promrelabel"
)
var (
allowedNames = regexp.MustCompile("^[a-zA-Z][a-zA-Z0-9_:]*$")
allowedFirstChar = regexp.MustCompile("^[a-zA-Z]")
replaceChars = regexp.MustCompile("[^a-zA-Z0-9_:]")
allowedTagKeys = regexp.MustCompile("^[a-zA-Z][a-zA-Z0-9_]*$")
)
func convertDuration(duration string) (time.Duration, error) {
@ -180,13 +179,8 @@ func modifyData(msg Metric, normalize bool) (Metric, error) {
}
/*
replace bad characters in metric name with _ per the data model
only replace if needed to reduce string processing time
*/
if !allowedNames.MatchString(name) {
finalMsg.Metric = replaceChars.ReplaceAllString(name, "_")
} else {
finalMsg.Metric = name
}
finalMsg.Metric = promrelabel.SanitizeName(name)
// replace bad characters in tag keys with _ per the data model
for key, value := range msg.Tags {
// if normalization requested, lowercase the key and value
@ -196,11 +190,8 @@ func modifyData(msg Metric, normalize bool) (Metric, error) {
}
/*
replace all explicitly bad characters with _
only replace if needed to reduce string processing time
*/
if !allowedTagKeys.MatchString(key) {
key = replaceChars.ReplaceAllString(key, "_")
}
key = promrelabel.SanitizeName(key)
// tags that start with __ are considered custom stats for internal prometheus stuff, we should drop them
if !strings.HasPrefix(key, "__") {
finalMsg.Tags[key] = value

View file

@ -176,6 +176,8 @@ curl 'http://localhost:8431/api/v1/labels' -H 'Authorization: Bearer eyJhbGciOiJ
The shortlist of configuration flags include the following:
```console
-auth.httpHeader
HTTP header name to look for JWT authorization token
-clusterMode
enable this for the cluster version
-datasource.appendTypePrefix

View file

@ -3,7 +3,6 @@ package relabel
import (
"flag"
"fmt"
"regexp"
"sync/atomic"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/bytesutil"
@ -115,9 +114,9 @@ func (ctx *Ctx) ApplyRelabeling(labels []prompb.Label) []prompb.Label {
for i := range tmpLabels {
label := &tmpLabels[i]
if label.Name == "__name__" {
label.Value = unsupportedPromChars.ReplaceAllString(label.Value, "_")
label.Value = promrelabel.SanitizeName(label.Value)
} else {
label.Name = unsupportedPromChars.ReplaceAllString(label.Name, "_")
label.Name = promrelabel.SanitizeName(label.Name)
}
}
}
@ -149,6 +148,3 @@ func (ctx *Ctx) ApplyRelabeling(labels []prompb.Label) []prompb.Label {
}
var metricsDropped = metrics.NewCounter(`vm_relabel_metrics_dropped_total`)
// See https://prometheus.io/docs/concepts/data_model/#metric-names-and-labels
var unsupportedPromChars = regexp.MustCompile(`[^a-zA-Z0-9_:]`)

View file

@ -104,6 +104,9 @@ func removeGroupTags(metricName *storage.MetricName, modifier *metricsql.Modifie
func aggrFuncExt(afe func(tss []*timeseries, modifier *metricsql.ModifierExpr) []*timeseries, argOrig []*timeseries,
modifier *metricsql.ModifierExpr, maxSeries int, keepOriginal bool) ([]*timeseries, error) {
// Remove empty time series, e.g. series with all NaN samples,
// since such series are ignored by aggregate functions.
argOrig = removeEmptySeries(argOrig)
arg := copyTimeseriesMetricNames(argOrig, keepOriginal)
// Perform grouping.

View file

@ -2281,6 +2281,27 @@ func TestExecSuccess(t *testing.T) {
resultExpected := []netstorage.Result{r}
f(q, resultExpected)
})
t.Run(`limit_offset NaN`, func(t *testing.T) {
t.Parallel()
// q returns 3 time series, where foo=3 contains only NaN values
// limit_offset suppose to apply offset for non-NaN series only
q := `limit_offset(1, 1, sort_by_label_desc((
label_set(time()*1, "foo", "1"),
label_set(time()*2, "foo", "2"),
label_set(time()*3, "foo", "3"),
) < 3000, "foo"))`
r := netstorage.Result{
MetricName: metricNameExpected,
Values: []float64{1000, 1200, 1400, 1600, 1800, 2000},
Timestamps: timestampsExpected,
}
r.MetricName.Tags = []storage.Tag{{
Key: []byte("foo"),
Value: []byte("1"),
}}
resultExpected := []netstorage.Result{r}
f(q, resultExpected)
})
t.Run(`sum(label_graphite_group)`, func(t *testing.T) {
t.Parallel()
q := `sort(sum by (__name__) (
@ -5480,6 +5501,12 @@ func TestExecSuccess(t *testing.T) {
resultExpected := []netstorage.Result{r}
f(q, resultExpected)
})
t.Run(`any(empty-series)`, func(t *testing.T) {
t.Parallel()
q := `any(label_set(time()<0, "foo", "bar"))`
resultExpected := []netstorage.Result{}
f(q, resultExpected)
})
t.Run(`group() by (test)`, func(t *testing.T) {
t.Parallel()
q := `group((

View file

@ -1854,7 +1854,9 @@ func transformLimitOffset(tfa *transformFuncArg) ([]*timeseries, error) {
if err != nil {
return nil, fmt.Errorf("cannot obtain offset arg: %w", err)
}
rvs := args[2]
// removeEmptySeries so offset will be calculated after empty series
// were filtered out.
rvs := removeEmptySeries(args[2])
if len(rvs) >= offset {
rvs = rvs[offset:]
}

View file

@ -19,6 +19,7 @@ The following tip changes can be tested by building VictoriaMetrics components f
**Update note 2:** [vmalert](https://docs.victoriametrics.com/vmalert.html) changes default value for command-line flag `-datasource.queryStep` from `0s` to `5m`. The change supposed to improve reliability of the rules evaluation when evaluation interval is lower than scraping interval.
* FEATURE: improve [relabeling](https://docs.victoriametrics.com/vmagent.html#relabeling) performance by up to 3x if non-trivial `regex` values are used.
* FEATURE: sanitize metric names for data ingested via [DataDog protocol](https://docs.victoriametrics.com/#how-to-send-data-from-datadog-agent) according to [DataDog metric naming](https://docs.datadoghq.com/metrics/custom_metrics/#naming-custom-metrics). The behaviour can be disabled by passing `-datadog.sanitizeMetricName=false` command-line flag. Thanks to @PerGon for [the pull request](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/3105).
* FEATURE: add `-usePromCompatibleNaming` command-line flag to [vmagent](https://docs.victoriametrics.com/vmagent.html), to single-node VictoriaMetrics and to `vminsert` component of VictoriaMetrics cluster. This flag can be used for normalizing the ingested metric names and label names to [Prometheus-compatible form](https://prometheus.io/docs/concepts/data_model/#metric-names-and-labels). If this flag is set, then all the chars unsupported by Prometheus are replaced with `_` chars in metric names and labels of the ingested samples. See [this feature request](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/3113).
* FEATURE: accept whitespace in metric names and tags ingested via [Graphite plaintext protocol](https://docs.victoriametrics.com/#how-to-send-data-from-graphite-compatible-agents-such-as-statsd) according to [the specs](https://graphite.readthedocs.io/en/latest/tags.html). See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/3102).
@ -30,12 +31,17 @@ The following tip changes can be tested by building VictoriaMetrics components f
* FEATURE: [vmui](https://docs.victoriametrics.com/#vmui): add `top queries` tab, which shows various stats for recently executed queries. See [these docs](https://docs.victoriametrics.com/#top-queries) and [this feature request](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/2707).
* FEATURE: [vmalert](https://docs.victoriametrics.com/vmalert.html): add `debug` mode to the alerting rule settings for printing additional information into logs during evaluation. See `debug` param in [alerting rule config](https://docs.victoriametrics.com/vmalert.html#alerting-rules).
* FEATURE: [vmalert](https://docs.victoriametrics.com/vmalert.html): add experimental feature for displaying last 10 states of the rule (recording or alerting) evaluation. The state is available on the Rule page, which can be opened by clicking on `Details` link next to Rule's name on the `/groups` page.
* FEATURE: [vmalert](https://docs.victoriametrics.com/vmalert.html): allow using extra labels in annotiations. See [this feature request](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/3013).
* FEATURE: [vmagent](https://docs.victoriametrics.com/vmagent.html): minimize the time needed for reading large responses from scrape targets in [stream parsing mode](https://docs.victoriametrics.com/vmagent.html#stream-parsing-mode). This should reduce scrape durations for such targets as [kube-state-metrics](https://github.com/kubernetes/kube-state-metrics) running in a big Kubernetes cluster.
* FEATURE: [MetricsQL](https://docs.victoriametrics.com/MetricsQL.html): add [sort_by_label_numeric](https://docs.victoriametrics.com/MetricsQL.html#sort_by_label_numeric) and [sort_by_label_numeric_desc](https://docs.victoriametrics.com/MetricsQL.html#sort_by_label_numeric_desc) functions for [numeric sort](https://www.gnu.org/software/coreutils/manual/html_node/Version-sort-is-not-the-same-as-numeric-sort.html) of input time series by the specified labels. See [this feature request](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/2938).
* FEATURE: [vmbackup](https://docs.victoriametrics.com/vmbackup.html) and [vmrestore](https://docs.victoriametrics.com/vmrestore.html): retry GCS operations for up to 3 minutes on temporary failures. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/3147).
* FEATURE: [vmgateway](https://docs.victoriametrics.com/vmgateway.html): add ability to extract JWT authorization token from non-standard HTTP header by passing it via `-auth.httpHeader` command-line flag. See [this feature request](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/3054).
* BUGFIX: [vmagent](https://docs.victoriametrics.com/vmagent.html): properly encode query params for aws signed requests, use `%20` instead of `+` as api requires. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/3171).
* BUGFIX: [vmagent](https://docs.victoriametrics.com/vmagent.html): properly parse relabel config when regex ending with escaped `$`. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/3131).
* BUGFIX: [MetricsQL](https://docs.victoriametrics.com/MetricsQL.html): properly calculate `rate_over_sum(m[d])` as `sum_over_time(m[d])/d`. Previously the `sum_over_time(m[d])` could be improperly divided by smaller than `d` time range. See [rate_over_sum() docs](https://docs.victoriametrics.com/MetricsQL.html#rate_over_sum) and [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/3045).
* BUGFIX: [MetricsQL](https://docs.victoriametrics.com/MetricsQL.html): properly calculate `increase(m[d])` over slow-changing counters with values smaller than 100. Previously [increase](https://docs.victoriametrics.com/MetricsQL.html#increase) could return unexpectedly big results in this case. See [the related issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/962) and [this pull request](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/3163).
* BUGFIX: [MetricsQL](https://docs.victoriametrics.com/MetricsQL.html): ignore empty series when applying [limit_offset](https://docs.victoriametrics.com/MetricsQL.html#limit_offset). It should improve queries with additional filters by value in expressions like `limit_offset(1,1, foo > 1)`.
* BUGFIX: [VictoriaMetrics cluster](https://docs.victoriametrics.com/Cluster-VictoriaMetrics.html): properly calculate query results at `vmselect`. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/3067). The issue has been introduced in [v1.81.0](https://docs.victoriametrics.com/CHANGELOG.html#v1810).
* BUGFIX: [VictoriaMetrics cluster](https://docs.victoriametrics.com/Cluster-VictoriaMetrics.html): log clear error when multiple identical `-storageNode` command-line flags are passed to `vmselect` or to `vminsert`. Previously these components were crashed with cryptic panic `metric ... is already registered` in this case. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/3076).
* BUGFIX: [vmui](https://docs.victoriametrics.com/#vmui): fix `RangeError: Maximum call stack size exceeded` error when the query returns too many data points at `Table` view. See [this pull request](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/3092/files).

View file

@ -208,7 +208,7 @@ See more details about cardinality limiter in [these docs](https://docs.victoria
## Troubleshooting
See [trobuleshooting docs](https://docs.victoriametrics.com/Troubleshooting.html).
See [troubleshooting docs](https://docs.victoriametrics.com/Troubleshooting.html).
## Readonly mode

File diff suppressed because it is too large Load diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 265 KiB

After

Width:  |  Height:  |  Size: 145 KiB

View file

@ -124,7 +124,7 @@ name: <string>
# params:
# nocache: ["1"] # disable caching for vmselect
# denyPartialResponse: ["true"] # fail if one or more vmstorage nodes returned an error
# extra_label: ["env=dev"] # apply additional label filter "env=dev" for all requests
# extra_label: ["env=dev"] # apply additional label filter "env=dev" for all requests
# see more details at https://docs.victoriametrics.com#prometheus-querying-api-enhancements
params:
[ <string>: [<string>, ...]]
@ -135,7 +135,7 @@ params:
# headers:
# - "CustomHeader: foo"
# - "CustomHeader2: bar"
# Headers set via this param have priority over headers set via `-datasource.headers` flag.
# Headers set via this param have priority over headers set via `-datasource.headers` flag.
headers:
[ <string>, ...]
@ -189,8 +189,8 @@ expr: <string>
[ for: <duration> | default = 0s ]
# Whether to print debug information into logs.
# Information includes alerts state changes and requests sent to the datasource.
# Please note, that if rule's query params contain sensitive
# Information includes alerts state changes and requests sent to the datasource.
# Please note, that if rule's query params contain sensitive
# information - it will be printed to logs.
# Is applicable to alerting rules only.
[ debug: <bool> | default = false ]
@ -204,9 +204,9 @@ annotations:
[ <labelname>: <tmpl_string> ]
```
#### Templating
#### Templating
It is allowed to use [Go templating](https://golang.org/pkg/text/template/) in annotations to format data, iterate over
It is allowed to use [Go templating](https://golang.org/pkg/text/template/) in annotations to format data, iterate over
or execute expressions.
The following variables are available in templating:
@ -228,7 +228,7 @@ and [reusable templates](#reusable-templates).
#### Reusable templates
Like in Alertmanager you can define [reusable templates](https://prometheus.io/docs/prometheus/latest/configuration/template_examples/#defining-reusable-templates)
to share same templates across annotations. Just define the templates in a file and
to share same templates across annotations. Just define the templates in a file and
set the path via `-rule.templates` flag.
For example, template `grafana.filter` can be defined as following:
@ -422,8 +422,8 @@ To avoid recording rules results and alerts state duplication in VictoriaMetrics
don't forget to configure [deduplication](https://docs.victoriametrics.com/Single-server-VictoriaMetrics.html#deduplication).
The recommended value for `-dedup.minScrapeInterval` must be greater or equal to vmalert's `evaluation_interval`.
If you observe inconsistent or "jumping" values in series produced by vmalert, try disabling `-datasource.queryTimeAlignment`
command line flag. Because of alignment, two or more vmalert HA pairs will produce results with the same timestamps.
But due of backfilling (data delivered to the datasource with some delay) values of such results may differ,
command line flag. Because of alignment, two or more vmalert HA pairs will produce results with the same timestamps.
But due of backfilling (data delivered to the datasource with some delay) values of such results may differ,
which would affect deduplication logic and result into "jumping" datapoints.
Alertmanager will automatically deduplicate alerts with identical labels, so ensure that
@ -443,8 +443,8 @@ This ability allows to aggregate data. For example, the following rule will calc
metric `http_requests` on the `5m` interval:
```yaml
- record: http_requests:avg5m
expr: avg_over_time(http_requests[5m])
- record: http_requests:avg5m
expr: avg_over_time(http_requests[5m])
```
Every time this rule will be evaluated, `vmalert` will backfill its results as a new time series `http_requests:avg5m`
@ -458,9 +458,9 @@ This ability allows to downsample data. For example, the following config will e
groups:
- name: my_group
interval: 5m
rules:
rules:
- record: http_requests:avg5m
expr: avg_over_time(http_requests[5m])
expr: avg_over_time(http_requests[5m])
```
Ability of `vmalert` to be configured with different `datasource.url` and `remoteWrite.url` allows
@ -478,7 +478,7 @@ or reducing resolution) and push results to "cold" cluster.
```
./bin/vmalert -rule=downsampling-rules.yml \ # Path to the file with rules configuration. Supports wildcard
-datasource.url=http://raw-cluster-vmselect:8481/select/0/prometheus # vmselect addr for executing recordi ng rules expressions
-datasource.url=http://raw-cluster-vmselect:8481/select/0/prometheus # vmselect addr for executing recording rules expressions
-remoteWrite.url=http://aggregated-cluster-vminsert:8480/insert/0/prometheus # vminsert addr to persist recording rules results
```
@ -500,7 +500,7 @@ we recommend using [vmagent](https://docs.victoriametrics.com/vmagent.html) as f
In this topology, `vmalert` is configured to persist rule results to `vmagent`. And `vmagent`
is configured to fan-out received data to two or more destinations.
Using `vmagent` as a proxy provides additional benefits such as
Using `vmagent` as a proxy provides additional benefits such as
[data persisting when storage is unreachable](https://docs.victoriametrics.com/vmagent.html#replication-and-high-availability),
or time series modification via [relabeling](https://docs.victoriametrics.com/vmagent.html#relabeling).
@ -661,7 +661,7 @@ Note that too small value passed to `-search.latencyOffset` may lead to incomple
Try the following recommendations in such cases:
* Always configure group's `evaluationInterval` to be bigger or equal to `scrape_interval` at which metrics
* Always configure group's `evaluationInterval` to be bigger or equal to `scrape_interval` at which metrics
are delivered to the datasource;
* If you know in advance, that data in datasource is delayed - try changing vmalert's `-datasource.lookback`
command-line flag to add a time shift for evaluations;
@ -675,7 +675,7 @@ after multiple consecutive evaluations, and at each evaluation their expression
becomes false, then alert's state resets to the initial state.
If `-remoteWrite.url` command-line flag is configured, vmalert will persist alert's state in form of time series
`ALERTS` and `ALERTS_FOR_STATE` to the specified destination. Such time series can be then queried via
`ALERTS` and `ALERTS_FOR_STATE` to the specified destination. Such time series can be then queried via
[vmui](https://docs.victoriametrics.com/Single-server-VictoriaMetrics.html#vmui) or Grafana to track how alerts state
changed in time.
@ -684,9 +684,9 @@ on `/vmalert/groups` page and check the `Last updates` section:
<img alt="vmalert state" src="vmalert_state.png">
Rows in the section represent ordered rule evaluations and their results. The column `curl` contains an example of
Rows in the section represent ordered rule evaluations and their results. The column `curl` contains an example of
HTTP request sent by vmalert to the `-datasource.url` during evaluation. If specific state shows that there were
no samples returned and curl command returns data - then it is very likely there was no data in datasource on the
no samples returned and curl command returns data - then it is very likely there was no data in datasource on the
moment when rule was evaluated.
vmalert also alows configuring more detailed logging for specific rule. Just set `debug: true` in rule's configuration
@ -766,11 +766,11 @@ The shortlist of configuration flags is the following:
-datasource.maxIdleConnections int
Defines the number of idle (keep-alive connections) to each configured datasource. Consider setting this value equal to the value: groups_total * group.concurrency. Too low a value may result in a high number of sockets in TIME_WAIT state. (default 100)
-datasource.oauth2.clientID string
Optional OAuth2 clientID to use for -datasource.url.
Optional OAuth2 clientID to use for -datasource.url.
-datasource.oauth2.clientSecret string
Optional OAuth2 clientSecret to use for -datasource.url.
-datasource.oauth2.clientSecretFile string
Optional OAuth2 clientSecretFile to use for -datasource.url.
Optional OAuth2 clientSecretFile to use for -datasource.url.
-datasource.oauth2.scopes string
Optional OAuth2 scopes to use for -datasource.url. Scopes must be delimited by ';'
-datasource.oauth2.tokenUrl string
@ -964,7 +964,7 @@ The shortlist of configuration flags is the following:
-remoteRead.oauth2.scopes string
Optional OAuth2 scopes to use for -remoteRead.url. Scopes must be delimited by ';'.
-remoteRead.oauth2.tokenUrl string
Optional OAuth2 tokenURL to use for -remoteRead.url.
Optional OAuth2 tokenURL to use for -remoteRead.url.
-remoteRead.showURL
Whether to show -remoteRead.url in the exported metrics. It is hidden by default, since it can contain sensitive info such as auth key
-remoteRead.tlsCAFile string
@ -1125,7 +1125,7 @@ and [DNS](https://prometheus.io/docs/prometheus/latest/configuration/configurati
For example:
```
static_configs:
static_configs:
- targets:
- localhost:9093
- localhost:9095
@ -1134,7 +1134,7 @@ consul_sd_configs:
- server: localhost:8500
services:
- alertmanager
dns_sd_configs:
- names:
- my.domain.com

View file

@ -180,6 +180,8 @@ curl 'http://localhost:8431/api/v1/labels' -H 'Authorization: Bearer eyJhbGciOiJ
The shortlist of configuration flags include the following:
```console
-auth.httpHeader
HTTP header name to look for JWT authorization token
-clusterMode
enable this for the cluster version
-datasource.appendTypePrefix

View file

@ -41,6 +41,9 @@ func signRequestWithTime(req *http.Request, service, region, payloadHash string,
datestamp := t.Format("20060102")
canonicalURL := uri.Path
canonicalQS := uri.Query().Encode()
// Replace "%20" with "+" according to AWS requirements.
// See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/3171
canonicalQS = strings.ReplaceAll(canonicalQS, "+", "%20")
canonicalHeaders := fmt.Sprintf("host:%s\nx-amz-date:%s\n", uri.Host, amzdate)
signedHeaders := "host;x-amz-date"

View file

@ -0,0 +1,48 @@
package bytesutil
import (
"sync"
"sync/atomic"
)
// FastStringMatcher implements fast matcher for strings.
//
// It caches string match results and returns them back on the next calls
// without calling the matchFunc, which may be expensive.
type FastStringMatcher struct {
m atomic.Value
mLen uint64
matchFunc func(s string) bool
}
// NewFastStringMatcher creates new matcher, which applies matchFunc to strings passed to Match()
//
// matchFunc must return the same result for the same input.
func NewFastStringMatcher(matchFunc func(s string) bool) *FastStringMatcher {
var fsm FastStringMatcher
fsm.m.Store(&sync.Map{})
fsm.matchFunc = matchFunc
return &fsm
}
// Match applies matchFunc to s and returns the result.
func (fsm *FastStringMatcher) Match(s string) bool {
m := fsm.m.Load().(*sync.Map)
v, ok := m.Load(s)
if ok {
// Fast path - s match result is found in the cache.
bp := v.(*bool)
return *bp
}
// Slow path - run matchFunc for s and store the result in the cache.
b := fsm.matchFunc(s)
bp := &b
m.Store(s, bp)
n := atomic.AddUint64(&fsm.mLen, 1)
if n > 100e3 {
atomic.StoreUint64(&fsm.mLen, 0)
fsm.m.Store(&sync.Map{})
}
return b
}

View file

@ -0,0 +1,25 @@
package bytesutil
import (
"strings"
"testing"
)
func TestFastStringMatcher(t *testing.T) {
fsm := NewFastStringMatcher(func(s string) bool {
return strings.HasPrefix(s, "foo")
})
f := func(s string, resultExpected bool) {
t.Helper()
for i := 0; i < 10; i++ {
result := fsm.Match(s)
if result != resultExpected {
t.Fatalf("unexpected result for Match(%q) at iteration %d; got %v; want %v", s, i, result, resultExpected)
}
}
}
f("", false)
f("foo", true)
f("a_b-C", false)
f("foobar", true)
}

View file

@ -0,0 +1,33 @@
package bytesutil
import (
"strings"
"sync/atomic"
"testing"
)
func BenchmarkFastStringMatcher(b *testing.B) {
for _, s := range []string{"", "foo", "foo-bar-baz", "http_requests_total"} {
b.Run(s, func(b *testing.B) {
benchmarkFastStringMatcher(b, s)
})
}
}
func benchmarkFastStringMatcher(b *testing.B, s string) {
fsm := NewFastStringMatcher(func(s string) bool {
return strings.HasPrefix(s, "foo")
})
b.ReportAllocs()
b.SetBytes(1)
b.RunParallel(func(pb *testing.PB) {
n := uint64(0)
for pb.Next() {
v := fsm.Match(s)
if v {
n++
}
}
atomic.AddUint64(&GlobalSink, n)
})
}

View file

@ -0,0 +1,57 @@
package bytesutil
import (
"strings"
"sync"
"sync/atomic"
)
// FastStringTransformer implements fast transformer for strings.
//
// It caches transformed strings and returns them back on the next calls
// without calling the transformFunc, which may be expensive.
type FastStringTransformer struct {
m atomic.Value
mLen uint64
transformFunc func(s string) string
}
// NewFastStringTransformer creates new transformer, which applies transformFunc to strings passed to Transform()
//
// transformFunc must return the same result for the same input.
func NewFastStringTransformer(transformFunc func(s string) string) *FastStringTransformer {
var fst FastStringTransformer
fst.m.Store(&sync.Map{})
fst.transformFunc = transformFunc
return &fst
}
// Transform applies transformFunc to s and returns the result.
func (fst *FastStringTransformer) Transform(s string) string {
m := fst.m.Load().(*sync.Map)
v, ok := m.Load(s)
if ok {
// Fast path - the transformed s is found in the cache.
sp := v.(*string)
return *sp
}
// Slow path - transform s and store it in the cache.
sTransformed := fst.transformFunc(s)
// Make a copy of s in order to limit memory usage to the s length,
// since the s may point to bigger string.
s = strings.Clone(s)
if sTransformed == s {
// point sTransformed to just allocated s, since it may point to s,
// which, in turn, can point to bigger string.
sTransformed = s
}
sp := &sTransformed
m.Store(s, sp)
n := atomic.AddUint64(&fst.mLen, 1)
if n > 100e3 {
atomic.StoreUint64(&fst.mLen, 0)
fst.m.Store(&sync.Map{})
}
return sTransformed
}

View file

@ -0,0 +1,22 @@
package bytesutil
import (
"strings"
"testing"
)
func TestFastStringTransformer(t *testing.T) {
fst := NewFastStringTransformer(strings.ToUpper)
f := func(s, resultExpected string) {
t.Helper()
for i := 0; i < 10; i++ {
result := fst.Transform(s)
if result != resultExpected {
t.Fatalf("unexpected result for Transform(%q) at iteration %d; got %q; want %q", s, i, result, resultExpected)
}
}
}
f("", "")
f("foo", "FOO")
f("a_b-C", "A_B-C")
}

View file

@ -0,0 +1,31 @@
package bytesutil
import (
"strings"
"sync/atomic"
"testing"
)
func BenchmarkFastStringTransformer(b *testing.B) {
for _, s := range []string{"", "foo", "foo-bar-baz", "http_requests_total"} {
b.Run(s, func(b *testing.B) {
benchmarkFastStringTransformer(b, s)
})
}
}
func benchmarkFastStringTransformer(b *testing.B, s string) {
fst := NewFastStringTransformer(strings.ToUpper)
b.ReportAllocs()
b.SetBytes(1)
b.RunParallel(func(pb *testing.PB) {
n := uint64(0)
for pb.Next() {
sTransformed := fst.Transform(s)
n += uint64(len(sTransformed))
}
atomic.AddUint64(&GlobalSink, n)
})
}
var GlobalSink uint64

View file

@ -1,4 +1,4 @@
package discoveryutils
package bytesutil
import (
"sync"

View file

@ -1,4 +1,4 @@
package discoveryutils
package bytesutil
import (
"fmt"

View file

@ -1,4 +1,4 @@
package discoveryutils
package bytesutil
import (
"fmt"

View file

@ -6,6 +6,7 @@ import (
"strconv"
"strings"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/bytesutil"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/envtemplate"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/fs"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
@ -346,7 +347,7 @@ func parseRelabelConfig(rc *RelabelConfig) (*parsedRelabelConfig, error) {
return nil, fmt.Errorf("`labels` config cannot be applied to `action=%s`; it is applied only to `action=graphite`", action)
}
}
return &parsedRelabelConfig{
prc := &parsedRelabelConfig{
SourceLabels: sourceLabels,
Separator: separator,
TargetLabel: targetLabel,
@ -365,7 +366,10 @@ func parseRelabelConfig(rc *RelabelConfig) (*parsedRelabelConfig, error) {
hasCaptureGroupInTargetLabel: strings.Contains(targetLabel, "$"),
hasCaptureGroupInReplacement: strings.Contains(replacement, "$"),
hasLabelReferenceInReplacement: strings.Contains(replacement, "{{"),
}, nil
}
prc.stringReplacer = bytesutil.NewFastStringTransformer(prc.replaceFullStringSlow)
prc.submatchReplacer = bytesutil.NewFastStringTransformer(prc.replaceStringSubmatchesSlow)
return prc, nil
}
func isDefaultRegex(expr string) bool {

View file

@ -161,6 +161,12 @@ func TestParseRelabelConfigsSuccess(t *testing.T) {
if err != nil {
t.Fatalf("unexpected error: %s", err)
}
if pcs != nil {
for _, prc := range pcs.prcs {
prc.stringReplacer = nil
prc.submatchReplacer = nil
}
}
if !reflect.DeepEqual(pcs, pcsExpected) {
t.Fatalf("unexpected pcs; got\n%#v\nwant\n%#v", pcs, pcsExpected)
}

View file

@ -35,6 +35,9 @@ type parsedRelabelConfig struct {
hasCaptureGroupInTargetLabel bool
hasCaptureGroupInReplacement bool
hasLabelReferenceInReplacement bool
stringReplacer *bytesutil.FastStringTransformer
submatchReplacer *bytesutil.FastStringTransformer
}
// String returns human-readable representation for prc.
@ -203,23 +206,29 @@ func (prc *parsedRelabelConfig) apply(labels []prompbmarshal.Label, labelsOffset
return labels
}
}
if re := prc.regex; re.HasPrefix() && !re.MatchString(bytesutil.ToUnsafeString(bb.B)) {
sourceStr := bytesutil.ToUnsafeString(bb.B)
if !prc.regex.MatchString(sourceStr) {
// Fast path - regexp mismatch.
relabelBufPool.Put(bb)
return labels
}
match := prc.RegexAnchored.FindSubmatchIndex(bb.B)
if match == nil {
// Fast path - nothing to replace.
relabelBufPool.Put(bb)
return labels
var valueStr string
if replacement == prc.Replacement {
// Fast path - the replacement wasn't modified, so it is safe calling stringReplacer.Transform.
valueStr = prc.stringReplacer.Transform(sourceStr)
} else {
// Slow path - the replacement has been modified, so the valueStr must be calculated
// from scratch based on the new replacement value.
match := prc.RegexAnchored.FindSubmatchIndex(bb.B)
valueStr = prc.expandCaptureGroups(replacement, sourceStr, match)
}
sourceStr := bytesutil.ToUnsafeString(bb.B)
nameStr := prc.TargetLabel
if prc.hasCaptureGroupInTargetLabel {
// Slow path - target_label contains regex capture groups, so the target_label
// must be calculated from the regex match.
match := prc.RegexAnchored.FindSubmatchIndex(bb.B)
nameStr = prc.expandCaptureGroups(nameStr, sourceStr, match)
}
valueStr := prc.expandCaptureGroups(replacement, sourceStr, match)
relabelBufPool.Put(bb)
return setLabelValue(labels, labelsOffset, nameStr, valueStr)
case "replace_all":
@ -229,8 +238,8 @@ func (prc *parsedRelabelConfig) apply(labels []prompbmarshal.Label, labelsOffset
bb.B = concatLabelValues(bb.B[:0], src, prc.SourceLabels, prc.Separator)
sourceStr := string(bb.B)
relabelBufPool.Put(bb)
valueStr, ok := prc.replaceStringSubmatches(sourceStr, prc.Replacement, prc.hasCaptureGroupInReplacement)
if ok {
valueStr := prc.replaceStringSubmatchesFast(sourceStr)
if valueStr != sourceStr {
labels = setLabelValue(labels, labelsOffset, prc.TargetLabel, valueStr)
}
return labels
@ -305,8 +314,8 @@ func (prc *parsedRelabelConfig) apply(labels []prompbmarshal.Label, labelsOffset
case "labelmap":
// Replace label names with the `replacement` if they match `regex`
for _, label := range src {
labelName, ok := prc.replaceFullString(label.Name, prc.Replacement, prc.hasCaptureGroupInReplacement)
if ok {
labelName := prc.replaceFullStringFast(label.Name)
if labelName != label.Name {
labels = setLabelValue(labels, labelsOffset, labelName, label.Value)
}
}
@ -315,7 +324,7 @@ func (prc *parsedRelabelConfig) apply(labels []prompbmarshal.Label, labelsOffset
// Replace all the occurrences of `regex` at label names with `replacement`
for i := range src {
label := &src[i]
label.Name, _ = prc.replaceStringSubmatches(label.Name, prc.Replacement, prc.hasCaptureGroupInReplacement)
label.Name = prc.replaceStringSubmatchesFast(label.Name)
}
return labels
case "labeldrop":
@ -360,16 +369,23 @@ func (prc *parsedRelabelConfig) apply(labels []prompbmarshal.Label, labelsOffset
}
}
func (prc *parsedRelabelConfig) replaceFullString(s, replacement string, hasCaptureGroupInReplacement bool) (string, bool) {
// replaceFullStringFast replaces s with the replacement if s matches '^regex$'.
//
// s is returned as is if it doesn't match '^regex$'.
func (prc *parsedRelabelConfig) replaceFullStringFast(s string) string {
prefix, complete := prc.regexOriginal.LiteralPrefix()
if complete && !hasCaptureGroupInReplacement {
replacement := prc.Replacement
if complete && !prc.hasCaptureGroupInReplacement {
if s == prefix {
return replacement, true
// Fast path - s matches literal regex
return replacement
}
return s, false
// Fast path - s doesn't match literal regex
return s
}
if !strings.HasPrefix(s, prefix) {
return s, false
// Fast path - s doesn't match literl prefix from regex
return s
}
if replacement == "$1" {
// Fast path for commonly used rule for deleting label prefixes such as:
@ -383,44 +399,49 @@ func (prc *parsedRelabelConfig) replaceFullString(s, replacement string, hasCapt
reSuffix := reStr[len(prefix):]
switch reSuffix {
case "(.*)":
return suffix, true
return suffix
case "(.+)":
if len(suffix) > 0 {
return suffix, true
return suffix
}
return s, false
return s
}
}
}
if re := prc.regex; re.HasPrefix() && !re.MatchString(s) {
if !prc.regex.MatchString(s) {
// Fast path - regex mismatch
return s, false
return s
}
// Slow path - handle the rest of cases.
return prc.stringReplacer.Transform(s)
}
// replaceFullStringSlow replaces s with the replacement if s matches '^regex$'.
//
// s is returned as is if it doesn't match '^regex$'.
func (prc *parsedRelabelConfig) replaceFullStringSlow(s string) string {
// Slow path - regexp processing
match := prc.RegexAnchored.FindStringSubmatchIndex(s)
if match == nil {
return s, false
return s
}
bb := relabelBufPool.Get()
bb.B = prc.RegexAnchored.ExpandString(bb.B[:0], replacement, s, match)
result := string(bb.B)
relabelBufPool.Put(bb)
return result, true
return prc.expandCaptureGroups(prc.Replacement, s, match)
}
func (prc *parsedRelabelConfig) replaceStringSubmatches(s, replacement string, hasCaptureGroupInReplacement bool) (string, bool) {
re := prc.regexOriginal
prefix, complete := re.LiteralPrefix()
if complete && !hasCaptureGroupInReplacement {
if !strings.Contains(s, prefix) {
return s, false
}
return strings.ReplaceAll(s, prefix, replacement), true
// replaceStringSubmatchesFast replaces all the regex matches with the replacement in s.
func (prc *parsedRelabelConfig) replaceStringSubmatchesFast(s string) string {
prefix, complete := prc.regexOriginal.LiteralPrefix()
if complete && !prc.hasCaptureGroupInReplacement && !strings.Contains(s, prefix) {
// Fast path - zero regex matches in s.
return s
}
if !re.MatchString(s) {
return s, false
}
return re.ReplaceAllString(s, replacement), true
// Slow path - replace all the regex matches in s with the replacement.
return prc.submatchReplacer.Transform(s)
}
// replaceStringSubmatchesSlow replaces all the regex matches with the replacement in s.
func (prc *parsedRelabelConfig) replaceStringSubmatchesSlow(s string) string {
return prc.regexOriginal.ReplaceAllString(s, prc.Replacement)
}
func (prc *parsedRelabelConfig) expandCaptureGroups(template, source string, match []int) string {
@ -558,3 +579,16 @@ func fillLabelReferences(dst []byte, replacement string, labels []prompbmarshal.
}
return dst
}
// SanitizeName replaces unsupported by Prometheus chars in metric names and label names with _.
//
// See https://prometheus.io/docs/concepts/data_model/#metric-names-and-labels
func SanitizeName(name string) string {
return promSanitizer.Transform(name)
}
var promSanitizer = bytesutil.NewFastStringTransformer(func(s string) string {
return unsupportedPromChars.ReplaceAllString(s, "_")
})
var unsupportedPromChars = regexp.MustCompile(`[^a-zA-Z0-9_:]`)

View file

@ -7,6 +7,22 @@ import (
"github.com/VictoriaMetrics/VictoriaMetrics/lib/prompbmarshal"
)
func TestSanitizeName(t *testing.T) {
f := func(s, resultExpected string) {
t.Helper()
for i := 0; i < 5; i++ {
result := SanitizeName(s)
if result != resultExpected {
t.Fatalf("unexpected result for SanitizeName(%q) at iteration %d; got %q; want %q", s, i, result, resultExpected)
}
}
}
f("", "")
f("a", "a")
f("foo.bar/baz:a", "foo_bar_baz:a")
f("foo...bar", "foo___bar")
}
func TestLabelsToString(t *testing.T) {
f := func(labels []prompbmarshal.Label, sExpected string) {
t.Helper()
@ -666,6 +682,15 @@ func TestApplyRelabelConfigs(t *testing.T) {
regex: "a(.+)"
`, `qwe{foo="bar",baz="aaa"}`, true, `qwe{abc="qwe.bar.aa",baz="aaa",foo="bar"}`)
})
// Check $ at the end of regex - see https://github.com/VictoriaMetrics/VictoriaMetrics/issues/3131
t.Run("replacement-with-$-at-the-end-of-regex", func(t *testing.T) {
f(`
- target_label: xyz
regex: "foo\\$$"
replacement: bar
source_labels: [xyz]
`, `metric{xyz="foo$",a="b"}`, true, `metric{a="b",xyz="bar"}`)
})
}
func TestFinalizeLabels(t *testing.T) {

View file

@ -8,6 +8,27 @@ import (
"github.com/VictoriaMetrics/VictoriaMetrics/lib/prompbmarshal"
)
func BenchmarkSanitizeName(b *testing.B) {
for _, name := range []string{"", "foo", "foo-bar-baz", "http_requests_total"} {
b.Run(name, func(b *testing.B) {
benchmarkSanitizeName(b, name)
})
}
}
func benchmarkSanitizeName(b *testing.B, name string) {
b.ReportAllocs()
b.SetBytes(1)
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
sanitizedName := SanitizeName(name)
GlobalSink += len(sanitizedName)
}
})
}
var GlobalSink int
func BenchmarkMatchRegexPrefixDotPlusMatchOptimized(b *testing.B) {
const pattern = "^foo.+$"
const s = "foobar"

View file

@ -34,7 +34,6 @@ import (
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promscrape/discovery/kubernetes"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promscrape/discovery/openstack"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promscrape/discovery/yandexcloud"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promscrape/discoveryutils"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promutils"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/proxy"
"github.com/VictoriaMetrics/metrics"
@ -1330,8 +1329,8 @@ func (swc *scrapeWorkConfig) getScrapeWork(target string, extraLabels, metaLabel
func internLabelStrings(labels []prompbmarshal.Label) {
for i := range labels {
label := &labels[i]
label.Name = discoveryutils.InternString(label.Name)
label.Value = discoveryutils.InternString(label.Value)
label.Name = bytesutil.InternString(label.Name)
label.Value = bytesutil.InternString(label.Value)
}
}

View file

@ -674,8 +674,9 @@ scrape_config_files:
}
func resetNonEssentialFields(sws []*ScrapeWork) {
for i := range sws {
sws[i].OriginalLabels = nil
for _, sw := range sws {
sw.OriginalLabels = nil
sw.MetricRelabelConfigs = nil
}
}
@ -1446,10 +1447,6 @@ scrape_configs:
},
AuthConfig: &promauth.Config{},
ProxyAuthConfig: &promauth.Config{},
MetricRelabelConfigs: mustParseRelabelConfigs(`
- source_labels: [foo]
target_label: abc
`),
jobNameOriginal: "foo",
},
})
@ -1847,8 +1844,10 @@ func TestScrapeConfigClone(t *testing.T) {
f := func(sc *ScrapeConfig) {
t.Helper()
scCopy := sc.clone()
if !reflect.DeepEqual(sc, scCopy) {
t.Fatalf("unexpected result after unmarshalJSON() for JSON:\n%s", sc.marshalJSON())
scJSON := sc.marshalJSON()
scCopyJSON := scCopy.marshalJSON()
if !reflect.DeepEqual(scJSON, scCopyJSON) {
t.Fatalf("unexpected cloned result:\ngot\n%s\nwant\n%s", scCopyJSON, scJSON)
}
}

View file

@ -6,9 +6,8 @@ import (
"regexp"
"sort"
"strconv"
"sync"
"sync/atomic"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/bytesutil"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/prompbmarshal"
)
@ -17,43 +16,14 @@ import (
//
// This has been copied from Prometheus sources at util/strutil/strconv.go
func SanitizeLabelName(name string) string {
m := sanitizedLabelNames.Load().(*sync.Map)
v, ok := m.Load(name)
if ok {
// Fast path - the sanitized label name is found in the cache.
sp := v.(*string)
return *sp
}
// Slow path - sanitize name and store it in the cache.
sanitizedName := invalidLabelCharRE.ReplaceAllString(name, "_")
// Make a copy of name in order to limit memory usage to the name length,
// since the name may point to bigger string.
s := string(append([]byte{}, name...))
if sanitizedName == name {
// point sanitizedName to just allocated s, since it may point to name,
// which, in turn, can point to bigger string.
sanitizedName = s
}
sp := &sanitizedName
m.Store(s, sp)
n := atomic.AddUint64(&sanitizedLabelNamesLen, 1)
if n > 100e3 {
atomic.StoreUint64(&sanitizedLabelNamesLen, 0)
sanitizedLabelNames.Store(&sync.Map{})
}
return sanitizedName
return labelNamesSanitizer.Transform(name)
}
var (
sanitizedLabelNames atomic.Value
sanitizedLabelNamesLen uint64
var labelNamesSanitizer = bytesutil.NewFastStringTransformer(func(s string) string {
return invalidLabelCharRE.ReplaceAllString(s, "_")
})
invalidLabelCharRE = regexp.MustCompile(`[^a-zA-Z0-9_]`)
)
func init() {
sanitizedLabelNames.Store(&sync.Map{})
}
var invalidLabelCharRE = regexp.MustCompile(`[^a-zA-Z0-9_]`)
// JoinHostPort returns host:port.
//

View file

@ -155,12 +155,16 @@ var requestPool sync.Pool
// sanitizeName performs DataDog-compatible santizing for metric names
//
// See https://docs.datadoghq.com/metrics/custom_metrics/#naming-custom-metrics
func sanitizeName(s string) string {
func sanitizeName(name string) string {
return namesSanitizer.Transform(name)
}
var namesSanitizer = bytesutil.NewFastStringTransformer(func(s string) string {
s = unsupportedDatadogChars.ReplaceAllString(s, "_")
s = multiUnderscores.ReplaceAllString(s, "_")
s = underscoresWithDots.ReplaceAllString(s, ".")
return s
}
})
var (
unsupportedDatadogChars = regexp.MustCompile(`[^0-9a-zA-Z_\.]+`)

View file

@ -3,6 +3,8 @@ package regexutil
import (
"regexp"
"strings"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/bytesutil"
)
// PromRegex implements an optimized string matching for Prometheus-like regex.
@ -13,6 +15,8 @@ import (
// - alternate strings such as "foo|bar|baz"
// - prefix match such as "foo.*" or "foo.+"
// - substring match such as ".*foo.*" or ".+bar.+"
//
// The rest of regexps are also optimized by returning cached match results for the same input strings.
type PromRegex struct {
// prefix contains literal prefix for regex.
// For example, prefix="foo" for regex="foo(a|b)"
@ -32,9 +36,8 @@ type PromRegex struct {
// For example, orValues contain ["foo","bar","baz"] for regex suffix="foo|bar|baz"
orValues []string
// reSuffix contains an anchored regexp built from suffix:
// "^(?:suffix)$"
reSuffix *regexp.Regexp
// reSuffixMatcher contains fast matcher for "^suffix$"
reSuffixMatcher *bytesutil.FastStringMatcher
}
// NewPromRegex returns PromRegex for the given expr.
@ -50,25 +53,18 @@ func NewPromRegex(expr string) (*PromRegex, error) {
// Anchor suffix to the beginning and the end of the matching string.
suffixExpr := "^(?:" + suffix + ")$"
reSuffix := regexp.MustCompile(suffixExpr)
reSuffixMatcher := bytesutil.NewFastStringMatcher(reSuffix.MatchString)
pr := &PromRegex{
prefix: prefix,
suffix: suffix,
substrDotStar: substrDotStar,
substrDotPlus: substrDotPlus,
orValues: orValues,
reSuffix: reSuffix,
prefix: prefix,
suffix: suffix,
substrDotStar: substrDotStar,
substrDotPlus: substrDotPlus,
orValues: orValues,
reSuffixMatcher: reSuffixMatcher,
}
return pr, nil
}
// HasPrefix returns true if pr contains non-empty literal prefix.
//
// For example, if pr is "foo(bar|baz)", then the prefix is "foo",
// so HasPrefix() returns true.
func (pr *PromRegex) HasPrefix() bool {
return len(pr.prefix) > 0
}
// MatchString retruns true if s matches pr.
//
// The pr is automatically anchored to the beginning and to the end
@ -106,7 +102,7 @@ func (pr *PromRegex) MatchString(s string) bool {
return len(s) > 0
}
// Fall back to slow path by matching the original regexp.
return pr.reSuffix.MatchString(s)
return pr.reSuffixMatcher.Match(s)
}
func getSubstringLiteral(expr, prefixSuffix string) string {

View file

@ -12,7 +12,7 @@ func RemoveStartEndAnchors(expr string) string {
for strings.HasPrefix(expr, "^") {
expr = expr[1:]
}
for strings.HasSuffix(expr, "$") {
for strings.HasSuffix(expr, "$") && !strings.HasSuffix(expr, "\\$") {
expr = expr[:len(expr)-1]
}
return expr

View file

@ -110,3 +110,22 @@ func TestSimplify(t *testing.T) {
// The transformed regexp mustn't match barx
f("(foo|bar$)x*", "", "(?:foo|bar$)x*")
}
func TestRemoveStartEndAnchors(t *testing.T) {
f := func(s, resultExpected string) {
t.Helper()
result := RemoveStartEndAnchors(s)
if result != resultExpected {
t.Fatalf("unexpected result for RemoveStartEndAnchors(%q); got %q; want %q", s, result, resultExpected)
}
}
f("", "")
f("a", "a")
f("^^abc", "abc")
f("a^b$c", "a^b$c")
f("$$abc^", "$$abc^")
f("^abc|de$", "abc|de")
f("abc\\$", "abc\\$")
f("^abc\\$$$", "abc\\$")
f("^a\\$b\\$$", "a\\$b\\$")
}