Compare commits

...

124 Commits

Author SHA1 Message Date
hagen1778
7a2ccee6d0 app/vmui: update error message for no matched rules
Signed-off-by: hagen1778 <roman@victoriametrics.com>
2025-03-03 16:54:55 +01:00
hagen1778
fad5c25187 update changelog
Signed-off-by: hagen1778 <roman@victoriametrics.com>
2025-03-03 16:47:51 +01:00
Yury Molodov
f7457531d5 vmui: show a warning message when no series match in Downsampling debug 2025-03-03 15:11:52 +01:00
Yury Molodov
bc1883daca vmui: fix infinite loader on downsampling page 2025-03-03 14:51:45 +01:00
Max Kotliar
649f4d5e00 docs/downsampling: fix terminology. replace "deduplication" with "downsample". (#8421)
### Describe Your Changes

I guess a "downsampling" term is more appropriate than "deduplication"
in the downsampling paragraph.

### Checklist

The following checks are **mandatory**:

- [x] My change adheres [VictoriaMetrics contributing
guidelines](https://docs.victoriametrics.com/contributing/).
2025-03-03 14:12:36 +01:00
Nikolay
a55ac4495f docs: update release guide (#8423)
* move testing step after assets upload
* add LTS build step before testing step

Signed-off-by: f41gh7 <nik@victoriametrics.com>
2025-03-03 14:10:06 +01:00
Hui Wang
52988ebdc8 vmalert: add time buckets stats pipe check for vlogs expression (#8400)
VictoriaLogs inserts `_time` field as a label in result when query with
[time buckets stats
pipe](https://docs.victoriametrics.com/victorialogs/logsql/#stats-by-time-buckets),
making the result meaningless and may lead to cardinality issues.
>curl --location --request POST
'https://play-vmlogs.victoriametrics.com/select/logsql/stats_query?query=_time%3A1m%20%7C%20stats%20by%20(_time%3A10s)%20count%20()%20as%20total'

>{"status":"success","data":{"resultType":"vector","result":[{"metric":{"__name__":"total","_time":"2025-01-24T12:31:30Z"},"value":[1737721904.4476516,"12"]},{"metric":{"__name__":"total","_time":"2025-01-24T12:31:10Z"},"value":[1737721904.4476516,"10"]},{"metric":{"__name__":"total","_time":"2025-01-24T12:31:00Z"},"value":[1737721904.4476516,"10"]},{"metric":{"__name__":"total","_time":"2025-01-24T12:31:20Z"},"value":[1737721904.4476516,"12"]},{"metric":{"__name__":"total","_time":"2025-01-24T12:30:50Z"},"value":[1737721904.4476516,"10"]},{"metric":{"__name__":"total","_time":"2025-01-24T12:30:40Z"},"value":[1737721904.4476516,"9"]}]}}%

---------

Signed-off-by: hagen1778 <roman@victoriametrics.com>
Co-authored-by: hagen1778 <roman@victoriametrics.com>
2025-03-03 14:09:34 +01:00
hagen1778
065a3d068c docs/victorialogs/vmalert: apply consistent formatting and fix typos
* use unified numeric list;
* apply line width limits;
* remove time filter from quantile examples, as we suggest to
not use time filters.

Signed-off-by: hagen1778 <roman@victoriametrics.com>
2025-03-03 13:57:30 +01:00
Artem Fetishev
43a742ba0c docs: bump last LTS versions
Signed-off-by: Artem Fetishev <rtm@victoriametrics.com>
2025-03-03 11:44:37 +01:00
Artem Fetishev
e69c744dd4 docs/CHANGELOG.md: update changelog with LTS release notes
Signed-off-by: Artem Fetishev <rtm@victoriametrics.com>
2025-03-03 11:43:00 +01:00
hagen1778
6db97d6f79 lib/timeutil: add test for ParseDuration
See https://github.com/VictoriaMetrics/VictoriaMetrics/pull/8403#discussion_r1976110052

Signed-off-by: hagen1778 <roman@victoriametrics.com>
2025-03-03 10:46:01 +01:00
Max Kotliar
7451a3631a docs: fix typo in docs (#8420)
### Describe Your Changes

Just a small typo in the docs

### Checklist

The following checks are **mandatory**:

- [ ] My change adheres [VictoriaMetrics contributing
guidelines](https://docs.victoriametrics.com/contributing/).
2025-03-03 10:26:41 +01:00
Roman Khavronenko
63f6ac3ff8 lib/promutils: move time-related funcs from promutils to timeutil (#8403)
Since funcs `ParseDuration` and `ParseTimeMsec` are used in vlogs,
vmalert, victoriametrics and other components, importing promutils only
for this reason makes them to export irrelevant
`vm_rows_invalid_total{type="prometheus"}` metric.

This change removes `vm_rows_invalid_total{type="prometheus"}` metric
from /metrics page for these components.

### Describe Your Changes

Please provide a brief description of the changes you made. Be as
specific as possible to help others understand the purpose and impact of
your modifications.

### Checklist

The following checks are **mandatory**:

- [ ] My change adheres [VictoriaMetrics contributing
guidelines](https://docs.victoriametrics.com/contributing/).

---------

Signed-off-by: hagen1778 <roman@victoriametrics.com>
2025-03-03 10:25:42 +01:00
Hui Wang
281f1a94e4 vmalert-tool: clean up the temporary storage path when process is ter… (#8407)
…minated by SIGTERM or SIGINT
2025-02-28 15:17:05 +01:00
Zakhar Bessarab
7f6156e46d docs: fix link to quay docker image for single-node (#8414)
### Describe Your Changes

Previous link was incorrectly created by adding repository name to
organization link.
All other links are using proper schema.

This is a follow-up for 6ff61a1c.

### Checklist

The following checks are **mandatory**:

- [x] My change adheres [VictoriaMetrics contributing
guidelines](https://docs.victoriametrics.com/contributing/).

Signed-off-by: Zakhar Bessarab <z.bessarab@victoriametrics.com>
2025-02-28 15:13:52 +01:00
Hui Wang
4015db18bc vmalert-tool: add flag -httpListenPort to specify the port used dur… (#8408)
…ing testing and make the temporary storage path more unique

address https://github.com/VictoriaMetrics/VictoriaMetrics/issues/8393

---------

Co-authored-by: Roman Khavronenko <roman@victoriametrics.com>
2025-02-28 11:25:35 +01:00
Zakhar Bessarab
99de272b72 lib/promrelabel/scrape_url: properly parse IPv6 address from __address__ label
Fix parsing of IPv6 addresses after discovery. Previously, it could lead
to target being discovered and discarded afterwards.

See: https://github.com/VictoriaMetrics/VictoriaMetrics/issues/8374

---------

Signed-off-by: Zakhar Bessarab <z.bessarab@victoriametrics.com>
2025-02-28 14:19:10 +04:00
Zakhar Bessarab
6ff61a1c09 docs/all: add refs to Quay mirror for docker images (#8406)
### Describe Your Changes

See: https://github.com/VictoriaMetrics/VictoriaMetrics/issues/4116

### Checklist

The following checks are **mandatory**:

- [x] My change adheres [VictoriaMetrics contributing
guidelines](https://docs.victoriametrics.com/contributing/).

---------

Signed-off-by: Zakhar Bessarab <z.bessarab@victoriametrics.com>
Co-authored-by: Roman Khavronenko <roman@victoriametrics.com>
2025-02-28 11:14:10 +01:00
Zakhar Bessarab
ed0db37d1d app/vmbackupmanager: fix error propagation for policy apply
Previously error text was silently discarded.

This commit properly formats error message and add actual error cause to it.
2025-02-28 10:40:50 +01:00
Hui Wang
a36c8a9679 app/vmgateway: fix data query in rate limiter
Properly format datasource query request in rate limiter
2025-02-28 10:40:46 +01:00
hagen1778
c80e2d16a5 deployment/docker: fix typo in markdown
Signed-off-by: hagen1778 <roman@victoriametrics.com>
2025-02-28 09:57:12 +01:00
hagen1778
e10b963a4d docs: link to internal docs for alerting and recording rules
Signed-off-by: hagen1778 <roman@victoriametrics.com>
2025-02-28 09:46:57 +01:00
hagen1778
3a04d9aca8 docs: fix markdown in stream aggregation docs
Availability mark was breaking the line.

Signed-off-by: hagen1778 <roman@victoriametrics.com>
2025-02-28 09:07:51 +01:00
Aliaksandr Valialkin
bfa9fee424 docs/VictoriaLogs/CHANGELOG.md: typo fix: filter -> filters 2025-02-28 00:09:00 +01:00
Aliaksandr Valialkin
d7431e69fd deployment/docker: update VictoriaLogs Docker image tag from v1.14.0-victorialogs to v1.15.0-victorialogs
See https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.15.0-victorialogs
2025-02-28 00:02:55 +01:00
Aliaksandr Valialkin
0e5e90fe74 docs/VictoriaLogs/CHANGELOG.md: cut v1.15.0-victorialogs release 2025-02-27 22:58:18 +01:00
Aliaksandr Valialkin
5790c5c75d Makefile: update golangc-lint from v1.64.4 to v1.64.5
See https://github.com/golangci/golangci-lint/releases/tag/v1.64.5
2025-02-27 22:54:19 +01:00
Aliaksandr Valialkin
744ac496bd lib/logstorage: add ability to specify field name prefixes inside fields (...) lists passed to pack_json and pack_logfmt pipes 2025-02-27 22:54:18 +01:00
Aliaksandr Valialkin
03a04b4408 docs/changelog/CHANGELOG.md: upgrading from Go1.23.6 to Go1.24.0 doesn't fix any know security issues
The upgrade from Go1.23.6 to Go1.24.0 is a feature, not a security fix.

This is a follow-up for ea6ed4232a
2025-02-27 22:54:18 +01:00
Zhu Jiekun
117b7ce6b7 bugfix: negative rate result when lookbehind window longer than search.maxLookback (#8378)
### Describe Your Changes

#8342 

fix negative rate result when the lookbehind window is longer than
`-search.maxLookback` or `-search.maxStalenessInterval` and data
contains gap.

This issue was introduced since
[v1.110.0](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/8072).

### Checklist

The following checks are **mandatory**:

- [x] My change adheres [VictoriaMetrics contributing
guidelines](https://docs.victoriametrics.com/contributing/).
2025-02-27 22:52:39 +01:00
Roman Khavronenko
38d46d149f lib/prompbmarshal: move MustParsePromMetrics to protoparser/prometheus (#8405)
`MustParsePromMetrics` imports `lib/protoparser/prometheus`, and this
package exposes the following metrics:
```
vm_protoparser_rows_read_total{type="promscrape"}
vm_rows_invalid_total{type="prometheus"}
```

It means every package that uses `lib/prompbmarshal` will start exposing
these metrics. For example, vlogs imports `lib/protoparser/common` which
uses `lib/prompbmarshal.Label`. And only because of this vlogs starts
exposing unrelated prometheus metrics on /metrics page.

Moving `MustParsePromMetrics` to `lib/protoparser/prometheus` seems like
the leas intrusive change.


-----------

Depends on another change
https://github.com/VictoriaMetrics/VictoriaMetrics/pull/8403

Signed-off-by: hagen1778 <roman@victoriametrics.com>
2025-02-27 22:50:27 +01:00
Aliaksandr Valialkin
84d5771b41 lib/logstorage: allow passing * at in(*), contains_any(*) and contains_all(*)
Such filters are equivalent to `match all` filter aka `*`. These filters are needed for VictoriaLogs plugin for Grafana.

See https://github.com/VictoriaMetrics/victorialogs-datasource/issues/238#issuecomment-2685447673
2025-02-27 11:37:43 +01:00
Roman Khavronenko
f39ce2aeef dashboards: update victorialogs dashboard (#8398)
* stats panels:
* * add ingested logs/bytes over 24h
* * fix correctness compression ratio and disk space usage panels
* overview panels:
* * add requests errors rate, logs rate, query duration panels
* add troubleshooting section:
* * add non-default falgs, dropped logs panels
* update descriptions of the panels and version requirements

Depends on https://github.com/VictoriaMetrics/VictoriaMetrics/pull/8396

--------------

<img width="1489" alt="image"
src="https://github.com/user-attachments/assets/5257b8e1-a2e7-4143-94c7-bb42b19e4206"
/>

<img width="1499" alt="image"
src="https://github.com/user-attachments/assets/1b521c58-6e12-4d38-b052-93abdac3b4fc"
/>

<img width="1468" alt="image"
src="https://github.com/user-attachments/assets/3010cf0a-8ca7-48d4-9967-328b37fdbc28"
/>

---------

Signed-off-by: hagen1778 <roman@victoriametrics.com>
2025-02-27 11:15:52 +01:00
Roman Khavronenko
ba2bf9e73a vlogs: add metrics to track select queries duration (#8396)
This change would help to track slow queries via alerts or dashboards.

---------

Signed-off-by: hagen1778 <roman@victoriametrics.com>
2025-02-27 11:11:26 +01:00
hagen1778
47d0bba8f0 docs: update never-firing alerts chapter
* add availability marker
* mention article https://victoriametrics.com/blog/never-firing-alerts/

Signed-off-by: hagen1778 <roman@victoriametrics.com>
2025-02-27 09:57:36 +01:00
Zakhar Bessarab
da059d1ea1 deployment/docker/make: publish images to multiple repos (#8391)
### Describe Your Changes

Updated `publish-via-docker` task to push images to multiple registries
as defined by `DOCKER_REGISTRIES`. By default, publish pushes images to
both docker.io and quay.io. It is possible to choose a single repo by
overriding `DOCKER_REGISTRIES`, for example: `DOCKER_REGISTRIES=quay.io
make publish-victoria-logs`

Note that `package-via-docker` task is not using multiple registries as
there is usually little sense in building same image with different tags
for local use.

See: https://github.com/VictoriaMetrics/VictoriaMetrics/issues/4116
### Checklist

The following checks are **mandatory**:

- [x] My change adheres [VictoriaMetrics contributing
guidelines](https://docs.victoriametrics.com/contributing/).

Signed-off-by: Zakhar Bessarab <z.bessarab@victoriametrics.com>
2025-02-27 11:49:50 +04:00
f41gh7
ea6ed4232a deployment/docker: update Go builder from 1.23.6 to 1.24.0
Changes: https://github.com/golang/go/issues?q=milestone%3AGo1.24
Release notes: https://tip.golang.org/doc/go1.24

Signed-off-by: f41gh7 <nik@victoriametrics.com>
2025-02-26 19:14:49 +01:00
Andrii Chubatiuk
b27e9437a3 app/vminsert: properly apply relabelingConfig for scrapped metrics
Due to regression introduced at v1.108.0 release with the following commit:
https://github.com/VictoriaMetrics/VictoriaMetrics/pull/7399/files#diff-0f7ec2fdf4bb6e94f786c37f19ec837995eca8c61529c3f3436ae332a4a93a5bR56

VictoriaMetrics single-node started to ignore global relabeling configuration.

 This commit properly checks if relabeling is needed.

Related issue:
https://github.com/VictoriaMetrics/VictoriaMetrics/issues/8389
2025-02-26 16:05:26 +01:00
Zhu Jiekun
3d3480140c lib/storage: properly cache extDB metricsID on search error
Previously, if indexDB search failed for some reason during search at previous indexDB (aka extDB), VictoriaMetrics stored empty search result at cache. It could cause incorrect search results at subsequent requests.

 This commit checks search error and stores request results only on success. 

Related issue:
https://github.com/VictoriaMetrics/VictoriaMetrics/issues/8345
2025-02-26 15:48:25 +01:00
Aliaksandr Valialkin
ae5e28524e lib/logstorage: do not treat a string with leading zeros as a number at tryParseUint64
The "00123" string shouldn't be treated as 123 number.

Updates https://github.com/VictoriaMetrics/VictoriaMetrics/issues/8361
2025-02-25 22:10:36 +01:00
Aliaksandr Valialkin
b484112f22 docs/VictoriaLogs/CHANGELOG.md: fix link to elasticsearch data ingestion docs 2025-02-25 21:31:16 +01:00
Aliaksandr Valialkin
1da04db33a deployment/docker: update VictoriaLogs Docker image tag from v1.13.0-victorialogs to v1.14.0-victorialogs
See https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.14.0-victorialogs
2025-02-25 21:24:42 +01:00
Aliaksandr Valialkin
ef2415e0c5 docs/VictoriaLogs/CHANGELOG.md: cut v1.14.0-victorialogs release 2025-02-25 21:19:58 +01:00
Aliaksandr Valialkin
0de7c8987c app/vlselect/logsql: follow-up for 521af4d7a3
The ts variable must be initialized to the q.GetTimestamp() for every processed row inside the `for range timestamps` loop.

The original bug has been introduced in the commit 42c21ff671

Updates https://github.com/VictoriaMetrics/VictoriaMetrics/issues/8312
Updates https://github.com/VictoriaMetrics/VictoriaMetrics/pull/8385
2025-02-25 21:07:54 +01:00
Zhu Jiekun
521af4d7a3 fix: [stats_query_range] fix inconsistent result caused by shared variable (#8385)
### Describe Your Changes

Previously, `timestamp` variable was shared by goroutines and modified
during `writeBlock`, verified in
https://github.com/VictoriaMetrics/VictoriaMetrics/issues/8312.

This pull request create new variable within each goroutine to avoid
race condition.

### Checklist

The following checks are **mandatory**:

- [x] My change adheres [VictoriaMetrics contributing
guidelines](https://docs.victoriametrics.com/contributing/).
- [ ]  Test on sandbox.

Co-authored-by: Aliaksandr Valialkin <valyala@victoriametrics.com>
2025-02-25 20:56:03 +01:00
Aliaksandr Valialkin
1f75e5bb59 lib/logstorage: optimize common regex filters generated by Grafana
For example, `field:~".+"`, `field:~".*"` or `field:""`

Replace such filters to faster ones. For example, `field:~".*"` is replaced with `*`,
while `field:~".+"` is replaced with `field:*`.
2025-02-25 20:34:28 +01:00
Aliaksandr Valialkin
fef2cb9dc7 lib/regexutil: speed up Regex.MatchString for ".*" 2025-02-25 20:34:28 +01:00
Roman Khavronenko
72bee659f5 docs: point github badge to the main action for master branch (#8388)
Before, badge could have shown the latest build result. Even for open
PRs. Now, it shows the latest build result for `master` branch.

See
https://docs.github.com/en/actions/monitoring-and-troubleshooting-workflows/monitoring-workflows/adding-a-workflow-status-badge

Signed-off-by: hagen1778 <roman@victoriametrics.com>
2025-02-25 18:59:05 +01:00
hagen1778
49fff7989b docs: fix link to release for search.disableImplicitConversion feature
Signed-off-by: hagen1778 <roman@victoriametrics.com>
2025-02-25 18:54:44 +01:00
Aliaksandr Valialkin
82cdcec6c6 lib/logstorage: run make fmt after 30974e7f3f 2025-02-25 18:37:40 +01:00
Aliaksandr Valialkin
128f6d78ff docs/VictoriaLogs/FAQ.md: add a question on how to estimate the needed compute resources for the particular workload 2025-02-25 18:24:51 +01:00
Aliaksandr Valialkin
30974e7f3f lib/logstorage: add le_field and lt_field filters
These filters can be used for selecting logs where one field value is less than another field value.
These filter complement `<=` and `<` filters for constant literals.
2025-02-25 18:24:50 +01:00
Aliaksandr Valialkin
edc750dd55 lib/logstorage: optimize eq_filter when it is applied to fields of the same type 2025-02-25 18:24:15 +01:00
Andrii Chubatiuk
7a1c84b6ec vlinsert: accept ES ping requests to endpoint without trailing slash (#8354)
### Describe Your Changes

related issue
https://github.com/VictoriaMetrics/VictoriaMetrics/issues/8353

### Checklist

The following checks are **mandatory**:

- [ ] My change adheres [VictoriaMetrics contributing
guidelines](https://docs.victoriametrics.com/contributing/).

---------

Signed-off-by: hagen1778 <roman@victoriametrics.com>
Co-authored-by: hagen1778 <roman@victoriametrics.com>
2025-02-25 15:48:40 +01:00
Roman Khavronenko
788c740d62 app/vmctl: respect time filter when exploring time series for influxdb (#8371)
https://github.com/VictoriaMetrics/VictoriaMetrics/issues/8259


https://docs.influxdata.com/influxdb/v1/query_language/explore-schema/#run-a-show-series-query-limited-by-time

### Checklist

The following checks are **mandatory**:

- [ ] My change adheres [VictoriaMetrics contributing
guidelines](https://docs.victoriametrics.com/contributing/).

---------

Signed-off-by: hagen1778 <roman@victoriametrics.com>
2025-02-25 09:26:58 +01:00
Roman Khavronenko
cddccfde57 dashboards: add new panels to VictoriaMetrics dashboards (#8369)
* FEATURE:
[dashboards/single](https://grafana.com/grafana/dashboards/10229) and
[dashboards/cluster](https://grafana.com/grafana/dashboards/11176): add
panel `Deduplication rate` that shows how many samples are
[deduplicated](https://docs.victoriametrics.com/#deduplication) during
merges or read queries by VictoriaMetrics components.
* FEATURE:
[dashboards/single](https://grafana.com/grafana/dashboards/10229) and
[dashboards/cluster](https://grafana.com/grafana/dashboards/11176): add
panel `Number of snapshots` that shows the max number of
[snapshots](https://docs.victoriametrics.com/#how-to-work-with-snapshots)
across vmstorage nodes. This panel should help in disk usage
[troubleshooting](https://docs.victoriametrics.com/#snapshot-troubleshooting).
* FEATURE:
[dashboards/single](https://grafana.com/grafana/dashboards/10229) and
[dashboards/cluster](https://grafana.com/grafana/dashboards/11176):
account for samples dropped according to [relabeling
config](https://docs.victoriametrics.com/#relabeling) in `Samples
dropped for last 1h` panel.
* FEATURE:
[dashboards/single](https://grafana.com/grafana/dashboards/10229) and
[dashboards/cluster](https://grafana.com/grafana/dashboards/11176): show
number of parts in the last partition on `LSM parts max by type` panel.
Before, the resulting graph could be skewed by the max number of parts
across all partitions. Displaying parts for the latest partition is the
correct way to show if storage is currently impacted by merge delays.
* FEATURE:
[dashboards/cluster](https://grafana.com/grafana/dashboards/11176): add
panel `Partial query results` that shows the number of served [partial
responses](https://docs.victoriametrics.com/cluster-victoriametrics/#cluster-availability)
by vmselects.


-------------

Sorry, git diff is a mess. Dashboards switched to Grafana 11 and it
broke consistency of changes.

---------

Signed-off-by: hagen1778 <roman@victoriametrics.com>
2025-02-25 09:25:34 +01:00
Roman Khavronenko
8f87427c81 deployment/rules: add alerting rule TooHighQueryLoad (#8365)
TooHighQueryLoad should trigger when vmsingle or vmselect can't start
processing read queries for last 15min.

Signed-off-by: hagen1778 <roman@victoriametrics.com>
2025-02-25 09:22:25 +01:00
hagen1778
6a675bb69b docs: put link to index-disabling-feature to a better place
Signed-off-by: hagen1778 <roman@victoriametrics.com>
2025-02-24 17:03:45 +01:00
hagen1778
3828c01540 docs: properly update version tooltips
The version tooltips were incorrectly set to `#tip` instead of `#`,
so ` make docs-update-version` didn't catch them.

Signed-off-by: hagen1778 <roman@victoriametrics.com>
2025-02-24 17:00:01 +01:00
f41gh7
0314b79727 docs: update vm apps versions to the v1.112.0 release
Signed-off-by: f41gh7 <nik@victoriametrics.com>
2025-02-24 15:55:06 +01:00
f41gh7
1e368add33 docs/changelog: mention LTS releases 2025-02-24 15:34:17 +01:00
f41gh7
1064c15454 CHANGELOG.md: cut v1.112.0 release 2025-02-24 15:34:17 +01:00
Aliaksandr Valialkin
bc69d5f1a4 lib/mergeset: explicitly pass the interval for flushing in-memory data to disk at MustOpenTable()
This allows using different intervals for flushing in-memory data among different mergeset.Table instances.

The initial user of this feature is lib/logstorage.Storage, which explicitly passes Storage.flushInterval
to every created mereset.Table instance. Previously mergeset.Table instances were using 5 seconds
flush interval, which didn't depend on the Storage.flushInterval.

Updates https://github.com/VictoriaMetrics/VictoriaMetrics/issues/4775
2025-02-24 15:23:50 +01:00
Aliaksandr Valialkin
6764a03fbe lib/logstorage: properly use datadb.flushInterval as an interval between flushes for the in-memory parts
The dataFlushInterval variable has been mistakenly introduced in the commit 9dbd0f9085

Updates https://github.com/VictoriaMetrics/VictoriaMetrics/issues/4775
2025-02-24 15:06:36 +01:00
Aliaksandr Valialkin
d72c486d79 vendor: update github.com/VictoriaMetrics/metricsql from v0.83.1 to v0.84.0
The new version of github.com/VictoriaMetrics/metricsql handles $__interval and $__rate_interval
inside rollup functions in more correct way - it drops square brakets, so VictoriaMetrics
could automatically detect the needed lookbehind window depending on the time distance between real samples.

For example, rate(m[$__rate_interval]) is parsed and processed as rate(m) now.
2025-02-24 15:01:08 +01:00
Aliaksandr Valialkin
2857188939 lib/logstorage: limit the maximum log field name length, which can be generated by JSONParser.ParseLogMessage
Make sure that the maximum log field name, which can be generated by JSONParser.ParseLogMessage,
doesn't exceed the hardcoded limit maxFieldNameSize. Stop flattening of nested JSON objects
when the resulting field name becomes longer than maxFieldNameSize, and return the nested JSON object
as a string instead.

This should prevent from parse errors when ingesting deeply nested JSON logs with long field names.
2025-02-24 12:55:24 +01:00
Aliaksandr Valialkin
c0b9732fc8 lib/logstorage: add a benchmark for JSONParser.ParseLogMessage 2025-02-24 12:50:49 +01:00
Aliaksandr Valialkin
bfad966e72 lib/encoding/zstd: reduce the number of cached zstd.Encoder instances
Use the real compression level supported by github.com/klauspost/compress/zstd as a cache map key.
The number of real compression levels is smaller than the number of zstd compression levels.
This should reduce the number of cached zstd.Encoder instances.

See https://github.com/klauspost/compress/discussions/1025
See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/7503#issuecomment-2500088591
2025-02-24 00:45:42 +01:00
Aliaksandr Valialkin
b8daa8afb4 docs/VictoriaLogs/LogsQL.md: typo fixes 2025-02-22 22:35:54 +01:00
Aliaksandr Valialkin
e977d6f63a deployment/docker: update VictoriaLogs Docker image tag from v1.12.0-victorialogs to v1.13.0-victorialogs
See https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.13.0-victorialogs
2025-02-22 22:24:48 +01:00
Aliaksandr Valialkin
ce7e408c14 docs/VictoriaLogs/CHANGELOG.md: cut v1.13.0-victorialogs 2025-02-22 21:55:58 +01:00
Aliaksandr Valialkin
f1eac36a80 lib/logstorage: add contains_any and contains_all filters
- `contains_any` selects logs with fields containing at least one word/phrase from the provided list.
  The provided list can be generated by a subquery.

- `contains_all` selects logs with fields containing all the words and phrases from the provided list.
  The provided list can be generated by a subquery.
2025-02-22 21:55:58 +01:00
Aliaksandr Valialkin
4275653f03 lib/logstorage: do not spend CPU time on preparing values for already filtered out rows according to bm at filterEqField.applyToBlockSearch 2025-02-22 21:55:58 +01:00
Aliaksandr Valialkin
1ffd5e9b69 lib/logstorage: avoid extra memory allocations at getEmptyStrings() 2025-02-22 21:55:57 +01:00
Aliaksandr Valialkin
c372e10937 lib/logstorage: add an ability to drop duplicate words at unpack_words pipe 2025-02-22 21:55:57 +01:00
Aliaksandr Valialkin
7da98b540b lib/logstorage: rename unpack_tokens to unpack_words pipe
The LogsQL defines a word at https://docs.victoriametrics.com/victorialogs/logsql/#word ,
so it is more natural to use unpack_words instead of unpack_tokens name for the pipe.
2025-02-22 21:55:56 +01:00
Aliaksandr Valialkin
387d0369da lib/logstorage: optimize OR filter a bit for many inner filters
Use two operations on bitmaps per each inner filter instead of three operations.
2025-02-22 21:55:56 +01:00
Aliaksandr Valialkin
69f02f83ae lib/logstorage: use clear() for clearing bitmap bits at resetBits() instead of a loop
The clear() call is easier to read and understand than the loop.
2025-02-22 21:55:55 +01:00
Aliaksandr Valialkin
3c1d738196 lib/logstorage: avoid calling bitmap.reset() at getBitmap()
The bitmap at getBitamp() must be already reset when it was returned to the pool via putBitamp().
Thise saves CPU a bit.
2025-02-22 21:55:55 +01:00
Aliaksandr Valialkin
35e1c35281 lib/logstorage: improve error logging for improperly escaped backslashes inside quoted strings
This should simplify debugging LogsQL queries by users
2025-02-22 21:55:54 +01:00
Aliaksandr Valialkin
dfcfaba374 lib/logstorage: add field1:eq_field(field2) filter, which returns logs with identical values at field1 and field2 2025-02-22 21:55:54 +01:00
Aliaksandr Valialkin
cd5b24b377 lib/logstorage: optimize len, hash and json_array_len pipes for repeated values
Re-use the previous result instead of calculating new result for repated input values
2025-02-22 21:55:54 +01:00
Aliaksandr Valialkin
d33e24ab9b lib/logstorage: add json_array_len pipe for calculating the length of JSON arrays 2025-02-22 21:55:53 +01:00
Aliaksandr Valialkin
cd73c1bafb lib/logstorage: refactor unroll_tokens into unpack_tokens pipe
unpack_tokens pipe generates a JSON array of unpacked tokens from the source field.
This composes better with other pipes such as unroll pipe.
2025-02-22 21:55:53 +01:00
Aliaksandr Valialkin
d32c697361 lib/logstorage: add unroll_tokens pipe for unrolling individual word tokens from the log field 2025-02-22 21:55:52 +01:00
Fred Navruzov
965e3dc699 docs/vmanomaly: fix 404 in dashboard guide (#8372)
### Describe Your Changes

- Fixed 404 links to anomaly score dashboard `.json` file on Github.
- Fixed broken markdown on QuickStart page.

### Checklist

The following checks are **mandatory**:

- [x] My change adheres [VictoriaMetrics contributing
guidelines](https://docs.victoriametrics.com/contributing/).
2025-02-22 20:26:58 +02:00
Fred Navruzov
7c27538993 docs: vmanomaly - add anomaly score dashboard and guide (#8284)
### Describe Your Changes

> (currently in DRAFT) This dashboard and the docs may look differently
after recent review and the feedback received.

As requested by our customers, this PR introduces `anomaly score`-based
dashboard for `vmanomaly` to improve the visual experience and ease the
drill down debugging process for respective anomaly detection setups.
Accompanying guide (under `default` preset mode) is provided as well.

- [For additional
benefits](https://docs.victoriametrics.com/victoriametrics-datasource/#motivation),
the dashboard is based on [VictoriaMetrics
datasource](https://docs.victoriametrics.com/victoriametrics-datasource/)
rather than on `Prometheus` datasource
- Tested locally and on our https://play.victoriametrics.com/
`anomaly_score` data (tenant ID 0)
- To check the guide, build the docs locally (`vmdocs`, `make run
local`) and follow to the
http://localhost:1313/anomaly-detection/presets/#default section

### Checklist

The following checks are **mandatory**:

- [x] My change adheres [VictoriaMetrics contributing
guidelines](https://docs.victoriametrics.com/contributing/).
2025-02-22 20:09:56 +02:00
f41gh7
93001ac931 make vmui-update 2025-02-21 14:08:00 +01:00
f41gh7
7e33efdb6b docs/changelog: follow-up after 9ca74d1fff
Move vmalert BUGFIX record into current release.

Signed-off-by: f41gh7 <nik@victoriametrics.com>
2025-02-21 13:24:24 +01:00
Zakhar Bessarab
9ca74d1fff app/vmalert: properly unregister exposed metrics for alerting rules
Previously if rule group parameters were changed, alerting rules related metrics could be deleted due to bug at `utils/metrics` package. 

 This commit introduces `metrics.Set` per rule group. It holds group and alerting rules metrics. It properly unregister alerting rules metrics and addresses issue.
In addition:
- expose group metrics only once group is started - this helps to avoid
exposing metrics for groups which are created during YAML unmarshaling
and only used to update existing group.
- properly close rules which are discarded after updating existing rules
so that metrics are also correctly closed.
- detect file renames and properly recreate groups "moved" between files.

Related issue:
https://github.com/VictoriaMetrics/VictoriaMetrics/issues/8229
2025-02-21 10:36:29 +01:00
Aliaksandr Valialkin
2318e57bbe deployment/docker: update VictoriaLogs Docker image tag from v1.11.0-victorialogs to v1.12.0-victorialogs
See https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.12.0-victorialogs
2025-02-20 22:41:07 +01:00
Aliaksandr Valialkin
1ea3f72d50 lib/logstorage: simplify usage of top, uniq and unroll pipes by allowing comma-separated list of fields without parens
Examples:

   - `top 5 x, y` is equivalent to `top 5 by (x, y)`
   - `uniq foo, bar` is equivalent to `uniq by (foo, bar)`
   - `unroll foo, bar` is equivalent to `unroll (foo, bar)`
2025-02-20 22:36:09 +01:00
Aliaksandr Valialkin
384a757c3d docs/VictoriaLogs/CHANGELOG.md: cut v1.12.0-victorialogs 2025-02-20 20:49:54 +01:00
Aliaksandr Valialkin
31e88a692d lib/logstorage: properly handle _time:<=max_time filter
_time:<=max_time filter must include logs with timestamps matching max_time.
For example, _time:<=2025-02-24Z must include logs with timestamps until the end of February 24, 2025.
2025-02-20 19:15:37 +01:00
Aliaksandr Valialkin
ffbd0ebbae lib/logstorage: allow using '>', '>=', '<' and '<=' in '_time:...' filter
Examples:

  _time:>=2025-02-24Z selects logs with timestamps bigger or equal to 2025-02-24 UTC
  _time:>1d selects logs with timestamps older than one day comparing to the current time

This simplifies writing queries with _time filters.
See https://docs.victoriametrics.com/victorialogs/logsql/#time-filter
2025-02-20 19:04:51 +01:00
Aliaksandr Valialkin
dabe9ff389 docs/VictoriaLogs/keyConcepts.md: small clarifications 2025-02-20 19:04:50 +01:00
Jose Gómez-Sellés
8d389a6628 docs/cloud: fix image routes in getstarted (#8367)
It looks like the version deployed works differently than in local, and
some paths are not taken correctly. This commit fixes that by adding
absolute paths to the 2 images included in this section.

### Checklist

The following checks are **mandatory**:

- [x] My change adheres [VictoriaMetrics contributing
guidelines](https://docs.victoriametrics.com/contributing/).
2025-02-20 17:34:56 +01:00
Zakhar Bessarab
cf0604a103 docs/security: update LTS version listed in security docs (#8366)
### Describe Your Changes

Please provide a brief description of the changes you made. Be as
specific as possible to help others understand the purpose and impact of
your modifications.

### Checklist

The following checks are **mandatory**:

- [ ] My change adheres [VictoriaMetrics contributing
guidelines](https://docs.victoriametrics.com/contributing/).
2025-02-20 17:00:49 +01:00
Jose Gómez-Sellés
eabe3783c4 docs: fix vmanomaly image broken links (#8364)
It looks like when using html image insertion, the path is changed. This
commit fixes that by redirecting to the previous dir in path.

The following checks are **mandatory**:

- [x] My change adheres [VictoriaMetrics contributing
guidelines](https://docs.victoriametrics.com/contributing/).
2025-02-20 16:46:48 +01:00
Jose Gómez-Sellés
de94879515 docs/cloud: rework quickstart guide (#8363)
This guide was outdated. the intention of these changes is to:
* Ease maintainability
* Ease user experience
* Encourage the user to quickly set up the product

Some images are removed because they were obsolete or to improve
readability

### Checklist

The following checks are **mandatory**:

- [x] My change adheres [VictoriaMetrics contributing
guidelines](https://docs.victoriametrics.com/contributing/).
2025-02-20 16:27:40 +01:00
Aliaksandr Valialkin
531ecdfa0d docs/VictoriaLogs/CHANGELOG.md: move the description of the change at ca1d1bc12b to the appropriate release
Updates https://github.com/VictoriaMetrics/VictoriaMetrics/issues/8314
Updates https://github.com/VictoriaMetrics/VictoriaMetrics/pull/8315
2025-02-20 16:14:19 +01:00
Andrii Chubatiuk
dc94ccf857 docs: set proper code syntax for snippets in cluster docs (#8362)
### Describe Your Changes

apply proper syntax for code blocks

### Checklist

The following checks are **mandatory**:

- [ ] My change adheres [VictoriaMetrics contributing
guidelines](https://docs.victoriametrics.com/contributing/).
2025-02-20 14:10:50 +01:00
Roman Khavronenko
9a2213eaa8 apptest: add tests for querying utf-8 label names (#8359)
Signed-off-by: hagen1778 <roman@victoriametrics.com>
2025-02-20 11:04:01 +01:00
Roman Khavronenko
1d4713f989 metricsql: support auto-format (prettify) for expressions that use quoted metric or label names (#8358)
See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/7703

Signed-off-by: hagen1778 <roman@victoriametrics.com>
2025-02-20 10:25:55 +01:00
Roman Khavronenko
85e2307e7c apptest/vmsingle: check for right status code in return (#8355)
follow-up for
855dfb324d

### Describe Your Changes

Please provide a brief description of the changes you made. Be as
specific as possible to help others understand the purpose and impact of
your modifications.

### Checklist

The following checks are **mandatory**:

- [ ] My change adheres [VictoriaMetrics contributing
guidelines](https://docs.victoriametrics.com/contributing/).

Signed-off-by: hagen1778 <roman@victoriametrics.com>
2025-02-20 10:05:57 +01:00
Yury Molodov
086ca47884 vmui: improve Explore Cardinality page (#8351)
### Describe Your Changes

This PR includes improvements to the **Explore Cardinality** page:  

- Improve number formatting for better readability.
- Add full error message display.
- Move values representing changes relative to the previous day to a
separate column for easier sorting.
- Refine UI styles for a better visual experience.

<details>

<summary>Demo UI</summary>

| Before | After |
|--------|-------|
| <img width="1269" alt="image"
src="https://github.com/user-attachments/assets/a188c94c-3a06-498d-994d-453080c436a7"
/> | <img width="1269" alt="image"
src="https://github.com/user-attachments/assets/9a42624c-2779-457f-9c98-920fca116eeb"
/> |
| <img width="1269" alt="image"
src="https://github.com/user-attachments/assets/cb4973ba-d7a0-475b-a7a4-f649607dc7b5"
/> | <img width="1269" alt="image"
src="https://github.com/user-attachments/assets/b396516e-2468-4835-b300-88b7a82deec0"
/> |

</details>

Related issue: #8318


### Checklist

The following checks are **mandatory**:

- [ ] My change adheres [VictoriaMetrics contributing
guidelines](https://docs.victoriametrics.com/contributing/).

---------

Signed-off-by: hagen1778 <roman@victoriametrics.com>
Co-authored-by: hagen1778 <roman@victoriametrics.com>
2025-02-20 09:51:49 +01:00
hagen1778
b5c21337d3 docs: fix typos in recent change lines
Signed-off-by: hagen1778 <roman@victoriametrics.com>
2025-02-20 09:14:15 +01:00
f41gh7
855dfb324d app/vmselect: add query resource limits priority
This commit adds support for overriding vmstorage `maxUniqueTimeseries` with specific
resource limits:
1. `-search.maxLabelsAPISeries` for
[/api/v1/labels](https://docs.victoriametrics.com/url-examples/#apiv1labels),
[/api/v1/label/.../values](https://docs.victoriametrics.com/url-examples/#apiv1labelvalues)
2. `-search. maxSeries` for
[/api/v1/series](https://docs.victoriametrics.com/url-examples/#apiv1series)
3. `-search.maxTSDBStatusSeries` for
[/api/v1/status/tsdb](https://docs.victoriametrics.com/#tsdb-stats)
4. `-search.maxDeleteSeries` for
[/api/v1/admin/tsdb/delete_series](https://docs.victoriametrics.com/url-examples/#apiv1admintsdbdelete_series)

Currently, this limit priority logic cannot be applied to flags
`-search.maxFederateSeries` and `-search.maxExportSeries`, because they
share the same RPC `search_v7` with the /api/v1/query and
/api/v1/query_range APIs, preventing vmstorage from identifying the
actual API of the request. To address that, we need to add additional
information to the protocol between vmstorage and vmselect, which should
be introduced in the future when possible.

Related issue:
https://github.com/VictoriaMetrics/VictoriaMetrics/issues/7857
2025-02-19 18:18:32 +01:00
Yury Molodov
a5ca5d370a vmui: add legend customization options
Legend settings have been added to **Graph Settings**.  

#### **New Features:**  
1. **Table View** – Toggle to display the legend in a table format.  
2. **Hide Common Values** – Option to hide fields with identical values
across all series.
3. **Hide Min/Medium/Max** – Ability to hide min, median, and max values
from the legend.
4. **Custom Label Format** – Set a custom format for series labels
(applies only to the legend).
5. **Group by Label** – Group legend entries based on a selected label.


Related Issue:
https://github.com/VictoriaMetrics/VictoriaMetrics/issues/8031
2025-02-19 18:09:14 +01:00
Gunju Kim
10d5e979a6 app/vmalert: add command line flag -notifier.sendTimeout
Currently, vmalert uses a fixed 10-second client timeout for notifiers,
which can prevent large sets of alerts from being sent successfully.

This introduces `-notifier.sendTimeout` flag to vmalert to control the
client timeout duration for the notifiers.


Related issue:
https://github.com/VictoriaMetrics/VictoriaMetrics/issues/8287
2025-02-19 18:04:31 +01:00
Andrii Chubatiuk
58a80a9718 app/vlinsert/syslog: properly parse log line with characters escaped by rfc5424
Inside PARAM-VALUE, the characters '"' (ABNF %d34), '\' (ABNF %d92),
and ']' (ABNF %d93) MUST be escaped.  This is necessary to avoid
parsing errors.  Escaping ']' would not strictly be necessary but is
REQUIRED by this specification to avoid syslog application
implementation errors.  Each of these three characters MUST be
escaped as '\"', '\\', and '\]' respectively.  The backslash is used
for control character escaping for consistency with its use for
escaping in other parts of the syslog message as well as in traditional syslog.

 Related RFC:
https://datatracker.ietf.org/doc/html/rfc5424#section-6.3.3

Related issue:
https://github.com/VictoriaMetrics/VictoriaMetrics/issues/8282
2025-02-19 18:01:54 +01:00
Zakhar Bessarab
f4b7e2e811 app/vmselect/filtersdebug: properly validate retention configuration
This commit properly validate retention flag value input for the enterprise debug UI.
API properly checks:
- presence of global retention configuration
- validate that retention is at least 1 day, same as vmstorage enforcement

Related issue:
 https://github.com/VictoriaMetrics/VictoriaMetrics/issues/8343

---------
Signed-off-by: Zakhar Bessarab <z.bessarab@victoriametrics.com>
2025-02-19 17:52:22 +01:00
Andrii Chubatiuk
e97c530fdc lib/protoparser/influx: add -influx.forceStreamMode flag to force parsing all Influx data in stream mode (#8319)
Addresses #8269

Signed-off-by: hagen1778 <roman@victoriametrics.com>
Co-authored-by: hagen1778 <roman@victoriametrics.com>
2025-02-19 17:05:02 +01:00
Roman Khavronenko
439af7927f docs: update histogram recommendation for stream aggr (#8349)
Use `rate_sum` instead of `total` output for the following reasons:
* `rate_sum` is far less sensitive for data delays than `total`, since
it represents the speed of change instead of absolute values.
* `rate_sum` remove need in using `rate` function for calculating final
results on query time.

### Describe Your Changes

Please provide a brief description of the changes you made. Be as
specific as possible to help others understand the purpose and impact of
your modifications.

### Checklist

The following checks are **mandatory**:

- [ ] My change adheres [VictoriaMetrics contributing
guidelines](https://docs.victoriametrics.com/contributing/).

Signed-off-by: hagen1778 <roman@victoriametrics.com>
2025-02-19 15:12:55 +01:00
hagen1778
ab1b1cd6eb deployment/docker: use default network for single-node installation
There is no point of using custom network as all components reside
in the same network anyway.

Signed-off-by: hagen1778 <roman@victoriametrics.com>
2025-02-19 14:03:28 +01:00
hagen1778
9049731766 docs: re-order items in VictoriaMetrics navigation tab
* make QuickStart first, similarly to vlogs docs
* push release guide to bottom, as it is not for users

Signed-off-by: hagen1778 <roman@victoriametrics.com>
2025-02-19 13:59:09 +01:00
Aliaksandr Valialkin
8d79b28b37 deployment: update VictoriaLogs Docker image tag from v1.10.1-victorialogs to v1.11.0-victorialogs
See https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.11.0-victorialogs
2025-02-19 13:34:11 +01:00
hagen1778
f927b08298 docs: clarify changelog line
Signed-off-by: hagen1778 <roman@victoriametrics.com>
2025-02-19 13:27:22 +01:00
hagen1778
1dac6fcdc4 docs: rm duplicated changelog line
Signed-off-by: hagen1778 <roman@victoriametrics.com>
2025-02-19 13:22:53 +01:00
hagen1778
3afd956041 docs: fix another markdown typo
Signed-off-by: hagen1778 <roman@victoriametrics.com>
2025-02-19 13:22:15 +01:00
hagen1778
c77306616e docs: fix markdown typo
Signed-off-by: hagen1778 <roman@victoriametrics.com>
2025-02-19 13:21:42 +01:00
Zakhar Bessarab
eccef5f6ec docs/guides: remove unneeded IDs for security context (#8344)
### Describe Your Changes

Based on feedback at:
e4240e61c6 (r1961497609)

Thanks to @vrutkovs for the feedback.

### Checklist

The following checks are **mandatory**:

- [x] My change adheres [VictoriaMetrics contributing
guidelines](https://docs.victoriametrics.com/contributing/).

Signed-off-by: Zakhar Bessarab <z.bessarab@victoriametrics.com>
2025-02-19 13:20:16 +01:00
Andrii Chubatiuk
c8fc903669 lib/streamaggr: added aggregation windows (#6314)
### Describe Your Changes

By default, stream aggregation and deduplication stores a single state
per each aggregation output result.
The data for each aggregator is flushed independently once per
aggregation interval. But there's no guarantee that
incoming samples with timestamps close to the aggregation interval's end
will get into it. For example, when aggregating
with `interval: 1m` a data sample with timestamp 1739473078 (18:57:59)
can fall into aggregation round `18:58:00` or `18:59:00`.
It depends on network lag, load, clock synchronization, etc. In most
scenarios it doesn't impact aggregation or
deduplication results, which are consistent within margin of error. But
for metrics represented as a collection of series,
like
[histograms](https://docs.victoriametrics.com/keyconcepts/#histogram),
such inaccuracy leads to invalid aggregation results.

For this case, streaming aggregation and deduplication support mode with
aggregation windows for current and previous state. With this mode,
flush doesn't happen immediately but is shifted by a calculated samples
lag that improves correctness for delayed data.

Enabling of this mode has increased resource usage: memory usage is
expected to double as aggregation will store two states
instead of one. However, this significantly improves accuracy of
calculations. Aggregation windows can be enabled via
the following settings:

- `-streamAggr.enableWindows` at [single-node
VictoriaMetrics](https://docs.victoriametrics.com/single-server-victoriametrics/)
and [vmagent](https://docs.victoriametrics.com/vmagent/). At
[vmagent](https://docs.victoriametrics.com/vmagent/)
`-remoteWrite.streamAggr.enableWindows` flag can be specified
individually per each `-remoteWrite.url`.
If one of these flags is set, then all aggregators will be using fixed
windows. In conjunction with `-remoteWrite.streamAggr.dedupInterval` or
`-streamAggr.dedupInterval` fixed aggregation windows are enabled on
deduplicator as well.
- `enable_windows` option in [aggregation
config](https://docs.victoriametrics.com/stream-aggregation/#stream-aggregation-config).
  It allows enabling aggregation windows for a specific aggregator.



### Checklist

The following checks are **mandatory**:

- [ ] My change adheres [VictoriaMetrics contributing
guidelines](https://docs.victoriametrics.com/contributing/).

---------

Signed-off-by: hagen1778 <roman@victoriametrics.com>
Co-authored-by: hagen1778 <roman@victoriametrics.com>
2025-02-19 13:19:33 +01:00
hagen1778
38bded4e58 lib/logstorage: adjust expected compression ratio in tests
A follow-up after 9bb5ba5d2f
that impacted compression ratio for data compressed with native GO zstd lib (`make test-pure`).

Signed-off-by: hagen1778 <roman@victoriametrics.com>
2025-02-19 13:11:37 +01:00
328 changed files with 20077 additions and 6386 deletions

View File

@@ -567,7 +567,7 @@ golangci-lint: install-golangci-lint
golangci-lint run
install-golangci-lint:
which golangci-lint || curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(shell go env GOPATH)/bin v1.64.4
which golangci-lint || curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(shell go env GOPATH)/bin v1.64.5
remove-golangci-lint:
rm -rf `which golangci-lint`

View File

@@ -3,7 +3,7 @@
![Latest Release](https://img.shields.io/github/v/release/VictoriaMetrics/VictoriaMetrics?sort=semver&label=&filter=!*-victorialogs&logo=github&labelColor=gray&color=gray&link=https%3A%2F%2Fgithub.com%2FVictoriaMetrics%2FVictoriaMetrics%2Freleases%2Flatest)
![Docker Pulls](https://img.shields.io/docker/pulls/victoriametrics/victoria-metrics?label=&logo=docker&logoColor=white&labelColor=2496ED&color=2496ED&link=https%3A%2F%2Fhub.docker.com%2Fr%2Fvictoriametrics%2Fvictoria-metrics)
![Go Report](https://goreportcard.com/badge/github.com/VictoriaMetrics/VictoriaMetrics?link=https%3A%2F%2Fgoreportcard.com%2Freport%2Fgithub.com%2FVictoriaMetrics%2FVictoriaMetrics)
![Build Status](https://github.com/VictoriaMetrics/VictoriaMetrics/workflows/main/badge.svg?link=https%3A%2F%2Fgithub.com%2FVictoriaMetrics%2FVictoriaMetrics%2Factions)
![Build Status](https://github.com/VictoriaMetrics/VictoriaMetrics/actions/workflows/main.yml/badge.svg?branch=master&link=https%3A%2F%2Fgithub.com%2FVictoriaMetrics%2FVictoriaMetrics%2Factions)
![codecov](https://codecov.io/gh/VictoriaMetrics/VictoriaMetrics/branch/master/graph/badge.svg?link=https%3A%2F%2Fcodecov.io%2Fgh%2FVictoriaMetrics%2FVictoriaMetrics)
![License](https://img.shields.io/github/license/VictoriaMetrics/VictoriaMetrics?labelColor=green&label=&link=https%3A%2F%2Fgithub.com%2FVictoriaMetrics%2FVictoriaMetrics%2Fblob%2Fmaster%2FLICENSE)
![Slack](https://img.shields.io/badge/Join-4A154B?logo=slack&link=https%3A%2F%2Fslack.victoriametrics.com)
@@ -22,7 +22,7 @@ Here are some resources and information about VictoriaMetrics:
- Documentation: [docs.victoriametrics.com](https://docs.victoriametrics.com)
- Case studies: [Grammarly, Roblox, Wix,...](https://docs.victoriametrics.com/casestudies/).
- Available: [Binary releases](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/latest), [Docker images](https://hub.docker.com/r/victoriametrics/victoria-metrics/), [Source code](https://github.com/VictoriaMetrics/VictoriaMetrics)
- Available: [Binary releases](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/latest), docker images [Docker Hub](https://hub.docker.com/r/victoriametrics/victoria-metrics/) and [Quay](https://quay.io/repository/victoriametrics/victoria-metrics), [Source code](https://github.com/VictoriaMetrics/VictoriaMetrics)
- Deployment types: [Single-node version](https://docs.victoriametrics.com/), [Cluster version](https://docs.victoriametrics.com/cluster-victoriametrics/), and [Enterprise version](https://docs.victoriametrics.com/enterprise/)
- Changelog: [CHANGELOG](https://docs.victoriametrics.com/changelog/), and [How to upgrade](https://docs.victoriametrics.com/#how-to-upgrade-victoriametrics)
- Community: [Slack](https://slack.victoriametrics.com/), [X (Twitter)](https://x.com/VictoriaMetrics), [LinkedIn](https://www.linkedin.com/company/victoriametrics/), [YouTube](https://www.youtube.com/@VictoriaMetrics)

View File

@@ -8,7 +8,7 @@ The following versions of VictoriaMetrics receive regular security fixes:
|---------|--------------------|
| [latest release](https://docs.victoriametrics.com/changelog/) | :white_check_mark: |
| v1.102.x [LTS line](https://docs.victoriametrics.com/lts-releases/) | :white_check_mark: |
| v1.97.x [LTS line](https://docs.victoriametrics.com/lts-releases/) | :white_check_mark: |
| v1.110.x [LTS line](https://docs.victoriametrics.com/lts-releases/) | :white_check_mark: |
| other releases | :x: |
See [this page](https://victoriametrics.com/security/) for more details.

View File

@@ -60,7 +60,7 @@ func RequestHandler(path string, w http.ResponseWriter, r *http.Request) bool {
return true
}
switch path {
case "/":
case "/", "":
switch r.Method {
case http.MethodGet:
// Return fake response for Elasticsearch ping request.

View File

@@ -46,7 +46,9 @@ func RequestHandler(w http.ResponseWriter, r *http.Request) bool {
return true
}
switch {
case strings.HasPrefix(path, "/elasticsearch/"):
case strings.HasPrefix(path, "/elasticsearch"):
// some clients may omit trailing slash
// see https://github.com/VictoriaMetrics/VictoriaMetrics/issues/8353
path = strings.TrimPrefix(path, "/elasticsearch")
return elasticsearch.RequestHandler(path, w, r)
case strings.HasPrefix(path, "/loki/"):

View File

@@ -18,7 +18,7 @@ import (
"github.com/VictoriaMetrics/VictoriaMetrics/lib/buildinfo"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/envflag"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promutils"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/timeutil"
)
var (
@@ -306,7 +306,7 @@ type timeFlag struct {
}
func (tf *timeFlag) Set(s string) error {
msec, err := promutils.ParseTimeMsec(s)
msec, err := timeutil.ParseTimeMsec(s)
if err != nil {
return fmt.Errorf("cannot parse time from %q: %w", s, err)
}

View File

@@ -22,7 +22,7 @@ import (
"github.com/VictoriaMetrics/VictoriaMetrics/lib/httputils"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logstorage"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promutils"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/timeutil"
)
// ProcessFacetsRequest handles /select/logsql/facets request.
@@ -116,7 +116,7 @@ func ProcessHitsRequest(ctx context.Context, w http.ResponseWriter, r *http.Requ
if stepStr == "" {
stepStr = "1d"
}
step, err := promutils.ParseDuration(stepStr)
step, err := timeutil.ParseDuration(stepStr)
if err != nil {
httpserver.Errorf(w, r, "cannot parse 'step' arg: %s", err)
return
@@ -131,7 +131,7 @@ func ProcessHitsRequest(ctx context.Context, w http.ResponseWriter, r *http.Requ
if offsetStr == "" {
offsetStr = "0s"
}
offset, err := promutils.ParseDuration(offsetStr)
offset, err := timeutil.ParseDuration(offsetStr)
if err != nil {
httpserver.Errorf(w, r, "cannot parse 'offset' arg: %s", err)
return
@@ -665,7 +665,7 @@ func ProcessStatsQueryRangeRequest(ctx context.Context, w http.ResponseWriter, r
if stepStr == "" {
stepStr = "1d"
}
step, err := promutils.ParseDuration(stepStr)
step, err := timeutil.ParseDuration(stepStr)
if err != nil {
err = fmt.Errorf("cannot parse 'step' arg: %s", err)
httpserver.SendPrometheusError(w, r, err)
@@ -688,19 +688,22 @@ func ProcessStatsQueryRangeRequest(ctx context.Context, w http.ResponseWriter, r
m := make(map[string]*statsSeries)
var mLock sync.Mutex
timestamp := q.GetTimestamp()
writeBlock := func(_ uint, timestamps []int64, columns []logstorage.BlockColumn) {
clonedColumnNames := make([]string, len(columns))
for i, c := range columns {
clonedColumnNames[i] = strings.Clone(c.Name)
}
for i := range timestamps {
// Do not move q.GetTimestamp() outside writeBlock, since ts
// must be initialized to query timestamp for every processed log row.
// See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/8312
ts := q.GetTimestamp()
labels := make([]logstorage.Field, 0, len(byFields))
for j, c := range columns {
if c.Name == "_time" {
nsec, ok := logstorage.TryParseTimestampRFC3339Nano(c.Values[i])
if ok {
timestamp = nsec
ts = nsec
continue
}
}
@@ -721,7 +724,7 @@ func ProcessStatsQueryRangeRequest(ctx context.Context, w http.ResponseWriter, r
dst = logstorage.MarshalFieldsToJSON(dst, labels)
key := string(dst)
p := statsPoint{
Timestamp: timestamp,
Timestamp: ts,
Value: strings.Clone(c.Values[i]),
}
@@ -1119,7 +1122,7 @@ func getTimeNsec(r *http.Request, argName string) (int64, bool, error) {
return 0, false, nil
}
currentTimestamp := time.Now().UnixNano()
nsecs, err := promutils.ParseTimeAt(s, currentTimestamp)
nsecs, err := timeutil.ParseTimeAt(s, currentTimestamp)
if err != nil {
return 0, false, fmt.Errorf("cannot parse %s=%s: %w", argName, s, err)
}

View File

@@ -176,50 +176,62 @@ func RequestHandler(w http.ResponseWriter, r *http.Request) bool {
func processSelectRequest(ctx context.Context, w http.ResponseWriter, r *http.Request, path string) bool {
httpserver.EnableCORS(w, r)
startTime := time.Now()
switch path {
case "/select/logsql/facets":
logsqlFacetsRequests.Inc()
logsql.ProcessFacetsRequest(ctx, w, r)
logsqlFacetsDuration.UpdateDuration(startTime)
return true
case "/select/logsql/field_names":
logsqlFieldNamesRequests.Inc()
logsql.ProcessFieldNamesRequest(ctx, w, r)
logsqlFieldNamesDuration.UpdateDuration(startTime)
return true
case "/select/logsql/field_values":
logsqlFieldValuesRequests.Inc()
logsql.ProcessFieldValuesRequest(ctx, w, r)
logsqlFieldValuesDuration.UpdateDuration(startTime)
return true
case "/select/logsql/hits":
logsqlHitsRequests.Inc()
logsql.ProcessHitsRequest(ctx, w, r)
logsqlHitsDuration.UpdateDuration(startTime)
return true
case "/select/logsql/query":
logsqlQueryRequests.Inc()
logsql.ProcessQueryRequest(ctx, w, r)
logsqlQueryDuration.UpdateDuration(startTime)
return true
case "/select/logsql/stats_query":
logsqlStatsQueryRequests.Inc()
logsql.ProcessStatsQueryRequest(ctx, w, r)
logsqlStatsQueryDuration.UpdateDuration(startTime)
return true
case "/select/logsql/stats_query_range":
logsqlStatsQueryRangeRequests.Inc()
logsql.ProcessStatsQueryRangeRequest(ctx, w, r)
logsqlStatsQueryRangeDuration.UpdateDuration(startTime)
return true
case "/select/logsql/stream_field_names":
logsqlStreamFieldNamesRequests.Inc()
logsql.ProcessStreamFieldNamesRequest(ctx, w, r)
logsqlStreamFieldNamesDuration.UpdateDuration(startTime)
return true
case "/select/logsql/stream_field_values":
logsqlStreamFieldValuesRequests.Inc()
logsql.ProcessStreamFieldValuesRequest(ctx, w, r)
logsqlStreamFieldValuesDuration.UpdateDuration(startTime)
return true
case "/select/logsql/stream_ids":
logsqlStreamIDsRequests.Inc()
logsql.ProcessStreamIDsRequest(ctx, w, r)
logsqlStreamIDsDuration.UpdateDuration(startTime)
return true
case "/select/logsql/streams":
logsqlStreamsRequests.Inc()
logsql.ProcessStreamsRequest(ctx, w, r)
logsqlStreamsDuration.UpdateDuration(startTime)
return true
default:
return false
@@ -240,16 +252,39 @@ func getMaxQueryDuration(r *http.Request) time.Duration {
}
var (
logsqlFacetsRequests = metrics.NewCounter(`vl_http_requests_total{path="/select/logsql/facets"}`)
logsqlFieldNamesRequests = metrics.NewCounter(`vl_http_requests_total{path="/select/logsql/field_names"}`)
logsqlFieldValuesRequests = metrics.NewCounter(`vl_http_requests_total{path="/select/logsql/field_values"}`)
logsqlHitsRequests = metrics.NewCounter(`vl_http_requests_total{path="/select/logsql/hits"}`)
logsqlQueryRequests = metrics.NewCounter(`vl_http_requests_total{path="/select/logsql/query"}`)
logsqlStatsQueryRequests = metrics.NewCounter(`vl_http_requests_total{path="/select/logsql/stats_query"}`)
logsqlStatsQueryRangeRequests = metrics.NewCounter(`vl_http_requests_total{path="/select/logsql/stats_query_range"}`)
logsqlStreamFieldNamesRequests = metrics.NewCounter(`vl_http_requests_total{path="/select/logsql/stream_field_names"}`)
logsqlFacetsRequests = metrics.NewCounter(`vl_http_requests_total{path="/select/logsql/facets"}`)
logsqlFacetsDuration = metrics.NewSummary(`vl_http_request_duration_seconds{path="/select/logsql/facets"}`)
logsqlFieldNamesRequests = metrics.NewCounter(`vl_http_requests_total{path="/select/logsql/field_names"}`)
logsqlFieldNamesDuration = metrics.NewSummary(`vl_http_request_duration_seconds{path="/select/logsql/field_names"}`)
logsqlFieldValuesRequests = metrics.NewCounter(`vl_http_requests_total{path="/select/logsql/field_values"}`)
logsqlFieldValuesDuration = metrics.NewSummary(`vl_http_request_duration_seconds{path="/select/logsql/field_values"}`)
logsqlHitsRequests = metrics.NewCounter(`vl_http_requests_total{path="/select/logsql/hits"}`)
logsqlHitsDuration = metrics.NewSummary(`vl_http_request_duration_seconds{path="/select/logsql/hits"}`)
logsqlQueryRequests = metrics.NewCounter(`vl_http_requests_total{path="/select/logsql/query"}`)
logsqlQueryDuration = metrics.NewSummary(`vl_http_request_duration_seconds{path="/select/logsql/query"}`)
logsqlStatsQueryRequests = metrics.NewCounter(`vl_http_requests_total{path="/select/logsql/stats_query"}`)
logsqlStatsQueryDuration = metrics.NewSummary(`vl_http_request_duration_seconds{path="/select/logsql/stats_query"}`)
logsqlStatsQueryRangeRequests = metrics.NewCounter(`vl_http_requests_total{path="/select/logsql/stats_query_range"}`)
logsqlStatsQueryRangeDuration = metrics.NewSummary(`vl_http_request_duration_seconds{path="/select/logsql/stats_query_range"}`)
logsqlStreamFieldNamesRequests = metrics.NewCounter(`vl_http_requests_total{path="/select/logsql/stream_field_names"}`)
logsqlStreamFieldNamesDuration = metrics.NewSummary(`vl_http_request_duration_seconds{path="/select/logsql/stream_field_names"}`)
logsqlStreamFieldValuesRequests = metrics.NewCounter(`vl_http_requests_total{path="/select/logsql/stream_field_values"}`)
logsqlStreamIDsRequests = metrics.NewCounter(`vl_http_requests_total{path="/select/logsql/stream_ids"}`)
logsqlStreamsRequests = metrics.NewCounter(`vl_http_requests_total{path="/select/logsql/streams"}`)
logsqlTailRequests = metrics.NewCounter(`vl_http_requests_total{path="/select/logsql/tail"}`)
logsqlStreamFieldValuesDuration = metrics.NewSummary(`vl_http_request_duration_seconds{path="/select/logsql/stream_field_values"}`)
logsqlStreamIDsRequests = metrics.NewCounter(`vl_http_requests_total{path="/select/logsql/stream_ids"}`)
logsqlStreamIDsDuration = metrics.NewSummary(`vl_http_request_duration_seconds{path="/select/logsql/stream_ids"}`)
logsqlStreamsRequests = metrics.NewCounter(`vl_http_requests_total{path="/select/logsql/streams"}`)
logsqlStreamsDuration = metrics.NewSummary(`vl_http_request_duration_seconds{path="/select/logsql/streams"}`)
// no need to track duration for tail requests, as they usually take long time
logsqlTailRequests = metrics.NewCounter(`vl_http_requests_total{path="/select/logsql/tail"}`)
)

View File

@@ -9,6 +9,7 @@ import (
"github.com/VictoriaMetrics/VictoriaMetrics/lib/prompbmarshal"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promrelabel"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/protoparser/prometheus"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/streamaggr"
"github.com/VictoriaMetrics/metrics"
)
@@ -53,7 +54,7 @@ func TestGetLabelsHash_Distribution(t *testing.T) {
}
func TestRemoteWriteContext_TryPush_ImmutableTimeseries(t *testing.T) {
f := func(streamAggrConfig, relabelConfig string, dedupInterval time.Duration, keepInput, dropInput bool, input string) {
f := func(streamAggrConfig, relabelConfig string, enableWindows bool, dedupInterval time.Duration, keepInput, dropInput bool, input string) {
t.Helper()
perURLRelabel, err := promrelabel.ParseRelabelConfigsData([]byte(relabelConfig))
if err != nil {
@@ -77,12 +78,15 @@ func TestRemoteWriteContext_TryPush_ImmutableTimeseries(t *testing.T) {
rowsDroppedByRelabel: metrics.GetOrCreateCounter(`bar`),
}
if dedupInterval > 0 {
rwctx.deduplicator = streamaggr.NewDeduplicator(nil, dedupInterval, nil, "dedup-global")
rwctx.deduplicator = streamaggr.NewDeduplicator(nil, enableWindows, dedupInterval, nil, "dedup-global")
}
if streamAggrConfig != "" {
pushNoop := func(_ []prompbmarshal.TimeSeries) {}
sas, err := streamaggr.LoadFromData([]byte(streamAggrConfig), pushNoop, nil, "global")
opts := streamaggr.Options{
EnableWindows: enableWindows,
}
sas, err := streamaggr.LoadFromData([]byte(streamAggrConfig), pushNoop, &opts, "global")
if err != nil {
t.Fatalf("cannot load streamaggr configs: %s", err)
}
@@ -91,7 +95,7 @@ func TestRemoteWriteContext_TryPush_ImmutableTimeseries(t *testing.T) {
}
offsetMsecs := time.Now().UnixMilli()
inputTss := prompbmarshal.MustParsePromMetrics(input, offsetMsecs)
inputTss := prometheus.MustParsePromMetrics(input, offsetMsecs)
expectedTss := make([]prompbmarshal.TimeSeries, len(inputTss))
// copy inputTss to make sure it is not mutated during TryPush call
@@ -114,13 +118,13 @@ func TestRemoteWriteContext_TryPush_ImmutableTimeseries(t *testing.T) {
- action: keep
source_labels: [env]
regex: "dev"
`, 0, false, false, `
`, false, 0, false, false, `
metric{env="dev"} 10
metric{env="bar"} 20
metric{env="dev"} 15
metric{env="bar"} 25
`)
f(``, ``, time.Hour, false, false, `
f(``, ``, true, time.Hour, false, false, `
metric{env="dev"} 10
metric{env="foo"} 20
metric{env="dev"} 15
@@ -130,7 +134,7 @@ metric{env="foo"} 25
- action: keep
source_labels: [env]
regex: "dev"
`, time.Hour, false, false, `
`, true, time.Hour, false, false, `
metric{env="dev"} 10
metric{env="bar"} 20
metric{env="dev"} 15
@@ -140,7 +144,7 @@ metric{env="bar"} 25
- action: keep
source_labels: [env]
regex: "dev"
`, time.Hour, true, false, `
`, true, time.Hour, true, false, `
metric{env="test"} 10
metric{env="dev"} 20
metric{env="foo"} 15
@@ -150,7 +154,7 @@ metric{env="dev"} 25
- action: keep
source_labels: [env]
regex: "dev"
`, time.Hour, false, true, `
`, true, time.Hour, false, true, `
metric{env="foo"} 10
metric{env="dev"} 20
metric{env="foo"} 15
@@ -160,7 +164,7 @@ metric{env="dev"} 25
- action: keep
source_labels: [env]
regex: "dev"
`, time.Hour, true, true, `
`, true, time.Hour, true, true, `
metric{env="dev"} 10
metric{env="test"} 20
metric{env="dev"} 15

View File

@@ -35,6 +35,9 @@ var (
"clients pushing data into the vmagent. See https://docs.victoriametrics.com/stream-aggregation/#ignore-aggregation-intervals-on-start")
streamAggrGlobalDropInputLabels = flagutil.NewArrayString("streamAggr.dropInputLabels", "An optional list of labels to drop from samples for aggregator "+
"before stream de-duplication and aggregation . See https://docs.victoriametrics.com/stream-aggregation/#dropping-unneeded-labels")
streamAggrGlobalEnableWindows = flag.Bool("streamAggr.enableWindows", false, "Enables aggregation within fixed windows for all global aggregators. "+
"This allows to get more precise results, but impacts resource usage as it requires twice more memory to store two states. "+
"See https://docs.victoriametrics.com/stream-aggregation/#aggregation-windows.")
// Per URL config
streamAggrConfig = flagutil.NewArrayString("remoteWrite.streamAggr.config", "Optional path to file with stream aggregation config for the corresponding -remoteWrite.url. "+
@@ -59,6 +62,9 @@ var (
"before stream de-duplication and aggregation with -remoteWrite.streamAggr.config and -remoteWrite.streamAggr.dedupInterval at the corresponding -remoteWrite.url. "+
"Multiple labels per remoteWrite.url must be delimited by '^^': -remoteWrite.streamAggr.dropInputLabels='replica^^az,replica'. "+
"See https://docs.victoriametrics.com/stream-aggregation/#dropping-unneeded-labels")
streamAggrEnableWindows = flagutil.NewArrayBool("remoteWrite.streamAggr.enableWindows", "Enables aggregation within fixed windows for all remote write's aggregators. "+
"This allows to get more precise results, but impacts resource usage as it requires twice more memory to store two states. "+
"See https://docs.victoriametrics.com/stream-aggregation/#aggregation-windows.")
)
// CheckStreamAggrConfigs checks -remoteWrite.streamAggr.config and -streamAggr.config.
@@ -135,7 +141,7 @@ func initStreamAggrConfigGlobal() {
}
dedupInterval := *streamAggrGlobalDedupInterval
if dedupInterval > 0 {
deduplicatorGlobal = streamaggr.NewDeduplicator(pushToRemoteStoragesTrackDropped, dedupInterval, *streamAggrGlobalDropInputLabels, "dedup-global")
deduplicatorGlobal = streamaggr.NewDeduplicator(pushToRemoteStoragesTrackDropped, *streamAggrGlobalEnableWindows, dedupInterval, *streamAggrGlobalDropInputLabels, "dedup-global")
}
}
@@ -161,7 +167,7 @@ func (rwctx *remoteWriteCtx) initStreamAggrConfig() {
if streamAggrDropInputLabels.GetOptionalArg(idx) != "" {
dropLabels = strings.Split(streamAggrDropInputLabels.GetOptionalArg(idx), "^^")
}
rwctx.deduplicator = streamaggr.NewDeduplicator(rwctx.pushInternalTrackDropped, dedupInterval, dropLabels, alias)
rwctx.deduplicator = streamaggr.NewDeduplicator(rwctx.pushInternalTrackDropped, *streamAggrGlobalEnableWindows, dedupInterval, dropLabels, alias)
}
}
@@ -207,6 +213,7 @@ func newStreamAggrConfigGlobal() (*streamaggr.Aggregators, error) {
IgnoreOldSamples: *streamAggrGlobalIgnoreOldSamples,
IgnoreFirstIntervals: *streamAggrGlobalIgnoreFirstIntervals,
KeepInput: *streamAggrGlobalKeepInput,
EnableWindows: *streamAggrGlobalEnableWindows,
}
sas, err := streamaggr.LoadFromFile(path, pushToRemoteStoragesTrackDropped, opts, "global")
@@ -240,6 +247,7 @@ func newStreamAggrConfigPerURL(idx int, pushFunc streamaggr.PushFunc) (*streamag
IgnoreOldSamples: streamAggrIgnoreOldSamples.GetOptionalArg(idx),
IgnoreFirstIntervals: streamAggrIgnoreFirstIntervals.GetOptionalArg(idx),
KeepInput: streamAggrKeepInput.GetOptionalArg(idx),
EnableWindows: streamAggrEnableWindows.GetOptionalArg(idx),
}
sas, err := streamaggr.LoadFromFile(path, pushFunc, opts, alias)

View File

@@ -51,6 +51,11 @@ Examples:
Usage: `Optional external URL to template in rule's labels or annotations.`,
Required: false,
},
&cli.StringFlag{
Name: "httpListenPort",
Usage: `Optional local port for incoming HTTP requests. If not specified, a random unoccupied port will be used.`,
Required: false,
},
&cli.StringFlag{
Name: "loggerLevel",
Usage: `Minimum level of errors to log. Possible values: INFO, WARN, ERROR, FATAL, PANIC (default "ERROR").`,
@@ -58,7 +63,7 @@ Examples:
},
},
Action: func(c *cli.Context) error {
if failed := unittest.UnitTest(c.StringSlice("files"), c.Bool("disableAlertgroupLabel"), c.StringSlice("external.label"), c.String("external.url"), c.String("loggerLevel")); failed {
if failed := unittest.UnitTest(c.StringSlice("files"), c.Bool("disableAlertgroupLabel"), c.StringSlice("external.label"), c.String("external.url"), c.String("httpListenPort"), c.String("loggerLevel")); failed {
return fmt.Errorf("unittest failed")
}
return nil

View File

@@ -4,13 +4,18 @@ import (
"context"
"flag"
"fmt"
"net"
"net/http"
"net/http/httptest"
"net/url"
"os"
"os/signal"
"path/filepath"
"reflect"
"sort"
"strconv"
"strings"
"syscall"
"time"
"gopkg.in/yaml.v2"
@@ -28,7 +33,6 @@ import (
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmselect/prometheus"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmselect/promql"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmstorage"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/flagutil"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/fs"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/httpserver"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
@@ -38,15 +42,9 @@ import (
var (
storagePath string
httpListenAddr = ":8880"
httpListenAddr string
// insert series from 1970-01-01T00:00:00
testStartTime = time.Unix(0, 0).UTC()
testPromWriteHTTPPath = "http://127.0.0.1" + httpListenAddr + "/api/v1/write"
testDataSourcePath = "http://127.0.0.1" + httpListenAddr + "/prometheus"
testRemoteWritePath = "http://127.0.0.1" + httpListenAddr
testHealthHTTPPath = "http://127.0.0.1" + httpListenAddr + "/health"
testStartTime = time.Unix(0, 0).UTC()
testLogLevel = "ERROR"
disableAlertgroupLabel bool
)
@@ -56,7 +54,7 @@ const (
)
// UnitTest runs unittest for files
func UnitTest(files []string, disableGroupLabel bool, externalLabels []string, externalURL, logLevel string) bool {
func UnitTest(files []string, disableGroupLabel bool, externalLabels []string, externalURL, httpListenPort, logLevel string) bool {
if logLevel != "" {
testLogLevel = logLevel
}
@@ -67,7 +65,42 @@ func UnitTest(files []string, disableGroupLabel bool, externalLabels []string, e
if err := templates.Load([]string{}, *eu); err != nil {
logger.Fatalf("failed to load template: %v", err)
}
storagePath = filepath.Join(os.TempDir(), testStoragePath)
// set up http server
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/prometheus/api/v1/query":
if err := prometheus.QueryHandler(nil, time.Now(), w, r); err != nil {
httpserver.Errorf(w, r, "%s", err)
}
case "/prometheus/api/v1/write", "/api/v1/write":
if err := promremotewrite.InsertHandler(r); err != nil {
httpserver.Errorf(w, r, "%s", err)
}
default:
}
})
if httpListenPort == "" {
server := httptest.NewServer(handler)
httpListenAddr = strings.Split(server.URL, ":")[2]
defer server.Close()
} else {
httpListenAddr = httpListenPort
ln, err := net.Listen("tcp", fmt.Sprintf(":%s", httpListenPort))
if err != nil {
logger.Fatalf("cannot listen on port %s: %v", httpListenPort, err)
}
go func() {
err = http.Serve(ln, handler)
if err != nil {
logger.Fatalf("cannot start http server: %v", err)
}
defer ln.Close()
}()
}
// adding time.Now().UnixNano() to avoid possible file conflict when multiple processes run on a single host
storagePath = filepath.Join(os.TempDir(), testStoragePath, strconv.FormatInt(time.Now().UnixNano(), 10))
processFlags()
vminsert.Init()
vmselect.Init()
@@ -102,16 +135,34 @@ func UnitTest(files []string, disableGroupLabel bool, externalLabels []string, e
}
var failed bool
for fileName, file := range testfiles {
if err := ruleUnitTest(fileName, file, labels); err != nil {
fmt.Println("FAILED")
fmt.Printf("failed to run unit test for file %q: \n%v", fileName, err)
failed = true
} else {
fmt.Println(" SUCCESS")
runTest := func() bool {
for fileName, file := range testfiles {
if err := ruleUnitTest(fileName, file, labels); err != nil {
fmt.Println("FAILED")
fmt.Printf("failed to run unit test for file %q: \n%v", fileName, err)
return true
}
fmt.Println("SUCCESS")
}
return false
}
finishCh := make(chan struct{}, 1)
go func() {
failed = runTest()
finishCh <- struct{}{}
}()
sigs := make(chan os.Signal, 1)
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
select {
case sig := <-sigs:
fmt.Printf("received signal %s\n", sig)
failed = true
break
case <-finishCh:
break
}
return failed
}
@@ -211,8 +262,8 @@ func processFlags() {
{flag: "search.disableCache", value: "true"},
// set storage retention time to 100 years, allow to store series from 1970-01-01T00:00:00.
{flag: "retentionPeriod", value: "100y"},
{flag: "datasource.url", value: testDataSourcePath},
{flag: "remoteWrite.url", value: testRemoteWritePath},
{flag: "datasource.url", value: fmt.Sprintf("http://127.0.0.1:%s/prometheus", httpListenAddr)},
{flag: "remoteWrite.url", value: fmt.Sprintf("http://127.0.0.1:%s", httpListenAddr)},
{flag: "notifier.blackhole", value: "true"},
} {
// panics if flag doesn't exist
@@ -224,27 +275,10 @@ func processFlags() {
func setUp() {
vmstorage.Init(promql.ResetRollupResultCacheIfNeeded)
var ab flagutil.ArrayBool
go httpserver.Serve([]string{httpListenAddr}, &ab, func(w http.ResponseWriter, r *http.Request) bool {
switch r.URL.Path {
case "/prometheus/api/v1/query":
if err := prometheus.QueryHandler(nil, time.Now(), w, r); err != nil {
httpserver.Errorf(w, r, "%s", err)
}
return true
case "/prometheus/api/v1/write", "/api/v1/write":
if err := promremotewrite.InsertHandler(r); err != nil {
httpserver.Errorf(w, r, "%s", err)
}
return true
default:
}
return false
})
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
readyCheckFunc := func() bool {
resp, err := http.Get(testHealthHTTPPath)
resp, err := http.Get(fmt.Sprintf("http://127.0.0.1:%s/health", httpListenAddr))
if err != nil {
return false
}
@@ -266,9 +300,6 @@ checkCheck:
}
func tearDown() {
if err := httpserver.Stop([]string{httpListenAddr}); err != nil {
logger.Errorf("cannot stop the webservice: %s", err)
}
vmstorage.Stop()
metrics.UnregisterAllMetrics()
fs.MustRemoveAll(storagePath)
@@ -283,7 +314,7 @@ func (tg *testGroup) test(evalInterval time.Duration, groupOrderMap map[string]i
if tg.Interval == nil {
tg.Interval = promutils.NewDuration(evalInterval)
}
err := writeInputSeries(tg.InputSeries, tg.Interval, testStartTime, testPromWriteHTTPPath)
err := writeInputSeries(tg.InputSeries, tg.Interval, testStartTime, fmt.Sprintf("http://127.0.0.1:%s/api/v1/write", httpListenAddr))
if err != nil {
return []error{err}
}

View File

@@ -8,7 +8,7 @@ func TestUnitTest_Failure(t *testing.T) {
f := func(files []string) {
t.Helper()
failed := UnitTest(files, false, nil, "", "")
failed := UnitTest(files, false, nil, "", "", "")
if !failed {
t.Fatalf("expecting failed test")
}
@@ -20,19 +20,20 @@ func TestUnitTest_Failure(t *testing.T) {
}
func TestUnitTest_Success(t *testing.T) {
f := func(disableGroupLabel bool, files []string, externalLabels []string, externalURL string) {
f := func(disableGroupLabel bool, files []string, externalLabels []string, externalURL, httpPort string) {
t.Helper()
failed := UnitTest(files, disableGroupLabel, externalLabels, externalURL, "")
failed := UnitTest(files, disableGroupLabel, externalLabels, externalURL, httpPort, "")
if failed {
t.Fatalf("unexpected failed test")
}
}
// run multi files
f(false, []string{"./testdata/test1.yaml", "./testdata/test2.yaml"}, []string{"cluster=prod"}, "http://grafana:3000")
// run multi files with random http port
f(false, []string{"./testdata/test1.yaml", "./testdata/test2.yaml"}, []string{"cluster=prod"}, "http://grafana:3000", "")
// disable group label
// template with null external values
f(true, []string{"./testdata/disable-group-label.yaml"}, nil, "")
// specify httpListenAddr
f(true, []string{"./testdata/disable-group-label.yaml"}, nil, "", "8880")
}

View File

@@ -157,6 +157,19 @@ func TestGroupValidate_Failure(t *testing.T) {
f(&Group{}, false, "group name must be set")
f(&Group{
Name: "both record and alert are not set",
Rules: []Rule{
{
Expr: "sum(up == 0 ) by (host)",
For: promutils.NewDuration(10 * time.Millisecond),
},
{
Expr: "sumSeries(time('foo.bar',10))",
},
},
}, false, "invalid rule")
f(&Group{
Name: "negative interval",
Interval: promutils.NewDuration(-1),
@@ -240,59 +253,6 @@ func TestGroupValidate_Failure(t *testing.T) {
},
}, false, "duplicate")
f(&Group{
Name: "test graphite with prometheus expr",
Type: NewGraphiteType(),
Rules: []Rule{
{
Expr: "sum(up == 0 ) by (host)",
For: promutils.NewDuration(10 * time.Millisecond),
},
{
Expr: "sumSeries(time('foo.bar',10))",
},
},
}, false, "invalid rule")
f(&Group{
Name: "test graphite inherit",
Type: NewGraphiteType(),
Rules: []Rule{
{
Expr: "sumSeries(time('foo.bar',10))",
For: promutils.NewDuration(10 * time.Millisecond),
},
{
Expr: "sum(up == 0 ) by (host)",
},
},
}, false, "either `record` or `alert` must be set")
f(&Group{
Name: "test vlogs with prometheus expr",
Type: NewVLogsType(),
Rules: []Rule{
{
Expr: "sum(up == 0 ) by (host)",
For: promutils.NewDuration(10 * time.Millisecond),
},
{
Expr: "sumSeries(time('foo.bar',10))",
},
},
}, false, "invalid rule")
// validate expressions
f(&Group{
Name: "test",
Rules: []Rule{
{
Record: "record",
Expr: "up | 0",
},
},
}, true, "invalid expression")
f(&Group{
Name: "test thanos",
Type: NewRawType("thanos"),
@@ -303,8 +263,20 @@ func TestGroupValidate_Failure(t *testing.T) {
},
}, true, "unknown datasource type")
// validate expressions
f(&Group{
Name: "test graphite",
Name: "test prometheus expr",
Type: NewPrometheusType(),
Rules: []Rule{
{
Record: "record",
Expr: "up | 0",
},
},
}, true, "bad prometheus expr")
f(&Group{
Name: "test graphite expr",
Type: NewGraphiteType(),
Rules: []Rule{
{Alert: "alert", Expr: "up == 1", Labels: map[string]string{
@@ -314,14 +286,63 @@ func TestGroupValidate_Failure(t *testing.T) {
}, true, "bad graphite expr")
f(&Group{
Name: "test vlogs",
Name: "test vlogs expr",
Type: NewVLogsType(),
Rules: []Rule{
{Alert: "alert", Expr: "stats count(*) as requests", Labels: map[string]string{
"description": "some-description",
}},
{Alert: "alert", Expr: "stats count(*) as requests"},
},
}, true, "bad LogsQL expr")
f(&Group{
Name: "test vlogs expr",
Type: NewVLogsType(),
Rules: []Rule{
{Alert: "alert", Expr: "_time: 1m | stats by (path, _time: 1m) count(*) as requests"},
},
}, true, "bad LogsQL expr")
f(&Group{
Name: "test graphite with prometheus expr",
Type: NewGraphiteType(),
Rules: []Rule{
{
Record: "r1",
ID: 1,
Expr: "sumSeries(time('foo.bar',10))",
For: promutils.NewDuration(10 * time.Millisecond),
},
{
Record: "r2",
ID: 2,
Expr: "sum(up == 0 ) by (host)",
},
},
}, true, "bad graphite expr")
f(&Group{
Name: "test vlogs with prometheus exp",
Type: NewVLogsType(),
Rules: []Rule{
{
Record: "r1",
Expr: "sum(up == 0 ) by (host)",
For: promutils.NewDuration(10 * time.Millisecond),
},
},
}, true, "bad LogsQL expr")
f(&Group{
Name: "test prometheus with vlogs exp",
Type: NewPrometheusType(),
Rules: []Rule{
{
Record: "r1",
Expr: "* | stats by (path) count()",
For: promutils.NewDuration(10 * time.Millisecond),
},
},
}, true, "bad prometheus expr")
}
func TestGroupValidate_Success(t *testing.T) {

View File

@@ -71,9 +71,18 @@ func (t *Type) ValidateExpr(expr string) error {
return fmt.Errorf("bad prometheus expr: %q, err: %w", expr, err)
}
case "vlogs":
if _, err := logstorage.ParseStatsQuery(expr, 0); err != nil {
q, err := logstorage.ParseStatsQuery(expr, 0)
if err != nil {
return fmt.Errorf("bad LogsQL expr: %q, err: %w", expr, err)
}
fields, _ := q.GetStatsByFields()
for i := range fields {
// VictoriaLogs inserts `_time` field as a label in result when query with `stats by (_time:step)`,
// making the result meaningless and may lead to cardinality issues.
if fields[i] == "_time" {
return fmt.Errorf("bad LogsQL expr: %q, err: cannot contain time buckets stats pipe `stats by (_time:step)`", expr)
}
}
default:
return fmt.Errorf("unknown datasource type=%q", t.Name)
}

View File

@@ -405,6 +405,9 @@ func configsEqual(a, b []config.Group) bool {
if a[i].Checksum != b[i].Checksum {
return false
}
if a[i].File != b[i].File {
return false
}
}
return true
}

View File

@@ -10,6 +10,8 @@ import (
"strings"
"time"
"github.com/VictoriaMetrics/metrics"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/utils"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/httputils"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promauth"
@@ -29,25 +31,34 @@ type AlertManager struct {
// stores already parsed RelabelConfigs object
relabelConfigs *promrelabel.ParsedConfigs
metrics *metrics
metrics *notifierMetrics
}
type metrics struct {
alertsSent *utils.Counter
alertsSendErrors *utils.Counter
type notifierMetrics struct {
set *metrics.Set
alertsSent *metrics.Counter
alertsSendErrors *metrics.Counter
}
func newMetrics(addr string) *metrics {
return &metrics{
alertsSent: utils.GetOrCreateCounter(fmt.Sprintf("vmalert_alerts_sent_total{addr=%q}", addr)),
alertsSendErrors: utils.GetOrCreateCounter(fmt.Sprintf("vmalert_alerts_send_errors_total{addr=%q}", addr)),
func newNotifierMetrics(addr string) *notifierMetrics {
set := metrics.NewSet()
metrics.RegisterSet(set)
return &notifierMetrics{
set: set,
alertsSent: set.GetOrCreateCounter(fmt.Sprintf("vmalert_alerts_sent_total{addr=%q}", addr)),
alertsSendErrors: set.GetOrCreateCounter(fmt.Sprintf("vmalert_alerts_send_errors_total{addr=%q}", addr)),
}
}
func (nm *notifierMetrics) close() {
metrics.UnregisterSet(nm.set, true)
}
// Close is a destructor method for AlertManager
func (am *AlertManager) Close() {
am.metrics.alertsSent.Unregister()
am.metrics.alertsSendErrors.Unregister()
am.metrics.close()
}
// Addr returns address where alerts are sent.
@@ -180,6 +191,6 @@ func NewAlertManager(alertManagerURL string, fn AlertURLGenerator, authCfg proma
relabelConfigs: relabelCfg,
client: &http.Client{Transport: tr},
timeout: timeout,
metrics: newMetrics(alertManagerURL),
metrics: newNotifierMetrics(alertManagerURL),
}, nil
}

View File

@@ -54,6 +54,7 @@ var (
"If multiple args are set, then they are applied independently for the corresponding -notifier.url")
oauth2Scopes = flagutil.NewArrayString("notifier.oauth2.scopes", "Optional OAuth2 scopes to use for -notifier.url. Scopes must be delimited by ';'. "+
"If multiple args are set, then they are applied independently for the corresponding -notifier.url")
sendTimeout = flagutil.NewArrayDuration("notifier.sendTimeout", 10*time.Second, "Timeout when sending alerts to the corresponding -notifier.url")
)
// cw holds a configWatcher for configPath configuration file
@@ -175,7 +176,7 @@ func notifiersFromFlags(gen AlertURLGenerator) ([]Notifier, error) {
}
addr = strings.TrimSuffix(addr, "/")
am, err := NewAlertManager(addr+alertManagerPath, gen, authCfg, nil, time.Second*10)
am, err := NewAlertManager(addr+alertManagerPath, gen, authCfg, nil, sendTimeout.GetOptionalArg(i))
if err != nil {
return nil, err
}

View File

@@ -6,7 +6,7 @@ import "context"
// to be sent.
type blackHoleNotifier struct {
addr string
metrics *metrics
metrics *notifierMetrics
}
// Send will send no notifications, but increase the metric.
@@ -22,8 +22,7 @@ func (bh blackHoleNotifier) Addr() string {
// Close unregister the metrics
func (bh *blackHoleNotifier) Close() {
bh.metrics.alertsSent.Unregister()
bh.metrics.alertsSendErrors.Unregister()
bh.metrics.close()
}
// newBlackHoleNotifier creates a new blackHoleNotifier
@@ -31,6 +30,6 @@ func newBlackHoleNotifier() *blackHoleNotifier {
address := "blackhole"
return &blackHoleNotifier{
addr: address,
metrics: newMetrics(address),
metrics: newNotifierMetrics(address),
}
}

View File

@@ -9,6 +9,8 @@ import (
"sync"
"time"
"github.com/VictoriaMetrics/metrics"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/config"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/datasource"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/notifier"
@@ -57,6 +59,69 @@ type alertingRuleMetrics struct {
seriesFetched *utils.Gauge
}
func newAlertingRuleMetrics(set *metrics.Set, ar *AlertingRule) *alertingRuleMetrics {
labels := fmt.Sprintf(`alertname=%q, group=%q, file=%q, id="%d"`, ar.Name, ar.GroupName, ar.File, ar.ID())
arm := &alertingRuleMetrics{}
arm.pending = utils.NewGauge(set, fmt.Sprintf(`vmalert_alerts_pending{%s}`, labels),
func() float64 {
ar.alertsMu.RLock()
defer ar.alertsMu.RUnlock()
var num int
for _, a := range ar.alerts {
if a.State == notifier.StatePending {
num++
}
}
return float64(num)
})
arm.active = utils.NewGauge(set, fmt.Sprintf(`vmalert_alerts_firing{%s}`, labels),
func() float64 {
ar.alertsMu.RLock()
defer ar.alertsMu.RUnlock()
var num int
for _, a := range ar.alerts {
if a.State == notifier.StateFiring {
num++
}
}
return float64(num)
})
arm.errors = utils.NewCounter(set, fmt.Sprintf(`vmalert_alerting_rules_errors_total{%s}`, labels))
arm.samples = utils.NewGauge(set, fmt.Sprintf(`vmalert_alerting_rules_last_evaluation_samples{%s}`, labels),
func() float64 {
e := ar.state.getLast()
return float64(e.Samples)
})
arm.seriesFetched = utils.NewGauge(set, fmt.Sprintf(`vmalert_alerting_rules_last_evaluation_series_fetched{%s}`, labels),
func() float64 {
e := ar.state.getLast()
if e.SeriesFetched == nil {
// means seriesFetched is unsupported
return -1
}
seriesFetched := float64(*e.SeriesFetched)
if seriesFetched == 0 && e.Samples > 0 {
// `alert: 0.95` will fetch no series
// but will get one time series in response.
seriesFetched = float64(e.Samples)
}
return seriesFetched
})
return arm
}
func (arm *alertingRuleMetrics) close() {
if arm == nil {
return
}
arm.errors.Unregister()
arm.active.Unregister()
arm.pending.Unregister()
arm.samples.Unregister()
arm.seriesFetched.Unregister()
}
// NewAlertingRule creates a new AlertingRule
func NewAlertingRule(qb datasource.QuerierBuilder, group *Group, cfg config.Rule) *AlertingRule {
ar := &AlertingRule{
@@ -81,8 +146,7 @@ func NewAlertingRule(qb datasource.QuerierBuilder, group *Group, cfg config.Rule
Headers: group.Headers,
Debug: cfg.Debug,
}),
alerts: make(map[uint64]*notifier.Alert),
metrics: &alertingRuleMetrics{},
alerts: make(map[uint64]*notifier.Alert),
}
entrySize := *ruleUpdateEntriesLimit
@@ -95,63 +159,17 @@ func NewAlertingRule(qb datasource.QuerierBuilder, group *Group, cfg config.Rule
ar.state = &ruleState{
entries: make([]StateEntry, entrySize),
}
labels := fmt.Sprintf(`alertname=%q, group=%q, file=%q, id="%d"`, ar.Name, group.Name, group.File, ar.ID())
ar.metrics.pending = utils.GetOrCreateGauge(fmt.Sprintf(`vmalert_alerts_pending{%s}`, labels),
func() float64 {
ar.alertsMu.RLock()
defer ar.alertsMu.RUnlock()
var num int
for _, a := range ar.alerts {
if a.State == notifier.StatePending {
num++
}
}
return float64(num)
})
ar.metrics.active = utils.GetOrCreateGauge(fmt.Sprintf(`vmalert_alerts_firing{%s}`, labels),
func() float64 {
ar.alertsMu.RLock()
defer ar.alertsMu.RUnlock()
var num int
for _, a := range ar.alerts {
if a.State == notifier.StateFiring {
num++
}
}
return float64(num)
})
ar.metrics.errors = utils.GetOrCreateCounter(fmt.Sprintf(`vmalert_alerting_rules_errors_total{%s}`, labels))
ar.metrics.samples = utils.GetOrCreateGauge(fmt.Sprintf(`vmalert_alerting_rules_last_evaluation_samples{%s}`, labels),
func() float64 {
e := ar.state.getLast()
return float64(e.Samples)
})
ar.metrics.seriesFetched = utils.GetOrCreateGauge(fmt.Sprintf(`vmalert_alerting_rules_last_evaluation_series_fetched{%s}`, labels),
func() float64 {
e := ar.state.getLast()
if e.SeriesFetched == nil {
// means seriesFetched is unsupported
return -1
}
seriesFetched := float64(*e.SeriesFetched)
if seriesFetched == 0 && e.Samples > 0 {
// `alert: 0.95` will fetch no series
// but will get one time series in response.
seriesFetched = float64(e.Samples)
}
return seriesFetched
})
ar.metrics = newAlertingRuleMetrics(group.metrics.set, ar)
return ar
}
func (ar *AlertingRule) registerMetrics(g *Group) {
ar.metrics = newAlertingRuleMetrics(g.metrics.set, ar)
}
// close unregisters rule metrics
func (ar *AlertingRule) close() {
ar.metrics.active.Unregister()
ar.metrics.pending.Unregister()
ar.metrics.errors.Unregister()
ar.metrics.samples.Unregister()
ar.metrics.seriesFetched.Unregister()
func (ar *AlertingRule) unregisterMetrics() {
ar.metrics.close()
}
// String implements Stringer interface

View File

@@ -11,6 +11,8 @@ import (
"testing"
"time"
"github.com/VictoriaMetrics/metrics"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/config"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/datasource"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/notifier"
@@ -1248,13 +1250,17 @@ func newTestAlertingRule(name string, waitFor time.Duration) *AlertingRule {
EvalInterval: waitFor,
alerts: make(map[uint64]*notifier.Alert),
state: &ruleState{entries: make([]StateEntry, 10)},
metrics: &alertingRuleMetrics{
errors: utils.GetOrCreateCounter(fmt.Sprintf(`vmalert_alerting_rules_errors_total{alertname=%q}`, name)),
},
metrics: getTestAlertingRuleMetrics(name),
}
return &rule
}
func getTestAlertingRuleMetrics(name string) *alertingRuleMetrics {
m := &alertingRuleMetrics{}
m.errors = utils.NewCounter(metrics.NewSet(), fmt.Sprintf(`vmalert_alerting_rules_errors_total{alertname=%q}`, name))
return m
}
func newTestAlertingRuleWithCustomFields(name string, waitFor, evalInterval, keepFiringFor time.Duration, annotation map[string]string) *AlertingRule {
rule := newTestAlertingRule(name, waitFor)
if evalInterval != 0 {

View File

@@ -13,6 +13,8 @@ import (
"github.com/cheggaaa/pb/v3"
"github.com/VictoriaMetrics/metrics"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/config"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/datasource"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/notifier"
@@ -20,7 +22,6 @@ import (
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/utils"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/prompbmarshal"
"github.com/VictoriaMetrics/metrics"
)
var (
@@ -74,19 +75,23 @@ type Group struct {
}
type groupMetrics struct {
iterationTotal *utils.Counter
iterationDuration *utils.Summary
iterationMissed *utils.Counter
iterationInterval *utils.Gauge
set *metrics.Set
iterationTotal *metrics.Counter
iterationDuration *metrics.Summary
iterationMissed *metrics.Counter
iterationInterval *metrics.Gauge
}
func newGroupMetrics(g *Group) *groupMetrics {
m := &groupMetrics{}
m.set = metrics.NewSet()
labels := fmt.Sprintf(`group=%q, file=%q`, g.Name, g.File)
m.iterationTotal = utils.GetOrCreateCounter(fmt.Sprintf(`vmalert_iteration_total{%s}`, labels))
m.iterationDuration = utils.GetOrCreateSummary(fmt.Sprintf(`vmalert_iteration_duration_seconds{%s}`, labels))
m.iterationMissed = utils.GetOrCreateCounter(fmt.Sprintf(`vmalert_iteration_missed_total{%s}`, labels))
m.iterationInterval = utils.GetOrCreateGauge(fmt.Sprintf(`vmalert_iteration_interval_seconds{%s}`, labels), func() float64 {
m.iterationTotal = m.set.GetOrCreateCounter(fmt.Sprintf(`vmalert_iteration_total{%s}`, labels))
m.iterationDuration = m.set.GetOrCreateSummary(fmt.Sprintf(`vmalert_iteration_duration_seconds{%s}`, labels))
m.iterationMissed = m.set.GetOrCreateCounter(fmt.Sprintf(`vmalert_iteration_missed_total{%s}`, labels))
m.iterationInterval = m.set.GetOrCreateGauge(fmt.Sprintf(`vmalert_iteration_interval_seconds{%s}`, labels), func() float64 {
g.mu.RLock()
i := g.Interval.Seconds()
g.mu.RUnlock()
@@ -95,6 +100,14 @@ func newGroupMetrics(g *Group) *groupMetrics {
return m
}
func (m *groupMetrics) start() {
metrics.RegisterSet(m.set)
}
func (m *groupMetrics) close() {
metrics.UnregisterSet(m.set, true)
}
// merges group rule labels into result map
// set2 has priority over set1.
func mergeLabels(groupName, ruleName string, set1, set2 map[string]string) map[string]string {
@@ -237,7 +250,7 @@ func (g *Group) updateWith(newGroup *Group) error {
if !ok {
// old rule is not present in the new list
// so we mark it for removing
g.Rules[i].close()
g.Rules[i].unregisterMetrics()
g.Rules[i] = nil
continue
}
@@ -257,6 +270,7 @@ func (g *Group) updateWith(newGroup *Group) error {
}
// add the rest of rules from registry
for _, nr := range rulesRegistry {
nr.registerMetrics(g)
newRules = append(newRules, nr)
}
// note that g.Interval is not updated here
@@ -295,13 +309,7 @@ func (g *Group) Close() {
g.InterruptEval()
<-g.finishedCh
g.metrics.iterationDuration.Unregister()
g.metrics.iterationTotal.Unregister()
g.metrics.iterationMissed.Unregister()
g.metrics.iterationInterval.Unregister()
for _, rule := range g.Rules {
rule.close()
}
g.metrics.close()
}
// SkipRandSleepOnGroupStart will skip random sleep delay in group first evaluation
@@ -311,6 +319,8 @@ var SkipRandSleepOnGroupStart bool
func (g *Group) Start(ctx context.Context, nts func() []notifier.Notifier, rw remotewrite.RWClient, rr datasource.QuerierBuilder) {
defer func() { close(g.finishedCh) }()
g.metrics.start()
evalTS := time.Now()
// sleep random duration to spread group rules evaluation
// over time in order to reduce load on datasource.

View File

@@ -10,6 +10,7 @@ import (
"testing"
"time"
"github.com/VictoriaMetrics/metrics"
"gopkg.in/yaml.v2"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/config"
@@ -41,6 +42,7 @@ func TestUpdateWith(t *testing.T) {
g := &Group{
Name: "test",
}
g.metrics = newGroupMetrics(g)
qb := &datasource.FakeQuerier{}
for _, r := range currentRules {
r.ID = config.HashRule(r)
@@ -50,6 +52,7 @@ func TestUpdateWith(t *testing.T) {
ng := &Group{
Name: "test",
}
ng.metrics = newGroupMetrics(ng)
for _, r := range newRules {
r.ID = config.HashRule(r)
ng.Rules = append(ng.Rules, ng.newRule(qb, r))
@@ -195,6 +198,7 @@ func TestUpdateDuringRandSleep(t *testing.T) {
Interval: 100 * time.Hour,
updateCh: make(chan *Group),
}
g.metrics = newGroupMetrics(g)
go g.Start(context.Background(), nil, nil, nil)
rule1 := AlertingRule{
@@ -209,6 +213,7 @@ func TestUpdateDuringRandSleep(t *testing.T) {
&rule1,
},
}
g1.metrics = newGroupMetrics(g1)
g.updateCh <- g1
time.Sleep(10 * time.Millisecond)
g.mu.RLock()
@@ -218,8 +223,9 @@ func TestUpdateDuringRandSleep(t *testing.T) {
g.mu.RUnlock()
rule2 := AlertingRule{
Name: "jobDown",
Expr: "up{job=\"vmagent\"}==0",
RuleID: 1,
Name: "jobDown",
Expr: "up{job=\"vmagent\"}==0",
Labels: map[string]string{
"foo": "bar",
"baz": "qux",
@@ -227,17 +233,32 @@ func TestUpdateDuringRandSleep(t *testing.T) {
}
g2 := &Group{
Rules: []Rule{
&rule1,
&rule2,
},
}
g2.metrics = newGroupMetrics(g2)
g.updateCh <- g2
time.Sleep(10 * time.Millisecond)
g.mu.RLock()
if len(g.Rules[0].(*AlertingRule).Labels) != 2 {
if len(g.Rules) != 2 {
t.Fatalf("expected to have updated rules")
}
if len(g.Rules[1].(*AlertingRule).Labels) != 2 {
t.Fatalf("expected to have updated labels")
}
g.mu.RUnlock()
metricsAfter := metrics.GetDefaultSet().ListMetricNames()
metricsRegistry := make(map[string]struct{}, len(metricsAfter))
for _, m := range metricsAfter {
if _, ok := metricsRegistry[m]; ok {
t.Fatalf("duplicate metric name %q", m)
}
metricsRegistry[m] = struct{}{}
}
g.Close()
}

View File

@@ -6,6 +6,8 @@ import (
"strings"
"time"
"github.com/VictoriaMetrics/metrics"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/config"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/datasource"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/utils"
@@ -45,6 +47,28 @@ type recordingRuleMetrics struct {
samples *utils.Gauge
}
func newRecordingRuleMetrics(set *metrics.Set, rr *RecordingRule) *recordingRuleMetrics {
rmr := &recordingRuleMetrics{}
labels := fmt.Sprintf(`recording=%q, group=%q, file=%q, id="%d"`, rr.Name, rr.GroupName, rr.File, rr.ID())
rmr.errors = utils.NewCounter(set, fmt.Sprintf(`vmalert_recording_rules_errors_total{%s}`, labels))
rmr.samples = utils.NewGauge(set, fmt.Sprintf(`vmalert_recording_rules_last_evaluation_samples{%s}`, labels),
func() float64 {
e := rr.state.getLast()
return float64(e.Samples)
})
return rmr
}
func (m *recordingRuleMetrics) close() {
if m == nil {
return
}
m.errors.Unregister()
m.samples.Unregister()
}
// String implements Stringer interface
func (rr *RecordingRule) String() string {
return rr.Name
@@ -67,7 +91,6 @@ func NewRecordingRule(qb datasource.QuerierBuilder, group *Group, cfg config.Rul
GroupID: group.ID(),
GroupName: group.Name,
File: group.File,
metrics: &recordingRuleMetrics{},
q: qb.BuildWithParams(datasource.QuerierParams{
DataSourceType: group.Type.String(),
ApplyIntervalAsTimeFilter: setIntervalAsTimeFilter(group.Type.String(), cfg.Expr),
@@ -87,21 +110,18 @@ func NewRecordingRule(qb datasource.QuerierBuilder, group *Group, cfg config.Rul
rr.state = &ruleState{
entries: make([]StateEntry, entrySize),
}
rr.metrics = newRecordingRuleMetrics(group.metrics.set, rr)
labels := fmt.Sprintf(`recording=%q, group=%q, file=%q, id="%d"`, rr.Name, group.Name, group.File, rr.ID())
rr.metrics.errors = utils.GetOrCreateCounter(fmt.Sprintf(`vmalert_recording_rules_errors_total{%s}`, labels))
rr.metrics.samples = utils.GetOrCreateGauge(fmt.Sprintf(`vmalert_recording_rules_last_evaluation_samples{%s}`, labels),
func() float64 {
e := rr.state.getLast()
return float64(e.Samples)
})
return rr
}
func (rr *RecordingRule) registerMetrics(g *Group) {
rr.metrics = newRecordingRuleMetrics(g.metrics.set, rr)
}
// close unregisters rule metrics
func (rr *RecordingRule) close() {
rr.metrics.errors.Unregister()
rr.metrics.samples.Unregister()
func (rr *RecordingRule) unregisterMetrics() {
rr.metrics.close()
}
// execRange executes recording rule on the given time range similarly to Exec.

View File

@@ -7,8 +7,9 @@ import (
"testing"
"time"
"github.com/VictoriaMetrics/metrics"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/datasource"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/utils"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/decimal"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/prompbmarshal"
)
@@ -306,15 +307,12 @@ func TestRecordingRuleLimit_Failure(t *testing.T) {
fq := &datasource.FakeQuerier{}
fq.Add(testMetrics...)
rule := &RecordingRule{Name: "job:foo",
state: &ruleState{entries: make([]StateEntry, 10)},
Labels: map[string]string{
"source": "test_limit",
},
metrics: &recordingRuleMetrics{
errors: utils.GetOrCreateCounter(`vmalert_recording_rules_errors_total{alertname="job:foo"}`),
},
metrics: getTestRecordingRuleMetrics(),
}
rule.q = fq
@@ -344,15 +342,12 @@ func TestRecordingRuleLimit_Success(t *testing.T) {
fq := &datasource.FakeQuerier{}
fq.Add(testMetrics...)
rule := &RecordingRule{Name: "job:foo",
state: &ruleState{entries: make([]StateEntry, 10)},
Labels: map[string]string{
"source": "test_limit",
},
metrics: &recordingRuleMetrics{
errors: utils.GetOrCreateCounter(`vmalert_recording_rules_errors_total{alertname="job:foo"}`),
},
metrics: getTestRecordingRuleMetrics(),
}
rule.q = fq
@@ -366,16 +361,19 @@ func TestRecordingRuleLimit_Success(t *testing.T) {
f(-1)
}
func getTestRecordingRuleMetrics() *recordingRuleMetrics {
m := newRecordingRuleMetrics(metrics.NewSet(), &RecordingRule{})
return m
}
func TestRecordingRuleExec_Negative(t *testing.T) {
rr := &RecordingRule{
Name: "job:foo",
Labels: map[string]string{
"job": "test",
},
state: &ruleState{entries: make([]StateEntry, 10)},
metrics: &recordingRuleMetrics{
errors: utils.GetOrCreateCounter(`vmalert_recording_rules_errors_total{alertname="job:foo"}`),
},
state: &ruleState{entries: make([]StateEntry, 10)},
metrics: getTestRecordingRuleMetrics(),
}
fq := &datasource.FakeQuerier{}
expErr := "connection reset by peer"

View File

@@ -27,9 +27,10 @@ type Rule interface {
// updateWith performs modification of current Rule
// with fields of the given Rule.
updateWith(Rule) error
// close performs the shutdown procedures for rule
// such as metrics unregister
close()
// unregister Rule metrics
unregisterMetrics()
// register Rule metrics with the given group
registerMetrics(g *Group)
}
var errDuplicate = errors.New("result contains metrics with the same labelset during evaluation. See https://docs.victoriametrics.com/vmalert/#series-with-the-same-labelset for details")

View File

@@ -33,7 +33,7 @@ import (
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/datasource"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/formatutil"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promutils"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/timeutil"
)
// go template execution fails when it's tree is empty
@@ -259,7 +259,7 @@ func templateFuncs() textTpl.FuncMap {
// parseDuration parses a duration string such as "1h" into the number of seconds it represents
"parseDuration": func(s string) (float64, error) {
d, err := promutils.ParseDuration(s)
d, err := timeutil.ParseDuration(s)
if err != nil {
return 0, err
}
@@ -268,7 +268,7 @@ func templateFuncs() textTpl.FuncMap {
// same with parseDuration but returns a time.Duration
"parseDurationTime": func(s string) (time.Duration, error) {
d, err := promutils.ParseDuration(s)
d, err := timeutil.ParseDuration(s)
if err != nil {
return 0, err
}

View File

@@ -4,11 +4,13 @@ import "github.com/VictoriaMetrics/metrics"
type namedMetric struct {
Name string
set *metrics.Set
}
// Unregister removes the metric by name from default registry
func (nm namedMetric) Unregister() {
metrics.UnregisterMetric(nm.Name)
nm.set.UnregisterMetric(nm.Name)
}
// Gauge is a metrics.Gauge with Name
@@ -17,11 +19,11 @@ type Gauge struct {
*metrics.Gauge
}
// GetOrCreateGauge creates a new Gauge with the given name
func GetOrCreateGauge(name string, f func() float64) *Gauge {
// NewGauge creates a new Gauge with the given name
func NewGauge(set *metrics.Set, name string, f func() float64) *Gauge {
return &Gauge{
namedMetric: namedMetric{Name: name},
Gauge: metrics.GetOrCreateGauge(name, f),
namedMetric: namedMetric{Name: name, set: set},
Gauge: set.NewGauge(name, f),
}
}
@@ -31,11 +33,11 @@ type Counter struct {
*metrics.Counter
}
// GetOrCreateCounter creates a new Counter with the given name
func GetOrCreateCounter(name string) *Counter {
// NewCounter creates a new Counter with the given name
func NewCounter(set *metrics.Set, name string) *Counter {
return &Counter{
namedMetric: namedMetric{Name: name},
Counter: metrics.GetOrCreateCounter(name),
namedMetric: namedMetric{Name: name, set: set},
Counter: set.NewCounter(name),
}
}
@@ -45,10 +47,10 @@ type Summary struct {
*metrics.Summary
}
// GetOrCreateSummary creates a new Summary with the given name
func GetOrCreateSummary(name string) *Summary {
// NewSummary creates a new Summary with the given name
func NewSummary(set *metrics.Set, name string) *Summary {
return &Summary{
namedMetric: namedMetric{Name: name},
Summary: metrics.GetOrCreateSummary(name),
namedMetric: namedMetric{Name: name, set: set},
Summary: set.NewSummary(name),
}
}

View File

@@ -21,14 +21,18 @@ func TestHandler(t *testing.T) {
fq.Add(datasource.Metric{
Values: []float64{1}, Timestamps: []int64{0},
})
g := &rule.Group{
g := rule.NewGroup(config.Group{
Name: "group",
File: "rules.yaml",
Concurrency: 1,
}
ar := rule.NewAlertingRule(fq, g, config.Rule{ID: 0, Alert: "alert"})
rr := rule.NewRecordingRule(fq, g, config.Rule{ID: 1, Record: "record"})
g.Rules = []rule.Rule{ar, rr}
Rules: []config.Rule{
{ID: 0, Alert: "alert"},
{ID: 1, Record: "record"},
},
}, fq, 1*time.Minute, nil)
ar := g.Rules[0].(*rule.AlertingRule)
rr := g.Rules[1].(*rule.RecordingRule)
g.ExecOnce(context.Background(), func() []notifier.Notifier { return nil }, nil, time.Time{})
m := &manager{groups: map[uint64]*rule.Group{

View File

@@ -4,6 +4,7 @@ import (
"fmt"
"reflect"
"testing"
"time"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/config"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/datasource"
@@ -15,20 +16,22 @@ func TestRecordingToApi(t *testing.T) {
fq.Add(datasource.Metric{
Values: []float64{1}, Timestamps: []int64{0},
})
g := &rule.Group{
entriesLimit := 44
g := rule.NewGroup(config.Group{
Name: "group",
File: "rules.yaml",
Concurrency: 1,
}
entriesLimit := 44
rr := rule.NewRecordingRule(fq, g, config.Rule{
ID: 1248,
Record: "record_name",
Expr: "up",
Labels: map[string]string{"label": "value"},
UpdateEntriesLimit: &entriesLimit,
})
Rules: []config.Rule{
{
ID: 1248,
Record: "record_name",
Expr: "up",
Labels: map[string]string{"label": "value"},
UpdateEntriesLimit: &entriesLimit,
},
},
}, fq, 1*time.Minute, nil)
rr := g.Rules[0].(*rule.RecordingRule)
expectedRes := apiRule{
Name: "record_name",

View File

@@ -344,7 +344,7 @@ var (
},
&cli.BoolFlag{
Name: influxSkipDatabaseLabel,
Usage: "Wether to skip adding the label 'db' to timeseries.",
Usage: "Whether to skip adding the label 'db' to timeseries.",
Value: false,
},
&cli.BoolFlag{

View File

@@ -144,9 +144,9 @@ func timeFilter(start, end string) string {
// by checking available (non-empty) tags, fields and measurements
// which unique combination represents all possible
// time series existing in database.
// The explore required to reduce the load on influx
// Explore is required to reduce the load on influx
// by querying field of the exact time series at once,
// instead of fetching all of the values over and over.
// instead of fetching all the values over and over.
//
// May contain non-existing time series.
func (c *Client) Explore() ([]*Series, error) {
@@ -340,10 +340,7 @@ func (c *Client) fieldsByMeasurement() (map[string][]string, error) {
}
func (c *Client) getSeries() ([]*Series, error) {
com := "show series"
if c.filterSeries != "" {
com = fmt.Sprintf("%s %s", com, c.filterSeries)
}
com := c.getSeriesCommand()
q := influx.Query{
Command: com,
Database: c.database,
@@ -389,6 +386,21 @@ func (c *Client) getSeries() ([]*Series, error) {
return result, nil
}
func (c *Client) getSeriesCommand() string {
com := "show series"
if c.filterSeries != "" {
com = fmt.Sprintf("%s %s", com, c.filterSeries)
}
if c.filterTime != "" {
joinStatement := " where "
if strings.Contains(strings.ToLower(com), joinStatement) {
joinStatement = " AND "
}
com = fmt.Sprintf("%s%s%s", com, joinStatement, c.filterTime)
}
return com
}
// getMeasurementTags get the tags for each measurement.
// tags are placed in a map without values (similar to a set) for quick lookups:
// {"measurement1": {"tag1", "tag2"}, "measurement2": {"tag3", "tag4"}}

View File

@@ -103,3 +103,26 @@ func TestTimeFilter(t *testing.T) {
// both start and end filters
f("2020-01-01T20:07:00Z", "2020-01-01T21:07:00Z", "time >= '2020-01-01T20:07:00Z' and time <= '2020-01-01T21:07:00Z'")
}
func TestGetSeriesCommand(t *testing.T) {
f := func(filterSeries, filterTime, expCommand string) {
t.Helper()
c := &Client{
filterTime: filterTime,
filterSeries: filterSeries,
}
gotCommand := c.getSeriesCommand()
if gotCommand != expCommand {
t.Fatalf("unexpected command\ngot\n%s\nwant\n%s", gotCommand, expCommand)
}
}
f("", "", "show series")
f("from cpu", "", "show series from cpu")
f("from cpu where arch='x86'", "", "show series from cpu where arch='x86'")
f("", "time >= '2020-01-01T20:07:00Z'", "show series where time >= '2020-01-01T20:07:00Z'")
f("from cpu", "time >= '2020-01-01T20:07:00Z'", "show series from cpu where time >= '2020-01-01T20:07:00Z'")
f("from cpu where arch='x86'", "time >= '2020-01-01T20:07:00Z'", "show series from cpu where arch='x86' AND time >= '2020-01-01T20:07:00Z'")
f("from cpu where arch='x86' AND hostname='host_2753'", "time >= '2020-01-01T20:07:00Z'", "show series from cpu where arch='x86' AND hostname='host_2753' AND time >= '2020-01-01T20:07:00Z'")
}

View File

@@ -4,7 +4,7 @@ import (
"fmt"
"time"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promutils"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/timeutil"
)
const (
@@ -16,7 +16,7 @@ const (
// ParseTime parses time in s string and returns time.Time object
// if parse correctly or error if not
func ParseTime(s string) (time.Time, error) {
msecs, err := promutils.ParseTimeMsec(s)
msecs, err := timeutil.ParseTimeMsec(s)
if err != nil {
return time.Time{}, fmt.Errorf("cannot parse %s: %w", s, err)
}

View File

@@ -36,6 +36,9 @@ var (
"See https://docs.victoriametrics.com/stream-aggregation/#ignoring-old-samples")
streamAggrIgnoreFirstIntervals = flag.Int("streamAggr.ignoreFirstIntervals", 0, "Number of aggregation intervals to skip after the start. Increase this value if you observe incorrect aggregation results after restarts. It could be caused by receiving unordered delayed data from clients pushing data into the database. "+
"See https://docs.victoriametrics.com/stream-aggregation/#ignore-aggregation-intervals-on-start")
streamAggrEnableWindows = flag.Bool("streamAggr.enableWindows", false, "Enables aggregation within fixed windows for all aggregators. "+
"This allows to get more precise results, but impacts resource usage as it requires twice more memory to store two states. "+
"See https://docs.victoriametrics.com/stream-aggregation/#aggregation-windows.")
)
var (
@@ -62,6 +65,7 @@ func CheckStreamAggrConfig() error {
DropInputLabels: *streamAggrDropInputLabels,
IgnoreOldSamples: *streamAggrIgnoreOldSamples,
IgnoreFirstIntervals: *streamAggrIgnoreFirstIntervals,
EnableWindows: *streamAggrEnableWindows,
}
sas, err := streamaggr.LoadFromFile(*streamAggrConfig, pushNoop, opts, "global")
if err != nil {
@@ -78,7 +82,7 @@ func InitStreamAggr() {
saCfgReloaderStopCh = make(chan struct{})
if *streamAggrConfig == "" {
if *streamAggrDedupInterval > 0 {
deduplicator = streamaggr.NewDeduplicator(pushAggregateSeries, *streamAggrDedupInterval, *streamAggrDropInputLabels, "global")
deduplicator = streamaggr.NewDeduplicator(pushAggregateSeries, *streamAggrEnableWindows, *streamAggrDedupInterval, *streamAggrDropInputLabels, "global")
}
return
}

View File

@@ -2,6 +2,7 @@ package prompush
import (
"github.com/VictoriaMetrics/VictoriaMetrics/app/vminsert/common"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vminsert/relabel"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/prompbmarshal"
"github.com/VictoriaMetrics/metrics"
@@ -49,6 +50,7 @@ func push(ctx *common.InsertCtx, tss []prompbmarshal.TimeSeries) {
}
ctx.Reset(rowsLen)
rowsTotal := 0
hasRelabeling := relabel.HasRelabeling()
for i := range tss {
ts := &tss[i]
rowsTotal += len(ts.Samples)
@@ -57,7 +59,7 @@ func push(ctx *common.InsertCtx, tss []prompbmarshal.TimeSeries) {
label := &ts.Labels[j]
ctx.AddLabel(label.Name, label.Value)
}
if !ctx.TryPrepareLabels(false) {
if !ctx.TryPrepareLabels(hasRelabeling) {
continue
}
var metricNameRaw []byte

View File

@@ -373,9 +373,17 @@ func getRollupConfigs(funcName string, rf rollupFunc, expr metricsql.Expr, start
func(values []float64, timestamps []int64), []*rollupConfig, error) {
preFunc := func(_ []float64, _ []int64) {}
funcName = strings.ToLower(funcName)
// window > lookbackDelta could result in negative delta.
// See issue: https://github.com/VictoriaMetrics/VictoriaMetrics/issues/8342
stalenessInterval := lookbackDelta
if stalenessInterval != 0 && stalenessInterval < window {
stalenessInterval = window
}
if rollupFuncsRemoveCounterResets[funcName] {
preFunc = func(values []float64, timestamps []int64) {
removeCounterResets(values, timestamps, lookbackDelta)
removeCounterResets(values, timestamps, stalenessInterval)
}
}
samplesScannedPerCall := rollupFuncsSamplesScannedPerCall[funcName]
@@ -487,7 +495,7 @@ func getRollupConfigs(funcName string, rf rollupFunc, expr metricsql.Expr, start
if rollupFuncsRemoveCounterResets[aggrFuncName] {
// There is no need to save the previous preFunc, since it is either empty or the same.
preFunc = func(values []float64, timestamps []int64) {
removeCounterResets(values, timestamps, lookbackDelta)
removeCounterResets(values, timestamps, stalenessInterval)
}
}
rf := rollupAggrFuncs[aggrFuncName]

View File

@@ -1,13 +0,0 @@
{
"files": {
"main.css": "./static/css/main.7fa18e1b.css",
"main.js": "./static/js/main.ba08300f.js",
"static/js/685.f772060c.chunk.js": "./static/js/685.f772060c.chunk.js",
"static/media/MetricsQL.md": "./static/media/MetricsQL.a00044c91d9781cf8557.md",
"index.html": "./index.html"
},
"entrypoints": [
"static/css/main.7fa18e1b.css",
"static/js/main.ba08300f.js"
]
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
.uplot,.uplot *,.uplot *:before,.uplot *:after{box-sizing:border-box}.uplot{font-family:system-ui,-apple-system,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji";line-height:1.5;width:min-content}.u-title{text-align:center;font-size:18px;font-weight:700}.u-wrap{position:relative;-webkit-user-select:none;user-select:none}.u-over,.u-under{position:absolute}.u-under{overflow:hidden}.uplot canvas{display:block;position:relative;width:100%;height:100%}.u-axis{position:absolute}.u-legend{font-size:14px;margin:auto;text-align:center}.u-inline{display:block}.u-inline *{display:inline-block}.u-inline tr{margin-right:16px}.u-legend th{font-weight:600}.u-legend th>*{vertical-align:middle;display:inline-block}.u-legend .u-marker{width:1em;height:1em;margin-right:4px;background-clip:padding-box!important}.u-inline.u-live th:after{content:":";vertical-align:middle}.u-inline:not(.u-live) .u-value{display:none}.u-series>*{padding:4px}.u-series th{cursor:pointer}.u-legend .u-off>*{opacity:.3}.u-select{background:#00000012;position:absolute;pointer-events:none}.u-cursor-x,.u-cursor-y{position:absolute;left:0;top:0;pointer-events:none;will-change:transform}.u-hz .u-cursor-x,.u-vt .u-cursor-y{height:100%;border-right:1px dashed #607D8B}.u-hz .u-cursor-y,.u-vt .u-cursor-x{width:100%;border-bottom:1px dashed #607D8B}.u-cursor-pt{position:absolute;top:0;left:0;border-radius:50%;border:0 solid;pointer-events:none;will-change:transform;background-clip:padding-box!important}.u-axis.u-off,.u-select.u-off,.u-cursor-x.u-off,.u-cursor-y.u-off,.u-cursor-pt.u-off{display:none}

File diff suppressed because one or more lines are too long

View File

@@ -1 +1,58 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="./favicon.svg"/><link rel="apple-touch-icon" href="./favicon.svg"/><link rel="mask-icon" href="./favicon.svg" color="#000000"><meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=5"/><meta name="theme-color" content="#000000"/><meta name="description" content="Explore and troubleshoot your VictoriaMetrics data"/><link rel="manifest" href="./manifest.json"/><title>vmui</title><script src="./dashboards/index.js" type="module"></script><meta name="twitter:card" content="summary"><meta name="twitter:title" content="UI for VictoriaMetrics"><meta name="twitter:site" content="@https://victoriametrics.com/"><meta name="twitter:description" content="Explore and troubleshoot your VictoriaMetrics data"><meta name="twitter:image" content="./preview.jpg"><meta property="og:type" content="website"><meta property="og:title" content="UI for VictoriaMetrics"><meta property="og:url" content="https://victoriametrics.com/"><meta property="og:description" content="Explore and troubleshoot your VictoriaMetrics data"><script defer="defer" src="./static/js/main.ba08300f.js"></script><link href="./static/css/main.7fa18e1b.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<link rel="icon" href="./favicon.svg"/>
<link rel="apple-touch-icon" href="./favicon.svg"/>
<link rel="mask-icon" href="./favicon.svg" color="#000000">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=5"/>
<meta name="theme-color" content="#000000"/>
<meta name="description" content="Explore and troubleshoot your VictoriaMetrics data"/>
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="./manifest.json"/>
<!--
Notice the use of in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>vmui</title>
<script src="./dashboards/index.js" type="module"></script>
<meta name="twitter:card" content="summary">
<meta name="twitter:title" content="UI for VictoriaMetrics">
<meta name="twitter:site" content="@https://victoriametrics.com/">
<meta name="twitter:description" content="Explore and troubleshoot your VictoriaMetrics data">
<meta name="twitter:image" content="./preview.jpg">
<meta property="og:type" content="website">
<meta property="og:title" content="UI for VictoriaMetrics">
<meta property="og:url" content="https://victoriametrics.com/">
<meta property="og:description" content="Explore and troubleshoot your VictoriaMetrics data">
<script type="module" crossorigin src="./assets/index-DzehQsnZ.js"></script>
<link rel="modulepreload" crossorigin href="./assets/vendor-DojlIpLz.js">
<link rel="stylesheet" crossorigin href="./assets/vendor-D1GxaB_c.css">
<link rel="stylesheet" crossorigin href="./assets/index-Cqbobgy7.css">
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>

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

@@ -1,38 +0,0 @@
/*!
Copyright (c) 2018 Jed Watson.
Licensed under the MIT License (MIT), see
http://jedwatson.github.io/classnames
*/
/**
* @remix-run/router v1.19.2
*
* Copyright (c) Remix Software Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE.md file in the root directory of this source tree.
*
* @license MIT
*/
/**
* React Router DOM v6.26.2
*
* Copyright (c) Remix Software Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE.md file in the root directory of this source tree.
*
* @license MIT
*/
/**
* React Router v6.26.2
*
* Copyright (c) Remix Software Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE.md file in the root directory of this source tree.
*
* @license MIT
*/

View File

@@ -1,4 +1,4 @@
FROM golang:1.23.6 AS build-web-stage
FROM golang:1.24.0 AS build-web-stage
COPY build /build
WORKDIR /build

View File

@@ -1,22 +1,18 @@
import React, { FC, useEffect, useMemo, useState } from "preact/compat";
import { gradMetal16 } from "../../../../utils/uplot";
import { SeriesItem, LegendItemType } from "../../../../types";
import "./style.scss";
import LegendItem from "../../Line/Legend/LegendItem/LegendItem";
import { ChartTooltipProps } from "../../ChartTooltip/ChartTooltip";
interface LegendHeatmapProps {
min: number
max: number
legendValue: ChartTooltipProps | null,
series: SeriesItem[]
}
const LegendHeatmap: FC<LegendHeatmapProps> = ({
min,
max,
legendValue,
series
}) => {
const [percent, setPercent] = useState(0);
@@ -54,13 +50,6 @@ const LegendHeatmap: FC<LegendHeatmapProps> = ({
<div className="vm-legend-heatmap__value">{minFormat}</div>
<div className="vm-legend-heatmap__value">{maxFormat}</div>
</div>
{series[1] && (
<LegendItem
key={series[1]?.label}
legend={series[1] as LegendItemType}
isHeatmap
/>
)}
</div>
);
};

View File

@@ -1,8 +1,15 @@
import React, { FC, useMemo } from "preact/compat";
import React, { FC } from "preact/compat";
import { LegendItemType } from "../../../../types";
import LegendItem from "./LegendItem/LegendItem";
import Accordion from "../../../Main/Accordion/Accordion";
import "./style.scss";
import LegendGroup from "./LegendGroup";
import { useLegendGroup } from "./hooks/useLegendGroup";
import { useGroupSeries } from "./hooks/useGroupSeries";
export type QueryGroup = {
group: number | string;
items: LegendItemType[]
}
interface LegendProps {
labels: LegendItemType[];
@@ -12,42 +19,34 @@ interface LegendProps {
}
const Legend: FC<LegendProps> = ({ labels, query, isAnomalyView, onChange }) => {
const groups = useMemo(() => {
return Array.from(new Set(labels.map(l => l.group)));
}, [labels]);
const showQueryNum = groups.length > 1;
const { groupByLabel } = useLegendGroup();
const groupSeries = useGroupSeries({ labels, query, groupByLabel });
return <>
<div className="vm-legend">
{groups.map((group) => (
<div
className="vm-legend-group"
key={group}
>
<Accordion
defaultExpanded={true}
title={(
<div className="vm-legend-group-title">
{showQueryNum && (
<span className="vm-legend-group-title__count">Query {group}: </span>
)}
<span className="vm-legend-group-title__query">{query[group - 1]}</span>
</div>
)}
<div>
{groupSeries.map(({ group, items }) => (
<div
className="vm-legend-group"
key={group}
>
<div>
{labels.filter(l => l.group === group).sort((x, y) => (y.median || 0) - (x.median || 0)).map((legendItem: LegendItemType) =>
<LegendItem
key={legendItem.label}
legend={legendItem}
isAnomalyView={isAnomalyView}
onChange={onChange}
/>
<Accordion
defaultExpanded={true}
title={(
<div className="vm-legend-group-title">
Group by{groupByLabel ? "" : " query"}: <b>{group}</b>
</div>
)}
</div>
</Accordion>
</div>
))}
>
<LegendGroup
labels={items}
isAnomalyView={isAnomalyView}
onChange={onChange}
/>
</Accordion>
</div>
))}
</div>
</div>
</>;
};

View File

@@ -0,0 +1,104 @@
import React, { FC, useMemo } from "preact/compat";
import Switch from "../../../../Main/Switch/Switch";
import { LegendDisplayType, useLegendView } from "../hooks/useLegendView";
import { useHideDuplicateFields } from "../hooks/useHideDuplicateFields";
import { useShowStats } from "../hooks/useShowStats";
import TextField from "../../../../Main/TextField/TextField";
import { useLegendFormat } from "../hooks/useLegendFormat";
import { WITHOUT_GROUPING } from "../../../../../constants/logs";
import Select from "../../../../Main/Select/Select";
import { useLegendGroup } from "../hooks/useLegendGroup";
import "./style.scss";
import { MetricResult } from "../../../../../api/types";
type Props = {
data: MetricResult[]
}
const LegendConfigs: FC<Props> = ({ data }) => {
const { isTableView, onChange: onChangeView } = useLegendView();
const { hideDuplicates, onChange: onChangeDuplicates } = useHideDuplicateFields();
const { hideStats, onChange: onChangeStats } = useShowStats();
const { format, onChange: onChangeFormat, onApply: onApplyFormat } = useLegendFormat();
const { groupByLabel, onChange: onChangeGroup } = useLegendGroup();
const uniqueFields = useMemo(() => {
const fields = data.flatMap(d => Object.keys(d.metric));
return Array.from(new Set(fields));
}, [data]);
const handleChangeView = (val: boolean) => {
const value = val ? LegendDisplayType.table : LegendDisplayType.lines;
onChangeView(value);
};
return (
<>
<div className="vm-legend-configs-item vm-legend-configs-item_switch">
<span className="vm-legend-configs-item__label">Table View</span>
<Switch
label={isTableView ? "Enabled" : "Disabled"}
value={isTableView}
onChange={handleChangeView}
/>
<span className="vm-legend-configs-item__info">
Switches between table and lines view.
</span>
</div>
<div className="vm-legend-configs-item vm-legend-configs-item_switch">
<span className="vm-legend-configs-item__label">Common Labels</span>
<Switch
label={hideDuplicates ? "Hide" : "Show"}
value={!hideDuplicates}
onChange={onChangeDuplicates}
/>
<span className="vm-legend-configs-item__info">
Shows or hides labels that are the same for all series.
</span>
</div>
<div className="vm-legend-configs-item vm-legend-configs-item_switch">
<span className="vm-legend-configs-item__label">Statistics</span>
<Switch
label={hideStats ? "Hide" : "Show"}
value={!hideStats}
onChange={onChangeStats}
/>
<span className="vm-legend-configs-item__info">
Displays min, median, and max values.
</span>
</div>
<div className="vm-legend-configs-item">
<TextField
label="Custom Legend Format"
placeholder={"{{label_name}}"}
value={format}
onChange={onChangeFormat}
onBlur={onApplyFormat}
onEnter={onApplyFormat}
/>
<span className="vm-legend-configs-item__info vm-legend-configs-item__info_input">
Customize legend labels with text and &#123;&#123;label_name&#125;&#125; placeholders.
</span>
</div>
<div className="vm-legend-configs-item">
<Select
label="Group Legend By"
value={groupByLabel}
list={[WITHOUT_GROUPING, ...uniqueFields]}
placeholder={WITHOUT_GROUPING}
onChange={onChangeGroup}
searchable
/>
<span className="vm-legend-configs-item__info">
Choose a label to group the legend. By default, legends are grouped by query.
</span>
</div>
</>
);
};
export default LegendConfigs;

View File

@@ -0,0 +1,26 @@
@use "src/styles/variables" as *;
.vm-legend-configs {
&-item {
display: grid;
align-items: center;
justify-content: stretch;
margin-top: calc($padding-global/2);
&_switch {
grid-template-columns: 1fr 100px;
margin-top: 0;
}
&__info {
margin-top: calc($padding-global/2);
font-size: $font-size-small;
color: $color-text-secondary;
line-height: 130%;
&_input {
margin-top: 0;
}
}
}
}

View File

@@ -0,0 +1,44 @@
import React, { FC, useMemo } from "react";
import { LegendItemType } from "../../../../types";
import { useLegendView } from "./hooks/useLegendView";
import LegendLines from "./LegendViews/LegendLines";
import LegendTable from "./LegendViews/LegendTable";
import { useHideDuplicateFields } from "./hooks/useHideDuplicateFields";
export type LegendProps = {
labels: LegendItemType[];
isAnomalyView?: boolean;
duplicateFields?: string[];
onChange: (item: LegendItemType, metaKey: boolean) => void;
}
const LegendGroup: FC<LegendProps> = ({ labels, isAnomalyView, onChange }) => {
const { isTableView } = useLegendView();
const { duplicateFields } = useHideDuplicateFields(labels);
const sortedLabels = useMemo(() => {
return labels.sort((x, y) => (y.median || 0) - (x.median || 0));
}, [labels]);
if (isTableView) {
return (
<LegendTable
labels={sortedLabels}
isAnomalyView={isAnomalyView}
duplicateFields={duplicateFields}
onChange={onChange}
/>
);
}
return (
<LegendLines
labels={sortedLabels}
isAnomalyView={isAnomalyView}
duplicateFields={duplicateFields}
onChange={onChange}
/>
);
};
export default LegendGroup;

View File

@@ -6,36 +6,41 @@ import classNames from "classnames";
import { getFreeFields } from "./helpers";
import useCopyToClipboard from "../../../../../hooks/useCopyToClipboard";
import { STATS_ORDER } from "../../../../../constants/graph";
import { useShowStats } from "../hooks/useShowStats";
import { useLegendFormat } from "../hooks/useLegendFormat";
import { getLabelAlias } from "../../../../../utils/metric";
interface LegendItemProps {
legend: LegendItemType;
onChange?: (item: LegendItemType, metaKey: boolean) => void;
isHeatmap?: boolean;
isAnomalyView?: boolean;
duplicateFields?: string[];
}
const LegendItem: FC<LegendItemProps> = ({ legend, onChange, isHeatmap, isAnomalyView }) => {
const LegendItem: FC<LegendItemProps> = ({ legend, onChange, duplicateFields, isAnomalyView }) => {
const copyToClipboard = useCopyToClipboard();
const { hideStats } = useShowStats();
const { format } = useLegendFormat();
const formattedLabel = getLabelAlias(legend.freeFormFields, format);
const freeFormFields = useMemo(() => {
const result = getFreeFields(legend);
return isHeatmap ? result.filter(f => f.key !== "vmrange") : result;
}, [legend, isHeatmap]);
return duplicateFields?.length
? result.filter(f => !duplicateFields.includes(f.key))
: result;
}, [legend, duplicateFields]);
const statsFormatted = legend.statsFormatted;
const showStats = Object.values(statsFormatted).some(v => v);
const handleClickFreeField = async (val: string) => {
await copyToClipboard(val, `${val} has been copied`);
};
const createHandlerClick = (legend: LegendItemType) => (e: MouseEvent<HTMLDivElement>) => {
onChange && onChange(legend, e.ctrlKey || e.metaKey);
};
const createHandlerCopy = (freeField: string) => (e: MouseEvent<HTMLDivElement>) => {
const createHandlerCopy = (freeField: string) => async (e: MouseEvent<HTMLDivElement>) => {
e.stopPropagation();
handleClickFreeField(freeField);
await copyToClipboard(freeField, `${freeField} has been copied`);
};
return (
@@ -43,12 +48,11 @@ const LegendItem: FC<LegendItemProps> = ({ legend, onChange, isHeatmap, isAnomal
className={classNames({
"vm-legend-item": true,
"vm-legend-row": true,
"vm-legend-item_hide": !legend.checked && !isHeatmap,
"vm-legend-item_static": isHeatmap,
"vm-legend-item_hide": !legend.checked,
})}
onClick={createHandlerClick(legend)}
>
{!isAnomalyView && !isHeatmap && (
{!isAnomalyView && (
<div
className="vm-legend-item__marker"
style={{ backgroundColor: legend.color }}
@@ -56,10 +60,12 @@ const LegendItem: FC<LegendItemProps> = ({ legend, onChange, isHeatmap, isAnomal
)}
<div className="vm-legend-item-info">
<span className="vm-legend-item-info__label">
{legend.hasAlias ? legend.label : (
{legend.hasAlias && legend.label}
{!legend.hasAlias && format && formattedLabel}
{!legend.hasAlias && !format && (
<>
{legend.freeFormFields["__name__"]}
{!!freeFormFields.length && <>&#123;</>}
{!!freeFormFields.length && <> &#123;</>}
{freeFormFields.map((f, i) => (
<span
className="vm-legend-item-info__free-fields"
@@ -75,7 +81,7 @@ const LegendItem: FC<LegendItemProps> = ({ legend, onChange, isHeatmap, isAnomal
)}
</span>
</div>
{!isHeatmap && showStats && (
{!hideStats && showStats && (
<div className="vm-legend-item-stats">
{STATS_ORDER.map((key, i) => (
<div

View File

@@ -18,18 +18,7 @@
&_hide {
text-decoration: line-through;
opacity: 0.5;
}
&_static {
grid-template-columns: 1fr;
margin: 0;
padding: 0;
cursor: default;
&:hover {
background-color: $color-background-block;
}
opacity: 0.2;
}
&__marker {

View File

@@ -0,0 +1,22 @@
import React, { FC } from "preact/compat";
import LegendItem from "../LegendItem/LegendItem";
import { LegendProps } from "../LegendGroup";
const LegendLines: FC<LegendProps> = ({ labels, isAnomalyView, duplicateFields, onChange }) => {
return (
<div className="vm-legend-item-container">
{labels.map((legendItem) =>
<LegendItem
key={legendItem.label}
legend={legendItem}
isAnomalyView={isAnomalyView}
duplicateFields={duplicateFields}
onChange={onChange}
/>
)}
</div>
);
};
export default LegendLines;

View File

@@ -0,0 +1,87 @@
import React, { FC, useMemo } from "preact/compat";
import { LegendProps } from "../LegendGroup";
import "./style.scss";
import { LegendItemType } from "../../../../../types";
import { MouseEvent } from "react";
import classNames from "classnames";
import get from "lodash.get";
import { STATS_ORDER } from "../../../../../constants/graph";
import { useShowStats } from "../hooks/useShowStats";
const statsColumns = STATS_ORDER.map(k => ({
key: `statsFormatted.${k}`,
title: k
}));
const LegendTable: FC<LegendProps> = ({ labels, duplicateFields, onChange }) => {
const { hideStats } = useShowStats();
const stats = hideStats ? [] : statsColumns;
const fields = useMemo(() => {
const fields = [...new Set(labels.flatMap(item => Object.keys(item.freeFormFields)))]
.map(f => ({ key: `freeFormFields.${f}`, title: f }));
return duplicateFields?.length
? fields.filter(f => !duplicateFields.includes(f.title))
: fields;
}, [labels, duplicateFields]);
const columns = fields.concat(stats);
const createHandlerClick = (legend: LegendItemType) => (e: MouseEvent<HTMLTableRowElement>) => {
onChange && onChange(legend, e.ctrlKey || e.metaKey);
};
return (
<div className="vm-legend-table__wrapper">
<table className="vm-legend-table">
<thead className="vm-legend-table-thead">
<tr className="vm-legend-table-row vm-legend-table_thead">
<th className="vm-legend-table-col vm-legend-table-col_marker vm-legend-table-col_thead"/>
{columns.map((col) => (
<th
key={col.key}
className="vm-legend-table-col vm-legend-table-col_thead"
>
{col.title}
</th>
))}
</tr>
</thead>
<tbody className="vm-legend-table-tbody">
{labels.map(row => (
<tr
key={row.label}
className={classNames({
"vm-legend-table-row": true,
"vm-legend-table-row_tbody": true,
"vm-legend-table-row_exclude": !row.checked
})}
onClick={createHandlerClick(row)}
>
<td className="vm-legend-table-col vm-legend-table-col_marker">
<div
className="vm-legend-item__marker"
style={{ backgroundColor: row.color }}
/>
</td>
{columns.map((col) => (
<td
key={`${col.key}_${row.label}`}
className="vm-legend-table-col"
>
<span className="vm-legend-table-col__content">
{get(row, col.key)}
</span>
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
);
};
export default LegendTable;

View File

@@ -0,0 +1,64 @@
@use "src/styles/variables" as *;
.vm-legend-table {
table-layout: auto;
max-width: 100%;
font-size: $font-size-small;
&__wrapper {
width: 100%;
max-height: 50vh;
overflow: auto;
}
&-row {
&_tbody {
cursor: pointer;
transition: 0.3s ease;
&:hover {
background-color: rgba(0, 0, 0, 0.1);
}
}
&_exclude {
text-decoration: line-through;
opacity: 0.2;
}
}
&-col {
vertical-align: middle;
text-align: left;
padding: calc($padding-global / 2);
overflow-wrap: anywhere;
&_thead {
position: sticky;
top: 0;
left: calc(14px + $padding-global);
font-weight: bold;
z-index: 2;
white-space: nowrap;
background: $color-background-block;
}
&_marker {
position: sticky;
left: 0;
width: calc(14px + $padding-global);
padding: 0 calc($padding-global / 2);
}
&_marker:not(th) {
z-index: 1;
}
&__content {
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
}

View File

@@ -0,0 +1,38 @@
import { useMemo } from "preact/compat";
import { LegendItemType } from "../../../../../types";
import { QueryGroup } from "../Legend";
type Props = {
labels: LegendItemType[];
query: string[];
groupByLabel?: string;
}
export const useGroupSeries = ({ labels, query, groupByLabel }: Props) => {
return useMemo(() => {
return getGroupSeries(labels, query, groupByLabel);
}, [labels, query, groupByLabel]);
};
const getGroupSeries = (
labels: LegendItemType[],
query: string[],
groupByLabel?: string
): QueryGroup[] => {
const groupMap = new Map<string | number, QueryGroup>();
for (const label of labels) {
const groupKey = groupByLabel
? `${groupByLabel}="${label.freeFormFields[groupByLabel] ?? ""}"`
: `${query[label.group - 1]}`;
if (!groupMap.has(groupKey)) {
groupMap.set(groupKey, { group: String(groupKey), items: [] });
}
const groupEntry = groupMap.get(groupKey) as QueryGroup;
groupEntry.items.push(label);
}
return Array.from(groupMap.values());
};

View File

@@ -0,0 +1,49 @@
import { useEffect, useMemo, useState } from "preact/compat";
import { useSearchParams } from "react-router-dom";
import { LegendQueryParams } from "../types";
import { LegendItemType } from "../../../../../types";
const urlKey = LegendQueryParams.hideDuplicates;
export const useHideDuplicateFields = (labels?: LegendItemType[]) => {
const [searchParams, setSearchParams] = useSearchParams();
const valueFromUrl = searchParams.get(urlKey) === "true";
const [hideDuplicates, setHideDuplicates] = useState(valueFromUrl);
const onChange = (show: boolean) => {
if (!show) {
searchParams.set(urlKey, "true");
} else {
searchParams.delete(urlKey);
}
setHideDuplicates(!show);
setSearchParams(searchParams);
};
useEffect(() => {
const value = searchParams.get(urlKey) === "true";
if (value !== hideDuplicates) {
setHideDuplicates(value);
}
}, [searchParams]);
const duplicateFields = useMemo(() => {
if (!hideDuplicates || !labels?.length || labels?.length < 2) {
return [];
}
const allKeys = [...new Set(labels.flatMap(l => Object.keys(l.freeFormFields || {})))];
return allKeys.filter(key => {
const firstValue = labels.find(l => l.freeFormFields[key])?.freeFormFields[key];
return labels.every(l => l.freeFormFields[key] === firstValue);
});
}, [labels, hideDuplicates]);
return {
hideDuplicates,
onChange,
duplicateFields
};
};

View File

@@ -0,0 +1,38 @@
import { useEffect, useState } from "preact/compat";
import { useSearchParams } from "react-router-dom";
import { LegendQueryParams } from "../types";
const urlKey = LegendQueryParams.format;
export const useLegendFormat = () => {
const [searchParams, setSearchParams] = useSearchParams();
const valueFromUrl = searchParams.get(urlKey) || "";
const [format, setFormat] = useState(valueFromUrl);
const onChange = (value: string) => {
setFormat(value);
};
const onApply = () => {
if (format) {
searchParams.set(urlKey, format);
} else {
searchParams.delete(urlKey);
}
setSearchParams(searchParams);
};
useEffect(() => {
const value = searchParams.get(urlKey);
if (value !== format) {
setFormat(value || "");
}
}, [searchParams]);
return {
format,
onChange,
onApply
};
};

View File

@@ -0,0 +1,34 @@
import { useEffect, useState } from "preact/compat";
import { useSearchParams } from "react-router-dom";
import { LegendQueryParams } from "../types";
import { WITHOUT_GROUPING } from "../../../../../constants/logs";
const urlKey = LegendQueryParams.group;
export const useLegendGroup = () => {
const [searchParams, setSearchParams] = useSearchParams();
const valueFromUrl = searchParams.get(urlKey) || "";
const [groupByLabel, setGroupByLabel] = useState(valueFromUrl);
const onChange =(value: string) => {
if (value && value !== WITHOUT_GROUPING) {
searchParams.set(urlKey, value);
} else {
searchParams.delete(urlKey);
}
setGroupByLabel(value);
setSearchParams(searchParams);
};
useEffect(() => {
const value = searchParams.get(urlKey);
if (value !== groupByLabel) {
setGroupByLabel(value || "");
}
}, [searchParams]);
return {
groupByLabel,
onChange,
};
};

View File

@@ -0,0 +1,41 @@
import { useEffect, useState } from "preact/compat";
import { useSearchParams } from "react-router-dom";
import { LegendQueryParams } from "../types";
export enum LegendDisplayType {
table = "table",
lines = "lines"
}
const urlKey = LegendQueryParams.view;
export const useLegendView = () => {
const [searchParams, setSearchParams] = useSearchParams();
const valueFromUrl = searchParams.get(urlKey) as LegendDisplayType;
const [view, setView] = useState<LegendDisplayType>(valueFromUrl || LegendDisplayType.lines);
const onChange = (type: LegendDisplayType) => {
if (type === LegendDisplayType.table) {
searchParams.set(urlKey, type);
} else {
searchParams.delete(urlKey);
}
setView(type);
setSearchParams(searchParams);
};
useEffect(() => {
const value = searchParams.get(urlKey);
if (value !== view) {
setView(value as LegendDisplayType);
}
}, [searchParams]);
return {
view,
isLinesView: view === LegendDisplayType.lines,
isTableView: view === LegendDisplayType.table,
onChange
};
};

View File

@@ -0,0 +1,34 @@
import { useEffect, useState } from "preact/compat";
import { useSearchParams } from "react-router-dom";
import { LegendQueryParams } from "../types";
const urlKey = LegendQueryParams.hideStats;
export const useShowStats = () => {
const [searchParams, setSearchParams] = useSearchParams();
const valueFromUrl = searchParams.get(urlKey) === "true";
const [hideStats, setHideStats] = useState(valueFromUrl);
const onChange = (showName: boolean) => {
if (!showName) {
searchParams.set(urlKey, "true");
} else {
searchParams.delete(urlKey);
}
setHideStats(!showName);
setSearchParams(searchParams);
};
useEffect(() => {
const value = searchParams.get(urlKey) === "true";
if (value !== hideStats) {
setHideStats(value);
}
}, [searchParams]);
return {
hideStats,
onChange
};
};

View File

@@ -3,8 +3,9 @@
.vm-legend {
position: relative;
display: flex;
flex-wrap: wrap;
flex-direction: column;
cursor: default;
width: 100%;
&-group {
min-width: 23%;
@@ -17,13 +18,12 @@
padding: $padding-small;
margin-bottom: 1px;
border-bottom: $border-divider;
font-size: $font-size-small;
color: $color-text-secondary;
&__count {
font-weight: bold;
margin-right: $padding-small;
}
&__query {
b {
margin-left: $padding-small;
color: $color-text;
}
}
}

View File

@@ -0,0 +1,7 @@
export enum LegendQueryParams {
view = "legend_view",
hideDuplicates = "legend_hide_duplicates",
hideStats = "legend_hide_stats",
format = "legend_format",
group = "legend_group"
}

View File

@@ -41,10 +41,13 @@ const AxesLimitsConfigurator: FC<AxesLimitsConfiguratorProps> = ({ yaxis, setYax
<Switch
value={yaxis.limits.enable}
onChange={toggleEnableLimits}
label={`${yaxis.limits.enable ? "Fixed" : "Auto"} limits`}
label={yaxis.limits.enable ? "Enabled" : "Disabled"}
fullWidth={isMobile}
/>
</div>
<span className="vm-legend-configs-item__info">
Enables manual setting of min and max values for the y-axis.
</span>
{yaxis.limits.enable && (
<div className="vm-axes-limits-list">
{axes.map(axis => (

View File

@@ -3,8 +3,6 @@
.vm-axes-limits {
display: grid;
align-items: center;
gap: $padding-global;
max-width: 300px;
&_mobile {
width: 100%;
@@ -23,8 +21,9 @@
&__inputs {
display: grid;
grid-template-columns: repeat(2, 120px);
grid-template-columns: repeat(2, auto);
gap: $padding-small;
margin-top: $padding-global;
}
}
}

View File

@@ -3,7 +3,6 @@ import AxesLimitsConfigurator from "./AxesLimitsConfigurator/AxesLimitsConfigura
import { AxisRange, YaxisState } from "../../../state/graph/reducer";
import { SettingsIcon } from "../../Main/Icons";
import Button from "../../Main/Button/Button";
import Popper from "../../Main/Popper/Popper";
import "./style.scss";
import Tooltip from "../../Main/Tooltip/Tooltip";
import useBoolean from "../../../hooks/useBoolean";
@@ -11,6 +10,8 @@ import LinesConfigurator from "./LinesConfigurator/LinesConfigurator";
import GraphTypeSwitcher from "./GraphTypeSwitcher/GraphTypeSwitcher";
import { MetricResult } from "../../../api/types";
import { isHistogramData } from "../../../utils/metric";
import LegendConfigs from "../../Chart/Line/Legend/LegendConfigs/LegendConfigs";
import Modal from "../../Main/Modal/Modal";
const title = "Graph settings";
@@ -32,8 +33,8 @@ const GraphSettings: FC<GraphSettingsProps> = ({ data, yaxis, setYaxisLimits, to
const displayHistogramMode = isHistogramData(data);
const {
value: openPopper,
toggle: toggleOpen,
value: isOpenSettings,
setTrue: handleOpen,
setFalse: handleClose,
} = useBoolean(false);
@@ -44,36 +45,46 @@ const GraphSettings: FC<GraphSettingsProps> = ({ data, yaxis, setYaxisLimits, to
<Button
variant="text"
startIcon={<SettingsIcon/>}
onClick={toggleOpen}
onClick={handleOpen}
ariaLabel="settings"
/>
</div>
</Tooltip>
<Popper
open={openPopper}
buttonRef={buttonRef}
placement="bottom-right"
onClose={handleClose}
title={title}
>
<div
className="vm-graph-settings-popper"
ref={popperRef}
{isOpenSettings && (
<Modal
onClose={handleClose}
title={title}
className="vm-graph-settings-modal"
>
<div className="vm-graph-settings-popper__body">
<AxesLimitsConfigurator
yaxis={yaxis}
setYaxisLimits={setYaxisLimits}
toggleEnableLimits={toggleEnableLimits}
/>
<LinesConfigurator
spanGaps={spanGaps.value}
onChange={spanGaps.onChange}
/>
{displayHistogramMode && <GraphTypeSwitcher onChange={handleClose}/>}
<div
className="vm-graph-settings-body"
ref={popperRef}
>
<div className="vm-graph-settings-body-section">
<h3 className="vm-graph-settings-body-section__title">Chart Options</h3>
<div className="vm-graph-settings-body-section-content">
<AxesLimitsConfigurator
yaxis={yaxis}
setYaxisLimits={setYaxisLimits}
toggleEnableLimits={toggleEnableLimits}
/>
<LinesConfigurator
spanGaps={spanGaps.value}
onChange={spanGaps.onChange}
/>
{displayHistogramMode && <GraphTypeSwitcher onChange={handleClose}/>}
</div>
</div>
<div className="vm-graph-settings-body-section">
<h3 className="vm-graph-settings-body-section__title">Legend Options</h3>
<div className="vm-graph-settings-body-section-content">
<LegendConfigs data={data}/>
</div>
</div>
</div>
</div>
</Popper>
</Modal>
)}
</div>
);
};

View File

@@ -19,6 +19,9 @@ const LinesConfigurator: FC<Props> = ({ spanGaps, onChange }) => {
label={spanGaps ? "Enabled" : "Disabled"}
fullWidth={isMobile}
/>
<span className="vm-legend-configs-item__info">
Connects data points by skipping null values instead of creating gaps.
</span>
</div>
);
};

View File

@@ -5,27 +5,46 @@
align-items: center;
gap: $padding-small;
&-popper {
&-modal {
.vm-modal-content-body {
padding: 0;
}
.vm-modal-content-header {
margin-bottom: 0;
}
}
&-body {
display: grid;
gap: $padding-global;
padding: $padding-small $padding-large $padding-large;
min-width: 300px;
&__body {
display: grid;
gap: $padding-large;
&-section {
border-bottom: $border-divider;
&:last-child {
border-bottom: none;
padding-bottom: $padding-large;
}
&__title {
padding: $padding-global;
font-weight: bold;
background: $color-hover-black;
}
&-content {
display: grid;
gap: $padding-large;
padding: $padding-large;
}
}
}
&-row {
display: grid;
gap: $padding-small;
grid-template-columns: minmax(150px, max-content) 1fr;
&__label {
&:after{
content: ":";
}
}
align-items: center;
justify-content: stretch;
grid-template-columns: 1fr 100px;
}
}

View File

@@ -90,6 +90,17 @@ export const InfoIcon = () => (
</svg>
);
export const InfoOutlinedIcon = () => (
<svg
viewBox="0 0 24 24"
fill="currentColor"
>
<path
d="M11 7h2v2h-2zm0 4h2v6h-2zm1-9C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2m0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8"
></path>
</svg>
);
export const WarningIcon = () => (
<svg
viewBox="0 0 24 24"

View File

@@ -1,7 +1,12 @@
import React from "preact/compat";
import "./style.scss";
const LineProgress = ({ value }: {value: number}) => (
type Props = {
value: number;
hideValue?: boolean;
}
const LineProgress = ({ value, hideValue }: Props) => (
<div className="vm-line-progress">
<div className="vm-line-progress-track">
<div
@@ -9,7 +14,9 @@ const LineProgress = ({ value }: {value: number}) => (
style={{ width: `${value}%` }}
/>
</div>
<span>{value.toFixed(2)}%</span>
{!hideValue && (
<span>{value.toFixed(2)}%</span>
)}
</div>
);

View File

@@ -10,7 +10,7 @@
&-track {
width: 100%;
height: 20px;
height: 12px;
background-color: $color-hover-black;
border-radius: $border-radius-small;

View File

@@ -266,7 +266,6 @@ const GraphView: FC<GraphViewProps> = ({
)}
{isHistogram && showLegend && (
<LegendHeatmap
series={series as SeriesItem[]}
min={yaxis.limits.range[1][0] || 0}
max={yaxis.limits.range[1][1] || 0}
legendValue={legendValue}

View File

@@ -1,5 +1,5 @@
import React, { FC } from "preact/compat";
import { InfoIcon } from "../../../components/Main/Icons";
import { InfoOutlinedIcon } from "../../../components/Main/Icons";
import Tooltip from "../../../components/Main/Tooltip/Tooltip";
import { TopHeapEntry } from "../types";
import { useSearchParams } from "react-router-dom";
@@ -71,12 +71,12 @@ const CardinalityTotals: FC<CardinalityTotalsProps> = ({
key={title}
>
<h4 className="vm-cardinality-totals-card__title">
{title}
{info && (
<Tooltip title={<p className="vm-cardinality-totals-card__tooltip">{info}</p>}>
<div className="vm-cardinality-totals-card__info-icon"><InfoIcon/></div>
<div className="vm-cardinality-totals-card__info-icon"><InfoOutlinedIcon/></div>
</Tooltip>
)}
{title}
</h4>
<span className="vm-cardinality-totals-card__value">{value}</span>
{!!dynamic && (

View File

@@ -5,7 +5,7 @@
flex-wrap: wrap;
align-content: flex-start;
justify-content: flex-start;
gap: $padding-medium;
gap: $padding-large;
flex-grow: 1;
&_mobile {
@@ -16,12 +16,12 @@
&-card {
display: grid;
grid-template-columns: auto 1fr;
align-items: center;
align-items: flex-end;
justify-content: center;
gap: $padding-small 4px;
&__info-icon {
width: 12px;
width: 15px;
display: flex;
align-items: center;
justify-content: center;

View File

@@ -5,7 +5,7 @@ import { Data, HeadCell } from "../Table/types";
import { MutableRef } from "preact/hooks";
import Tabs from "../../../components/Main/Tabs/Tabs";
import { useMemo, useState } from "preact/compat";
import { ChartIcon, InfoIcon, TableIcon } from "../../../components/Main/Icons";
import { ChartIcon, InfoIcon, InfoOutlinedIcon, TableIcon } from "../../../components/Main/Icons";
import "./style.scss";
import classNames from "classnames";
import useDeviceDetect from "../../../hooks/useDeviceDetect";
@@ -81,7 +81,9 @@ const MetricsContent: FC<MetricsProperties> = ({
className="vm-metrics-content-header__tip"
/>}
>
<div className="vm-metrics-content-header__tip-icon"><InfoIcon/></div>
<div className="vm-metrics-content-header__tip-icon">
<InfoOutlinedIcon/>
</div>
</Tooltip>
)}
{sectionTitle}

View File

@@ -1,8 +1,11 @@
@use "src/styles/variables" as *;
.vm-metrics-content {
border: $border-divider;
&-header {
margin: -$padding-medium 0-$padding-medium 0;
background-color: $color-hover-black;
&__title {
display: flex;
@@ -21,7 +24,7 @@
}
&__tip-icon {
width: 12px;
width: 15px;
display: flex;
align-items: center;
justify-content: center;
@@ -36,7 +39,7 @@
&__table {
padding-top: $padding-medium;
width: calc(100vw - ($padding-medium * 4) - var(--scrollbar-width));
width: calc(100vw - ($padding-large * 4) - var(--scrollbar-width));
overflow: auto;
@media (max-width: 768px) {
@@ -50,6 +53,12 @@
.vm-table-cell_header {
white-space: nowrap;
}
th,
td {
vertical-align: middle;
height: 33px;
}
}
&_mobile &__table {

View File

@@ -23,18 +23,21 @@ const TableCells: FC<CardinalityTableCells> = ({
const progressPrev = totalSeriesPrev > 0 ? row.valuePrev / totalSeriesPrev * 100 : -1;
const hasProgresses = [progress, progressPrev].some(p => p === -1);
const diffPercent = progress - progressPrev;
const relationPrevDay = hasProgresses ? "" : `${diffPercent.toFixed(2)}%`;
const formattedValue = row.value.toLocaleString("en-US");
const formattedPrevValue = row.valuePrev.toLocaleString("en-US");
const formattedDiff = Math.abs(row.diff).toLocaleString("en-US");
const diffPercentByTotal = progress - progressPrev;
const relationPrevDay = hasProgresses ? "" : `${Math.abs(diffPercentByTotal).toFixed(2)}%`;
const diffPercent = `${Math.abs(row.diffPercent).toFixed(2)}%`;
const handleActionClick = () => {
onActionClick(row.name);
};
return <>
<td
className="vm-table-cell"
key={row.name}
>
<td className="vm-table-cell">
<span
className={"vm-link vm-link_colored"}
onClick={handleActionClick}
@@ -42,62 +45,71 @@ const TableCells: FC<CardinalityTableCells> = ({
{row.name}
</span>
</td>
<td
className="vm-table-cell"
key={row.value}
>
{row.value}
{!!row.diff && (
<Tooltip title={`in relation to the previous day: ${row.valuePrev}`}>
<span
className={classNames({
"vm-dynamic-number": true,
"vm-dynamic-number_positive": row.diff < 0,
"vm-dynamic-number_negative": row.diff > 0,
})}
>
&nbsp;{row.diff > 0 ? "+" : ""}{row.diff}
</span>
</Tooltip>
)}
<td className="vm-table-cell vm-table-cell_compact">
{formattedValue}
</td>
<td className="vm-table-cell vm-table-cell_compact">
<Tooltip title={`in relation to the previous day: ${formattedPrevValue}`}>
<span
className={classNames({
"vm-dynamic-number": true,
"vm-dynamic-number_positive vm-dynamic-number_down": row.diff < 0,
"vm-dynamic-number_negative vm-dynamic-number_up": row.diff > 0,
})}
>
{formattedDiff}
</span>
</Tooltip>
</td>
<td className="vm-table-cell vm-table-cell_compact">
<Tooltip title={`in relation to the previous day: ${formattedPrevValue}`}>
<div
className={classNames({
"vm-dynamic-number": true,
"vm-dynamic-number_positive vm-dynamic-number_down": row.diff < 0,
"vm-dynamic-number_negative vm-dynamic-number_up": row.diff > 0,
})}
>
{diffPercent}
</div>
</Tooltip>
</td>
{progress > 0 && (
<td
className="vm-table-cell"
key={row.progressValue}
>
<td className="vm-table-cell">
<div className="vm-cardinality-panel-table__progress">
<LineProgress value={progress}/>
{relationPrevDay && (
<Tooltip title={"in relation to the previous day"}>
<span
className={classNames({
"vm-dynamic-number": true,
"vm-dynamic-number_positive vm-dynamic-number_down": diffPercent < 0,
"vm-dynamic-number_negative vm-dynamic-number_up": diffPercent > 0,
})}
>
{relationPrevDay}
</span>
</Tooltip>
)}
<LineProgress
value={progress}
hideValue
/>
<span className="vm-dynamic-number vm-dynamic-number_static">
{progress.toFixed(2)}%
</span>
<Tooltip title={"in relation to the previous day"}>
<span
className={classNames({
"vm-dynamic-number": true,
"vm-dynamic-number_no-change": diffPercentByTotal === 0,
"vm-dynamic-number_positive vm-dynamic-number_down": diffPercentByTotal < 0,
"vm-dynamic-number_negative vm-dynamic-number_up": diffPercentByTotal > 0,
})}
>
{relationPrevDay}
</span>
</Tooltip>
</div>
</td>
)}
<td
className="vm-table-cell vm-table-cell_right"
key={"action"}
>
<div className="vm-table-cell__content">
<Tooltip title={`Filter by ${row.name}`}>
<Tooltip title={<span>Filter by <code>`{row.name}`</code></span>}>
<Button
variant="text"
size="small"
onClick={handleActionClick}
>
<PlayCircleOutlineIcon/>
</Button>
startIcon={<PlayCircleOutlineIcon/>}
/>
</Tooltip>
</div>
</td>

View File

@@ -2,7 +2,7 @@ import { MouseEvent } from "react";
import React from "preact/compat";
import { Data, EnhancedHeaderTableProps } from "./types";
import classNames from "classnames";
import { ArrowDropDownIcon, InfoIcon } from "../../../components/Main/Icons";
import { ArrowDropDownIcon, InfoOutlinedIcon } from "../../../components/Main/Icons";
import Tooltip from "../../../components/Main/Tooltip/Tooltip";
export function EnhancedTableHead(props: EnhancedHeaderTableProps) {
@@ -18,21 +18,22 @@ export function EnhancedTableHead(props: EnhancedHeaderTableProps) {
<th
className={classNames({
"vm-table-cell vm-table-cell_header": true,
"vm-table-cell_sort": headCell.id !== "action" && headCell.id !== "percentage",
"vm-table-cell_sort": headCell.sortable,
"vm-table-cell_right": headCell.id === "action",
})}
key={headCell.id}
onClick={createSortHandler(headCell.id as keyof Data)}
onClick={headCell.sortable ? createSortHandler(headCell.id as keyof Data) : undefined}
>
<div className="vm-table-cell__content">
{
headCell.info ?
<Tooltip title={headCell.info}>
<div className="vm-metrics-content-header__tip-icon"><InfoIcon /></div>
{headCell.label}
</Tooltip>: <>{headCell.label}</>
}
{headCell.id !== "action" && headCell.id !== "percentage" && (
{headCell.info && (
<Tooltip title={headCell.info}>
<div className="vm-metrics-content-header__tip-icon">
<InfoOutlinedIcon/>
</div>
</Tooltip>
)}
{headCell.label}
{headCell.sortable && (
<div
className={classNames({
"vm-table__sort-icon": true,

View File

@@ -6,6 +6,7 @@ export interface HeadCell {
id: string;
label: string | ReactNode;
info?: string;
sortable?: boolean;
}
export interface EnhancedHeaderTableProps {
@@ -29,6 +30,7 @@ export interface Data {
name: string;
value: number;
diff: number;
diffPercent: number;
valuePrev: number;
progressValue: number;
actions: string;

View File

@@ -2,6 +2,14 @@ import { Containers, Tabs, TSDBStatus } from "./types";
import { useRef } from "preact/compat";
import { HeadCell } from "./Table/types";
import {
METRIC_NAMES_HEADERS,
LABEL_NAMES_HEADERS,
FOCUS_LABEL_VALUES_HEADERS,
LABEL_VALUE_PAIRS_HEADERS,
LABEL_NAMES_WITH_UNIQUE_VALUES_HEADERS,
} from "./appTableHeaders";
interface AppState {
tabs: Tabs;
containerRefs: Containers<HTMLDivElement>;
@@ -161,99 +169,3 @@ export default class AppConfigurator {
return prev ? this.tsdbStatus.totalSeriesPrev : this.tsdbStatus.totalSeries;
}
}
const METRIC_NAMES_HEADERS = [
{
id: "name",
label: "Metric name",
},
{
id: "value",
label: "Number of series",
},
{
id: "percentage",
label: "Share in total",
info: "Shows the share of a metric to the total number of series"
},
{
id: "action",
label: "",
}
] as HeadCell[];
const LABEL_NAMES_HEADERS = [
{
id: "name",
label: "Label name",
},
{
id: "value",
label: "Number of series",
},
{
id: "percentage",
label: "Share in total",
info: "Shows the share of the label to the total number of series"
},
{
id: "action",
label: "",
}
] as HeadCell[];
const FOCUS_LABEL_VALUES_HEADERS = [
{
id: "name",
label: "Label value",
},
{
id: "value",
label: "Number of series",
},
{
id: "percentage",
label: "Share in total",
},
{
disablePadding: false,
id: "action",
label: "",
numeric: false,
}
] as HeadCell[];
export const LABEL_VALUE_PAIRS_HEADERS = [
{
id: "name",
label: "Label=value pair",
},
{
id: "value",
label: "Number of series",
},
{
id: "percentage",
label: "Share in total",
info: "Shows the share of the label value pair to the total number of series"
},
{
id: "action",
label: "",
}
] as HeadCell[];
export const LABEL_NAMES_WITH_UNIQUE_VALUES_HEADERS = [
{
id: "name",
label: "Label name",
},
{
id: "value",
label: "Number of unique values",
},
{
id: "action",
label: "",
}
] as HeadCell[];

View File

@@ -0,0 +1,119 @@
import { HeadCell } from "./Table/types";
const actionHeader = {
id: "action",
label: "",
};
const diffHeader = {
id: "diff",
label: "1d",
info: "Shows the absolute difference compared to the previous day.",
sortable: true,
};
const diffPercentHeader = {
id: "diffPercent",
label: "1d %",
info: "Shows the percentage difference compared to the previous day.",
sortable: true,
};
export const METRIC_NAMES_HEADERS: HeadCell[] = [
{
id: "name",
label: "Metric name",
sortable: true,
},
{
id: "value",
label: "Number of series",
sortable: true,
},
diffHeader,
diffPercentHeader,
{
id: "percentage",
label: "Share in total",
info: "Shows the share of a metric to the total number of series"
},
actionHeader
];
export const LABEL_NAMES_HEADERS: HeadCell[] = [
{
id: "name",
label: "Label name",
sortable: true,
},
{
id: "value",
label: "Number of series",
sortable: true,
},
diffHeader,
diffPercentHeader,
{
id: "percentage",
label: "Share in total",
info: "Shows the share of the label to the total number of series"
},
actionHeader,
];
export const FOCUS_LABEL_VALUES_HEADERS: HeadCell[] = [
{
id: "name",
label: "Label value",
sortable: true,
},
{
id: "value",
label: "Number of series",
sortable: true,
},
diffHeader,
diffPercentHeader,
{
id: "percentage",
label: "Share in total",
},
actionHeader,
];
export const LABEL_VALUE_PAIRS_HEADERS = [
{
id: "name",
label: "Label=value pair",
sortable: true,
},
{
id: "value",
label: "Number of series",
sortable: true,
},
diffHeader,
diffPercentHeader,
{
id: "percentage",
label: "Share in total",
info: "Shows the share of the label value pair to the total number of series"
},
actionHeader,
];
export const LABEL_NAMES_WITH_UNIQUE_VALUES_HEADERS: HeadCell[] = [
{
id: "name",
label: "Label name",
sortable: true,
},
{
id: "value",
label: "Number of unique values",
sortable: true,
},
diffHeader,
diffPercentHeader,
actionHeader
];

View File

@@ -25,7 +25,7 @@ export const useFetchQuery = (): {
const topN = +(searchParams.get("topN") || 10);
const date = searchParams.get("date") || dayjs().tz().format(DATE_FORMAT);
const prevDate = usePrevious(date);
const prevTotal = useRef<{data: TSDBStatus}>();
const prevTotal = useRef<{ data: TSDBStatus }>();
const { serverUrl } = useAppState();
const [isLoading, setIsLoading] = useState(false);
@@ -35,10 +35,16 @@ export const useFetchQuery = (): {
const getResponseJson = async (url: string) => {
const response = await fetch(url);
const json = await response.json();
if (response.ok) {
return await response.json();
return json;
}
throw new Error(`Request failed with status ${response.status}`);
console.error(`Error fetching ${url}:`, json);
const errorType = json.errorType || ErrorTypes.unknownType;
const errorMessage = json?.error || json?.message || "see console for more details";
const error = [errorType, errorMessage].join("\r\n");
throw new Error(error);
};
const calculateDiffs = (result: TSDBStatus, prevResult: TSDBStatus) => {
@@ -50,7 +56,10 @@ export const useFetchQuery = (): {
if (Array.isArray(entries) && Array.isArray(prevEntries)) {
entries.forEach((entry) => {
const valuePrev = prevEntries.find(prevEntry => prevEntry.name === entry.name)?.value;
entry.diff = valuePrev ? entry.value - valuePrev : 0;
const diff = valuePrev ? entry.value - valuePrev : 0;
const diffPercent = valuePrev ? (diff / valuePrev) * 100 : 0;
entry.diff = diff;
entry.diffPercent = diffPercent;
entry.valuePrev = valuePrev || 0;
});
}

View File

@@ -3,7 +3,7 @@
.vm-cardinality-panel {
display: grid;
align-items: flex-start;
gap: $padding-medium;
gap: $padding-large;
&_mobile, &_mobile &-tips {
gap: $padding-small;
@@ -24,19 +24,16 @@
th:first-child {
width: 60%;
}
th:not(:first-child) {
width: auto;
}
}
&__progress {
display: grid;
grid-template-columns: minmax(200px, 1fr) 70px;
grid-template-columns: minmax(160px, 1fr) repeat(2, auto);
white-space: nowrap;
align-items: center;
justify-content: flex-start;
gap: $padding-small;
font-size: $font-size-small;
}
}
}

View File

@@ -9,7 +9,7 @@ export const useDebugDownsamplingFilters = () => {
const { serverUrl } = useAppState();
const [searchParams, setSearchParams] = useSearchParams();
const [data, setData] = useState<Map<string, string[]>>(new Map());
const [data, setData] = useState<Map<string, string[] | null>>(new Map());
const [loading, setLoading] = useState(false);
const [metricsError, setMetricsError] = useState<ErrorTypes | string>();
const [flagsError, setFlagsError] = useState<ErrorTypes | string>();

View File

@@ -7,6 +7,7 @@ import { PlayIcon, WikiIcon } from "../../components/Main/Icons";
import { useDebugDownsamplingFilters } from "./hooks/useDebugDownsamplingFilters";
import Spinner from "../../components/Main/Spinner/Spinner";
import { useSearchParams } from "react-router-dom";
import classNames from "classnames";
const example = {
flags: `-downsampling.period={env="dev"}:7d:5m,{env="dev"}:30d:30m
@@ -54,7 +55,14 @@ const DownsamplingFilters: FC = () => {
for (const [key, value] of data) {
rows.push(<tr className="vm-table__row">
<td className="vm-table-cell">{key}</td>
<td className="vm-table-cell">{value.join(" ")}</td>
<td
className={classNames({
"vm-table-cell": true,
"vm-table-cell_empty": !value,
})}
>
{value ? value.join(" ") : "No matching rules found!"}
</td>
</tr>);
}
return (

View File

@@ -1,9 +1,18 @@
@use "src/styles/variables" as *;
.vm-dynamic-number {
position: relative;
display: flex;
align-items: center;
padding-left: 1em;
font-size: $font-size-small;
color: $color-text-disabled;
&_static {
color: $color-text-secondary;
padding-left: 0;
}
&_positive {
color: $color-success;
}
@@ -12,15 +21,22 @@
color: $color-error;
}
&:before {
position: absolute;
top: auto;
left: 0;
width: 1em;
}
&_down {
&:before {
content: "";
content: " ";
}
}
&_up {
&:before {
content: "";
content: " ";
}
}
}

View File

@@ -31,6 +31,7 @@
vertical-align: top;
line-height: 1.5;
overflow-wrap: anywhere;
font-variant-numeric: tabular-nums;
&__content {
display: flex;
@@ -88,6 +89,16 @@
width: 100%;
overflow-wrap: normal;
}
&_compact {
white-space: nowrap;
width: 100px;
}
&_empty {
color: $color-text-secondary;
font-style: italic;
}
}
&__sort-icon {

View File

@@ -5,7 +5,7 @@ export const getNameForMetric = (result: MetricBase, alias?: string, showQueryNu
const queryPrefix = showQueryNum ? `[Query ${result.group}] ` : "";
if (alias) {
return alias.replace(/\{\{(\w+)}}/g, (_, key) => result.metric[key] || "");
return getLabelAlias(result.metric, alias);
}
const name = `${queryPrefix}${__name__ || ""}`;
@@ -21,6 +21,10 @@ export const getNameForMetric = (result: MetricBase, alias?: string, showQueryNu
return `${name}{${fieldsString}}`;
};
export const getLabelAlias = (fields: { [p: string]: string }, alias: string) => {
return alias.replace(/\{\{(\w+)}}/g, (_, key) => fields[key] || "");
};
export const promValueToNumber = (s: string): number => {
// See https://prometheus.io/docs/prometheus/latest/querying/api/#expression-query-result-formats
switch (s) {

View File

@@ -32,34 +32,30 @@ func (c *Client) CloseConnections() {
c.httpCli.CloseIdleConnections()
}
// Get sends a HTTP GET request. Once the function receives a response, it
// checks whether the response status code matches the expected one and returns
// the response body to the caller.
func (c *Client) Get(t *testing.T, url string, wantStatusCode int) string {
// Get sends a HTTP GET request, returns
// the response body and status code to the caller.
func (c *Client) Get(t *testing.T, url string) (string, int) {
t.Helper()
return c.do(t, http.MethodGet, url, "", nil, wantStatusCode)
return c.do(t, http.MethodGet, url, "", nil)
}
// Post sends a HTTP POST request. Once the function receives a response, it
// checks whether the response status code matches the expected one and returns
// the response body to the caller.
func (c *Client) Post(t *testing.T, url, contentType string, data []byte, wantStatusCode int) string {
// Post sends a HTTP POST request, returns
// the response body and status code to the caller.
func (c *Client) Post(t *testing.T, url, contentType string, data []byte) (string, int) {
t.Helper()
return c.do(t, http.MethodPost, url, contentType, data, wantStatusCode)
return c.do(t, http.MethodPost, url, contentType, data)
}
// PostForm sends a HTTP POST request containing the POST-form data. Once the
// function receives a response, it checks whether the response status code
// matches the expected one and returns the response body to the caller.
func (c *Client) PostForm(t *testing.T, url string, data url.Values, wantStatusCode int) string {
// PostForm sends a HTTP POST request containing the POST-form data, returns
// the response body and status code to the caller.
func (c *Client) PostForm(t *testing.T, url string, data url.Values) (string, int) {
t.Helper()
return c.Post(t, url, "application/x-www-form-urlencoded", []byte(data.Encode()), wantStatusCode)
return c.Post(t, url, "application/x-www-form-urlencoded", []byte(data.Encode()))
}
// do prepares a HTTP request, sends it to the server, receives the response
// from the server, ensures then response code matches the expected one, reads
// the rentire response body and returns it to the caller.
func (c *Client) do(t *testing.T, method, url, contentType string, data []byte, wantStatusCode int) string {
// from the server, returns the response body and status code to the caller.
func (c *Client) do(t *testing.T, method, url, contentType string, data []byte) (string, int) {
t.Helper()
req, err := http.NewRequest(method, url, bytes.NewReader(data))
@@ -77,11 +73,7 @@ func (c *Client) do(t *testing.T, method, url, contentType string, data []byte,
body := readAllAndClose(t, res.Body)
if got, want := res.StatusCode, wantStatusCode; got != want {
t.Fatalf("unexpected response code: got %d, want %d (body: %s)", got, want, body)
}
return body
return body, res.StatusCode
}
// readAllAndClose reads everything from the response body and then closes it.
@@ -116,7 +108,10 @@ func (app *ServesMetrics) GetIntMetric(t *testing.T, metricName string) int {
func (app *ServesMetrics) GetMetric(t *testing.T, metricName string) float64 {
t.Helper()
metrics := app.cli.Get(t, app.metricsURL, http.StatusOK)
metrics, statusCode := app.cli.Get(t, app.metricsURL)
if statusCode != http.StatusOK {
t.Fatalf("unexpected status code: got %d, want %d", statusCode, http.StatusOK)
}
for _, metric := range strings.Split(metrics, "\n") {
value, found := strings.CutPrefix(metric, metricName)
if found {
@@ -139,7 +134,10 @@ func (app *ServesMetrics) GetMetricsByPrefix(t *testing.T, prefix string) []floa
values := []float64{}
metrics := app.cli.Get(t, app.metricsURL, http.StatusOK)
metrics, statusCode := app.cli.Get(t, app.metricsURL)
if statusCode != http.StatusOK {
t.Fatalf("unexpected status code: got %d, want %d", statusCode, http.StatusOK)
}
for _, metric := range strings.Split(metrics, "\n") {
if !strings.HasPrefix(metric, prefix) {
continue

View File

@@ -88,8 +88,10 @@ func (qos *QueryOpts) getTenant() string {
// PrometheusAPIV1QueryResponse is an inmemory representation of the
// /prometheus/api/v1/query or /prometheus/api/v1/query_range response.
type PrometheusAPIV1QueryResponse struct {
Status string
Data *QueryData
Status string
Data *QueryData
ErrorType string
Error string
}
// NewPrometheusAPIV1QueryResponse is a test helper function that creates a new
@@ -185,6 +187,8 @@ type PrometheusAPIV1SeriesResponse struct {
IsPartial bool
Data []map[string]string
Trace *Trace
ErrorType string
Error string
}
// NewPrometheusAPIV1SeriesResponse is a test helper function that creates a new

View File

@@ -20,6 +20,69 @@ func millis(s string) int64 {
return t.UnixMilli()
}
func TestSingleInstantQuery(t *testing.T) {
tc := apptest.NewTestCase(t)
defer tc.Stop()
sut := tc.MustStartDefaultVmsingle()
testInstantQueryWithUTFNames(t, sut)
testInstantQueryDoesNotReturnStaleNaNs(t, sut)
}
func TestClusterInstantQuery(t *testing.T) {
tc := apptest.NewTestCase(t)
defer tc.Stop()
sut := tc.MustStartDefaultCluster()
testInstantQueryWithUTFNames(t, sut)
testInstantQueryDoesNotReturnStaleNaNs(t, sut)
}
func testInstantQueryWithUTFNames(t *testing.T, sut apptest.PrometheusWriteQuerier) {
data := []pb.TimeSeries{
{
Labels: []pb.Label{
{Name: "__name__", Value: "3fooµ¥"},
{Name: "3👋tfにちは", Value: "漢©®€£"},
},
Samples: []pb.Sample{
{Value: 1, Timestamp: millis("2024-01-01T00:01:00Z")},
},
},
}
sut.PrometheusAPIV1Write(t, data, apptest.QueryOpts{})
sut.ForceFlush(t)
var got, want *apptest.PrometheusAPIV1QueryResponse
cmpOptions := []cmp.Option{
cmpopts.IgnoreFields(apptest.PrometheusAPIV1QueryResponse{}, "Status", "Data.ResultType"),
cmpopts.EquateNaNs(),
}
want = apptest.NewPrometheusAPIV1QueryResponse(t, `{"data": {"result": [{"metric": {"__name__": "3fooµ¥", "3👋tfにちは": "漢©®€£"}}]}}`)
fn := func(query string) {
got = sut.PrometheusAPIV1Query(t, query, apptest.QueryOpts{
Step: "5m",
Time: "2024-01-01T00:01:00.000Z",
})
want.Data.Result[0].Sample = apptest.NewSample(t, "2024-01-01T00:01:00Z", 1)
if diff := cmp.Diff(want, got, cmpOptions...); diff != "" {
t.Errorf("unexpected response (-want, +got):\n%s", diff)
}
}
fn(`{"3fooµ¥"}`)
fn(`{__name__="3fooµ¥"}`)
fn(`{__name__=~"3fo.*"}`)
fn(`{__name__=~".*µ¥"}`)
fn(`{"3fooµ¥", "3👋tfにちは"="漢©®€£"}`)
fn(`{"3fooµ¥", "3👋tfにちは"=~"漢.*"}`)
fn(`{"3👋tfにちは"="漢©®€£"}`)
}
var staleNaNsData = func() []pb.TimeSeries {
return []pb.TimeSeries{
{
@@ -43,24 +106,6 @@ var staleNaNsData = func() []pb.TimeSeries {
}
}()
func TestSingleInstantQueryDoesNotReturnStaleNaNs(t *testing.T) {
tc := apptest.NewTestCase(t)
defer tc.Stop()
sut := tc.MustStartDefaultVmsingle()
testInstantQueryDoesNotReturnStaleNaNs(t, sut)
}
func TestClusterInstantQueryDoesNotReturnStaleNaNs(t *testing.T) {
tc := apptest.NewTestCase(t)
defer tc.Stop()
sut := tc.MustStartDefaultCluster()
testInstantQueryDoesNotReturnStaleNaNs(t, sut)
}
func testInstantQueryDoesNotReturnStaleNaNs(t *testing.T, sut apptest.PrometheusWriteQuerier) {
sut.PrometheusAPIV1Write(t, staleNaNsData, apptest.QueryOpts{})

View File

@@ -0,0 +1,182 @@
package tests
import (
"os"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/VictoriaMetrics/VictoriaMetrics/apptest"
)
func TestClusterMaxUniqueTimeseries(t *testing.T) {
os.RemoveAll(t.Name())
cmpOpt := cmpopts.IgnoreFields(apptest.PrometheusAPIV1QueryResponse{}, "Status", "Data.ResultType")
tc := apptest.NewTestCase(t)
defer tc.Stop()
vmstorage := tc.MustStartVmstorage("vmstorage", []string{
"-storageDataPath=" + tc.Dir() + "/vmstorage",
"-retentionPeriod=100y",
"-search.maxUniqueTimeseries=2",
})
vminsert := tc.MustStartVminsert("vminsert", []string{
"-storageNode=" + vmstorage.VminsertAddr(),
})
vmselectNoLimit := tc.MustStartVmselect("vmselect1", []string{
"-storageNode=" + vmstorage.VmselectAddr(),
"-search.tenantCacheExpireDuration=0",
})
vmselectSmallLimit := tc.MustStartVmselect("vmselect2", []string{
"-storageNode=" + vmstorage.VmselectAddr(),
"-search.tenantCacheExpireDuration=0",
"-search.maxUniqueTimeseries=1",
})
vmselectBigLimit := tc.MustStartVmselect("vmselect3", []string{
"-storageNode=" + vmstorage.VmselectAddr(),
"-search.tenantCacheExpireDuration=0",
"-search.maxUniqueTimeseries=3",
})
var commonSamples = []string{
`foo_bar1{instance="a"} 1.00 1652169660000`,
`foo_bar2{instance="a"} 1.00 1652169660000`,
`foo_bar2{instance="b"} 2.00 1652169660000`,
`foo_bar3{instance="a"} 1.00 1652169660000`,
`foo_bar3{instance="b"} 2.00 1652169660000`,
`foo_bar3{instance="c"} 3.00 1652169660000`,
}
// write data to two tenants
tenantIDs := []string{"0:0", "1:15"}
for _, tenantID := range tenantIDs {
vminsert.PrometheusAPIV1ImportPrometheus(t, commonSamples, apptest.QueryOpts{Tenant: tenantID})
vmstorage.ForceFlush(t)
}
instantCT := "2022-05-10T08:05:00.000Z"
// success - `/api/v1/query`
want := apptest.NewPrometheusAPIV1QueryResponse(t,
`{"data":
{"result":[
{"metric":{"__name__":"foo_bar1","instance":"a"},"value":[1652169900,"1"]}
]
}
}`,
)
queryRes := vmselectSmallLimit.PrometheusAPIV1Query(t, "foo_bar1", apptest.QueryOpts{
Time: instantCT,
})
if diff := cmp.Diff(want, queryRes, cmpOpt); diff != "" {
t.Fatalf("unexpected response (-want, +got):\n%s", diff)
}
// success - multitenant `/api/v1/query`
// query is split into two queries for each tenant, so the final result can exceed the limit.
want = apptest.NewPrometheusAPIV1QueryResponse(t,
`{"data":
{"result":[
{"metric":{"__name__":"foo_bar1","instance":"a","vm_account_id":"0","vm_project_id":"0"},"value":[1652169900,"1"]},
{"metric":{"__name__":"foo_bar1","instance":"a","vm_account_id":"1","vm_project_id":"15"},"value":[1652169900,"1"]}
]
}
}`,
)
queryRes = vmselectSmallLimit.PrometheusAPIV1Query(t, "foo_bar1", apptest.QueryOpts{
Time: instantCT,
Tenant: "multitenant",
})
if diff := cmp.Diff(want, queryRes, cmpOpt); diff != "" {
t.Fatalf("unexpected response (-want, +got):\n%s", diff)
}
// fail - `/api/v1/query`, exceed vmselect `maxUniqueTimeseries`
queryRes = vmselectSmallLimit.PrometheusAPIV1Query(t, "foo_bar2", apptest.QueryOpts{
Time: instantCT,
})
if queryRes.ErrorType != "422" {
t.Fatalf("unexpected status code, got %s, want %d, error message is: %v", queryRes.ErrorType, 422, queryRes.Error)
}
// fail - `/api/v1/query`, exceed vmstorage `maxUniqueTimeseries`
queryRes = vmselectNoLimit.PrometheusAPIV1Query(t, "foo_bar3", apptest.QueryOpts{
Time: instantCT,
})
if queryRes.ErrorType != "422" {
t.Fatalf("unexpected status code, got %s, want %d, error message is: %v", queryRes.ErrorType, 422, queryRes.Error)
}
// fail - `/api/v1/query`, vmselect `maxUniqueTimeseries` cannot exceed vmstorage `maxUniqueTimeseries`
queryRes = vmselectBigLimit.PrometheusAPIV1Query(t, "foo_bar3", apptest.QueryOpts{
Time: instantCT,
})
if queryRes.ErrorType != "422" {
t.Fatalf("unexpected status code, got %s, want %d, error message is: %v", queryRes.ErrorType, 422, queryRes.Error)
}
}
func TestClusterMaxSeries(t *testing.T) {
os.RemoveAll(t.Name())
cmpSROpt := cmpopts.IgnoreFields(apptest.PrometheusAPIV1SeriesResponse{}, "Status", "IsPartial")
tc := apptest.NewTestCase(t)
defer tc.Stop()
vmstorage := tc.MustStartVmstorage("vmstorage", []string{
"-storageDataPath=" + tc.Dir() + "/vmstorage",
"-retentionPeriod=100y",
"-search.maxUniqueTimeseries=2",
})
vminsert := tc.MustStartVminsert("vminsert", []string{
"-storageNode=" + vmstorage.VminsertAddr(),
})
vmselectBigLimit := tc.MustStartVmselect("vmselect2", []string{
"-storageNode=" + vmstorage.VmselectAddr(),
"-search.tenantCacheExpireDuration=0",
"-search.maxSeries=3",
})
vmselectSmallLimit := tc.MustStartVmselect("vmselect1", []string{
"-storageNode=" + vmstorage.VmselectAddr(),
"-search.tenantCacheExpireDuration=0",
"-search.maxSeries=1",
})
var commonSamples = []string{
`foo_bar3{instance="a"} 1.00 1652169660000`,
`foo_bar3{instance="b"} 2.00 1652169660000`,
`foo_bar3{instance="c"} 3.00 1652169660000`,
}
// write data
vminsert.PrometheusAPIV1ImportPrometheus(t, commonSamples, apptest.QueryOpts{})
vmstorage.ForceFlush(t)
// success - `/api/v1/series`, vmselect `maxLabelsAPISeries` can exceed vmstorage `maxLabelsAPISeries``
wantSR := apptest.NewPrometheusAPIV1SeriesResponse(t,
`{"data": [
{"__name__":"foo_bar3","instance":"a"},
{"__name__":"foo_bar3","instance":"b"},
{"__name__":"foo_bar3","instance":"c"}
]
}`)
seriesRes := vmselectBigLimit.PrometheusAPIV1Series(t, "foo_bar3", apptest.QueryOpts{
Start: "2022-05-10T08:03:00.000Z",
})
if diff := cmp.Diff(wantSR.Sort(), seriesRes.Sort(), cmpSROpt); diff != "" {
t.Fatalf("unexpected response (-want, +got):\n%s", diff)
}
// fail - `/api/v1/series`, exceed vmselect `maxSeries`
seriesRes1 := vmselectSmallLimit.PrometheusAPIV1Series(t, "foo_bar3", apptest.QueryOpts{
Start: "2022-05-10T08:03:00.000Z",
})
if seriesRes1.ErrorType != "422" {
t.Fatalf("unexpected status code, got %s, want %d, error message is: %v", seriesRes.ErrorType, 422, seriesRes1.Error)
}
}

View File

@@ -88,7 +88,13 @@ func (app *Vminsert) InfluxWrite(t *testing.T, records []string, opts QueryOpts)
url := fmt.Sprintf("http://%s/insert/%s/influx/write", app.httpListenAddr, opts.getTenant())
data := []byte(strings.Join(records, "\n"))
app.cli.Post(t, url, "text/plain", data, http.StatusNoContent)
app.sendBlocking(t, len(records), func() {
_, statusCode := app.cli.Post(t, url, "text/plain", data)
if statusCode != http.StatusNoContent {
t.Fatalf("unexpected status code: got %d, want %d", statusCode, http.StatusNoContent)
}
})
}
// PrometheusAPIV1Write is a test helper function that inserts a
@@ -101,7 +107,10 @@ func (app *Vminsert) PrometheusAPIV1Write(t *testing.T, records []pb.TimeSeries,
wr := pb.WriteRequest{Timeseries: records}
data := snappy.Encode(nil, wr.MarshalProtobuf(nil))
app.sendBlocking(t, len(records), func() {
app.cli.Post(t, url, "application/x-protobuf", data, http.StatusNoContent)
_, statusCode := app.cli.Post(t, url, "application/x-protobuf", data)
if statusCode != http.StatusNoContent {
t.Fatalf("unexpected status code: got %d, want %d", statusCode, http.StatusNoContent)
}
})
}
@@ -122,7 +131,10 @@ func (app *Vminsert) PrometheusAPIV1ImportPrometheus(t *testing.T, records []str
}
data := []byte(strings.Join(records, "\n"))
app.sendBlocking(t, len(records), func() {
app.cli.Post(t, url, "text/plain", data, http.StatusNoContent)
_, statusCode := app.cli.Post(t, url, "text/plain", data)
if statusCode != http.StatusNoContent {
t.Fatalf("unexpected status code: got %d, want %d", statusCode, http.StatusNoContent)
}
})
}

View File

@@ -2,7 +2,6 @@ package apptest
import (
"fmt"
"net/http"
"regexp"
"testing"
)
@@ -66,7 +65,7 @@ func (app *Vmselect) PrometheusAPIV1Export(t *testing.T, query string, opts Quer
values := opts.asURLValues()
values.Add("match[]", query)
values.Add("format", "promapi")
res := app.cli.PostForm(t, exportURL, values, http.StatusOK)
res, _ := app.cli.PostForm(t, exportURL, values)
return NewPrometheusAPIV1QueryResponse(t, res)
}
@@ -82,7 +81,7 @@ func (app *Vmselect) PrometheusAPIV1Query(t *testing.T, query string, opts Query
values := opts.asURLValues()
values.Add("query", query)
res := app.cli.PostForm(t, queryURL, values, http.StatusOK)
res, _ := app.cli.PostForm(t, queryURL, values)
return NewPrometheusAPIV1QueryResponse(t, res)
}
@@ -98,7 +97,7 @@ func (app *Vmselect) PrometheusAPIV1QueryRange(t *testing.T, query string, opts
values := opts.asURLValues()
values.Add("query", query)
res := app.cli.PostForm(t, queryURL, values, http.StatusOK)
res, _ := app.cli.PostForm(t, queryURL, values)
return NewPrometheusAPIV1QueryResponse(t, res)
}
@@ -113,7 +112,7 @@ func (app *Vmselect) PrometheusAPIV1Series(t *testing.T, matchQuery string, opts
values := opts.asURLValues()
values.Add("match[]", matchQuery)
res := app.cli.PostForm(t, seriesURL, values, http.StatusOK)
res, _ := app.cli.PostForm(t, seriesURL, values)
return NewPrometheusAPIV1SeriesResponse(t, res)
}
@@ -127,7 +126,8 @@ func (app *Vmselect) DeleteSeries(t *testing.T, matchQuery string, opts QueryOpt
values := opts.asURLValues()
values.Add("match[]", matchQuery)
res := app.cli.PostForm(t, seriesURL, values, http.StatusNoContent)
// TODO(@rtm0): Add DeleteSeriesResponse.
res, _ := app.cli.PostForm(t, seriesURL, values)
if res != "" {
t.Fatalf("unexpected non-empty DeleteSeries response=%q", res)
}

View File

@@ -80,7 +80,10 @@ func StartVmsingle(instance string, flags []string, cli *Client) (*Vmsingle, err
func (app *Vmsingle) ForceFlush(t *testing.T) {
t.Helper()
app.cli.Get(t, app.forceFlushURL, http.StatusOK)
_, statusCode := app.cli.Get(t, app.forceFlushURL)
if statusCode != http.StatusOK {
t.Fatalf("unexpected status code: got %d, want %d", statusCode, http.StatusOK)
}
}
// InfluxWrite is a test helper function that inserts a
@@ -92,7 +95,10 @@ func (app *Vmsingle) InfluxWrite(t *testing.T, records []string, _ QueryOpts) {
t.Helper()
data := []byte(strings.Join(records, "\n"))
app.cli.Post(t, app.influxLineWriteURL, "text/plain", data, http.StatusNoContent)
_, statusCode := app.cli.Post(t, app.influxLineWriteURL, "text/plain", data)
if statusCode != http.StatusNoContent {
t.Fatalf("unexpected status code: got %d, want %d", statusCode, http.StatusNoContent)
}
}
// PrometheusAPIV1Write is a test helper function that inserts a
@@ -103,7 +109,10 @@ func (app *Vmsingle) PrometheusAPIV1Write(t *testing.T, records []pb.TimeSeries,
wr := pb.WriteRequest{Timeseries: records}
data := snappy.Encode(nil, wr.MarshalProtobuf(nil))
app.cli.Post(t, app.prometheusAPIV1WriteURL, "application/x-protobuf", data, http.StatusNoContent)
_, statusCode := app.cli.Post(t, app.prometheusAPIV1WriteURL, "application/x-protobuf", data)
if statusCode != http.StatusNoContent {
t.Fatalf("unexpected status code: got %d, want %d", statusCode, http.StatusNoContent)
}
}
// PrometheusAPIV1ImportPrometheus is a test helper function that inserts a
@@ -115,7 +124,10 @@ func (app *Vmsingle) PrometheusAPIV1ImportPrometheus(t *testing.T, records []str
t.Helper()
data := []byte(strings.Join(records, "\n"))
app.cli.Post(t, app.prometheusAPIV1ImportPrometheusURL, "text/plain", data, http.StatusNoContent)
_, statusCode := app.cli.Post(t, app.prometheusAPIV1ImportPrometheusURL, "text/plain", data)
if statusCode != http.StatusNoContent {
t.Fatalf("unexpected status code: got %d, want %d", statusCode, http.StatusNoContent)
}
}
// PrometheusAPIV1Export is a test helper function that performs the export of
@@ -129,7 +141,7 @@ func (app *Vmsingle) PrometheusAPIV1Export(t *testing.T, query string, opts Quer
values.Add("match[]", query)
values.Add("format", "promapi")
res := app.cli.PostForm(t, app.prometheusAPIV1ExportURL, values, http.StatusOK)
res, _ := app.cli.PostForm(t, app.prometheusAPIV1ExportURL, values)
return NewPrometheusAPIV1QueryResponse(t, res)
}
@@ -143,7 +155,7 @@ func (app *Vmsingle) PrometheusAPIV1Query(t *testing.T, query string, opts Query
values := opts.asURLValues()
values.Add("query", query)
res := app.cli.PostForm(t, app.prometheusAPIV1QueryURL, values, http.StatusOK)
res, _ := app.cli.PostForm(t, app.prometheusAPIV1QueryURL, values)
return NewPrometheusAPIV1QueryResponse(t, res)
}
@@ -158,7 +170,7 @@ func (app *Vmsingle) PrometheusAPIV1QueryRange(t *testing.T, query string, opts
values := opts.asURLValues()
values.Add("query", query)
res := app.cli.PostForm(t, app.prometheusAPIV1QueryRangeURL, values, http.StatusOK)
res, _ := app.cli.PostForm(t, app.prometheusAPIV1QueryRangeURL, values)
return NewPrometheusAPIV1QueryResponse(t, res)
}
@@ -172,7 +184,7 @@ func (app *Vmsingle) PrometheusAPIV1Series(t *testing.T, matchQuery string, opts
values := opts.asURLValues()
values.Add("match[]", matchQuery)
res := app.cli.PostForm(t, app.prometheusAPIV1SeriesURL, values, http.StatusOK)
res, _ := app.cli.PostForm(t, app.prometheusAPIV1SeriesURL, values)
return NewPrometheusAPIV1SeriesResponse(t, res)
}

View File

@@ -77,7 +77,10 @@ func (app *Vmstorage) VmselectAddr() string {
func (app *Vmstorage) ForceFlush(t *testing.T) {
t.Helper()
app.cli.Get(t, app.forceFlushURL, http.StatusOK)
_, statusCode := app.cli.Get(t, app.forceFlushURL)
if statusCode != http.StatusOK {
t.Fatalf("unexpected status code: got %d, want %d", statusCode, http.StatusOK)
}
}
// String returns the string representation of the vmstorage app state.

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