Compare commits

...

72 Commits

Author SHA1 Message Date
Jayice
4d14b0f95a update documentation 2026-06-03 17:21:44 +08:00
Jayice
7ef0733071 support range() function in MetricQL 2026-06-03 15:39:05 +08:00
Zakhar Bessarab
9d4c06210c app/vmauth: take JWT users into a total count of users for startup message
Previously, config like this:
```
users:
  - jwt:
      skip_verify: true
    url_prefix:
      - "http://backend"
```
Would print a message saying that 0 users were loaded which leads to
confusion.

Related PR https://github.com/VictoriaMetrics/VictoriaMetrics/pull/11050/
2026-06-02 17:51:11 +02:00
Dmytro Kozlov
347d2e0fef app/vmctl: implement migration from mimir object storage
This commit adds the ability to read blocks from Mimir object storage, process
them, and store them in VictoriaMetrics.
This new version of the migration can read Mimir object storage from the
file system and S3, GCP and Azure.

Under the hood, the vmctl tries to read the bucket-index.json file and
collect blocks via the defined filters. Depending on the used path vmctl
decides how to retrieve data. If it is not the file system, it will
download each file from the block and store it in the `tmp` folder.
Process this block folder and remove it from the file system.

Fixes https://github.com/VictoriaMetrics/VictoriaMetrics/issues/7717
2026-06-02 16:51:54 +02:00
Alexander Frolov
f3de1f4ac7 vmselect: allow partial responses for storage node groups
Previously vmselect returned http.StatusServiceUnavailable instead of a partial response when a single storage group is unavailable, even when partial responses are allowed.

According to the cluster documentation, vmselect should continue serving queries from available vmstorage nodes and mark incomplete responses as partial.

This commit fixes behavior and allows partial respones for grouped storage nodes.

Fixes https://github.com/VictoriaMetrics/VictoriaMetrics/issues/11009
2026-06-02 16:17:02 +02:00
f41gh7
a9032ecd1d docs/changelog: sort changelog entries 2026-06-02 15:44:08 +02:00
Nikolay
ce712f0bc9 docs/vmauth: add security recommendations for vmselect multitenant reads
Currently, vmselect uses `OR` logic to filter out tenants for
 `multitenant` requests. And if vmauth is used to enforce tenant
 requests with `extra_label` or `extra_filters`. It's required to set
 `extra_label`, `extra_filters` and `extra_filters[]` explicitly at
 configuration file to prevent possible enforcement bypass.
2026-06-02 15:34:17 +02:00
Nikolay
23a3ff4174 app/vmselect: fixes utf-8 escaping at federate API
Prometheus compatible scrapers failed to parse `/federate` API responses
from VictoriaMetrics, if it contains any utf-8 characters.

This commit adds best effort negotiation check for /federate API
requests. By default, VictoriaMetrics emmits output in the same format.
But in case of Accept: allow-utf-8 header, VictoriaMetrics switches into
quoted output for time series name and label names according to
Prometheus v3.0 text exposition format.
https://prometheus.io/docs/guides/utf8/#querying

Fixes https://github.com/VictoriaMetrics/VictoriaMetrics/issues/10968
2026-06-02 14:41:07 +02:00
Hui Wang
b638f95aba app/vmalert: reset group evaluation timestamp when system clock is modified backwards
The issue could happen subtly and be hard to debug. It might also
generate confusing results. For example, if the system clock for vmalert
is moved 30m backward, vmalert would reset the evaluation timestamps to
30m ago, causing duplicate evaluations for timestamps within that 30m
window. Exposing a new group metric `vmalert_iteration_reset_total` to
help debuging this issue.

fixes:
https://github.com/VictoriaMetrics/VictoriaMetrics/issues/10985
https://github.com/VictoriaMetrics/VictoriaMetrics/issues/10423.
2026-06-02 14:40:15 +02:00
Alexander Frolov
2345b2b4ed vmui: clarify partial results warning text
Update the VMUI partial results warning to say `storage nodes` instead
of `vmstorage nodes`.

People operating VictoriaMetrics and people primarily using the UI are
often different audiences. UI users may not know what `vmstorage` means,
but `storage nodes` is easier to understand while preserving the meaning
of the warning.
2026-06-02 14:39:13 +02:00
Hui Wang
b6edf40198 app/vmalert: support match[] parameter in /api/v1/rules and /api/v1/alerts APIs
If multiple match[] parameters are passed, rules that match any of the
provided label selector sets are returned.
For `/api/v1/rules`, the matching is performed against the labels
defined in each rule configuration;
For `/api/v1/alerts`, the matching is performed against the labels
attached in each alert.

The current `search` in [VMUI
alerting](https://play.victoriametrics.com/select/0/prometheus/graph/#/rules)
can only filter by group and rule name. The label filtering in the
vmalert UI is implemented on the frontend and matches exact label
content rather than label selectors. Both UI could be improved after
support this parameter.

fixes https://github.com/VictoriaMetrics/VictoriaMetrics/issues/11020
2026-06-02 14:38:36 +02:00
Nikolay
b220066049 lib/prompb: properly reuse memory for native histogram parsing
Previously, bucket spans were incorrectly re-used due to missing `clear`
call.
It could produce unexpected results with span bucket value from
previously parsed histogram.
 In addition, if timeseries had a multiple data values for the same
 histogram, metric name could add unexpected `bucket` prefix multiple
 times, producing `_bucket_bucket` metric names.

  This commit properly clears reused memory.

 Fixes https://github.com/VictoriaMetrics/VictoriaMetrics/issues/11041
2026-06-02 14:36:58 +02:00
Aliaksandr Valialkin
9ea1770ba4 app/vmctl: mention --vm-significant-figures option in the description for the --vm-round-digits option
These options are related, so it is better from the discoveribility PoV to cross-mention them.
2026-06-01 22:18:11 +02:00
Aliaksandr Valialkin
24d176fd2c app/vmagent: mention the -remoteWrite.significantFigures command-line flag in the description for the -remoteWrite.roundDigits flag
These flags are related, so it is better from the discoverity PoV to cross-mention them.
2026-06-01 22:17:16 +02:00
Hui Wang
e5c277237e app/vmalert: fix notifiers page in web UI appearing blank (#11036)
Fixes https://github.com/VictoriaMetrics/VictoriaMetrics/issues/11035.

There is no search box on the vmalert UI Notifiers page, but the notifier group containers have the `vm-group` CSS class applied. This class is hidden by default and only becomes visible after `filterRules()` adds the `vm-found` class. Since `filterRules()` is only called when a search box exists, it is never invoked on the Notifiers
page, leaving all content invisible.

The bug was introduced in v1.129.0 in 32ed45b672 (diff-e349265135dddcf960e58d2ada6be0fc18b76603f74c05107cbd1f348eb4d62b)

PR: https://github.com/VictoriaMetrics/VictoriaMetrics/pull/11036

Co-authored-by: Max Kotliar <mkotlyar@victoriametrics.com>
2026-06-01 18:25:52 +03:00
Immanuel Tikhonov
0656a0a702 app/vmui: persist "Disable deduplication" setting between page refreshes (#11004)
`Disable deduplication` was being saved into `TABLE_COMPACT`. Because of that the toggle did not survive reload, and it could also stomp the `Compact view` setting.

Repro:
1. Open `Raw Query` in vmui.
2. Turn on `Disable deduplication`.
3. Reload the page.
4. The toggle is off again.
5. If `Compact view` was set before, this toggle can mess with it too.

Fix:
- save the toggle under `REDUCE_MEM_USAGE`
- read it back on init
- add a regression test and changelog note

Checks:
- `npm test`
- `npm run typecheck`
- `npm run build`

PR https://github.com/VictoriaMetrics/VictoriaMetrics/pull/11004

Co-authored-by: Max Kotliar <mkotlyar@victoriametrics.com>
2026-06-01 16:18:50 +03:00
Andrii Chubatiuk
95977d272f app/vmui: chore. remove unused tab item color property (#11008)
Consistently use $color-primary for tabs and remove unused tab item
style customization. Also, this allows us to override the primary color for all page items at once. 

It could be useful in the Cloud.

PR https://github.com/VictoriaMetrics/VictoriaMetrics/pull/11008
2026-06-01 16:12:10 +03:00
Rudransh Shrivastava
2b9973b970 .github: enforce least privilege permissions for action jobs (#11037)
Scope permissions to jobs and set `permissions: {}` at workflow level.

Note: I added and removed permissions in some workflows. The added
permissions make previously implicit defaults explicit.

Signed-off-by: Rudransh Shrivastava <rudransh@victoriametrics.com>
2026-06-01 16:08:57 +03:00
Pablo (Tomas) Fernandez
6797776820 docs: fix broken anchor to changelog in operator docs (#11038)
There is a broken link/anchor to the operator changelog. This PR fixes
the link so it works.

This is linked to: https://github.com/VictoriaMetrics/vmdocs/issues/221

PR https://github.com/VictoriaMetrics/VictoriaMetrics/pull/11038
2026-06-01 16:07:54 +03:00
Stephan Burns
d2e554fb2b deployment/docker/rules: fix rule description indentation (#11039)
Fixes the indentation of the description line. Can cause some automatic
indentation tools to wrongly assume the indentation level, causing manifest
to not apply.

PR https://github.com/VictoriaMetrics/VictoriaMetrics/pull/11039

Signed-off-by: Stephan Burns <34520077+Sleuth56@users.noreply.github.com>
2026-06-01 16:06:27 +03:00
Ashwin Upadhyay
a4ec77fc02 app/vmselect: stop integrate() from extrapolating past series end (#10974)
### Problem

`integrate(metric[1h])` evaluated at a timestamp past the end of the
series keeps adding `last_value * (query_time - last_sample_time)` to
the running sum, treating the metric as if it continued at `last_value`
forever. For a constant series ending at `t_end`, querying
`integrate(metric[1h])` at `t_end + 30min` returns roughly `last_value *
3600` instead of the correct partial-window integral.

Reporter's repro is in the issue: constant value `0.11` integrated over
`[1h]`, queried past the series end, yields `~3600 * 0.11 = 396` instead
of tapering off as the window slides past the data.

The existing workaround `integrate((metric default 0)[1h])` produces the
right number but loses precision when the inner expression is not a bare
selector, and explodes memory at high cardinality (samples are
materialised for every step over the full lookbehind, even where the
metric never existed).

### Root cause

`rollupIntegrate` in `app/vmselect/promql/rollup.go` always executes:

```go
dt := float64(rfa.currTimestamp-prevTimestamp) / 1e3
sum += prevValue * dt
```

after the per-sample loop. `prevValue` is the last observed sample's
value, `prevTimestamp` is its timestamp. The block adds the rectangle
from the last sample to the query timestamp regardless of whether the
series has any continuation.

### Fix

Only apply the tail rectangle when the series has any sample past the
lookbehind window — `rfa.realNextValue` is non-NaN in that case. When it
is NaN, the series has effectively ended at `prevTimestamp`; there is no
area to accrue.

This mirrors how `rollupLifetime` bounds itself by actual data presence
rather than by the query timestamp.

### Trade-off

For live queries on healthy series with regular scrapes, `realNextValue`
is non-NaN for every point except (sometimes) the very last point at
\"now\", where the next scrape hasn't landed yet. In that one case the
integral at \"now\" will be smaller by at most one scrape-interval-worth
of `last_value` — equivalent to evaluating the query one scrape interval
earlier. That seems acceptable in exchange for fixing the much larger
overcount when querying past data end.

### Tests

- Updated `TestRollupFuncsNoWindow/integrate` — at `tEnd=160` the last
sample is at `ts=130` and there is no further data, so `realNextValue`
is NaN and the tail is dropped. Expected value changes from `1.36` to
`0.34`.
- Added `TestRollupFuncsNoWindow/integrate_past_series_end` — constant
`value=1` series across `[0, 3600s]`, query `integrate(metric[1h])`
across `[0, 10800s]` at `600s` step, assert integration past `t=7200s`
does not exceed `1` (it would be `~3600` under the old behaviour).
- Full `./app/vmselect/promql/...` suite green.

### Changelog

Added a `BUGFIX` bullet under `## tip` in
`docs/victoriametrics/changelog/CHANGELOG.md`.

Fixes https://github.com/VictoriaMetrics/VictoriaMetrics/issues/9474
PR https://github.com/VictoriaMetrics/VictoriaMetrics/pull/10974

---------

Signed-off-by: wtfashwin <ashwinupadhyay09@gmail.com>
Co-authored-by: Max Kotliar <mkotlyar@victoriametrics.com>
2026-06-01 15:34:42 +03:00
hagen1778
cbb3439526 docs/plyagrounds: update iximiuz links to nicer ones
The old links are still available.
The new links were added recently and are easier to remember.

Signed-off-by: hagen1778 <roman@victoriametrics.com>
2026-05-29 11:40:45 +02:00
Artem Fetishev
b67007a975 vmsingle: move storage flags to vmstorage/main.go (#11027)
- storage-only flags are moved to vmstorage/main.go
- vmsingle-specific initializations are moved to the very end of Init() func
- make force-merge goroutine return in case of error

These changes reduce diff with cluster vmstorage.

Signed-off-by: Artem Fetishev <rtm@victoriametrics.com>
2026-05-28 14:22:02 +02:00
Artem Fetishev
59610a66e1 apptest: refactor apps (#11026)
- Separate process creation and type instantiation
- Get the binary path either from an env var or use default path
- move apps that use legacy indexdb to a separate *_legacy.go files

Related to https://github.com/VictoriaMetrics/VictoriaMetrics/pull/10926

---------

Signed-off-by: Artem Fetishev <rtm@victoriametrics.com>
Signed-off-by: Artem Fetishev <149964189+rtm0@users.noreply.github.com>
Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
2026-05-27 21:22:09 +02:00
Roman Khavronenko
255365db50 docs: add HA section to stream aggregation (#11006)
Adds guidance on how to build horizontally scalable pipeline for stream
aggregation.

---------

Signed-off-by: hagen1778 <roman@victoriametrics.com>
Signed-off-by: Pablo (Tomas) Fernandez <46322567+TomFern@users.noreply.github.com>
Co-authored-by: Pablo (Tomas) Fernandez <46322567+TomFern@users.noreply.github.com>
Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
2026-05-27 16:11:49 +02:00
Roman Khavronenko
4065fce536 docs: simplify wording around -remoteWrite.shardByURL.* (#11005)
The main attempt is to reduce verbosity of the text.

---------

Signed-off-by: hagen1778 <roman@victoriametrics.com>
Signed-off-by: Roman Khavronenko <hagen1778@gmail.com>
Co-authored-by: Pablo (Tomas) Fernandez <46322567+TomFern@users.noreply.github.com>
2026-05-27 16:11:08 +02:00
Roman Khavronenko
08d4273d22 docs: add iximuz page to playgrounds (#10998)
* mention a new iximuz page for VM that contains all related resources
* mention playgrounds at quick start, so users can try it without
installing

Signed-off-by: hagen1778 <roman@victoriametrics.com>
2026-05-27 16:10:56 +02:00
Roman Khavronenko
ef83198eb1 docs: mention new stream aggregation skill (#10997)
See
76f8c13fc6

Signed-off-by: hagen1778 <roman@victoriametrics.com>
2026-05-27 16:10:41 +02:00
Roman Khavronenko
f61b632469 docs/vmalert: clarify available integrations (#10996)
* mention that vmalert has integrations with different datasource types
across doc
* add Integrations section to group everything related to connecting
vmalert to other systems
* change menu hierarchy to reflect the documentation changes
* mention that Grafana can be integrated with vmalert for displaying
rules

---------

Signed-off-by: hagen1778 <roman@victoriametrics.com>
Signed-off-by: Pablo (Tomas) Fernandez <46322567+TomFern@users.noreply.github.com>
Co-authored-by: Pablo (Tomas) Fernandez <46322567+TomFern@users.noreply.github.com>
Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
2026-05-27 16:10:23 +02:00
Roman Khavronenko
e4c7b557fd app/vmalert: clarify parser type in expr validation (#10995)
Before, having `prometheus` or `graphite` could have been confusing for
users. It was also inconsistent with `LogsQL` for `vlogs`. Also removed
extra spaces.

---------

Signed-off-by: hagen1778 <roman@victoriametrics.com>
2026-05-27 16:09:31 +02:00
Artem Fetishev
d0c6aa681f vmsingle: rename DataPath variable to storageDataPath (#11018)
This way the variable will match the corresponding name in cluster
branch which will reduce diff between branches.

Signed-off-by: Artem Fetishev <rtm@victoriametrics.com>
2026-05-26 19:17:05 +02:00
Hui Wang
c3525bf0bc lib/protoparser/opentelemetry: support disable scope and resource attributes label promotions
This commit adds four flags to allow managing label promotion for
resource attributes and OTel scope metadata, while staying compatible
with
[Prometheus](https://prometheus.io/docs/prometheus/latest/configuration/configuration/):
- `-opentelemetry.promoteScopeMetadata` - promote OTel scope metadata
(i.e. name, version, schema URL, and attributes) to metric labels.
- `opentelemetry.promoteAllResourceAttributes` - promote all resource
attributes to labels, except for the ones configured with
`-opentelemetry.ignoreResourceAttributes`.
- `opentelemetry.promoteResourceAttributes` - promote specific list of
resource attributes to labels. It cannot be configured simultaneously
with `opentelemetry.promoteAllResourceAttributes`.
- `opentelemetry.ignoreResourceAttributes` - which resource attributes
to ignore, can only be set when
`opentelemetry.promoteAllResourceAttributes` is true.

`-opentelemetry.promoteScopeMetadata` and
`opentelemetry.promoteAllResourceAttributes` are enabled by default in
order to preserve the current behavior.

fixes https://github.com/VictoriaMetrics/VictoriaMetrics/issues/10931.
2026-05-26 17:47:07 +02:00
Artem Fetishev
9e4bfebb74 changelog: revert previous release version
Signed-off-by: Artem Fetishev <rtm@victoriametrics.com>
2026-05-25 15:50:33 +02:00
Artem Fetishev
c2079a7880 docs: update flags with actual v1.144.0 binaries
Signed-off-by: Artem Fetishev <rtm@victoriametrics.com>
2026-05-25 14:12:55 +02:00
Artem Fetishev
3586757707 docs: bump version to v1.144.0
Signed-off-by: Artem Fetishev <rtm@victoriametrics.com>
2026-05-25 14:10:32 +02:00
Artem Fetishev
30cb4e831e deplyoment/docker: bump version to v1.144.0
Signed-off-by: Artem Fetishev <rtm@victoriametrics.com>
2026-05-25 14:09:16 +02:00
Artem Fetishev
ecfae87e4d docs: forward port LTS v1.122.23 changelog to upstream
Signed-off-by: Artem Fetishev <rtm@victoriametrics.com>
2026-05-25 13:32:58 +02:00
Artem Fetishev
bff02a6284 docs: forward port LTS v1.136.10 changelog to upstream
Signed-off-by: Artem Fetishev <rtm@victoriametrics.com>
2026-05-25 13:32:14 +02:00
Artem Fetishev
5e2bdf8220 Bump golang.org/x/net to v0.55.0
This fixes https://pkg.go.dev/vuln/GO-2026-5026

Signed-off-by: Artem Fetishev <rtm@victoriametrics.com>
2026-05-25 12:31:08 +02:00
Artem Fetishev
d82ad68f60 docs/changelog: cut release v1.144.0
Signed-off-by: Artem Fetishev <rtm@victoriametrics.com>
2026-05-22 17:05:50 +02:00
Artem Fetishev
dcbd8ef721 app/vmselect: run make vmui-update
Signed-off-by: Artem Fetishev <rtm@victoriametrics.com>
2026-05-22 17:01:56 +02:00
JAYICE
886c7762eb app/vmui: add timestamp and value column for csv data exported from table view (#10979)
Fixes https://github.com/VictoriaMetrics/VictoriaMetrics/issues/10975

before:
<img width="342" height="67" alt="image"
src="https://github.com/user-attachments/assets/a972857b-6a6c-4ec3-86fb-371b72a4d6d9"
/>

after:
<img width="426" height="115" alt="image"
src="https://github.com/user-attachments/assets/ddb540ed-5c1b-4431-a5c5-3576292dd21e"
/>

---------

Signed-off-by: JAYICE <1185430411@qq.com>
Co-authored-by: Max Kotliar <mkotlyar@victoriametrics.com>
2026-05-22 15:35:19 +03:00
Roman Khavronenko
d3006b25e6 app/vmui: display null/NaN/staleNaN values on RawQuery chart (#10986)
`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 chart could improve debugging experience.

Screenshots: 
<img width="1487" height="833" alt="image"
src="https://github.com/user-attachments/assets/2c80cd52-9d37-41f7-ad73-a48335b8aef2"
/>

Decisions:
1. Since null value can't be mapped on Y axis - it is placed on the very
bottom as `X` sign. It is ok to have it placed on top or as a dotted
line
2. It has a tooltip explaining what it is
3. Its value is not accounted in cumulative min/max/avg tolltip of the
whole time series on the graph
4. Null value can't exactly state it is a stale marker - such a value
can come from many places

Values to import as example dataset:
```
{"metric":{"__name__":"cpu_usage","job":"test","instance":"127.0.0.1:8080","cpu":"1","exported_instance":"foo","exported_job":"foo"},"values":[null,0.2,0.2,null,0.2,null,0.2,0.2,null,0.2,null,0.2,null,0.2,null,0.2,null,0.2,null,0.2,null,0.2,null,0.2,0.2,0.2,0.2,null,0.2,0.2,null,0.2,0.2,null,0.2,0.2,null,0.2,null,0.2,null,0.2,null,0.2,null,0.2,null,0.2,0.2],"timestamps":[1779362323611,1779362323617,1779362333617,1779362343610,1779362343617,1779362353610,1779362353617,1779362363617,1779362373610,1779362373617,1779362383610,1779362383617,1779362393610,1779362393617,1779362403610,1779362403617,1779362413610,1779362413617,1779362423610,1779362423617,1779362433610,1779362433617,1779362443610,1779362443617,1779362453617,1779362463617,1779362473617,1779362483610,1779362483617,1779362493617,1779362503610,1779362503617,1779362513617,1779362523610,1779362523617,1779362533617,1779362543609,1779362543617,1779362553609,1779362553617,1779362563609,1779362563617,1779362573609,1779362573617,1779362583609,1779362583617,1779362593609,1779362593617,1779362603617]}
```
------------

Feature like that would significantly help in debugging issues like
https://github.com/VictoriaMetrics/VictoriaMetrics/issues/10893

@Loori-R I am sorry, but this PR is rather a PoC - it was mostly
generated with AI help.

---------

Signed-off-by: hagen1778 <roman@victoriametrics.com>
Co-authored-by: Max Kotliar <mkotlyar@victoriametrics.com>
2026-05-22 15:29:31 +03:00
andriibeee
c41e967ee1 vmstorage: vmselect RPC server must respect -futureRetention (#10881)
Flag `-denyQueriesOutsideRetention` now also rejects queries past future
retention limit. Default behavior is unchanged.

Note that HTTP error code has been changed from 503 to 400.

Fixes https://github.com/VictoriaMetrics/VictoriaMetrics/issues/10879.

---------

Signed-off-by: Andrii Beskomornyi <andriibeee@gmail.com>
Co-authored-by: Artem Fetishev <rtm@victoriametrics.com>
2026-05-22 13:00:21 +02:00
andriibeee
0c9a011e0a lib/timeutil: handle int64 overflow in relative duration parsing (#10883)
Fix `int64` overflow in `ParseTimeAt()` in `lib/timeutil/time.go` when
converting time expressed as string into nanoseconds.

Fixes https://github.com/VictoriaMetrics/VictoriaMetrics/issues/10880.

---------

Signed-off-by: Andrii Beskomornyi <andriibeee@gmail.com>
Co-authored-by: Artem Fetishev <rtm@victoriametrics.com>
2026-05-22 12:17:04 +02:00
Vin :)
8d82977303 feat(vmauth): extend JWT claim-based templating (#10930)
This PR extends `vmauth` JWT templating support by adding individual `{{.MetricsAccountID}}` and `{{.MetricsProjectID}}` placeholders derived from `vm_access` claims.

Example:
```yaml
headers:
  - "AccountID: {{.MetricsAccountID}}"
  - "ProjectID: {{.MetricsProjectID}}"
url_prefix: "http://vmcluster:8480/insert/prometheus"
```

Fixes https://github.com/VictoriaMetrics/VictoriaMetrics/issues/10927
PR https://github.com/VictoriaMetrics/VictoriaMetrics/pull/10930

---------

Signed-off-by: Max Kotliar <kotlyar.maksim@gmail.com>
Co-authored-by: Max Kotliar <mkotlyar@victoriametrics.com>
Co-authored-by: Max Kotliar <kotlyar.maksim@gmail.com>
2026-05-22 10:38:20 +03:00
JAYICE
560c5bb32a app/vminsert: switch slowness-based rerouting to use p90 saturation (#10901)
Rerouting from a slow storage node is triggered only when the cluster's p90 saturation is below 0.6 (meaning the cluster has spare capacity) and the slow node's saturation exceeds the p90 by more than 20%.

This fixes a rerouting storm that could occur under high cluster load: when
all vmstorage saturations are close to each other, different vminsert nodes could disagree on which vmstorage is "the slowest" (since each calculates saturation independently), causing rerouting from many nodes simultaneously and making degradation worse. In a known customer case, 30% of vmstorage nodes were being rerouted from at the same time.

Previously, the median was used as the reference point with a 0.80 cutoff, which allowed rerouting even when the cluster was significantly loaded.

Fixes https://github.com/VictoriaMetrics/VictoriaMetrics/issues/10876
PR https://github.com/VictoriaMetrics/VictoriaMetrics/pull/10901
2026-05-22 10:32:10 +03:00
Max Kotliar
758c6587cc lib/persistentqueue: skip zero-length blocks as corrupt data in readBlock (#10984)
Zero-length blocks indicate file corruption. Valid blocks should always
have a non-zero length. Previously, PR
https://github.com/VictoriaMetrics/VictoriaMetrics/pull/6241 worked
around this at the client level by [silently skipping such
blocks](b1dea965aa/app/vmagent/remotewrite/client.go (L314)).

Move the check into readBlock, consistent with how other corrupted
headers are handled there, and remove the now-redundant empty-block
guards from the client.

Follow-up on discussion
https://github.com/VictoriaMetrics/VictoriaMetrics/pull/10932#discussion_r3264924234
2026-05-22 10:18:46 +03:00
Roman Khavronenko
97af1731a4 docs: update links leading to moved sections (#10989)
This changes old links that were pointing to the documentation section
that were moved.

Signed-off-by: hagen1778 <roman@victoriametrics.com>
2026-05-22 10:16:06 +03:00
Max Kotliar
a6b867dab8 docs: showcase Developer Goals and Contribution on top left menu (#10990)
The links are the same and BC.

Address
https://github.com/VictoriaMetrics/VictoriaMetrics/issues/10608#issuecomment-4040039785

<img width="1512" height="872" alt="Screenshot 2026-05-21 at 18 25 22"
src="https://github.com/user-attachments/assets/9b04fca0-02ca-4852-bcf8-c8fec89d806c"
/>
2026-05-22 10:13:33 +03:00
dependabot[bot]
776720e5d7 build(deps): bump github/codeql-action from 4.35.2 to 4.35.3 (#10992)
Bumps [github/codeql-action](https://github.com/github/codeql-action)
from 4.35.2 to 4.35.3.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/github/codeql-action/releases">github/codeql-action's
releases</a>.</em></p>
<blockquote>
<h2>v4.35.3</h2>
<ul>
<li><em>Upcoming breaking change</em>: Add a deprecation warning for
customers using CodeQL version 2.19.3 and earlier. These versions of
CodeQL were discontinued on 9 April 2026 alongside GitHub Enterprise
Server 3.15, and will be unsupported by the next minor release of the
CodeQL Action. <a
href="https://redirect.github.com/github/codeql-action/pull/3837">#3837</a></li>
<li>Configurations for private registries that use Cloudsmith or GCP
OIDC are now accepted. <a
href="https://redirect.github.com/github/codeql-action/pull/3850">#3850</a></li>
<li>Best-effort connection tests for private registries now use
<code>GET</code> requests instead of <code>HEAD</code> for better
compatibility with various registry implementations. For NuGet feeds,
the test is now always performed against the service index. <a
href="https://redirect.github.com/github/codeql-action/pull/3853">#3853</a></li>
<li>Fixed a bug where two diagnostics produced within the same
millisecond could overwrite each other on disk, causing one of them to
be lost. <a
href="https://redirect.github.com/github/codeql-action/pull/3852">#3852</a></li>
<li>Update default CodeQL bundle version to <a
href="https://github.com/github/codeql-action/releases/tag/codeql-bundle-v2.25.3">2.25.3</a>.
<a
href="https://redirect.github.com/github/codeql-action/pull/3865">#3865</a></li>
</ul>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/github/codeql-action/blob/main/CHANGELOG.md">github/codeql-action's
changelog</a>.</em></p>
<blockquote>
<h1>CodeQL Action Changelog</h1>
<p>See the <a
href="https://github.com/github/codeql-action/releases">releases
page</a> for the relevant changes to the CodeQL CLI and language
packs.</p>
<h2>[UNRELEASED]</h2>
<ul>
<li><em>Breaking change</em>: Bump the minimum required CodeQL bundle
version to 2.19.4. <a
href="https://redirect.github.com/github/codeql-action/pull/3894">#3894</a></li>
<li>Add support for SHA-256 Git object IDs. <a
href="https://redirect.github.com/github/codeql-action/pull/3893">#3893</a></li>
</ul>
<h2>4.35.5 - 15 May 2026</h2>
<ul>
<li>We have improved how the JavaScript bundles for the CodeQL Action
are generated to avoid duplication across bundles and reduce the size of
the repository by around 70%. This should have no effect on the runtime
behaviour of the CodeQL Action. <a
href="https://redirect.github.com/github/codeql-action/pull/3899">#3899</a></li>
<li>For performance and accuracy reasons, <a
href="https://redirect.github.com/github/roadmap/issues/1158">improved
incremental analysis</a> will now only be enabled on a pull request when
diff-informed analysis is also enabled for that run. If diff-informed
analysis is unavailable (for example, because the PR diff ranges could
not be computed), the action will fall back to a full analysis. <a
href="https://redirect.github.com/github/codeql-action/pull/3791">#3791</a></li>
<li>If multiple inputs are provided for the GitHub-internal
<code>analysis-kinds</code> input, only <code>code-scanning</code> will
be enabled. The <code>analysis-kinds</code> input is experimental, for
GitHub-internal use only, and may change without notice at any time. <a
href="https://redirect.github.com/github/codeql-action/pull/3892">#3892</a></li>
<li>Added an experimental change which, when running a Code Scanning
analysis for a PR with <a
href="https://redirect.github.com/github/roadmap/issues/1158">improved
incremental analysis</a> enabled, prefers CodeQL CLI versions that have
a cached overlay-base database for the configured languages. This speeds
up analysis for a repository when there is not yet a cached overlay-base
database for the latest CLI version. We expect to roll this change out
to everyone in May. <a
href="https://redirect.github.com/github/codeql-action/pull/3880">#3880</a></li>
</ul>
<h2>4.35.4 - 07 May 2026</h2>
<ul>
<li>Update default CodeQL bundle version to <a
href="https://github.com/github/codeql-action/releases/tag/codeql-bundle-v2.25.4">2.25.4</a>.
<a
href="https://redirect.github.com/github/codeql-action/pull/3881">#3881</a></li>
</ul>
<h2>4.35.3 - 01 May 2026</h2>
<ul>
<li><em>Upcoming breaking change</em>: Add a deprecation warning for
customers using CodeQL version 2.19.3 and earlier. These versions of
CodeQL were discontinued on 9 April 2026 alongside GitHub Enterprise
Server 3.15, and will be unsupported by the next minor release of the
CodeQL Action. <a
href="https://redirect.github.com/github/codeql-action/pull/3837">#3837</a></li>
<li>Configurations for private registries that use Cloudsmith or GCP
OIDC are now accepted. <a
href="https://redirect.github.com/github/codeql-action/pull/3850">#3850</a></li>
<li>Best-effort connection tests for private registries now use
<code>GET</code> requests instead of <code>HEAD</code> for better
compatibility with various registry implementations. For NuGet feeds,
the test is now always performed against the service index. <a
href="https://redirect.github.com/github/codeql-action/pull/3853">#3853</a></li>
<li>Fixed a bug where two diagnostics produced within the same
millisecond could overwrite each other on disk, causing one of them to
be lost. <a
href="https://redirect.github.com/github/codeql-action/pull/3852">#3852</a></li>
<li>Update default CodeQL bundle version to <a
href="https://github.com/github/codeql-action/releases/tag/codeql-bundle-v2.25.3">2.25.3</a>.
<a
href="https://redirect.github.com/github/codeql-action/pull/3865">#3865</a></li>
</ul>
<h2>4.35.2 - 15 Apr 2026</h2>
<ul>
<li>The undocumented TRAP cache cleanup feature that could be enabled
using the <code>CODEQL_ACTION_CLEANUP_TRAP_CACHES</code> environment
variable is deprecated and will be removed in May 2026. If you are
affected by this, we recommend disabling TRAP caching by passing the
<code>trap-caching: false</code> input to the <code>init</code> Action.
<a
href="https://redirect.github.com/github/codeql-action/pull/3795">#3795</a></li>
<li>The Git version 2.36.0 requirement for improved incremental analysis
now only applies to repositories that contain submodules. <a
href="https://redirect.github.com/github/codeql-action/pull/3789">#3789</a></li>
<li>Python analysis on GHES no longer extracts the standard library,
relying instead on models of the standard library. This should result in
significantly faster extraction and analysis times, while the effect on
alerts should be minimal. <a
href="https://redirect.github.com/github/codeql-action/pull/3794">#3794</a></li>
<li>Fixed a bug in the validation of OIDC configurations for private
registries that was added in CodeQL Action 4.33.0 / 3.33.0. <a
href="https://redirect.github.com/github/codeql-action/pull/3807">#3807</a></li>
<li>Update default CodeQL bundle version to <a
href="https://github.com/github/codeql-action/releases/tag/codeql-bundle-v2.25.2">2.25.2</a>.
<a
href="https://redirect.github.com/github/codeql-action/pull/3823">#3823</a></li>
</ul>
<h2>4.35.1 - 27 Mar 2026</h2>
<ul>
<li>Fix incorrect minimum required Git version for <a
href="https://redirect.github.com/github/roadmap/issues/1158">improved
incremental analysis</a>: it should have been 2.36.0, not 2.11.0. <a
href="https://redirect.github.com/github/codeql-action/pull/3781">#3781</a></li>
</ul>
<h2>4.35.0 - 27 Mar 2026</h2>
<ul>
<li>Reduced the minimum Git version required for <a
href="https://redirect.github.com/github/roadmap/issues/1158">improved
incremental analysis</a> from 2.38.0 to 2.11.0. <a
href="https://redirect.github.com/github/codeql-action/pull/3767">#3767</a></li>
<li>Update default CodeQL bundle version to <a
href="https://github.com/github/codeql-action/releases/tag/codeql-bundle-v2.25.1">2.25.1</a>.
<a
href="https://redirect.github.com/github/codeql-action/pull/3773">#3773</a></li>
</ul>
<h2>4.34.1 - 20 Mar 2026</h2>
<ul>
<li>Downgrade default CodeQL bundle version to <a
href="https://github.com/github/codeql-action/releases/tag/codeql-bundle-v2.24.3">2.24.3</a>
due to issues with a small percentage of Actions and JavaScript
analyses. <a
href="https://redirect.github.com/github/codeql-action/pull/3762">#3762</a></li>
</ul>
<h2>4.34.0 - 20 Mar 2026</h2>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="e46ed2cbd0"><code>e46ed2c</code></a>
Merge pull request <a
href="https://redirect.github.com/github/codeql-action/issues/3867">#3867</a>
from github/update-v4.35.3-8c6e48dbe</li>
<li><a
href="b73d1d1634"><code>b73d1d1</code></a>
Add changelog entry for <a
href="https://redirect.github.com/github/codeql-action/issues/3853">#3853</a></li>
<li><a
href="24e0bb00a9"><code>24e0bb0</code></a>
Reorder changelog entries</li>
<li><a
href="ec298daba7"><code>ec298da</code></a>
Update changelog for v4.35.3</li>
<li><a
href="8c6e48dbe0"><code>8c6e48d</code></a>
Merge pull request <a
href="https://redirect.github.com/github/codeql-action/issues/3865">#3865</a>
from github/update-bundle/codeql-bundle-v2.25.3</li>
<li><a
href="719098349e"><code>7190983</code></a>
Add changelog note</li>
<li><a
href="2bb209555a"><code>2bb2095</code></a>
Update default bundle to codeql-bundle-v2.25.3</li>
<li><a
href="7851e55dc3"><code>7851e55</code></a>
Merge pull request <a
href="https://redirect.github.com/github/codeql-action/issues/3850">#3850</a>
from github/mbg/private-registry/cloudsmith-gcp</li>
<li><a
href="262a15f6cf"><code>262a15f</code></a>
Add generic non-printable chars test for OIDC configs</li>
<li><a
href="a6109b1c07"><code>a6109b1</code></a>
Merge pull request <a
href="https://redirect.github.com/github/codeql-action/issues/3853">#3853</a>
from github/mbg/start-proxy/improved-checks</li>
<li>Additional commits viewable in <a
href="95e58e9a2c...e46ed2cbd0">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=github/codeql-action&package-manager=github_actions&previous-version=4.35.2&new-version=4.35.3)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-22 10:12:26 +03:00
Max Kotliar
734efe8f7e Makefile: follow-up on prev commit 37a662b7
The commit adds caching for GOCACHE and GOMODCACHE directory. Also
caches govulncheck binary.

Follow up on
37a662b7e7
2026-05-21 16:57:07 +03:00
Max Kotliar
37a662b7e7 Makefile: add govulncheck-docker target (#10981)
govulncheck run locally uses the local Go version, which may differ from
the one used to build production binaries.

For example, local go1.26.2 may report vulnerabilities already fixed in
go1.26.3 used by the builder.
In this case the command would report issues which has to be manually
triaged by a developer.

Add govulncheck-docker target that runs govulncheck inside a Docker
container using the same version used to build production binaries.

The command will be used in release scripts and could be used manually.

PR https://github.com/VictoriaMetrics/VictoriaMetrics/pull/10981
2026-05-21 15:58:30 +03:00
Yury Moladau
e303965b6c app/vmui: show exact values in chart tooltip (#10988)
Preserve exact series values in graph tooltips instead of rounding them by significant digits. This prevents large values from being displayed as rounded approximations while keeping axis formatting unchanged.

Fixes https://github.com/VictoriaMetrics/VictoriaMetrics/issues/10952
PR: https://github.com/VictoriaMetrics/VictoriaMetrics/pull/10988

| Before | After |
|---|---|
| <img width="572" height="393" alt="image"
src="https://github.com/user-attachments/assets/b16637a4-e51d-48bc-9684-7e8ade99a509"
/> | <img width="572" height="393" alt="image"
src="https://github.com/user-attachments/assets/aa3a3411-ca5d-484e-aa64-e71a8ef4c485"
/> |

Signed-off-by: Yury Molodov <yurymolodov@gmail.com>
2026-05-21 15:47:16 +03:00
Zhu Jiekun
69869d7d08 docs: add retention related content to quick start guide #Productionization section (#10966)
The `--retentionPeriod` flag is missing in several quick start guide
examples. This may cause users to overlook the parameter and incorrectly
believe data will be stored permanently until manually deleted.

But the quick start guide is not intended for production deployment, and
we have dedicated section `#Productionization` there already. We should
mention `--retentionPeriod` flag in this section.

This change could be helpful to
https://github.com/VictoriaMetrics/VictoriaMetrics/issues/249#issuecomment-4468637250

---------

Signed-off-by: Zhu Jiekun <jiekun@victoriametrics.com>
Co-authored-by: Roman Khavronenko <roman@victoriametrics.com>
2026-05-21 14:55:26 +03:00
Roman Khavronenko
3160979048 docs: add section explaining force flush (#10977)
The section was added as result of user's feedback. It documents
beahvior of this handler and expectations of its use.

---------

Signed-off-by: hagen1778 <roman@victoriametrics.com>
Signed-off-by: Max Kotliar <kotlyar.maksim@gmail.com>
Co-authored-by: Max Kotliar <mkotlyar@victoriametrics.com>
Co-authored-by: Pablo Fernandez <46322567+TomFern@users.noreply.github.com>
2026-05-21 14:31:37 +03:00
Hui Wang
a45ec9a6a0 docs/vmalert: mention how to access a label with dot in its name in vmalert templating (#10980)
Fixes https://github.com/VictoriaMetrics/VictoriaMetrics/issues/10911
2026-05-21 14:29:04 +03:00
Fred Navruzov
af595acc73 docs/vmanomaly: fix typos/duplicates (#10978)
Small not-urgent fixes for typos/duplicates in docs and apptest
2026-05-21 14:27:18 +03:00
Nikolay
b1dea965aa app/vminsert: add clusternative.tls configuration flags
Previously, it was not possible to configure mTLS between multi-level
vminserts. But vmselect supported such feature. It was a configuration
discrepancy.

This commit adds the same flags to the vminsert.

Fixes https://github.com/VictoriaMetrics/VictoriaMetrics/issues/10958
2026-05-20 12:38:08 +02:00
Stephan Burns
df9750a968 docs/enterprise.md: add clarification for when license expires (#10956)
Many people are concerned about what happens when the license expires on
a VictoriaMetrics component. This PR aims to address those concerns.

PR https://github.com/VictoriaMetrics/VictoriaMetrics/pull/10956

---------

Signed-off-by: Stephan Burns <34520077+Sleuth56@users.noreply.github.com>
Signed-off-by: Pablo (Tomas) Fernandez <46322567+TomFern@users.noreply.github.com>
Co-authored-by: Pablo Fernandez <46322567+TomFern@users.noreply.github.com>
Co-authored-by: Max Kotliar <mkotlyar@victoriametrics.com>
2026-05-19 17:51:57 +03:00
Rudransh Shrivastava
bc9320aaf3 .github: pin actions to full-length commit SHAs (#10953)
Pin GitHub actions to their full-length commit SHAs.
Semver tags were updated to be more precise: e.g. `v7` to `v7.0.0`

---------

Signed-off-by: Rudransh Shrivastava <rudransh@victoriametrics.com>
2026-05-19 17:34:00 +03:00
Victoria Nduka
10b3f388dd docs/quick-start.md: fix punctuation and grammar in Quick-Start.md (#10957)
Fix punctuation and grammar in Quick-Start.md

PR https://github.com/VictoriaMetrics/VictoriaMetrics/pull/10957

Signed-off-by: Victoria Nduka <122698422+nwanduka@users.noreply.github.com>
2026-05-19 15:00:56 +03:00
f41gh7
6d88370d78 app/vmselect: hide vmalert.proxyURL as secret flag
follow-up for cc45a139db

This commit registers vmselect and vminsert `vmalert.proxyURL` flags as a secret flag in the
same way as vmagent does.
2026-05-19 13:30:22 +02:00
f41gh7
548e6ef6bb app/vmalert: hide *.headers as secret flags
follow-up for 33d8e02ea8

This commit registers vmalert `*.headers` flags as a secret flags in the
same way as vmagent does.
2026-05-19 13:30:22 +02:00
Pablo (Tomas) Fernandez
a4278f77d5 docs/vmagent.md: grammar and proofread pass for docs; structurize multitenancy doc for vmagent (#10946)
This is a grammar pass and proofread for
https://github.com/VictoriaMetrics/VictoriaMetrics/pull/10943

---------

Signed-off-by: hagen1778 <roman@victoriametrics.com>
Signed-off-by: Pablo (Tomas) Fernandez <46322567+TomFern@users.noreply.github.com>
Co-authored-by: hagen1778 <roman@victoriametrics.com>
2026-05-19 14:24:26 +03:00
Phuong Le
cc45a139db vmagent/remotewrite: mark remoteWrite.proxyURL as secret
This change marks `-remoteWrite.proxyURL` as a secret flag in vmagent
remotewrite, similarly to other sensitive remote write flags.

Related PR https://github.com/VictoriaMetrics/VictoriaMetrics/pull/10971/
2026-05-19 13:20:41 +02:00
Max Kotliar
828a82aea2 app/vmagent: drain in-memory blocks to rw first during shutdown, fallback to persited queue after (#10932)
vmagent would try to flush in-memory blocks to rw for the duration of graceful shutdown (5 seconds) only
after falling back and storing them in the persisted queue.

Fixes https://github.com/VictoriaMetrics/VictoriaMetrics/issues/9996
PR https://github.com/VictoriaMetrics/VictoriaMetrics/pull/10932
2026-05-19 12:43:30 +03:00
Max Kotliar
f2bf5d82ce docs/changelog: add links to the issues\prs 2026-05-18 19:31:59 +03:00
Max Kotliar
bd98a1d2fa docs/changelog: fix link to the issue
follow-up on
e613c3fd6b
2026-05-18 19:22:53 +03:00
Max Kotliar
4a1ceccee4 app/vmagent: fix sharding correctness when disableOnDiskQueue is set (#10947)
When -remoteWrite.shardByURL is enabled, and one of the remote write
targets has -remoteWrite.disableOnDiskQueue set becomes blocked, samples
could be rerouted to other shards (see `getEligibleRemoteWriteCtxs` impl), breaking the sharding guarantee. Fix this by always using `rwctxsGlobal` in sharding mode.

Add a startup check that requires `-remoteWrite.disableOnDiskQueue` to be
configured uniformly across all targets when -remoteWrite.shardByURL` is enabled.

Fixes https://github.com/VictoriaMetrics/VictoriaMetrics/issues/10507
PR https://github.com/VictoriaMetrics/VictoriaMetrics/pull/10947
2026-05-18 14:46:24 +03:00
Max Kotliar
48a3eb0215 dashboards: zoom in the "CPU spent on GC" panel. (#10955)
The "CPU spent on GC" panel often shows sub-1% GC CPU usage, which is
barely visible with the current fixed Y-axis settings. Removing the
fixed scale allows Grafana to auto-adjust the Y axis and makes small
fluctuations easier to observe.

PR https://github.com/VictoriaMetrics/VictoriaMetrics/pull/10955

Before:
<img width="1499" height="477" alt="Screenshot 2026-05-14 at 18 35 14"
src="https://github.com/user-attachments/assets/8a78bea4-3dcd-4f26-a40e-304acbf68eb1"
/>

After:
<img width="1508" height="501" alt="Screenshot 2026-05-14 at 18 34 51"
src="https://github.com/user-attachments/assets/aefd2fbc-6b1c-42a9-b45d-e44bcd30be48"
/>
2026-05-18 14:39:13 +03:00
Immanuel Tikhonov
200c03416f chore: use %s instead of %w for error formatting in logger calls
Previously, errors in app/vmalert-tool and lib packages used the %w verb
in logger.Errorf calls, which is intended for wrapping errors via fmt.Errorf.
Using %w with the logger package does not wrap the error — instead, it prints
a malformed %!w(...) placeholder rather than the actual error message.

This commit replaces all affected occurrences of %w with %s to correctly
format and display errors.

Related PR: https://github.com/VictoriaMetrics/VictoriaMetrics/pull/10962
2026-05-18 13:13:51 +02:00
297 changed files with 13987 additions and 27165 deletions

View File

@@ -22,8 +22,7 @@ on:
- '!app/vmui/**'
- '.github/workflows/build.yml'
permissions:
contents: read
permissions: {}
concurrency:
cancel-in-progress: true
@@ -32,6 +31,8 @@ concurrency:
jobs:
build:
name: ${{ matrix.os }}-${{ matrix.arch }}
permissions:
contents: read
runs-on: ubuntu-latest
strategy:
fail-fast: false
@@ -63,11 +64,11 @@ jobs:
arch: amd64
steps:
- name: Code checkout
uses: actions/checkout@v6
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Setup Go
id: go
uses: actions/setup-go@v6
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
cache-dependency-path: |
go.sum

View File

@@ -5,11 +5,15 @@ on:
paths:
- "docs/victoriametrics/changelog/CHANGELOG.md"
permissions: {}
jobs:
tip-lint:
permissions:
contents: read
runs-on: 'ubuntu-latest'
steps:
- uses: 'actions/checkout@v6'
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
# needed for proper diff
fetch-depth: 0

View File

@@ -3,12 +3,16 @@ name: check-commit-signed
on:
pull_request:
permissions: {}
jobs:
check-commit-signed:
permissions:
contents: read
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v6
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0 # we need full history for commit verification

View File

@@ -6,20 +6,22 @@ on:
pull_request:
paths:
- 'vendor'
permissions:
contents: read
permissions: {}
jobs:
build:
name: Build
permissions:
contents: read
runs-on: ubuntu-latest
steps:
- name: Code checkout
uses: actions/checkout@master
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Setup Go
id: go
uses: actions/setup-go@v6
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
go-version-file: 'go.mod'
cache: false
@@ -27,7 +29,7 @@ jobs:
- run: go version
- name: Cache Go artifacts
uses: actions/cache@v5
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: |
~/.cache/go-build

View File

@@ -18,6 +18,8 @@ concurrency:
cancel-in-progress: true
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
permissions: {}
jobs:
analyze:
name: Analyze
@@ -29,18 +31,18 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up Go
id: go
uses: actions/setup-go@v6
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
cache: false
go-version-file: 'go.mod'
- run: go version
- name: Cache Go artifacts
uses: actions/cache@v5
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: |
~/.cache/go-build
@@ -50,14 +52,14 @@ jobs:
restore-keys: go-artifacts-${{ runner.os }}-codeql-analyze-${{ steps.go.outputs.go-version }}-
- name: Initialize CodeQL
uses: github/codeql-action/init@v4.35.2
uses: github/codeql-action/init@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3
with:
languages: go
- name: Autobuild
uses: github/codeql-action/autobuild@v4.35.2
uses: github/codeql-action/autobuild@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v4.35.2
uses: github/codeql-action/analyze@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3
with:
category: 'language:go'

View File

@@ -7,28 +7,30 @@ on:
- 'docs/**'
- '.github/workflows/docs.yaml'
workflow_dispatch: {}
permissions:
contents: read # This is required for actions/checkout and to commit back image update
deployments: write
permissions: {}
jobs:
build:
name: Build
permissions:
contents: read
runs-on: ubuntu-latest
steps:
- name: Code checkout
uses: actions/checkout@v6
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
path: __vm
- name: Checkout private code
uses: actions/checkout@v6
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
repository: VictoriaMetrics/vmdocs
token: ${{ secrets.VM_BOT_GH_TOKEN }}
path: __vm-docs
- name: Import GPG key
uses: crazy-max/ghaction-import-gpg@v7
uses: crazy-max/ghaction-import-gpg@2dc316deee8e90f13e1a351ab510b4d5bc0c82cd # v7.0.0
id: import-gpg
with:
gpg_private_key: ${{ secrets.VM_BOT_GPG_PRIVATE_KEY }}

View File

@@ -18,8 +18,7 @@ on:
- 'go.*'
- '.github/workflows/main.yml'
permissions:
contents: read
permissions: {}
concurrency:
cancel-in-progress: true
@@ -29,14 +28,16 @@ concurrency:
jobs:
lint:
name: lint
permissions:
contents: read
runs-on: ubuntu-latest
steps:
- name: Code checkout
uses: actions/checkout@v6
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Setup Go
id: go
uses: actions/setup-go@v6
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
cache-dependency-path: |
go.sum
@@ -47,7 +48,7 @@ jobs:
- run: go version
- name: Cache golangci-lint
uses: actions/cache@v5
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: |
~/.cache/golangci-lint
@@ -61,6 +62,8 @@ jobs:
unit:
name: unit
permissions:
contents: read
runs-on: ubuntu-latest
strategy:
@@ -72,11 +75,11 @@ jobs:
steps:
- name: Code checkout
uses: actions/checkout@v6
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Setup Go
id: go
uses: actions/setup-go@v6
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
cache-dependency-path: |
go.sum
@@ -90,15 +93,17 @@ jobs:
apptest:
name: apptest
permissions:
contents: read
runs-on: apptest
steps:
- name: Code checkout
uses: actions/checkout@v6
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Setup Go
id: go
uses: actions/setup-go@v6
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
cache-dependency-path: |
go.sum

View File

@@ -16,11 +16,7 @@ on:
- 'app/vmui/packages/vmui/**'
- '.github/workflows/vmui.yml'
permissions:
contents: read
packages: read
pull-requests: read
checks: write
permissions: {}
concurrency:
cancel-in-progress: true
@@ -29,14 +25,18 @@ concurrency:
jobs:
vmui-checks:
name: VMUI Checks (lint, test, typecheck)
permissions:
checks: write
contents: read
pull-requests: read
runs-on: ubuntu-latest
steps:
- name: Code checkout
uses: actions/checkout@v6
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Cache node_modules
id: cache
uses: actions/cache@v5
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: app/vmui/packages/vmui/node_modules
key: vmui-deps-${{ runner.os }}-${{ hashFiles('app/vmui/packages/vmui/package-lock.json', 'app/vmui/Dockerfile-build') }}
@@ -69,7 +69,7 @@ jobs:
VMUI_SKIP_INSTALL: true
- name: Annotate Code Linting Results
uses: ataylorme/eslint-annotate-action@v3
uses: ataylorme/eslint-annotate-action@d57a1193d4c59cbfbf3f86c271f42612f9dbd9e9 # 3.0.0
with:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
report-json: app/vmui/packages/vmui/vmui-lint-report.json

View File

@@ -485,8 +485,8 @@ apptest-legacy: victoria-metrics-race vmbackup-race vmrestore-race
curl --output-dir /tmp -LO $${URL}/$${VMSINGLE} && tar xzf /tmp/$${VMSINGLE} -C $${DIR} && \
curl --output-dir /tmp -LO $${URL}/$${VMCLUSTER} && tar xzf /tmp/$${VMCLUSTER} -C $${DIR} \
); \
VM_LEGACY_VMSINGLE_PATH=$${DIR}/victoria-metrics-prod \
VM_LEGACY_VMSTORAGE_PATH=$${DIR}/vmstorage-prod \
VMSINGLE_V1_132_0_PATH=$${DIR}/victoria-metrics-prod \
VMSTORAGE_V1_132_0_PATH=$${DIR}/vmstorage-prod \
go test ./apptest/tests -run="^TestLegacySingle.*"
benchmark:
@@ -535,6 +535,15 @@ remove-golangci-lint:
govulncheck: install-govulncheck
govulncheck ./...
govulncheck-docker:
docker run -w $(PWD) -v $(PWD):$(PWD) \
-v govulncheck-gomod-cache:/root/go/pkg/mod \
-v govulncheck-gobuild-cache:/root/.cache/go-build \
-v govulncheck-go-bin:/root/go/bin \
--env="GOCACHE=/root/.cache/go-build" \
--env="GOMODCACHE=/root/go/pkg/mod" \
"$(GO_BUILDER_IMAGE)" /bin/sh -c "which govulncheck || go install golang.org/x/vuln/cmd/govulncheck@latest && govulncheck ./..."
install-govulncheck:
which govulncheck || go install golang.org/x/vuln/cmd/govulncheck@latest

View File

@@ -22,7 +22,6 @@ import (
"github.com/VictoriaMetrics/VictoriaMetrics/lib/procutil"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promscrape"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/pushmetrics"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/storage"
)
var (
@@ -30,21 +29,11 @@ var (
useProxyProtocol = flagutil.NewArrayBool("httpListenAddr.useProxyProtocol", "Whether to use proxy protocol for connections accepted at the corresponding -httpListenAddr . "+
"See https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt . "+
"With enabled proxy protocol http server cannot serve regular /metrics endpoint. Use -pushmetrics.url for metrics pushing")
minScrapeInterval = flag.Duration("dedup.minScrapeInterval", 0, "Leave only the last sample in every time series per each discrete interval "+
"equal to -dedup.minScrapeInterval > 0. See also -streamAggr.dedupInterval and https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/#deduplication")
dryRun = flag.Bool("dryRun", false, "Whether to check config files without running VictoriaMetrics. The following config files are checked: "+
"-promscrape.config, -relabelConfig and -streamAggr.config. Unknown config entries aren't allowed in -promscrape.config by default. "+
"This can be changed with -promscrape.config.strictParse=false command-line flag")
inmemoryDataFlushInterval = flag.Duration("inmemoryDataFlushInterval", 5*time.Second, "The interval for guaranteed saving of in-memory data to disk. "+
"The saved data survives unclean shutdowns such as OOM crash, hardware reset, SIGKILL, etc. "+
"Bigger intervals may help increase the lifetime of flash storage with limited write cycles (e.g. Raspberry PI). "+
"Smaller intervals increase disk IO load. Minimum supported value is 1s")
maxIngestionRate = flag.Int("maxIngestionRate", 0, "The maximum number of samples vmsingle can receive per second. Data ingestion is paused when the limit is exceeded. "+
"By default there are no limits on samples ingestion rate.")
finalDedupScheduleInterval = flag.Duration("storage.finalDedupScheduleCheckInterval", time.Hour, "The interval for checking when final deduplication process should be started."+
"Storage unconditionally adds 25% jitter to the interval value on each check evaluation."+
" Changing the interval to the bigger values may delay downsampling, deduplication for historical data."+
" See also https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/#deduplication")
)
func main() {
@@ -87,12 +76,6 @@ func main() {
}
logger.Infof("starting VictoriaMetrics at %q...", listenAddrs)
startTime := time.Now()
storage.SetDedupInterval(*minScrapeInterval)
storage.SetDataFlushInterval(*inmemoryDataFlushInterval)
if *finalDedupScheduleInterval < time.Hour {
logger.Fatalf("-dedup.finalDedupScheduleCheckInterval cannot be smaller than 1 hour; got %s", *finalDedupScheduleInterval)
}
storage.SetFinalDedupScheduleInterval(*finalDedupScheduleInterval)
vmstorage.Init(promql.ResetRollupResultCacheIfNeeded)
vmselect.Init()
vminsertcommon.StartIngestionRateLimiter(*maxIngestionRate)

View File

@@ -118,6 +118,7 @@ func main() {
remotewrite.InitSecretFlags()
buildinfo.Init()
logger.Init()
opentelemetry.Init()
timeserieslimits.Init(*maxLabelsPerTimeseries, *maxLabelNameLen, *maxLabelValueLen)
if promscrape.IsDryRun() {

View File

@@ -25,6 +25,11 @@ var (
rowsPerInsert = metrics.NewHistogram(`vmagent_rows_per_insert{type="opentelemetry"}`)
)
// Init must be called after flag.Parse and before using the opentelemetry package.
func Init() {
stream.InitDecodeOptions()
}
// InsertHandlerForReader processes metrics from given reader.
func InsertHandlerForReader(at *auth.Token, r io.Reader, encoding string) error {
return stream.ParseStream(r, encoding, nil, func(tss []prompb.TimeSeries, mms []prompb.MetricMetadata) error {

View File

@@ -2,6 +2,7 @@ package remotewrite
import (
"bytes"
"context"
"errors"
"fmt"
"io"
@@ -310,11 +311,6 @@ func (c *client) runWorker() {
if !ok {
return
}
if len(block) == 0 {
// skip empty data blocks from sending
// see https://github.com/VictoriaMetrics/VictoriaMetrics/pull/6241
continue
}
go func() {
startTime := time.Now()
ch <- c.sendBlock(block)
@@ -330,15 +326,20 @@ func (c *client) runWorker() {
c.fq.MustWriteBlockIgnoreDisabledPQ(block)
return
case <-c.stopCh:
// c must be stopped. Wait for a while in the hope the block will be sent.
graceDuration := 5 * time.Second
// c must be stopped. Wait up to 5 seconds for the in-flight request to complete.
// If it succeeds, drain the remaining in-memory queue before returning.
stopCtx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
select {
case ok := <-ch:
if !ok {
// Return unsent block to the queue.
c.fq.MustWriteBlockIgnoreDisabledPQ(block)
} else {
c.drainInMemoryQueue(stopCtx, block[:0])
}
case <-time.After(graceDuration):
case <-stopCtx.Done():
// Return unsent block to the queue.
c.fq.MustWriteBlockIgnoreDisabledPQ(block)
}
@@ -508,6 +509,32 @@ again:
goto again
}
func (c *client) drainInMemoryQueue(stopCtx context.Context, block []byte) {
var ok bool
for {
select {
case <-stopCtx.Done():
return
default:
}
block, ok = c.fq.MustReadInMemoryBlock(block[:0])
if !ok {
// The in memory queue has already been drained,
// or persisted queue is being used.
// In this case it is guaranteed that fq will be empty
return
}
// at this stage c.stopCh should be closed
// so sendBlock function should not perform retries
if ok := c.sendBlock(block); !ok {
c.fq.MustWriteBlockIgnoreDisabledPQ(block)
return
}
}
}
var remoteWriteRejectedLogger = logger.WithThrottler("remoteWriteRejected", 5*time.Second)
var remoteWriteRetryLogger = logger.WithThrottler("remoteWriteRetry", 5*time.Second)

View File

@@ -53,6 +53,9 @@ func TestInitSecretFlags(t *testing.T) {
if !flagutil.IsSecretFlag("remotewrite.headers") {
t.Fatalf("expecting remoteWrite.headers to be secret")
}
if !flagutil.IsSecretFlag("remotewrite.proxyurl") {
t.Fatalf("expecting remoteWrite.proxyURL to be secret")
}
flagutil.UnregisterAllSecretFlags()
*showRemoteWriteURL = true
@@ -63,6 +66,9 @@ func TestInitSecretFlags(t *testing.T) {
if !flagutil.IsSecretFlag("remotewrite.headers") {
t.Fatalf("expecting remoteWrite.headers to remain secret")
}
if !flagutil.IsSecretFlag("remotewrite.proxyurl") {
t.Fatalf("expecting remoteWrite.proxyURL to remain secret")
}
}
func TestRepackBlockFromZstdToSnappy(t *testing.T) {

View File

@@ -79,7 +79,8 @@ var (
"writing them to remote storage. "+
"Examples: -remoteWrite.roundDigits=2 would round 1.236 to 1.24, while -remoteWrite.roundDigits=-1 would round 126.78 to 130. "+
"By default, digits rounding is disabled. Set it to 100 for disabling it for a particular remote storage. "+
"This option may be used for improving data compression for the stored metrics")
"This option may be used for improving data compression for the stored metrics. "+
"See also -remoteWrite.significantFigures")
sortLabels = flag.Bool("sortLabels", false, `Whether to sort labels for incoming samples before writing them to all the configured remote storage systems. `+
`This may be needed for reducing memory usage at remote storage when the order of labels in incoming samples is random. `+
`For example, if m{k1="v1",k2="v2"} may be sent as m{k2="v2",k1="v1"}`+
@@ -151,6 +152,8 @@ func InitSecretFlags() {
// remoteWrite.url can contain authentication codes, so hide it at `/metrics` output.
flagutil.RegisterSecretFlag("remoteWrite.url")
}
// remoteWrite.proxyURL can contain authentication codes.
flagutil.RegisterSecretFlag("remoteWrite.proxyURL")
// remoteWrite.headers can contain auth headers such as Authorization and API keys.
flagutil.RegisterSecretFlag("remoteWrite.headers")
}
@@ -169,6 +172,18 @@ func Init() {
if len(*remoteWriteURLs) == 0 {
logger.Fatalf("at least one `-remoteWrite.url` command-line flag must be set")
}
if *shardByURL && len(*disableOnDiskQueue) > 1 {
disableOnDiskQueues := *disableOnDiskQueue
firstValue := disableOnDiskQueues[0]
for _, v := range disableOnDiskQueues[1:] {
if firstValue != v {
logger.Fatalf("all -remoteWrite.url targets must have the same -remoteWrite.disableOnDiskQueue setting when -remoteWrite.shardByURL is enabled; " +
"either enable or disable -remoteWrite.disableOnDiskQueue for all targets")
}
}
}
if limit := getMaxHourlySeries(); limit > 0 {
hourlySeriesLimiter = bloomfilter.NewLimiter(limit, time.Hour)
_ = metrics.NewGauge(`vmagent_hourly_series_limit_max_series`, func() float64 {
@@ -501,7 +516,9 @@ func tryPush(at *auth.Token, wr *prompb.WriteRequest, forceDropSamplesOnFailure
//
// calculateHealthyRwctxIdx will rely on the order of rwctx to be in ascending order.
func getEligibleRemoteWriteCtxs(tss []prompb.TimeSeries, forceDropSamplesOnFailure bool) ([]*remoteWriteCtx, bool) {
if !disableOnDiskQueueAny {
// When -remoteWrite.shardByURL=true always use all configured remote writes to preserve stable metrics distribution across shards.
// See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/10507
if !disableOnDiskQueueAny || *shardByURL {
return rwctxsGlobal, true
}
@@ -516,12 +533,6 @@ func getEligibleRemoteWriteCtxs(tss []prompb.TimeSeries, forceDropSamplesOnFailu
return nil, false
}
rowsCount := getRowsCount(tss)
if *shardByURL {
// Todo: When shardByURL is enabled, the following metrics won't be 100% accurate. Because vmagent don't know
// which rwctx should data be pushed to yet. Let's consider the hashing algorithm fair and will distribute
// data to all rwctxs evenly.
rowsCount = rowsCount / len(rwctxsGlobal)
}
rwctx.rowsDroppedOnPushFailure.Add(rowsCount)
}
}

View File

@@ -61,7 +61,7 @@ func UnitTest(files []string, disableGroupLabel bool, externalLabels []string, e
}
eu, err := url.Parse(externalURL)
if err != nil {
logger.Fatalf("failed to parse external URL: %w", err)
logger.Fatalf("failed to parse external URL: %s", err)
}
if err := templates.Load([]string{}, *eu); err != nil {
logger.Fatalf("failed to load template: %v", err)

View File

@@ -113,15 +113,15 @@ func (g *Group) Validate(validateTplFn ValidateTplFn, validateExpressions bool)
// because correct types must be inherited after unmarshalling.
exprValidator := g.Type.ValidateExpr
if err := exprValidator(r.Expr); err != nil {
return fmt.Errorf("invalid expression for rule %q: %w", ruleName, err)
return fmt.Errorf("invalid expression for rule %q: %w", ruleName, err)
}
}
if validateTplFn != nil {
if err := validateTplFn(r.Annotations); err != nil {
return fmt.Errorf("invalid annotations for rule %q: %w", ruleName, err)
return fmt.Errorf("invalid annotations for rule %q: %w", ruleName, err)
}
if err := validateTplFn(r.Labels); err != nil {
return fmt.Errorf("invalid labels for rule %q: %w", ruleName, err)
return fmt.Errorf("invalid labels for rule %q: %w", ruleName, err)
}
}
}

View File

@@ -121,7 +121,7 @@ func TestParse_Failure(t *testing.T) {
f([]string{"testdata/dir/rules2-bad.rules"}, "function \"unknown\" not defined")
f([]string{"testdata/dir/rules3-bad.rules"}, "either `record` or `alert` must be set")
f([]string{"testdata/dir/rules4-bad.rules"}, "either `record` or `alert` must be set")
f([]string{"testdata/rules/rules1-bad.rules"}, "bad graphite expr")
f([]string{"testdata/rules/rules1-bad.rules"}, "bad GraphiteQL expr")
f([]string{"testdata/rules/vlog-rules0-bad.rules"}, "bad LogsQL expr")
f([]string{"testdata/dir/rules6-bad.rules"}, "missing ':' in header")
f([]string{"testdata/rules/rules-multi-doc-bad.rules"}, "unknown fields")
@@ -283,7 +283,7 @@ func TestGroupValidate_Failure(t *testing.T) {
Expr: "up | 0",
},
},
}, true, "bad prometheus expr")
}, true, "bad MetricsQL expr")
f(&Group{
Name: "test graphite expr",
@@ -293,7 +293,7 @@ func TestGroupValidate_Failure(t *testing.T) {
"description": "some-description",
}},
},
}, true, "bad graphite expr")
}, true, "bad GraphiteQL expr")
f(&Group{
Name: "test vlogs expr",
@@ -327,7 +327,7 @@ func TestGroupValidate_Failure(t *testing.T) {
Expr: "sum(up == 0 ) by (host)",
},
},
}, true, "bad graphite expr")
}, true, "bad GraphiteQL expr")
f(&Group{
Name: "test vlogs with prometheus exp",
@@ -351,7 +351,7 @@ func TestGroupValidate_Failure(t *testing.T) {
For: promutil.NewDuration(10 * time.Millisecond),
},
},
}, true, "bad prometheus expr")
}, true, "bad MetricsQL expr")
}
func TestGroupValidate_Success(t *testing.T) {

View File

@@ -66,11 +66,11 @@ func (t *Type) ValidateExpr(expr string) error {
switch t.String() {
case "graphite":
if _, err := graphiteql.Parse(expr); err != nil {
return fmt.Errorf("bad graphite expr: %q, err: %w", expr, err)
return fmt.Errorf("bad GraphiteQL expr: %q, err: %w", expr, err)
}
case "prometheus":
if _, err := metricsql.Parse(expr); err != nil {
return fmt.Errorf("bad prometheus expr: %q, err: %w", expr, err)
return fmt.Errorf("bad MetricsQL expr: %q, err: %w", expr, err)
}
case "vlogs":
q, err := logstorage.ParseStatsQuery(expr, 0)

View File

@@ -64,6 +64,7 @@ func InitSecretFlags() {
if !*showDatasourceURL {
flagutil.RegisterSecretFlag("datasource.url")
}
flagutil.RegisterSecretFlag("datasource.headers")
}
// ShowDatasourceURL whether to show -datasource.url with sensitive information

View File

@@ -105,7 +105,7 @@ func (cw *configWatcher) add(typeK TargetType, interval time.Duration, targetsFn
}
targetMetadata, errors := getTargetMetadata(targetsFn, cw.cfg)
for _, err := range errors {
logger.Errorf("failed to init notifier for %q: %w", typeK, err)
logger.Errorf("failed to init notifier for %q: %s", typeK, err)
}
cw.updateTargets(typeK, targetMetadata, cw.cfg, cw.genFn)
}
@@ -274,7 +274,7 @@ func (cw *configWatcher) updateTargets(key TargetType, targetMts map[string]targ
for addr, metadata := range targetMts {
am, err := NewAlertManager(addr, genFn, cfg.HTTPClientConfig, metadata.alertRelabelConfigs, cfg.Timeout.Duration())
if err != nil {
logger.Errorf("failed to init %s notifier with addr %q: %w", key, addr, err)
logger.Errorf("failed to init %s notifier with addr %q: %s", key, addr, err)
continue
}
updatedTargets = append(updatedTargets, Target{

View File

@@ -194,6 +194,7 @@ func InitSecretFlags() {
if !*showNotifierURL {
flagutil.RegisterSecretFlag("notifier.url")
}
flagutil.RegisterSecretFlag("notifier.headers")
}
func notifiersFromFlags(gen AlertURLGenerator) ([]Notifier, error) {

View File

@@ -59,6 +59,7 @@ func InitSecretFlags() {
if !*showRemoteReadURL {
flagutil.RegisterSecretFlag("remoteRead.url")
}
flagutil.RegisterSecretFlag("remoteRead.headers")
}
// Init creates a Querier from provided flag values.

View File

@@ -62,6 +62,7 @@ func InitSecretFlags() {
if !*showRemoteWriteURL {
flagutil.RegisterSecretFlag("remoteWrite.url")
}
flagutil.RegisterSecretFlag("remoteWrite.headers")
}
// Init creates Client object from given flags.

View File

@@ -95,6 +95,7 @@ type groupMetrics struct {
iterationTotal *metrics.Counter
iterationDuration *metrics.Summary
iterationMissed *metrics.Counter
iterationReset *metrics.Counter
iterationInterval *metrics.Gauge
}
@@ -330,6 +331,7 @@ func (g *Group) Init() {
g.metrics.iterationTotal = g.metrics.set.NewCounter(fmt.Sprintf(`vmalert_iteration_total{%s}`, labels))
g.metrics.iterationDuration = g.metrics.set.NewSummary(fmt.Sprintf(`vmalert_iteration_duration_seconds{%s}`, labels))
g.metrics.iterationMissed = g.metrics.set.NewCounter(fmt.Sprintf(`vmalert_iteration_missed_total{%s}`, labels))
g.metrics.iterationReset = g.metrics.set.NewCounter(fmt.Sprintf(`vmalert_iteration_reset_total{%s}`, labels))
g.metrics.iterationInterval = g.metrics.set.NewGauge(fmt.Sprintf(`vmalert_iteration_interval_seconds{%s}`, labels), func() float64 {
i := g.Interval.Seconds()
return i
@@ -474,14 +476,16 @@ func (g *Group) Start(ctx context.Context, rw remotewrite.RWClient, rr datasourc
if missed < 0 {
// missed can become < 0 due to irregular delays during evaluation
// which can result in time.Since(evalTS) < g.Interval;
// or the system wall clock was changed backward
missed = 0
// or the system wall clock was changed backward,
// Reset the evalTS to the current time.
evalTS = time.Now()
g.metrics.iterationReset.Inc()
} else {
evalTS = evalTS.Add((missed + 1) * g.Interval)
}
if missed > 0 {
g.metrics.iterationMissed.Inc()
}
evalTS = evalTS.Add((missed + 1) * g.Interval)
eval(evalCtx, evalTS)
}

View File

@@ -11,6 +11,8 @@ import (
"strconv"
"strings"
"github.com/VictoriaMetrics/metricsql"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/config"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/notifier"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/rule"
@@ -160,12 +162,12 @@ func (rh *requestHandler) handler(w http.ResponseWriter, r *http.Request) bool {
case "/vmalert/api/v1/alerts", "/api/v1/alerts":
// path used by Grafana for ng alerting
gf, err := newGroupsFilter(r)
af, err := newAlertsFilter(r)
if err != nil {
errJson(w, r, err)
return true
}
data, err := rh.listAlerts(gf)
data, err := rh.listAlerts(af)
if err != nil {
errJson(w, r, err)
return true
@@ -325,6 +327,48 @@ func (gf *groupsFilter) matches(group *rule.Group) bool {
return true
}
type alertsFilter struct {
gf *groupsFilter
match [][]metricsql.LabelFilter
}
func getMatchFilters(matches []string) ([][]metricsql.LabelFilter, *httpserver.ErrorWithStatusCode) {
if len(matches) == 0 {
return nil, nil
}
tfss := make([][]metricsql.LabelFilter, 0, len(matches))
for _, s := range matches {
expr, err := metricsql.Parse(s)
if err != nil {
return nil, errResponse(fmt.Errorf(`invalid parameter "match[]": failed to parse %q: %w`, s, err), http.StatusBadRequest)
}
me, ok := expr.(*metricsql.MetricExpr)
if !ok {
return nil, errResponse(fmt.Errorf(`invalid parameter "match[]": expecting metricSelector; got %q`, expr.AppendString(nil)), http.StatusBadRequest)
}
if len(me.LabelFilterss) == 0 {
return nil, errResponse(fmt.Errorf(`invalid parameter "match[]": labelFilterss cannot be empty`), http.StatusBadRequest)
}
tfss = append(tfss, me.LabelFilterss...)
}
return tfss, nil
}
func newAlertsFilter(r *http.Request) (*alertsFilter, *httpserver.ErrorWithStatusCode) {
gf, err := newGroupsFilter(r)
if err != nil {
return nil, err
}
var af alertsFilter
af.gf = gf
af.match, err = getMatchFilters(r.Form["match[]"])
if err != nil {
return nil, err
}
return &af, nil
}
// see https://prometheus.io/docs/prometheus/latest/querying/api/#rules
type rulesFilter struct {
gf *groupsFilter
@@ -335,6 +379,7 @@ type rulesFilter struct {
maxGroups int
pageNum int
search string
match [][]metricsql.LabelFilter
extendedStates bool
}
@@ -355,7 +400,10 @@ func newRulesFilter(r *http.Request) (*rulesFilter, *httpserver.ErrorWithStatusC
return nil, errResponse(fmt.Errorf(`invalid parameter "type": not supported value %q`, ruleTypeParam), http.StatusBadRequest)
}
}
rf.match, err = getMatchFilters(r.Form["match[]"])
if err != nil {
return nil, err
}
states := vs["state"]
if len(states) == 0 {
states = vs["filter"]
@@ -416,12 +464,47 @@ func (rf *rulesFilter) matchesRule(r *rule.ApiRule) bool {
if len(rf.ruleNames) > 0 && !slices.Contains(rf.ruleNames, r.Name) {
return false
}
if !areLabelsMatch(r.Labels, rf.match) {
return false
}
if len(rf.states) == 0 {
return true
}
return slices.Contains(rf.states, r.State)
}
func areLabelsMatch(labels map[string]string, matches [][]metricsql.LabelFilter) bool {
if len(matches) == 0 {
return true
}
// labels need to match at least one of the provided match[] arg
return slices.ContainsFunc(matches, func(filters []metricsql.LabelFilter) bool {
for _, mf := range filters {
if !isLabelFilterMatch(labels[mf.Label], mf) {
return false
}
}
return true
})
}
func isLabelFilterMatch(s string, match metricsql.LabelFilter) bool {
if !match.IsRegexp {
if match.IsNegative {
return s != match.Value
}
return s == match.Value
}
re, err := metricsql.CompileRegexpAnchored(match.Value)
if err != nil {
return false
}
if match.IsNegative {
return !re.MatchString(s)
}
return re.MatchString(s)
}
func (rh *requestHandler) groups(rf *rulesFilter) *listGroupsResponse {
rh.m.groupsMu.RLock()
defer rh.m.groupsMu.RUnlock()
@@ -543,14 +626,14 @@ func (rh *requestHandler) groupAlerts() []rule.GroupAlerts {
return gAlerts
}
func (rh *requestHandler) listAlerts(gf *groupsFilter) ([]byte, *httpserver.ErrorWithStatusCode) {
func (rh *requestHandler) listAlerts(af *alertsFilter) ([]byte, *httpserver.ErrorWithStatusCode) {
rh.m.groupsMu.RLock()
defer rh.m.groupsMu.RUnlock()
lr := listAlertsResponse{Status: "success"}
lr.Data.Alerts = make([]*rule.ApiAlert, 0)
for _, group := range rh.m.groups {
if !gf.matches(group) {
if !af.gf.matches(group) {
continue
}
g := group.ToAPI()
@@ -558,7 +641,11 @@ func (rh *requestHandler) listAlerts(gf *groupsFilter) ([]byte, *httpserver.Erro
if r.Type != rule.TypeAlerting {
continue
}
lr.Data.Alerts = append(lr.Data.Alerts, r.Alerts...)
for _, alert := range r.Alerts {
if areLabelsMatch(alert.Labels, af.match) {
lr.Data.Alerts = append(lr.Data.Alerts, alert)
}
}
}
}

View File

@@ -348,7 +348,7 @@
typeK, ns := keys[i], targets[notifier.TargetType(keys[i])]
count := len(ns)
%}
<div class="w-100 flex-column vm-group">
<div class="w-100 flex-column">
<span class="d-flex justify-content-between" id="group-{%s typeK %}">
<a href="#group-{%s typeK %}">{%s typeK %} ({%d count %})</a>
<span
@@ -361,7 +361,7 @@
<div id="item-{%s typeK %}" class="collapse show">
<table class="table table-striped table-hover table-sm">
<thead>
<tr class="vm-item">
<tr>
<th scope="col">Labels</th>
<th scope="col">Address</th>
</tr>

View File

@@ -1115,7 +1115,7 @@ func StreamListTargets(qw422016 *qt422016.Writer, r *http.Request, targets map[n
//line app/vmalert/web.qtpl:350
qw422016.N().S(`
<div class="w-100 flex-column vm-group">
<div class="w-100 flex-column">
<span class="d-flex justify-content-between" id="group-`)
//line app/vmalert/web.qtpl:352
qw422016.E().S(typeK)
@@ -1152,7 +1152,7 @@ func StreamListTargets(qw422016 *qt422016.Writer, r *http.Request, targets map[n
qw422016.N().S(`" class="collapse show">
<table class="table table-striped table-hover table-sm">
<thead>
<tr class="vm-item">
<tr>
<th scope="col">Labels</th>
<th scope="col">Address</th>
</tr>

View File

@@ -10,6 +10,8 @@ import (
"testing"
"time"
"github.com/VictoriaMetrics/metricsql"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/config"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/datasource"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/notifier"
@@ -37,12 +39,14 @@ func TestHandler(t *testing.T) {
Concurrency: 1,
Rules: []config.Rule{
{
ID: 0,
Alert: "alert",
ID: 0,
Alert: "alert",
Labels: map[string]string{"job": "foo"},
},
{
ID: 1,
Record: "record",
Labels: map[string]string{"job": "bar"},
},
},
}, fq, 1*time.Minute, nil)
@@ -128,6 +132,18 @@ func TestHandler(t *testing.T) {
if length := len(lr.Data.Alerts); length != 2 {
t.Fatalf("expected 2 alert got %d", length)
}
lr = listAlertsResponse{}
getResp(t, ts.URL+`/api/v1/alerts?match[]={job="foo"}`, &lr, 200)
if length := len(lr.Data.Alerts); length != 3 {
t.Fatalf("expected 3 alerts got %d", length)
}
lr = listAlertsResponse{}
getResp(t, ts.URL+`/api/v1/alerts?match[]={job="bar"}`, &lr, 200)
if length := len(lr.Data.Alerts); length != 0 {
t.Fatalf("expected 0 alerts got %d", length)
}
})
t.Run("/api/v1/alert?alertID&groupID", func(t *testing.T) {
expAlert := rule.NewAlertAPI(ar, ar.GetAlerts()[0])
@@ -242,6 +258,13 @@ func TestHandler(t *testing.T) {
check("/vmalert/api/v1/rules?datasource_type=graphite", 200, 1, 2)
check("/vmalert/api/v1/rules?datasource_type=graphiti", 400, 0, 0)
// invalid match[] params
check(`/vmalert/api/v1/rules?match[]={job=!"foo"}`, 400, 0, 0)
check(`/vmalert/api/v1/rules?match[]={job="foo"}`, 200, 3, 3)
check(`/vmalert/api/v1/rules?match[]={job="bar"}`, 200, 3, 3)
check(`/vmalert/api/v1/rules?match[]={job="bar"}&match[]={job="foo"}`, 200, 3, 6)
check(`/vmalert/api/v1/rules?match[]={job="barzz"}`, 200, 0, 0)
// no filtering expected due to bad params
check("/api/v1/rules?type=badParam", 400, 0, 0)
check("/api/v1/rules?foo=bar", 200, 3, 6)
@@ -367,3 +390,116 @@ func TestEmptyResponse(t *testing.T) {
}
})
}
func TestMatchesRule(t *testing.T) {
parseMatch := func(t *testing.T, selectors []string) [][]metricsql.LabelFilter {
t.Helper()
var match [][]metricsql.LabelFilter
for _, s := range selectors {
expr, err := metricsql.Parse(s)
if err != nil {
t.Fatalf("failed to parse selector %q: %v", s, err)
}
me, ok := expr.(*metricsql.MetricExpr)
if !ok {
t.Fatalf("expected MetricExpr for %q, got %T", s, expr)
}
match = append(match, me.LabelFilterss...)
}
return match
}
f := func(t *testing.T, selectors []string, labels map[string]string, wantMatch bool) {
t.Helper()
rf := &rulesFilter{
gf: &groupsFilter{},
match: parseMatch(t, selectors),
}
r := &rule.ApiRule{Labels: labels}
got := rf.matchesRule(r)
if got != wantMatch {
t.Fatalf("matchesRule(%v) with selectors %v: got %v, want %v",
labels, selectors, got, wantMatch)
}
}
f(t, nil, map[string]string{"foo": "bar"}, true)
f(t, []string{`{foo="bar"}`}, map[string]string{"foo": "bar"}, true)
f(t, []string{`{foo="bar"}`}, map[string]string{"foo": "baz"}, false)
f(t, []string{`{foo="bar"}`}, map[string]string{"bar": "baz"}, false)
f(t, []string{`{foo=""}`}, map[string]string{"bar": "baz"}, true)
f(t, []string{`{foo!="bar"}`}, map[string]string{"foo": "baz"}, true)
f(t, []string{`{foo!="bar"}`}, map[string]string{"foo": "bar"}, false)
f(t, []string{`{foo=~"bar.*"}`}, map[string]string{"foo": "bar"}, true)
f(t, []string{`{foo=~"bar.*"}`}, map[string]string{"foo": "baz"}, false)
f(t, []string{`{bar=~"baz|bar"}`}, map[string]string{"bar": "baz"}, true)
f(t, []string{`{bar=~"baz|bar"}`}, map[string]string{"bar": "bar"}, true)
f(t, []string{`{bar=~"baz|bar"}`}, map[string]string{"bar": "foo"}, false)
f(t, []string{`{foo!~"bar.*"}`}, map[string]string{"foo": "baz"}, true)
f(t, []string{`{foo!~"bar.*"}`}, map[string]string{"foo": "bar"}, false)
// single match[] with multiple filters
f(t,
[]string{`{job="foo",instance="bar"}`},
map[string]string{"job": "foo", "instance": "bar"},
true,
)
f(t,
[]string{`{job="foo",instance="bar"}`},
map[string]string{"job": "other", "instance": "bar"},
false,
)
f(t,
[]string{`{foo="bar",baz=~"b.*"}`},
map[string]string{"foo": "bar", "baz": "bazinga"},
true,
)
f(t,
[]string{`{foo="bar",baz=~"b.*"}`},
map[string]string{"foo": "other", "baz": "bazinga"},
false,
)
// multiple matches[]
f(t,
[]string{`{foo="bar"}`, `{foo="baz"}`},
map[string]string{"foo": "baz"},
true,
)
f(t,
[]string{`{foo="bar"}`, `{foo="baz"}`},
map[string]string{"foo": "unknown"},
false,
)
f(t,
[]string{`{foo=~"bar.*"}`, `{bar=~"baz.*"}`},
map[string]string{"bar": "bazinga"},
true,
)
f(t,
[]string{`{foo=~"bar.*"}`, `{bar=~"baz.*"}`},
map[string]string{"foo": "bartender"},
true,
)
f(t,
[]string{`{foo=~"bar.*"}`, `{bar=~"baz.*"}`},
map[string]string{"foo": "other", "bar": "other"},
false,
)
f(t,
[]string{`{job="foo",instance="bar"}`, `{foo="bar"}`},
map[string]string{"foo": "bar"},
true,
)
f(t,
[]string{`{job="foo", instance="bar"}`, `{foo="bar"}`},
map[string]string{"instance": "barr", "job": "foo"},
false,
)
}

View File

@@ -889,7 +889,8 @@ func reloadAuthConfig() (bool, error) {
}
mp := authUsers.Load()
logger.Infof("loaded information about %d users from -auth.config=%q", len(*mp), *authConfigPath)
jwtc := jwtAuthCache.Load()
logger.Infof("loaded information about %d users from -auth.config=%q", len(*mp)+len(jwtc.users), *authConfigPath)
return true, nil
}

View File

@@ -130,6 +130,16 @@ users:
- "http://vmselect1:8481/select/{{.MetricsTenant}}/prometheus"
- "http://vmselect2:8481/select/{{.MetricsTenant}}/prometheus"
# JWT-based routing using header-based tenant identification (VictoriaMetrics cluster)
# The AccountID and ProjectID from JWT vm_access claims are injected as HTTP headers.
- name: jwt-header-tenant
jwt:
skip_verify: true
headers:
- "AccountID: {{.MetricsAccountID}}"
- "ProjectID: {{.MetricsProjectID}}"
url_prefix: "http://vminsert:8480/insert/prometheus"
# Requests without Authorization header are proxied according to `unauthorized_user` section.
# Requests are proxied in round-robin fashion between `url_prefix` backends.
# The deny_partial_response query arg is added to all the proxied requests.

View File

@@ -17,6 +17,8 @@ import (
const (
metricsTenantPlaceholder = `{{.MetricsTenant}}`
metricsAccountIDPlaceholder = `{{.MetricsAccountID}}`
metricsProjectIDPlaceholder = `{{.MetricsProjectID}}`
metricsExtraLabelsPlaceholder = `{{.MetricsExtraLabels}}`
metricsExtraFiltersPlaceholder = `{{.MetricsExtraFilters}}`
@@ -30,6 +32,8 @@ const (
var allPlaceholders = []string{
metricsTenantPlaceholder,
metricsAccountIDPlaceholder,
metricsProjectIDPlaceholder,
metricsExtraLabelsPlaceholder,
metricsExtraFiltersPlaceholder,
logsAccountIDPlaceholder,
@@ -40,6 +44,8 @@ var allPlaceholders = []string{
var urlPathPlaceHolders = []string{
metricsTenantPlaceholder,
metricsAccountIDPlaceholder,
metricsProjectIDPlaceholder,
logsAccountIDPlaceholder,
logsProjectIDPlaceholder,
}
@@ -371,6 +377,8 @@ func jwtClaimsData(vma *jwt.VMAccessClaim) map[string][]string {
data := map[string][]string{
// TODO: optimize at parsing stage
metricsTenantPlaceholder: {fmt.Sprintf("%d:%d", vma.MetricsAccountID, vma.MetricsProjectID)},
metricsAccountIDPlaceholder: {fmt.Sprintf("%d", vma.MetricsAccountID)},
metricsProjectIDPlaceholder: {fmt.Sprintf("%d", vma.MetricsProjectID)},
metricsExtraLabelsPlaceholder: vma.MetricsExtraLabels,
metricsExtraFiltersPlaceholder: vma.MetricsExtraFilters,

View File

@@ -170,13 +170,13 @@ users:
url_prefix: http://foo.bar
`, "cannot parse public key from file \""+publicKeyFile+"\": failed to parse key \"invalidPEM\": failed to decode PEM block containing public key")
// unsupported placeholder in a header
// unsupported placeholder in a URL path
f(`
users:
- jwt:
skip_verify: true
url_prefix: http://foo.bar/{{.UnsupportedPlaceholder}}/foo`,
"invalid placeholder found in URL request path: \"/{{.UnsupportedPlaceholder}}/foo\", supported values are: {{.MetricsTenant}}, {{.MetricsExtraLabels}}, {{.MetricsExtraFilters}}, {{.LogsAccountID}}, {{.LogsProjectID}}, {{.LogsExtraFilters}}, {{.LogsExtraStreamFilters}}",
"invalid placeholder found in URL request path: \"/{{.UnsupportedPlaceholder}}/foo\", supported values are: {{.MetricsTenant}}, {{.MetricsAccountID}}, {{.MetricsProjectID}}, {{.MetricsExtraLabels}}, {{.MetricsExtraFilters}}, {{.LogsAccountID}}, {{.LogsProjectID}}, {{.LogsExtraFilters}}, {{.LogsExtraStreamFilters}}",
)
// unsupported placeholder in a header
f(`
@@ -187,7 +187,7 @@ users:
- "AccountID: {{.UnsupportedPlaceholder}}"
url_prefix: http://foo.bar
`,
"request header: \"AccountID\" has unsupported placeholder: \"{{.UnsupportedPlaceholder}}\", supported values are: {{.MetricsTenant}}, {{.MetricsExtraLabels}}, {{.MetricsExtraFilters}}, {{.LogsAccountID}}, {{.LogsProjectID}}, {{.LogsExtraFilters}}, {{.LogsExtraStreamFilters}}",
"request header: \"AccountID\" has unsupported placeholder: \"{{.UnsupportedPlaceholder}}\", supported values are: {{.MetricsTenant}}, {{.MetricsAccountID}}, {{.MetricsProjectID}}, {{.MetricsExtraLabels}}, {{.MetricsExtraFilters}}, {{.LogsAccountID}}, {{.LogsProjectID}}, {{.LogsExtraFilters}}, {{.LogsExtraStreamFilters}}",
)
// spaces in templating not allowed
@@ -199,7 +199,19 @@ users:
- "AccountID: {{ .LogsAccountID }}"
url_prefix: http://foo.bar
`,
"request header: \"AccountID\" has unsupported placeholder: \"{{ .LogsAccountID }}\", supported values are: {{.MetricsTenant}}, {{.MetricsExtraLabels}}, {{.MetricsExtraFilters}}, {{.LogsAccountID}}, {{.LogsProjectID}}, {{.LogsExtraFilters}}, {{.LogsExtraStreamFilters}}",
"request header: \"AccountID\" has unsupported placeholder: \"{{ .LogsAccountID }}\", supported values are: {{.MetricsTenant}}, {{.MetricsAccountID}}, {{.MetricsProjectID}}, {{.MetricsExtraLabels}}, {{.MetricsExtraFilters}}, {{.LogsAccountID}}, {{.LogsProjectID}}, {{.LogsExtraFilters}}, {{.LogsExtraStreamFilters}}",
)
// placeholder must match the entire header value
f(`
users:
- jwt:
skip_verify: true
headers:
- "AccountID: foo {{.MetricsAccountID}}"
url_prefix: http://foo.bar
`,
"request header: \"AccountID\" has unsupported placeholder: \"foo {{.MetricsAccountID}}\", supported values are: {{.MetricsTenant}}, {{.MetricsAccountID}}, {{.MetricsProjectID}}, {{.MetricsExtraLabels}}, {{.MetricsExtraFilters}}, {{.LogsAccountID}}, {{.LogsProjectID}}, {{.LogsExtraFilters}}, {{.LogsExtraStreamFilters}}",
)
// oidc is not an object
@@ -364,10 +376,25 @@ users:
url_prefix: http://foo.bar
`, validRSAPublicKey, validECDSAPublicKey))
// metrics header placeholders
f(`
users:
- jwt:
skip_verify: true
headers:
- "MetricsAccountID: {{.MetricsAccountID}}"
- "MetricsProjectID: {{.MetricsProjectID}}"
url_prefix: http://foo.bar
`)
// logs header placeholders
f(`
users:
- jwt:
skip_verify: true
headers:
- "LogsAccountID: {{.LogsAccountID}}"
- "LogsProjectID: {{.LogsProjectID}}"
url_prefix: http://foo.bar
`)

View File

@@ -545,6 +545,31 @@ requested_url={BACKEND}/path2/foo/?de=fg`
if n := retries.Load(); n != 2 {
t.Fatalf("unexpected number of retries; got %d; want 2", n)
}
// make sure that empty config value erases client extra filters and extra labels
cfgStr = `
unauthorized_user:
url_prefix: {BACKEND}/foo?bar=baz&extra_filters[]=&extra_label=&extra_filters=`
requestURL = "http://some-host.com/abc/def?some_arg=some_value&extra_filters[]=baz&extra_label=tenant=admin&extra_filters=bar"
backendHandler = func(w http.ResponseWriter, r *http.Request) {
h := w.Header()
h.Set("Connection", "close")
h.Set("Foo", "bar")
var bb bytes.Buffer
if err := r.Header.Write(&bb); err != nil {
panic(fmt.Errorf("unexpected error when marshaling headers: %w", err))
}
fmt.Fprintf(w, "requested_url=http://%s%s\n%s", r.Host, r.URL, bb.String())
}
responseExpected = `
statusCode=200
Foo: bar
requested_url={BACKEND}/foo/abc/def?bar=baz&extra_filters=&extra_filters%5B%5D=&extra_label=&some_arg=some_value
Pass-Header: abc
User-Agent: vmauth
X-Forwarded-For: 12.34.56.78, 42.2.3.84`
f(cfgStr, requestURL, backendHandler, responseExpected)
}
func TestJWTRequestHandler(t *testing.T) {
@@ -851,6 +876,30 @@ users:
responseExpected,
)
// test header injection and URL templating with individual placeholders
request = httptest.NewRequest(`GET`, "http://some-host.com/api/v1/query", nil)
request.Header.Set(`Authorization`, `Bearer `+fullToken)
responseExpected = `
statusCode=200
path: /select/123/234/api/v1/query
query:
headers:
AccountID=123
ProjectID=234`
f(fmt.Sprintf(
`
users:
- jwt:
public_keys:
- %q
url_prefix: {BACKEND}/select/{{.MetricsAccountID}}/{{.MetricsProjectID}}
headers:
- "AccountID: {{.MetricsAccountID}}"
- "ProjectID: {{.MetricsProjectID}}"`, string(publicKeyPEM)),
request,
responseExpected,
)
// extra_label and extra_filters from vm_access claim merged with statically defined
request = httptest.NewRequest(`GET`, "http://some-host.com/api/v1/query", nil)
request.Header.Set(`Authorization`, `Bearer `+fullToken)

View File

@@ -146,7 +146,8 @@ var (
Name: vmRoundDigits,
Value: 100,
Usage: "Round metric values to the given number of decimal digits after the point. " +
"This option may be used for increasing on-disk compression level for the stored metrics",
"This option may be used for increasing on-disk compression level for the stored metrics. " +
"See also --vm-significant-figures option",
},
&cli.StringSliceFlag{
Name: vmExtraLabel,
@@ -500,6 +501,96 @@ var (
}
)
const (
mimirPath = "mimir-path"
mimirTenantID = "mimir-tenant-id"
mimirConcurrency = "mimir-concurrency"
mimirFilterTimeStart = "mimir-filter-time-start"
mimirFilterTimeEnd = "mimir-filter-time-end"
mimirFilterLabel = "mimir-filter-label"
mimirFilterLabelValue = "mimir-filter-label-value"
mimirCredsFilePath = "mimir-creds-file-path"
mimirConfigFilePath = "mimir-config-file-path"
mimirConfigProfile = "mimir-config-profile"
mimirCustomS3Endpoint = "mimir-custom-s3-endpoint"
mimirS3ForcePathStyle = "mimir-s3-force-path-style"
mimirS3TLSInsecureSkipVerify = "mimir-s3-tls-insecure-skip-verify"
mimirSSEKMSKeyID = "mimir-s3-sse-kms-key-id"
mimirSSEAlgorithm = "mimir-s3-sse-algorithm"
)
var (
mimirFlags = []cli.Flag{
&cli.StringFlag{
Name: mimirPath,
Usage: "Path to Mimir storage bucket or local folder.",
Required: true,
},
&cli.StringFlag{
Name: mimirTenantID,
Usage: "Tenant ID for Mimir storage",
},
&cli.IntFlag{
Name: mimirConcurrency,
Usage: "Number of concurrently running block readers",
Value: 1,
},
&cli.StringFlag{
Name: mimirFilterTimeStart,
Usage: "The time filter in RFC3339 format to select timeseries with timestamp equal or higher than provided value. E.g. '2020-01-01T20:07:00Z'",
Required: true,
},
&cli.StringFlag{
Name: mimirFilterTimeEnd,
Usage: "The time filter in RFC3339 format to select timeseries with timestamp equal or lower than provided value. E.g. '2020-01-01T20:07:00Z'",
Required: true,
},
&cli.StringFlag{
Name: mimirFilterLabel,
Usage: "Mimir label name to filter timeseries by. E.g. '__name__' will filter timeseries by name.",
},
&cli.StringFlag{
Name: mimirFilterLabelValue,
Usage: fmt.Sprintf("Regular expression to filter label from %q flag.", mimirFilterLabel),
Value: ".*",
},
&cli.StringFlag{
Name: mimirCredsFilePath,
Usage: "Path to file with GCS or S3 credentials. Credentials are loaded from default locations if not set. See https://cloud.google.com/iam/docs/creating-managing-service-account-keys and https://docs.aws.amazon.com/general/latest/gr/aws-security-credentials.html",
},
&cli.StringFlag{
Name: mimirConfigFilePath,
Usage: "Path to file with S3 configs. Configs are loaded from default location if not set. See https://docs.aws.amazon.com/general/latest/gr/aws-security-credentials.html",
},
&cli.StringFlag{
Name: mimirConfigProfile,
Usage: "Profile name for S3 configs. If no set, the value of the environment variable will be loaded (AWS_PROFILE or AWS_DEFAULT_PROFILE), or if both not set, DefaultSharedConfigProfile is used",
},
&cli.StringFlag{
Name: mimirCustomS3Endpoint,
Usage: "Custom S3 endpoint for use with S3-compatible storages (e.g. MinIO). S3 is used if not set",
},
&cli.BoolFlag{
Name: mimirS3ForcePathStyle,
Usage: "Prefixing endpoint with bucket name when set false, true by default.",
Value: true,
},
&cli.BoolFlag{
Name: mimirS3TLSInsecureSkipVerify,
Usage: "Whether to skip TLS verification when connecting to the S3 endpoint.",
},
&cli.StringFlag{
Name: mimirSSEKMSKeyID,
Usage: "SSE KMS Key ID for use with S3-compatible storages.",
},
&cli.StringFlag{
Name: mimirSSEAlgorithm,
Usage: "SSE algorithm for use with S3-compatible storages.",
},
}
)
const (
vmNativeFilterMatch = "vm-native-filter-match"
vmNativeFilterTimeStart = "vm-native-filter-time-start"

View File

@@ -18,6 +18,7 @@ import (
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmctl/auth"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmctl/backoff"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmctl/barpool"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmctl/mimir"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmctl/native"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmctl/remoteread"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
@@ -297,12 +298,12 @@ func main() {
},
},
{
Name: "thanos",
Usage: "Migrate time series from Thanos blocks (supports raw and downsampled data)",
Flags: mergeFlags(globalFlags, thanosFlags, vmFlags),
Name: "mimir",
Usage: "Migrate time series from Mimir object storage or local filesystem",
Flags: mergeFlags(globalFlags, mimirFlags, vmFlags),
Before: beforeFn,
Action: func(c *cli.Context) error {
fmt.Println("Thanos import mode")
fmt.Println("Mimir import mode")
vmCfg, err := initConfigVM(c)
if err != nil {
@@ -314,6 +315,54 @@ func main() {
return fmt.Errorf("failed to create VM importer: %s", err)
}
mCfg := mimir.Config{
Filter: mimir.Filter{
TimeMin: c.String(mimirFilterTimeStart),
TimeMax: c.String(mimirFilterTimeEnd),
Label: c.String(mimirFilterLabel),
LabelValue: c.String(mimirFilterLabelValue),
},
Path: c.String(mimirPath),
TenantID: c.String(mimirTenantID),
CredsFilePath: c.String(mimirCredsFilePath),
ConfigFilePath: c.String(mimirConfigFilePath),
ConfigProfile: c.String(mimirConfigProfile),
CustomS3Endpoint: c.String(mimirCustomS3Endpoint),
S3ForcePathStyle: c.Bool(mimirS3ForcePathStyle),
S3TLSInsecureSkipVerify: c.Bool(mimirS3TLSInsecureSkipVerify),
SSEKMSKeyID: c.String(mimirSSEKMSKeyID),
SSEAlgorithm: c.String(mimirSSEAlgorithm),
}
cl, err := mimir.NewClient(ctx, mCfg)
if err != nil {
return fmt.Errorf("failed to create mimir client: %s", err)
}
pp := prometheusProcessor{
cl: cl,
im: importer,
cc: c.Int(mimirConcurrency),
isVerbose: c.Bool(globalVerbose),
}
return pp.run(ctx)
},
},
{
Name: "thanos",
Usage: "Migrate time series from Thanos blocks (supports raw and downsampled data)",
Flags: mergeFlags(globalFlags, thanosFlags, vmFlags),
Before: beforeFn,
Action: func(c *cli.Context) error {
fmt.Println("Thanos import mode")
vmCfg, err := initConfigVM(c)
if err != nil {
return fmt.Errorf("failed to init VM configuration: %s", err)
}
importer, err = vm.NewImporter(ctx, vmCfg)
if err != nil {
return fmt.Errorf("failed to create VM importer: %s", err)
}
thanosCfg := thanos.Config{
Snapshot: c.String(thanosSnapshot),
Filter: thanos.Filter{

View File

@@ -0,0 +1,195 @@
package mimir
import (
"fmt"
"log"
"os"
"path/filepath"
"sync"
"github.com/oklog/ulid/v2"
"github.com/prometheus/prometheus/tsdb"
"github.com/prometheus/prometheus/tsdb/tombstones"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/backup/common"
)
var _ tsdb.BlockReader = (*lazyBlockReader)(nil)
// lazyBlockReader is stores block id and segment num information.
// It is used to lazily fetch and parse block data.
// It implements tsdb.BlockReader interface.
type lazyBlockReader struct {
// Block ID.
ID ulid.ULID
// SegmentsNum stores the number of chunks segments in the block.
SegmentsNum int
mu sync.Mutex
reader *tsdb.Block
tempDirPath string
fs common.RemoteFS
err error
}
// newLazyBlockReader returns a new LazyBlockReader for the given block.
func newLazyBlockReader(block *Block, fs common.RemoteFS) (*lazyBlockReader, error) {
if block.SegmentsFormat != "1b6d" {
return nil, fmt.Errorf("unsupported segments format: %s", block.SegmentsFormat)
}
return &lazyBlockReader{
ID: block.ID,
SegmentsNum: block.SegmentsNum,
fs: fs,
}, nil
}
func (lbr *lazyBlockReader) initialize() error {
lbr.mu.Lock()
defer lbr.mu.Unlock()
if lbr.reader != nil {
return nil
}
// fetching block and parse it and store it in lbr.reader
temp, err := lbr.mkTempDir()
if err != nil {
return fmt.Errorf("failed to create temp dir: %s", err)
}
lbr.tempDirPath = temp
// TODO: replace fetchFile and writeFile with buffered IO if needed
meta, err := lbr.fetchFile(metaFilename)
if err != nil {
return err
}
if err := lbr.writeFile(temp, metaFilename, meta); err != nil {
return fmt.Errorf("failed to write meta file: %w", err)
}
idx, err := lbr.fetchFile(indexFilename)
if err != nil {
return fmt.Errorf("failed to fetch index file %q: %w", indexFilename, err)
}
if err := lbr.writeFile(temp, indexFilename, idx); err != nil {
return err
}
for i := 1; i <= lbr.SegmentsNum; i++ {
// segments formats has format 1b06d
// https://github.com/grafana/mimir/blob/main/pkg/storage/tsdb/bucketindex/index.go#L32
chunkName := fmt.Sprintf("%06d", i)
blockChunkPath := filepath.Join("chunks", chunkName)
chunk, err := lbr.fetchFile(blockChunkPath)
if err != nil {
return fmt.Errorf("failed to fetch chunk file: %q: %w", chunkName, err)
}
if err := lbr.writeFile(temp, blockChunkPath, chunk); err != nil {
return fmt.Errorf("failed to write chunk file: %q: %s", chunkName, err)
}
}
// Set postingDecoder to nil because
// If it is nil then a default decoder is used, compatible with Prometheus v2.
pb, err := tsdb.OpenBlock(nil, temp, nil, nil)
if err != nil {
return fmt.Errorf("failed to open block %q: %w", lbr.ID, err)
}
lbr.reader = pb
return nil
}
// Index returns an IndexReader over the block's data.
func (lbr *lazyBlockReader) Index() (tsdb.IndexReader, error) {
if err := lbr.initialize(); err != nil {
return nil, err
}
return lbr.reader.Index()
}
// Chunks returns a ChunkReader over the block's data.
func (lbr *lazyBlockReader) Chunks() (tsdb.ChunkReader, error) {
if err := lbr.initialize(); err != nil {
return nil, err
}
return lbr.reader.Chunks()
}
// Tombstones returns a tombstones.Reader over the block's deleted data.
func (lbr *lazyBlockReader) Tombstones() (tombstones.Reader, error) {
if err := lbr.initialize(); err != nil {
return nil, err
}
return lbr.reader.Tombstones()
}
// Meta provides meta information about the block reader.
func (lbr *lazyBlockReader) Meta() tsdb.BlockMeta {
if err := lbr.initialize(); err != nil {
lbr.err = fmt.Errorf("cannot get BlockMeta: %w", err)
return tsdb.BlockMeta{}
}
return lbr.reader.Meta()
}
// Size returns the number of bytes that the block takes up on disk.
func (lbr *lazyBlockReader) Size() int64 {
if err := lbr.initialize(); err != nil {
lbr.err = fmt.Errorf("error get Size of the block: %s, return zero size", err)
return 0
}
return lbr.reader.Size()
}
// Err returns the last error that occurred on the block reader.
func (lbr *lazyBlockReader) Err() error {
return lbr.err
}
// Close closes block and releases all resources
func (lbr *lazyBlockReader) Close() error {
lbr.mu.Lock()
defer lbr.mu.Unlock()
if lbr.reader == nil {
return nil
}
err := lbr.reader.Close()
if err := os.RemoveAll(lbr.tempDirPath); err != nil {
log.Printf("failed to remove temp dir: %s", err)
}
lbr.reader = nil
lbr.tempDirPath = ""
return err
}
func (lbr *lazyBlockReader) mkTempDir() (string, error) {
temp, err := os.MkdirTemp("", lbr.ID.String())
if err != nil {
return "", fmt.Errorf("failed to create temp dir: %s", err)
}
err = os.Mkdir(filepath.Join(temp, "chunks"), os.ModePerm)
if err != nil {
return "", fmt.Errorf("failed to create temp dir: %s", err)
}
return temp, nil
}
func (lbr *lazyBlockReader) fetchFile(filePath string) ([]byte, error) {
blockID := lbr.ID.String()
blockPath := filepath.Join(blockID, filePath)
has, err := lbr.fs.HasFile(blockPath)
if err != nil {
return nil, err
}
if !has {
return nil, fmt.Errorf("block meta %s not found", blockID)
}
return lbr.fs.ReadFile(blockPath)
}
func (lbr *lazyBlockReader) writeFile(folder string, filename string, file []byte) error {
fileName := filepath.Join(folder, filename)
return os.WriteFile(fileName, file, os.ModePerm)
}

238
app/vmctl/mimir/mimir.go Normal file
View File

@@ -0,0 +1,238 @@
package mimir
import (
"bytes"
"compress/gzip"
"context"
"encoding/json"
"fmt"
"log"
"github.com/oklog/ulid/v2"
"github.com/prometheus/prometheus/model/labels"
"github.com/prometheus/prometheus/tsdb"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmctl/prometheus"
utils "github.com/VictoriaMetrics/VictoriaMetrics/app/vmctl/vmctlutil"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/backup/common"
)
const (
bucketIndex = "bucket-index.json"
bucketIndexCompressedFilename = bucketIndex + ".gz"
metaFilename = "meta.json"
indexFilename = "index"
)
// BlockDeletionMark holds the information about a block's deletion mark in the index.
// This type was copied from the mimir repository https://github.com/grafana/mimir/blob/main/pkg/storage/tsdb/bucketindex/index.go#L234.
type BlockDeletionMark struct {
// Block ID.
ID ulid.ULID `json:"block_id"`
// DeletionTime is a unix timestamp (seconds precision) of when the block was marked to be deleted.
DeletionTime int64 `json:"deletion_time"`
}
// Block holds the information about a block in the index.
// This is a partial implementation of the https://github.com/grafana/mimir/blob/main/pkg/storage/tsdb/bucketindex/index.go#L73
type Block struct {
// Block ID.
ID ulid.ULID `json:"block_id"`
// MinTime and MaxTime specify the time range all samples in the block are in (millis precision).
MinTime int64 `json:"min_time"`
MaxTime int64 `json:"max_time"`
// SegmentsFormat and SegmentsNum stores the format and number of chunks segments
// in the block.
SegmentsFormat string `json:"segments_format,omitempty"`
SegmentsNum int `json:"segments_num,omitempty"`
}
// Index contains all known blocks and markers of a tenant.
// This is a partial implementation pof the https://github.com/grafana/mimir/blob/main/pkg/storage/tsdb/bucketindex/index.go#L36
type Index struct {
// Version of the index format.
Version int `json:"version"`
// List of complete blocks (partial blocks are excluded from the index).
Blocks []*Block `json:"blocks"`
}
// Config contains a list of params needed
// for reading mimir snapshots
type Config struct {
// Path to remote storage bucket
Path string
// TenantID is the tenant id for the storage
TenantID string
Filter Filter
CredsFilePath string
ConfigFilePath string
ConfigProfile string
CustomS3Endpoint string
S3ForcePathStyle bool
S3TLSInsecureSkipVerify bool
SSEKMSKeyID string
SSEAlgorithm string
}
// Filter contains configuration for filtering
// the timeseries
type Filter struct {
TimeMin string
TimeMax string
Label string
LabelValue string
}
// Client is a wrapper over Prometheus tsdb.DBReader
type Client struct {
common.RemoteFS
filter filter
}
type filter struct {
min, max int64
label string
labelValue string
}
func (f filter) inRange(minTime, maxTime int64) bool {
fmin, fmax := f.min, f.max
if minTime == 0 {
fmin = minTime
}
if fmax == 0 {
fmax = maxTime
}
return minTime <= fmax && fmin <= maxTime
}
// NewClient creates and validates new Client
// with given Config
func NewClient(ctx context.Context, cfg Config) (*Client, error) {
if cfg.Path == "" {
return nil, fmt.Errorf("path cannot be empty")
}
if cfg.TenantID != "" {
cfg.Path = fmt.Sprintf("%s/%s", cfg.Path, cfg.TenantID)
}
var c Client
rfs, err := newRemoteFS(ctx, cfg)
if err != nil {
return nil, fmt.Errorf("cannot parse `-src`=%q: %w", cfg.Path, err)
}
c.RemoteFS = rfs
timeMin, err := utils.ParseTime(cfg.Filter.TimeMin)
if err != nil {
return nil, fmt.Errorf("failed to parse min time in filter: %s", err)
}
timeMax, err := utils.ParseTime(cfg.Filter.TimeMax)
if err != nil {
return nil, fmt.Errorf("failed to parse max time in filter: %s", err)
}
c.filter = filter{
min: timeMin.UnixMilli(),
max: timeMax.UnixMilli(),
label: cfg.Filter.Label,
labelValue: cfg.Filter.LabelValue,
}
return &c, nil
}
// Explore a fetches bucket-index.json file from a remote storage or local filesystem
// and filter blocks via the defined time range, but does not take into account label filters.
func (c *Client) Explore() ([]tsdb.BlockReader, error) {
log.Printf("Fetching blocks from remote storage")
indexFile, err := c.fetchIndexFile()
if err != nil {
return nil, fmt.Errorf("failed to fetch index file: %s", err)
}
var blocksToImport []tsdb.BlockReader
for _, block := range indexFile.Blocks {
if !c.filter.inRange(block.MinTime, block.MaxTime) {
// Skipping block outside of time range
continue
}
if block.ID.String() == "" {
continue
}
lazyBlockReader, err := newLazyBlockReader(block, c.RemoteFS)
if err != nil {
return nil, fmt.Errorf("failed to create lazy block reader: %s", err)
}
blocksToImport = append(blocksToImport, lazyBlockReader)
}
return blocksToImport, nil
}
// Read reads the given BlockReader according to configured
// time and label filters.
func (c *Client) Read(ctx context.Context, block tsdb.BlockReader) (*prometheus.CloseableSeriesSet, error) {
meta := block.Meta()
if b, ok := block.(*lazyBlockReader); ok && b.Err() != nil {
return nil, fmt.Errorf("failed to read block: %s", b.Err())
}
if meta.ULID.String() == "" {
return nil, fmt.Errorf("unexpected block without id")
}
minTime, maxTime := meta.MinTime, meta.MaxTime
if c.filter.min != 0 {
minTime = c.filter.min
}
if c.filter.max != 0 {
maxTime = c.filter.max
}
q, err := tsdb.NewBlockQuerier(block, minTime, maxTime)
if err != nil {
return nil, err
}
ss := q.Select(ctx, false, nil, labels.MustNewMatcher(labels.MatchRegexp, c.filter.label, c.filter.labelValue))
return &prometheus.CloseableSeriesSet{SeriesSet: ss, Close: q.Close}, nil
}
func (c *Client) fetchIndexFile() (*Index, error) {
has, err := c.HasFile(bucketIndexCompressedFilename)
if err != nil {
return nil, err
}
if !has {
return nil, fmt.Errorf("bucket-index.json.gz not found")
}
file, err := c.ReadFile(bucketIndexCompressedFilename)
if err != nil {
return nil, fmt.Errorf("failed to read bucket index: %s", err)
}
r := bytes.NewReader(file)
// Read all the content.
gzipReader, err := gzip.NewReader(r)
if err != nil {
return nil, fmt.Errorf("failed to create gzip reader: %s", err)
}
var indexFile Index
err = json.NewDecoder(gzipReader).Decode(&indexFile)
if err != nil {
return nil, fmt.Errorf("failed to decode bucket index: %s", err)
}
return &indexFile, nil
}

View File

@@ -0,0 +1,93 @@
package mimir
import (
"context"
"fmt"
"path/filepath"
"strings"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/backup/azremote"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/backup/common"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/backup/fsremote"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/backup/gcsremote"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/backup/s3remote"
)
// newRemoteFS returns new remote fs from the given Config.
func newRemoteFS(ctx context.Context, cfg Config) (common.RemoteFS, error) {
if len(cfg.Path) == 0 {
return nil, fmt.Errorf("path cannot be empty")
}
n := strings.Index(cfg.Path, "://")
if n < 0 {
return nil, fmt.Errorf("missing scheme in path %q. Supported schemes: `gs://`, `s3://`, `azblob://`, `fs://`", cfg.Path)
}
scheme := cfg.Path[:n]
dir := cfg.Path[n+len("://"):]
switch scheme {
case "fs":
if !filepath.IsAbs(dir) {
return nil, fmt.Errorf("dir must be absolute; got %q", dir)
}
fsr := &fsremote.FS{
Dir: filepath.Clean(dir),
}
return fsr, nil
case "gcs", "gs":
n := strings.Index(dir, "/")
if n < 0 {
return nil, fmt.Errorf("missing directory on the gcs bucket %q", dir)
}
bucket := dir[:n]
dir = dir[n:]
fsr := &gcsremote.FS{
CredsFilePath: cfg.CredsFilePath,
Bucket: bucket,
Dir: dir,
}
if err := fsr.Init(ctx); err != nil {
return nil, fmt.Errorf("cannot initialize connection to gcs: %w", err)
}
return fsr, nil
case "azblob":
n := strings.Index(dir, "/")
if n < 0 {
return nil, fmt.Errorf("missing directory on the AZBlob container %q", dir)
}
bucket := dir[:n]
dir = dir[n:]
fsr := &azremote.FS{
Container: bucket,
Dir: dir,
}
if err := fsr.Init(ctx); err != nil {
return nil, fmt.Errorf("cannot initialize connection to AZBlob: %w", err)
}
return fsr, nil
case "s3":
n := strings.Index(dir, "/")
if n < 0 {
return nil, fmt.Errorf("missing directory on the s3 bucket %q", dir)
}
bucket := dir[:n]
dir = dir[n:]
fsr := &s3remote.FS{
CredsFilePath: cfg.CredsFilePath,
ConfigFilePath: cfg.ConfigFilePath,
CustomEndpoint: cfg.CustomS3Endpoint,
TLSInsecureSkipVerify: cfg.S3TLSInsecureSkipVerify,
S3ForcePathStyle: cfg.S3ForcePathStyle,
ProfileName: cfg.ConfigProfile,
Bucket: bucket,
Dir: dir,
SSEKMSKeyId: cfg.SSEKMSKeyID,
SSEAlgorithm: s3remote.StringToEncryptionAlgorithm(cfg.SSEAlgorithm),
}
if err := fsr.Init(ctx); err != nil {
return nil, fmt.Errorf("cannot initialize connection to s3: %w", err)
}
return fsr, nil
default:
return nil, fmt.Errorf("unsupported scheme %q", scheme)
}
}

View File

@@ -3,6 +3,7 @@ package main
import (
"context"
"fmt"
"io"
"log"
"strings"
"sync"
@@ -18,10 +19,17 @@ import (
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmctl/vm"
)
// Runner is an interface for fetching and reading
// snapshot blocks
type Runner interface {
Explore() ([]tsdb.BlockReader, error)
Read(context.Context, tsdb.BlockReader) (*prometheus.CloseableSeriesSet, error)
}
type prometheusProcessor struct {
// prometheus client fetches and reads
// Runner fetches and reads
// snapshot blocks
cl *prometheus.Client
cl Runner
// importer performs import requests
// for timeseries data returned from
// snapshot blocks
@@ -48,7 +56,7 @@ func (pp *prometheusProcessor) run(ctx context.Context) error {
return nil
}
if err := pp.processBlocks(blocks); err != nil {
if err := pp.processBlocks(ctx, blocks); err != nil {
return fmt.Errorf("migration failed: %s", err)
}
@@ -57,11 +65,17 @@ func (pp *prometheusProcessor) run(ctx context.Context) error {
return nil
}
func (pp *prometheusProcessor) do(b tsdb.BlockReader) error {
ss, err := pp.cl.Read(b)
func (pp *prometheusProcessor) do(ctx context.Context, b tsdb.BlockReader) error {
css, err := pp.cl.Read(ctx, b)
if err != nil {
return fmt.Errorf("failed to read block: %s", err)
}
defer func() {
if err := css.Close(); err != nil {
log.Printf("cannot close SeriesSet for block: %q : %s\n", b.Meta().ULID, err)
}
}()
ss := css.SeriesSet
var it chunkenc.Iterator
for ss.Next() {
var name string
@@ -114,7 +128,7 @@ func (pp *prometheusProcessor) do(b tsdb.BlockReader) error {
return ss.Err()
}
func (pp *prometheusProcessor) processBlocks(blocks []tsdb.BlockReader) error {
func (pp *prometheusProcessor) processBlocks(ctx context.Context, blocks []tsdb.BlockReader) error {
promBlocksTotal.Add(len(blocks))
bar := barpool.AddWithTemplate(fmt.Sprintf(barTpl, "Processing blocks"), len(blocks))
if err := barpool.Start(); err != nil {
@@ -130,11 +144,16 @@ func (pp *prometheusProcessor) processBlocks(blocks []tsdb.BlockReader) error {
for range pp.cc {
wg.Go(func() {
for br := range blockReadersCh {
if err := pp.do(br); err != nil {
if err := pp.do(ctx, br); err != nil {
promErrorsTotal.Inc()
errCh <- fmt.Errorf("read failed for block %q: %s", br.Meta().ULID, err)
errCh <- fmt.Errorf("cannot read block %q: %s", br.Meta().ULID, err)
return
}
if cb, ok := br.(io.Closer); ok {
if err := cb.Close(); err != nil {
errCh <- fmt.Errorf("cannot close block: %q: %w", br.Meta().ULID, err)
}
}
promBlocksProcessed.Inc()
bar.Increment()
}

View File

@@ -8,6 +8,8 @@ import (
"github.com/prometheus/prometheus/model/labels"
"github.com/prometheus/prometheus/storage"
"github.com/prometheus/prometheus/tsdb"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmctl/vmctlutil"
)
// Config contains a list of params needed
@@ -60,13 +62,13 @@ func NewClient(cfg Config) (*Client, error) {
return nil, fmt.Errorf("failed to open snapshot %q: %s", cfg.Snapshot, err)
}
c := &Client{DBReadOnly: db}
minTime, maxTime, err := parseTime(cfg.Filter.TimeMin, cfg.Filter.TimeMax)
timeMin, timeMax, err := parseTime(cfg.Filter.TimeMin, cfg.Filter.TimeMax)
if err != nil {
return nil, fmt.Errorf("failed to parse time in filter: %s", err)
}
c.filter = filter{
min: minTime,
max: maxTime,
min: timeMin,
max: timeMax,
label: cfg.Filter.Label,
labelValue: cfg.Filter.LabelValue,
}
@@ -83,7 +85,7 @@ func (c *Client) Explore() ([]tsdb.BlockReader, error) {
if err != nil {
return nil, fmt.Errorf("failed to fetch blocks: %s", err)
}
s := &Stats{
s := &vmctlutil.Stats{
Filtered: c.filter.min != 0 || c.filter.max != 0 || c.filter.label != "",
Blocks: len(blocks),
}
@@ -108,9 +110,15 @@ func (c *Client) Explore() ([]tsdb.BlockReader, error) {
return blocksToImport, nil
}
// CloseableSeriesSet defines a SeriesSet with Close method
type CloseableSeriesSet struct {
SeriesSet storage.SeriesSet
Close func() error
}
// Read reads the given BlockReader according to configured
// time and label filters.
func (c *Client) Read(block tsdb.BlockReader) (storage.SeriesSet, error) {
func (c *Client) Read(ctx context.Context, block tsdb.BlockReader) (*CloseableSeriesSet, error) {
minTime, maxTime := block.Meta().MinTime, block.Meta().MaxTime
if c.filter.min != 0 {
minTime = c.filter.min
@@ -122,8 +130,8 @@ func (c *Client) Read(block tsdb.BlockReader) (storage.SeriesSet, error) {
if err != nil {
return nil, err
}
ss := q.Select(context.Background(), false, nil, labels.MustNewMatcher(labels.MatchRegexp, c.filter.label, c.filter.labelValue))
return ss, nil
ss := q.Select(ctx, false, nil, labels.MustNewMatcher(labels.MatchRegexp, c.filter.label, c.filter.labelValue))
return &CloseableSeriesSet{ss, q.Close}, nil
}
func parseTime(start, end string) (int64, int64, error) {

View File

@@ -1,4 +1,4 @@
package prometheus
package vmctlutil
import (
"fmt"
@@ -18,7 +18,7 @@ type Stats struct {
// String returns string representation for s.
func (s Stats) String() string {
str := fmt.Sprintf("Prometheus snapshot stats:\n"+
str := fmt.Sprintf("Snapshot stats:\n"+
" blocks found: %d;\n"+
" blocks skipped by time filter: %d;\n"+
" min time: %d (%v);\n"+

View File

@@ -20,6 +20,9 @@ func TestGetTime_Failure(t *testing.T) {
// negative time
f("-292273086-05-16T16:47:06Z")
// relative duration that resolves to a timestamp before 1970
f("-9223372036.855")
}
func TestGetTime_Success(t *testing.T) {
@@ -77,9 +80,6 @@ func TestGetTime_Success(t *testing.T) {
// float timestamp representation",
f("1562529662.324", time.Date(2019, 7, 7, 20, 01, 02, 324e6, time.UTC))
// negative timestamp
f("-9223372036.855", time.Date(1970, 01, 01, 00, 00, 00, 00, time.UTC))
// big timestamp
f("1223372036855", time.Date(2008, 10, 7, 9, 33, 56, 855e6, time.UTC))

View File

@@ -89,6 +89,7 @@ var staticServer = http.FileServer(http.FS(staticFiles))
func Init() {
relabel.Init()
common.InitStreamAggr()
opentelemetry.Init()
protoparserutil.StartUnmarshalWorkers()
if len(*graphiteListenAddr) > 0 {
graphiteServer = graphiteserver.MustStart(*graphiteListenAddr, *graphiteUseProxyProtocol, graphite.InsertHandler)

View File

@@ -20,6 +20,11 @@ var (
metadataInserted = metrics.NewCounter(`vm_metadata_rows_inserted_total{type="opentelemetry"}`)
)
// Init must be called after flag.Parse and before using the opentelemetry package.
func Init() {
stream.InitDecodeOptions()
}
// InsertHandler processes opentelemetry metrics.
func InsertHandler(req *http.Request) error {
extraLabels, err := protoparserutil.GetExtraLabels(req)

View File

@@ -60,15 +60,17 @@ func getDefaultMaxConcurrentRequests() int {
// Init initializes vmselect
func Init() {
tmpDirPath := *vmstorage.DataPath + "/tmp"
tmpDirPath := vmstorage.DataPath() + "/tmp"
fs.MustRemoveDirContents(tmpDirPath)
netstorage.InitTmpBlocksDir(tmpDirPath)
promql.InitRollupResultCache(*vmstorage.DataPath + "/cache/rollupResult")
promql.InitRollupResultCache(vmstorage.DataPath() + "/cache/rollupResult")
prometheus.InitMaxUniqueTimeseries(*maxConcurrentRequests)
concurrencyLimitCh = make(chan struct{}, *maxConcurrentRequests)
initVMUIConfig()
initVMAlertProxy()
flagutil.RegisterSecretFlag("vmalert.proxyURL")
}
// Stop stops vmselect

View File

@@ -990,9 +990,6 @@ func ExportBlocks(qt *querytracer.Tracer, sq *storage.SearchQuery, deadline sear
return fmt.Errorf("timeout exceeded before starting data export: %s", deadline.String())
}
tr := sq.GetTimeRange()
if err := vmstorage.CheckTimeRange(tr); err != nil {
return err
}
tfss, err := setupTfss(qt, tr, sq.TagFilterss, sq.MaxMetrics, deadline)
if err != nil {
return err
@@ -1098,9 +1095,6 @@ func SearchMetricNames(qt *querytracer.Tracer, sq *storage.SearchQuery, deadline
// Setup search.
tr := sq.GetTimeRange()
if err := vmstorage.CheckTimeRange(tr); err != nil {
return nil, err
}
tfss, err := setupTfss(qt, tr, sq.TagFilterss, sq.MaxMetrics, deadline)
if err != nil {
return nil, err
@@ -1127,9 +1121,6 @@ func ProcessSearchQuery(qt *querytracer.Tracer, sq *storage.SearchQuery, deadlin
// Setup search.
tr := sq.GetTimeRange()
if err := vmstorage.CheckTimeRange(tr); err != nil {
return nil, err
}
tfss, err := setupTfss(qt, tr, sq.TagFilterss, sq.MaxMetrics, deadline)
if err != nil {
return nil, err

View File

@@ -2,13 +2,16 @@
"math"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmselect/netstorage"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/bytesutil"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promrelabel"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/storage"
) %}
{% stripspace %}
// Federate writes rs in /federate format.
// See https://prometheus.io/docs/prometheus/latest/federation/
{% func Federate(rs *netstorage.Result) %}
{% func Federate(rs *netstorage.Result, escapeScheme string) %}
{% code
values := rs.Values
timestamps := rs.Timestamps
@@ -24,10 +27,54 @@
See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/3185
{% endcomment %}
{% return %}
{% endif %}
{%= prometheusMetricName(&rs.MetricName) %}{% space %}
{% endif %}
{% switch escapeScheme %}
{% case federateEscapeSchemeUTF8 %}
{%= prometheusFederateMetricNameUTF8(&rs.MetricName) %}{% space %}
{% case federateEscapeSchemeUnderscore %}
{%= prometheusFederateMetricNameEscapeUnderscore(&rs.MetricName) %}{% space %}
{% case "" %}
{%= prometheusMetricName(&rs.MetricName) %}{% space %}
{% endswitch %}
{%f= lastValue %}{% space %}
{%dl= timestamps[len(timestamps)-1] %}{% newline %}
{% endfunc %}
{% func prometheusFederateMetricNameEscapeUnderscore(mn *storage.MetricName) %}
{%s= promrelabel.SanitizeMetricName(bytesutil.ToUnsafeString(mn.MetricGroup)) %}
{% if len(mn.Tags) > 0 %}
{
{% code tags := mn.Tags %}
{%s= promrelabel.SanitizeLabelName(bytesutil.ToUnsafeString(tags[0].Key)) %}={%= escapePrometheusLabel(tags[0].Value) %}
{% code tags = tags[1:] %}
{% for i := range tags %}
{% code tag := &tags[i] %}
,{%s= promrelabel.SanitizeLabelName(bytesutil.ToUnsafeString(tag.Key)) %}={%= escapePrometheusLabel(tag.Value) %}
{% endfor %}
}
{% endif %}
{% endfunc %}
{% func prometheusFederateMetricNameUTF8(mn *storage.MetricName) %}
{
{%= escapePrometheusLabel(mn.MetricGroup) %}
{% if len(mn.Tags) > 0 %}
,
{% code tags := mn.Tags %}
{%= escapePrometheusLabel(tags[0].Key) %}={%= escapePrometheusLabel(tags[0].Value) %}
{% code tags = tags[1:] %}
{% for i := range tags %}
{% code tag := &tags[i] %}
,{%= escapePrometheusLabel(tag.Key) %}={%= escapePrometheusLabel(tag.Value) %}
{% endfor %}
{% endif %}
}
{% endfunc %}
{% endstripspace %}

View File

@@ -9,82 +9,241 @@ import (
"math"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmselect/netstorage"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/bytesutil"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promrelabel"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/storage"
)
// Federate writes rs in /federate format.// See https://prometheus.io/docs/prometheus/latest/federation/
//line app/vmselect/prometheus/federate.qtpl:11
//line app/vmselect/prometheus/federate.qtpl:14
import (
qtio422016 "io"
qt422016 "github.com/valyala/quicktemplate"
)
//line app/vmselect/prometheus/federate.qtpl:11
//line app/vmselect/prometheus/federate.qtpl:14
var (
_ = qtio422016.Copy
_ = qt422016.AcquireByteBuffer
)
//line app/vmselect/prometheus/federate.qtpl:11
func StreamFederate(qw422016 *qt422016.Writer, rs *netstorage.Result) {
//line app/vmselect/prometheus/federate.qtpl:13
//line app/vmselect/prometheus/federate.qtpl:14
func StreamFederate(qw422016 *qt422016.Writer, rs *netstorage.Result, escapeScheme string) {
//line app/vmselect/prometheus/federate.qtpl:16
values := rs.Values
timestamps := rs.Timestamps
//line app/vmselect/prometheus/federate.qtpl:16
//line app/vmselect/prometheus/federate.qtpl:19
if len(timestamps) == 0 || len(values) == 0 {
//line app/vmselect/prometheus/federate.qtpl:16
//line app/vmselect/prometheus/federate.qtpl:19
return
//line app/vmselect/prometheus/federate.qtpl:16
//line app/vmselect/prometheus/federate.qtpl:19
}
//line app/vmselect/prometheus/federate.qtpl:18
//line app/vmselect/prometheus/federate.qtpl:21
lastValue := values[len(values)-1]
//line app/vmselect/prometheus/federate.qtpl:20
//line app/vmselect/prometheus/federate.qtpl:23
if math.IsNaN(lastValue) {
//line app/vmselect/prometheus/federate.qtpl:26
//line app/vmselect/prometheus/federate.qtpl:29
return
//line app/vmselect/prometheus/federate.qtpl:27
//line app/vmselect/prometheus/federate.qtpl:30
}
//line app/vmselect/prometheus/federate.qtpl:28
streamprometheusMetricName(qw422016, &rs.MetricName)
//line app/vmselect/prometheus/federate.qtpl:28
qw422016.N().S(` `)
//line app/vmselect/prometheus/federate.qtpl:29
//line app/vmselect/prometheus/federate.qtpl:32
switch escapeScheme {
//line app/vmselect/prometheus/federate.qtpl:33
case federateEscapeSchemeUTF8:
//line app/vmselect/prometheus/federate.qtpl:34
streamprometheusFederateMetricNameUTF8(qw422016, &rs.MetricName)
//line app/vmselect/prometheus/federate.qtpl:34
qw422016.N().S(` `)
//line app/vmselect/prometheus/federate.qtpl:36
case federateEscapeSchemeUnderscore:
//line app/vmselect/prometheus/federate.qtpl:37
streamprometheusFederateMetricNameEscapeUnderscore(qw422016, &rs.MetricName)
//line app/vmselect/prometheus/federate.qtpl:37
qw422016.N().S(` `)
//line app/vmselect/prometheus/federate.qtpl:39
case "":
//line app/vmselect/prometheus/federate.qtpl:40
streamprometheusMetricName(qw422016, &rs.MetricName)
//line app/vmselect/prometheus/federate.qtpl:40
qw422016.N().S(` `)
//line app/vmselect/prometheus/federate.qtpl:41
}
//line app/vmselect/prometheus/federate.qtpl:43
qw422016.N().F(lastValue)
//line app/vmselect/prometheus/federate.qtpl:29
//line app/vmselect/prometheus/federate.qtpl:43
qw422016.N().S(` `)
//line app/vmselect/prometheus/federate.qtpl:30
//line app/vmselect/prometheus/federate.qtpl:44
qw422016.N().DL(timestamps[len(timestamps)-1])
//line app/vmselect/prometheus/federate.qtpl:30
//line app/vmselect/prometheus/federate.qtpl:44
qw422016.N().S(`
`)
//line app/vmselect/prometheus/federate.qtpl:31
//line app/vmselect/prometheus/federate.qtpl:45
}
//line app/vmselect/prometheus/federate.qtpl:31
func WriteFederate(qq422016 qtio422016.Writer, rs *netstorage.Result) {
//line app/vmselect/prometheus/federate.qtpl:31
//line app/vmselect/prometheus/federate.qtpl:45
func WriteFederate(qq422016 qtio422016.Writer, rs *netstorage.Result, escapeScheme string) {
//line app/vmselect/prometheus/federate.qtpl:45
qw422016 := qt422016.AcquireWriter(qq422016)
//line app/vmselect/prometheus/federate.qtpl:31
StreamFederate(qw422016, rs)
//line app/vmselect/prometheus/federate.qtpl:31
//line app/vmselect/prometheus/federate.qtpl:45
StreamFederate(qw422016, rs, escapeScheme)
//line app/vmselect/prometheus/federate.qtpl:45
qt422016.ReleaseWriter(qw422016)
//line app/vmselect/prometheus/federate.qtpl:31
//line app/vmselect/prometheus/federate.qtpl:45
}
//line app/vmselect/prometheus/federate.qtpl:31
func Federate(rs *netstorage.Result) string {
//line app/vmselect/prometheus/federate.qtpl:31
//line app/vmselect/prometheus/federate.qtpl:45
func Federate(rs *netstorage.Result, escapeScheme string) string {
//line app/vmselect/prometheus/federate.qtpl:45
qb422016 := qt422016.AcquireByteBuffer()
//line app/vmselect/prometheus/federate.qtpl:31
WriteFederate(qb422016, rs)
//line app/vmselect/prometheus/federate.qtpl:31
//line app/vmselect/prometheus/federate.qtpl:45
WriteFederate(qb422016, rs, escapeScheme)
//line app/vmselect/prometheus/federate.qtpl:45
qs422016 := string(qb422016.B)
//line app/vmselect/prometheus/federate.qtpl:31
//line app/vmselect/prometheus/federate.qtpl:45
qt422016.ReleaseByteBuffer(qb422016)
//line app/vmselect/prometheus/federate.qtpl:31
//line app/vmselect/prometheus/federate.qtpl:45
return qs422016
//line app/vmselect/prometheus/federate.qtpl:31
//line app/vmselect/prometheus/federate.qtpl:45
}
//line app/vmselect/prometheus/federate.qtpl:47
func streamprometheusFederateMetricNameEscapeUnderscore(qw422016 *qt422016.Writer, mn *storage.MetricName) {
//line app/vmselect/prometheus/federate.qtpl:48
qw422016.N().S(promrelabel.SanitizeMetricName(bytesutil.ToUnsafeString(mn.MetricGroup)))
//line app/vmselect/prometheus/federate.qtpl:49
if len(mn.Tags) > 0 {
//line app/vmselect/prometheus/federate.qtpl:49
qw422016.N().S(`{`)
//line app/vmselect/prometheus/federate.qtpl:51
tags := mn.Tags
//line app/vmselect/prometheus/federate.qtpl:52
qw422016.N().S(promrelabel.SanitizeLabelName(bytesutil.ToUnsafeString(tags[0].Key)))
//line app/vmselect/prometheus/federate.qtpl:52
qw422016.N().S(`=`)
//line app/vmselect/prometheus/federate.qtpl:52
streamescapePrometheusLabel(qw422016, tags[0].Value)
//line app/vmselect/prometheus/federate.qtpl:53
tags = tags[1:]
//line app/vmselect/prometheus/federate.qtpl:54
for i := range tags {
//line app/vmselect/prometheus/federate.qtpl:55
tag := &tags[i]
//line app/vmselect/prometheus/federate.qtpl:55
qw422016.N().S(`,`)
//line app/vmselect/prometheus/federate.qtpl:56
qw422016.N().S(promrelabel.SanitizeLabelName(bytesutil.ToUnsafeString(tag.Key)))
//line app/vmselect/prometheus/federate.qtpl:56
qw422016.N().S(`=`)
//line app/vmselect/prometheus/federate.qtpl:56
streamescapePrometheusLabel(qw422016, tag.Value)
//line app/vmselect/prometheus/federate.qtpl:57
}
//line app/vmselect/prometheus/federate.qtpl:57
qw422016.N().S(`}`)
//line app/vmselect/prometheus/federate.qtpl:59
}
//line app/vmselect/prometheus/federate.qtpl:60
}
//line app/vmselect/prometheus/federate.qtpl:60
func writeprometheusFederateMetricNameEscapeUnderscore(qq422016 qtio422016.Writer, mn *storage.MetricName) {
//line app/vmselect/prometheus/federate.qtpl:60
qw422016 := qt422016.AcquireWriter(qq422016)
//line app/vmselect/prometheus/federate.qtpl:60
streamprometheusFederateMetricNameEscapeUnderscore(qw422016, mn)
//line app/vmselect/prometheus/federate.qtpl:60
qt422016.ReleaseWriter(qw422016)
//line app/vmselect/prometheus/federate.qtpl:60
}
//line app/vmselect/prometheus/federate.qtpl:60
func prometheusFederateMetricNameEscapeUnderscore(mn *storage.MetricName) string {
//line app/vmselect/prometheus/federate.qtpl:60
qb422016 := qt422016.AcquireByteBuffer()
//line app/vmselect/prometheus/federate.qtpl:60
writeprometheusFederateMetricNameEscapeUnderscore(qb422016, mn)
//line app/vmselect/prometheus/federate.qtpl:60
qs422016 := string(qb422016.B)
//line app/vmselect/prometheus/federate.qtpl:60
qt422016.ReleaseByteBuffer(qb422016)
//line app/vmselect/prometheus/federate.qtpl:60
return qs422016
//line app/vmselect/prometheus/federate.qtpl:60
}
//line app/vmselect/prometheus/federate.qtpl:62
func streamprometheusFederateMetricNameUTF8(qw422016 *qt422016.Writer, mn *storage.MetricName) {
//line app/vmselect/prometheus/federate.qtpl:62
qw422016.N().S(`{`)
//line app/vmselect/prometheus/federate.qtpl:64
streamescapePrometheusLabel(qw422016, mn.MetricGroup)
//line app/vmselect/prometheus/federate.qtpl:65
if len(mn.Tags) > 0 {
//line app/vmselect/prometheus/federate.qtpl:65
qw422016.N().S(`,`)
//line app/vmselect/prometheus/federate.qtpl:67
tags := mn.Tags
//line app/vmselect/prometheus/federate.qtpl:68
streamescapePrometheusLabel(qw422016, tags[0].Key)
//line app/vmselect/prometheus/federate.qtpl:68
qw422016.N().S(`=`)
//line app/vmselect/prometheus/federate.qtpl:68
streamescapePrometheusLabel(qw422016, tags[0].Value)
//line app/vmselect/prometheus/federate.qtpl:69
tags = tags[1:]
//line app/vmselect/prometheus/federate.qtpl:70
for i := range tags {
//line app/vmselect/prometheus/federate.qtpl:71
tag := &tags[i]
//line app/vmselect/prometheus/federate.qtpl:71
qw422016.N().S(`,`)
//line app/vmselect/prometheus/federate.qtpl:72
streamescapePrometheusLabel(qw422016, tag.Key)
//line app/vmselect/prometheus/federate.qtpl:72
qw422016.N().S(`=`)
//line app/vmselect/prometheus/federate.qtpl:72
streamescapePrometheusLabel(qw422016, tag.Value)
//line app/vmselect/prometheus/federate.qtpl:73
}
//line app/vmselect/prometheus/federate.qtpl:74
}
//line app/vmselect/prometheus/federate.qtpl:74
qw422016.N().S(`}`)
//line app/vmselect/prometheus/federate.qtpl:76
}
//line app/vmselect/prometheus/federate.qtpl:76
func writeprometheusFederateMetricNameUTF8(qq422016 qtio422016.Writer, mn *storage.MetricName) {
//line app/vmselect/prometheus/federate.qtpl:76
qw422016 := qt422016.AcquireWriter(qq422016)
//line app/vmselect/prometheus/federate.qtpl:76
streamprometheusFederateMetricNameUTF8(qw422016, mn)
//line app/vmselect/prometheus/federate.qtpl:76
qt422016.ReleaseWriter(qw422016)
//line app/vmselect/prometheus/federate.qtpl:76
}
//line app/vmselect/prometheus/federate.qtpl:76
func prometheusFederateMetricNameUTF8(mn *storage.MetricName) string {
//line app/vmselect/prometheus/federate.qtpl:76
qb422016 := qt422016.AcquireByteBuffer()
//line app/vmselect/prometheus/federate.qtpl:76
writeprometheusFederateMetricNameUTF8(qb422016, mn)
//line app/vmselect/prometheus/federate.qtpl:76
qs422016 := string(qb422016.B)
//line app/vmselect/prometheus/federate.qtpl:76
qt422016.ReleaseByteBuffer(qb422016)
//line app/vmselect/prometheus/federate.qtpl:76
return qs422016
//line app/vmselect/prometheus/federate.qtpl:76
}

View File

@@ -8,15 +8,15 @@ import (
)
func TestFederate(t *testing.T) {
f := func(rs *netstorage.Result, expectedResult string) {
f := func(rs *netstorage.Result, escapeScheme string, expectedResult string) {
t.Helper()
result := Federate(rs)
result := Federate(rs, escapeScheme)
if result != expectedResult {
t.Fatalf("unexpected result; got\n%s\nwant\n%s", result, expectedResult)
}
}
f(&netstorage.Result{}, ``)
f(&netstorage.Result{}, ``, ``)
f(&netstorage.Result{
MetricName: storage.MetricName{
@@ -39,5 +39,60 @@ func TestFederate(t *testing.T) {
},
Values: []float64{1.23},
Timestamps: []int64{123},
}, `foo{a="b",qqq="\\",abc="a<b\"\\c"} 1.23 123`+"\n")
}, ``, `foo{a="b",qqq="\\",abc="a<b\"\\c"} 1.23 123`+"\n")
f(&netstorage.Result{
MetricName: storage.MetricName{
MetricGroup: []byte("foo.bar"),
Tags: []storage.Tag{
{
Key: []byte("some.!other"),
Value: []byte("value.unchanged!."),
},
{
Key: []byte("qqq"),
Value: []byte("\\"),
},
{
Key: []byte("!key"),
Value: []byte("value"),
},
{
Key: []byte("abc"),
// Verify that < isn't encoded. See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/5431
Value: []byte("a<b\"\\c"),
},
},
},
Values: []float64{1.23},
Timestamps: []int64{123},
}, federateEscapeSchemeUnderscore, `foo_bar{some__other="value.unchanged!.",qqq="\\",_key="value",abc="a<b\"\\c"} 1.23 123`+"\n")
f(&netstorage.Result{
MetricName: storage.MetricName{
MetricGroup: []byte("foo.bar"),
Tags: []storage.Tag{
{
Key: []byte("some.!other"),
Value: []byte("value.unchanged!."),
},
{
Key: []byte("qqq"),
Value: []byte("\\"),
},
{
Key: []byte("!key"),
Value: []byte("value"),
},
{
Key: []byte(`ab"c`),
// Verify that < isn't encoded. See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/5431
Value: []byte("a<b\"\\c"),
},
},
},
Values: []float64{1.23},
Timestamps: []int64{123},
}, federateEscapeSchemeUTF8, `{"foo.bar","some.!other"="value.unchanged!.","qqq"="\\","!key"="value","ab\"c"="a<b\"\\c"} 1.23 123`+"\n")
}

View File

@@ -9,16 +9,17 @@ import (
)
func BenchmarkFederate(b *testing.B) {
rs := &netstorage.Result{
MetricName: storage.MetricName{
MetricGroup: []byte("foo_bar_bazaaaa_total"),
MetricGroup: []byte("foo_bar_?_._bazaaaa_total"),
Tags: []storage.Tag{
{
Key: []byte("instance"),
Key: []byte("instance:job"),
Value: []byte("foobarbaz:2344"),
},
{
Key: []byte("job"),
Key: []byte("job.name"),
Value: []byte("aaabbbccc"),
},
},
@@ -27,12 +28,22 @@ func BenchmarkFederate(b *testing.B) {
Timestamps: []int64{1234567890},
}
b.ReportAllocs()
b.RunParallel(func(pb *testing.PB) {
var bb bytes.Buffer
for pb.Next() {
bb.Reset()
WriteFederate(&bb, rs)
}
})
f := func(name, escapeScheme string) {
b.Helper()
b.Run(name, func(b *testing.B) {
b.ReportAllocs()
b.RunParallel(func(pb *testing.PB) {
var bb bytes.Buffer
for pb.Next() {
bb.Reset()
WriteFederate(&bb, rs, escapeScheme)
}
})
})
}
f("without escape", "")
f("allow-utf-8", federateEscapeSchemeUTF8)
f("legacy-underscore", federateEscapeSchemeUnderscore)
}

View File

@@ -108,6 +108,11 @@ func PrettifyQuery(w http.ResponseWriter, r *http.Request) {
_ = bw.Flush()
}
const (
federateEscapeSchemeUnderscore = "underscore"
federateEscapeSchemeUTF8 = "utf-8"
)
// FederateHandler implements /federate . See https://prometheus.io/docs/prometheus/latest/federation/
func FederateHandler(startTime time.Time, w http.ResponseWriter, r *http.Request) error {
defer federateDuration.UpdateDuration(startTime)
@@ -132,6 +137,21 @@ func FederateHandler(startTime time.Time, w http.ResponseWriter, r *http.Request
return fmt.Errorf("cannot fetch data for %q: %w", sq, err)
}
// add best-effort format negotiation
// modern version of Prometheus always set allow-utf-8 in order to properly parse utf-8 names and labels
// prometheus below v3 uses underscore escaping by default and it's the most common standard
var escapeScheme string
accept := r.Header.Get("Accept")
if len(accept) > 0 && strings.Contains(accept, "allow-utf-8") {
escapeScheme = federateEscapeSchemeUTF8
}
// try fallback to legacy underscore escaping if needed for Prometheus only,
// it's not widely used after Prometheus v3.0 release
// most of the Prometheus scrapers already use allow-utf-8 header
isPrometheus := strings.HasPrefix(r.UserAgent(), "Prometheus")
if len(escapeScheme) == 0 && isPrometheus {
escapeScheme = federateEscapeSchemeUnderscore
}
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
bw := bufferedwriter.Get(w)
defer bufferedwriter.Put(bw)
@@ -141,7 +161,7 @@ func FederateHandler(startTime time.Time, w http.ResponseWriter, r *http.Request
return err
}
bb := sw.getBuffer(workerID)
WriteFederate(bb, rs)
WriteFederate(bb, rs, escapeScheme)
return sw.maybeFlushBuffer(bb)
})
if err == nil {

View File

@@ -8801,6 +8801,17 @@ func TestExecSuccess(t *testing.T) {
resultExpected := []netstorage.Result{r}
f(q, resultExpected)
})
t.Run(`range()`, func(t *testing.T) {
t.Parallel()
q := `range()`
r := netstorage.Result{
MetricName: metricNameExpected,
Values: []float64{1000, 1000, 1000, 1000, 1000, 1000},
Timestamps: timestampsExpected,
}
resultExpected := []netstorage.Result{r}
f(q, resultExpected)
})
t.Run(`step()`, func(t *testing.T) {
t.Parallel()
q := `time() / step()`

View File

@@ -2439,8 +2439,15 @@ func rollupIntegrate(rfa *rollupFuncArg) float64 {
prevTimestamp = timestamp
prevValue = v
}
dt := float64(rfa.currTimestamp-prevTimestamp) / 1e3
sum += prevValue * dt
// Only extrapolate the last value through to currTimestamp when the time
// series has any sample after the lookbehind window. When realNextValue is
// NaN the series has effectively ended at prevTimestamp, so accruing area
// past it would overcount the integral.
// See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/9474
if !math.IsNaN(rfa.realNextValue) {
dt := float64(rfa.currTimestamp-prevTimestamp) / 1e3
sum += prevValue * dt
}
return sum
}

View File

@@ -1385,10 +1385,65 @@ func TestRollupFuncsNoWindow(t *testing.T) {
if samplesScanned != 24 {
t.Fatalf("expecting 24 samplesScanned from rollupConfig.Do; got %d", samplesScanned)
}
valuesExpected := []float64{nan, 2.148, 1.593, 1.156, 1.36}
// At tEnd=160 the series has no samples past the window (last sample is at
// ts=130), so integrate() must not extrapolate prevValue through tEnd.
// See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/9474
valuesExpected := []float64{nan, 2.148, 1.593, 1.156, 0.34}
timestampsExpected := []int64{0, 40, 80, 120, 160}
testRowsEqual(t, values, rc.Timestamps, valuesExpected, timestampsExpected)
})
t.Run("integrate_past_series_end", func(t *testing.T) {
// Constant series of value 1.0 from t=0..3600s (1h) at 60s step.
// Query integrate(metric[1h]) across t=0..10800s with 600s step.
// For t=0..3600s the window overlap with the data is [0,t], so the integral grows from 0 to 3600 (seconds).
// After the series ends, integrate must NOT keep accruing 3600 — it
// should taper to 0 once the lookbehind window is entirely past the
// last sample.
// See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/9474
var testValues []int64
var testTimestamps []float64
for t := int64(0); t <= 3600_000; t += 60_000 {
testValues = append(testValues, t)
testTimestamps = append(testTimestamps, 1.0)
}
rc := rollupConfig{
Func: rollupIntegrate,
Start: 0,
End: 10800_000,
Step: 600_000,
Window: 3600_000,
MaxPointsPerSeries: 1e4,
}
rc.Timestamps = rc.getTimestamps()
values, _ := rc.Do(nil, testTimestamps, testValues)
for i, ti := range rc.Timestamps {
v := values[i]
// For t<=3600s: window overlap is [0,ti], integral equals ti in seconds.
if ti <= 3600_000 {
expV := float64(ti / 1e3)
if v != expV {
t.Fatalf("unexpected integrate result at t=%ds, want=%.3f got=%.3f", ti/1e3, expV, v)
}
continue
}
// For 3600s<t<7200s: data is partially outside the window, so the
// integral shrinks linearly from 3600 to 0 as t approaches 7200s.
if ti > 3600_000 && ti < 7200_000 {
expV := float64((7200_000 - ti) / 1e3)
if v != expV {
t.Fatalf("unexpected integrate result at t=%ds, want=%.3f got=%.3f", ti/1e3, expV, v)
}
continue
}
if ti >= 7200_000 {
// Window entirely past data end: must be NaN.
if !math.IsNaN(v) {
t.Fatalf("unexpected integrate result at t=%ds, want=NaN got=%.3f", ti/1e3, v)
}
}
}
})
t.Run("distinct_over_time_1", func(t *testing.T) {
rc := rollupConfig{
Func: rollupDistinct,

View File

@@ -90,6 +90,7 @@ var transformFuncs = map[string]transformFunc{
"rand": newTransformRand(newRandFloat64),
"rand_exponential": newTransformRand(newRandExpFloat64),
"rand_normal": newTransformRand(newRandNormFloat64),
"range": newTransformFuncZeroArgs(transformRange),
"range_avg": newTransformFuncRange(runningAvg),
"range_first": transformRangeFirst,
"range_last": transformRangeLast,
@@ -2808,6 +2809,10 @@ func transformEnd(tfa *transformFuncArg) float64 {
return float64(tfa.ec.End) / 1e3
}
func transformRange(tfa *transformFuncArg) float64 {
return float64(tfa.ec.End-tfa.ec.Start) / 1e3
}
// copyTimeseries returns a copy of tss.
func copyTimeseries(tss []*timeseries) []*timeseries {
rvs := make([]*timeseries, len(tss))

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -37,11 +37,11 @@
<meta property="og:title" content="UI for VictoriaMetrics">
<meta property="og:url" content="https://victoriametrics.com/">
<meta property="og:description" content="Explore and troubleshoot your VictoriaMetrics data">
<script type="module" crossorigin src="./assets/index-C7gvW_Zn.js"></script>
<script type="module" crossorigin src="./assets/index-U3iNn2Tx.js"></script>
<link rel="modulepreload" crossorigin href="./assets/rolldown-runtime-COnpUsM8.js">
<link rel="modulepreload" crossorigin href="./assets/vendor-C8Kwp93_.js">
<link rel="stylesheet" crossorigin href="./assets/vendor-CnsZ1jie.css">
<link rel="stylesheet" crossorigin href="./assets/index-D2OEy8Ra.css">
<link rel="stylesheet" crossorigin href="./assets/index-BL7jEFBa.css">
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>

View File

@@ -31,6 +31,7 @@ import (
)
var (
storageDataPath = flag.String("storageDataPath", "victoria-metrics-data", "Path to storage data")
retentionPeriod = flagutil.NewRetentionDuration("retentionPeriod", "1M", "Data with timestamps outside the retentionPeriod is automatically deleted. The minimum retentionPeriod is 24h or 1d. "+
"See https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/#retention. See also -retentionFilter")
futureRetention = flagutil.NewRetentionDuration("futureRetention", "2d", "Data with timestamps bigger than now+futureRetention is automatically deleted. "+
@@ -43,9 +44,6 @@ var (
precisionBits = flag.Int("precisionBits", 64, "The number of precision bits to store per each value. Lower precision bits improves data compression at the cost of precision loss")
// DataPath is a path to storage data.
DataPath = flag.String("storageDataPath", "victoria-metrics-data", "Path to storage data")
_ = flag.Duration("finalMergeDelay", 0, "Deprecated: this flag does nothing")
_ = flag.Int("bigMergeConcurrency", 0, "Deprecated: this flag does nothing")
_ = flag.Int("smallMergeConcurrency", 0, "Deprecated: this flag does nothing")
@@ -53,11 +51,17 @@ var (
retentionTimezoneOffset = flag.Duration("retentionTimezoneOffset", 0, "The offset for performing indexdb rotation. "+
"If set to 0, then the indexdb rotation is performed at 4am UTC time per each -retentionPeriod. "+
"If set to 2h, then the indexdb rotation is performed at 4am EET time (the timezone with +2h offset)")
minScrapeInterval = flag.Duration("dedup.minScrapeInterval", 0, "Leave only the last sample in every time series per each discrete interval "+
"equal to -dedup.minScrapeInterval > 0. See also -streamAggr.dedupInterval and https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/#deduplication")
inmemoryDataFlushInterval = flag.Duration("inmemoryDataFlushInterval", 5*time.Second, "The interval for guaranteed saving of in-memory data to disk. "+
"The saved data survives unclean shutdowns such as OOM crash, hardware reset, SIGKILL, etc. "+
"Bigger intervals may help increase the lifetime of flash storage with limited write cycles (e.g. Raspberry PI). "+
"Smaller intervals increase disk IO load. Minimum supported value is 1s")
logNewSeries = flag.Bool("logNewSeries", false, "Whether to log new series. This option is for debug purposes only. It can lead to performance issues "+
"when big number of new series are ingested into VictoriaMetrics")
denyQueriesOutsideRetention = flag.Bool("denyQueriesOutsideRetention", false, "Whether to deny queries outside the configured -retentionPeriod. "+
"When set, then /api/v1/query_range would return '503 Service Unavailable' error for queries with 'from' value outside -retentionPeriod. "+
denyQueriesOutsideRetention = flag.Bool("denyQueriesOutsideRetention", false, "Whether to deny queries outside the configured -retentionPeriod and -futureRetention. "+
"When set, then /api/v1/query_range will return an error for queries with 'from' value outside -retentionPeriod or 'to' value beyond -futureRetention. "+
"This may be useful when multiple data sources with distinct retentions are hidden behind query-tee")
maxHourlySeries = flag.Int64("storage.maxHourlySeries", 0, "The maximum number of unique series can be added to the storage during the last hour. "+
"Excess series are logged and dropped. This can be useful for limiting series cardinality. See https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/#cardinality-limiter . "+
@@ -70,6 +74,11 @@ var (
minFreeDiskSpaceBytes = flagutil.NewBytes("storage.minFreeDiskSpaceBytes", 100e6, "The minimum free disk space at -storageDataPath after which the storage stops accepting new data")
finalDedupScheduleInterval = flag.Duration("storage.finalDedupScheduleCheckInterval", time.Hour, "The interval for checking when final deduplication process should be started."+
"Storage unconditionally adds 25% jitter to the interval value on each check evaluation."+
" Changing the interval to the bigger values may delay downsampling, deduplication for historical data."+
" See also https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/#deduplication")
cacheSizeStorageTSID = flagutil.NewBytes("storage.cacheSizeStorageTSID", 0, "Overrides max size for storage/tsid cache. "+
"See https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/#cache-tuning")
cacheSizeStorageMetricName = flagutil.NewBytes("storage.cacheSizeStorageMetricName", 0, "Overrides max size for storage/metricName cache. "+
@@ -103,19 +112,8 @@ var (
"If set to 0 or a negative value, defaults to 1% of allowed memory.")
)
// CheckTimeRange returns true if the given tr is denied for querying.
func CheckTimeRange(tr storage.TimeRange) error {
if !*denyQueriesOutsideRetention {
return nil
}
minAllowedTimestamp := int64(fasttime.UnixTimestamp()*1000) - retentionPeriod.Milliseconds()
if tr.MinTimestamp > minAllowedTimestamp {
return nil
}
return &httpserver.ErrorWithStatusCode{
Err: fmt.Errorf("the given time range %s is outside the allowed -retentionPeriod=%s according to -denyQueriesOutsideRetention", &tr, retentionPeriod),
StatusCode: http.StatusServiceUnavailable,
}
func DataPath() string {
return *storageDataPath
}
// Init initializes vmstorage.
@@ -124,11 +122,16 @@ func Init(resetCacheIfNeeded func(mrs []storage.MetricRow)) {
logger.Fatalf("invalid `-precisionBits`: %s", err)
}
resetResponseCacheIfNeeded = resetCacheIfNeeded
storage.SetDedupInterval(*minScrapeInterval)
storage.SetDataFlushInterval(*inmemoryDataFlushInterval)
storage.LegacySetRetentionTimezoneOffset(*retentionTimezoneOffset)
storage.SetFreeDiskSpaceLimit(minFreeDiskSpaceBytes.N)
storage.SetTSIDCacheSize(cacheSizeStorageTSID.IntN())
storage.SetTagFiltersCacheSize(cacheSizeIndexDBTagFilters.IntN())
if *finalDedupScheduleInterval < time.Hour {
logger.Fatalf("-storage.finalDedupScheduleCheckInterval cannot be smaller than 1 hour; got %s", *finalDedupScheduleInterval)
}
storage.SetFinalDedupScheduleInterval(*finalDedupScheduleInterval)
storage.SetMetricNamesStatsCacheSize(cacheSizeMetricNamesStats.IntN())
storage.SetMetricNameCacheSize(cacheSizeStorageMetricName.IntN())
storage.SetMetadataStorageSize(metadataStorageSize.IntN())
@@ -147,21 +150,21 @@ func Init(resetCacheIfNeeded func(mrs []storage.MetricRow)) {
if *idbPrefillStart > 23*time.Hour {
logger.Panicf("-storage.idbPrefillStart cannot exceed 23 hours; got %s", idbPrefillStart)
}
logger.Infof("opening storage at %q with -retentionPeriod=%s", *DataPath, retentionPeriod)
fs.RegisterPathFsMetrics(*storageDataPath)
logger.Infof("opening storage at %q with -retentionPeriod=%s", *storageDataPath, retentionPeriod)
startTime := time.Now()
WG = syncwg.WaitGroup{}
opts := storage.OpenOptions{
Retention: retentionPeriod.Duration(),
FutureRetention: futureRetention.Duration(),
MaxHourlySeries: getMaxHourlySeries(),
MaxDailySeries: getMaxDailySeries(),
DisablePerDayIndex: *disablePerDayIndex,
TrackMetricNamesStats: *trackMetricNamesStats,
IDBPrefillStart: *idbPrefillStart,
LogNewSeries: *logNewSeries,
Retention: retentionPeriod.Duration(),
FutureRetention: futureRetention.Duration(),
DenyQueriesOutsideRetention: *denyQueriesOutsideRetention,
MaxHourlySeries: getMaxHourlySeries(),
MaxDailySeries: getMaxDailySeries(),
DisablePerDayIndex: *disablePerDayIndex,
TrackMetricNamesStats: *trackMetricNamesStats,
IDBPrefillStart: *idbPrefillStart,
LogNewSeries: *logNewSeries,
}
strg := storage.MustOpenStorage(*DataPath, opts)
Storage = strg
strg := storage.MustOpenStorage(*storageDataPath, opts)
initStaleSnapshotsRemover(strg)
var m storage.Metrics
@@ -172,7 +175,7 @@ func Init(resetCacheIfNeeded func(mrs []storage.MetricRow)) {
rowsCount := tm.SmallRowsCount + tm.BigRowsCount
sizeBytes := tm.SmallSizeBytes + tm.BigSizeBytes
logger.Infof("successfully opened storage %q in %.3f seconds; partsCount: %d; blocksCount: %d; rowsCount: %d; sizeBytes: %d",
*DataPath, time.Since(startTime).Seconds(), partsCount, blocksCount, rowsCount, sizeBytes)
*storageDataPath, time.Since(startTime).Seconds(), partsCount, blocksCount, rowsCount, sizeBytes)
// register storage metrics
storageMetrics = metrics.NewSet()
@@ -180,7 +183,10 @@ func Init(resetCacheIfNeeded func(mrs []storage.MetricRow)) {
writeStorageMetrics(w, strg)
})
metrics.RegisterSet(storageMetrics)
fs.RegisterPathFsMetrics(*DataPath)
WG = syncwg.WaitGroup{}
resetResponseCacheIfNeeded = resetCacheIfNeeded
Storage = strg
}
var storageMetrics *metrics.Set
@@ -325,7 +331,7 @@ func Stop() {
metrics.UnregisterSet(storageMetrics, true)
storageMetrics = nil
logger.Infof("gracefully closing the storage at %s", *DataPath)
logger.Infof("gracefully closing the storage at %s", *storageDataPath)
startTime := time.Now()
WG.WaitAndBlock()
stopStaleSnapshotsRemover()
@@ -352,6 +358,7 @@ func RequestHandler(w http.ResponseWriter, r *http.Request) bool {
startTime := time.Now()
if err := Storage.ForceMergePartitions(partitionNamePrefix); err != nil {
logger.Errorf("error in forced merge for partition_prefix=%q: %s", partitionNamePrefix, err)
return
}
logger.Infof("forced merge for partition_prefix=%q has been successfully finished in %.3f seconds", partitionNamePrefix, time.Since(startTime).Seconds())
}()
@@ -365,6 +372,7 @@ func RequestHandler(w http.ResponseWriter, r *http.Request) bool {
Storage.DebugFlush()
return true
}
if path == "/internal/log_new_series" {
if !httpserver.CheckAuthFlag(w, r, logNewSeriesAuthKey) {
return true
@@ -529,15 +537,15 @@ func writeStorageMetrics(w io.Writer, strg *storage.Storage) {
tm := &m.TableMetrics
idbm := &m.TableMetrics.IndexDBMetrics
metrics.WriteGaugeUint64(w, fmt.Sprintf(`vm_free_disk_space_bytes{path=%q}`, *DataPath), fs.MustGetFreeSpace(*DataPath))
metrics.WriteGaugeUint64(w, fmt.Sprintf(`vm_free_disk_space_limit_bytes{path=%q}`, *DataPath), uint64(minFreeDiskSpaceBytes.N))
metrics.WriteGaugeUint64(w, fmt.Sprintf(`vm_total_disk_space_bytes{path=%q}`, *DataPath), fs.MustGetTotalSpace(*DataPath))
metrics.WriteGaugeUint64(w, fmt.Sprintf(`vm_free_disk_space_bytes{path=%q}`, *storageDataPath), fs.MustGetFreeSpace(*storageDataPath))
metrics.WriteGaugeUint64(w, fmt.Sprintf(`vm_free_disk_space_limit_bytes{path=%q}`, *storageDataPath), uint64(minFreeDiskSpaceBytes.N))
metrics.WriteGaugeUint64(w, fmt.Sprintf(`vm_total_disk_space_bytes{path=%q}`, *storageDataPath), fs.MustGetTotalSpace(*storageDataPath))
isReadOnly := 0
if strg.IsReadOnly() {
isReadOnly = 1
}
metrics.WriteGaugeUint64(w, fmt.Sprintf(`vm_storage_is_read_only{path=%q}`, *DataPath), uint64(isReadOnly))
metrics.WriteGaugeUint64(w, fmt.Sprintf(`vm_storage_is_read_only{path=%q}`, *storageDataPath), uint64(isReadOnly))
metrics.WriteGaugeUint64(w, `vm_active_merges{type="storage/inmemory"}`, tm.ActiveInmemoryMerges)
metrics.WriteGaugeUint64(w, `vm_active_merges{type="storage/small"}`, tm.ActiveSmallMerges)

View File

@@ -3,6 +3,7 @@ export interface MetricBase {
metric: {
[key: string]: string;
};
nullTimestamps?: number[];
}
export interface MetricResult extends MetricBase {

View File

@@ -16,6 +16,7 @@ export interface ChartTooltipProps {
point: { top: number, left: number };
unit?: string;
statsFormatted?: SeriesItemStatsFormatted;
description?: ReactNode;
isSticky?: boolean;
info?: ReactNode;
marker?: string;
@@ -34,6 +35,7 @@ const ChartTooltip: FC<ChartTooltipProps> = ({
unit = "",
info,
statsFormatted,
description,
isSticky,
marker,
duplicateCount = 0,
@@ -173,6 +175,7 @@ const ChartTooltip: FC<ChartTooltipProps> = ({
))}
</table>
)}
{description && <p className="vm-chart-tooltip__description">{description}</p>}
{info && <p className="vm-chart-tooltip__info">{info}</p>}
</div>
), u.root);

View File

@@ -143,4 +143,10 @@ $chart-tooltip-y: -1 * ($padding-global + $chart-tooltip-half-icon);
word-break: break-all;
white-space: pre-wrap;
}
&__description {
word-break: break-word;
white-space: pre-wrap;
opacity: 0.85;
}
}

View File

@@ -1,7 +1,7 @@
export const seriesFetchedWarning = `No match!
export const seriesFetchedWarning = `No match!
This query hasn't selected any time series from database.
Either the requested metrics are missing in the database,
or there is a typo in series selector.`;
export const partialWarning = `The shown results are marked as PARTIAL.
The result is marked as partial if one or more vmstorage nodes failed to respond to the query.`;
The result is marked as partial if one or more storage nodes failed to respond to the query.`;

View File

@@ -71,7 +71,7 @@ const RulesHeader = ({
<TextField
label="Search"
value={search}
placeholder="Filter by rule, name or labels"
placeholder="Filter by group or rule name"
startIcon={<SearchIcon />}
onChange={onChangeSearch}
/>

View File

@@ -1,6 +1,5 @@
import { Component, FC, Ref } from "preact/compat";
import classNames from "classnames";
import { getCssVariable } from "../../../utils/theme";
import { TabItemType } from "./Tabs";
import TabItemWrapper from "./TabItemWrapper";
import "./style.scss";
@@ -8,7 +7,6 @@ import "./style.scss";
interface TabItemProps {
activeItem: string
item: TabItemType
color?: string
onChange?: (value: string) => void
activeNavRef: Ref<Component>
isNavLink?: boolean
@@ -17,7 +15,6 @@ interface TabItemProps {
const TabItem: FC<TabItemProps> = ({
activeItem,
item,
color = getCssVariable("color-primary"),
activeNavRef,
onChange,
isNavLink
@@ -35,7 +32,6 @@ const TabItem: FC<TabItemProps> = ({
})}
isNavLink={isNavLink}
to={item.value}
style={{ color: color }}
onClick={createHandlerClickTab(item.value)}
ref={activeItem === item.value ? activeNavRef : undefined}
>

View File

@@ -6,7 +6,6 @@ interface TabItemWrapperProps {
to: string
isNavLink?: boolean
className: string
style: { color: string }
children: ReactNode
onClick: () => void
}

View File

@@ -1,6 +1,5 @@
import { Component, FC, useRef, useState } from "preact/compat";
import { ReactNode, useEffect } from "react";
import { getCssVariable } from "../../../utils/theme";
import TabItem from "./TabItem";
import "./style.scss";
import useWindowSize from "../../../hooks/useWindowSize";
@@ -15,7 +14,6 @@ export interface TabItemType {
interface TabsProps {
activeItem: string
items: TabItemType[]
color?: string
onChange?: (value: string) => void
indicatorPlacement?: "bottom" | "top"
isNavLink?: boolean
@@ -24,7 +22,6 @@ interface TabsProps {
const Tabs: FC<TabsProps> = ({
activeItem,
items,
color = getCssVariable("color-primary"),
onChange,
indicatorPlacement = "bottom",
isNavLink,
@@ -48,14 +45,13 @@ const Tabs: FC<TabsProps> = ({
activeItem={activeItem}
item={item}
onChange={onChange}
color={color}
activeNavRef={activeNavRef}
isNavLink={isNavLink}
/>
))}
<div
className="vm-tabs__indicator"
style={{ ...indicatorPosition, borderColor: color }}
style={{ ...indicatorPosition }}
/>
</div>;
};

View File

@@ -14,7 +14,7 @@
align-items: center;
justify-content: center;
padding: $padding-global $padding-small;
color: inherit;
color: $color-primary;
text-decoration: none;
text-transform: capitalize;
font-size: inherit;
@@ -46,5 +46,6 @@
position: absolute;
border-bottom: 2px solid;
transition: width 200ms ease, left 300ms cubic-bezier(0.280, 0.840, 0.420, 1);
border-color: $color-primary;
}
}

View File

@@ -4,7 +4,7 @@ import { ChartTooltipProps } from "../../components/Chart/ChartTooltip/ChartTool
import { SeriesItem } from "../../types";
import dayjs from "dayjs";
import { DATE_FULL_TIMEZONE_FORMAT } from "../../constants/date";
import { formatPrettyNumber, getMetricName } from "../../utils/uplot";
import { getMetricName } from "../../utils/uplot";
import { MetricResult } from "../../api/types";
import useEventListener from "../useEventListener";
@@ -15,19 +15,65 @@ interface LineTooltipHook {
unit?: string;
}
// Pixel proximity for detecting hover over null-timestamp X markers drawn at chart bottom.
const NULL_HOVER_PROX = 8;
// Half the visual marker height in CSS px (BASE_POINT_SIZE * 1.4 / 2 from scatter.ts).
// scatter.ts lifts the marker center by this amount above yMin so the icon sits inside
// the plot area; the hover y-anchor must match that offset.
const NULL_MARKER_HALF_CSS = 2.8;
interface NullHover {
seriesIdx: number;
timestamp: number;
}
const findNullHover = (u: uPlot): NullHover | null => {
const cursorLeft = u.cursor.left ?? -1;
const cursorTop = u.cursor.top ?? -1;
if (cursorLeft < 0 || cursorTop < 0) return null;
const scaleY = u.scales["1"];
if (!scaleY || scaleY.min == null) return null;
const yPos = u.valToPos(scaleY.min, "1") - NULL_MARKER_HALF_CSS;
if (Math.abs(cursorTop - yPos) > NULL_HOVER_PROX) return null;
let best: { seriesIdx: number; timestamp: number; dist: number } | null = null;
for (let s = 1; s < u.series.length; s++) {
const seriesItem = u.series[s] as SeriesItem;
if (!seriesItem.show) continue;
const nullTs = seriesItem.nullTimestamps;
if (!nullTs || !nullTs.length) continue;
for (let i = 0; i < nullTs.length; i++) {
const t = nullTs[i];
const xPos = u.valToPos(t, "x");
const dist = Math.abs(cursorLeft - xPos);
if (dist < NULL_HOVER_PROX && (best === null || dist < best.dist)) {
best = { seriesIdx: s, timestamp: t, dist };
}
}
}
return best ? { seriesIdx: best.seriesIdx, timestamp: best.timestamp } : null;
};
const NULL_DESCRIPTION = "\"null\" can be a staleness marker or an actual NaN/null value produced by exporter.";
const useLineTooltip = ({ u, metrics, series, unit }: LineTooltipHook) => {
const [showTooltip, setShowTooltip] = useState(false);
const [tooltipIdx, setTooltipIdx] = useState({ seriesIdx: -1, dataIdx: -1 });
const [nullTooltip, setNullTooltip] = useState<NullHover | null>(null);
const [stickyTooltips, setStickyToolTips] = useState<ChartTooltipProps[]>([]);
const resetTooltips = () => {
setStickyToolTips([]);
setTooltipIdx({ seriesIdx: -1, dataIdx: -1 });
setNullTooltip(null);
};
const setCursor = (u: uPlot) => {
const dataIdx = u.cursor.idx ?? -1;
setTooltipIdx(prev => ({ ...prev, dataIdx }));
setNullTooltip(findNullHover(u));
};
const seriesFocus = (u: uPlot, sidx: (number | null)) => {
@@ -35,7 +81,36 @@ const useLineTooltip = ({ u, metrics, series, unit }: LineTooltipHook) => {
setTooltipIdx(prev => ({ ...prev, seriesIdx }));
};
const getNullTooltipProps = (hit: NullHover): ChartTooltipProps => {
const { seriesIdx, timestamp } = hit;
const metricItem = metrics[seriesIdx - 1];
const seriesItem = series[seriesIdx] as SeriesItem;
const groups = new Set(metrics.map(m => m.group));
const group = metricItem?.group || 0;
const yMin = u?.scales?.[1]?.min ?? 0;
const point = {
top: u ? u.valToPos(yMin, seriesItem?.scale || "1") - NULL_MARKER_HALF_CSS : 0,
left: u ? u.valToPos(timestamp, "x") : 0,
};
return {
u,
id: `null_${seriesIdx}_${timestamp}`,
title: groups.size > 1 ? `Query ${group}` : "",
dates: [dayjs(timestamp * 1000).tz().format(DATE_FULL_TIMEZONE_FORMAT)],
value: "null",
info: getMetricName(metricItem, seriesItem),
description: NULL_DESCRIPTION,
marker: `${seriesItem?.stroke}`,
point,
};
};
const getTooltipProps = useCallback((): ChartTooltipProps => {
if (nullTooltip) return getNullTooltipProps(nullTooltip);
const { seriesIdx, dataIdx } = tooltipIdx;
const metricItem = metrics[seriesIdx - 1];
const seriesItem = series[seriesIdx] as SeriesItem;
@@ -44,8 +119,6 @@ const useLineTooltip = ({ u, metrics, series, unit }: LineTooltipHook) => {
const group = metricItem?.group || 0;
const value = u?.data?.[seriesIdx]?.[dataIdx] || 0;
const min = u?.scales?.[1]?.min || 0;
const max = u?.scales?.[1]?.max || 1;
const date = u?.data?.[0]?.[dataIdx] || 0;
let duplicateCount = 1;
@@ -80,13 +153,13 @@ const useLineTooltip = ({ u, metrics, series, unit }: LineTooltipHook) => {
id: `${seriesIdx}_${dataIdx}`,
title: groups.size > 1 ? `Query ${group}` : "",
dates: [date ? dayjs(date * 1000).tz().format(DATE_FULL_TIMEZONE_FORMAT) : "-"],
value: formatPrettyNumber(value, min, max),
value: value.toLocaleString("en-US", { maximumFractionDigits: 20 }),
info: getMetricName(metricItem, seriesItem),
statsFormatted: seriesItem?.statsFormatted,
marker: `${seriesItem?.stroke}`,
duplicateCount,
};
}, [u, tooltipIdx, metrics, series, unit]);
}, [u, tooltipIdx, metrics, series, unit, nullTooltip]);
const handleClick = useCallback(() => {
if (!showTooltip) return;
@@ -101,8 +174,9 @@ const useLineTooltip = ({ u, metrics, series, unit }: LineTooltipHook) => {
};
useEffect(() => {
setShowTooltip(tooltipIdx.dataIdx !== -1 && tooltipIdx.seriesIdx !== -1);
}, [tooltipIdx]);
const normalHit = tooltipIdx.dataIdx !== -1 && tooltipIdx.seriesIdx !== -1;
setShowTooltip(normalHit || nullTooltip !== null);
}, [tooltipIdx, nullTooltip]);
useEventListener("click", handleClick);

View File

@@ -20,7 +20,7 @@ describe("convertMetricsDataToCSV", () => {
},
];
const result = convertMetricsDataToCSV(data);
expect(result).toBe("header1,header2\n123,value2");
expect(result).toBe("header1,header2,__timestamp__,__value__\n123,value2,1623945600,123");
});
it("should return a valid CSV string for multiple metric entries with values", () => {
@@ -43,7 +43,7 @@ describe("convertMetricsDataToCSV", () => {
},
];
const result = convertMetricsDataToCSV(data);
expect(result).toBe("header1,header2\n123,value2\n456,value4");
expect(result).toBe("header1,header2,__timestamp__,__value__\n123,value2,1623945600,123\n456,value4,1623949200,456");
});
it("should handle metric entries with multiple values field", () => {
@@ -58,7 +58,7 @@ describe("convertMetricsDataToCSV", () => {
},
];
const result = convertMetricsDataToCSV(data);
expect(result).toBe("header1,header2\n123-456,values");
expect(result).toBe("header1,header2,__timestamp__,__value__\n123-456,values,-,-");
});
it("should handle a combination of metric entries with value and values", () => {
@@ -81,6 +81,19 @@ describe("convertMetricsDataToCSV", () => {
},
];
const result = convertMetricsDataToCSV(data);
expect(result).toBe("header1,header2\n123,first\n456-789,second");
expect(result).toBe("header1,header2,__timestamp__,__value__\n123,first,1623945600,123\n456-789,second,-,-");
});
it("should return value and timestamp if metric field is empty", () => {
const data: InstantMetricResult[] = [
{
value: [1623945600, "123"],
group: 0,
metric: {}
},
];
const result = convertMetricsDataToCSV(data);
expect(result).toBe("__timestamp__,__value__\n1623945600,123");
});
});

View File

@@ -3,16 +3,22 @@ import { getColumns, MetricCategory } from "../../hooks/useSortedCategories";
import { formatValueToCSV } from "../../utils/csv";
const getHeaders = (data: InstantMetricResult[]): string => {
return getColumns(data).map(({ key }) => key).join(",");
const metricHeaders = getColumns(data).map(({ key }) => key);
return [...metricHeaders, "__timestamp__", "__value__"].join(",");
};
const getRows = (data: InstantMetricResult[], headers: MetricCategory[]) => {
return data?.map(d => headers.map(c => formatValueToCSV(d.metric[c.key] || "-")).join(","));
return data?.map(d => {
const metricPart = headers.map(c => formatValueToCSV(d.metric[c.key] || "-"));
const timestamp = d.value ? formatValueToCSV(String(d.value[0])) : "-";
const value = d.value ? formatValueToCSV(d.value[1]) : "-";
return [...metricPart, timestamp, value].join(",");
});
};
export const convertMetricsDataToCSV = (data: InstantMetricResult[]): string => {
if (!data.length) return "";
const headers = getHeaders(data);
if (!headers.length) return "";
const rows = getRows(data, getColumns(data));
return [headers, ...rows].join("\n");
};

View File

@@ -149,15 +149,21 @@ export const useFetchExport = ({ hideQuery, showAllSeries }: FetchQueryParams):
const pointsToTake = shouldDownsample ? maxPointsPerSeries : totalPoints;
const step = shouldDownsample ? totalPoints / maxPointsPerSeries : 1;
const values: [number, number][] = Array.from({ length: pointsToTake }, (_, i) => {
const values: [number, number][] = new Array(pointsToTake);
const nullTimestamps: number[] = [];
for (let i = 0; i < pointsToTake; i++) {
const idx = shouldDownsample ? Math.floor(i * step) : i;
return [rawTimestamps[idx] / 1000, rawValues[idx]];
});
const ts = rawTimestamps[idx] / 1000;
const raw = rawValues[idx];
if (raw === null) nullTimestamps.push(ts);
values[i] = [ts, raw as number];
}
tempData.push({
group: counter,
metric: jsonLine.metric,
values,
nullTimestamps,
} as MetricBase);
}

View File

@@ -0,0 +1,35 @@
import { afterEach, describe, expect, it, vi, type Mock } from "vitest";
import { getFromStorage, saveToStorage } from "../../utils/storage";
vi.mock("../../utils/storage", () => ({
getFromStorage: vi.fn(),
saveToStorage: vi.fn(),
}));
describe("customPanel reducer", () => {
afterEach(() => {
vi.resetAllMocks();
vi.resetModules();
});
it("persists reduceMemUsage under its own storage key", async () => {
const { reducer, initialCustomPanelState } = await import("./reducer");
reducer(initialCustomPanelState, { type: "TOGGLE_REDUCE_MEM_USAGE" });
expect(saveToStorage).toHaveBeenCalledWith("REDUCE_MEM_USAGE", true);
expect(saveToStorage).not.toHaveBeenCalledWith("TABLE_COMPACT", true);
});
it("hydrates reduceMemUsage from storage", async () => {
const getFromStorageMock = getFromStorage as Mock;
getFromStorageMock.mockImplementation((key: string) => {
if (key === "REDUCE_MEM_USAGE") return true;
return undefined;
});
const { initialCustomPanelState } = await import("./reducer");
expect(initialCustomPanelState.reduceMemUsage).toBe(true);
});
});

View File

@@ -35,7 +35,7 @@ export const initialCustomPanelState: CustomPanelState = {
isTracingEnabled: false,
seriesLimits: limitsStorage ? JSON.parse(limitsStorage) : DEFAULT_MAX_SERIES,
tableCompact: getFromStorage("TABLE_COMPACT") as boolean || false,
reduceMemUsage: false
reduceMemUsage: getFromStorage("REDUCE_MEM_USAGE") as boolean || false
};
export function reducer(state: CustomPanelState, action: CustomPanelAction): CustomPanelState {
@@ -69,7 +69,7 @@ export function reducer(state: CustomPanelState, action: CustomPanelAction): Cus
tableCompact: !state.tableCompact
};
case "TOGGLE_REDUCE_MEM_USAGE":
saveToStorage("TABLE_COMPACT", !state.reduceMemUsage);
saveToStorage("REDUCE_MEM_USAGE", !state.reduceMemUsage);
return {
...state,
reduceMemUsage: !state.reduceMemUsage

View File

@@ -11,6 +11,7 @@ export interface SeriesItem extends Series {
statsFormatted: SeriesItemStatsFormatted;
median: number;
hasAlias?: boolean;
nullTimestamps?: number[];
}
export interface HideSeriesArgs {

View File

@@ -7,6 +7,7 @@ export const ALL_STORAGE_KEYS = [
"SERIES_LIMITS",
"LEGEND_AUTO_COLLAPSE",
"TABLE_COMPACT",
"REDUCE_MEM_USAGE",
"TIMEZONE",
"DISABLED_DEFAULT_TIMEZONE",
"THEME",

View File

@@ -103,6 +103,28 @@ export const drawPoints = (u: uPlot, seriesIdx: number) => {
u.ctx.lineWidth = 1.4 * uPlot.pxRatio;
u.ctx.strokeStyle = u.ctx.fillStyle;
u.ctx.stroke(squaresPath);
const nullTs = (series as unknown as { nullTimestamps?: number[] }).nullTimestamps;
if (nullTs && nullTs.length) {
const xSize = BASE_POINT_SIZE * 1.4 * uPlot.pxRatio;
const xHalf = xSize / 2;
// Lift the marker by half its size so the entire icon sits inside the plot area
// (yMin maps to the plot's bottom edge, so centering on it would clip the lower half).
const cy = valToPosY(yMin, scaleY, yDim, yOff) - xHalf;
const xPath = new Path2D();
for (let i = 0; i < nullTs.length; i++) {
const t = nullTs[i];
if (t < xMin || t > xMax) continue;
const cx = valToPosX(t, scaleX, xDim, xOff);
xPath.moveTo(cx - xHalf, cy - xHalf);
xPath.lineTo(cx + xHalf, cy + xHalf);
xPath.moveTo(cx + xHalf, cy - xHalf);
xPath.lineTo(cx - xHalf, cy + xHalf);
}
u.ctx.lineWidth = 1.6 * uPlot.pxRatio;
u.ctx.strokeStyle = u.ctx.fillStyle;
u.ctx.stroke(xPath);
}
};
uPlot.orient(u, seriesIdx, orientCallback);

View File

@@ -38,6 +38,7 @@ export const getSeriesItemContext = (data: MetricResult[], hideSeries: string[],
show: !includesHideSeries(label, hideSeries),
scale: "1",
paths: isRawQuery ? drawPoints : undefined,
nullTimestamps: d.nullTimestamps,
...getSeriesStatistics(d),
};
};

View File

@@ -22,6 +22,7 @@ var (
vminsertAddrRE = regexp.MustCompile(`accepting vminsert conns at (.*:\d{1,5})$`)
vminsertClusterNativeAddrRE = regexp.MustCompile(`started TCP clusternative server at "(.*:\d{1,5})"`)
vmselectAddrRE = regexp.MustCompile(`accepting vmselect conns at (.*:\d{1,5})$`)
vmauthHttpListenAddrRE = regexp.MustCompile(`pprof handlers are exposed at http://(.*:\d{1,5})/debug/pprof/`)
)
// app represents an instance of some VictoriaMetrics server (such as vmstorage,

View File

@@ -79,24 +79,25 @@ type PrometheusWriteQuerier interface {
// QueryOpts contains various params used for querying or ingesting data
type QueryOpts struct {
Tenant string
Timeout string
Start string
End string
Time string
Step string
ExtraFilters []string
ExtraLabels []string
Trace string
ReduceMemUsage string
MaxLookback string
LatencyOffset string
Format string
NoCache string
Headers http.Header
From string
Until string
StorageStep string
Tenant string
Timeout string
Start string
End string
Time string
Step string
ExtraFilters []string
ExtraLabels []string
Trace string
ReduceMemUsage string
MaxLookback string
LatencyOffset string
Format string
NoCache string
Headers http.Header
From string
Until string
StorageStep string
DenyPartialResponse string
}
func (qos *QueryOpts) getHeaders() http.Header {
@@ -132,6 +133,7 @@ func (qos *QueryOpts) asURLValues() url.Values {
addNonEmpty("from", qos.From)
addNonEmpty("until", qos.Until)
addNonEmpty("storage_step", qos.StorageStep)
addNonEmpty("deny_partial_response", qos.DenyPartialResponse)
return uv
}

View File

@@ -88,19 +88,11 @@ func (tc *TestCase) MustStartDefaultVmsingle() *Vmsingle {
}
// MustStartVmsingle is a test helper function that starts an instance of
// vmsingle located at ../../bin/victoria-metrics-race and fails the test if the app
// fails to start.
// vmsingle (latest version) and fails the test if the app fails to start.
func (tc *TestCase) MustStartVmsingle(instance string, flags []string) *Vmsingle {
tc.t.Helper()
return tc.MustStartVmsingleAt(instance, "../../bin/victoria-metrics-race", flags)
}
// MustStartVmsingleAt is a test helper function that starts an instance of
// vmsingle and fails the test if the app fails to start.
func (tc *TestCase) MustStartVmsingleAt(instance, binary string, flags []string) *Vmsingle {
tc.t.Helper()
app, err := StartVmsingleAt(instance, binary, flags, tc.cli, tc.output)
app, err := StartVmsingle(instance, flags, tc.cli, tc.output)
if err != nil {
tc.t.Fatalf("Could not start %s: %v", instance, err)
}
@@ -109,19 +101,11 @@ func (tc *TestCase) MustStartVmsingleAt(instance, binary string, flags []string)
}
// MustStartVmstorage is a test helper function that starts an instance of
// vmstorage located at ../../bin/vmstorage-race and fails the test if the app fails
// to start.
// vmstorage (latest version) and fails the test if the app fails to start.
func (tc *TestCase) MustStartVmstorage(instance string, flags []string) *Vmstorage {
tc.t.Helper()
return tc.MustStartVmstorageAt(instance, "../../bin/vmstorage-race", flags)
}
// MustStartVmstorageAt is a test helper function that starts an instance of
// vmstorage and fails the test if the app fails to start.
func (tc *TestCase) MustStartVmstorageAt(instance string, binary string, flags []string) *Vmstorage {
tc.t.Helper()
app, err := StartVmstorageAt(instance, binary, flags, tc.cli, tc.output)
app, err := StartVmstorage(instance, flags, tc.cli, tc.output)
if err != nil {
tc.t.Fatalf("Could not start %s: %v", instance, err)
}
@@ -130,7 +114,7 @@ func (tc *TestCase) MustStartVmstorageAt(instance string, binary string, flags [
}
// MustStartVmselect is a test helper function that starts an instance of
// vmselect and fails the test if the app fails to start.
// vmselect (latest version) and fails the test if the app fails to start.
func (tc *TestCase) MustStartVmselect(instance string, flags []string) *Vmselect {
tc.t.Helper()
@@ -290,10 +274,8 @@ func (tc *TestCase) MustStartDefaultCluster() *Vmcluster {
// tests usually come paired with corresponding vmsingle tests.
type ClusterOptions struct {
Vmstorage1Instance string
Vmstorage1Binary string
Vmstorage1Flags []string
Vmstorage2Instance string
Vmstorage2Binary string
Vmstorage2Flags []string
VminsertInstance string
VminsertFlags []string
@@ -305,15 +287,8 @@ type ClusterOptions struct {
func (tc *TestCase) MustStartCluster(opts *ClusterOptions) *Vmcluster {
tc.t.Helper()
if opts.Vmstorage1Binary == "" {
opts.Vmstorage1Binary = "../../bin/vmstorage-race"
}
vmstorage1 := tc.MustStartVmstorageAt(opts.Vmstorage1Instance, opts.Vmstorage1Binary, opts.Vmstorage1Flags)
if opts.Vmstorage2Binary == "" {
opts.Vmstorage2Binary = "../../bin/vmstorage-race"
}
vmstorage2 := tc.MustStartVmstorageAt(opts.Vmstorage2Instance, opts.Vmstorage2Binary, opts.Vmstorage2Flags)
vmstorage1 := tc.MustStartVmstorage(opts.Vmstorage1Instance, opts.Vmstorage1Flags)
vmstorage2 := tc.MustStartVmstorage(opts.Vmstorage2Instance, opts.Vmstorage2Flags)
opts.VminsertFlags = append(opts.VminsertFlags, []string{
"-storageNode=" + vmstorage1.VminsertAddr() + "," + vmstorage2.VminsertAddr(),

View File

@@ -0,0 +1,50 @@
package apptest
// MustStartVmsingle_v1_132_0 is a test helper function that starts an instance
// of vmsingle-v1.132.0 (last version that uses legacy index) and fails the test
// if the app fails to start.
func (tc *TestCase) MustStartVmsingle_v1_132_0(instance string, flags []string) *Vmsingle {
tc.t.Helper()
app, err := StartVmsingle_v1_132_0(instance, flags, tc.cli, tc.output)
if err != nil {
tc.t.Fatalf("Could not start %s: %v", instance, err)
}
tc.addApp(instance, app)
return app
}
// MustStartVmstorage_v1_132_0 is a test helper function that starts an instance
// of vmstorage-v1.132.0 (last version that uses legacy index) and fails the
// test if the app fails to start.
func (tc *TestCase) MustStartVmstorage_v1_132_0(instance string, flags []string) *Vmstorage {
tc.t.Helper()
app, err := StartVmstorage_v1_132_0(instance, flags, tc.cli, tc.output)
if err != nil {
tc.t.Fatalf("Could not start %s: %v", instance, err)
}
tc.addApp(instance, app)
return app
}
// MustStartCluster_v1_132_0 starts a cluster with vmstorage-v1.132.0 with
// custom flags.
func (tc *TestCase) MustStartCluster_v1_132_0(opts *ClusterOptions) *Vmcluster {
tc.t.Helper()
vmstorage1 := tc.MustStartVmstorage_v1_132_0(opts.Vmstorage1Instance, opts.Vmstorage1Flags)
vmstorage2 := tc.MustStartVmstorage_v1_132_0(opts.Vmstorage2Instance, opts.Vmstorage2Flags)
opts.VminsertFlags = append(opts.VminsertFlags, []string{
"-storageNode=" + vmstorage1.VminsertAddr() + "," + vmstorage2.VminsertAddr(),
}...)
vminsert := tc.MustStartVminsert(opts.VminsertInstance, opts.VminsertFlags)
opts.VmselectFlags = append(opts.VmselectFlags, []string{
"-storageNode=" + vmstorage1.VmselectAddr() + "," + vmstorage2.VmselectAddr(),
}...)
vmselect := tc.MustStartVmselect(opts.VmselectInstance, opts.VmselectFlags)
return &Vmcluster{vminsert, vmselect, []*Vmstorage{vmstorage1, vmstorage2}}
}

View File

@@ -2,7 +2,6 @@ package tests
import (
"fmt"
"os"
"path/filepath"
"slices"
"testing"
@@ -11,11 +10,6 @@ import (
at "github.com/VictoriaMetrics/VictoriaMetrics/apptest"
)
var (
legacyVmsinglePath = os.Getenv("VM_LEGACY_VMSINGLE_PATH")
legacyVmstoragePath = os.Getenv("VM_LEGACY_VMSTORAGE_PATH")
)
type testLegacyDeleteSeriesOpts struct {
startLegacySUT func() at.PrometheusWriteQuerier
startNewSUT func() at.PrometheusWriteQuerier
@@ -31,7 +25,7 @@ func TestLegacySingleDeleteSeries(t *testing.T) {
opts := testLegacyDeleteSeriesOpts{
startLegacySUT: func() at.PrometheusWriteQuerier {
return tc.MustStartVmsingleAt("vmsingle-legacy", legacyVmsinglePath, []string{
return tc.MustStartVmsingle_v1_132_0("vmsingle-legacy", []string{
"-storageDataPath=" + storageDataPath,
"-retentionPeriod=100y",
"-search.maxStalenessInterval=1m",
@@ -64,15 +58,13 @@ func TestLegacyClusterDeleteSeries(t *testing.T) {
opts := testLegacyDeleteSeriesOpts{
startLegacySUT: func() at.PrometheusWriteQuerier {
return tc.MustStartCluster(&at.ClusterOptions{
return tc.MustStartCluster_v1_132_0(&at.ClusterOptions{
Vmstorage1Instance: "vmstorage1-legacy",
Vmstorage1Binary: legacyVmstoragePath,
Vmstorage1Flags: []string{
"-storageDataPath=" + storage1DataPath,
"-retentionPeriod=100y",
},
Vmstorage2Instance: "vmstorage2-legacy",
Vmstorage2Binary: legacyVmstoragePath,
Vmstorage2Flags: []string{
"-storageDataPath=" + storage2DataPath,
"-retentionPeriod=100y",
@@ -191,7 +183,7 @@ func testLegacyDeleteSeries(tc *at.TestCase, opts testLegacyDeleteSeriesOpts) {
// - start legacy vmsingle
// - insert data1
// - confirm that metric names and samples are searcheable
// - confirm that metric names and samples are searchable
// - stop legacy vmsingle
const step = 24 * 3600 * 1000 // 24h
start1 := time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC).UnixMilli()
@@ -204,12 +196,12 @@ func testLegacyDeleteSeries(tc *at.TestCase, opts testLegacyDeleteSeriesOpts) {
opts.stopLegacySUT()
// - start new vmsingle
// - confirm that data1 metric names and samples are searcheable
// - confirm that data1 metric names and samples are searchable
// - delete data1
// - confirm that data1 metric names and samples are not searcheable anymore
// - confirm that data1 metric names and samples are not searchable anymore
// - insert data2 (same metric names, different dates)
// - confirm that metric names become searcheable again
// - confirm that data1 samples are not searchable and data2 samples are searcheable
// - confirm that metric names become searchable again
// - confirm that data1 samples are not searchable and data2 samples are searchable
newSUT := opts.startNewSUT()
assertSearchResults(newSUT, `{__name__=~".*"}`, start1, end1, "1d", want1)
@@ -230,7 +222,7 @@ func testLegacyDeleteSeries(tc *at.TestCase, opts testLegacyDeleteSeriesOpts) {
// - restart new vmsingle
// - confirm that metric names still searchable, data1 samples are not
// searchable, and data2 samples are searcheable
// searchable, and data2 samples are searchable
opts.stopNewSUT()
newSUT = opts.startNewSUT()
@@ -255,7 +247,7 @@ func TestLegacySingleBackupRestore(t *testing.T) {
opts := testLegacyBackupRestoreOpts{
startLegacySUT: func() at.PrometheusWriteQuerier {
return tc.MustStartVmsingleAt("vmsingle-legacy", legacyVmsinglePath, []string{
return tc.MustStartVmsingle_v1_132_0("vmsingle-legacy", []string{
"-storageDataPath=" + storageDataPath,
"-retentionPeriod=100y",
"-search.disableCache=true",
@@ -298,15 +290,13 @@ func TestLegacyClusterBackupRestore(t *testing.T) {
opts := testLegacyBackupRestoreOpts{
startLegacySUT: func() at.PrometheusWriteQuerier {
return tc.MustStartCluster(&at.ClusterOptions{
return tc.MustStartCluster_v1_132_0(&at.ClusterOptions{
Vmstorage1Instance: "vmstorage1-legacy",
Vmstorage1Binary: legacyVmstoragePath,
Vmstorage1Flags: []string{
"-storageDataPath=" + storage1DataPath,
"-retentionPeriod=100y",
},
Vmstorage2Instance: "vmstorage2-legacy",
Vmstorage2Binary: legacyVmstoragePath,
Vmstorage2Flags: []string{
"-storageDataPath=" + storage2DataPath,
"-retentionPeriod=100y",
@@ -583,7 +573,7 @@ func TestLegacySingleDowngrade(t *testing.T) {
opts := testLegacyDowngradeOpts{
startLegacySUT: func() at.PrometheusWriteQuerier {
return tc.MustStartVmsingleAt("vmsingle-legacy", legacyVmsinglePath, []string{
return tc.MustStartVmsingle_v1_132_0("vmsingle-legacy", []string{
"-storageDataPath=" + storageDataPath,
"-retentionPeriod=100y",
"-search.disableCache=true",
@@ -618,15 +608,13 @@ func TestLegacyClusterDowngrade(t *testing.T) {
opts := testLegacyDowngradeOpts{
startLegacySUT: func() at.PrometheusWriteQuerier {
return tc.MustStartCluster(&at.ClusterOptions{
return tc.MustStartCluster_v1_132_0(&at.ClusterOptions{
Vmstorage1Instance: "vmstorage1-legacy",
Vmstorage1Binary: legacyVmstoragePath,
Vmstorage1Flags: []string{
"-storageDataPath=" + storage1DataPath,
"-retentionPeriod=100y",
},
Vmstorage2Instance: "vmstorage2-legacy",
Vmstorage2Binary: legacyVmstoragePath,
Vmstorage2Flags: []string{
"-storageDataPath=" + storage2DataPath,
"-retentionPeriod=100y",

View File

@@ -1015,35 +1015,42 @@ func testGroupSkipSlowReplicas(tc *apptest.TestCase, opts *testGroupReplicationO
func testGroupPartialResponse(tc *apptest.TestCase, opts *testGroupReplicationOpts) {
t := tc.T()
assertSeries := func(app *apptest.Vmselect, wantPartial bool) {
assertSeries := func(app *apptest.Vmselect, denyPartialResponse string, want *apptest.PrometheusAPIV1SeriesResponse) {
t.Helper()
tc.Assert(&apptest.AssertOptions{
Msg: "unexpected /api/v1/series response",
Got: func() any {
return app.PrometheusAPIV1Series(t, `{__name__=~".*"}`, apptest.QueryOpts{
Start: "2024-01-01T00:00:00Z",
End: "2024-01-31T00:00:00Z",
Start: "2024-01-01T00:00:00Z",
End: "2024-01-31T00:00:00Z",
DenyPartialResponse: denyPartialResponse,
}).Sort()
},
Want: &apptest.PrometheusAPIV1SeriesResponse{
Status: "success",
IsPartial: wantPartial,
},
Want: want,
CmpOpts: []cmp.Option{
cmpopts.IgnoreFields(apptest.PrometheusAPIV1SeriesResponse{}, "Data"),
cmpopts.IgnoreFields(apptest.PrometheusAPIV1SeriesResponse{}, "Data", "Error"),
},
})
}
mustReturnPartialResponse := true
mustReturnFullResponse := false
allowPartialResponse := ""
denyPartialResponse := "1"
mustReturnPartialResponse := &apptest.PrometheusAPIV1SeriesResponse{
Status: "success",
IsPartial: true,
}
mustReturnFullResponse := &apptest.PrometheusAPIV1SeriesResponse{
Status: "success",
IsPartial: false,
}
// All vmstorage replicas are available so both vmselects must return full
// response.
assertSeries(opts.c.vmselect, mustReturnFullResponse)
assertSeries(opts.c.vmselectGroupRF, mustReturnFullResponse)
assertSeries(opts.c.vmselectGlobalRF, mustReturnFullResponse)
assertSeries(opts.c.vmselectGroupGlobalRF, mustReturnFullResponse)
assertSeries(opts.c.vmselect, allowPartialResponse, mustReturnFullResponse)
assertSeries(opts.c.vmselectGroupRF, allowPartialResponse, mustReturnFullResponse)
assertSeries(opts.c.vmselectGlobalRF, allowPartialResponse, mustReturnFullResponse)
assertSeries(opts.c.vmselectGroupGlobalRF, allowPartialResponse, mustReturnFullResponse)
// Stop groupRF-1 vmstorage nodes in first group.
//
@@ -1053,10 +1060,10 @@ func testGroupPartialResponse(tc *apptest.TestCase, opts *testGroupReplicationOp
// about the replication factor and therefore they must still be able to
// return full dataset.
opts.c.storageGroups[0].stopNodes(tc, opts.groupRF-1)
assertSeries(opts.c.vmselect, mustReturnPartialResponse)
assertSeries(opts.c.vmselectGroupRF, mustReturnFullResponse)
assertSeries(opts.c.vmselectGlobalRF, mustReturnFullResponse)
assertSeries(opts.c.vmselectGroupGlobalRF, mustReturnFullResponse)
assertSeries(opts.c.vmselect, allowPartialResponse, mustReturnPartialResponse)
assertSeries(opts.c.vmselectGroupRF, allowPartialResponse, mustReturnFullResponse)
assertSeries(opts.c.vmselectGlobalRF, allowPartialResponse, mustReturnFullResponse)
assertSeries(opts.c.vmselectGroupGlobalRF, allowPartialResponse, mustReturnFullResponse)
// Stop groupRF-1 vmstorages in the remaining groups.
//
@@ -1066,10 +1073,10 @@ func testGroupPartialResponse(tc *apptest.TestCase, opts *testGroupReplicationOp
for g := 1; g < len(opts.c.storageGroups); g++ {
opts.c.storageGroups[g].stopNodes(tc, opts.groupRF-1)
}
assertSeries(opts.c.vmselect, mustReturnPartialResponse)
assertSeries(opts.c.vmselectGroupRF, mustReturnFullResponse)
assertSeries(opts.c.vmselectGlobalRF, mustReturnPartialResponse)
assertSeries(opts.c.vmselectGroupGlobalRF, mustReturnFullResponse)
assertSeries(opts.c.vmselect, allowPartialResponse, mustReturnPartialResponse)
assertSeries(opts.c.vmselectGroupRF, allowPartialResponse, mustReturnFullResponse)
assertSeries(opts.c.vmselectGlobalRF, allowPartialResponse, mustReturnPartialResponse)
assertSeries(opts.c.vmselectGroupGlobalRF, allowPartialResponse, mustReturnFullResponse)
// Stop one more vmstorage in the first group.
//
@@ -1077,10 +1084,10 @@ func testGroupPartialResponse(tc *apptest.TestCase, opts *testGroupReplicationOp
// because it is unaware of replication across groups. vmselectGroupGlobalRF
// will continue retuning full dataset.
opts.c.storageGroups[0].stopNodes(tc, 1)
assertSeries(opts.c.vmselect, mustReturnPartialResponse)
assertSeries(opts.c.vmselectGroupRF, mustReturnPartialResponse)
assertSeries(opts.c.vmselectGlobalRF, mustReturnPartialResponse)
assertSeries(opts.c.vmselectGroupGlobalRF, mustReturnFullResponse)
assertSeries(opts.c.vmselect, allowPartialResponse, mustReturnPartialResponse)
assertSeries(opts.c.vmselectGroupRF, allowPartialResponse, mustReturnPartialResponse)
assertSeries(opts.c.vmselectGlobalRF, allowPartialResponse, mustReturnPartialResponse)
assertSeries(opts.c.vmselectGroupGlobalRF, allowPartialResponse, mustReturnFullResponse)
// Stop one more vmstoarge in remaining globarRF-1 groups.
//
@@ -1089,19 +1096,56 @@ func testGroupPartialResponse(tc *apptest.TestCase, opts *testGroupReplicationOp
for g := 1; g < opts.globalRF-1; g++ {
opts.c.storageGroups[g].stopNodes(tc, 1)
}
assertSeries(opts.c.vmselect, mustReturnPartialResponse)
assertSeries(opts.c.vmselectGroupRF, mustReturnPartialResponse)
assertSeries(opts.c.vmselectGlobalRF, mustReturnPartialResponse)
assertSeries(opts.c.vmselectGroupGlobalRF, mustReturnFullResponse)
assertSeries(opts.c.vmselect, allowPartialResponse, mustReturnPartialResponse)
assertSeries(opts.c.vmselectGroupRF, allowPartialResponse, mustReturnPartialResponse)
assertSeries(opts.c.vmselectGlobalRF, allowPartialResponse, mustReturnPartialResponse)
assertSeries(opts.c.vmselectGroupGlobalRF, allowPartialResponse, mustReturnFullResponse)
// Stop one more vmstoarge in one more group.
//
// vmselectGroupGlobalRF must now return partial dataset.
opts.c.storageGroups[opts.globalRF].stopNodes(tc, 1)
assertSeries(opts.c.vmselect, mustReturnPartialResponse)
assertSeries(opts.c.vmselectGroupRF, mustReturnPartialResponse)
assertSeries(opts.c.vmselectGlobalRF, mustReturnPartialResponse)
assertSeries(opts.c.vmselectGroupGlobalRF, mustReturnPartialResponse)
assertSeries(opts.c.vmselect, allowPartialResponse, mustReturnPartialResponse)
assertSeries(opts.c.vmselectGroupRF, allowPartialResponse, mustReturnPartialResponse)
assertSeries(opts.c.vmselectGlobalRF, allowPartialResponse, mustReturnPartialResponse)
assertSeries(opts.c.vmselectGroupGlobalRF, allowPartialResponse, mustReturnPartialResponse)
// Stop all the remaining vmstorage nodes except a single node.
//
// At this point vmselects still must be able to return partial response
// because at least one vmstorage node has successfully returned results.
n := len(opts.c.storageGroups[0].vmstorages)
opts.c.storageGroups[0].stopNodes(tc, n-1)
for g := 1; g < len(opts.c.storageGroups); g++ {
n := len(opts.c.storageGroups[g].vmstorages)
opts.c.storageGroups[g].stopNodes(tc, n)
}
assertSeries(opts.c.vmselect, allowPartialResponse, mustReturnPartialResponse)
assertSeries(opts.c.vmselectGroupRF, allowPartialResponse, mustReturnPartialResponse)
assertSeries(opts.c.vmselectGlobalRF, allowPartialResponse, mustReturnPartialResponse)
assertSeries(opts.c.vmselectGroupGlobalRF, allowPartialResponse, mustReturnPartialResponse)
mustReturnUnavailableError := &apptest.PrometheusAPIV1SeriesResponse{
Status: "error",
ErrorType: "503",
}
// vmselects must return an error for the same request when partial
// responses are denied explicitly.
assertSeries(opts.c.vmselect, denyPartialResponse, mustReturnUnavailableError)
assertSeries(opts.c.vmselectGroupRF, denyPartialResponse, mustReturnUnavailableError)
assertSeries(opts.c.vmselectGlobalRF, denyPartialResponse, mustReturnUnavailableError)
assertSeries(opts.c.vmselectGroupGlobalRF, denyPartialResponse, mustReturnUnavailableError)
// Stop the last remaining vmstorage node.
//
// vmselects must return an error when there are no successful vmstorage
// responses.
opts.c.storageGroups[0].stopNodes(tc, 1)
assertSeries(opts.c.vmselect, allowPartialResponse, mustReturnUnavailableError)
assertSeries(opts.c.vmselectGroupRF, allowPartialResponse, mustReturnUnavailableError)
assertSeries(opts.c.vmselectGlobalRF, allowPartialResponse, mustReturnUnavailableError)
assertSeries(opts.c.vmselectGroupGlobalRF, allowPartialResponse, mustReturnUnavailableError)
}
// TestClusterReplication_PartialResponseMultitenant checks how vmselect handles some

View File

@@ -0,0 +1,51 @@
{
"ulid": "01JFJBS3YP1SHZ3PJQ6HK76EC3",
"minTime": 1734709200000,
"maxTime": 1734709320000,
"stats": {
"numSamples": 400,
"numSeries": 100,
"numChunks": 100
},
"compaction": {
"level": 1,
"sources": [
"01JFJBS3YP1SHZ3PJQ6HK76EC3"
],
"parents": [
{
"ulid": "00000000000000000000000000",
"minTime": 0,
"maxTime": 0
}
],
"hints": [
"from-out-of-order"
]
},
"version": 1,
"out_of_order": false,
"thanos": {
"labels": {},
"downsample": {
"resolution": 0
},
"source": "receive",
"segment_files": [
"000001"
],
"files": [
{
"rel_path": "chunks/000001",
"size_bytes": 4808
},
{
"rel_path": "index",
"size_bytes": 55021
},
{
"rel_path": "meta.json"
}
]
}
}

Binary file not shown.

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,139 @@
package tests
import (
"encoding/json"
"fmt"
"io"
"os"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/VictoriaMetrics/VictoriaMetrics/apptest"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/fs"
)
const (
testMimirPath = "testdata/mimir-tsdb"
expectedMimirResponseFile = "./testdata/mimir-tsdb/expected_response.json"
)
func TestSingleVmctlMimirProtocol(t *testing.T) {
fs.MustRemoveDir(t.Name())
tc := apptest.NewTestCase(t)
defer tc.Stop()
vmsingleDst := tc.MustStartDefaultVmsingle()
vmAddr := fmt.Sprintf("http://%s/", vmsingleDst.HTTPAddr())
dir, err := os.Getwd()
if err != nil {
t.Fatalf("cannot get current working directory: %s", err)
}
path := fmt.Sprintf("fs://%s/%s", dir, testMimirPath)
vmctlFlags := []string{
`mimir`,
`--mimir-tenant-id=anonymous`,
`--mimir-filter-time-start=2024-12-01T00:00:00Z`,
`--mimir-filter-time-end=2024-12-31T23:59:59Z`,
`--mimir-custom-s3-endpoint=http://localhost:9000`,
`--mimir-path=` + path,
`--vm-addr=` + vmAddr,
`--disable-progress-bar=true`,
`--vm-concurrency=6`,
`--mimir-concurrency=6`,
}
testMimirProtocol(tc, vmsingleDst, vmctlFlags)
}
func TestClusterVmctlMimirProtocol(t *testing.T) {
fs.MustRemoveDir(t.Name())
tc := apptest.NewTestCase(t)
defer tc.Stop()
cluster := tc.MustStartDefaultCluster()
vmAddr := fmt.Sprintf("http://%s/", cluster.Vminsert.HTTPAddr())
dir, err := os.Getwd()
if err != nil {
t.Fatalf("cannot get current working directory: %s", err)
}
path := fmt.Sprintf("fs://%s/%s", dir, testMimirPath)
vmctlFlags := []string{
`mimir`,
`--mimir-tenant-id=anonymous`,
`--mimir-filter-time-start=2024-12-01T00:00:00Z`,
`--mimir-filter-time-end=2024-12-31T23:59:59Z`,
`--mimir-custom-s3-endpoint=http://localhost:9000`,
`--mimir-path=` + path,
`--vm-addr=` + vmAddr,
`--disable-progress-bar=true`,
`--vm-concurrency=6`,
`--mimir-concurrency=6`,
}
testMimirProtocol(tc, cluster, vmctlFlags)
}
func testMimirProtocol(tc *apptest.TestCase, sut apptest.PrometheusWriteQuerier, vmctlFlags []string) {
t := tc.T()
t.Helper()
cmpOpt := cmpopts.IgnoreFields(apptest.PrometheusAPIV1QueryResponse{}, "Status", "Data.ResultType")
// test for empty data request
got := sut.PrometheusAPIV1Query(t, `{__name__=~".*"}`, apptest.QueryOpts{
Step: "5m",
Time: "2025-06-02T17:14:00Z",
})
want := apptest.NewPrometheusAPIV1QueryResponse(t, `{"data":{"result":[]}}`)
if diff := cmp.Diff(want, got, cmpOpt); diff != "" {
t.Errorf("unexpected response (-want, +got):\n%s", diff)
}
tc.MustStartVmctl("vmctl", vmctlFlags)
sut.ForceFlush(t)
// open the expected series response file
file, err := os.Open(expectedMimirResponseFile)
if err != nil {
t.Fatalf("cannot open expected series response file: %s", err)
}
defer file.Close()
bytes, err := io.ReadAll(file)
if err != nil {
t.Fatalf("cannot read expected series response file: %s", err)
}
var wantResponse apptest.PrometheusAPIV1QueryResponse
if err := json.Unmarshal(bytes, &wantResponse); err != nil {
t.Fatalf("cannot unmarshal expected series response file: %s", err)
}
wantResponse.Sort()
tc.Assert(&apptest.AssertOptions{
// For cluster version, we need to wait longer for the metrics to be stored
Retries: 300,
Msg: `unexpected metrics stored on vmsingle via the prometheus protocol`,
Got: func() any {
expected := sut.PrometheusAPIV1Export(t, `{__name__=~".*"}`, apptest.QueryOpts{
Start: "2024-12-01T15:31:10Z",
End: "2024-12-31T15:32:20Z",
})
expected.Sort()
return expected.Data.Result
},
Want: wantResponse.Data.Result,
CmpOpts: []cmp.Option{
cmpopts.IgnoreFields(apptest.PrometheusAPIV1QueryResponse{}, "Status", "Data.ResultType"),
},
})
}

View File

@@ -16,43 +16,63 @@ import (
"github.com/golang/snappy"
)
// Vmagent holds the state of a vmagent app and provides vmagent-specific functions
type Vmagent struct {
*app
*metricsClient
httpListenAddr string
cli *Client
}
// StartVmagent starts an instance of vmagent with the given flags. It also
// sets the default flags and populates the app instance state with runtime
// values extracted from the application log (such as httpListenAddr)
// StartVmagent starts the latest version of vmagent.
//
// The path to the binary can be provided via VMAGENT_PATH environment
// variable. If the variable is not set, ../../bin/vmagent-race will be
// used.
func StartVmagent(instance string, flags []string, cli *Client, promScrapeConfigFilePath string, output io.Writer) (*Vmagent, error) {
extractREs := []*regexp.Regexp{
httpListenAddrRE,
binary := os.Getenv("VMAGENT_PATH")
if binary == "" {
binary = "../../bin/vmagent-race"
}
app, stderrExtracts, err := startApp(instance, "../../bin/vmagent-race", flags, &appOptions{
app, stderrExtracts, err := startApp(instance, binary, flags, &appOptions{
defaultFlags: map[string]string{
"-httpListenAddr": "127.0.0.1:0",
"-promscrape.config": promScrapeConfigFilePath,
"-remoteWrite.tmpDataPath": fmt.Sprintf("%s/%s-%d", os.TempDir(), instance, time.Now().UnixNano()),
},
extractREs: extractREs,
output: output,
extractREs: []*regexp.Regexp{
httpListenAddrRE,
},
output: output,
})
if err != nil {
return nil, err
}
return newVmagent(app, cli, vmagentRuntimeValues{
httpListenAddr: stderrExtracts[0],
}), nil
}
type vmagentRuntimeValues struct {
httpListenAddr string
}
func newVmagent(app *app, cli *Client, rt vmagentRuntimeValues) *Vmagent {
return &Vmagent{
app: app,
metricsClient: newMetricsClient(cli, stderrExtracts[0]),
httpListenAddr: stderrExtracts[0],
cli: cli,
}, nil
metricsClient: newMetricsClient(cli, rt.httpListenAddr),
httpListenAddr: rt.httpListenAddr,
}
}
// Vmagent holds the state of a vmagent app and provides vmagent-specific
// functions.
type Vmagent struct {
*app
*metricsClient
cli *Client
httpListenAddr string
}
// HTTPAddr returns the address at which the vmagent process is listening
// for http connections.
func (app *Vmagent) HTTPAddr() string {
return app.httpListenAddr
}
// APIV1ImportPrometheus is a test helper function that inserts a
@@ -203,12 +223,6 @@ func (app *Vmagent) PrometheusAPIV1Write(t *testing.T, wr prompb.WriteRequest, o
})
}
// HTTPAddr returns the address at which the vmagent process is listening
// for http connections.
func (app *Vmagent) HTTPAddr() string {
return app.httpListenAddr
}
// sendBlocking sends the data to vmstorage by executing `send` function and
// waits until the data is actually sent.
//

View File

@@ -2,6 +2,7 @@ package apptest
import (
"io"
"os"
"regexp"
"syscall"
"testing"
@@ -10,7 +11,48 @@ import (
"github.com/VictoriaMetrics/VictoriaMetrics/lib/fs"
)
var httpBuilitinListenAddrRE = regexp.MustCompile(`pprof handlers are exposed at http://(.*:\d{1,5})/debug/pprof/`)
// StartVmauth starts the latest version of vmauth.
//
// The path to the binary can be provided via VMAUTH_PATH environment
// variable. If the variable is not set, ../../bin/vmauth-race will be
// used.
func StartVmauth(instance string, flags []string, cli *Client, configFilePath string, output io.Writer) (*Vmauth, error) {
binary := os.Getenv("VMAUTH_PATH")
if binary == "" {
binary = "../../bin/vmauth-race"
}
app, stderrExtracts, err := startApp(instance, binary, flags, &appOptions{
defaultFlags: map[string]string{
"-httpListenAddr": "127.0.0.1:0",
"-auth.config": configFilePath,
},
extractREs: []*regexp.Regexp{
vmauthHttpListenAddrRE,
},
output: output,
})
if err != nil {
return nil, err
}
return newVmauth(app, cli, configFilePath, vmauthRuntimeValues{
httpListenAddr: stderrExtracts[0],
}), nil
}
type vmauthRuntimeValues struct {
httpListenAddr string
}
func newVmauth(app *app, cli *Client, configFilePath string, rt vmauthRuntimeValues) *Vmauth {
return &Vmauth{
app: app,
metricsClient: newMetricsClient(cli, rt.httpListenAddr),
httpListenAddr: rt.httpListenAddr,
configFilePath: configFilePath,
cli: cli,
}
}
// Vmauth holds the state of a vmauth app and provides vmauth-specific
// functions.
@@ -18,38 +60,14 @@ type Vmauth struct {
*app
*metricsClient
cli *Client
httpListenAddr string
configFilePath string
cli *Client
}
// StartVmauth starts an instance of vmauth with the given flags. It also
// sets the default flags and populates the app instance state with runtime
// values extracted from the application log (such as httpListenAddr)
func StartVmauth(instance string, flags []string, cli *Client, configFilePath string, output io.Writer) (*Vmauth, error) {
extractREs := []*regexp.Regexp{
httpBuilitinListenAddrRE,
}
app, stderrExtracts, err := startApp(instance, "../../bin/vmauth-race", flags, &appOptions{
defaultFlags: map[string]string{
"-httpListenAddr": "127.0.0.1:0",
"-auth.config": configFilePath,
},
extractREs: extractREs,
output: output,
})
if err != nil {
return nil, err
}
return &Vmauth{
app: app,
metricsClient: newMetricsClient(cli, stderrExtracts[0]),
httpListenAddr: stderrExtracts[0],
configFilePath: configFilePath,
cli: cli,
}, nil
// GetHTTPListenAddr returns listen http addr
func (app *Vmauth) GetHTTPListenAddr() string {
return app.httpListenAddr
}
// UpdateConfiguration updates the vmauth configuration file with the provided YAML content,
@@ -79,8 +97,3 @@ func (app *Vmauth) UpdateConfiguration(t *testing.T, configFileYAML string) {
t.Fatalf("config were not reloaded after SIGHUP signal; previous total: %d, current total: %d", prevTotal, currTotal)
}
// GetHTTPListenAddr returns listen http addr
func (app *Vmauth) GetHTTPListenAddr() string {
return app.httpListenAddr
}

View File

@@ -1,15 +1,26 @@
package apptest
import "io"
import (
"io"
"os"
)
// StartVmbackup starts an instance of vmbackup with the given flags and waits
// until it exits.
// StartVmbackup starts the latest version of vmbackup with the given flags and
// waits until it exits.
//
// The path to the binary can be provided via VMBACKUP_PATH environment
// variable. If the variable is not set, ../../bin/vmbackup-race will be
// used.
func StartVmbackup(instance, storageDataPath, snapshotCreateURL, dst string, output io.Writer) error {
binary := os.Getenv("VMBACKUP_PATH")
if binary == "" {
binary = "../../bin/vmbackup-race"
}
flags := []string{
"-storageDataPath=" + storageDataPath,
"-snapshot.createURL=" + snapshotCreateURL,
"-dst=" + dst,
}
_, _, err := startApp(instance, "../../bin/vmbackup-race", flags, &appOptions{wait: true, output: output})
_, _, err := startApp(instance, binary, flags, &appOptions{wait: true, output: output})
return err
}

View File

@@ -1,9 +1,23 @@
package apptest
import "io"
import (
"io"
"os"
)
// StartVmctl starts an instance of vmctl cli with the given flags
// StartVmctl starts the latest version of vmctl with the given flags and
// waits until it exits.
//
// The path to the binary can be provided via VMCTL_PATH environment
// variable. If the variable is not set, ../../bin/vmctl-race will be
// used.
func StartVmctl(instance string, flags []string, output io.Writer) error {
_, _, err := startApp(instance, "../../bin/vmctl-race", flags, &appOptions{wait: true, output: output})
binary := os.Getenv("VMCTL_PATH")
if binary == "" {
binary = "../../bin/vmctl-race"
}
_, _, err := startApp(instance, binary, flags, &appOptions{wait: true, output: output})
return err
}

View File

@@ -3,23 +3,13 @@ package apptest
import (
"fmt"
"io"
"os"
"regexp"
"strings"
"testing"
"time"
)
// Vminsert holds the state of a vminsert app and provides vminsert-specific
// functions.
type Vminsert struct {
*app
*metricsClient
*vminsertClient
httpListenAddr string
clusternativeListenAddr string
}
// storageNodes returns the storage node addresses passed to vminsert via
// -storageNode command line flag.
func storageNodes(flags []string) []string {
@@ -31,9 +21,11 @@ func storageNodes(flags []string) []string {
return nil
}
// StartVminsert starts an instance of vminsert with the given flags. It also
// sets the default flags and populates the app instance state with runtime
// values extracted from the application log (such as httpListenAddr)
// StartVminsert starts the latest version of vminsert.
//
// The path to the binary can be provided via VMINSERT_PATH environment
// variable. If the variable is not set, ../../bin/vminsert-race will be
// used.
func StartVminsert(instance string, flags []string, cli *Client, output io.Writer) (*Vminsert, error) {
extractREs := []*regexp.Regexp{
httpListenAddrRE,
@@ -48,11 +40,15 @@ func StartVminsert(instance string, flags []string, cli *Client, output io.Write
extractREs = append(extractREs, regexp.MustCompile(logRecord))
}
app, stderrExtracts, err := startApp(instance, "../../bin/vminsert-race", flags, &appOptions{
binary := os.Getenv("VMINSERT_PATH")
if binary == "" {
binary = "../../bin/vminsert-race"
}
app, stderrExtracts, err := startApp(instance, binary, flags, &appOptions{
defaultFlags: map[string]string{
"-httpListenAddr": "127.0.0.1:0",
"-clusternativeListenAddr": "127.0.0.1:0",
"-graphiteListenAddr": ":0",
"-graphiteListenAddr": "127.0.0.1:0",
"-opentsdbListenAddr": "127.0.0.1:0",
"-clusternative.vminsertConnsShutdownDuration": "1ms",
},
@@ -63,27 +59,56 @@ func StartVminsert(instance string, flags []string, cli *Client, output io.Write
return nil, err
}
metricsClient := newMetricsClient(cli, stderrExtracts[0])
return &Vminsert{
app: app,
metricsClient: metricsClient,
vminsertClient: &vminsertClient{
vminsertCli: cli,
url: func(op, path string, opts QueryOpts) string {
return getClusterPath(stderrExtracts[0], op, path, opts)
},
openTSDBURL: func(op, path string, opts QueryOpts) string {
return getClusterPath(stderrExtracts[3], op, path, opts)
},
graphiteListenAddr: stderrExtracts[2],
sendBlocking: func(t *testing.T, numRecordsToSend int, send func()) {
t.Helper()
sendBlocking(t, metricsClient, numRecordsToSend, send)
},
},
return newVminsert(app, cli, vminsertRuntimeValues{
httpListenAddr: stderrExtracts[0],
clusternativeListenAddr: stderrExtracts[1],
}, nil
graphiteListenAddr: stderrExtracts[2],
openTSDBListenAddr: stderrExtracts[3],
}), nil
}
type vminsertRuntimeValues struct {
httpListenAddr string
clusternativeListenAddr string
graphiteListenAddr string
openTSDBListenAddr string
}
func newVminsert(app *app, cli *Client, rt vminsertRuntimeValues) *Vminsert {
metricsClient := newMetricsClient(cli, rt.httpListenAddr)
vminsertClient := &vminsertClient{
vminsertCli: cli,
url: func(op, path string, opts QueryOpts) string {
return getClusterPath(rt.httpListenAddr, op, path, opts)
},
openTSDBURL: func(op, path string, opts QueryOpts) string {
return getClusterPath(rt.openTSDBListenAddr, op, path, opts)
},
graphiteListenAddr: rt.graphiteListenAddr,
sendBlocking: func(t *testing.T, numRecordsToSend int, send func()) {
t.Helper()
sendBlocking(t, metricsClient, numRecordsToSend, send)
},
}
return &Vminsert{
app: app,
metricsClient: metricsClient,
vminsertClient: vminsertClient,
httpListenAddr: rt.httpListenAddr,
clusternativeListenAddr: rt.clusternativeListenAddr,
}
}
// Vminsert holds the state of a vminsert app and provides vminsert-specific
// functions.
type Vminsert struct {
*app
*metricsClient
*vminsertClient
httpListenAddr string
clusternativeListenAddr string
}
// ClusternativeListenAddr returns the address at which the vminsert process is

View File

@@ -1,14 +1,25 @@
package apptest
import "io"
import (
"io"
"os"
)
// StartVmrestore starts an instance of vmrestore with the given flags and waits
// until it exits.
// StartVmrestore starts the latest version of vmrestore with the given flags
// and waits until it exits.
//
// The path to the binary can be provided via VMRESTORE_PATH environment
// variable. If the variable is not set, ../../bin/vmrestore-race will be
// used.
func StartVmrestore(instance, src, storageDataPath string, output io.Writer) error {
binary := os.Getenv("VMRESTORE_PATH")
if binary == "" {
binary = "../../bin/vmrestore-race"
}
flags := []string{
"-src=" + src,
"-storageDataPath=" + storageDataPath,
}
_, _, err := startApp(instance, "../../bin/vmrestore-race", flags, &appOptions{wait: true, output: output})
_, _, err := startApp(instance, binary, flags, &appOptions{wait: true, output: output})
return err
}

View File

@@ -3,26 +3,21 @@ package apptest
import (
"fmt"
"io"
"os"
"regexp"
)
// Vmselect holds the state of a vmselect app and provides vmselect-specific
// functions.
type Vmselect struct {
*app
*metricsClient
*vmselectClient
httpListenAddr string
clusternativeListenAddr string
cli *Client
}
// StartVmselect starts an instance of vmselect with the given flags. It also
// sets the default flags and populates the app instance state with runtime
// values extracted from the application log (such as httpListenAddr)
// StartVmselect starts the latest version of vmselect.
//
// The path to the binary can be provided via VMSELECT_PATH environment
// variable. If the variable is not set, ../../bin/vmselect-race will be
// used.
func StartVmselect(instance string, flags []string, cli *Client, output io.Writer) (*Vmselect, error) {
app, stderrExtracts, err := startApp(instance, "../../bin/vmselect-race", flags, &appOptions{
binary := os.Getenv("VMSELECT_PATH")
if binary == "" {
binary = "../../bin/vmselect-race"
}
app, stderrExtracts, err := startApp(instance, binary, flags, &appOptions{
defaultFlags: map[string]string{
"-httpListenAddr": "127.0.0.1:0",
"-clusternativeListenAddr": "127.0.0.1:0",
@@ -37,21 +32,43 @@ func StartVmselect(instance string, flags []string, cli *Client, output io.Write
return nil, err
}
return newVmselect(app, cli, vmselectRuntimeValues{
httpListenAddr: stderrExtracts[0],
clusternativeListenAddr: stderrExtracts[1],
}), nil
}
type vmselectRuntimeValues struct {
httpListenAddr string
clusternativeListenAddr string
}
func newVmselect(app *app, cli *Client, rt vmselectRuntimeValues) *Vmselect {
return &Vmselect{
app: app,
metricsClient: newMetricsClient(cli, stderrExtracts[0]),
metricsClient: newMetricsClient(cli, rt.httpListenAddr),
vmselectClient: &vmselectClient{
vmselectCli: cli,
url: func(op, path string, opts QueryOpts) string {
return getClusterPath(stderrExtracts[0], op, path, opts)
return getClusterPath(rt.httpListenAddr, op, path, opts)
},
metricNamesStatsResetURL: fmt.Sprintf("http://%s/admin/api/v1/admin/status/metric_names_stats/reset", stderrExtracts[0]),
tenantsURL: fmt.Sprintf("http://%s/admin/tenants", stderrExtracts[0]),
metricNamesStatsResetURL: fmt.Sprintf("http://%s/admin/api/v1/admin/status/metric_names_stats/reset", rt.httpListenAddr),
tenantsURL: fmt.Sprintf("http://%s/admin/tenants", rt.httpListenAddr),
},
httpListenAddr: stderrExtracts[0],
clusternativeListenAddr: stderrExtracts[1],
cli: cli,
}, nil
httpListenAddr: rt.httpListenAddr,
clusternativeListenAddr: rt.clusternativeListenAddr,
}
}
// Vmselect holds the state of a vmselect app and provides vmselect-specific
// functions.
type Vmselect struct {
*app
*metricsClient
*vmselectClient
httpListenAddr string
clusternativeListenAddr string
}
// ClusternativeListenAddr returns the address at which the vmselect process is

Some files were not shown because too many files have changed in this diff Show More