mirror of
https://github.com/VictoriaMetrics/VictoriaMetrics.git
synced 2026-07-16 22:00:54 +03:00
Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
663ef1ee45 | ||
|
|
c4e9d59b15 | ||
|
|
868ae63e9a | ||
|
|
a3bda3c4e2 | ||
|
|
14ce82b5ff | ||
|
|
21cc4ef693 | ||
|
|
52de485bc9 | ||
|
|
aeaa0919c2 | ||
|
|
2b600e3646 | ||
|
|
0ca68c7f79 | ||
|
|
a2a960b0ba | ||
|
|
6d80ee6ab8 | ||
|
|
25af4d28ff |
@@ -462,7 +462,9 @@ func requestHandler(w http.ResponseWriter, r *http.Request) bool {
|
||||
return true
|
||||
case "/prometheus/metric-relabel-debug", "/metric-relabel-debug":
|
||||
promscrapeMetricRelabelDebugRequests.Inc()
|
||||
promscrape.WriteMetricRelabelDebug(w, r)
|
||||
rwGlobalRelabelConfigs := remotewrite.GetRemoteWriteRelabelConfigString()
|
||||
rwURLRelabelConfigss := remotewrite.GetURLRelabelConfigString()
|
||||
promscrape.WriteMetricRelabelDebug(w, r, rwGlobalRelabelConfigs, rwURLRelabelConfigss)
|
||||
return true
|
||||
case "/prometheus/target-relabel-debug", "/target-relabel-debug":
|
||||
promscrapeTargetRelabelDebugRequests.Inc()
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"github.com/VictoriaMetrics/metrics"
|
||||
"gopkg.in/yaml.v2"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/bytesutil"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/fasttime"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/flagutil"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
|
||||
@@ -82,6 +83,16 @@ func WriteRelabelConfigData(w io.Writer) {
|
||||
_, _ = w.Write(*p)
|
||||
}
|
||||
|
||||
// GetRemoteWriteRelabelConfigString returns -remoteWrite.relabelConfig contents in string
|
||||
func GetRemoteWriteRelabelConfigString() string {
|
||||
var bb bytesutil.ByteBuffer
|
||||
WriteRelabelConfigData(&bb)
|
||||
if bb.Len() == 0 {
|
||||
return ""
|
||||
}
|
||||
return string(bb.B)
|
||||
}
|
||||
|
||||
// WriteURLRelabelConfigData writes -remoteWrite.urlRelabelConfig contents to w
|
||||
func WriteURLRelabelConfigData(w io.Writer) {
|
||||
p := remoteWriteURLRelabelConfigData.Load()
|
||||
@@ -108,6 +119,24 @@ func WriteURLRelabelConfigData(w io.Writer) {
|
||||
_, _ = w.Write(d)
|
||||
}
|
||||
|
||||
// GetURLRelabelConfigString returns -remoteWrite.urlRelabelConfig contents in []string
|
||||
func GetURLRelabelConfigString() []string {
|
||||
p := remoteWriteURLRelabelConfigData.Load()
|
||||
if p == nil {
|
||||
return nil
|
||||
}
|
||||
var ss []string
|
||||
for i := range *remoteWriteURLs {
|
||||
cfgData := (*p)[i]
|
||||
var cfgDataBytes []byte
|
||||
if cfgData != nil {
|
||||
cfgDataBytes, _ = yaml.Marshal(cfgData)
|
||||
}
|
||||
ss = append(ss, string(cfgDataBytes))
|
||||
}
|
||||
return ss
|
||||
}
|
||||
|
||||
func reloadRelabelConfigs() {
|
||||
rcs := allRelabelConfigs.Load()
|
||||
if !rcs.isSet() {
|
||||
|
||||
@@ -541,7 +541,7 @@ func handleStaticAndSimpleRequests(w http.ResponseWriter, r *http.Request, path
|
||||
return true
|
||||
case "/metric-relabel-debug":
|
||||
promscrapeMetricRelabelDebugRequests.Inc()
|
||||
promscrape.WriteMetricRelabelDebug(w, r)
|
||||
promscrape.WriteMetricRelabelDebug(w, r, "", nil)
|
||||
return true
|
||||
case "/target-relabel-debug":
|
||||
promscrapeTargetRelabelDebugRequests.Inc()
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM golang:1.26.4 AS build-web-stage
|
||||
FROM golang:1.26.5 AS build-web-stage
|
||||
COPY build /build
|
||||
|
||||
WORKDIR /build
|
||||
|
||||
@@ -32,7 +32,7 @@ const CardinalityTotals: FC<CardinalityTotalsProps> = ({
|
||||
const match = searchParams.get("match");
|
||||
const focusLabel = searchParams.get("focusLabel");
|
||||
const isMetric = /__name__/.test(match || "");
|
||||
|
||||
const showMetricNameStats = !(match || focusLabel);
|
||||
const progress = totalSeries / totalSeriesAll * 100;
|
||||
const diff = totalSeries - totalSeriesPrev;
|
||||
const dynamic = Math.abs(diff) / totalSeriesPrev * 100;
|
||||
@@ -56,7 +56,7 @@ const CardinalityTotals: FC<CardinalityTotalsProps> = ({
|
||||
},
|
||||
].filter(t => t.display);
|
||||
|
||||
if (!totals.length) {
|
||||
if (!totals.length && !showMetricNameStats) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -75,7 +75,7 @@ const CardinalityTotals: FC<CardinalityTotalsProps> = ({
|
||||
<h4 className="vm-cardinality-totals-card__title">
|
||||
{info && (
|
||||
<Tooltip title={<p className="vm-cardinality-totals-card__tooltip">{info}</p>}>
|
||||
<div className="vm-cardinality-totals-card__info-icon"><InfoOutlinedIcon/></div>
|
||||
<div className="vm-cardinality-totals-card__info-icon"><InfoOutlinedIcon /></div>
|
||||
</Tooltip>
|
||||
)}
|
||||
{title}
|
||||
@@ -99,7 +99,10 @@ const CardinalityTotals: FC<CardinalityTotalsProps> = ({
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
<CardinalityMetricNameStats metricNameStats={metricNameStats}/>
|
||||
{
|
||||
showMetricNameStats &&
|
||||
<CardinalityMetricNameStats metricNameStats={metricNameStats} />
|
||||
}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -7,7 +7,7 @@ ROOT_IMAGE ?= alpine:3.24.1
|
||||
ROOT_IMAGE_SCRATCH ?= scratch
|
||||
CERTS_IMAGE := alpine:3.24.1
|
||||
|
||||
GO_BUILDER_IMAGE := golang:1.26.4
|
||||
GO_BUILDER_IMAGE := golang:1.26.5
|
||||
|
||||
BUILDER_IMAGE := local/builder:2.0.0-$(shell echo $(GO_BUILDER_IMAGE) | tr :/ __)-1
|
||||
BASE_IMAGE := local/base:1.1.4-$(shell echo $(ROOT_IMAGE) | tr :/ __)-$(shell echo $(CERTS_IMAGE) | tr :/ __)
|
||||
|
||||
@@ -14,6 +14,178 @@ aliases:
|
||||
- /quick-start/index.html
|
||||
- /quick-start/
|
||||
---
|
||||
There are two ways to get started with VictoriaMetrics:
|
||||
|
||||
* [Try it locally](https://docs.victoriametrics.com/victoriametrics/quick-start/#try-it-locally) - if you just want to see how VictoriaMetrics works,
|
||||
go with the single binary: download it, start it with one command, and see live metrics
|
||||
in the built-in UI in a couple of minutes. No Docker, no configuration files
|
||||
and no extra components are required;
|
||||
* [Install it](https://docs.victoriametrics.com/victoriametrics/quick-start/#how-to-install) - if you want to set up VictoriaMetrics for real use,
|
||||
pick a distribution (single-node, cluster or cloud) and an installation method
|
||||
(Docker, Helm, binary releases, etc.).
|
||||
|
||||
If you'd rather not install anything at all, try [Playgrounds](https://docs.victoriametrics.com/playgrounds/) -
|
||||
a list of publicly available playgrounds for VictoriaMetrics software.
|
||||
|
||||
Whichever way you choose, you may also find interesting the other sections of this page,
|
||||
like how to [write](https://docs.victoriametrics.com/victoriametrics/quick-start/#write-data) and [query](https://docs.victoriametrics.com/victoriametrics/quick-start/#query-data) data,
|
||||
[alerting](https://docs.victoriametrics.com/victoriametrics/quick-start/#alerting),
|
||||
[data migration](https://docs.victoriametrics.com/victoriametrics/quick-start/#data-migration) from other TSDBs,
|
||||
and [productionization](https://docs.victoriametrics.com/victoriametrics/quick-start/#productionization)
|
||||
best practices for running VictoriaMetrics in production.
|
||||
|
||||
## Try it locally
|
||||
|
||||
The fastest way to try VictoriaMetrics on your own machine is its binary - the only thing needed to run it.
|
||||
|
||||
### Step 1: Download the binary
|
||||
|
||||
Create a directory for this test drive, so all the files created along the way stay in one place:
|
||||
|
||||
```sh
|
||||
mkdir vm-quick-start && cd vm-quick-start
|
||||
```
|
||||
|
||||
Download the `victoria-metrics-<os>-<arch>-<version>.tar.gz` archive for your OS and architecture
|
||||
from the [releases page](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/latest)
|
||||
and unpack it. It contains a single `victoria-metrics-prod` binary.
|
||||
|
||||
For example, on Linux with `amd64` architecture:
|
||||
|
||||
```sh
|
||||
wget https://github.com/VictoriaMetrics/VictoriaMetrics/releases/download/v1.147.0/victoria-metrics-linux-amd64-v1.147.0.tar.gz
|
||||
tar xzf victoria-metrics-linux-amd64-v1.147.0.tar.gz
|
||||
```
|
||||
|
||||
The binary is self-contained and requires no installation - it is ready to run as is.
|
||||
|
||||
### Step 2: Start VictoriaMetrics
|
||||
|
||||
Starting VictoriaMetrics is as simple as executing the binary, with no arguments at all.
|
||||
But since it is nicer to have some data to explore right after the start, let's also enable self-scraping
|
||||
via the `-selfScrapeInterval` command-line flag, so VictoriaMetrics collects metrics about itself:
|
||||
|
||||
```sh
|
||||
./victoria-metrics-prod -selfScrapeInterval=10s
|
||||
```
|
||||
|
||||
VictoriaMetrics prints a couple of dozen log lines on start, describing the storage, caches
|
||||
and memory limits it sets up. Look for these two lines confirming that it is up and running:
|
||||
|
||||
```sh
|
||||
2026-07-10T16:55:06.615Z info app/victoria-metrics/main.go:102 started VictoriaMetrics in 0.019 seconds
|
||||
...
|
||||
2026-07-10T16:55:06.615Z info lib/httpserver/httpserver.go:148 started server at http://0.0.0.0:8428/
|
||||
```
|
||||
|
||||
That's it - VictoriaMetrics is running, listening on port `8428` and scraping its own metrics
|
||||
(CPU and memory usage, the number of stored metrics, request rates, etc.) every 10 seconds.
|
||||
If you list the `vm-quick-start` directory, you can see a new `victoria-metrics-data` directory
|
||||
created next to the binary - this is where the collected data is stored.
|
||||
|
||||
Now that metrics are being collected, it's time to look at them.
|
||||
|
||||
### Step 3: Explore the metrics
|
||||
|
||||
Open [http://localhost:8428/vmui](http://localhost:8428/vmui) in your browser to access [vmui](https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/#vmui) -
|
||||
the built-in UI for querying and graphing metrics. You should see its query page:
|
||||
|
||||

|
||||
|
||||
Self-scraped metrics become queryable within ~30 seconds after the start.
|
||||
|
||||
Try the following:
|
||||
|
||||
* Open the [metrics explorer](https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/#metrics-explorer)
|
||||
at [http://localhost:8428/vmui/#/metrics](http://localhost:8428/vmui/#/metrics) to browse all collected metrics;
|
||||
* Enter a query in the input field at [http://localhost:8428/vmui](http://localhost:8428/vmui) and press `Enter`. For example:
|
||||
* `process_resident_memory_bytes` - memory used by VictoriaMetrics;
|
||||
* `rate(process_cpu_seconds_total)` - its CPU usage;
|
||||
* `vm_rows{type=~"storage/.+"}` - the number of stored [raw samples](https://docs.victoriametrics.com/victoriametrics/keyconcepts/#raw-samples).
|
||||
|
||||
Queries are written in [MetricsQL](https://docs.victoriametrics.com/victoriametrics/metricsql/) -
|
||||
a PromQL-compatible query language, so any PromQL query works here as well.
|
||||
|
||||
So far the only available metrics are the ones VictoriaMetrics reports about itself - let's collect something more interesting.
|
||||
|
||||
### Step 4 (optional): Collect metrics from your system
|
||||
|
||||
Self-scraped metrics only describe VictoriaMetrics itself. To monitor your machine (CPU, memory, disk, network),
|
||||
run [node_exporter](https://github.com/prometheus/node_exporter) - the standard Prometheus exporter for host metrics -
|
||||
and let VictoriaMetrics scrape it. Single-node VictoriaMetrics has a built-in
|
||||
[Prometheus-compatible scraper](https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/#how-to-scrape-prometheus-exporters-such-as-node-exporter),
|
||||
so no other components are needed.
|
||||
|
||||
1. Download the `node_exporter-<version>.<os>-<arch>.tar.gz` archive for your OS and architecture
|
||||
from the [releases page](https://github.com/prometheus/node_exporter/releases/latest) and unpack it.
|
||||
For example, on Linux with `amd64` architecture:
|
||||
|
||||
```sh
|
||||
wget https://github.com/prometheus/node_exporter/releases/download/v1.12.0/node_exporter-1.12.0.linux-amd64.tar.gz
|
||||
tar xzf node_exporter-1.12.0.linux-amd64.tar.gz
|
||||
```
|
||||
|
||||
Unlike VictoriaMetrics, it unpacks into its own directory. Start it from there:
|
||||
|
||||
```sh
|
||||
./node_exporter-1.12.0.linux-amd64/node_exporter
|
||||
```
|
||||
|
||||
It exposes host metrics at [http://localhost:9100/metrics](http://localhost:9100/metrics).
|
||||
|
||||
1. Create a `scrape.yaml` file with the following contents:
|
||||
|
||||
```yaml
|
||||
scrape_configs:
|
||||
- job_name: node-exporter
|
||||
static_configs:
|
||||
- targets:
|
||||
- localhost:9100
|
||||
```
|
||||
|
||||
1. Restart VictoriaMetrics with the `-promscrape.config` command-line flag pointing to this file:
|
||||
|
||||
```sh
|
||||
./victoria-metrics-prod -selfScrapeInterval=10s -promscrape.config=scrape.yaml
|
||||
```
|
||||
|
||||
Check [http://localhost:8428/targets](http://localhost:8428/targets) - the `node-exporter` target should have `state: up`.
|
||||
The target shows up as `state: down` until the first scrape happens, which can take some seconds -
|
||||
just reload the page a bit later.
|
||||
Now query host metrics in [vmui](http://localhost:8428/vmui). For example:
|
||||
|
||||
* `node_memory_MemAvailable_bytes` - available memory on your machine;
|
||||
* `100 - avg(rate(node_cpu_seconds_total{mode="idle"})) * 100` - overall CPU usage in percent.
|
||||
|
||||
See [scrape config examples](https://docs.victoriametrics.com/victoriametrics/scrape_config_examples/) for more advanced scrape configurations.
|
||||
|
||||
Scraping pulls metrics from targets, but it is not the only way to get data in - metrics can also be pushed directly.
|
||||
|
||||
### Step 5 (optional): Push your own metrics
|
||||
|
||||
VictoriaMetrics also accepts metrics pushed via [many popular protocols](https://docs.victoriametrics.com/victoriametrics/keyconcepts/#push-model).
|
||||
For example, insert a measurement using the InfluxDB line protocol with a plain `curl` command:
|
||||
|
||||
```sh
|
||||
curl -d 'room_temperature,room=kitchen value=21.5' http://localhost:8428/write
|
||||
```
|
||||
|
||||
Then query `room_temperature_value` in [vmui](http://localhost:8428/vmui) to see it.
|
||||
Take into account that freshly ingested data can take up to 30 seconds to show up in query results,
|
||||
so don't worry if it doesn't appear immediately - just retry the query a bit later.
|
||||
|
||||
Once you're done experimenting, tidying everything up takes a single command.
|
||||
|
||||
### Cleanup
|
||||
|
||||
Stop VictoriaMetrics and node_exporter with `Ctrl+C` in their respective terminals. All the collected data lives in the `victoria-metrics-data` directory -
|
||||
delete it if you want to start from scratch. To remove all traces of this test drive,
|
||||
delete the whole `vm-quick-start` directory created at [step 1](https://docs.victoriametrics.com/victoriametrics/quick-start/#step-1-download-the-binary).
|
||||
|
||||
This test drive only scratches the surface of what VictoriaMetrics can do. Ready for a real setup?
|
||||
Continue with the installation options below, or learn the [key concepts](https://docs.victoriametrics.com/victoriametrics/keyconcepts/)
|
||||
of writing and querying data first.
|
||||
|
||||
## How to install
|
||||
|
||||
VictoriaMetrics is available in the following distributions:
|
||||
@@ -42,9 +214,6 @@ Just download VictoriaMetrics and follow [these instructions](https://docs.victo
|
||||
See [available integrations](https://docs.victoriametrics.com/victoriametrics/integrations/) with other systems like
|
||||
[Prometheus](https://docs.victoriametrics.com/victoriametrics/integrations/prometheus/) or [Grafana](https://docs.victoriametrics.com/victoriametrics/integrations/grafana/).
|
||||
|
||||
> Want to see VictoriaMetrics in action, but without installing anything?
|
||||
> Try [Playgrounds](https://docs.victoriametrics.com/playgrounds/) - a list of publicly available playgrounds for VictoriaMetrics software.
|
||||
|
||||
VictoriaMetrics is developed at a fast pace, so it is recommended to periodically check the [CHANGELOG](https://docs.victoriametrics.com/victoriametrics/changelog/)
|
||||
and perform [regular upgrades](https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/#how-to-upgrade-victoriametrics).
|
||||
|
||||
@@ -465,6 +634,15 @@ It is recommended to read [Replication and data safety](https://docs.victoriamet
|
||||
|
||||
For backup configuration, please refer to [vmbackup documentation](https://docs.victoriametrics.com/victoriametrics/vmbackup/).
|
||||
|
||||
### Graceful shutdown
|
||||
|
||||
To gracefully shut down a VictoriaMetrics process, send SIGTERM or SIGINT signal and wait until the process exits.
|
||||
See [how to send signals to processes](https://stackoverflow.com/questions/33239959/send-signal-to-process-from-command-line).
|
||||
|
||||
Graceful shutdown is required for data safety. A successful graceful shutdown guarantees that pending in-memory data and
|
||||
ongoing writes are flushed on disk before the process exits. During graceful shutdown, VictoriaMetrics stops accepting
|
||||
new HTTP connections and waits for in-flight requests to finish until `-http.maxGracefulShutdownDuration` expires.
|
||||
|
||||
### Configuring limits
|
||||
|
||||
To avoid excessive resource usage or performance degradation, limits must be in place:
|
||||
|
||||
@@ -26,42 +26,48 @@ See also [LTS releases](https://docs.victoriametrics.com/victoriametrics/lts-rel
|
||||
|
||||
## tip
|
||||
|
||||
* SECURITY: upgrade Go builder from Go1.26.4 to Go1.26.5. See [the list of issues addressed in Go1.26.5](https://github.com/golang/go/issues?q=milestone%3AGo1.26.5%20label%3ACherryPickApproved).
|
||||
|
||||
* FEATURE: [MetricsQL](https://docs.victoriametrics.com/victoriametrics/metricsql/): support `fill` modifiers to allow missing series on either side of a binary operation to be filled with a provided default value. See [#10598](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/10598).
|
||||
* FEATURE: [vmagent](https://docs.victoriametrics.com/victoriametrics/vmagent/) and [vmsingle](https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/): support scraping metrics over Unix domain sockets. The socket path can be configured via the `__unix_socket__` target label. See [#11156](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/11156). Thanks to @vinyas-bharadwaj for contribution.
|
||||
* FEATURE: [vmagent](https://docs.victoriametrics.com/victoriametrics/vmagent/) and [vmsingle](https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/): Improve background discovery performance for [http_sd](https://docs.victoriametrics.com/victoriametrics/sd_configs/#http_sd_configs) discovery. See [#8838](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/8838).
|
||||
* FEATURE: [vmagent](https://docs.victoriametrics.com/victoriametrics/vmagent/): restore broken persistent queue chunk file from the last valid written block in case of ungraceful vmagent shutdown. See [#11192](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/11192).
|
||||
* FEATURE: [vmagent](https://docs.victoriametrics.com/victoriametrics/vmagent/) and [vmsingle](https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/): allow overriding `max_scrape_size` on a per-target basis via the `__max_scrape_size__` label during target relabeling. See [#11188](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/11188).
|
||||
* FEATURE: [vmstorage](https://docs.victoriametrics.com/victoriametrics/cluster-victoriametrics/) and [vmsingle](https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/): add `-maxBackfillAge` command-line flag for limiting ingestion of samples with historical timestamps, for example, when older data has been moved between storage tiers (nvme/hdd, hot/cold). See [#11199](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/11199). Thanks to @AshwinRamaniPsg for contribution.
|
||||
* FEATURE: [vmagent](https://docs.victoriametrics.com/victoriametrics/vmagent/): automatically preload relabeling rules configured via `-remoteWrite.relabelConfig` and `-remoteWrite.urlRelabelConfig` in the [metrics relabel debug UI](https://docs.victoriametrics.com/victoriametrics/relabeling/#relabel-debugging). See [#9918](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/9918).
|
||||
|
||||
* BUGFIX: `vminsert` in [VictoriaMetrics cluster](https://docs.victoriametrics.com/victoriametrics/cluster-victoriametrics/): Now drops metadata blocks when communicating with vmstorage nodes over the legacy RPC protocol. To avoid this limitation, upgrade `vmstorage` to a version that supports the new RPC protocol (>= [v1.137.0](https://github.com/VictoriaMetrics/VictoriaMetrics/blob/master/docs/victoriametrics/changelog/CHANGELOG.md#v11370)). See [#11146](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/11146).
|
||||
* BUGFIX: [vmbackup](https://docs.victoriametrics.com/victoriametrics/vmbackup/) and [vmbackupmanager](https://docs.victoriametrics.com/victoriametrics/vmbackupmanager/): retry S3 requests failing with `HTTP 429` status code or `TooManyRequests` error code. Previously such requests were not retried, so a short burst of rate limiting would fail the whole backup. See [#11218](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/11218). Thanks to @gautamrizwani for contribution.
|
||||
* BUGFIX: `vmselect` in [VictoriaMetrics cluster](https://docs.victoriametrics.com/victoriametrics/cluster-victoriametrics/): properly apply limit to metrics metadata response. See [#11139](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/11139).
|
||||
* BUGFIX: [vmui](https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/#vmui): keep only one header navigation dropdown (`Explore`, `Tools`) open at a time. Previously, hovering across two dropdowns could briefly leave both open due to the close delay. See [#11224](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/11224). Thanks to @antedotee for contribution.
|
||||
* BUGFIX: [vmagent](https://docs.victoriametrics.com/victoriametrics/vmagent/): fix a possible data race when processing OpenTelemetry metadata. See [#11238](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/11238). Thanks to @nevgeny for contribution.
|
||||
* BUGFIX: [vmagent](https://docs.victoriametrics.com/victoriametrics/vmagent/): flush pending persistent queue data to chunk file before updating the metadata. This prevents the metadata writer offset from getting ahead of the chunk file size and avoids losing the persistent queue after an unclean shutdown. See [#11192](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/11192).
|
||||
* BUGFIX: [vmagent](https://docs.victoriametrics.com/victoriametrics/vmagent/): fix increased CPU and memory usage when `-remoteWrite.urlRelabelConfig` or `-remoteWrite.streamAggr.config` flags are used. The bug was introduced in [#10854](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/10854) and existed since [v1.147.0](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.147.0). See [#11250](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/11250).
|
||||
* BUGFIX: [vmui](https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/#vmui): preserve newline formatting in alert and rule annotations on the Alerting page. See [#11171](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/11171).
|
||||
* BUGFIX: [vmui](https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/#vmui): hide `Total metric names` stats on [Cardinality Explorer](https://docs.victoriametrics.com/victoriametrics/#cardinality-explorer) page when user selects a specific metric or label to focus. See [#11154](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/11154) for details. Thanks to @lghuy05 for the contribution.
|
||||
* BUGFIX: [vmui](https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/#vmui): keep only one header navigation dropdown (`Explore`, `Tools`) open at a time. Previously, hovering across two dropdowns could briefly leave both open due to the close delay. See [#11224](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/11224). Thanks to @antedotee for contribution.
|
||||
|
||||
## [v1.147.0](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.147.0)
|
||||
|
||||
Released at 2026-07-06
|
||||
|
||||
**Update Note 1:** [vmagent](https://docs.victoriametrics.com/victoriametrics/vmagent/): contains a bug that causes increased CPU and memory usage when `-remoteWrite.urlRelabelConfig` or `-remoteWrite.streamAggr.config` flags are used. The bug was introduced in [#10854](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/10854). Upgrade to v1.148.0 or rollback to v1.146.0. See [#11250](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/11250).
|
||||
|
||||
* SECURITY: upgrade base docker image (Alpine) from 3.23.4 to 3.24.1. See [Alpine 3.24.1 release notes](https://www.alpinelinux.org/posts/Alpine-3.24.1-released.html).
|
||||
|
||||
* FEATURE: [vmauth](https://docs.victoriametrics.com/victoriametrics/vmauth/): add `default_vm_access_claim` field into `jwt` section of auth config. It could be used at [JWT claim placeholders](https://docs.victoriametrics.com/victoriametrics/vmauth/#jwt-claim-based-request-templating), if `JWT` token doesn't have `vm_access` claim. See [#11054](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/11054).
|
||||
* FEATURE: [vmagent](https://docs.victoriametrics.com/victoriametrics/vmagent/): reduces CPU usage by 10% at [sharding among remote storages](https://docs.victoriametrics.com/victoriametrics/vmagent/#sharding-among-remote-storages). See [#11113](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/11113). Thanks to @bennf for contribution.
|
||||
* FEATURE: [vmagent](https://docs.victoriametrics.com/victoriametrics/vmagent/), `vminsert` in [VictoriaMetrics cluster](https://docs.victoriametrics.com/victoriametrics/cluster-victoriametrics/) and [vmsingle](https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/): introduce `64KiB` size limit for `metric metadata` fields - `Unit`, `Help` and `MetricFamilyName`. See [#11128](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/11128).
|
||||
* FEATURE: [vmagent](https://docs.victoriametrics.com/victoriametrics/vmagent/) and [vmsingle](https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/): reduce CPU usage for storing scrape target labels. See [#10919](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/10919).
|
||||
* FEATURE: [vmagent](https://docs.victoriametrics.com/victoriametrics/vmagent/): add support for [Monitoring Data eXchange (MDX)](https://docs.victoriametrics.com/victoriametrics/vmagent/#monitoring-data-exchange): the ability to route only metrics from VictoriaMetrics services to a specific `-remoteWrite.url`. MDX is useful for building monitoring-of-monitoring where one remote storage should receive the full metric stream and another should receive only VictoriaMetrics metrics. Enable per destination with `-remoteWrite.mdx.enable=true`. See [#10600](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/10600).
|
||||
* FEATURE: [vmsingle](https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/) and `vmstorage` in [VictoriaMetrics cluster](https://docs.victoriametrics.com/victoriametrics/cluster-victoriametrics/): expose `vm_data_size_bytes{type="storage/metaindex"}` and `vm_data_size_bytes{type="indexdb/metaindex"}` metrics for tracking memory occupied by metaindex data. See [#11204](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/11204). Thanks to @SamarthBagga for contribution.
|
||||
* FEATURE: [vmsingle](https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/) and `vmselect` in [VictoriaMetrics cluster](https://docs.victoriametrics.com/victoriametrics/cluster-victoriametrics/): add `optimize_repeated_binary_op_subexprs=1` query arg to [/api/v1/query_range](https://docs.victoriametrics.com/victoriametrics/keyconcepts/#range-query) for executing binary operator sides sequentially when they share the same optimized aggregate rollup result expression. This allows the second side to reuse rollup result cache populated by the first side. See [#10575](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/10575). Thanks to @xhebox for the contribution.
|
||||
* FEATURE: [vmauth](https://docs.victoriametrics.com/victoriametrics/vmauth/): prevent possible password brute-force attacks with an artificial 2-3 second delay as recommended by [OWASP](https://owasp.org/Top10/2025/A07_2025-Authentication_Failures). See [#11180](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/11180).
|
||||
* FEATURE: [vmsingle](https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/): Add the support of vmselect RPC to vmsingle so that single node can be queried by a vmselect from a vmcluster deployment. See [#4328](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/4328), [#10926](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/10926), and the [documentation](https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/#multi-tenancy).
|
||||
* FEATURE: [alerts](https://github.com/VictoriaMetrics/VictoriaMetrics/blob/master/deployment/docker/rules): add `InvalidAuthTokenRequestErrors` alerting rule to [vmauth alerts](https://github.com/VictoriaMetrics/VictoriaMetrics/blob/master/deployment/docker/rules/alerts-vmauth.yml). The new rule notifies when vmauth receives requests with invalid or missing auth tokens, which may indicate a client misconfiguration, expired token use, or brute-force attack. See [#11180](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/11180).
|
||||
* FEATURE: [vmsingle](https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/): Add the support of vmselect RPC to vmsingle so that single node can be queried by a vmselect from a vmcluster deployment. See [4328](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/4328), [10926](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/10926), and the [documentation](https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/#multi-tenancy).
|
||||
* FEATURE: [alerts](https://github.com/VictoriaMetrics/VictoriaMetrics/blob/master/deployment/docker/rules): add `AlertingRuleResultsApproachingLimit` and `RecordingRuleResultsApproachingLimit` alerting rules to [vmalert alerts](https://github.com/VictoriaMetrics/VictoriaMetrics/blob/master/deployment/docker/rules/alerts-vmalert.yml). These alerts notify when a rule's last evaluation samples exceed 90% of the configured group results limit. See [#11179](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/11179). Thanks to @vinyas-bharadwaj for the contribution.
|
||||
* FEATURE: [vmauth](https://docs.victoriametrics.com/victoriametrics/vmauth/): prevent possible password brute-force attacks with an artificial 2-3 second delay as recommended by [OWASP](https://owasp.org/Top10/2025/A07_2025-Authentication_Failures). See [#11180](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/11180).
|
||||
* FEATURE: [vmauth](https://docs.victoriametrics.com/victoriametrics/vmauth/): allow log requests with missing or invalid auth tokens to [access log](https://docs.victoriametrics.com/victoriametrics/vmauth/#access-log). This is useful for identifying `remote_addr` IPs performing brute-force attacks. See [#11180](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/11180).
|
||||
* FEATURE: [vmauth](https://docs.victoriametrics.com/victoriametrics/vmauth/): fall through to `unauthorized_user` when a [JWT token](https://docs.victoriametrics.com/victoriametrics/vmauth/#jwt-token-auth-proxy) has no `vm_access` claim and no `default_vm_access_claim` is configured. Previously, vmauth returned `401 Unauthorized` immediately in this case, which prevented `unauthorized_user` from handling such requests. See [#5740](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/5740).
|
||||
* FEATURE: [MetricsQL](https://docs.victoriametrics.com/victoriametrics/metricsql/): improve the selection algorithm of [buckets_limit](https://docs.victoriametrics.com/victoriametrics/metricsql/#buckets_limit) to remove consecutive empty buckets at the beginning and end to obtain more accurate min and max values. See [#10417](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/10417).
|
||||
* FEATURE: [vmalert](https://docs.victoriametrics.com/victoriametrics/vmalert/): expose `vmalert_group_rule_results_limit` metric to indicate the number of alerts or recording results that a single rule within the group can produce. See [#11179](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/11179). Thanks to @vinyas-bharadwaj for the contribution.
|
||||
* FEATURE: [alerts](https://github.com/VictoriaMetrics/VictoriaMetrics/blob/master/deployment/docker/rules): add `AlertingRuleResultsApproachingLimit` and `RecordingRuleResultsApproachingLimit` alerting rules to [vmalert alerts](https://github.com/VictoriaMetrics/VictoriaMetrics/blob/master/deployment/docker/rules/alerts-vmalert.yml). These alerts notify when a rule's last evaluation samples exceed 90% of the configured group results limit. See [#11179](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/11179). Thanks to @vinyas-bharadwaj for the contribution.
|
||||
* FEATURE: [vmagent](https://docs.victoriametrics.com/victoriametrics/vmagent/): add support for [Monitoring Data eXchange (MDX)](https://docs.victoriametrics.com/victoriametrics/vmagent/#monitoring-data-exchange): the ability to route only metrics from VictoriaMetrics services to a specific `-remoteWrite.url`. MDX is useful for building monitoring-of-monitoring where one remote storage should receive the full metric stream and another should receive only VictoriaMetrics metrics. Enable per destination with `-remoteWrite.mdx.enable=true`. See [#10600](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/10600).
|
||||
|
||||
* BUGFIX: all VictoriaMetrics components: cancel in-flight HTTP requests shortly before `-http.maxGracefulShutdownDuration` elapses during graceful shutdown, so they can drain and the shutdown completes cleanly within that window instead of timing out and exiting via `logger.Fatalf` -> `os.Exit`. This prevents skipping the storage flush and losing in-memory data when long-lived requests are in flight (such as VictoriaLogs live tailing). See [#1502](https://github.com/VictoriaMetrics/VictoriaLogs/issues/1502).
|
||||
* BUGFIX: `vminsert` in [VictoriaMetrics cluster](https://docs.victoriametrics.com/victoriametrics/cluster-victoriametrics/) and [vmsingle](https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/): properly check values range for the limits configured with flags `-maxLabelsPerTimeseries`, `-maxLabelNameLen` and `-maxLabelValueLen`. It must be in range `1..65535`. See [#11128](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/11128).
|
||||
@@ -93,8 +99,8 @@ Released at 2026-06-22
|
||||
* BUGFIX: [vmrestore](https://docs.victoriametrics.com/victoriametrics/vmrestore/): disallow restoring parts outside the configured `-storageDataPath` directory. See [710c920d](https://github.com/VictoriaMetrics/VictoriaMetrics/commit/710c920d6083327042a309e449fae4383617d817).
|
||||
* BUGFIX: `vmselect` in [VictoriaMetrics cluster](https://docs.victoriametrics.com/victoriametrics/cluster-victoriametrics/): correctly apply long tenant filters. Previously, such filters could be truncated, causing tenants to be matched incorrectly. See [#11096](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/11096). Thanks for @fxrlv for the contribution.
|
||||
* BUGFIX: `vmselect` in [VictoriaMetrics cluster](https://docs.victoriametrics.com/victoriametrics/cluster-victoriametrics/): fix corrupted metrics metadata when a response contains multiple rows. See [#11115](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/11115). Thanks for @fxrlv for the contribution.
|
||||
* BUGFIX: [vmbackup](https://docs.victoriametrics.com/vmbackup/), [vmbackupmanager](https://docs.victoriametrics.com/victoriametrics/vmbackupmanager/): do not fail backup list if directory is absent while using `fs://` destination to align with other protocols. See [6c3c548](https://github.com/VictoriaMetrics/VictoriaMetrics/commit/6c3c548ddb0385b749e731f52276f130e2a4e4a8)
|
||||
* BUGFIX: `vmselect` in [VictoriaMetrics cluster](https://docs.victoriametrics.com/victoriametrics/cluster-victoriametrics/): don't cache empty responses for tenant IDs discovery during [multitenant queries](https://docs.victoriametrics.com/Cluster-VictoriaMetrics.html#multitenant-reads). This problem was visible during integration tests when multitenant queries were executed before the first ingestion happened. See [#10982](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/10982)
|
||||
* BUGFIX: [vmbackup](https://docs.victoriametrics.com/vmbackup/), [vmbackupmanager](https://docs.victoriametrics.com/victoriametrics/vmbackupmanager/): do not fail backup list if directory is absent while using `fs://` destination to align with other protocols. See [6c3c548](https://github.com/VictoriaMetrics/VictoriaMetrics/commit/6c3c548ddb0385b749e731f52276f130e2a4e4a8)
|
||||
* BUGFIX: [vmsingle](https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/) and `vmselect` in [VictoriaMetrics cluster](https://docs.victoriametrics.com/victoriametrics/cluster-victoriametrics/): properly escape `metricFamilyName` at metrics metadata response. See [#11129](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/11129). Thanks for @fxrlv for the contribution.
|
||||
* BUGFIX: [vmsingle](https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/) and `vmstorage` in [VictoriaMetrics cluster](https://docs.victoriametrics.com/victoriametrics/cluster-victoriametrics/): prevent more cases of panic during directory deletion on `NFS`-based mounts. See [#11060](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/11060).
|
||||
|
||||
@@ -119,9 +125,9 @@ Released at 2026-06-08
|
||||
* BUGFIX: [vmauth](https://docs.victoriametrics.com/victoriametrics/vmauth/): properly calculate number of loaded users to be printed in startup log. Previously, it was only accounting for static users and skipped JWT configuration entries. See [#11050](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/11050/).
|
||||
* BUGFIX: [MetricsQL](https://docs.victoriametrics.com/victoriametrics/metricsql/): `integrate()` no longer extrapolates the last sample's value past the end of the time series. Previously, querying `integrate(metric[1h])` at a timestamp where the series had already ended would keep accruing area as if the last value continued indefinitely, producing values much larger than the true integral. See [#9474](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/9474). Thanks to @wtfashwin for contribution.
|
||||
* BUGFIX: `vmselect` in [VictoriaMetrics cluster](https://docs.victoriametrics.com/victoriametrics/cluster-victoriametrics/): avoid returning HTTP 503 for queries with partial results when a storage group is unavailable and `-search.denyPartialResponse` is disabled. See [#11009](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/11009). Thanks to @fxrlv for the contribution.
|
||||
* BUGFIX: `vmselect` in [VictoriaMetrics cluster](https://docs.victoriametrics.com/victoriametrics/cluster-victoriametrics/): fix intermittent `write: connection timed out` errors caused by silently dropped TCP connections being reused from the connection pool. See [#10735](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/10735#issuecomment-4535832301).
|
||||
* BUGFIX: [vmsingle](https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/) and `vmselect` in [VictoriaMetrics cluster](https://docs.victoriametrics.com/victoriametrics/cluster-victoriametrics/): properly escape `utf-8` label names for [/federate](https://docs.victoriametrics.com/victoriametrics/#federation) API requests. See [#10968](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/10968).
|
||||
* BUGFIX: [vmui](https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/#vmui): persist the `Disable deduplication` toggle under its own local storage key. Before this fix, the toggle state was lost after reload and could overwrite the `Compact view` table setting. See [#11004](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/11004). Thanks to @immanuwell for the contribution.
|
||||
* BUGFIX: `vmselect` in [VictoriaMetrics cluster](https://docs.victoriametrics.com/victoriametrics/cluster-victoriametrics/): fix intermittent `write: connection timed out` errors caused by silently dropped TCP connections being reused from the connection pool. See [#10735](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/10735#issuecomment-4535832301).
|
||||
|
||||
## [v1.144.0](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.144.0)
|
||||
|
||||
@@ -129,28 +135,28 @@ Released at 2026-05-22
|
||||
|
||||
* FEATURE: all VictoriaMetrics components: improve logging for the `-memory.allowedBytes` flag to warn about excessively low value (less than 1MB). See issue [#10935](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/10935).
|
||||
* FEATURE: [vmagent](https://docs.victoriametrics.com/victoriametrics/vmagent/) and [vmalert](https://docs.victoriametrics.com/victoriametrics/vmalert/): add `basicAuth.usernameFile` command-line flags for reading basic auth username from a file, similar to the existing `basicAuth.passwordFile`. The file is re-read every second. See [#9436](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/9436). Thanks to @kimjune01 for the contribution.
|
||||
* FEATURE: [vmagent](https://docs.victoriametrics.com/victoriametrics/vmagent/): drain in-memory remote write queue on shutdown within the 5-second grace period before falling back to persisting blocks to disk. See [#9996](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/9996)
|
||||
* FEATURE: [vmsingle](https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/), `vminsert` in [VictoriaMetrics cluster](https://docs.victoriametrics.com/victoriametrics/cluster-victoriametrics/) and [vmagent](https://docs.victoriametrics.com/victoriametrics/vmagent/): add `-opentelemetry.labelNameUnderscoreSanitization` command-line flag to control whether to enable prepending of `key` to labels starting with `_` when `-opentelemetry.usePrometheusNaming` is enabled. See [OpenTelemetry](https://docs.victoriametrics.com/victoriametrics/integrations/opentelemetry/) docs and [#9663](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/9663). Thanks to @andriibeee for the contribution.
|
||||
* FEATURE: [vmui](https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/#vmui): improve the [Top Queries](https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/#top-queries) table UI. Duration columns now display human-readable values (e.g. `1.23s`) instead of raw seconds, memory column shows human-readable sizes (e.g. `1.23 MB`), instant queries are labeled as `instant` instead of empty string, and column headers now show tooltips with descriptions. See [#10790](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/10790).
|
||||
* FEATURE: [vmagent](https://docs.victoriametrics.com/victoriametrics/vmagent/): drain in-memory remote write queue on shutdown within the 5-second grace period before falling back to persisting blocks to disk. See [#9996](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/9996)
|
||||
* FEATURE: [vmui](https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/#vmui): display `null` values on `Raw Query` chart. `null` values can be actual `NaN` or `null` values exposed by the exporter, or [stale markers](https://docs.victoriametrics.com/victoriametrics/vmagent/#prometheus-staleness-markers). Before, vmui Raw Query was silently dropping non-numeric values. Displaying such values on the chart could improve the debugging experience. See [#10986](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/10986).
|
||||
* FEATURE: `vminsert` in [VictoriaMetrics cluster](https://docs.victoriametrics.com/victoriametrics/cluster-victoriametrics/): Improve [slowness-based rerouting](https://docs.victoriametrics.com/victoriametrics/cluster-victoriametrics/#slowness-based-re-routing) to prevent rerouting storms under high cluster load. Previously, rerouting could cascade across storage nodes when the whole cluster was saturated, making the situation worse. Now rerouting only activates when the cluster p90 saturation is below 60%, and the slowest node is more than 20% slower than p90. See [#10876](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/10876).
|
||||
* FEATURE: [vmauth](https://docs.victoriametrics.com/victoriametrics/vmauth/): add `{{.MetricsAccountID}}` and `{{.MetricsProjectID}}` [JWT claim placeholders](https://docs.victoriametrics.com/victoriametrics/vmauth/#jwt-claim-based-request-templating) for use in `headers` and `url_prefix` config fields. Previously, only the combined `{{.MetricsTenant}}` (`accountID:projectID`) JWT placeholder was supported, making it impossible to configure [multitenancy via headers](https://docs.victoriametrics.com/victoriametrics/cluster-victoriametrics/#multitenancy-via-headers). See [#10927](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/10927). Thanks to @Vinayak9769 for the contribution.
|
||||
* FEATURE: [vmui](https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/#vmui): display `null` values on `Raw Query` chart. `null` values can be actual `NaN` or `null` values exposed by the exporter, or [stale markers](https://docs.victoriametrics.com/victoriametrics/vmagent/#prometheus-staleness-markers). Before, vmui Raw Query was silently dropping non-numeric values. Displaying such values on the chart could improve the debugging experience. See [#10986](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/10986).
|
||||
|
||||
* BUGFIX: [stream aggregation](https://docs.victoriametrics.com/victoriametrics/stream-aggregation/): stop emitting stale values for `quantiles(...)` outputs when a time series has no samples during the current aggregation interval. See [#10918](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/10918). Thanks to @alexei38 for the contribution.
|
||||
* BUGFIX: [stream aggregation](https://docs.victoriametrics.com/victoriametrics/stream-aggregation/): extend delay on aggregation windows flush by the biggest lag among pushed samples. Before, the delay was calculated as 95th percentile across samples, which could underrepresent outliers and reject them from aggregation as "too old". See [#10402](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/10402).
|
||||
* BUGFIX: [vmagent](https://docs.victoriametrics.com/victoriametrics/vmagent/): fix a bug in [cardinality limiters](https://docs.victoriametrics.com/victoriametrics/vmagent/#cardinality-limiter) where series with different labels, like `{a="bc"}` and `{ab="c"}`, could be incorrectly treated as identical and dropped. See [#10937](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/10937).
|
||||
* BUGFIX: [vmagent](https://docs.victoriametrics.com/victoriametrics/vmagent/): hide values passed to `-remoteWrite.headers` in startup logs, `/metrics`, and `/flags`, since they can contain sensitive HTTP headers such as `Authorization` and API keys.
|
||||
* BUGFIX: [vmagent](https://docs.victoriametrics.com/victoriametrics/vmagent/): hide values passed to `-remoteWrite.proxyURL` in startup logs, `/metrics`, and `/flags`, since they can contain sensitive credentials.
|
||||
* BUGFIX: [vmagent](https://docs.victoriametrics.com/victoriametrics/vmagent/): return error on startup if `-remoteWrite.disableOnDiskQueue` is not configured uniformly across all `-remoteWrite.url` targets when `-remoteWrite.shardByURL` is enabled. Either all targets must have it enabled or all must have it disabled. See [#10507](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/10507).
|
||||
* BUGFIX: [vmagent](https://docs.victoriametrics.com/victoriametrics/vmagent/): prevent unintentional rerouting of samples to other sharding targets when one of the `-remoteWrite.url` targets with `-remoteWrite.disableOnDiskQueue` becomes blocked. Previously this could break the sharding guarantee by sending samples to wrong targets instead of dropping or retrying them. See [#10507](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/10507).
|
||||
* BUGFIX: [vmalert](https://docs.victoriametrics.com/victoriametrics/vmalert/): hide values passed to `-remoteWrite.headers`,`remoteRead.headers`, `datasource.headers` and `notifier.headers` in startup logs, `/metrics`, and `/flags`, since they can contain sensitive HTTP headers such as `Authorization` and API keys.
|
||||
* BUGFIX: `vminsert` in [VictoriaMetrics cluster](https://docs.victoriametrics.com/victoriametrics/cluster-victoriametrics/): properly establish [mtls](https://docs.victoriametrics.com/victoriametrics/cluster-victoriametrics/#mtls-protection) connection between vmstorage and vminsert. Regression was introduced in v1.130.0 release for the enterprise version of vmstorage. See [#10972](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/10972).
|
||||
* BUGFIX: [vmrestore](https://docs.victoriametrics.com/victoriametrics/vmrestore/): fix a bug where specifying `-storageDataPath` with a trailing slash could cause `vmrestore` to panic. See [#10823](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/10823). Thanks to @utafrali for the contribution.
|
||||
* BUGFIX: [vmagent](https://docs.victoriametrics.com/victoriametrics/vmagent/): prevent unintentional rerouting of samples to other sharding targets when one of the `-remoteWrite.url` targets with `-remoteWrite.disableOnDiskQueue` becomes blocked. Previously this could break the sharding guarantee by sending samples to wrong targets instead of dropping or retrying them. See [#10507](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/10507).
|
||||
* BUGFIX: [vmagent](https://docs.victoriametrics.com/victoriametrics/vmagent/): return error on startup if `-remoteWrite.disableOnDiskQueue` is not configured uniformly across all `-remoteWrite.url` targets when `-remoteWrite.shardByURL` is enabled. Either all targets must have it enabled or all must have it disabled. See [#10507](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/10507).
|
||||
* BUGFIX: [vmsingle](https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/) and `vmselect` in [VictoriaMetrics cluster](https://docs.victoriametrics.com/victoriametrics/cluster-victoriametrics/): hide values passed to `vmalert.proxyURL` in startup logs, `/metrics`, and `/flags`, since they can contain sensitive HTTP headers such as `Authorization` and API keys.
|
||||
* BUGFIX: [vmui](https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/#vmui): preserve exact series values in graph tooltips instead of rounding them by significant digits. See [#10952](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/10952).
|
||||
* BUGFIX: all VictoriaMetrics components: fix int64 overflow when parsing [timestamp parameters](https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/#timestamp-formats) with relative durations. See [#10880](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/10880).
|
||||
* BUGFIX: [vmsingle](https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/) and `vmstorage` in [VictoriaMetrics cluster](https://docs.victoriametrics.com/victoriametrics/cluster-victoriametrics/): `-denyQueriesOutsideRetention` now also rejects queries whose end time is beyond `-futureRetention`. See [#10879](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/10879).
|
||||
* BUGFIX: [vmui](https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/#vmui): preserve exact series values in graph tooltips instead of rounding them by significant digits. See [#10952](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/10952).
|
||||
* BUGFIX: [vmui](https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/#vmui): add missing `__timestamp__` and `__value__` columns to CSV exported from the table view on the Query tab. See [#10975](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/10975).
|
||||
* BUGFIX: all VictoriaMetrics components: fix int64 overflow when parsing [timestamp parameters](https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/#timestamp-formats) with relative durations. See [#10880](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/10880).
|
||||
|
||||
## [v1.143.0](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.143.0)
|
||||
|
||||
@@ -160,10 +166,10 @@ Released at 2026-05-08
|
||||
|
||||
* FEATURE: all VictoriaMetrics components: suppress TCP health check errors when `-tls` flag is set. See [#10538](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/10538).
|
||||
* FEATURE: [vmagent](https://docs.victoriametrics.com/victoriametrics/vmagent/) and [vmsingle](https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/): add `__meta_hetzner_robot_datacenter` label for `robot` role in [hetzner_sd_configs](https://docs.victoriametrics.com/victoriametrics/sd_configs/#hetzner_sd_configs). See [#10909](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/10909). Thanks to @juliusrickert for contribution.
|
||||
* FEATURE: [vmagent](https://docs.victoriametrics.com/victoriametrics/vmagent/), [vmsingle](https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/), `vmselect` in [VictoriaMetrics cluster](https://docs.victoriametrics.com/victoriametrics/cluster-victoriametrics/) and `vmstorage` in [VictoriaMetrics cluster](https://docs.victoriametrics.com/victoriametrics/cluster-victoriametrics/): introduce the `vm_fs_info` metric. It exposes the filesystem type (e.g., ext4, xfs, nfs) used for `-*Path` related flags. See [#10482](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/10482).
|
||||
* FEATURE: [vmagent](https://docs.victoriametrics.com/victoriametrics/vmagent/), [vmsingle](https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/), `vminsert` in [VictoriaMetrics cluster](https://docs.victoriametrics.com/victoriametrics/cluster-victoriametrics/): add support for [Prometheus native histogram](https://prometheus.io/docs/specs/native_histograms/) during ingestion. See [#10743](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/10743).
|
||||
* FEATURE: [vmalert](https://docs.victoriametrics.com/victoriametrics/vmalert/): add `-rule.stripFilePath` to support stripping rule file paths in logs and all API responses, including /metrics. See [#5625](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/5625).
|
||||
* FEATURE: [vmalert](https://docs.victoriametrics.com/victoriametrics/vmalert/): add `formatTime` template function for formatting a Unix timestamp using the provided layout. For example, `{{ now | formatTime "2006-01-02T15:04:05Z07:00" }}` returns the current time in RFC3339 format. See issue [#10624](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/10624). Thanks to @andriibeee for the contribution.
|
||||
* FEATURE: [vmagent](https://docs.victoriametrics.com/victoriametrics/vmagent/), [vmsingle](https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/), `vminsert` in [VictoriaMetrics cluster](https://docs.victoriametrics.com/victoriametrics/cluster-victoriametrics/): add support for [Prometheus native histogram](https://prometheus.io/docs/specs/native_histograms/) during ingestion. See [#10743](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/10743).
|
||||
* FEATURE: [vmagent](https://docs.victoriametrics.com/victoriametrics/vmagent/), [vmsingle](https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/), `vmselect` in [VictoriaMetrics cluster](https://docs.victoriametrics.com/victoriametrics/cluster-victoriametrics/) and `vmstorage` in [VictoriaMetrics cluster](https://docs.victoriametrics.com/victoriametrics/cluster-victoriametrics/): introduce the `vm_fs_info` metric. It exposes the filesystem type (e.g., ext4, xfs, nfs) used for `-*Path` related flags. See [#10482](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/10482).
|
||||
* FEATURE: [dashboards/vmagent](https://grafana.com/grafana/dashboards/12683): add `Kafka (Enterprise)` row with panels for monitoring traffic (bytes), messages in/out, producer and consumer errors. See [#10728](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/10728).
|
||||
* FEATURE: [vmalert](https://docs.victoriametrics.com/victoriametrics/vmalert/): support sending data to the configured `-remoteWrite.url` via [VictoriaMetrics remote write protocol](https://docs.victoriametrics.com/victoriametrics/vmagent/#victoriametrics-remote-write-protocol). See [#10929](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/10929).
|
||||
|
||||
|
||||
BIN
docs/victoriametrics/quick-start-vmui.webp
Normal file
BIN
docs/victoriametrics/quick-start-vmui.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
@@ -513,6 +513,7 @@ URLs for scrape targets are composed of the following parts:
|
||||
[scrape_config](https://docs.victoriametrics.com/victoriametrics/sd_configs/#scrape_configs)
|
||||
or by updating/setting the corresponding `__param_*` labels during
|
||||
relabeling.
|
||||
- Unix domain socket path (e.g. `/var/run/node_exporter.sock`) can be configured during target relabeling via a special label - `__unix_socket__`. If this label is set, `vmagent` tunnels the scrape request through the specified Unix domain socket instead of connecting to a TCP socket. The `__address__` label is still used to populate the `instance` label and construct the HTTP request URL, but the actual network connection goes through the Unix socket.
|
||||
|
||||
The resulting scrape URL looks like the following:
|
||||
|
||||
|
||||
2
go.mod
2
go.mod
@@ -1,6 +1,6 @@
|
||||
module github.com/VictoriaMetrics/VictoriaMetrics
|
||||
|
||||
go 1.26.4
|
||||
go 1.26.5
|
||||
|
||||
require (
|
||||
cloud.google.com/go/storage v1.62.3
|
||||
|
||||
@@ -25,8 +25,6 @@ const MaxBlockSize = 32 * 1024 * 1024
|
||||
// DefaultChunkFileSize represents default chunk file size
|
||||
const DefaultChunkFileSize = (MaxBlockSize + 8) * 16
|
||||
|
||||
const blockHeaderSize = 8
|
||||
|
||||
var chunkFileNameRegex = regexp.MustCompile("^[0-9A-F]{16}$")
|
||||
|
||||
// queue represents persistent queue.
|
||||
@@ -131,7 +129,7 @@ func mustOpen(path, name string, maxPendingBytes int64) *queue {
|
||||
}
|
||||
|
||||
func mustOpenInternal(path, name string, chunkFileSize, maxBlockSize, maxPendingBytes uint64) *queue {
|
||||
if chunkFileSize < blockHeaderSize || chunkFileSize-blockHeaderSize < maxBlockSize {
|
||||
if chunkFileSize < 8 || chunkFileSize-8 < maxBlockSize {
|
||||
logger.Panicf("BUG: too small chunkFileSize=%d for maxBlockSize=%d; chunkFileSize must fit at least one block", chunkFileSize, maxBlockSize)
|
||||
}
|
||||
if maxBlockSize <= 0 {
|
||||
@@ -282,30 +280,14 @@ func tryOpeningQueue(path, name string, chunkFileSize, maxBlockSize, maxPendingB
|
||||
q.writerFlushedOffset = mi.WriterOffset
|
||||
if fileSize := fs.MustFileSize(q.writerPath); fileSize != q.writerLocalOffset {
|
||||
if fileSize < q.writerLocalOffset {
|
||||
validChunkFileSize := mustGetChunkValidDataSize(q.writerPath, q.maxBlockSize)
|
||||
logger.Warnf("%q size (%d bytes) is smaller than the writer offset (%d bytes); "+
|
||||
"this may be the case on unclean shutdown (OOM, `kill -9`, hardware reset); resetting writer to fileSize: %d",
|
||||
q.writerPath, fileSize, q.writerLocalOffset, validChunkFileSize)
|
||||
mi.WriterOffset = offset + validChunkFileSize
|
||||
q.writerOffset = mi.WriterOffset
|
||||
q.writerLocalOffset = mi.WriterOffset % q.chunkFileSize
|
||||
q.writerFlushedOffset = mi.WriterOffset
|
||||
// The recovered writer offset may end up smaller than mi.ReaderOffset.
|
||||
// Do not clamp the reader offset in this case: it means the reader has already
|
||||
// consumed data that is now lost, so the remaining queue state cannot be trusted.
|
||||
// The readerOffset > writerOffset check below will fail queue opening,
|
||||
// and the caller (mustOpen) will drop the queue and recreate it from scratch.
|
||||
} else {
|
||||
logger.Warnf("%q size (%d bytes) is bigger than writer offset (%d bytes); "+
|
||||
"this may be the case on unclean shutdown (OOM, `kill -9`, hardware reset); trying to fix it by adjusting fileSize to %d",
|
||||
q.writerPath, fileSize, q.writerLocalOffset, q.writerLocalOffset)
|
||||
}
|
||||
if err := os.Truncate(q.writerPath, int64(q.writerLocalOffset)); err != nil {
|
||||
logger.Panicf("FATAL: cannot truncate chunk: %q to size: %d: %s", q.writerPath, q.writerLocalOffset, err)
|
||||
}
|
||||
if err := mi.WriteToFile(metainfoPath); err != nil {
|
||||
logger.Panicf("FATAL: cannot update metainfo file: %q: %s", metainfoPath, err)
|
||||
logger.Errorf("%q size (%d bytes) is smaller than the writer offset (%d bytes); removing the file",
|
||||
q.writerPath, fileSize, q.writerLocalOffset)
|
||||
fs.MustRemovePath(q.writerPath)
|
||||
continue
|
||||
}
|
||||
logger.Warnf("%q size (%d bytes) is bigger than writer offset (%d bytes); "+
|
||||
"this may be the case on unclean shutdown (OOM, `kill -9`, hardware reset); trying to fix it by adjusting fileSize to %d",
|
||||
q.writerPath, fileSize, q.writerLocalOffset, q.writerLocalOffset)
|
||||
}
|
||||
w, err := filestream.OpenWriterAt(q.writerPath, int64(q.writerLocalOffset), false)
|
||||
if err != nil {
|
||||
@@ -375,7 +357,7 @@ func (q *queue) MustWriteBlock(block []byte) {
|
||||
}
|
||||
if q.maxPendingBytes > 0 {
|
||||
// Drain the oldest blocks until the number of pending bytes becomes enough for the block.
|
||||
blockSize := uint64(len(block) + blockHeaderSize)
|
||||
blockSize := uint64(len(block) + 8)
|
||||
maxPendingBytes := q.maxPendingBytes
|
||||
if blockSize < maxPendingBytes {
|
||||
maxPendingBytes -= blockSize
|
||||
@@ -413,7 +395,7 @@ func (q *queue) writeBlock(block []byte) error {
|
||||
defer func() {
|
||||
writeDurationSeconds.Add(time.Since(startTime).Seconds())
|
||||
}()
|
||||
if q.writerLocalOffset+q.maxBlockSize+blockHeaderSize > q.chunkFileSize {
|
||||
if q.writerLocalOffset+q.maxBlockSize+8 > q.chunkFileSize {
|
||||
if err := q.nextChunkFileForWrite(); err != nil {
|
||||
return fmt.Errorf("cannot create next chunk file: %w", err)
|
||||
}
|
||||
@@ -426,7 +408,7 @@ func (q *queue) writeBlock(block []byte) error {
|
||||
err := q.write(header.B)
|
||||
headerBufPool.Put(header)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot write header with size %d bytes to %q: %w", blockHeaderSize, q.writerPath, err)
|
||||
return fmt.Errorf("cannot write header with size 8 bytes to %q: %w", q.writerPath, err)
|
||||
}
|
||||
|
||||
// Write block contents.
|
||||
@@ -487,7 +469,7 @@ func (q *queue) readBlock(dst []byte) ([]byte, error) {
|
||||
defer func() {
|
||||
readDurationSeconds.Add(time.Since(startTime).Seconds())
|
||||
}()
|
||||
if q.readerLocalOffset+q.maxBlockSize+blockHeaderSize > q.chunkFileSize {
|
||||
if q.readerLocalOffset+q.maxBlockSize+8 > q.chunkFileSize {
|
||||
if err := q.nextChunkFileForRead(); err != nil {
|
||||
return dst, fmt.Errorf("cannot open next chunk file: %w", err)
|
||||
}
|
||||
@@ -496,12 +478,12 @@ func (q *queue) readBlock(dst []byte) ([]byte, error) {
|
||||
again:
|
||||
// Read block len.
|
||||
header := headerBufPool.Get()
|
||||
header.B = bytesutil.ResizeNoCopyMayOverallocate(header.B, blockHeaderSize)
|
||||
header.B = bytesutil.ResizeNoCopyMayOverallocate(header.B, 8)
|
||||
err := q.readFull(header.B)
|
||||
blockLen := encoding.UnmarshalUint64(header.B)
|
||||
headerBufPool.Put(header)
|
||||
if err != nil {
|
||||
logger.Errorf("skipping corrupted %q, since header with size %d bytes cannot be read from it: %s", q.readerPath, blockHeaderSize, err)
|
||||
logger.Errorf("skipping corrupted %q, since header with size 8 bytes cannot be read from it: %s", q.readerPath, err)
|
||||
if err := q.skipBrokenChunkFile(); err != nil {
|
||||
return dst, err
|
||||
}
|
||||
@@ -687,39 +669,3 @@ func (mi *metainfo) ReadFromFile(path string) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func mustGetChunkValidDataSize(filePath string, maxBlockSize uint64) uint64 {
|
||||
f, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
logger.Panicf("FATAL: cannot open file: %s", err)
|
||||
}
|
||||
defer fs.MustClose(f)
|
||||
|
||||
fi, err := f.Stat()
|
||||
if err != nil {
|
||||
logger.Panicf("FATAL: cannot read file stat: %s", err)
|
||||
}
|
||||
fileSize := uint64(fi.Size())
|
||||
|
||||
var offset uint64
|
||||
var header [blockHeaderSize]byte
|
||||
for {
|
||||
if offset+blockHeaderSize > fileSize {
|
||||
return offset
|
||||
}
|
||||
if _, err := io.ReadFull(f, header[:]); err != nil {
|
||||
return offset
|
||||
}
|
||||
blockLen := encoding.UnmarshalUint64(header[:])
|
||||
if blockLen == 0 || blockLen > maxBlockSize {
|
||||
return offset
|
||||
}
|
||||
if offset+blockHeaderSize+blockLen > fileSize {
|
||||
return offset
|
||||
}
|
||||
offset += blockHeaderSize + blockLen
|
||||
if _, err := f.Seek(int64(offset), io.SeekStart); err != nil {
|
||||
return offset
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -137,243 +137,6 @@ func TestQueueOpen(t *testing.T) {
|
||||
q.MustClose()
|
||||
fs.MustRemoveDir(path)
|
||||
})
|
||||
t.Run("damaged-writer-file-tail", func(t *testing.T) {
|
||||
path := "damaged-writer-file-tail"
|
||||
fs.MustRemoveDir(path)
|
||||
|
||||
q := mustOpen(path, "foobar", 0)
|
||||
block1 := []byte("valid block 1")
|
||||
block2 := []byte("valid block 2")
|
||||
q.MustWriteBlock(block1)
|
||||
q.MustWriteBlock(block2)
|
||||
q.MustClose()
|
||||
|
||||
chunkPath := filepath.Join(path, fmt.Sprintf("%016X", 0))
|
||||
chunkFileSize := fs.MustFileSize(chunkPath)
|
||||
|
||||
if err := os.Truncate(chunkPath, int64(chunkFileSize)-1); err != nil {
|
||||
t.Fatalf("unexpected error: %s", err)
|
||||
}
|
||||
|
||||
q = mustOpen(path, "foobar", 0)
|
||||
|
||||
var buf []byte
|
||||
var ok bool
|
||||
buf, ok = q.MustReadBlockNonblocking(buf[:0])
|
||||
if !ok {
|
||||
t.Fatalf("expected block to be readable")
|
||||
}
|
||||
if string(buf) != string(block1) {
|
||||
t.Fatalf("unexpected block got: %q, want: %q", buf, block1)
|
||||
}
|
||||
|
||||
_, ok = q.MustReadBlockNonblocking(buf[:0])
|
||||
if ok {
|
||||
t.Fatalf("expected second block to be dropped")
|
||||
}
|
||||
q.MustClose()
|
||||
|
||||
expectedChunkFileSize := uint64(blockHeaderSize + len(block1))
|
||||
chunkFileSize = fs.MustFileSize(chunkPath)
|
||||
if chunkFileSize != expectedChunkFileSize {
|
||||
t.Fatalf("unexpected chunk file size: got %d; want %d", chunkFileSize, expectedChunkFileSize)
|
||||
}
|
||||
|
||||
fs.MustRemoveDir(path)
|
||||
})
|
||||
t.Run("damaged-writer-file-extra-tail", func(t *testing.T) {
|
||||
path := "damaged-writer-file-extra-tail"
|
||||
fs.MustRemoveDir(path)
|
||||
|
||||
q := mustOpen(path, "foobar", 0)
|
||||
blocks := [][]byte{
|
||||
[]byte("valid block 1"),
|
||||
[]byte("valid block 2"),
|
||||
}
|
||||
for _, block := range blocks {
|
||||
q.MustWriteBlock(block)
|
||||
}
|
||||
q.MustClose()
|
||||
|
||||
chunkPath := filepath.Join(path, fmt.Sprintf("%016X", 0))
|
||||
originChunkFileSize := fs.MustFileSize(chunkPath)
|
||||
|
||||
f, err := os.OpenFile(chunkPath, os.O_WRONLY|os.O_APPEND, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %s", err)
|
||||
}
|
||||
if _, err := f.Write([]byte("corrupted block")); err != nil {
|
||||
t.Fatalf("cannot append corrupted tail: %s", err)
|
||||
}
|
||||
if err := f.Close(); err != nil {
|
||||
t.Fatalf("cannot close chunk file: %s", err)
|
||||
}
|
||||
|
||||
q = mustOpen(path, "foobar", 0)
|
||||
|
||||
var buf []byte
|
||||
var ok bool
|
||||
for _, block := range blocks {
|
||||
buf, ok = q.MustReadBlockNonblocking(buf[:0])
|
||||
if !ok {
|
||||
t.Fatalf("expected block to be readable")
|
||||
}
|
||||
if string(buf) != string(block) {
|
||||
t.Fatalf("unexpected block got: %q, want: %q", buf, block)
|
||||
}
|
||||
}
|
||||
q.MustClose()
|
||||
chunkFileSize := fs.MustFileSize(chunkPath)
|
||||
if chunkFileSize != originChunkFileSize {
|
||||
t.Fatalf("unexpected chunk file size: got %d; want %d", chunkFileSize, originChunkFileSize)
|
||||
}
|
||||
fs.MustRemoveDir(path)
|
||||
})
|
||||
t.Run("damaged-writer-file-partial-header", func(t *testing.T) {
|
||||
path := "damaged-writer-file-partial-header"
|
||||
fs.MustRemoveDir(path)
|
||||
|
||||
q := mustOpen(path, "foobar", 0)
|
||||
block1 := []byte("valid block 1")
|
||||
block2 := []byte("valid block 2")
|
||||
q.MustWriteBlock(block1)
|
||||
q.MustWriteBlock(block2)
|
||||
q.MustClose()
|
||||
|
||||
chunkPath := filepath.Join(path, fmt.Sprintf("%016X", 0))
|
||||
// Keep block1 intact plus only 3 bytes of the second block's header.
|
||||
newSize := int64(blockHeaderSize + len(block1) + 3)
|
||||
if err := os.Truncate(chunkPath, newSize); err != nil {
|
||||
t.Fatalf("unexpected error: %s", err)
|
||||
}
|
||||
|
||||
q = mustOpen(path, "foobar", 0)
|
||||
buf, ok := q.MustReadBlockNonblocking(nil)
|
||||
if !ok {
|
||||
t.Fatalf("expected block to be readable")
|
||||
}
|
||||
if string(buf) != string(block1) {
|
||||
t.Fatalf("unexpected block got: %q, want: %q", buf, block1)
|
||||
}
|
||||
if _, ok := q.MustReadBlockNonblocking(buf[:0]); ok {
|
||||
t.Fatalf("expected second block to be dropped")
|
||||
}
|
||||
q.MustClose()
|
||||
|
||||
expectedChunkFileSize := uint64(blockHeaderSize + len(block1))
|
||||
if chunkFileSize := fs.MustFileSize(chunkPath); chunkFileSize != expectedChunkFileSize {
|
||||
t.Fatalf("unexpected chunk file size: got %d; want %d", chunkFileSize, expectedChunkFileSize)
|
||||
}
|
||||
fs.MustRemoveDir(path)
|
||||
})
|
||||
t.Run("damaged-writer-file-corrupted-header", func(t *testing.T) {
|
||||
path := "damaged-writer-file-corrupted-header"
|
||||
fs.MustRemoveDir(path)
|
||||
|
||||
q := mustOpen(path, "foobar", 0)
|
||||
block1 := []byte("valid block 1")
|
||||
block2 := []byte("valid block 2")
|
||||
block3 := []byte("valid block 3")
|
||||
q.MustWriteBlock(block1)
|
||||
q.MustWriteBlock(block2)
|
||||
q.MustWriteBlock(block3)
|
||||
q.MustClose()
|
||||
|
||||
chunkPath := filepath.Join(path, fmt.Sprintf("%016X", 0))
|
||||
chunkFileSize := fs.MustFileSize(chunkPath)
|
||||
|
||||
// Overwrite the second block's header with an impossible blockLen
|
||||
// and drop the last byte so the recovery scan is triggered.
|
||||
f, err := os.OpenFile(chunkPath, os.O_WRONLY, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %s", err)
|
||||
}
|
||||
garbage := [blockHeaderSize]byte{0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}
|
||||
if _, err := f.WriteAt(garbage[:], int64(blockHeaderSize+len(block1))); err != nil {
|
||||
t.Fatalf("cannot corrupt block header: %s", err)
|
||||
}
|
||||
if err := f.Close(); err != nil {
|
||||
t.Fatalf("cannot close chunk file: %s", err)
|
||||
}
|
||||
if err := os.Truncate(chunkPath, int64(chunkFileSize)-1); err != nil {
|
||||
t.Fatalf("unexpected error: %s", err)
|
||||
}
|
||||
|
||||
q = mustOpen(path, "foobar", 0)
|
||||
buf, ok := q.MustReadBlockNonblocking(nil)
|
||||
if !ok {
|
||||
t.Fatalf("expected block to be readable")
|
||||
}
|
||||
if string(buf) != string(block1) {
|
||||
t.Fatalf("unexpected block got: %q, want: %q", buf, block1)
|
||||
}
|
||||
if _, ok := q.MustReadBlockNonblocking(buf[:0]); ok {
|
||||
t.Fatalf("expected remaining blocks to be dropped")
|
||||
}
|
||||
q.MustClose()
|
||||
|
||||
expectedChunkFileSize := uint64(blockHeaderSize + len(block1))
|
||||
if chunkFileSize := fs.MustFileSize(chunkPath); chunkFileSize != expectedChunkFileSize {
|
||||
t.Fatalf("unexpected chunk file size: got %d; want %d", chunkFileSize, expectedChunkFileSize)
|
||||
}
|
||||
fs.MustRemoveDir(path)
|
||||
})
|
||||
t.Run("damaged-writer-file-second-chunk", func(t *testing.T) {
|
||||
path := "damaged-writer-file-second-chunk"
|
||||
fs.MustRemoveDir(path)
|
||||
|
||||
const maxBlockSize = 64
|
||||
const chunkFileSize = (maxBlockSize + blockHeaderSize) * 2
|
||||
|
||||
newBlock := func(fill byte) []byte {
|
||||
b := make([]byte, 32)
|
||||
for i := range b {
|
||||
b[i] = fill
|
||||
}
|
||||
return b
|
||||
}
|
||||
block1 := newBlock('1')
|
||||
block2 := newBlock('2')
|
||||
block3 := newBlock('3')
|
||||
block4 := newBlock('4')
|
||||
|
||||
q := mustOpenInternal(path, "foobar", chunkFileSize, maxBlockSize, 0)
|
||||
q.MustWriteBlock(block1)
|
||||
q.MustWriteBlock(block2)
|
||||
// blocks 3 and 4 land in the second chunk file
|
||||
q.MustWriteBlock(block3)
|
||||
q.MustWriteBlock(block4)
|
||||
q.MustClose()
|
||||
|
||||
secondChunkPath := filepath.Join(path, fmt.Sprintf("%016X", chunkFileSize))
|
||||
secondChunkSize := fs.MustFileSize(secondChunkPath)
|
||||
if err := os.Truncate(secondChunkPath, int64(secondChunkSize)-1); err != nil {
|
||||
t.Fatalf("unexpected error: %s", err)
|
||||
}
|
||||
|
||||
q = mustOpenInternal(path, "foobar", chunkFileSize, maxBlockSize, 0)
|
||||
var buf []byte
|
||||
var ok bool
|
||||
for _, block := range [][]byte{block1, block2, block3} {
|
||||
buf, ok = q.MustReadBlockNonblocking(buf[:0])
|
||||
if !ok {
|
||||
t.Fatalf("expected block to be readable")
|
||||
}
|
||||
if string(buf) != string(block) {
|
||||
t.Fatalf("unexpected block got: %q, want: %q", buf, block)
|
||||
}
|
||||
}
|
||||
if _, ok := q.MustReadBlockNonblocking(buf[:0]); ok {
|
||||
t.Fatalf("expected last block to be dropped")
|
||||
}
|
||||
q.MustClose()
|
||||
|
||||
expectedSize := uint64(blockHeaderSize + len(block3))
|
||||
if gotSize := fs.MustFileSize(secondChunkPath); gotSize != expectedSize {
|
||||
t.Fatalf("unexpected second chunk file size: got %d; want %d", gotSize, expectedSize)
|
||||
}
|
||||
fs.MustRemoveDir(path)
|
||||
})
|
||||
}
|
||||
|
||||
func TestQueueResetIfEmpty(t *testing.T) {
|
||||
|
||||
@@ -9,47 +9,48 @@ import (
|
||||
)
|
||||
|
||||
// WriteMetricRelabelDebug writes /metric-relabel-debug page to w with the corresponding args.
|
||||
func WriteMetricRelabelDebug(w io.Writer, targetID, metric, relabelConfigs, format string, err error) {
|
||||
writeRelabelDebug(w, false, targetID, metric, relabelConfigs, format, err)
|
||||
func WriteMetricRelabelDebug(w io.Writer, targetID, metric, relabelConfigs string, urlRelabelIndexLength, urlRelabelIndexCurrent int, format string, err error) {
|
||||
writeRelabelDebug(w, false, targetID, metric, relabelConfigs, urlRelabelIndexLength, urlRelabelIndexCurrent, format, err)
|
||||
}
|
||||
|
||||
// WriteTargetRelabelDebug writes /target-relabel-debug page to w with the corresponding args.
|
||||
func WriteTargetRelabelDebug(w io.Writer, targetID, metric, relabelConfigs, format string, err error) {
|
||||
writeRelabelDebug(w, true, targetID, metric, relabelConfigs, format, err)
|
||||
writeRelabelDebug(w, true, targetID, metric, relabelConfigs, 0, 0, format, err)
|
||||
}
|
||||
|
||||
func writeRelabelDebug(w io.Writer, isTargetRelabel bool, targetID, metric, relabelConfigs, format string, err error) {
|
||||
func writeRelabelDebug(w io.Writer, isTargetRelabel bool, targetID, metric, relabelConfigs string, urlRelabelIndexLength, urlRelabelIndexCurrent int, format string, err error) {
|
||||
if metric == "" {
|
||||
metric = "{}"
|
||||
}
|
||||
targetURL := ""
|
||||
if err != nil {
|
||||
WriteRelabelDebugSteps(w, targetURL, targetID, format, nil, metric, relabelConfigs, err)
|
||||
WriteRelabelDebugSteps(w, targetURL, targetID, format, nil, metric, relabelConfigs, urlRelabelIndexLength, urlRelabelIndexCurrent, isTargetRelabel, err)
|
||||
return
|
||||
}
|
||||
|
||||
metric, err = normalizeInputLabels(metric)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("cannot parse metric: %w", err)
|
||||
WriteRelabelDebugSteps(w, targetURL, targetID, format, nil, metric, relabelConfigs, err)
|
||||
WriteRelabelDebugSteps(w, targetURL, targetID, format, nil, metric, relabelConfigs, urlRelabelIndexLength, urlRelabelIndexCurrent, isTargetRelabel, err)
|
||||
return
|
||||
}
|
||||
|
||||
labels, err := promutil.NewLabelsFromString(metric)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("cannot parse metric: %w", err)
|
||||
WriteRelabelDebugSteps(w, targetURL, targetID, format, nil, metric, relabelConfigs, err)
|
||||
WriteRelabelDebugSteps(w, targetURL, targetID, format, nil, metric, relabelConfigs, urlRelabelIndexLength, urlRelabelIndexCurrent, isTargetRelabel, err)
|
||||
return
|
||||
}
|
||||
|
||||
pcs, err := ParseRelabelConfigsData([]byte(relabelConfigs))
|
||||
if err != nil {
|
||||
err = fmt.Errorf("cannot parse relabel configs: %w", err)
|
||||
WriteRelabelDebugSteps(w, targetURL, targetID, format, nil, metric, relabelConfigs, err)
|
||||
WriteRelabelDebugSteps(w, targetURL, targetID, format, nil, metric, relabelConfigs, urlRelabelIndexLength, urlRelabelIndexCurrent, isTargetRelabel, err)
|
||||
return
|
||||
}
|
||||
|
||||
dss, targetURL := newDebugRelabelSteps(pcs, labels, isTargetRelabel)
|
||||
WriteRelabelDebugSteps(w, targetURL, targetID, format, dss, metric, relabelConfigs, nil)
|
||||
WriteRelabelDebugSteps(w, targetURL, targetID, format, dss, metric, relabelConfigs, urlRelabelIndexLength, urlRelabelIndexCurrent, isTargetRelabel, nil)
|
||||
}
|
||||
|
||||
func newDebugRelabelSteps(pcs *ParsedConfigs, labels *promutil.Labels, isTargetRelabel bool) ([]DebugStep, string) {
|
||||
@@ -140,6 +141,10 @@ func getChangedLabelNames(in, out *promutil.Labels) map[string]struct{} {
|
||||
func normalizeInputLabels(metric string) (string, error) {
|
||||
metric = strings.TrimSpace(metric)
|
||||
|
||||
if strings.ContainsRune(metric, '\n') {
|
||||
return metric, fmt.Errorf("only one time series is allowed; got multiple lines")
|
||||
}
|
||||
|
||||
openBrace := strings.Contains(metric, `{`)
|
||||
closeBrace := strings.Contains(metric, `}`)
|
||||
|
||||
|
||||
@@ -6,37 +6,66 @@
|
||||
|
||||
{% stripspace %}
|
||||
|
||||
{% func RelabelDebugSteps(targetURL, targetID, format string, dss []DebugStep, metric, relabelConfigs string, err error) %}
|
||||
{% func RelabelDebugSteps(targetURL, targetID, format string, dss []DebugStep, metric, relabelConfigs string, urlRelabelIndexLength, urlRelabelIndexCurrent int, isTargetRelabel bool, err error) %}
|
||||
{% if format == "json" %}
|
||||
{%= RelabelDebugStepsJSON(targetURL, targetID, dss, metric, relabelConfigs, err) %}
|
||||
{%= RelabelDebugStepsJSON(targetURL, targetID, dss, metric, relabelConfigs, urlRelabelIndexLength, urlRelabelIndexCurrent, isTargetRelabel, err) %}
|
||||
{% else %}
|
||||
{%= RelabelDebugStepsHTML(targetURL, targetID, dss, metric, relabelConfigs, err) %}
|
||||
{%= RelabelDebugStepsHTML(targetURL, targetID, dss, metric, relabelConfigs, urlRelabelIndexLength, urlRelabelIndexCurrent, isTargetRelabel, err) %}
|
||||
{% endif %}
|
||||
{% endfunc %}
|
||||
|
||||
{% func RelabelDebugStepsHTML(targetURL, targetID string, dss []DebugStep, metric, relabelConfigs string, err error) %}
|
||||
{% func RelabelDebugStepsHTML(targetURL, targetID string, dss []DebugStep, metric, relabelConfigs string, urlRelabelIndexLength, urlRelabelIndexCurrent int, isTargetRelabel bool, err error) %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
{%= htmlcomponents.CommonHeader() %}
|
||||
<title>Metric relabel debug</title>
|
||||
<script>
|
||||
function submitRelabelDebugForm(e) {
|
||||
var form = e.target;
|
||||
var method = "GET";
|
||||
if (form.elements["relabel_configs"].value.length + form.elements["metric"].value.length > 1000) {
|
||||
method = "POST";
|
||||
}
|
||||
form.method = method;
|
||||
function setRelabelDebugFormMethod(form) {
|
||||
form.method = (form.elements["relabel_configs"].value.length + form.elements["metric"].value.length > 1000) ? "POST" : "GET";
|
||||
}
|
||||
|
||||
function reloadRelabelConfigs(select) {
|
||||
if (!confirm('Reload will discard all modifications to the current configuration. Continue?')) {
|
||||
select.value = select.prevValue;
|
||||
return;
|
||||
}
|
||||
document.getElementById('reload_url_relabel_configs').value = '1';
|
||||
setRelabelDebugFormMethod(select.form);
|
||||
select.form.submit();
|
||||
}
|
||||
|
||||
function initRelabelConfigsHighlight() {
|
||||
var ta = document.getElementById('relabel-configs-input');
|
||||
var bd = document.getElementById('relabel-configs-backdrop');
|
||||
if (!ta || !bd) return;
|
||||
function escapeHtml(s) {
|
||||
return s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
|
||||
}
|
||||
function highlight(text) {
|
||||
return text.split('\n').map(function(line) {
|
||||
var e = escapeHtml(line);
|
||||
return /^\s*#/.test(line)
|
||||
? '<span style="color:#999">'+e+'</span>'
|
||||
: '<span style="color:#212529">'+e+'</span>';
|
||||
}).join('\n');
|
||||
}
|
||||
function update() { bd.innerHTML = highlight(ta.value)+'\n'; }
|
||||
ta.addEventListener('input', update);
|
||||
ta.addEventListener('scroll', function() { bd.scrollTop = ta.scrollTop; });
|
||||
update();
|
||||
}
|
||||
document.addEventListener('DOMContentLoaded', initRelabelConfigsHighlight);
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
{%= htmlcomponents.Navbar() %}
|
||||
<div class="container-fluid">
|
||||
<a href="https://docs.victoriametrics.com/victoriametrics/relabeling/" target="_blank">Relabeling docs</a>{% space %}
|
||||
<a href="https://docs.victoriametrics.com/victoriametrics/relabeling/" target="_blank">Relabeling Cookbook</a>{% space %}|{% space %}
|
||||
<a href="https://docs.victoriametrics.com/victoriametrics/relabeling/#relabeling-stages" target="_blank">Relabeling Stages</a>
|
||||
|
||||
{% if targetID != "" %}
|
||||
{% space %}|{% space %}
|
||||
{% if targetURL != "" %}
|
||||
<a href="metric-relabel-debug?id={%s targetID %}">Metric relabel debug</a>
|
||||
{% else %}
|
||||
@@ -50,14 +79,14 @@ function submitRelabelDebugForm(e) {
|
||||
{% endif %}
|
||||
|
||||
<div class="m-3">
|
||||
<form method="POST" onsubmit="submitRelabelDebugForm(event)">
|
||||
{%= relabelDebugFormInputs(metric, relabelConfigs) %}
|
||||
<form method="POST" onsubmit="setRelabelDebugFormMethod(this)">
|
||||
{%= relabelDebugFormInputs(metric, relabelConfigs, urlRelabelIndexLength, urlRelabelIndexCurrent, isTargetRelabel, targetID) %}
|
||||
{% if targetID != "" %}
|
||||
<input type="hidden" name="id" value="{%s targetID %}" />
|
||||
{% endif %}
|
||||
<input type="submit" value="Submit" class="btn btn-primary m-1" />
|
||||
{% if targetID != "" %}
|
||||
<button type="button" onclick="location.href='?id={%s targetID %}'" class="btn btn-secondary m-1">Reset</button>
|
||||
<button type="button" onclick="location.href='?id={%s targetID %}'" class="btn btn-secondary m-1">Reset All</button>
|
||||
{% endif %}
|
||||
</form>
|
||||
</div>
|
||||
@@ -72,15 +101,44 @@ function submitRelabelDebugForm(e) {
|
||||
</html>
|
||||
{% endfunc %}
|
||||
|
||||
{% func relabelDebugFormInputs(metric, relabelConfigs string) %}
|
||||
{% func relabelDebugFormInputs(metric, relabelConfigs string, urlRelabelIndexLength, urlRelabelIndexCurrent int, isTargetRelabel bool, targetID string) %}
|
||||
<div>
|
||||
Relabel configs:<br/>
|
||||
<textarea name="relabel_configs" style="width: 100%; height: 15em; font-family: monospace" class="m-1">{%s relabelConfigs %}</textarea>
|
||||
</div>
|
||||
<!-- show remote write relabel reload only for scrape metric relabel debug and pure relabel debug. discovery debug should not display this section -->
|
||||
{% if !isTargetRelabel %}
|
||||
<div>
|
||||
<div class="m-1">
|
||||
<div class="d-flex align-items-center gap-2 mt-1">
|
||||
Configs:
|
||||
{% if urlRelabelIndexLength > 0 %}
|
||||
<input type="hidden" name="reload_url_relabel_configs" id="reload_url_relabel_configs" value="" />
|
||||
<select name="url_relabel_configs_index" class="form-select form-select-sm w-auto" onfocus="this.prevValue=this.value" onchange="reloadRelabelConfigs(this)">
|
||||
{% for i := range urlRelabelIndexLength %}
|
||||
{% if urlRelabelIndexCurrent == i %}
|
||||
<option value="{%d i %}" selected="selected">remote-write-url-{%d i %}</option>
|
||||
{% else %}
|
||||
<option value="{%d i %}">remote-write-url-{%d i %}</option>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</select>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
Configs:
|
||||
{% endif %}
|
||||
|
||||
<div>
|
||||
Labels:<br/>
|
||||
<textarea name="metric" style="width: 100%; height: 5em; font-family: monospace" class="m-1">{%s metric %}</textarea>
|
||||
<!-- the following text area css was generated with the help of AI to display yaml comments (starting with #) in gray. it could be rewritten in the future -->
|
||||
<div class="m-1" style="position:relative;height:15em;">
|
||||
<div id="relabel-configs-backdrop" style="position:absolute;top:0;left:0;right:0;bottom:0;pointer-events:none;overflow:hidden;font-family:monospace;white-space:pre-wrap;padding:0.375rem 0.75rem;border:1px solid transparent;"></div>
|
||||
<textarea id="relabel-configs-input" name="relabel_configs" class="form-control" style="position:absolute;top:0;left:0;height:100%;font-family:monospace;color:transparent;caret-color:#212529;background:transparent;resize:none;overflow-y:scroll;">
|
||||
{%s relabelConfigs %}
|
||||
</textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
A Time Series:<br/>
|
||||
<textarea name="metric" style="width: 100%; height: 5em; font-family: monospace" class="m-1" placeholder="up{job="job_name",instance="host:port"}">{%s metric %}</textarea>
|
||||
</div>
|
||||
{% endfunc %}
|
||||
|
||||
@@ -153,7 +211,7 @@ function submitRelabelDebugForm(e) {
|
||||
{% endif %}
|
||||
{% endfunc %}
|
||||
|
||||
{% func RelabelDebugStepsJSON(targetURL, targetID string, dss []DebugStep, metric, relabelConfigs string, err error) %}
|
||||
{% func RelabelDebugStepsJSON(targetURL, targetID string, dss []DebugStep, metric, relabelConfigs string, urlRelabelIndexLength, urlRelabelIndexCurrent int, isTargetRelabel bool, err error) %}
|
||||
{
|
||||
{% if err != nil %}
|
||||
"status": "error",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -10,10 +10,10 @@ import (
|
||||
|
||||
// TestWriteRelabelDebugSupportFormats verifies the relabeling debug input, rules and output.
|
||||
func TestWriteRelabelDebugSupportFormats(t *testing.T) {
|
||||
f := func(input, rule, expect string) {
|
||||
f := func(input, rules, expect string) {
|
||||
// execute
|
||||
outputWriter := bytes.NewBuffer(nil)
|
||||
writeRelabelDebug(outputWriter, false, "", input, rule, "json", nil)
|
||||
writeRelabelDebug(outputWriter, false, "", input, rules, 0, 0, "json", nil)
|
||||
|
||||
// the response is in JSON with HTML content, extract the `resultingLabels` in JSON and unescape it.
|
||||
resultingLabels := fastjson.GetString(outputWriter.Bytes(), `resultingLabels`)
|
||||
@@ -41,4 +41,20 @@ func TestWriteRelabelDebugSupportFormats(t *testing.T) {
|
||||
f(`{_name__="metric_name"`, ruleTestParsing, ``)
|
||||
f(`_name__="metric_name}"`, ruleTestParsing, ``)
|
||||
f(`metrics_name}"`, ruleTestParsing, ``)
|
||||
|
||||
// test multiple rules including remote writes
|
||||
// drop all labels and add one in URL relabeling
|
||||
rule1 := `
|
||||
- action: labeldrop
|
||||
regex: "drop_me_metrics_relabel"
|
||||
`
|
||||
rule2 := `
|
||||
- action: labeldrop
|
||||
regex: "drop_me_remote_write_relabel"
|
||||
`
|
||||
rule3 := `
|
||||
- target_label: add_me_url_relabel
|
||||
replacement: added
|
||||
`
|
||||
f(`{__name__="metric_name", drop_me_metrics_relabel="1", drop_me_remote_write_relabel="2"}`, rule1+rule2+rule3, `metric_name{add_me_url_relabel="added"}`)
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
@@ -53,10 +54,18 @@ func newClient(ctx context.Context, sw *ScrapeWork) (*client, error) {
|
||||
return nil
|
||||
}
|
||||
dialFunc := netutil.NewStatDialFunc("vm_promscrape")
|
||||
if sw.UnixSocket != "" {
|
||||
dialFunc = netutil.NewStatDialFuncWithDial("vm_promscrape", func(ctx context.Context, _, _ string) (net.Conn, error) {
|
||||
return netutil.Dialer.DialContext(ctx, "unix", sw.UnixSocket)
|
||||
})
|
||||
}
|
||||
proxyURL := sw.ProxyURL
|
||||
var proxyURLFunc func(*http.Request) (*url.URL, error)
|
||||
|
||||
if proxyURL != nil {
|
||||
if sw.UnixSocket != "" {
|
||||
return nil, fmt.Errorf("proxyURL: %q cannot be used for scraping unix_socket target: %q", proxyURL, sw.UnixSocket)
|
||||
}
|
||||
// case for direct http proxy connection.
|
||||
// must be used for http based scrape targets
|
||||
// since standard golang http.transport has special case for it
|
||||
|
||||
@@ -1325,6 +1325,9 @@ func (swc *scrapeWorkConfig) getScrapeWork(target string, extraLabels, metaLabel
|
||||
}
|
||||
streamParse = b
|
||||
}
|
||||
// Read __unix_socket__ option from __unix_socket__ label.
|
||||
unixSocket := labels.Get("__unix_socket__")
|
||||
|
||||
// Remove labels with "__" prefix according to https://www.robustperception.io/life-of-a-label/
|
||||
labels.RemoveLabelsWithDoubleUnderscorePrefix()
|
||||
// Add missing "instance" label according to https://www.robustperception.io/life-of-a-label
|
||||
@@ -1369,6 +1372,7 @@ func (swc *scrapeWorkConfig) getScrapeWork(target string, extraLabels, metaLabel
|
||||
LabelLimit: labelLimit,
|
||||
NoStaleMarkers: swc.noStaleMarkers,
|
||||
AuthToken: at,
|
||||
UnixSocket: unixSocket,
|
||||
|
||||
jobNameOriginal: swc.jobName,
|
||||
}
|
||||
|
||||
@@ -3,34 +3,104 @@ package promscrape
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promutil"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/httpserver"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promrelabel"
|
||||
)
|
||||
|
||||
// WriteMetricRelabelDebug serves requests to /metric-relabel-debug page
|
||||
func WriteMetricRelabelDebug(w http.ResponseWriter, r *http.Request) {
|
||||
// WriteMetricRelabelDebug serves requests to /metric-relabel-debug page.
|
||||
// remotewrite-related relabel configs could be empty as vmsingle doesn't provide remote write feature.
|
||||
func WriteMetricRelabelDebug(w http.ResponseWriter, r *http.Request, rwGlobalRelabelConfigs string, rwURLRelabelConfigss []string) {
|
||||
targetID := r.FormValue("id")
|
||||
metric := r.FormValue("metric")
|
||||
relabelConfigs := r.FormValue("relabel_configs")
|
||||
|
||||
// if set, it means user selected another URL from the dropdown and everything will be reloaded.
|
||||
reloadRWURLRelabelConfigs := r.FormValue("reload_url_relabel_configs")
|
||||
rwURLRelabelConfigsIdxStr := r.FormValue("url_relabel_configs_index")
|
||||
|
||||
format := r.FormValue("format")
|
||||
var err error
|
||||
|
||||
if metric == "" && relabelConfigs == "" && targetID != "" {
|
||||
pcs, labels, ok := getMetricRelabelContextByTargetID(targetID)
|
||||
if !ok {
|
||||
err = fmt.Errorf("cannot find target for id=%s", targetID)
|
||||
targetID = ""
|
||||
} else {
|
||||
metric = labels.String()
|
||||
relabelConfigs = pcs.String()
|
||||
// if all per-URL config is empty, it means no per-URL rule is configured.
|
||||
// set it to 0 so the user do not see the options in debug page.
|
||||
rwURLRelabelConfigsLength := 0
|
||||
for _, urlRelabelConfig := range rwURLRelabelConfigss {
|
||||
if urlRelabelConfig != "" {
|
||||
rwURLRelabelConfigsLength = len(rwURLRelabelConfigss)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
rwURLRelabelConfigsIdx, idxErr := strconv.Atoi(rwURLRelabelConfigsIdxStr)
|
||||
if idxErr != nil {
|
||||
rwURLRelabelConfigsIdx = -1
|
||||
}
|
||||
|
||||
// load the initial data with specific remote write URL index (default 0) in 2 cases:
|
||||
// - relabel config is empty. load scrape relabel (if targetID exist) + remote write related relabel (always).
|
||||
// - `reload` is set. load scrape relabel (if targetID exist) + reload remote write related relabel (by the URL index).
|
||||
init := metric == "" && relabelConfigs == "" && reloadRWURLRelabelConfigs == ""
|
||||
reload := reloadRWURLRelabelConfigs != ""
|
||||
if init || reload {
|
||||
// scrape related relabel labels & rules
|
||||
var (
|
||||
pcs = &promrelabel.ParsedConfigs{} // could be empty
|
||||
labels *promutil.Labels
|
||||
ok bool
|
||||
)
|
||||
if targetID != "" {
|
||||
pcs, labels, ok = getMetricRelabelContextByTargetID(targetID)
|
||||
if !ok {
|
||||
err = fmt.Errorf("cannot find target for id=%s", targetID)
|
||||
targetID = ""
|
||||
} else {
|
||||
metric = "up"
|
||||
metric += labels.String()
|
||||
}
|
||||
}
|
||||
|
||||
// general relabel rules (remote write)
|
||||
// set the per-URL remote write relabel according to index, any error will fall back the index to 0.
|
||||
rwURLRelabelConfigs := ""
|
||||
if len(rwURLRelabelConfigss) > 0 {
|
||||
// ignore the error if the input is invalid or exceed the length, and fallback to 0.
|
||||
if rwURLRelabelConfigsIdx < 0 || rwURLRelabelConfigsIdx >= len(rwURLRelabelConfigss) {
|
||||
rwURLRelabelConfigsIdx = 0
|
||||
}
|
||||
rwURLRelabelConfigs = rwURLRelabelConfigss[rwURLRelabelConfigsIdx]
|
||||
}
|
||||
|
||||
relabelConfigs = composeRelabelConfigs(pcs.String(), rwGlobalRelabelConfigs, rwURLRelabelConfigs, rwURLRelabelConfigsIdx)
|
||||
}
|
||||
|
||||
if format == "json" {
|
||||
httpserver.EnableCORS(w, r)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
}
|
||||
promrelabel.WriteMetricRelabelDebug(w, targetID, metric, relabelConfigs, format, err)
|
||||
promrelabel.WriteMetricRelabelDebug(w, targetID, metric, relabelConfigs, rwURLRelabelConfigsLength, rwURLRelabelConfigsIdx, format, err)
|
||||
}
|
||||
|
||||
func composeRelabelConfigs(relabelConfigs, rwGlobalRelabelConfigs, rwURLRelabelConfigs string, rwURLIdx int) string {
|
||||
if relabelConfigs != "" {
|
||||
relabelConfigs = "# -promscrape.config .scrape_configs[].metric_relabel_configs\n" + strings.TrimSpace(relabelConfigs) + "\n"
|
||||
}
|
||||
|
||||
if rwGlobalRelabelConfigs != "" {
|
||||
relabelConfigs += "\n# -remoteWrite.relabelConfig"
|
||||
relabelConfigs += "\n" + strings.TrimSpace(rwGlobalRelabelConfigs) + "\n"
|
||||
}
|
||||
|
||||
if rwURLRelabelConfigs != "" {
|
||||
relabelConfigs += fmt.Sprintf("\n# -remoteWrite.urlRelabelConfig=remote-write-url-%d", rwURLIdx)
|
||||
relabelConfigs += "\n" + strings.TrimSpace(rwURLRelabelConfigs) + "\n"
|
||||
}
|
||||
|
||||
return relabelConfigs
|
||||
}
|
||||
|
||||
// WriteTargetRelabelDebug generates response for /target-relabel-debug page
|
||||
@@ -48,7 +118,7 @@ func WriteTargetRelabelDebug(w http.ResponseWriter, r *http.Request) {
|
||||
targetID = ""
|
||||
} else {
|
||||
metric = labels.labelsString()
|
||||
relabelConfigs = pcs.String()
|
||||
relabelConfigs = "# -promscrape.config .scrape_configs[].relabel_configs\n" + pcs.String()
|
||||
}
|
||||
}
|
||||
if format == "json" {
|
||||
|
||||
@@ -157,6 +157,9 @@ type ScrapeWork struct {
|
||||
// The Tenant Info
|
||||
AuthToken *auth.Token
|
||||
|
||||
// Optional path to Unix domain socket for scraping metrics over Unix domain socket.
|
||||
UnixSocket string
|
||||
|
||||
// The original 'job_name'
|
||||
jobNameOriginal string
|
||||
}
|
||||
@@ -174,12 +177,12 @@ func (sw *ScrapeWork) key() string {
|
||||
// Do not take into account OriginalLabels, since they can be changed with relabeling.
|
||||
// Do not take into account RelabelConfigs, since it is already applied to Labels.
|
||||
// Take into account JobNameOriginal in order to capture the case when the original job_name is changed via relabeling.
|
||||
key := fmt.Sprintf("JobNameOriginal=%s, ScrapeURL=%s, ScrapeInterval=%s, ScrapeTimeout=%s, HonorLabels=%v, "+
|
||||
key := fmt.Sprintf("JobNameOriginal=%s, ScrapeURL=%s, UnixSocket=%s, ScrapeInterval=%s, ScrapeTimeout=%s, HonorLabels=%v, "+
|
||||
"HonorTimestamps=%v, DenyRedirects=%v, Labels=%s, ExternalLabels=%s, MaxScrapeSize=%d, "+
|
||||
"ProxyURL=%s, ProxyAuthConfig=%s, AuthConfig=%s, MetricRelabelConfigs=%q, "+
|
||||
"SampleLimit=%d, DisableCompression=%v, DisableKeepAlive=%v, StreamParse=%v, "+
|
||||
"ScrapeAlignInterval=%s, ScrapeOffset=%s, SeriesLimit=%d, LabelLimit=%d, NoStaleMarkers=%v",
|
||||
sw.jobNameOriginal, sw.ScrapeURL, sw.ScrapeInterval, sw.ScrapeTimeout, sw.HonorLabels,
|
||||
sw.jobNameOriginal, sw.ScrapeURL, sw.UnixSocket, sw.ScrapeInterval, sw.ScrapeTimeout, sw.HonorLabels,
|
||||
sw.HonorTimestamps, sw.DenyRedirects, sw.Labels.String(), sw.ExternalLabels.String(), sw.MaxScrapeSize,
|
||||
sw.ProxyURL.String(), sw.ProxyAuthConfig.String(), sw.AuthConfig.String(), sw.MetricRelabelConfigs.String(),
|
||||
sw.SampleLimit, sw.DisableCompression, sw.DisableKeepAlive, sw.StreamParse,
|
||||
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/fs"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/memory"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/syncwg"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/timeutil"
|
||||
)
|
||||
|
||||
@@ -130,6 +131,9 @@ type partition struct {
|
||||
// wg.Add() must be called under partsLock after checking whether stopCh isn't closed.
|
||||
// This should prevent from calling wg.Add() after stopCh is closed and wg.Wait() is called.
|
||||
wg sync.WaitGroup
|
||||
|
||||
// Use syncwg instead of sync, since Add/Wait may be called from concurrent goroutines.
|
||||
flushPendingItemsWG syncwg.WaitGroup
|
||||
}
|
||||
|
||||
// partWrapper is a wrapper for the part.
|
||||
@@ -881,6 +885,9 @@ func (pt *partition) MustClose() {
|
||||
func (pt *partition) DebugFlush() {
|
||||
pt.idb.tb.DebugFlush()
|
||||
pt.flushPendingRows(true)
|
||||
|
||||
// Wait for background flushers to finish.
|
||||
pt.flushPendingItemsWG.Wait()
|
||||
}
|
||||
|
||||
func (pt *partition) startInmemoryPartsMergers() {
|
||||
@@ -1011,7 +1018,9 @@ func (pt *partition) pendingRowsFlusher() {
|
||||
}
|
||||
|
||||
func (pt *partition) flushPendingRows(isFinal bool) {
|
||||
pt.flushPendingItemsWG.Add(1)
|
||||
pt.rawRows.flush(pt.flushRowssToInmemoryParts, isFinal)
|
||||
pt.flushPendingItemsWG.Done()
|
||||
}
|
||||
|
||||
func (pt *partition) flushInmemoryRowsToFiles() {
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"reflect"
|
||||
"regexp"
|
||||
"sort"
|
||||
"sync"
|
||||
"testing"
|
||||
"testing/quick"
|
||||
"time"
|
||||
@@ -191,6 +192,67 @@ func TestSearch_VariousTimeRanges(t *testing.T) {
|
||||
testStorageOpOnVariousTimeRanges(t, f)
|
||||
}
|
||||
|
||||
// TestStorageAddFlushSearchDataConcurrently verifies that concurrent goroutines
|
||||
// can read their own writes.
|
||||
//
|
||||
// This test focuses on reading the data. For reading the index see
|
||||
// TestStorageAddFlushSearchMetricNamesConcurrently in storage_test.go.
|
||||
func TestStorageAddFlushSearchDataConcurrently(t *testing.T) {
|
||||
defer testRemoveAll(t)
|
||||
|
||||
s := MustOpenStorage(t.Name(), OpenOptions{})
|
||||
defer s.MustClose()
|
||||
|
||||
const numMetrics = 100
|
||||
f := func(workerID int, tr TimeRange) error {
|
||||
mrs := make([]MetricRow, numMetrics)
|
||||
step := (tr.MaxTimestamp - tr.MinTimestamp) / int64(numMetrics)
|
||||
for i := range numMetrics {
|
||||
name := fmt.Sprintf("metric_%04d_%04d", workerID, i)
|
||||
mn := MetricName{MetricGroup: []byte(name)}
|
||||
mrs[i].MetricNameRaw = mn.marshalRaw(nil)
|
||||
mrs[i].Timestamp = tr.MinTimestamp + int64(i)*step
|
||||
mrs[i].Value = float64(i)
|
||||
}
|
||||
|
||||
s.AddRows(mrs, defaultPrecisionBits)
|
||||
s.DebugFlush()
|
||||
|
||||
tfs := NewTagFilters()
|
||||
re := fmt.Sprintf(`metric_%04d.*`, workerID)
|
||||
if err := tfs.Add(nil, []byte(re), false, true); err != nil {
|
||||
return fmt.Errorf("tfs.Add(%q) failed unexpectedly: %w", re, err)
|
||||
}
|
||||
return testAssertSearchResult(s, tr, tfs, mrs)
|
||||
}
|
||||
|
||||
const concurrency = 20
|
||||
var wg sync.WaitGroup
|
||||
errs := make([]error, concurrency)
|
||||
for workerID := range concurrency {
|
||||
wg.Go(func() {
|
||||
for m := time.Month(1); m <= 12; m++ {
|
||||
tr := TimeRange{
|
||||
MinTimestamp: time.Date(2025, m, 1, 0, 0, 0, 0, time.UTC).UnixMilli(),
|
||||
MaxTimestamp: time.Date(2025, m+1, 0, 0, 0, 0, 0, time.UTC).UnixMilli() - 1,
|
||||
}
|
||||
err := f(workerID, tr)
|
||||
if err != nil {
|
||||
errs[workerID] = fmt.Errorf("worker %d failed on tr=%v: %w", workerID, &tr, err)
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
for _, err := range errs {
|
||||
if err != nil {
|
||||
t.Errorf("%s", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testSearchInternal(s *Storage, tr TimeRange, mrs []MetricRow) error {
|
||||
for i := range 10 {
|
||||
// Prepare TagFilters for search.
|
||||
|
||||
@@ -528,113 +528,87 @@ func TestStorageDeletePendingSeries(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestStorageDeleteSeries(t *testing.T) {
|
||||
path := "TestStorageDeleteSeries"
|
||||
s := MustOpenStorage(path, OpenOptions{})
|
||||
defer testRemoveAll(t)
|
||||
|
||||
// Verify no label names exist
|
||||
lns, err := s.SearchLabelNames(nil, nil, TimeRange{}, 1e5, 1e9, noDeadline)
|
||||
if err != nil {
|
||||
t.Fatalf("error in SearchLabelNames() at the start: %s", err)
|
||||
}
|
||||
if len(lns) != 0 {
|
||||
t.Fatalf("found non-empty tag keys at the start: %q", lns)
|
||||
}
|
||||
|
||||
t.Run("serial", func(t *testing.T) {
|
||||
for i := range 3 {
|
||||
if err := testStorageDeleteSeries(s, 0); err != nil {
|
||||
t.Fatalf("unexpected error on iteration %d: %s", i, err)
|
||||
}
|
||||
|
||||
// Re-open the storage in order to check how deleted metricIDs
|
||||
// are persisted.
|
||||
s.MustClose()
|
||||
s = MustOpenStorage(path, OpenOptions{})
|
||||
for _, concurrency := range []int{1, 4} {
|
||||
for _, disablePerDayIndex := range []bool{false, true} {
|
||||
name := fmt.Sprintf("concurrency=%d/disablePerDayIndex=%t", concurrency, disablePerDayIndex)
|
||||
t.Run(name, func(t *testing.T) {
|
||||
testStorageDeleteSeries(t, concurrency, disablePerDayIndex)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("concurrent", func(t *testing.T) {
|
||||
ch := make(chan error, 3)
|
||||
for i := range cap(ch) {
|
||||
go func(workerNum int) {
|
||||
var err error
|
||||
for range 2 {
|
||||
err = testStorageDeleteSeries(s, workerNum)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
ch <- err
|
||||
}(i)
|
||||
}
|
||||
tt := time.NewTimer(30 * time.Second)
|
||||
for i := range cap(ch) {
|
||||
select {
|
||||
case err := <-ch:
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error on iteration %d: %s", i, err)
|
||||
}
|
||||
case <-tt.C:
|
||||
t.Fatalf("timeout on iteration %d", i)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Verify no more tag keys exist
|
||||
lns, err = s.SearchLabelNames(nil, nil, TimeRange{}, 1e5, 1e9, noDeadline)
|
||||
if err != nil {
|
||||
t.Fatalf("error in SearchLabelNames after the test: %s", err)
|
||||
}
|
||||
if len(lns) != 0 {
|
||||
t.Fatalf("found non-empty tag keys after the test: %q", lns)
|
||||
}
|
||||
|
||||
s.MustClose()
|
||||
fs.MustRemoveDir(path)
|
||||
}
|
||||
|
||||
func testStorageDeleteSeries(s *Storage, workerNum int) error {
|
||||
func testStorageDeleteSeries(t *testing.T, concurrency int, disablePerDayIndex bool) {
|
||||
tr := TimeRange{
|
||||
MinTimestamp: time.Date(2026, 1, 15, 0, 0, 0, 0, time.UTC).UnixMilli(),
|
||||
MaxTimestamp: time.Date(2026, 1, 15, 23, 59, 59, 999_999_999, time.UTC).UnixMilli(),
|
||||
}
|
||||
|
||||
s := MustOpenStorage(t.Name(), OpenOptions{
|
||||
DisablePerDayIndex: disablePerDayIndex,
|
||||
})
|
||||
defer s.MustClose()
|
||||
|
||||
errs := make([]error, concurrency)
|
||||
var wg sync.WaitGroup
|
||||
for workerNum := range concurrency {
|
||||
wg.Go(func() {
|
||||
var err error
|
||||
for range 10 {
|
||||
err = testStorageDeleteSeriesForWorker(workerNum, s, tr)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
errs[workerNum] = err
|
||||
})
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
for i, err := range errs {
|
||||
if err != nil {
|
||||
t.Fatalf("[worker %d] %s", i, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testStorageDeleteSeriesForWorker(workerNum int, s *Storage, tr TimeRange) error {
|
||||
rng := rand.New(rand.NewSource(1))
|
||||
const rowsPerMetric = 100
|
||||
const metricsCount = 30
|
||||
|
||||
workerTag := fmt.Appendf(nil, "workerTag_%d", workerNum)
|
||||
|
||||
lnsAll := make(map[string]bool)
|
||||
lnsAll["__name__"] = true
|
||||
for i := range metricsCount {
|
||||
var mrs []MetricRow
|
||||
var mn MetricName
|
||||
job := fmt.Sprintf("job_%d_%d", i, workerNum)
|
||||
instance := fmt.Sprintf("instance_%d_%d", i, workerNum)
|
||||
mn.Tags = []Tag{
|
||||
{[]byte("job"), []byte(job)},
|
||||
{[]byte("instance"), []byte(instance)},
|
||||
{workerTag, []byte("foobar")},
|
||||
mn := MetricName{
|
||||
MetricGroup: fmt.Appendf(nil, "metric_%d_%d", i, workerNum),
|
||||
Tags: []Tag{
|
||||
{[]byte("job"), fmt.Appendf(nil, "job_%d_%d", i, workerNum)},
|
||||
{[]byte("instance"), fmt.Appendf(nil, "instance_%d_%d", i, workerNum)},
|
||||
{workerTag, []byte("foobar")},
|
||||
},
|
||||
}
|
||||
for i := range mn.Tags {
|
||||
lnsAll[string(mn.Tags[i].Key)] = true
|
||||
}
|
||||
mn.MetricGroup = fmt.Appendf(nil, "metric_%d_%d", i, workerNum)
|
||||
metricNameRaw := mn.marshalRaw(nil)
|
||||
|
||||
var mrs []MetricRow
|
||||
for range rowsPerMetric {
|
||||
timestamp := rng.Int63n(1e10)
|
||||
value := rng.NormFloat64() * 1e6
|
||||
|
||||
mr := MetricRow{
|
||||
MetricNameRaw: metricNameRaw,
|
||||
Timestamp: timestamp,
|
||||
Value: value,
|
||||
}
|
||||
mrs = append(mrs, mr)
|
||||
mrs = append(mrs, MetricRow{
|
||||
MetricNameRaw: mn.marshalRaw(nil),
|
||||
Timestamp: tr.MinTimestamp + rng.Int63n(tr.MaxTimestamp-tr.MinTimestamp),
|
||||
Value: rng.NormFloat64() * 1e6,
|
||||
})
|
||||
}
|
||||
s.AddRows(mrs, defaultPrecisionBits)
|
||||
}
|
||||
s.DebugFlush()
|
||||
|
||||
// Verify tag values exist
|
||||
tvs, err := s.SearchLabelValues(nil, string(workerTag), nil, TimeRange{}, 1e5, 1e9, noDeadline)
|
||||
tvs, err := s.SearchLabelValues(nil, string(workerTag), nil, tr, 1e5, 1e9, noDeadline)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error in SearchLabelValues before metrics removal: %w", err)
|
||||
}
|
||||
@@ -643,7 +617,7 @@ func testStorageDeleteSeries(s *Storage, workerNum int) error {
|
||||
}
|
||||
|
||||
// Verify tag keys exist
|
||||
lns, err := s.SearchLabelNames(nil, nil, TimeRange{}, 1e5, 1e9, noDeadline)
|
||||
lns, err := s.SearchLabelNames(nil, nil, tr, 1e5, 1e9, noDeadline)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error in SearchLabelNames before metrics removal: %w", err)
|
||||
}
|
||||
@@ -651,20 +625,18 @@ func testStorageDeleteSeries(s *Storage, workerNum int) error {
|
||||
return fmt.Errorf("unexpected label names before metrics removal: %w", err)
|
||||
}
|
||||
|
||||
var sr Search
|
||||
tr := TimeRange{
|
||||
MinTimestamp: 0,
|
||||
MaxTimestamp: 2e10,
|
||||
}
|
||||
metricBlocksCount := func(tfs *TagFilters) int {
|
||||
// Verify the number of blocks
|
||||
n := 0
|
||||
countMetricBlocks := func(tfs *TagFilters) (int, error) {
|
||||
var sr Search
|
||||
sr.Init(nil, s, []*TagFilters{tfs}, tr, 1e5, noDeadline)
|
||||
defer sr.MustClose()
|
||||
n := 0
|
||||
for sr.NextMetricBlock() {
|
||||
n++
|
||||
}
|
||||
sr.MustClose()
|
||||
return n
|
||||
if err := sr.Error(); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
for i := range metricsCount {
|
||||
tfs := NewTagFilters()
|
||||
@@ -675,17 +647,26 @@ func testStorageDeleteSeries(s *Storage, workerNum int) error {
|
||||
if err := tfs.Add([]byte("job"), []byte(job), false, false); err != nil {
|
||||
return fmt.Errorf("cannot add job tag filter: %w", err)
|
||||
}
|
||||
if n := metricBlocksCount(tfs); n == 0 {
|
||||
n, err := countMetricBlocks(tfs)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to count metric blocks for tfs=%s: %w", tfs, err)
|
||||
}
|
||||
if n == 0 {
|
||||
return fmt.Errorf("expecting non-zero number of metric blocks for tfs=%s", tfs)
|
||||
}
|
||||
deletedCount, err := s.DeleteSeries(nil, []*TagFilters{tfs}, 1e9)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot delete metrics: %w", err)
|
||||
}
|
||||
s.DebugFlush()
|
||||
if deletedCount == 0 {
|
||||
return fmt.Errorf("expecting non-zero number of deleted metrics on iteration %d", i)
|
||||
}
|
||||
if n := metricBlocksCount(tfs); n != 0 {
|
||||
n, err = countMetricBlocks(tfs)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to count metric blocks for tfs=%s: %w", tfs, err)
|
||||
}
|
||||
if n != 0 {
|
||||
return fmt.Errorf("expecting zero metric blocks after DeleteSeries call for tfs=%s; got %d blocks", tfs, n)
|
||||
}
|
||||
|
||||
@@ -694,6 +675,7 @@ func testStorageDeleteSeries(s *Storage, workerNum int) error {
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot delete empty tfss: %w", err)
|
||||
}
|
||||
s.DebugFlush()
|
||||
if deletedCount != 0 {
|
||||
return fmt.Errorf("expecting zero deleted metrics for empty tfss; got %d", deletedCount)
|
||||
}
|
||||
@@ -704,10 +686,14 @@ func testStorageDeleteSeries(s *Storage, workerNum int) error {
|
||||
if err := tfs.Add(nil, fmt.Appendf(nil, "metric_.+_%d", workerNum), false, true); err != nil {
|
||||
return fmt.Errorf("cannot add regexp tag filter for worker metrics: %w", err)
|
||||
}
|
||||
if n := metricBlocksCount(tfs); n != 0 {
|
||||
n, err := countMetricBlocks(tfs)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to count metric blocks for tfs=%s: %w", tfs, err)
|
||||
}
|
||||
if n != 0 {
|
||||
return fmt.Errorf("expecting zero metric blocks after deleting all the metrics; got %d blocks", n)
|
||||
}
|
||||
tvs, err = s.SearchLabelValues(nil, string(workerTag), nil, TimeRange{}, 1e5, 1e9, noDeadline)
|
||||
tvs, err = s.SearchLabelValues(nil, string(workerTag), nil, tr, 1e5, 1e9, noDeadline)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error in SearchLabelValues after all the metrics are removed: %w", err)
|
||||
}
|
||||
@@ -4324,3 +4310,82 @@ func TestStorage_futureTimestamps(t *testing.T) {
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// TestStorageAddFlushSearchMetricNamesConcurrently verifies that concurrent
|
||||
// goroutines can read their own writes.
|
||||
//
|
||||
// This test focuses on reading the index. For reading the data see
|
||||
// TestStorageAddFlushSearchDataConcurrently in search_test.go.
|
||||
func TestStorageAddFlushSearchMetricNamesConcurrently(t *testing.T) {
|
||||
defer testRemoveAll(t)
|
||||
|
||||
s := MustOpenStorage(t.Name(), OpenOptions{})
|
||||
defer s.MustClose()
|
||||
|
||||
const numMetrics = 100
|
||||
f := func(workerID int, tr TimeRange) error {
|
||||
mrs := make([]MetricRow, numMetrics)
|
||||
want := make([]string, numMetrics)
|
||||
step := (tr.MaxTimestamp - tr.MinTimestamp) / int64(numMetrics)
|
||||
for i := range numMetrics {
|
||||
timestamp := tr.MinTimestamp + int64(i)*step
|
||||
name := fmt.Sprintf("metric_%04d_%d", workerID, timestamp)
|
||||
want[i] = name
|
||||
mn := MetricName{MetricGroup: []byte(name)}
|
||||
mrs[i].MetricNameRaw = mn.marshalRaw(nil)
|
||||
mrs[i].Timestamp = timestamp
|
||||
}
|
||||
|
||||
s.AddRows(mrs, defaultPrecisionBits)
|
||||
s.DebugFlush()
|
||||
|
||||
tfs := NewTagFilters()
|
||||
re := fmt.Sprintf(`metric_%04d.*`, workerID)
|
||||
if err := tfs.Add(nil, []byte(re), false, true); err != nil {
|
||||
return fmt.Errorf("tfs.Add(%q) failed unexpectedly: %w", re, err)
|
||||
}
|
||||
got, err := s.SearchMetricNames(nil, []*TagFilters{tfs}, tr, 1e9, noDeadline)
|
||||
if err != nil {
|
||||
return fmt.Errorf("SearchMetricNames(%v) failed unexpectedly: %w", tfs, err)
|
||||
}
|
||||
for i, name := range got {
|
||||
var mn MetricName
|
||||
if err := mn.UnmarshalString(name); err != nil {
|
||||
return fmt.Errorf("Could not unmarshal metric name %q: %w", name, err)
|
||||
}
|
||||
got[i] = string(mn.MetricGroup)
|
||||
}
|
||||
slices.Sort(got)
|
||||
|
||||
if diff := cmp.Diff(want, got); diff != "" {
|
||||
return fmt.Errorf("unexpected metric names (-want, +got):\n%s", diff)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
const concurrency = 20
|
||||
var wg sync.WaitGroup
|
||||
errs := make([]error, concurrency)
|
||||
for workerID := range concurrency {
|
||||
wg.Go(func() {
|
||||
for m := time.Month(1); m <= 12; m++ {
|
||||
tr := TimeRange{
|
||||
MinTimestamp: time.Date(2025, m, 1, 0, 0, 0, 0, time.UTC).UnixMilli(),
|
||||
MaxTimestamp: time.Date(2025, m+1, 0, 0, 0, 0, 0, time.UTC).UnixMilli() - 1,
|
||||
}
|
||||
err := f(workerID, tr)
|
||||
if err != nil {
|
||||
errs[workerID] = fmt.Errorf("worker %d failed on tr=%v: %w", workerID, &tr, err)
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
for _, err := range errs {
|
||||
if err != nil {
|
||||
t.Errorf("%s", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user