Compare commits

...

111 Commits

Author SHA1 Message Date
f41gh7
61b84e9021 CHANGELOG.md: cut v1.107.0 release 2024-11-29 17:45:47 +01:00
f41gh7
6d0420b454 make docs-update-version 2024-11-29 17:45:18 +01:00
f41gh7
5bf2d6a689 make vmui-update 2024-11-29 17:43:55 +01:00
f41gh7
765ce1b181 app/vmctl: follow-up after vendor-update
Comment broken tests for remote_read integration test.
 Prometheus broke library compatibility and it's required to rewrite tests.
 Also, test structure and format should be revisited and improved according to our test code style.

Signed-off-by: f41gh7 <nik@victoriametrics.com>
2024-11-29 14:45:18 +01:00
f41gh7
036f33de48 app/vmagent: follow-up 430163d and 680b8c2
Removes global defaultAuthToken, since it's no longer needed.
It was added as fallback for 'remoteWrite.multitenantURL' feature.
This feature was deprecated at v1.102 version and removed.

 Updates newRemoteWriteCtxs function, it shouldn't accept auth.Token no longer.
This was also a part of remove feature.

Signed-off-by: f41gh7 <nik@victoriametrics.com>
2024-11-29 14:36:59 +01:00
Nikolay
430163d01a app/vmagent: fixes multitenant token parse
Previously, vmagent produced parsing error for 'multitenant' auth token
value for the cases:
* data ingestion with enableMultitentEndpoints
* data scrapping at promscrape

 It's inconsistent to the other VictoriaMetrics components.
Since 'multitenant' is well-known token value for multitenancy via
labels. And vmagent is intended to be compatible with vminsert ingestion
endpoints.

 This commit replaces NewToken with NewTokenPossibleMultitenant function
for token parsing. It allows to use multitenant value for it. And it
makes token values consistent for the all components.

Related issue:
https://github.com/VictoriaMetrics/VictoriaMetrics/issues/7694
2024-11-29 14:04:07 +01:00
Aliaksandr Valialkin
7c40b95224 vendor: run make vendor-update 2024-11-29 13:48:50 +01:00
f41gh7
cd89923e3c app/vmauth: fixes ip_filters typo at example_config
url_map section cannot have ip_filters. Only top level config section
have it.
2024-11-29 11:01:00 +01:00
f41gh7
f404e0b3a5 app/vmauth: fixes ip_filters init for uauthorized_user
Previously ip_filters wasn't properly inited for this part of config.
It resulted to bypass requests for this section.

 This commit properly inits `ip_filter`.
2024-11-29 11:01:00 +01:00
f41gh7
1d4a4eb8b0 app/vmauth: add ip_filter deny metrics
Previously, all requests rejected by `ip_filter` were silently aborted.

This commit adds new metrics:
* vmauth_user_ip_denies_total
* vmauth_global_ip_denies_total
* vmauth_unauthorized_user_ip_denies_total

 It adds observability to this feature and allow to measure rejected requests.

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

Signed-off-by: f41gh7 <nik@victoriametrics.com>
2024-11-29 11:00:32 +01:00
Github Actions
30903d9361 Automatic update helm docs from VictoriaMetrics/helm-charts@f5f1b9c (#7691)
Automated changes by
[create-pull-request](https://github.com/peter-evans/create-pull-request)
GitHub action

Signed-off-by: Github Actions <133988544+victoriametrics-bot@users.noreply.github.com>
Co-authored-by: AndrewChubatiuk <3162380+AndrewChubatiuk@users.noreply.github.com>
2024-11-29 10:30:34 +01:00
Hui Wang
000a918f38 app/vmalert-tool: print an error message if no rule group is found under 'rule_files'
Both vmalert and vmalert-tool support multiple `rule_files` and use
directory as a file, so it's ok if some files don't contain any rule
group. But vmalert-tool should warn the user if no rule group is found
in any of the `rule_files`.

Related issue:
https://github.com/VictoriaMetrics/VictoriaMetrics/issues/7663
2024-11-29 10:29:04 +01:00
Nikolay
9c35807368 app/vmauth: add removeXFFHTTPHeaderValue flag
Previously, there was no option to replace value of `X-Forwarded-For`
HTTP Header. It was only possible to completely remove it. It's not good
solution, since backend may require this information. But using direct
value of this header is insecure. And requires complex knowledge of
infrastruce at backend side (see spoofing X-Forwarded-For articles).

This commit adds new flag, that replaces content of `X-Forwarded-For`
HTTP Header value with current `RemoteAddress` of client that send
request.
It should be used if `vmauth` is directly attached to the internet.

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

---------

Signed-off-by: f41gh7 <nik@victoriametrics.com>
2024-11-29 10:25:47 +01:00
Nikolay
92512cbe54 app/vmauth: add real_ip_header to ip_filters
This commit allows vmauth to obtain client IP address from HTTP Headers.
Main scenario for it is vmauth located behind reverse-proxy.

 It adds both global and per user configuration settings: -httpRealIPHeader and `real_ip_header` config option.

vmauth try to obtain IP from header if this setting is set. If header is not exists, vmauth fallbacks to `remoteAddress`.

Commit also updates incorrect benchmarks and align test package naming for ip_filters

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

Signed-off-by: f41gh7 <nik@victoriametrics.com>
2024-11-28 14:55:35 +01:00
Yury Molodov
dec9a2f023 vmui: fix predefined panels
### Describe Your Changes

- Fixes the handling of the `showLegend` flag.  
- Fixes the handling of `alias`.  
- Adds support for alias templates, allowing dynamic substitutions like
`{{label_name}}`.

Related issue:
https://github.com/VictoriaMetrics/VictoriaMetrics/issues/7565
2024-11-28 13:47:37 +01:00
Andrii Chubatiuk
0f06d2f072 deployment/docker: use vector for local setup (#7681) 2024-11-28 13:25:25 +01:00
Github Actions
9c6a4d915d Automatic update helm docs from VictoriaMetrics/helm-charts@a5bc12b (#7685)
Automated changes by
[create-pull-request](https://github.com/peter-evans/create-pull-request)
GitHub action

Signed-off-by: Github Actions <133988544+victoriametrics-bot@users.noreply.github.com>
Co-authored-by: AndrewChubatiuk <3162380+AndrewChubatiuk@users.noreply.github.com>
2024-11-28 13:25:10 +01:00
Fred Navruzov
6fa3283d04 docs/vmanomaly - release 1.18.5 (#7684)
### Describe Your Changes

docs/vmanomaly - release 1.18.5 doc updates

### Checklist

The following checks are **mandatory**:

- [x] My change adheres [VictoriaMetrics contributing
guidelines](https://docs.victoriametrics.com/contributing/).
2024-11-28 13:34:45 +02:00
Nikolay
20d5314833 app/vmauth: adds dryRun flag for config validation
This flag only validates configuration file pointed by `auth.config`
flag.

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

Signed-off-by: f41gh7 <nik@victoriametrics.com>
2024-11-27 13:51:48 +01:00
Yury Molodov
10c42668a1 vmui/logs: optimize memory consumption (#7524)
### Describe Your Changes

- **Memory Optimization**: Reduced memory consumption on the "Group" and
"JSON" tabs by approximately 30%.

- **Table Pagination**: Added pagination to the "Table" view with an
option to select the number of rows displayed (from 10 to 1000 items per
page, with a default of 1000). This change significantly reduced memory
usage by approximately 75%.

Related to #7185

### Checklist

The following checks are **mandatory**:

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

---------

Co-authored-by: Roman Khavronenko <roman@victoriametrics.com>
2024-11-27 13:49:06 +01:00
Andrii Chubatiuk
60d587f55b docs/vmgateway: added to documentation a case, when vm_access claim value is of string type 2024-11-27 11:13:59 +01:00
Viet Hung Nguyen
466cbee433 docs: fix rule unittest rule_files point to alerts.yaml (#7664)
### Describe Your Changes

Fix wrong path to rules_file in vmalert-tool doc

### Checklist

The following checks are **mandatory**:

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

Signed-off-by: Viet Hung Nguyen <hvn@familug.org>
Co-authored-by: Hui Wang <haley@victoriametrics.com>
2024-11-27 08:04:21 +01:00
Hui Wang
cdbe69e62b docs: add multitenat endpoints usage for vmalert multitenancy (#7665)
### 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/).
2024-11-27 08:01:11 +01:00
Zhu Jiekun
7374a8813d lib/promscrape/discovery: properly apply the resource_group filter for Azure service discovery
Previously, this filter did not apply to virtual
machine scale sets, causing all virtual machines to be discovered.

 This commit conditionally adds `resource_group` filter for Azure service discovery on virtual
machine scale sets. 

 Related issue:
https://github.com/VictoriaMetrics/VictoriaMetrics/issues/7630.
2024-11-26 19:06:43 +01:00
Nikolay
bb99ddf957 apptest: adds cluster test for multitenant API requests
This commit adds integration test for multitenant via labels feature -
https://docs.victoriametrics.com/cluster-victoriametrics/#multitenancy-via-labels

It also extends current test models in order to:
- accept float timestamps returned from /api/v1/query_range and query
api
https://github.com/VictoriaMetrics/VictoriaMetrics/blob/master/app/vmselect/prometheus/util.qtpl#L43
- accept arbitrary query url params for requests. It simplifies testing
for the different VM API extensions


---------

Signed-off-by: f41gh7 <nik@victoriametrics.com>
2024-11-26 19:05:03 +01:00
Andrii Chubatiuk
9cfdbc582f refactoring: changed prompb to prompbmarshal everythere where internal series transformations are happening (#7409)
### Describe Your Changes

doing similar changes for both vmagent and vminsert (like one in
https://github.com/VictoriaMetrics/VictoriaMetrics/pull/7399) ends up
with almost same implementations for each of packages instead of having
this shared code in one place. one of the reasons is the same Timeseries
and Labels structure from different prompb and prompbmarshal packages.
My proposal is to use structures from prompb package only to
marshal/unmarshal sent/received data, but for internal transformations
use only structures from prompbmarshal package

Another example, where it already can help to simplify code is streaming
aggregation pipeline for vmsingle (now it first marshals
prompb.Timeseries to storage.MetricRow and then if streaming aggregation
or deduplication is enabled it unmarshals all the series back but to
prompbmarshal.Timeseries)

### Checklist

The following checks are **mandatory**:

- [ ] My change adheres [VictoriaMetrics contributing
guidelines](https://docs.victoriametrics.com/contributing/).
2024-11-26 12:45:17 +01:00
Roman Khavronenko
8ab1261750 app/vmauth: dump requests that failed the route rules to stderr (#7649)
Additional info from the dump can be used to debug rotuing rules.

https://pkg.go.dev/net/http/httputil#DumpRequest

### 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>
2024-11-26 10:36:27 +01:00
Artem Navoiev
b1324360b8 docs: update formatting on cloud billing page
Signed-off-by: Artem Navoiev <tenmozes@gmail.com>
2024-11-26 09:52:53 +01:00
matty
c3fa806b2f docs: Bump LTS versions (#7658)
### Describe Your Changes

LTS versions are out of date, linking to old releases. This updates them
to the latest.

### Checklist

The following checks are **mandatory**:

- [x] My change adheres [VictoriaMetrics contributing
guidelines](https://docs.victoriametrics.com/contributing/).
2024-11-26 09:45:06 +01:00
Hui Wang
786a10835b alerts-vmalert: reserve rule name for description (#7659) 2024-11-26 09:22:00 +01:00
Artem Navoiev
3adc7abf8f docs: add cloud billing page (#7655)
### 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: Artem Navoiev <tenmozes@gmail.com>
2024-11-25 11:03:15 -08:00
Github Actions
2962e35c0b Automatic update helm docs from VictoriaMetrics/helm-charts@ebc6d67 (#7653)
Automated changes by
[create-pull-request](https://github.com/peter-evans/create-pull-request)
GitHub action

Signed-off-by: Github Actions <133988544+victoriametrics-bot@users.noreply.github.com>
Co-authored-by: AndrewChubatiuk <3162380+AndrewChubatiuk@users.noreply.github.com>
2024-11-25 18:07:27 +01:00
Andrei Baidarov
727bc02a5c vmagent: set up a timeout for tcp connection establishment during k8s discovery
Previously, default dial timeout was used for kubernetes API server connection.

 This commit changes it for custom dialer used by the all VictoriaMetrics components. It has lower connection timeout (30s by default). 


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

---------
Co-authored-by: f41gh7 <nik@victoriametrics.com>
2024-11-25 18:02:09 +01:00
hagen1778
71d774f76d docs: re-structure multitenancy docs
Signed-off-by: hagen1778 <roman@victoriametrics.com>
2024-11-25 13:10:30 +01:00
hagen1778
530b731101 docs: update otel docs
* cross-link related doc chapters and guides about otel
* mention different URL format for cluster version of VM

Signed-off-by: hagen1778 <roman@victoriametrics.com>
2024-11-25 13:10:14 +01:00
hagen1778
ec81deb7e8 dashboards: fix Ingestion row for vmagent dashboard
Previously, clicking on Ingestion row could result in a visual blip.
Re-ordering panels within the row seems to fix it.

Signed-off-by: hagen1778 <roman@victoriametrics.com>
2024-11-25 12:40:56 +01:00
hagen1778
0e6731323a docs: update link for tsdb stats word
The link was pointing to itself, which doesn't make sense.
Changed it to point to https://docs.victoriametrics.com/url-examples/#apiv1statustsdb

Signed-off-by: hagen1778 <roman@victoriametrics.com>
2024-11-25 12:27:26 +01:00
Nikolay
bdac00f674 app/vmselect: fixes multitenant cache init
Previously multitenant cache was inited before flag.Parse call. It
didn't allow to change cache expiration value and default value was
always used.

 This commit adds cache init at the first time cache was called.

 Also this commit adds small cache improvements:
* chore for cleanup cache, it now uses common pattern for in-place items
filtering
* fail cache request fast if item is already expired

---------
Signed-off-by: f41gh7 <nik@victoriametrics.com>
Co-authored-by: Roman Khavronenko <roman@victoriametrics.com>
2024-11-25 11:51:30 +01:00
Andrei Baidarov
037808dad5 app/vmselect: fix panic/incorrect tenant in key
This is a follow-up after 3120dc2

- Consistently use key for rollupCache in multitenant mode cache keys use different authTokens. Previously it could lead to panic in rare cases when cache state was inconsistent.
- Do not share `err` variable across goroutines for `processBlock` function. It could lead to data races.

Related issue https://github.com/VictoriaMetrics/VictoriaMetrics/issues/7549
---------
Signed-off-by: Andrei Baidarov <abaidarov@yandex.ru>
Co-authored-by: f41gh7 <nik@victoriametrics.com>
2024-11-25 11:50:49 +01:00
Hui Wang
508bafced3 vmalert-tool: exit immediately with error message if no test file found in -files (#7627) 2024-11-25 11:43:50 +01:00
Github Actions
0b2b96422d Automatic update helm docs from VictoriaMetrics/helm-charts@6a5b14b (#7642)
Automated changes by
[create-pull-request](https://github.com/peter-evans/create-pull-request)
GitHub action

Signed-off-by: Github Actions <133988544+victoriametrics-bot@users.noreply.github.com>
Co-authored-by: AndrewChubatiuk <3162380+AndrewChubatiuk@users.noreply.github.com>
Co-authored-by: Roman Khavronenko <roman@victoriametrics.com>
2024-11-25 10:08:51 +01:00
dmitry-shur
2328656f87 docs: fixed typo in FAQ (#7643)
### 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/).
2024-11-25 09:43:00 +01:00
Github Actions
9d5ef1cdd1 Automatic update helm docs from VictoriaMetrics/helm-charts@a582432 (#7641)
Automated changes by
[create-pull-request](https://github.com/peter-evans/create-pull-request)
GitHub action

Signed-off-by: Github Actions <133988544+victoriametrics-bot@users.noreply.github.com>
Co-authored-by: AndrewChubatiuk <3162380+AndrewChubatiuk@users.noreply.github.com>
2024-11-25 09:10:51 +01:00
hagen1778
c9837de9cd docs: add vmui section to cluster docs
It looks like users get confused with vmui availability in vmselect.
Adding a note about it to cluster docs should improve situation.

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

Signed-off-by: hagen1778 <roman@victoriametrics.com>
2024-11-24 14:18:26 +01:00
Artem Navoiev
6c6e469bfb docs: cloud mention silver level in support
Signed-off-by: Artem Navoiev <tenmozes@gmail.com>
2024-11-23 11:08:18 +01:00
Hui Wang
6ff1de89a9 vmalert: fix alert states restoration (#7624)
Previously, when the alert got resolved shortly before the vmalert
process shuts down, this could result in false alerts.

This change switches vmalert to use MetricsQL function during alerts state restore, which makes it
incompatible for state restoration with PromQL.

---------

Co-authored-by: Roman Khavronenko <roman@victoriametrics.com>
2024-11-22 09:11:31 +01:00
Artem Fetishev
f2d1f0716b apptest: add tests for stale nans in instant query (#7621)
### Describe Your Changes

These are the integration tests that confirm that instant queries may
return stale NaNs when the query contains a rollup function.

The bug was reported at #5806. There is also a fix: #7275. The tests in
this PR will be used co confirm that the fix works.

Some test refactoring has been done along the way. Sorry, couldn't
resist.

### Checklist

The following checks are **mandatory**:

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

---------

Signed-off-by: Artem Fetishev <rtm@victoriametrics.com>
2024-11-21 19:39:17 +01:00
Github Actions
4319d9f2b0 Automatic update Grafana datasource docs from VictoriaMetrics/victoriametrics-datasource@a5e2cb4 (#7622) 2024-11-21 19:32:27 +01:00
Dmytro Kozlov
5a97d512c0 deployment/docker: update victoriametrics datasource versions to the latest releases (#7623)
### Describe Your Changes

Updated victoriametrics-datasource to the newest
[release](https://github.com/VictoriaMetrics/victoriametrics-datasource/releases/tag/v0.10.3)
version

### Checklist

The following checks are **mandatory**:

- [x] My change adheres [VictoriaMetrics contributing
guidelines](https://docs.victoriametrics.com/contributing/).
2024-11-21 19:32:04 +01:00
hagen1778
a15fcac1b6 docs: update differences between vmagent and Prometheus in FAQ
* mention stream aggregation
* rm statement that Prometheus can only pull data, which is not true anymore
* mention absence of backfilling limitations

Signed-off-by: hagen1778 <roman@victoriametrics.com>
2024-11-21 16:30:59 +01:00
Ivan Yurochko
5dd879cd17 lib/streamaggr: add ignore_first_sample_interval param for streamaggr cfg (#7313)
### Describe Your Changes

As of right now by default aggregated output in streaming aggregation
takes a staleness interval and only starts sending first samples after
the staleness interval passes. We have a use case where we prefer to
start sending data as soon as we have any. This adds the option to
configure when we start sending first samples

https://github.com/VictoriaMetrics/VictoriaMetrics/issues/7116

### Checklist

The following checks are **mandatory**:

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

---------

Co-authored-by: hagen1778 <roman@victoriametrics.com>
2024-11-21 16:20:22 +01:00
Github Actions
84b4b5f3e5 Automatic update helm docs from VictoriaMetrics/helm-charts@b0095d3 (#7618)
Automated changes by
[create-pull-request](https://github.com/peter-evans/create-pull-request)
GitHub action

Signed-off-by: Github Actions <133988544+victoriametrics-bot@users.noreply.github.com>
Co-authored-by: AndrewChubatiuk <3162380+AndrewChubatiuk@users.noreply.github.com>
2024-11-21 16:04:37 +01:00
hagen1778
a4b3ce9641 docs: mention Raw Query tab in vmui
https://github.com/VictoriaMetrics/VictoriaMetrics/issues/7024
Signed-off-by: hagen1778 <roman@victoriametrics.com>
2024-11-21 16:03:05 +01:00
Yury Molodov
cd0ad293fe vmui: add Raw Query tab (#7575)
### Describe Your Changes

1. **Add new `Raw Query` tab**  
A new `Raw Query` tab has been added to the
[vmui](https://docs.victoriametrics.com/#vmui) interface for displaying
raw data. The tab uses the `/api/v1/export` API endpoint. Related issue:
[#7024](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/7024)


2. **Fix rendering of isolated points on the graph**  
Previously, isolated points (not connected to other points on the left
or right) were not visible on the graph. Now, they are rendered
correctly.
 

### Checklist

The following checks are **mandatory**:

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

---------

Co-authored-by: Roman Khavronenko <roman@victoriametrics.com>
2024-11-21 15:52:50 +01:00
Nikolay
bb399518db app/vmselect: properly return binary pow function result (#7619)
Previously, for `^` aka pow function calls, VictoriaMetrics returned `1`
if left arg was Nan. For example, given query=`(hour()==2)^1` returns 1
for NaN produced by hour() == 2 function. It added additional non-exist
datapoints to the timeseries.

This commit port bugfix from `metricql` package and adds test for it.
Now, VictoriaMetrics
correctly returns `NaN` for such cases.

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

Signed-off-by: f41gh7 <nik@victoriametrics.com>
2024-11-21 15:16:28 +01:00
Hui Wang
1bd927e3fe vmalert: remove deprecated cmd-line flags -datasource.lookback, `da… (#6779)
…tasource.queryTimeAlignment` and `remoteRead.ignoreRestoreErrors`

Those flags were all deprecated before
[v1.101.0](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.101.0).
2024-11-21 13:58:09 +01:00
Artem Fetishev
3383589fd1 lib/storage: confirm that changing retention period can cause previous indexDB deletion (#7569)
### Describe Your Changes

Add test cases proving that it is possible to lose indexDB after
changing the retention period. See #7609

### Checklist

The following checks are **mandatory**:

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

---------

Signed-off-by: Artem Fetishev <rtm@victoriametrics.com>
Co-authored-by: Roman Khavronenko <roman@victoriametrics.com>
2024-11-21 10:44:21 +01:00
Github Actions
f07574a78e Automatic update operator docs from VictoriaMetrics/operator@b3e19a2 (#7608)
Automated changes by
[create-pull-request](https://github.com/peter-evans/create-pull-request)
GitHub action

Signed-off-by: Github Actions <133988544+victoriametrics-bot@users.noreply.github.com>
Co-authored-by: f41gh7 <18450869+f41gh7@users.noreply.github.com>
2024-11-21 09:48:14 +01:00
Github Actions
689196048f Automatic update helm docs from VictoriaMetrics/helm-charts@8a9669e (#7616)
Automated changes by
[create-pull-request](https://github.com/peter-evans/create-pull-request)
GitHub action

Signed-off-by: Github Actions <133988544+victoriametrics-bot@users.noreply.github.com>
Co-authored-by: AndrewChubatiuk <3162380+AndrewChubatiuk@users.noreply.github.com>
2024-11-21 09:47:46 +01:00
Artem Fetishev
61532930e6 Makefile: remove -v flag from integration tests
This is a follow-up for 49fe403af1

 Commit disables the verbosity in integration
tests after confirming that the tests run in both master and cluster
branches.


Signed-off-by: Artem Fetishev <rtm@victoriametrics.com>
2024-11-20 23:47:32 +01:00
Will Jordan
a19a4f34ff lib/tenantmetrics: improves CounterMap performance with large numbers of tenants
Previously, map for storing tenant metrics was re-created to each newly ingested tenant. It has significant performance impact for systems with large number of tenants.

 This commit addresses this issue by changing algorithm of creating tenant metric records at map. Instead of map re-creation, it uses `sync.Map` primitive.

Benchmark results:

```
goos: linux
goarch: amd64
pkg: github.com/VictoriaMetrics/VictoriaMetrics/lib/tenantmetrics
cpu: AMD Ryzen 9 5900X 12-Core Processor
                                            │ lib/tenantmetrics/orig.bench │     lib/tenantmetrics/new.bench     │
                                            │            sec/op            │    sec/op     vs base               │
CounterMapGrowth/n=100,nProcs=GOMAXPROCS-24                  1943.2µ ±  5%   248.0µ ± 11%  -87.24% (p=0.001 n=7)
CounterMapGrowth/n=100-24                                    434.63µ ±  5%   98.82µ ± 16%  -77.26% (p=0.001 n=7)
CounterMapGrowth/n=1000-24                                   32.719m ± 20%   1.425m ±  5%  -95.65% (p=0.001 n=7)
CounterMapGrowth/n=10000-24                                 3653.60m ±  5%   18.00m ±  2%  -99.51% (p=0.001 n=7)
geomean                                                       17.83m         890.4µ        -95.00%
```

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

---
Co-authored-by: Artem Fetishev <rtm@victoriametrics.com>
2024-11-20 18:42:47 +01:00
Dmytro Kozlov
93c63d77c0 deployment/docker: update victorialogs datasource versions to the latest releases (#7604)
### Describe Your Changes

Updated the victorialogs data source version to the v0.8.0 release

### Checklist

The following checks are **mandatory**:

- [x] My change adheres [VictoriaMetrics contributing
guidelines](https://docs.victoriametrics.com/contributing/).
2024-11-20 16:38:20 +01:00
dependabot[bot]
8735fb12fb build(deps): bump codecov/codecov-action from 4 to 5 (#7545)
Bumps
[codecov/codecov-action](https://github.com/codecov/codecov-action) from
4 to 5.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/codecov/codecov-action/releases">codecov/codecov-action's
releases</a>.</em></p>
<blockquote>
<h2>v5.0.0</h2>
<h2>v5 Release</h2>
<p><code>v5</code> of the Codecov GitHub Action will use the <a
href="https://github.com/codecov/wrapper">Codecov Wrapper</a> to
encapsulate the <a
href="https://github.com/codecov/codecov-cli">CLI</a>. This will help
ensure that the Action gets updates quicker.</p>
<h3>Migration Guide</h3>
<p>The <code>v5</code> release also coincides with the opt-out feature
for tokens for public repositories. In the <code>Global Upload
Token</code> section of the settings page of an organization in
codecov.io, you can set the ability for Codecov to receive a coverage
reports from any source. This will allow contributors or other members
of a repository to upload without needing access to the Codecov token.
For more details see <a
href="https://docs.codecov.com/docs/codecov-tokens#uploading-without-a-token">how
to upload without a token</a>.</p>
<blockquote>
<p>[!WARNING]<br />
<strong>The following arguments have been changed</strong></p>
<ul>
<li><code>file</code> (this has been deprecated in favor of
<code>files</code>)</li>
<li><code>plugin</code> (this has been deprecated in favor of
<code>plugins</code>)</li>
</ul>
</blockquote>
<p>The following arguments have been added:</p>
<ul>
<li><code>binary</code></li>
<li><code>gcov_args</code></li>
<li><code>gcov_executable</code></li>
<li><code>gcov_ignore</code></li>
<li><code>gcov_include</code></li>
<li><code>report_type</code></li>
<li><code>skip_validation</code></li>
<li><code>swift_project</code></li>
</ul>
<p>You can see their usage in the <code>action.yml</code> <a
href="https://github.com/codecov/codecov-action/blob/main/action.yml">file</a>.</p>
<h2>What's Changed</h2>
<ul>
<li>chore(deps): bump to eslint9+ and remove eslint-config-google by <a
href="https://github.com/thomasrockhu-codecov"><code>@​thomasrockhu-codecov</code></a>
in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1591">codecov/codecov-action#1591</a></li>
<li>build(deps-dev): bump <code>@​octokit/webhooks-types</code> from
7.5.1 to 7.6.1 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1595">codecov/codecov-action#1595</a></li>
<li>build(deps-dev): bump typescript from 5.6.2 to 5.6.3 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1604">codecov/codecov-action#1604</a></li>
<li>build(deps-dev): bump <code>@​typescript-eslint/parser</code> from
8.8.0 to 8.8.1 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1601">codecov/codecov-action#1601</a></li>
<li>build(deps): bump <code>@​actions/core</code> from 1.11.0 to 1.11.1
by <a href="https://github.com/dependabot"><code>@​dependabot</code></a>
in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1597">codecov/codecov-action#1597</a></li>
<li>build(deps): bump github/codeql-action from 3.26.9 to 3.26.11 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1596">codecov/codecov-action#1596</a></li>
<li>build(deps-dev): bump <code>@​typescript-eslint/eslint-plugin</code>
from 8.8.0 to 8.8.1 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1600">codecov/codecov-action#1600</a></li>
<li>build(deps-dev): bump eslint from 9.11.1 to 9.12.0 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1598">codecov/codecov-action#1598</a></li>
<li>build(deps): bump github/codeql-action from 3.26.11 to 3.26.12 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1609">codecov/codecov-action#1609</a></li>
<li>build(deps): bump actions/checkout from 4.2.0 to 4.2.1 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1608">codecov/codecov-action#1608</a></li>
<li>build(deps): bump actions/upload-artifact from 4.4.0 to 4.4.3 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1607">codecov/codecov-action#1607</a></li>
<li>build(deps-dev): bump <code>@​typescript-eslint/parser</code> from
8.8.1 to 8.9.0 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1612">codecov/codecov-action#1612</a></li>
<li>build(deps-dev): bump <code>@​typescript-eslint/eslint-plugin</code>
from 8.8.1 to 8.9.0 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1611">codecov/codecov-action#1611</a></li>
<li>build(deps-dev): bump <code>@​typescript-eslint/eslint-plugin</code>
from 8.9.0 to 8.10.0 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1615">codecov/codecov-action#1615</a></li>
<li>build(deps-dev): bump eslint from 9.12.0 to 9.13.0 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1618">codecov/codecov-action#1618</a></li>
<li>build(deps): bump github/codeql-action from 3.26.12 to 3.26.13 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1617">codecov/codecov-action#1617</a></li>
<li>build(deps-dev): bump <code>@​typescript-eslint/parser</code> from
8.9.0 to 8.10.0 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1614">codecov/codecov-action#1614</a></li>
<li>build(deps-dev): bump <code>@​typescript-eslint/eslint-plugin</code>
from 8.10.0 to 8.11.0 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1620">codecov/codecov-action#1620</a></li>
<li>build(deps-dev): bump <code>@​typescript-eslint/parser</code> from
8.10.0 to 8.11.0 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1619">codecov/codecov-action#1619</a></li>
<li>build(deps-dev): bump <code>@​types/jest</code> from 29.5.13 to
29.5.14 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1622">codecov/codecov-action#1622</a></li>
<li>build(deps): bump actions/checkout from 4.2.1 to 4.2.2 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1625">codecov/codecov-action#1625</a></li>
<li>build(deps): bump github/codeql-action from 3.26.13 to 3.27.0 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1624">codecov/codecov-action#1624</a></li>
<li>build(deps-dev): bump <code>@​typescript-eslint/eslint-plugin</code>
from 8.11.0 to 8.12.1 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1626">codecov/codecov-action#1626</a></li>
<li>build(deps-dev): bump <code>@​typescript-eslint/eslint-plugin</code>
from 8.12.1 to 8.12.2 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1629">codecov/codecov-action#1629</a></li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md">codecov/codecov-action's
changelog</a>.</em></p>
<blockquote>
<h2>4.0.0-beta.2</h2>
<h3>Fixes</h3>
<ul>
<li><a
href="https://redirect.github.com/codecov/codecov-action/issues/1085">#1085</a>
not adding -n if empty to do-upload command</li>
</ul>
<h2>4.0.0-beta.1</h2>
<p><code>v4</code> represents a move from the <a
href="https://github.com/codecov/uploader">universal uploader</a> to the
<a href="https://github.com/codecov/codecov-cli">Codecov CLI</a>.
Although this will unlock new features for our users, the CLI is not yet
at feature parity with the universal uploader.</p>
<h3>Breaking Changes</h3>
<ul>
<li>No current support for <code>aarch64</code> and <code>alpine</code>
architectures.</li>
<li>Tokenless uploading is unsuported</li>
<li>Various arguments to the Action have been removed</li>
</ul>
<h2>3.1.4</h2>
<h3>Fixes</h3>
<ul>
<li><a
href="https://redirect.github.com/codecov/codecov-action/issues/967">#967</a>
Fix typo in README.md</li>
<li><a
href="https://redirect.github.com/codecov/codecov-action/issues/971">#971</a>
fix: add back in working dir</li>
<li><a
href="https://redirect.github.com/codecov/codecov-action/issues/969">#969</a>
fix: CLI option names for uploader</li>
</ul>
<h3>Dependencies</h3>
<ul>
<li><a
href="https://redirect.github.com/codecov/codecov-action/issues/970">#970</a>
build(deps-dev): bump <code>@​types/node</code> from 18.15.12 to
18.16.3</li>
<li><a
href="https://redirect.github.com/codecov/codecov-action/issues/979">#979</a>
build(deps-dev): bump <code>@​types/node</code> from 20.1.0 to
20.1.2</li>
<li><a
href="https://redirect.github.com/codecov/codecov-action/issues/981">#981</a>
build(deps-dev): bump <code>@​types/node</code> from 20.1.2 to
20.1.4</li>
</ul>
<h2>3.1.3</h2>
<h3>Fixes</h3>
<ul>
<li><a
href="https://redirect.github.com/codecov/codecov-action/issues/960">#960</a>
fix: allow for aarch64 build</li>
</ul>
<h3>Dependencies</h3>
<ul>
<li><a
href="https://redirect.github.com/codecov/codecov-action/issues/957">#957</a>
build(deps-dev): bump jest-junit from 15.0.0 to 16.0.0</li>
<li><a
href="https://redirect.github.com/codecov/codecov-action/issues/958">#958</a>
build(deps): bump openpgp from 5.7.0 to 5.8.0</li>
<li><a
href="https://redirect.github.com/codecov/codecov-action/issues/959">#959</a>
build(deps-dev): bump <code>@​types/node</code> from 18.15.10 to
18.15.12</li>
</ul>
<h2>3.1.2</h2>
<h3>Fixes</h3>
<ul>
<li><a
href="https://redirect.github.com/codecov/codecov-action/issues/718">#718</a>
Update README.md</li>
<li><a
href="https://redirect.github.com/codecov/codecov-action/issues/851">#851</a>
Remove unsupported path_to_write_report argument</li>
<li><a
href="https://redirect.github.com/codecov/codecov-action/issues/898">#898</a>
codeql-analysis.yml</li>
<li><a
href="https://redirect.github.com/codecov/codecov-action/issues/901">#901</a>
Update README to contain correct information - inputs and negate
feature</li>
<li><a
href="https://redirect.github.com/codecov/codecov-action/issues/955">#955</a>
fix: add in all the extra arguments for uploader</li>
</ul>
<h3>Dependencies</h3>
<ul>
<li><a
href="https://redirect.github.com/codecov/codecov-action/issues/819">#819</a>
build(deps): bump openpgp from 5.4.0 to 5.5.0</li>
<li><a
href="https://redirect.github.com/codecov/codecov-action/issues/835">#835</a>
build(deps): bump node-fetch from 3.2.4 to 3.2.10</li>
<li><a
href="https://redirect.github.com/codecov/codecov-action/issues/840">#840</a>
build(deps): bump ossf/scorecard-action from 1.1.1 to 2.0.4</li>
<li><a
href="https://redirect.github.com/codecov/codecov-action/issues/841">#841</a>
build(deps): bump <code>@​actions/core</code> from 1.9.1 to 1.10.0</li>
<li><a
href="https://redirect.github.com/codecov/codecov-action/issues/843">#843</a>
build(deps): bump <code>@​actions/github</code> from 5.0.3 to 5.1.1</li>
<li><a
href="https://redirect.github.com/codecov/codecov-action/issues/869">#869</a>
build(deps): bump node-fetch from 3.2.10 to 3.3.0</li>
<li><a
href="https://redirect.github.com/codecov/codecov-action/issues/872">#872</a>
build(deps-dev): bump jest-junit from 13.2.0 to 15.0.0</li>
<li><a
href="https://redirect.github.com/codecov/codecov-action/issues/879">#879</a>
build(deps): bump decode-uri-component from 0.2.0 to 0.2.2</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="968872560f"><code>9688725</code></a>
Update README.md</li>
<li><a
href="2112eaec1b"><code>2112eae</code></a>
chore(deps): bump wrapper to 0.0.23 (<a
href="https://redirect.github.com/codecov/codecov-action/issues/1644">#1644</a>)</li>
<li><a
href="193421c5b3"><code>193421c</code></a>
fixL use the correct source (<a
href="https://redirect.github.com/codecov/codecov-action/issues/1642">#1642</a>)</li>
<li><a
href="6018df70b0"><code>6018df7</code></a>
fix: update container builds (<a
href="https://redirect.github.com/codecov/codecov-action/issues/1640">#1640</a>)</li>
<li><a
href="eff1a643d6"><code>eff1a64</code></a>
fix: add missing vars (<a
href="https://redirect.github.com/codecov/codecov-action/issues/1638">#1638</a>)</li>
<li><a
href="4582d54fd3"><code>4582d54</code></a>
Update README.md (<a
href="https://redirect.github.com/codecov/codecov-action/issues/1639">#1639</a>)</li>
<li><a
href="bb7467c2bc"><code>bb7467c</code></a>
feat: use wrapper (<a
href="https://redirect.github.com/codecov/codecov-action/issues/1621">#1621</a>)</li>
<li><a
href="1d6059880c"><code>1d60598</code></a>
build(deps-dev): bump <code>@​typescript-eslint/eslint-plugin</code>
from 8.12.2 to 8.13.0 ...</li>
<li><a
href="e587ce276e"><code>e587ce2</code></a>
build(deps-dev): bump <code>@​typescript-eslint/parser</code> from
8.12.2 to 8.13.0 (<a
href="https://redirect.github.com/codecov/codecov-action/issues/1635">#1635</a>)</li>
<li><a
href="e43f28e103"><code>e43f28e</code></a>
build(deps-dev): bump <code>@​typescript-eslint/parser</code> from
8.11.0 to 8.12.2 (<a
href="https://redirect.github.com/codecov/codecov-action/issues/1628">#1628</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/codecov/codecov-action/compare/v4...v5">compare
view</a></li>
</ul>
</details>
<br />


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

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

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

---

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

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Roman Khavronenko <roman@victoriametrics.com>
2024-11-20 16:37:54 +01:00
Alexander Frolov
7454d938cc app/vmagent: respect Pushgateway protocol in multi-tenant vmagent handler (#7571)
### Describe Your Changes

fixes https://github.com/VictoriaMetrics/VictoriaMetrics/issues/3636 for
multi-tenant vmagent handler

### Checklist

The following checks are **mandatory**:

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

---------

Co-authored-by: Roman Khavronenko <roman@victoriametrics.com>
2024-11-20 16:34:25 +01:00
Artem Fetishev
49fe403af1 apptest: typical cluster configuration for business logic tests
Add the ability to create a simple cluster configuration for tests that
do not verify the cluster-specific behavior but instead are focused on
the business logic tests, such as API surface or MetricsQL. For such
tests this cluster configuration will be enough in most cases.

Cluster-specific tests should continue creating custom configurations.

---------

Signed-off-by: Artem Fetishev <rtm@victoriametrics.com>
2024-11-20 16:30:55 +01:00
Hui Wang
580fb3ad85 doc: clarify rule group type (#7595)
address https://github.com/VictoriaMetrics/VictoriaMetrics/issues/7585
2024-11-20 16:23:13 +01:00
Renato Monteiro
4485877a83 docs: fix small grammatical error in CONTRIBUTING.md (#7596) 2024-11-20 16:22:42 +01:00
Github Actions
a8997e97de Automatic update operator docs from VictoriaMetrics/operator@c2b7a5a (#7598)
Automated changes by
[create-pull-request](https://github.com/peter-evans/create-pull-request)
GitHub action

Signed-off-by: Github Actions <133988544+victoriametrics-bot@users.noreply.github.com>
Co-authored-by: f41gh7 <18450869+f41gh7@users.noreply.github.com>
2024-11-20 16:21:49 +01:00
Github Actions
d3316b333d Automatic update Grafana datasource docs from VictoriaMetrics/victorialogs-datasource@c4d6669 (#7602) 2024-11-20 16:21:12 +01:00
Hui Wang
71f521fc0c vmalert: revert the default value of -remoteWrite.maxQueueSize from… (#7570)
… `1_000_000` to `100_000`

It was bumped in
[v1.104.0](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.104.0),
which increases memory usage and is not needed for most setups. See
[this
issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/7471).
2024-11-20 16:20:51 +01:00
Github Actions
6bf49aef03 Automatic update operator docs from VictoriaMetrics/operator@1e7c2c5 (#7582)
Automated changes by
[create-pull-request](https://github.com/peter-evans/create-pull-request)
GitHub action

Signed-off-by: Github Actions <133988544+victoriametrics-bot@users.noreply.github.com>
Co-authored-by: Haleygo <39937150+Haleygo@users.noreply.github.com>
2024-11-19 16:40:51 +01:00
Github Actions
a8c5035d3d Automatic update helm docs from VictoriaMetrics/helm-charts@131846f (#7581)
Automated changes by
[create-pull-request](https://github.com/peter-evans/create-pull-request)
GitHub action

Signed-off-by: Github Actions <133988544+victoriametrics-bot@users.noreply.github.com>
Co-authored-by: AndrewChubatiuk <3162380+AndrewChubatiuk@users.noreply.github.com>
2024-11-19 16:40:20 +01:00
Mathias Palmersheim
43a45cf4e3 Updated vmanomaly product link (#7580)
### 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**:

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

fixes #7579 7579
2024-11-18 15:56:48 -06:00
Fred Navruzov
a534df6cf3 docs/vmanomaly: add self-monitoring section (#7558)
### Describe Your Changes

- Added self-monitoring guide for `vmanomaly`.
- Added cross-referencing on other pages.
- Slight improvements in wording on related pages
- Update references to v1.18.4
- [x] publish Grafana dashboard to
https://grafana.com/orgs/victoriametrics/dashboards:
https://grafana.com/grafana/dashboards/22337-victoriametrics-vmanomaly/

@AndrewChubatiuk , JFYI if it somehow impacts your work on supporting
`vmanomaly` in operator.

### Checklist

The following checks are **mandatory**:

- [x] My change adheres [VictoriaMetrics contributing
guidelines](https://docs.victoriametrics.com/contributing/).
2024-11-18 20:14:46 +02:00
JAYICE
a696ef21df docs: correct the wrong description of vector support in the docs (#7566)
### Describe Your Changes

https://github.com/VictoriaMetrics/VictoriaMetrics/issues/7557

Vector only supports OpenTelemetry as a source.
2024-11-18 16:30:43 +01:00
f41gh7
d4d3ec877e {deployment/,docs/}: point app versions to the lastest v1.106.1 release
Signed-off-by: f41gh7 <nik@victoriametrics.com>
2024-11-18 15:07:30 +01:00
f41gh7
4ade6f9d25 docs/changelog: mention 1.106.1,1.102.7 and 1.97.12 releases
Signed-off-by: f41gh7 <nik@victoriametrics.com>
2024-11-18 14:33:04 +01:00
Hui Wang
37f1c76e71 add vlogs type of rule in example (#7548) 2024-11-18 14:28:16 +08:00
f41gh7
4b74baf696 CHANGELOG.md: cut v1.106.1 release
remove upcoming v1.107.0 release from changelog
2024-11-15 23:38:06 +01:00
f41gh7
d5f52adf3d make vmui-update 2024-11-15 19:21:51 +01:00
f41gh7
0abefae46c CHANGELOG.md: cut v1.107.0 release 2024-11-15 18:06:48 +01:00
Zakhar Bessarab
11af902b22 lib/storage/downsampling: handle dedup separately from downsampling rules with filters
Previously, dedup was added as a downsampling rule with 0s offset to all downsmapling rules with filters. That enforced a metric name lookup even in cases it is not needed.
For example, the following configuration: `-dedup.minScrapeInterval=10s -downsampling.period={__name__=~"node.*"}:1h:1m` would be parsed as: `{__name__=~"node.*"}:1h:1m {}:0s:10s`

This commit changes this logic and treats dedup as a separate case. This allows to perform metric name lookups only in cases when timestamp of current partition can be eligible to use some of downsampling filters. Newer parts will not trigger metric name lookup and will apply deduplication directly.

Related issue:
https://github.com/VictoriaMetrics/VictoriaMetrics/issues/7440
---------
Signed-off-by: Zakhar Bessarab <z.bessarab@victoriametrics.com>
Co-authored-by: f41gh7 <nik@victoriametrics.com>
2024-11-15 17:56:47 +01:00
Nikolay
1985110de2 lib/storage: properly check for minMissingTimestamps
After changes at commit 787b9cd. Minimal timestamps for extDB check was performed without context of the index search prefix.
It worked fine for Single node version, but for cluster version a different prefix was used for
metricID search requests. It may lead to incomplete results, if minimal missing timestamp was cached
for the tenant with different ingestion patterns.

 Minimal reproducible case is:
- metrics were ingested for tenants 0 and 1
- at some point in time metrics ingestion for tenant 1 stopped
- index records have the following timestamps layout:
 tenant 0:  1,2,3,4,5,6
 tenant 1:  1,2,3,4
- after indexDB rotation, containsTimeRange lookups may produce
  incorrect results:
 time range request for tenant 1 - 5:6 caches 5 as min timestamp
 request for the same or smaller time range for tenant 0 now returns
empty results.

Second case:
- requests for the tenant without metrics always updates atomic value with incorrect minimal time range for other tenants.

 This commit replaces single atomic with map of search prefix keys. It should have slight performance overhead,
but work consistently for cluster version. minMissingTimestamp is cached by prefix search key, which included tenantID.

 Since it will be only populated at runtime, it doesn't hold unused tenants for queries.

Related issue:
https://github.com/VictoriaMetrics/VictoriaMetrics/issues/7417
2024-11-15 16:25:13 +01:00
Andrei Baidarov
479ae93e04 app/vmselect: fixes possible panics for multitenant queries
This commit fixes panic for multitenant requests and empty storage node responses for tenants api.

 It also optimizes `populateSqTenantTokensIfNeeded` function calls, by making it only once for query request. Previously it was incorrectly called multiple times per each storage node request.

Related issue:
https://github.com/VictoriaMetrics/VictoriaMetrics/issues/7549
---------
Signed-off-by: f41gh7 <nik@victoriametrics.com>
Co-authored-by: f41gh7 <nik@victoriametrics.com>
2024-11-15 16:22:30 +01:00
Aliaksandr Valialkin
fc0b6c62fe docs: refer to the https://itnext.io/how-do-open-source-solutions-for-logs-work-elasticsearch-loki-and-victorialogs-9f7097ecbc2f in the appropriate places 2024-11-15 14:29:01 +01:00
Aliaksandr Valialkin
47a52f5f40 docs/Articles.md: fix broken link to Open-sourcing VictoriaMetrics article 2024-11-15 14:15:13 +01:00
andriibeee
5d85968659 app/vmauth: fix unauthorized_user routing inconsistency
This commit makes vmauth respect the routing config for unauthorized
requests for requests that despite having Authorization header failed to
authorize successfully.

 It covers the following use-cases:
- vmauth is used at load-balanacer and must forward requests as is. There is no any authorization configs.
- vmauth has authorization config, but it must forward requests with invalid credential tokens to some other backend.

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

---------
Signed-off-by: Andrii <andriibeee@gmail.com>
2024-11-15 12:28:25 +01:00
Fred Navruzov
a335ed23c7 docs/vmanomaly - patch release 1.18.3 (#7540)
### Describe Your Changes

`vmanomaly` docs update with patch release 1.18.3

### Checklist

The following checks are **mandatory**:

- [x] My change adheres [VictoriaMetrics contributing
guidelines](https://docs.victoriametrics.com/contributing/).
2024-11-14 19:59:13 +02:00
Hui Wang
18afeff742 app/vmalert: fix flaky ut TestRecordingRule_Exec
The order of stale metrics can't be controlled in recording rule, only
use two time series then.
2024-11-14 15:30:39 +01:00
hagen1778
6b903d79a9 dashboards: rename datapoints to logs for vlogs dashboard
Logs has more clear menaing than `datapoints` in this case.

Signed-off-by: hagen1778 <roman@victoriametrics.com>
2024-11-14 07:14:39 -07:00
Hui Wang
b09272ccac app/vmalert: improve performances when rules produce large volumes of results
1. Avoid storing the last evaluation results outside of rules, check for
stale time series as soon as possible;
2. remove duplicated template `Clone()`.

This pull request is primarily reducing memory usage when rules produce
large volumes of results, as seen in
https://github.com/VictoriaMetrics/VictoriaMetrics/issues/6894.
The CPU time spent on garbage collection remains high and may be
addressed in a separate PR.
2024-11-14 12:23:39 +01:00
Zhu Jiekun
0a6d58b4ca app/vmctl: add command-line flag list to doc
This commit  adds documentation for  **Command-line flags** of vmctl.

---
Related issue: 
https://github.com/VictoriaMetrics/VictoriaMetrics/issues/7521
2024-11-14 11:23:48 +01:00
Hui Wang
304996bc08 docs/vmalert: clarify some vmalert flags
Some flags are shared between datasourceURL and remoteReadURL, some
flags are not valid for victoriaLogs as the datasource.
2024-11-14 11:21:35 +01:00
Crazygit
5eb6a0f9da docs/vlogs: fixes typo for VL-Extra-Field
`VL-Extra-Field` should changed to `VL-Extra-Fields` according to code
2024-11-14 11:20:30 +01:00
Artem Navoiev
1cf5cf05db docs: vmbackupmanager specify schedule
Signed-off-by: Artem Navoiev <tenmozes@gmail.com>
2024-11-14 10:43:37 +01:00
Artem Navoiev
e5b4812d77 dashboards: fix read queries graph
Signed-off-by: Artem Navoiev <tenmozes@gmail.com>
2024-11-14 10:24:29 +01:00
Fred Navruzov
5696a087b8 docs/vmanomaly - release 1.18.2 (#7533)
### Describe Your Changes

docs update for `vmanomaly` release 1.18.2

### Checklist

The following checks are **mandatory**:

- [x] My change adheres [VictoriaMetrics contributing
guidelines](https://docs.victoriametrics.com/contributing/).
2024-11-13 18:52:20 +02:00
hagen1778
02e5fb81c5 dashboards: make dashboards-sync after 683f8c2780
Signed-off-by: hagen1778 <roman@victoriametrics.com>
2024-11-12 17:58:05 -07:00
hagen1778
6fdc111fd2 dashboards: set Y-min to 0 for stats panel with range queries
Y-min set to 0 gives better understanding of changes, as it shows absolute change.
Otherwise, the panel will show relative change and could make a false impression
of the changes.

Other panels in dashboards are either instant (no historical data displayed),
or already set to Y-min: 0.

Signed-off-by: hagen1778 <roman@victoriametrics.com>
2024-11-12 17:56:17 -07:00
Aliaksandr Valialkin
7e02cb484c deployment/docker: update VictoriaLogs from v0.42.0-victorialogs to v1.0.0-victorialogs
See https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.0.0-victorialogs
2024-11-12 19:15:57 +01:00
Aliaksandr Valialkin
99607e2f3b docs/VictoriaLogs/CHANGELOG.md: mention that VictoriaLogs gained all the planned features 2024-11-12 19:07:09 +01:00
Aliaksandr Valialkin
1bf58b2f13 docs/VictoriaLogs: cut v1.0.0-victorialogs 2024-11-12 18:31:59 +01:00
Aliaksandr Valialkin
4837dc6e09 docs/VictoriaLogs/querying/vlogscli.md: replace vlogsql with vlogscli everywhere in the docs
`vlogsql` was the initial name of the command-line utility for interactive querying of logs.
Later it was renamed to `vlogscli`.

Thanks to @grinapo for the bugreport at https://github.com/VictoriaMetrics/VictoriaMetrics/issues/7512
2024-11-12 18:21:00 +01:00
Aliaksandr Valialkin
33a4f275b1 app/vmauth: properly inherit user-level options at url_map when url_prefix isnt set at the user level
The following user-level options must be unconditionally inherited by url_map, since this is what most users expect:

- retry_status_codes
- load_balancing_policy
- drop_src_path_prefix_parts
- discover_backend_ips

Updates https://github.com/VictoriaMetrics/VictoriaMetrics/issues/7519
2024-11-12 17:53:19 +01:00
Hui Wang
32b89447ae dashboards: add file label filter to vmalert dashboard panels (#7515)
Previously, metrics from groups with the same name but in different
files could be mixed in the results.

e.g. the evaluation time
[here](https://grafana.maas.victoriametrics.com/d/LzldHAVnz/victoriametrics-vmalert?orgId=1&var-ds=PE8D8DB4BEE4E4B22&var-job=All&var-instance=All&var-file=%2Fetc%2Fvmalert%2Fconfig%2Fvm-per-tenant-rulefiles-0%2Fmaas-tenant-1011-maas-1011-vm-health.yaml&var-group=All&var-topk=5&editPanel=23)
is the total for multiple groups from different tenants.
2024-11-12 09:00:39 -07:00
Github Actions
254d9e2729 Automatic update helm docs from VictoriaMetrics/helm-charts@ae2dabc (#7522)
Automated changes by
[create-pull-request](https://github.com/peter-evans/create-pull-request)
GitHub action

Signed-off-by: Github Actions <133988544+victoriametrics-bot@users.noreply.github.com>
Co-authored-by: AndrewChubatiuk <3162380+AndrewChubatiuk@users.noreply.github.com>
2024-11-12 09:00:19 -07:00
Github Actions
8b0f5b2315 Automatic update operator docs from VictoriaMetrics/operator@593d523 (#7510)
Automated changes by
[create-pull-request](https://github.com/peter-evans/create-pull-request)
GitHub action

Signed-off-by: Github Actions <133988544+victoriametrics-bot@users.noreply.github.com>
Co-authored-by: Amper <495795+Amper@users.noreply.github.com>
2024-11-12 08:59:51 -07:00
Zhu Jiekun
58ae3772fe vmctl: add docs migrating from cluster to single with --vm-native-disable-binary-protocol (#7520)
### Describe Your Changes

when migrating from cluster to single, the native export API attach
`vm_account_id` and `vm_project_id` labels.
`--vm-native-disable-binary-protocol` is a workaround to remove them,
which is available since v1.93.0 but did not documented in vmctl doc.

See: https://github.com/VictoriaMetrics/VictoriaMetrics/pull/4716

### Checklist

The following checks are **mandatory**:

- [x] My change adheres [VictoriaMetrics contributing
guidelines](https://docs.victoriametrics.com/contributing/).
2024-11-12 08:59:04 -07:00
Artem Fetishev
58dae07b7a tests: cover key concepts with more tests (#7516)
More key concept tests

-  Verify how the time range points are calculated
-  Vefify that a range query is equivalent to many instant queries

Fix docs accordingly.

### Checklist

The following checks are **mandatory**:

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

Signed-off-by: Artem Fetishev <rtm@victoriametrics.com>
2024-11-12 08:57:54 -07:00
Aliaksandr Valialkin
83fc33af89 app/vmauth: simplify the logic for the fix at a0a154511a
The fix at a0a154511a looks too complicated and fragile:

- It moves buMin initialization to the place, which is far from its usage.
- It embeds unclear logic on selecting the proper buMin if it is broken,
  into unrelated loop.

The actual fix must be more clear:

$ git diff 95acca6b52 -- app/vmauth/

-               if n := bu.concurrentRequests.Load(); n < minRequests {
+               if n := bu.concurrentRequests.Load(); n < minRequests || buMin.isBroken() {

This should simplify further maintenance of this code.

Updates https://github.com/VictoriaMetrics/VictoriaMetrics/pull/7489
Updates https://github.com/VictoriaMetrics/VictoriaMetrics/issues/3061
2024-11-12 16:43:07 +01:00
Fred Navruzov
47b7487b5f docs: vmanomaly - release 1.18.1 (#7517)
### Describe Your Changes

doc updates for vmanomaly v1.18.1

### Checklist

The following checks are **mandatory**:

- [x] My change adheres [VictoriaMetrics contributing
guidelines](https://docs.victoriametrics.com/contributing/).
2024-11-12 10:20:29 +02:00
1612 changed files with 430981 additions and 31394 deletions

View File

@@ -88,6 +88,35 @@ jobs:
run: make ${{ matrix.scenario}}
- name: Publish coverage
uses: codecov/codecov-action@v4
uses: codecov/codecov-action@v5
with:
file: ./coverage.txt
integration-test:
name: integration-test
needs: [lint, test]
runs-on: ubuntu-latest
steps:
- name: Code checkout
uses: actions/checkout@v4
- name: Setup Go
id: go
uses: actions/setup-go@v5
with:
cache: false
go-version: stable
- name: Cache Go artifacts
uses: actions/cache@v4
with:
path: |
~/.cache/go-build
~/go/bin
~/go/pkg/mod
key: go-artifacts-${{ runner.os }}-${{ matrix.scenario }}-${{ steps.go.outputs.go-version }}-${{ hashFiles('go.sum', 'Makefile', 'app/**/Makefile') }}
restore-keys: go-artifacts-${{ runner.os }}-${{ matrix.scenario }}-
- name: Run integration tests
run: make integration-test

View File

@@ -10,7 +10,7 @@ import (
"github.com/VictoriaMetrics/VictoriaMetrics/lib/bytesutil"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/decimal"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/prompb"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/prompbmarshal"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/protoparser/prometheus"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/storage"
)
@@ -48,7 +48,7 @@ func selfScraper(scrapeInterval time.Duration) {
var bb bytesutil.ByteBuffer
var rows prometheus.Rows
var mrs []storage.MetricRow
var labels []prompb.Label
var labels []prompbmarshal.Label
t := time.NewTicker(scrapeInterval)
f := func(currentTime time.Time, sendStaleMarkers bool) {
currentTimestamp := currentTime.UnixNano() / 1e6
@@ -99,11 +99,11 @@ func selfScraper(scrapeInterval time.Duration) {
}
}
func addLabel(dst []prompb.Label, key, value string) []prompb.Label {
func addLabel(dst []prompbmarshal.Label, key, value string) []prompbmarshal.Label {
if len(dst) < cap(dst) {
dst = dst[:len(dst)+1]
} else {
dst = append(dst, prompb.Label{})
dst = append(dst, prompbmarshal.Label{})
}
lb := &dst[len(dst)-1]
lb.Name = key

View File

@@ -498,7 +498,7 @@ func processMultitenantRequest(w http.ResponseWriter, r *http.Request, path stri
httpserver.Errorf(w, r, `unsupported multitenant prefix: %q; expected "insert"`, p.Prefix)
return true
}
at, err := auth.NewToken(p.AuthToken)
at, err := auth.NewTokenPossibleMultitenant(p.AuthToken)
if err != nil {
httpserver.Errorf(w, r, "cannot obtain auth token: %s", err)
return true
@@ -510,7 +510,13 @@ func processMultitenantRequest(w http.ResponseWriter, r *http.Request, path stri
httpserver.Errorf(w, r, "%s", err)
return true
}
w.WriteHeader(http.StatusNoContent)
statusCode := http.StatusNoContent
if strings.HasPrefix(p.Suffix, "prometheus/api/v1/import/prometheus/metrics/job/") {
// Return 200 status code for pushgateway requests.
// See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/3636
statusCode = http.StatusOK
}
w.WriteHeader(statusCode)
return true
}
if strings.HasPrefix(p.Suffix, "datadog/") {

View File

@@ -99,9 +99,6 @@ var (
// rwctxsGlobal contains statically populated entries when -remoteWrite.url is specified.
rwctxsGlobal []*remoteWriteCtx
// Data without tenant id is written to defaultAuthToken if -enableMultitenantHandlers is specified.
defaultAuthToken = &auth.Token{}
// ErrQueueFullHTTPRetry must be returned when TryPush() returns false.
ErrQueueFullHTTPRetry = &httpserver.ErrorWithStatusCode{
Err: fmt.Errorf("remote storage systems cannot keep up with the data ingestion rate; retry the request later " +
@@ -209,7 +206,7 @@ func Init() {
initStreamAggrConfigGlobal()
rwctxsGlobal = newRemoteWriteCtxs(nil, *remoteWriteURLs)
rwctxsGlobal = newRemoteWriteCtxs(*remoteWriteURLs)
disableOnDiskQueues := []bool(*disableOnDiskQueue)
disableOnDiskQueueAny = slices.Contains(disableOnDiskQueues, true)
@@ -294,7 +291,7 @@ var (
relabelConfigTimestamp = metrics.NewCounter(`vmagent_relabel_config_last_reload_success_timestamp_seconds`)
)
func newRemoteWriteCtxs(at *auth.Token, urls []string) []*remoteWriteCtx {
func newRemoteWriteCtxs(urls []string) []*remoteWriteCtx {
if len(urls) == 0 {
logger.Panicf("BUG: urls must be non-empty")
}
@@ -316,11 +313,6 @@ func newRemoteWriteCtxs(at *auth.Token, urls []string) []*remoteWriteCtx {
logger.Fatalf("invalid -remoteWrite.url=%q: %s", remoteWriteURL, err)
}
sanitizedURL := fmt.Sprintf("%d:secret-url", i+1)
if at != nil {
// Construct full remote_write url for the given tenant according to https://docs.victoriametrics.com/cluster-victoriametrics/#url-format
remoteWriteURL.Path = fmt.Sprintf("%s/insert/%d:%d/prometheus/api/v1/write", remoteWriteURL.Path, at.AccountID, at.ProjectID)
sanitizedURL = fmt.Sprintf("%s:%d:%d", sanitizedURL, at.AccountID, at.ProjectID)
}
if *showRemoteWriteURL {
sanitizedURL = fmt.Sprintf("%d:%s", i+1, remoteWriteURL)
}
@@ -411,11 +403,6 @@ func TryPush(at *auth.Token, wr *prompbmarshal.WriteRequest) bool {
func tryPush(at *auth.Token, wr *prompbmarshal.WriteRequest, forceDropSamplesOnFailure bool) bool {
tss := wr.Timeseries
if at == nil && MultitenancyEnabled() {
// Write data to default tenant if at isn't set when multitenancy is enabled.
at = defaultAuthToken
}
var tenantRctx *relabelCtx
if at != nil {
// Convert at to (vm_account_id, vm_project_id) labels.

View File

@@ -9,6 +9,7 @@ import (
"time"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/datasource"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/prompbmarshal"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promutils"
"github.com/VictoriaMetrics/metricsql"
)
@@ -48,7 +49,7 @@ Outer:
}
var expSamples []parsedSample
for _, s := range mt.ExpSamples {
expLb := datasource.Labels{}
expLb := []prompbmarshal.Label{}
if s.Labels != "" {
metricsqlExpr, err := metricsql.Parse(s.Labels)
if err != nil {
@@ -64,7 +65,7 @@ Outer:
}
if len(metricsqlMetricExpr.LabelFilterss) > 0 {
for _, l := range metricsqlMetricExpr.LabelFilterss[0] {
expLb = append(expLb, datasource.Label{
expLb = append(expLb, prompbmarshal.Label{
Name: l.Label,
Value: l.Value,
})

View File

@@ -0,0 +1,4 @@
rule_files:
- non-existing-file.yaml
tests: []

View File

@@ -74,8 +74,7 @@ func UnitTest(files []string, disableGroupLabel bool, externalLabels []string, e
logger.Fatalf("failed to load test files %q: %v", files, err)
}
if len(testfiles) == 0 {
fmt.Println("no test file found")
return false
logger.Fatalf("no test file found")
}
labels := make(map[string]string)
@@ -97,8 +96,8 @@ 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("\nfailed to run unit test for file %q: \n%v", fileName, err)
fmt.Println("FAILED")
fmt.Printf("failed to run unit test for file %q: \n%v", fileName, err)
failed = true
} else {
fmt.Println(" SUCCESS")
@@ -109,7 +108,7 @@ func UnitTest(files []string, disableGroupLabel bool, externalLabels []string, e
}
func ruleUnitTest(filename string, content []byte, externalLabels map[string]string) []error {
fmt.Println("\nUnit Testing: ", filename)
fmt.Println("\n\nUnit Testing: ", filename)
var unitTestInp unitTestFile
if err := yaml.UnmarshalStrict(content, &unitTestInp); err != nil {
return []error{fmt.Errorf("failed to unmarshal file: %w", err)}
@@ -139,6 +138,9 @@ func ruleUnitTest(filename string, content []byte, externalLabels map[string]str
if err != nil {
return []error{fmt.Errorf("failed to parse `rule_files`: %w", err)}
}
if len(testGroups) == 0 {
return []error{fmt.Errorf("found no rule group in %v", unitTestInp.RuleFiles)}
}
var errs []error
for _, t := range unitTestInp.Tests {

View File

@@ -24,7 +24,8 @@ func TestUnitTest_Failure(t *testing.T) {
}
}
// failing test
f([]string{"./testdata/failed-test-with-missing-rulefile.yaml"})
f([]string{"./testdata/failed-test.yaml"})
}

View File

@@ -46,8 +46,8 @@ const (
graphitePrefix = "/graphite"
)
func (s *Client) setGraphiteReqParams(r *http.Request, query string) {
if s.appendTypePrefix {
func (c *Client) setGraphiteReqParams(r *http.Request, query string) {
if c.appendTypePrefix {
r.URL.Path += graphitePrefix
}
r.URL.Path += graphitePath
@@ -58,7 +58,7 @@ func (s *Client) setGraphiteReqParams(r *http.Request, query string) {
q.Set("target", query)
q.Set("until", "now")
for k, vs := range s.extraParams {
for k, vs := range c.extraParams {
if q.Has(k) { // extraParams are prior to params in URL
q.Del(k)
}

View File

@@ -9,6 +9,7 @@ import (
"time"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/bytesutil"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/prompbmarshal"
"github.com/valyala/fastjson"
)
@@ -16,7 +17,8 @@ import (
var (
disablePathAppend = flag.Bool("remoteRead.disablePathAppend", false, "Whether to disable automatic appending of '/api/v1/query' or '/select/logsql/stats_query' path "+
"to the configured -datasource.url and -remoteRead.url")
disableStepParam = flag.Bool("datasource.disableStepParam", false, "Whether to disable adding 'step' param to the issued instant queries. "+
disableStepParam = flag.Bool("datasource.disableStepParam", false, "Whether to disable adding 'step' param in instant queries to the configured -datasource.url and -remoteRead.url. "+
"Only valid for prometheus datasource. "+
"This might be useful when using vmalert with datasources that do not support 'step' param for instant queries, like Google Managed Prometheus. "+
"It is not recommended to enable this flag if you use vmalert with VictoriaMetrics.")
)
@@ -81,14 +83,14 @@ func (pi *promInstant) Unmarshal(b []byte) error {
labels := metric.GetObject()
r := &pi.ms[i]
r.Labels = make([]Label, 0, labels.Len())
r.Labels = make([]prompbmarshal.Label, 0, labels.Len())
labels.Visit(func(key []byte, v *fastjson.Value) {
lv, errLocal := v.StringBytes()
if errLocal != nil {
err = fmt.Errorf("error when parsing label value %q: %s", v, errLocal)
return
}
r.Labels = append(r.Labels, Label{
r.Labels = append(r.Labels, prompbmarshal.Label{
Name: string(key),
Value: string(lv),
})
@@ -218,8 +220,8 @@ func parsePrometheusResponse(req *http.Request, resp *http.Response) (res Result
return res, nil
}
func (s *Client) setPrometheusInstantReqParams(r *http.Request, query string, timestamp time.Time) {
if s.appendTypePrefix {
func (c *Client) setPrometheusInstantReqParams(r *http.Request, query string, timestamp time.Time) {
if c.appendTypePrefix {
r.URL.Path += "/prometheus"
}
if !*disablePathAppend {
@@ -227,22 +229,22 @@ func (s *Client) setPrometheusInstantReqParams(r *http.Request, query string, ti
}
q := r.URL.Query()
q.Set("time", timestamp.Format(time.RFC3339))
if !*disableStepParam && s.evaluationInterval > 0 { // set step as evaluationInterval by default
if !*disableStepParam && c.evaluationInterval > 0 { // set step as evaluationInterval by default
// always convert to seconds to keep compatibility with older
// Prometheus versions. See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/1943
q.Set("step", fmt.Sprintf("%ds", int(s.evaluationInterval.Seconds())))
q.Set("step", fmt.Sprintf("%ds", int(c.evaluationInterval.Seconds())))
}
if !*disableStepParam && s.queryStep > 0 { // override step with user-specified value
if !*disableStepParam && c.queryStep > 0 { // override step with user-specified value
// always convert to seconds to keep compatibility with older
// Prometheus versions. See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/1943
q.Set("step", fmt.Sprintf("%ds", int(s.queryStep.Seconds())))
q.Set("step", fmt.Sprintf("%ds", int(c.queryStep.Seconds())))
}
r.URL.RawQuery = q.Encode()
s.setReqParams(r, query)
c.setReqParams(r, query)
}
func (s *Client) setPrometheusRangeReqParams(r *http.Request, query string, start, end time.Time) {
if s.appendTypePrefix {
func (c *Client) setPrometheusRangeReqParams(r *http.Request, query string, start, end time.Time) {
if c.appendTypePrefix {
r.URL.Path += "/prometheus"
}
if !*disablePathAppend {
@@ -251,11 +253,11 @@ func (s *Client) setPrometheusRangeReqParams(r *http.Request, query string, star
q := r.URL.Query()
q.Add("start", start.Format(time.RFC3339))
q.Add("end", end.Format(time.RFC3339))
if s.evaluationInterval > 0 { // set step as evaluationInterval by default
if c.evaluationInterval > 0 { // set step as evaluationInterval by default
// always convert to seconds to keep compatibility with older
// Prometheus versions. See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/1943
q.Set("step", fmt.Sprintf("%ds", int(s.evaluationInterval.Seconds())))
q.Set("step", fmt.Sprintf("%ds", int(c.evaluationInterval.Seconds())))
}
r.URL.RawQuery = q.Encode()
s.setReqParams(r, query)
c.setReqParams(r, query)
}

View File

@@ -14,6 +14,7 @@ import (
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/utils"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promauth"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/prompbmarshal"
)
var (
@@ -144,12 +145,12 @@ func TestVMInstantQuery(t *testing.T) {
}
expected := []Metric{
{
Labels: []Label{{Value: "vm_rows", Name: "__name__"}, {Value: "bar", Name: "foo"}},
Labels: []prompbmarshal.Label{{Value: "vm_rows", Name: "__name__"}, {Value: "bar", Name: "foo"}},
Timestamps: []int64{1583786142},
Values: []float64{13763},
},
{
Labels: []Label{{Value: "vm_requests", Name: "__name__"}, {Value: "baz", Name: "foo"}},
Labels: []prompbmarshal.Label{{Value: "vm_requests", Name: "__name__"}, {Value: "baz", Name: "foo"}},
Timestamps: []int64{1583786140},
Values: []float64{2000},
},
@@ -214,7 +215,7 @@ func TestVMInstantQuery(t *testing.T) {
}
exp := []Metric{
{
Labels: []Label{{Value: "constantLine(10)", Name: "name"}},
Labels: []prompbmarshal.Label{{Value: "constantLine(10)", Name: "name"}},
Timestamps: []int64{1611758403},
Values: []float64{10},
},
@@ -236,12 +237,12 @@ func TestVMInstantQuery(t *testing.T) {
}
expected = []Metric{
{
Labels: []Label{{Value: "total", Name: "stats_result"}, {Value: "bar", Name: "foo"}},
Labels: []prompbmarshal.Label{{Value: "total", Name: "stats_result"}, {Value: "bar", Name: "foo"}},
Timestamps: []int64{1583786142},
Values: []float64{13763},
},
{
Labels: []Label{{Value: "total", Name: "stats_result"}, {Value: "baz", Name: "foo"}},
Labels: []prompbmarshal.Label{{Value: "total", Name: "stats_result"}, {Value: "baz", Name: "foo"}},
Timestamps: []int64{1583786140},
Values: []float64{2000},
},
@@ -444,7 +445,7 @@ func TestVMRangeQuery(t *testing.T) {
t.Fatalf("expected 1 metric got %d in %+v", len(m), m)
}
expected := Metric{
Labels: []Label{{Value: "vm_rows", Name: "__name__"}},
Labels: []prompbmarshal.Label{{Value: "vm_rows", Name: "__name__"}},
Timestamps: []int64{1583786142},
Values: []float64{13763},
}
@@ -475,7 +476,7 @@ func TestVMRangeQuery(t *testing.T) {
t.Fatalf("expected 1 metric got %d in %+v", len(m), m)
}
expected = Metric{
Labels: []Label{{Value: "total", Name: "stats_result"}},
Labels: []prompbmarshal.Label{{Value: "total", Name: "stats_result"}},
Timestamps: []int64{1583786142},
Values: []float64{10},
}

View File

@@ -6,7 +6,7 @@ import (
"time"
)
func (s *Client) setVLogsInstantReqParams(r *http.Request, query string, timestamp time.Time) {
func (c *Client) setVLogsInstantReqParams(r *http.Request, query string, timestamp time.Time) {
// there is no type path prefix in victorialogs APIs right now, ignore appendTypePrefix.
if !*disablePathAppend {
r.URL.Path += "/select/logsql/stats_query"
@@ -16,15 +16,15 @@ func (s *Client) setVLogsInstantReqParams(r *http.Request, query string, timesta
q.Set("time", timestamp.Format(time.RFC3339))
// set the `start` and `end` params if applyIntervalAsTimeFilter is enabled(time filter is missing in the rule expr),
// so the query will be executed in time range [timestamp - evaluationInterval, timestamp].
if s.applyIntervalAsTimeFilter && s.evaluationInterval > 0 {
q.Set("start", timestamp.Add(-s.evaluationInterval).Format(time.RFC3339))
if c.applyIntervalAsTimeFilter && c.evaluationInterval > 0 {
q.Set("start", timestamp.Add(-c.evaluationInterval).Format(time.RFC3339))
q.Set("end", timestamp.Format(time.RFC3339))
}
r.URL.RawQuery = q.Encode()
s.setReqParams(r, query)
c.setReqParams(r, query)
}
func (s *Client) setVLogsRangeReqParams(r *http.Request, query string, start, end time.Time) {
func (c *Client) setVLogsRangeReqParams(r *http.Request, query string, start, end time.Time) {
// there is no type path prefix in victorialogs APIs right now, ignore appendTypePrefix.
if !*disablePathAppend {
r.URL.Path += "/select/logsql/stats_query_range"
@@ -33,11 +33,11 @@ func (s *Client) setVLogsRangeReqParams(r *http.Request, query string, start, en
q.Add("start", start.Format(time.RFC3339))
q.Add("end", end.Format(time.RFC3339))
// set step as evaluationInterval by default
if s.evaluationInterval > 0 {
q.Set("step", fmt.Sprintf("%ds", int(s.evaluationInterval.Seconds())))
if c.evaluationInterval > 0 {
q.Set("step", fmt.Sprintf("%ds", int(c.evaluationInterval.Seconds())))
}
r.URL.RawQuery = q.Encode()
s.setReqParams(r, query)
c.setReqParams(r, query)
}
func parseVLogsResponse(req *http.Request, resp *http.Response) (res Result, err error) {

View File

@@ -8,6 +8,8 @@ import (
"sort"
"strconv"
"time"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/prompbmarshal"
)
// Querier interface wraps Query and QueryRange methods
@@ -55,7 +57,7 @@ type QuerierParams struct {
// Metric is the basic entity which should be return by datasource
type Metric struct {
Labels []Label
Labels []prompbmarshal.Label
Timestamps []int64
Values []float64
}
@@ -72,22 +74,9 @@ func (m *Metric) SetLabel(key, value string) {
m.AddLabel(key, value)
}
// SetLabels sets the given map as Metric labels
func (m *Metric) SetLabels(ls map[string]string) {
var i int
m.Labels = make([]Label, len(ls))
for k, v := range ls {
m.Labels[i] = Label{
Name: k,
Value: v,
}
i++
}
}
// AddLabel appends the given label to the label set
func (m *Metric) AddLabel(key, value string) {
m.Labels = append(m.Labels, Label{Name: key, Value: value})
m.Labels = append(m.Labels, prompbmarshal.Label{Name: key, Value: value})
}
// DelLabel deletes the given label from the label set
@@ -110,14 +99,8 @@ func (m *Metric) Label(key string) string {
return ""
}
// Label represents metric's label
type Label struct {
Name string
Value string
}
// Labels is collection of Label
type Labels []Label
type Labels []prompbmarshal.Label
func (ls Labels) Len() int { return len(ls) }
func (ls Labels) Swap(i, j int) { ls[i], ls[j] = ls[j], ls[i] }
@@ -172,7 +155,7 @@ func LabelCompare(a, b Labels) int {
// ConvertToLabels convert map to Labels
func ConvertToLabels(m map[string]string) (labelset Labels) {
for k, v := range m {
labelset = append(labelset, Label{
labelset = append(labelset, prompbmarshal.Label{
Name: k,
Value: v,
})

View File

@@ -11,7 +11,6 @@ import (
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/utils"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/flagutil"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/httputils"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/netutil"
)
@@ -48,22 +47,15 @@ var (
oauth2TokenURL = flag.String("datasource.oauth2.tokenUrl", "", "Optional OAuth2 tokenURL to use for -datasource.url")
oauth2Scopes = flag.String("datasource.oauth2.scopes", "", "Optional OAuth2 scopes to use for -datasource.url. Scopes must be delimited by ';'")
lookBack = flag.Duration("datasource.lookback", 0, `Deprecated: please adjust "-search.latencyOffset" at datasource side `+
`or specify "latency_offset" in rule group's params. Lookback defines how far into the past to look when evaluating queries. `+
`For example, if the datasource.lookback=5m then param "time" with value now()-5m will be added to every query.`)
queryStep = flag.Duration("datasource.queryStep", 5*time.Minute, "How far a value can fallback to when evaluating queries. "+
queryStep = flag.Duration("datasource.queryStep", 5*time.Minute, "How far a value can fallback to when evaluating queries to the configured -datasource.url and -remoteRead.url. Only valid for prometheus datasource. "+
"For example, if -datasource.queryStep=15s then param \"step\" with value \"15s\" will be added to every query. "+
"If set to 0, rule's evaluation interval will be used instead.")
queryTimeAlignment = flag.Bool("datasource.queryTimeAlignment", true, `Deprecated: please use "eval_alignment" in rule group instead. `+
`Whether to align "time" parameter with evaluation interval. `+
"Alignment supposed to produce deterministic results despite number of vmalert replicas or time they were started. "+
"See more details at https://github.com/VictoriaMetrics/VictoriaMetrics/pull/1257")
maxIdleConnections = flag.Int("datasource.maxIdleConnections", 100, `Defines the number of idle (keep-alive connections) to each configured datasource. Consider setting this value equal to the value: groups_total * group.concurrency. Too low a value may result in a high number of sockets in TIME_WAIT state.`)
idleConnectionTimeout = flag.Duration("datasource.idleConnTimeout", 50*time.Second, `Defines a duration for idle (keep-alive connections) to exist. Consider setting this value less than "-http.idleConnTimeout". It must prevent possible "write: broken pipe" and "read: connection reset by peer" errors.`)
disableKeepAlive = flag.Bool("datasource.disableKeepAlive", false, `Whether to disable long-lived connections to the datasource. `+
`If true, disables HTTP keep-alive and will only use the connection to the server for a single HTTP request.`)
roundDigits = flag.Int("datasource.roundDigits", 0, `Adds "round_digits" GET param to datasource requests. `+
`In VM "round_digits" limits the number of digits after the decimal point in response values.`)
roundDigits = flag.Int("datasource.roundDigits", 0, `Adds "round_digits" GET param to datasource requests which limits the number of digits after the decimal point in response values. `+
`Only valid for VictoriaMetrics as the datasource.`)
)
// InitSecretFlags must be called after flag.Parse and before any logging
@@ -90,12 +82,6 @@ func Init(extraParams url.Values) (QuerierBuilder, error) {
if *addr == "" {
return nil, fmt.Errorf("datasource.url is empty")
}
if !*queryTimeAlignment {
logger.Warnf("flag `-datasource.queryTimeAlignment` is deprecated and will be removed in next releases. Please use `eval_alignment` in rule group instead.")
}
if *lookBack != 0 {
logger.Warnf("flag `-datasource.lookback` is deprecated and will be removed in next releases. Please adjust `-search.latencyOffset` at datasource side or specify `latency_offset` in rule group's params. See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/5155 for details.")
}
tr, err := httputils.Transport(*addr, *tlsCertFile, *tlsKeyFile, *tlsCAFile, *tlsServerName, *tlsInsecureSkipVerify)
if err != nil {

View File

@@ -3,6 +3,8 @@ package datasource
import (
"reflect"
"testing"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/prompbmarshal"
)
func TestPromInstant_UnmarshalPositive(t *testing.T) {
@@ -21,7 +23,7 @@ func TestPromInstant_UnmarshalPositive(t *testing.T) {
f(`[{"metric":{"__name__":"up"},"value":[1583780000,"42"]}]`, []Metric{
{
Labels: []Label{{Name: "__name__", Value: "up"}},
Labels: []prompbmarshal.Label{{Name: "__name__", Value: "up"}},
Timestamps: []int64{1583780000},
Values: []float64{42},
},
@@ -31,17 +33,17 @@ func TestPromInstant_UnmarshalPositive(t *testing.T) {
{"metric":{"__name__":"foo"},"value":[1583780001,"7"]},
{"metric":{"__name__":"baz", "instance":"bar"},"value":[1583780002,"8"]}]`, []Metric{
{
Labels: []Label{{Name: "__name__", Value: "up"}},
Labels: []prompbmarshal.Label{{Name: "__name__", Value: "up"}},
Timestamps: []int64{1583780000},
Values: []float64{42},
},
{
Labels: []Label{{Name: "__name__", Value: "foo"}},
Labels: []prompbmarshal.Label{{Name: "__name__", Value: "foo"}},
Timestamps: []int64{1583780001},
Values: []float64{7},
},
{
Labels: []Label{{Name: "__name__", Value: "baz"}, {Name: "instance", Value: "bar"}},
Labels: []prompbmarshal.Label{{Name: "__name__", Value: "baz"}, {Name: "instance", Value: "bar"}},
Timestamps: []int64{1583780002},
Values: []float64{8},
},

View File

@@ -78,8 +78,6 @@ absolute path to all .tpl files in root.
externalLabels = flagutil.NewArrayString("external.label", "Optional label in the form 'Name=value' to add to all generated recording rules and alerts. "+
"In case of conflicts, original labels are kept with prefix `exported_`.")
remoteReadIgnoreRestoreErrors = flag.Bool("remoteRead.ignoreRestoreErrors", true, "Whether to ignore errors from remote storage when restoring alerts state on startup. DEPRECATED - this flag has no effect and will be removed in the next releases.")
dryRun = flag.Bool("dryRun", false, "Whether to check only config files without running vmalert. The rules file are validated. The -rule flag must be specified.")
)
@@ -97,10 +95,6 @@ func main() {
buildinfo.Init()
logger.Init()
if !*remoteReadIgnoreRestoreErrors {
logger.Warnf("flag `remoteRead.ignoreRestoreErrors` is deprecated and will be removed in next releases.")
}
err := templates.Load(*ruleTemplatesPath, true)
if err != nil {
logger.Fatalf("failed to parse %q: %s", *ruleTemplatesPath, err)

View File

@@ -167,14 +167,8 @@ type tplData struct {
ExternalURL string
}
func templateAnnotation(dst io.Writer, text string, data tplData, tmpl *textTpl.Template, execute bool) error {
tpl, err := tmpl.Clone()
if err != nil {
return fmt.Errorf("error cloning template before parse annotation: %w", err)
}
// Clone() doesn't copy tpl Options, so we set them manually
tpl = tpl.Option("missingkey=zero")
tpl, err = tpl.Parse(text)
func templateAnnotation(dst io.Writer, text string, data tplData, tpl *textTpl.Template, execute bool) error {
tpl, err := tpl.Parse(text)
if err != nil {
return fmt.Errorf("error parsing annotation template: %w", err)
}

View File

@@ -33,7 +33,7 @@ func TestAlertExecTemplate(t *testing.T) {
qFn := func(_ string) ([]datasource.Metric, error) {
return []datasource.Metric{
{
Labels: []datasource.Label{
Labels: []prompbmarshal.Label{
{Name: "foo", Value: "bar"},
{Name: "baz", Value: "qux"},
},
@@ -41,7 +41,7 @@ func TestAlertExecTemplate(t *testing.T) {
Timestamps: []int64{1},
},
{
Labels: []datasource.Label{
Labels: []prompbmarshal.Label{
{Name: "foo", Value: "garply"},
{Name: "baz", Value: "fred"},
},

View File

@@ -14,7 +14,7 @@ import (
)
var (
addr = flag.String("remoteRead.url", "", "Optional URL to datasource compatible with Prometheus HTTP API. It can be single node VictoriaMetrics or vmselect."+
addr = flag.String("remoteRead.url", "", "Optional URL to datasource compatible with MetricsQL. It can be single node VictoriaMetrics or vmselect."+
"Remote read is used to restore alerts state."+
"This configuration makes sense only if `vmalert` was configured with `remoteWrite.url` before and has been successfully persisted its state. "+
"Supports address in the form of IP address with a port (e.g., http://127.0.0.1:8428) or DNS SRV record. "+

View File

@@ -27,7 +27,7 @@ var defaultConcurrency = cgroup.AvailableCPUs() * 2
const (
defaultMaxBatchSize = 1e4
defaultMaxQueueSize = 1e6
defaultMaxQueueSize = 1e5
defaultFlushInterval = 2 * time.Second
defaultWriteTimeout = 30 * time.Second
)

View File

@@ -14,8 +14,10 @@ import (
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/notifier"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/templates"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/utils"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/decimal"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/prompbmarshal"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promrelabel"
)
// AlertingRule is basic alert entity
@@ -454,13 +456,16 @@ func (ar *AlertingRule) exec(ctx context.Context, ts time.Time, limit int) ([]pr
ar.logDebugf(ts, a, "created in state PENDING")
}
var numActivePending int
var tss []prompbmarshal.TimeSeries
for h, a := range ar.alerts {
// if alert wasn't updated in this iteration
// means it is resolved already
if _, ok := updated[h]; !ok {
if a.State == notifier.StatePending {
// alert was in Pending state - it is not
// active anymore
// alert was in Pending state - it is not active anymore
// add stale time series
tss = append(tss, pendingAlertStaleTimeSeries(a.Labels, ts.Unix(), true)...)
delete(ar.alerts, h)
ar.logDebugf(ts, a, "PENDING => DELETED: is absent in current evaluation round")
continue
@@ -478,6 +483,9 @@ func (ar *AlertingRule) exec(ctx context.Context, ts time.Time, limit int) ([]pr
if ts.Sub(a.KeepFiringSince) >= ar.KeepFiringFor {
a.State = notifier.StateInactive
a.ResolvedAt = ts
// add stale time series
tss = append(tss, firingAlertStaleTimeSeries(a.Labels, ts.Unix())...)
ar.logDebugf(ts, a, "FIRING => INACTIVE: is absent in current evaluation round")
continue
}
@@ -489,6 +497,10 @@ func (ar *AlertingRule) exec(ctx context.Context, ts time.Time, limit int) ([]pr
a.State = notifier.StateFiring
a.Start = ts
alertsFired.Inc()
if ar.For > 0 {
// add stale time series
tss = append(tss, pendingAlertStaleTimeSeries(a.Labels, ts.Unix(), false)...)
}
ar.logDebugf(ts, a, "PENDING => FIRING: %s since becoming active at %v", ts.Sub(a.ActiveAt), a.ActiveAt)
}
}
@@ -497,7 +509,7 @@ func (ar *AlertingRule) exec(ctx context.Context, ts time.Time, limit int) ([]pr
curState.Err = fmt.Errorf("exec exceeded limit of %d with %d alerts", limit, numActivePending)
return nil, curState.Err
}
return ar.toTimeSeries(ts.Unix()), nil
return append(tss, ar.toTimeSeries(ts.Unix())...), nil
}
func (ar *AlertingRule) expandTemplates(m datasource.Metric, qFn templates.QueryFn, ts time.Time) (*labelSet, map[string]string, error) {
@@ -522,6 +534,7 @@ func (ar *AlertingRule) expandTemplates(m datasource.Metric, qFn templates.Query
return ls, as, nil
}
// toTimeSeries creates `ALERTS` and `ALERTS_FOR_STATE` for active alerts
func (ar *AlertingRule) toTimeSeries(timestamp int64) []prompbmarshal.TimeSeries {
var tss []prompbmarshal.TimeSeries
for _, a := range ar.alerts {
@@ -601,26 +614,83 @@ func (ar *AlertingRule) alertToTimeSeries(a *notifier.Alert, timestamp int64) []
}
func alertToTimeSeries(a *notifier.Alert, timestamp int64) prompbmarshal.TimeSeries {
labels := make(map[string]string)
var labels []prompbmarshal.Label
for k, v := range a.Labels {
labels[k] = v
labels = append(labels, prompbmarshal.Label{
Name: k,
Value: v,
})
}
// __name__ already been dropped, no need to check duplication
labels = append(labels, prompbmarshal.Label{Name: "__name__", Value: alertMetricName})
if ol := promrelabel.GetLabelByName(labels, alertStateLabel); ol != nil {
ol.Value = a.State.String()
} else {
labels = append(labels, prompbmarshal.Label{Name: alertStateLabel, Value: a.State.String()})
}
labels["__name__"] = alertMetricName
labels[alertStateLabel] = a.State.String()
return newTimeSeries([]float64{1}, []int64{timestamp}, labels)
}
// alertForToTimeSeries returns a timeseries that represents
// alertForToTimeSeries returns a time series that represents
// state of active alerts, where value is time when alert become active
func alertForToTimeSeries(a *notifier.Alert, timestamp int64) prompbmarshal.TimeSeries {
labels := make(map[string]string)
var labels []prompbmarshal.Label
for k, v := range a.Labels {
labels[k] = v
labels = append(labels, prompbmarshal.Label{
Name: k,
Value: v,
})
}
labels["__name__"] = alertForStateMetricName
// __name__ already been dropped, no need to check duplication
labels = append(labels, prompbmarshal.Label{Name: "__name__", Value: alertForStateMetricName})
return newTimeSeries([]float64{float64(a.ActiveAt.Unix())}, []int64{timestamp}, labels)
}
// pendingAlertStaleTimeSeries returns stale `ALERTS` and `ALERTS_FOR_STATE` time series
// for alerts which changed their state from Pending to Inactive or Firing.
func pendingAlertStaleTimeSeries(ls map[string]string, timestamp int64, includeAlertForState bool) []prompbmarshal.TimeSeries {
var result []prompbmarshal.TimeSeries
var baseLabels []prompbmarshal.Label
for k, v := range ls {
baseLabels = append(baseLabels, prompbmarshal.Label{
Name: k,
Value: v,
})
}
// __name__ already been dropped, no need to check duplication
alertsLabels := append(baseLabels, prompbmarshal.Label{Name: "__name__", Value: alertMetricName})
alertsLabels = append(alertsLabels, prompbmarshal.Label{Name: alertStateLabel, Value: notifier.StatePending.String()})
result = append(result, newTimeSeries([]float64{decimal.StaleNaN}, []int64{timestamp}, alertsLabels))
if includeAlertForState {
alertsForStateLabels := append(baseLabels, prompbmarshal.Label{Name: "__name__", Value: alertForStateMetricName})
result = append(result, newTimeSeries([]float64{decimal.StaleNaN}, []int64{timestamp}, alertsForStateLabels))
}
return result
}
// firingAlertStaleTimeSeries returns stale `ALERTS` and `ALERTS_FOR_STATE` time series
// for alerts which changed their state from Firing to Inactive.
func firingAlertStaleTimeSeries(ls map[string]string, timestamp int64) []prompbmarshal.TimeSeries {
var baseLabels []prompbmarshal.Label
for k, v := range ls {
baseLabels = append(baseLabels, prompbmarshal.Label{
Name: k,
Value: v,
})
}
// __name__ already been dropped, no need to check duplication
alertsLabels := append(baseLabels, prompbmarshal.Label{Name: "__name__", Value: alertMetricName})
alertsLabels = append(alertsLabels, prompbmarshal.Label{Name: alertStateLabel, Value: notifier.StateFiring.String()})
alertsForStateLabels := append(baseLabels, prompbmarshal.Label{Name: "__name__", Value: alertForStateMetricName})
return []prompbmarshal.TimeSeries{
newTimeSeries([]float64{decimal.StaleNaN}, []int64{timestamp}, alertsLabels),
newTimeSeries([]float64{decimal.StaleNaN}, []int64{timestamp}, alertsForStateLabels),
}
}
// restore restores the value of ActiveAt field for active alerts,
// based on previously written time series `alertForStateMetricName`.
// Only rules with For > 0 can be restored.
@@ -641,7 +711,8 @@ func (ar *AlertingRule) restore(ctx context.Context, q datasource.Querier, ts ti
for k, v := range ar.Labels {
labelsFilter += fmt.Sprintf(",%s=%q", k, v)
}
expr := fmt.Sprintf("last_over_time(%s{%s%s}[%ds])",
// use `default_rollup()` instead of `last_over_time()` here to accounts for possible staleness markers
expr := fmt.Sprintf("default_rollup(%s{%s%s}[%ds])",
alertForStateMetricName, nameStr, labelsFilter, int(lookback.Seconds()))
res, _, err := q.Query(ctx, expr, ts)

View File

@@ -15,6 +15,7 @@ import (
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/datasource"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/notifier"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/utils"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/decimal"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/prompbmarshal"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promutils"
)
@@ -28,7 +29,7 @@ func TestAlertingRuleToTimeSeries(t *testing.T) {
rule.alerts[alert.ID] = alert
tss := rule.toTimeSeries(timestamp.Unix())
if err := compareTimeSeries(t, tssExpected, tss); err != nil {
t.Fatalf("timeseries mismatch: %s", err)
t.Fatalf("timeseries mismatch for rule %q: %s", rule.Name, err)
}
}
@@ -36,14 +37,23 @@ func TestAlertingRuleToTimeSeries(t *testing.T) {
State: notifier.StateFiring,
ActiveAt: timestamp.Add(time.Second),
}, []prompbmarshal.TimeSeries{
newTimeSeries([]float64{1}, []int64{timestamp.UnixNano()}, map[string]string{
"__name__": alertMetricName,
alertStateLabel: notifier.StateFiring.String(),
newTimeSeries([]float64{1}, []int64{timestamp.UnixNano()}, []prompbmarshal.Label{
{
Name: "__name__",
Value: alertMetricName,
},
{
Name: alertStateLabel,
Value: notifier.StateFiring.String(),
},
}),
newTimeSeries([]float64{float64(timestamp.Add(time.Second).Unix())},
[]int64{timestamp.UnixNano()},
map[string]string{
"__name__": alertForStateMetricName,
[]prompbmarshal.Label{
{
Name: "__name__",
Value: alertForStateMetricName,
},
}),
})
@@ -54,18 +64,40 @@ func TestAlertingRuleToTimeSeries(t *testing.T) {
"instance": "bar",
},
}, []prompbmarshal.TimeSeries{
newTimeSeries([]float64{1}, []int64{timestamp.UnixNano()}, map[string]string{
"__name__": alertMetricName,
alertStateLabel: notifier.StateFiring.String(),
"job": "foo",
"instance": "bar",
}),
newTimeSeries([]float64{1}, []int64{timestamp.UnixNano()},
[]prompbmarshal.Label{
{
Name: "__name__",
Value: alertMetricName,
},
{
Name: alertStateLabel,
Value: notifier.StateFiring.String(),
},
{
Name: "job",
Value: "foo",
},
{
Name: "instance",
Value: "bar",
},
}),
newTimeSeries([]float64{float64(timestamp.Add(time.Second).Unix())},
[]int64{timestamp.UnixNano()},
map[string]string{
"__name__": alertForStateMetricName,
"job": "foo",
"instance": "bar",
[]prompbmarshal.Label{
{
Name: "__name__",
Value: alertForStateMetricName,
},
{
Name: "job",
Value: "foo",
},
{
Name: "instance",
Value: "bar",
},
}),
})
@@ -73,18 +105,29 @@ func TestAlertingRuleToTimeSeries(t *testing.T) {
State: notifier.StateFiring, ActiveAt: timestamp.Add(time.Second),
Labels: map[string]string{
alertStateLabel: "foo",
"__name__": "bar",
},
}, []prompbmarshal.TimeSeries{
newTimeSeries([]float64{1}, []int64{timestamp.UnixNano()}, map[string]string{
"__name__": alertMetricName,
alertStateLabel: notifier.StateFiring.String(),
newTimeSeries([]float64{1}, []int64{timestamp.UnixNano()}, []prompbmarshal.Label{
{
Name: "__name__",
Value: alertMetricName,
},
{
Name: alertStateLabel,
Value: notifier.StateFiring.String(),
},
}),
newTimeSeries([]float64{float64(timestamp.Add(time.Second).Unix())},
[]int64{timestamp.UnixNano()},
map[string]string{
"__name__": alertForStateMetricName,
alertStateLabel: "foo",
[]prompbmarshal.Label{
{
Name: "__name__",
Value: alertForStateMetricName,
},
{
Name: alertStateLabel,
Value: "foo",
},
}),
})
@@ -92,14 +135,23 @@ func TestAlertingRuleToTimeSeries(t *testing.T) {
State: notifier.StateFiring,
ActiveAt: timestamp.Add(time.Second),
}, []prompbmarshal.TimeSeries{
newTimeSeries([]float64{1}, []int64{timestamp.UnixNano()}, map[string]string{
"__name__": alertMetricName,
alertStateLabel: notifier.StateFiring.String(),
newTimeSeries([]float64{1}, []int64{timestamp.UnixNano()}, []prompbmarshal.Label{
{
Name: "__name__",
Value: alertMetricName,
},
{
Name: alertStateLabel,
Value: notifier.StateFiring.String(),
},
}),
newTimeSeries([]float64{float64(timestamp.Add(time.Second).Unix())},
[]int64{timestamp.UnixNano()},
map[string]string{
"__name__": alertForStateMetricName,
[]prompbmarshal.Label{
{
Name: "__name__",
Value: alertForStateMetricName,
},
}),
})
@@ -107,12 +159,21 @@ func TestAlertingRuleToTimeSeries(t *testing.T) {
State: notifier.StatePending,
ActiveAt: timestamp.Add(time.Second),
}, []prompbmarshal.TimeSeries{
newTimeSeries([]float64{1}, []int64{timestamp.UnixNano()}, map[string]string{
"__name__": alertMetricName,
alertStateLabel: notifier.StatePending.String(),
newTimeSeries([]float64{1}, []int64{timestamp.UnixNano()}, []prompbmarshal.Label{
{
Name: "__name__",
Value: alertMetricName,
},
{
Name: alertStateLabel,
Value: notifier.StatePending.String(),
},
}),
newTimeSeries([]float64{float64(timestamp.Add(time.Second).Unix())}, []int64{timestamp.UnixNano()}, map[string]string{
"__name__": alertForStateMetricName,
newTimeSeries([]float64{float64(timestamp.Add(time.Second).Unix())}, []int64{timestamp.UnixNano()}, []prompbmarshal.Label{
{
Name: "__name__",
Value: alertForStateMetricName,
},
}),
})
}
@@ -124,7 +185,9 @@ func TestAlertingRule_Exec(t *testing.T) {
alert *notifier.Alert
}
f := func(rule *AlertingRule, steps [][]datasource.Metric, alertsExpected map[int][]testAlert) {
ts, _ := time.Parse(time.RFC3339, "2024-10-29T00:00:00Z")
f := func(rule *AlertingRule, steps [][]datasource.Metric, alertsExpected map[int][]testAlert, tssExpected map[int][]prompbmarshal.TimeSeries) {
t.Helper()
fq := &datasource.FakeQuerier{}
@@ -134,13 +197,19 @@ func TestAlertingRule_Exec(t *testing.T) {
Name: "TestRule_Exec",
}
rule.GroupID = fakeGroup.ID()
ts := time.Now()
for i, step := range steps {
fq.Reset()
fq.Add(step...)
if _, err := rule.exec(context.TODO(), ts, 0); err != nil {
tss, err := rule.exec(context.TODO(), ts, 0)
if err != nil {
t.Fatalf("unexpected error: %s", err)
}
// check generate time series
if _, ok := tssExpected[i]; ok {
if err := compareTimeSeries(t, tssExpected[i], tss); err != nil {
t.Fatalf("generated time series mismatch for rule %q in step %d: %s", rule.Name, i, err)
}
}
// shift the execution timestamp before the next iteration
ts = ts.Add(defaultStep)
@@ -174,13 +243,21 @@ func TestAlertingRule_Exec(t *testing.T) {
}
}
f(newTestAlertingRule("empty", 0), [][]datasource.Metric{}, nil)
f(newTestAlertingRule("empty", 0), [][]datasource.Metric{}, nil, nil)
f(newTestAlertingRule("empty labels", 0), [][]datasource.Metric{
f(newTestAlertingRule("empty_labels", 0), [][]datasource.Metric{
{datasource.Metric{Values: []float64{1}, Timestamps: []int64{1}}},
}, map[int][]testAlert{
0: {{alert: &notifier.Alert{State: notifier.StateFiring}}},
})
},
map[int][]prompbmarshal.TimeSeries{
0: {
{Labels: []prompbmarshal.Label{{Name: "__name__", Value: alertMetricName}, {Name: "alertname", Value: "empty_labels"}, {Name: "alertstate", Value: "firing"}},
Samples: []prompbmarshal.Sample{{Value: 1, Timestamp: ts.UnixNano() / 1e6}}},
{Labels: []prompbmarshal.Label{{Name: "__name__", Value: alertForStateMetricName}, {Name: "alertname", Value: "empty_labels"}},
Samples: []prompbmarshal.Sample{{Value: float64(ts.Unix()), Timestamp: ts.UnixNano() / 1e6}}},
},
})
f(newTestAlertingRule("single-firing=>inactive=>firing=>inactive=>inactive", 0), [][]datasource.Metric{
{metricWithLabels(t, "name", "foo")},
@@ -194,6 +271,25 @@ func TestAlertingRule_Exec(t *testing.T) {
2: {{labels: []string{"name", "foo"}, alert: &notifier.Alert{State: notifier.StateFiring}}},
3: {{labels: []string{"name", "foo"}, alert: &notifier.Alert{State: notifier.StateInactive}}},
4: {{labels: []string{"name", "foo"}, alert: &notifier.Alert{State: notifier.StateInactive}}},
}, map[int][]prompbmarshal.TimeSeries{
0: {
{Labels: []prompbmarshal.Label{{Name: "__name__", Value: alertMetricName}, {Name: "alertname", Value: "single-firing=>inactive=>firing=>inactive=>inactive"}, {Name: "alertstate", Value: "firing"}, {Name: "name", Value: "foo"}},
Samples: []prompbmarshal.Sample{{Value: 1, Timestamp: ts.UnixNano() / 1e6}}},
{Labels: []prompbmarshal.Label{{Name: "__name__", Value: alertForStateMetricName}, {Name: "alertname", Value: "single-firing=>inactive=>firing=>inactive=>inactive"}, {Name: "name", Value: "foo"}},
Samples: []prompbmarshal.Sample{{Value: float64(ts.Unix()), Timestamp: ts.UnixNano() / 1e6}}},
},
1: {
{Labels: []prompbmarshal.Label{{Name: "__name__", Value: alertMetricName}, {Name: "alertname", Value: "single-firing=>inactive=>firing=>inactive=>inactive"}, {Name: "alertstate", Value: "firing"}, {Name: "name", Value: "foo"}},
Samples: []prompbmarshal.Sample{{Value: decimal.StaleNaN, Timestamp: ts.Add(defaultStep).UnixNano() / 1e6}}},
{Labels: []prompbmarshal.Label{{Name: "__name__", Value: alertForStateMetricName}, {Name: "alertname", Value: "single-firing=>inactive=>firing=>inactive=>inactive"}, {Name: "name", Value: "foo"}},
Samples: []prompbmarshal.Sample{{Value: decimal.StaleNaN, Timestamp: ts.Add(defaultStep).UnixNano() / 1e6}}},
},
2: {
{Labels: []prompbmarshal.Label{{Name: "__name__", Value: alertMetricName}, {Name: "alertname", Value: "single-firing=>inactive=>firing=>inactive=>inactive"}, {Name: "alertstate", Value: "firing"}, {Name: "name", Value: "foo"}},
Samples: []prompbmarshal.Sample{{Value: 1, Timestamp: ts.Add(2*defaultStep).UnixNano() / 1e6}}},
{Labels: []prompbmarshal.Label{{Name: "__name__", Value: alertForStateMetricName}, {Name: "alertname", Value: "single-firing=>inactive=>firing=>inactive=>inactive"}, {Name: "name", Value: "foo"}},
Samples: []prompbmarshal.Sample{{Value: float64(ts.Add(2 * defaultStep).Unix()), Timestamp: ts.Add(2*defaultStep).UnixNano() / 1e6}}},
},
})
f(newTestAlertingRule("single-firing=>inactive=>firing=>inactive=>inactive=>firing", 0), [][]datasource.Metric{
@@ -210,7 +306,7 @@ func TestAlertingRule_Exec(t *testing.T) {
3: {{labels: []string{"name", "foo"}, alert: &notifier.Alert{State: notifier.StateInactive}}},
4: {{labels: []string{"name", "foo"}, alert: &notifier.Alert{State: notifier.StateInactive}}},
5: {{labels: []string{"name", "foo"}, alert: &notifier.Alert{State: notifier.StateFiring}}},
})
}, nil)
f(newTestAlertingRule("multiple-firing", 0), [][]datasource.Metric{
{
@@ -224,7 +320,7 @@ func TestAlertingRule_Exec(t *testing.T) {
{labels: []string{"name", "foo1"}, alert: &notifier.Alert{State: notifier.StateFiring}},
{labels: []string{"name", "foo2"}, alert: &notifier.Alert{State: notifier.StateFiring}},
},
})
}, nil)
// 1: fire first alert
// 2: fire second alert, set first inactive
@@ -233,27 +329,57 @@ func TestAlertingRule_Exec(t *testing.T) {
{metricWithLabels(t, "name", "foo")},
{metricWithLabels(t, "name", "foo1")},
{metricWithLabels(t, "name", "foo2")},
},
map[int][]testAlert{
0: {
{labels: []string{"name", "foo"}, alert: &notifier.Alert{State: notifier.StateFiring}},
},
1: {
{labels: []string{"name", "foo"}, alert: &notifier.Alert{State: notifier.StateInactive}},
{labels: []string{"name", "foo1"}, alert: &notifier.Alert{State: notifier.StateFiring}},
},
2: {
{labels: []string{"name", "foo"}, alert: &notifier.Alert{State: notifier.StateInactive}},
{labels: []string{"name", "foo1"}, alert: &notifier.Alert{State: notifier.StateInactive}},
{labels: []string{"name", "foo2"}, alert: &notifier.Alert{State: notifier.StateFiring}},
},
})
}, map[int][]testAlert{
0: {
{labels: []string{"name", "foo"}, alert: &notifier.Alert{State: notifier.StateFiring}},
},
1: {
{labels: []string{"name", "foo"}, alert: &notifier.Alert{State: notifier.StateInactive}},
{labels: []string{"name", "foo1"}, alert: &notifier.Alert{State: notifier.StateFiring}},
},
2: {
{labels: []string{"name", "foo"}, alert: &notifier.Alert{State: notifier.StateInactive}},
{labels: []string{"name", "foo1"}, alert: &notifier.Alert{State: notifier.StateInactive}},
{labels: []string{"name", "foo2"}, alert: &notifier.Alert{State: notifier.StateFiring}},
},
}, map[int][]prompbmarshal.TimeSeries{
0: {
{Labels: []prompbmarshal.Label{{Name: "__name__", Value: alertMetricName}, {Name: "alertname", Value: "multiple-steps-firing"}, {Name: "alertstate", Value: "firing"}, {Name: "name", Value: "foo"}},
Samples: []prompbmarshal.Sample{{Value: 1, Timestamp: ts.UnixNano() / 1e6}}},
{Labels: []prompbmarshal.Label{{Name: "__name__", Value: alertForStateMetricName}, {Name: "alertname", Value: "multiple-steps-firing"}, {Name: "name", Value: "foo"}},
Samples: []prompbmarshal.Sample{{Value: float64(ts.Unix()), Timestamp: ts.UnixNano() / 1e6}}},
},
1: {
// stale time series for foo, `firing -> inactive`
{Labels: []prompbmarshal.Label{{Name: "__name__", Value: alertMetricName}, {Name: "alertname", Value: "multiple-steps-firing"}, {Name: "alertstate", Value: "firing"}, {Name: "name", Value: "foo"}},
Samples: []prompbmarshal.Sample{{Value: decimal.StaleNaN, Timestamp: ts.Add(defaultStep).UnixNano() / 1e6}}},
{Labels: []prompbmarshal.Label{{Name: "__name__", Value: alertForStateMetricName}, {Name: "alertname", Value: "multiple-steps-firing"}, {Name: "name", Value: "foo"}},
Samples: []prompbmarshal.Sample{{Value: decimal.StaleNaN, Timestamp: ts.Add(defaultStep).UnixNano() / 1e6}}},
// new time series for foo1
{Labels: []prompbmarshal.Label{{Name: "__name__", Value: alertMetricName}, {Name: "alertname", Value: "multiple-steps-firing"}, {Name: "alertstate", Value: "firing"}, {Name: "name", Value: "foo1"}},
Samples: []prompbmarshal.Sample{{Value: 1, Timestamp: ts.Add(defaultStep).UnixNano() / 1e6}}},
{Labels: []prompbmarshal.Label{{Name: "__name__", Value: alertForStateMetricName}, {Name: "alertname", Value: "multiple-steps-firing"}, {Name: "name", Value: "foo1"}},
Samples: []prompbmarshal.Sample{{Value: float64(ts.Add(defaultStep).Unix()), Timestamp: ts.Add(defaultStep).UnixNano() / 1e6}}},
},
2: {
// stale time series for foo1
{Labels: []prompbmarshal.Label{{Name: "__name__", Value: alertMetricName}, {Name: "alertname", Value: "multiple-steps-firing"}, {Name: "alertstate", Value: "firing"}, {Name: "name", Value: "foo1"}},
Samples: []prompbmarshal.Sample{{Value: decimal.StaleNaN, Timestamp: ts.Add(2*defaultStep).UnixNano() / 1e6}}},
{Labels: []prompbmarshal.Label{{Name: "__name__", Value: alertForStateMetricName}, {Name: "alertname", Value: "multiple-steps-firing"}, {Name: "name", Value: "foo1"}},
Samples: []prompbmarshal.Sample{{Value: decimal.StaleNaN, Timestamp: ts.Add(2*defaultStep).UnixNano() / 1e6}}},
// new time series for foo2
{Labels: []prompbmarshal.Label{{Name: "__name__", Value: alertMetricName}, {Name: "alertname", Value: "multiple-steps-firing"}, {Name: "alertstate", Value: "firing"}, {Name: "name", Value: "foo2"}},
Samples: []prompbmarshal.Sample{{Value: 1, Timestamp: ts.Add(2*defaultStep).UnixNano() / 1e6}}},
{Labels: []prompbmarshal.Label{{Name: "__name__", Value: alertForStateMetricName}, {Name: "alertname", Value: "multiple-steps-firing"}, {Name: "name", Value: "foo2"}},
Samples: []prompbmarshal.Sample{{Value: float64(ts.Add(2 * defaultStep).Unix()), Timestamp: ts.Add(2*defaultStep).UnixNano() / 1e6}}},
},
})
f(newTestAlertingRule("for-pending", time.Minute), [][]datasource.Metric{
{metricWithLabels(t, "name", "foo")},
}, map[int][]testAlert{
0: {{labels: []string{"name", "foo"}, alert: &notifier.Alert{State: notifier.StatePending}}},
})
}, nil)
f(newTestAlertingRule("for-fired", defaultStep), [][]datasource.Metric{
{metricWithLabels(t, "name", "foo")},
@@ -261,6 +387,22 @@ func TestAlertingRule_Exec(t *testing.T) {
}, map[int][]testAlert{
0: {{labels: []string{"name", "foo"}, alert: &notifier.Alert{State: notifier.StatePending}}},
1: {{labels: []string{"name", "foo"}, alert: &notifier.Alert{State: notifier.StateFiring}}},
}, map[int][]prompbmarshal.TimeSeries{
0: {
{Labels: []prompbmarshal.Label{{Name: "__name__", Value: alertMetricName}, {Name: "alertname", Value: "for-fired"}, {Name: "alertstate", Value: "pending"}, {Name: "name", Value: "foo"}},
Samples: []prompbmarshal.Sample{{Value: 1, Timestamp: ts.UnixNano() / 1e6}}},
{Labels: []prompbmarshal.Label{{Name: "__name__", Value: alertForStateMetricName}, {Name: "alertname", Value: "for-fired"}, {Name: "name", Value: "foo"}},
Samples: []prompbmarshal.Sample{{Value: float64(ts.Unix()), Timestamp: ts.UnixNano() / 1e6}}},
},
1: {
// stale time series for `pending -> firing`
{Labels: []prompbmarshal.Label{{Name: "__name__", Value: alertMetricName}, {Name: "alertname", Value: "for-fired"}, {Name: "alertstate", Value: "pending"}, {Name: "name", Value: "foo"}},
Samples: []prompbmarshal.Sample{{Value: decimal.StaleNaN, Timestamp: ts.Add(defaultStep).UnixNano() / 1e6}}},
{Labels: []prompbmarshal.Label{{Name: "__name__", Value: alertMetricName}, {Name: "alertname", Value: "for-fired"}, {Name: "alertstate", Value: "firing"}, {Name: "name", Value: "foo"}},
Samples: []prompbmarshal.Sample{{Value: 1, Timestamp: ts.Add(defaultStep).UnixNano() / 1e6}}},
{Labels: []prompbmarshal.Label{{Name: "__name__", Value: alertForStateMetricName}, {Name: "alertname", Value: "for-fired"}, {Name: "name", Value: "foo"}},
Samples: []prompbmarshal.Sample{{Value: float64(ts.Add(defaultStep).Unix()), Timestamp: ts.Add(defaultStep).UnixNano() / 1e6}}},
},
})
f(newTestAlertingRule("for-pending=>empty", time.Second), [][]datasource.Metric{
@@ -272,6 +414,26 @@ func TestAlertingRule_Exec(t *testing.T) {
0: {{labels: []string{"name", "foo"}, alert: &notifier.Alert{State: notifier.StatePending}}},
1: {{labels: []string{"name", "foo"}, alert: &notifier.Alert{State: notifier.StatePending}}},
2: {},
}, map[int][]prompbmarshal.TimeSeries{
0: {
{Labels: []prompbmarshal.Label{{Name: "__name__", Value: alertMetricName}, {Name: "alertname", Value: "for-pending=>empty"}, {Name: "alertstate", Value: "pending"}, {Name: "name", Value: "foo"}},
Samples: []prompbmarshal.Sample{{Value: 1, Timestamp: ts.UnixNano() / 1e6}}},
{Labels: []prompbmarshal.Label{{Name: "__name__", Value: alertForStateMetricName}, {Name: "alertname", Value: "for-pending=>empty"}, {Name: "name", Value: "foo"}},
Samples: []prompbmarshal.Sample{{Value: float64(ts.Unix()), Timestamp: ts.UnixNano() / 1e6}}},
},
1: {
{Labels: []prompbmarshal.Label{{Name: "__name__", Value: alertMetricName}, {Name: "alertname", Value: "for-pending=>empty"}, {Name: "alertstate", Value: "pending"}, {Name: "name", Value: "foo"}},
Samples: []prompbmarshal.Sample{{Value: 1, Timestamp: ts.Add(defaultStep).UnixNano() / 1e6}}},
{Labels: []prompbmarshal.Label{{Name: "__name__", Value: alertForStateMetricName}, {Name: "alertname", Value: "for-pending=>empty"}, {Name: "name", Value: "foo"}},
Samples: []prompbmarshal.Sample{{Value: float64(ts.Unix()), Timestamp: ts.Add(defaultStep).UnixNano() / 1e6}}},
},
// stale time series for `pending -> inactive`
2: {
{Labels: []prompbmarshal.Label{{Name: "__name__", Value: alertMetricName}, {Name: "alertname", Value: "for-pending=>empty"}, {Name: "alertstate", Value: "pending"}, {Name: "name", Value: "foo"}},
Samples: []prompbmarshal.Sample{{Value: decimal.StaleNaN, Timestamp: ts.Add(2*defaultStep).UnixNano() / 1e6}}},
{Labels: []prompbmarshal.Label{{Name: "__name__", Value: alertForStateMetricName}, {Name: "alertname", Value: "for-pending=>empty"}, {Name: "name", Value: "foo"}},
Samples: []prompbmarshal.Sample{{Value: decimal.StaleNaN, Timestamp: ts.Add(2*defaultStep).UnixNano() / 1e6}}},
},
})
f(newTestAlertingRule("for-pending=>firing=>inactive=>pending=>firing", defaultStep), [][]datasource.Metric{
@@ -287,7 +449,7 @@ func TestAlertingRule_Exec(t *testing.T) {
2: {{labels: []string{"name", "foo"}, alert: &notifier.Alert{State: notifier.StateInactive}}},
3: {{labels: []string{"name", "foo"}, alert: &notifier.Alert{State: notifier.StatePending}}},
4: {{labels: []string{"name", "foo"}, alert: &notifier.Alert{State: notifier.StateFiring}}},
})
}, nil)
f(newTestAlertingRuleWithCustomFields("for-pending=>firing=>keepfiring=>firing", defaultStep, 0, defaultStep, nil), [][]datasource.Metric{
{metricWithLabels(t, "name", "foo")},
@@ -300,7 +462,7 @@ func TestAlertingRule_Exec(t *testing.T) {
1: {{labels: []string{"name", "foo"}, alert: &notifier.Alert{State: notifier.StateFiring}}},
2: {{labels: []string{"name", "foo"}, alert: &notifier.Alert{State: notifier.StateFiring}}},
3: {{labels: []string{"name", "foo"}, alert: &notifier.Alert{State: notifier.StateFiring}}},
})
}, nil)
f(newTestAlertingRuleWithCustomFields("for-pending=>firing=>keepfiring=>keepfiring=>inactive=>pending=>firing", defaultStep, 0, 2*defaultStep, nil), [][]datasource.Metric{
{metricWithLabels(t, "name", "foo")},
@@ -321,7 +483,7 @@ func TestAlertingRule_Exec(t *testing.T) {
4: {{labels: []string{"name", "foo"}, alert: &notifier.Alert{State: notifier.StateInactive}}},
5: {{labels: []string{"name", "foo"}, alert: &notifier.Alert{State: notifier.StatePending}}},
6: {{labels: []string{"name", "foo"}, alert: &notifier.Alert{State: notifier.StateFiring}}},
})
}, nil)
}
func TestAlertingRuleExecRange(t *testing.T) {
@@ -477,7 +639,7 @@ func TestAlertingRuleExecRange(t *testing.T) {
{Values: []float64{1, 1, 1}, Timestamps: []int64{1, 3, 5}},
{
Values: []float64{1, 1}, Timestamps: []int64{1, 5},
Labels: []datasource.Label{{Name: "foo", Value: "bar"}},
Labels: []prompbmarshal.Label{{Name: "foo", Value: "bar"}},
},
}, []*notifier.Alert{
{State: notifier.StatePending, ActiveAt: time.Unix(1, 0)},
@@ -523,7 +685,7 @@ func TestAlertingRuleExecRange(t *testing.T) {
{Values: []float64{1, 1}, Timestamps: []int64{1, 100}},
{
Values: []float64{1, 1}, Timestamps: []int64{1, 5},
Labels: []datasource.Label{{Name: "foo", Value: "bar"}},
Labels: []prompbmarshal.Label{{Name: "foo", Value: "bar"}},
},
}, []*notifier.Alert{
{
@@ -629,7 +791,7 @@ func TestGroup_Restore(t *testing.T) {
// one active alert with state restore
ts := time.Now().Truncate(time.Hour)
fqr.Set(`last_over_time(ALERTS_FOR_STATE{alertgroup="TestRestore",alertname="foo"}[3600s])`,
fqr.Set(`default_rollup(ALERTS_FOR_STATE{alertgroup="TestRestore",alertname="foo"}[3600s])`,
stateMetric("foo", ts))
fn(
[]config.Rule{{Alert: "foo", Expr: "foo", For: promutils.NewDuration(time.Second)}},
@@ -642,7 +804,7 @@ func TestGroup_Restore(t *testing.T) {
// two rules, two active alerts, one with state restored
ts = time.Now().Truncate(time.Hour)
fqr.Set(`last_over_time(ALERTS_FOR_STATE{alertgroup="TestRestore",alertname="bar"}[3600s])`,
fqr.Set(`default_rollup(ALERTS_FOR_STATE{alertgroup="TestRestore",alertname="bar"}[3600s])`,
stateMetric("bar", ts))
fn(
[]config.Rule{
@@ -662,9 +824,9 @@ func TestGroup_Restore(t *testing.T) {
// two rules, two active alerts, two with state restored
ts = time.Now().Truncate(time.Hour)
fqr.Set(`last_over_time(ALERTS_FOR_STATE{alertgroup="TestRestore",alertname="foo"}[3600s])`,
fqr.Set(`default_rollup(ALERTS_FOR_STATE{alertgroup="TestRestore",alertname="foo"}[3600s])`,
stateMetric("foo", ts))
fqr.Set(`last_over_time(ALERTS_FOR_STATE{alertgroup="TestRestore",alertname="bar"}[3600s])`,
fqr.Set(`default_rollup(ALERTS_FOR_STATE{alertgroup="TestRestore",alertname="bar"}[3600s])`,
stateMetric("bar", ts))
fn(
[]config.Rule{
@@ -684,7 +846,7 @@ func TestGroup_Restore(t *testing.T) {
// one active alert but wrong state restore
ts = time.Now().Truncate(time.Hour)
fqr.Set(`last_over_time(ALERTS_FOR_STATE{alertname="bar",alertgroup="TestRestore"}[3600s])`,
fqr.Set(`default_rollup(ALERTS_FOR_STATE{alertname="bar",alertgroup="TestRestore"}[3600s])`,
stateMetric("wrong alert", ts))
fn(
[]config.Rule{{Alert: "foo", Expr: "foo", For: promutils.NewDuration(time.Second)}},
@@ -697,7 +859,7 @@ func TestGroup_Restore(t *testing.T) {
// one active alert with labels
ts = time.Now().Truncate(time.Hour)
fqr.Set(`last_over_time(ALERTS_FOR_STATE{alertgroup="TestRestore",alertname="foo",env="dev"}[3600s])`,
fqr.Set(`default_rollup(ALERTS_FOR_STATE{alertgroup="TestRestore",alertname="foo",env="dev"}[3600s])`,
stateMetric("foo", ts, "env", "dev"))
fn(
[]config.Rule{{Alert: "foo", Expr: "foo", Labels: map[string]string{"env": "dev"}, For: promutils.NewDuration(time.Second)}},
@@ -710,7 +872,7 @@ func TestGroup_Restore(t *testing.T) {
// one active alert with restore labels missmatch
ts = time.Now().Truncate(time.Hour)
fqr.Set(`last_over_time(ALERTS_FOR_STATE{alertgroup="TestRestore",alertname="foo",env="dev"}[3600s])`,
fqr.Set(`default_rollup(ALERTS_FOR_STATE{alertgroup="TestRestore",alertname="foo",env="dev"}[3600s])`,
stateMetric("foo", ts, "env", "dev", "team", "foo"))
fn(
[]config.Rule{{Alert: "foo", Expr: "foo", Labels: map[string]string{"env": "dev"}, For: promutils.NewDuration(time.Second)}},
@@ -1047,7 +1209,7 @@ func newTestAlertingRuleWithCustomFields(name string, waitFor, evalInterval, kee
func TestAlertingRule_ToLabels(t *testing.T) {
metric := datasource.Metric{
Labels: []datasource.Label{
Labels: []prompbmarshal.Label{
{Name: "instance", Value: "0.0.0.0:8800"},
{Name: "group", Value: "vmalert"},
{Name: "alertname", Value: "ConfigurationReloadFailure"},

View File

@@ -8,12 +8,9 @@ import (
"fmt"
"hash/fnv"
"net/url"
"strconv"
"sync"
"time"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/bytesutil"
"github.com/cheggaaa/pb/v3"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/config"
@@ -21,7 +18,6 @@ import (
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/notifier"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/remotewrite"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/utils"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/decimal"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/prompbmarshal"
"github.com/VictoriaMetrics/metrics"
@@ -350,10 +346,9 @@ func (g *Group) Start(ctx context.Context, nts func() []notifier.Notifier, rw re
}
e := &executor{
Rw: rw,
Notifiers: nts,
notifierHeaders: g.NotifierHeaders,
previouslySentSeriesToRW: make(map[uint64]map[string][]prompbmarshal.Label),
Rw: rw,
Notifiers: nts,
notifierHeaders: g.NotifierHeaders,
}
g.infof("started")
@@ -426,8 +421,6 @@ func (g *Group) Start(ctx context.Context, nts func() []notifier.Notifier, rw re
continue
}
// ensure that staleness is tracked for existing rules only
e.purgeStaleSeries(g.Rules)
e.notifierHeaders = g.NotifierHeaders
g.mu.Unlock()
@@ -539,10 +532,9 @@ func (g *Group) Replay(start, end time.Time, rw remotewrite.RWClient, maxDataPoi
// ExecOnce evaluates all the rules under group for once with given timestamp.
func (g *Group) ExecOnce(ctx context.Context, nts func() []notifier.Notifier, rw remotewrite.RWClient, evalTS time.Time) chan error {
e := &executor{
Rw: rw,
Notifiers: nts,
notifierHeaders: g.NotifierHeaders,
previouslySentSeriesToRW: make(map[uint64]map[string][]prompbmarshal.Label),
Rw: rw,
Notifiers: nts,
notifierHeaders: g.NotifierHeaders,
}
if len(g.Rules) < 1 {
return nil
@@ -633,13 +625,6 @@ type executor struct {
notifierHeaders map[string]string
Rw remotewrite.RWClient
previouslySentSeriesToRWMu sync.Mutex
// previouslySentSeriesToRW stores series sent to RW on previous iteration
// map[ruleID]map[ruleLabels][]prompb.Label
// where `ruleID` is ID of the Rule within a Group
// and `ruleLabels` is []prompb.Label marshalled to a string
previouslySentSeriesToRW map[uint64]map[string][]prompbmarshal.Label
}
// execConcurrently executes rules concurrently if concurrency>1
@@ -706,11 +691,6 @@ func (e *executor) exec(ctx context.Context, r Rule, ts time.Time, resolveDurati
if err := pushToRW(tss); err != nil {
return err
}
staleSeries := e.getStaleSeries(r, tss, ts)
if err := pushToRW(staleSeries); err != nil {
return err
}
}
ar, ok := r.(*AlertingRule)
@@ -737,79 +717,3 @@ func (e *executor) exec(ctx context.Context, r Rule, ts time.Time, resolveDurati
wg.Wait()
return errGr.Err()
}
var bbPool bytesutil.ByteBufferPool
// getStaleSeries checks whether there are stale series from previously sent ones.
func (e *executor) getStaleSeries(r Rule, tss []prompbmarshal.TimeSeries, timestamp time.Time) []prompbmarshal.TimeSeries {
bb := bbPool.Get()
defer bbPool.Put(bb)
ruleLabels := make(map[string][]prompbmarshal.Label, len(tss))
for _, ts := range tss {
// convert labels to strings, so we can compare with previously sent series
bb.B = labelsToString(bb.B, ts.Labels)
ruleLabels[string(bb.B)] = ts.Labels
bb.Reset()
}
rID := r.ID()
var staleS []prompbmarshal.TimeSeries
// check whether there are series which disappeared and need to be marked as stale
e.previouslySentSeriesToRWMu.Lock()
for key, labels := range e.previouslySentSeriesToRW[rID] {
if _, ok := ruleLabels[key]; ok {
continue
}
// previously sent series are missing in current series, so we mark them as stale
ss := newTimeSeriesPB([]float64{decimal.StaleNaN}, []int64{timestamp.Unix()}, labels)
staleS = append(staleS, ss)
}
// set previous series to current
e.previouslySentSeriesToRW[rID] = ruleLabels
e.previouslySentSeriesToRWMu.Unlock()
return staleS
}
// purgeStaleSeries deletes references in tracked
// previouslySentSeriesToRW list to Rules which aren't present
// in the given activeRules list. The method is used when the list
// of loaded rules has changed and executor has to remove
// references to non-existing rules.
func (e *executor) purgeStaleSeries(activeRules []Rule) {
newPreviouslySentSeriesToRW := make(map[uint64]map[string][]prompbmarshal.Label)
e.previouslySentSeriesToRWMu.Lock()
for _, rule := range activeRules {
id := rule.ID()
prev, ok := e.previouslySentSeriesToRW[id]
if ok {
// keep previous series for staleness detection
newPreviouslySentSeriesToRW[id] = prev
}
}
e.previouslySentSeriesToRW = nil
e.previouslySentSeriesToRW = newPreviouslySentSeriesToRW
e.previouslySentSeriesToRWMu.Unlock()
}
func labelsToString(dst []byte, labels []prompbmarshal.Label) []byte {
dst = append(dst, '{')
for i, label := range labels {
if len(label.Name) == 0 {
dst = append(dst, "__name__"...)
} else {
dst = append(dst, label.Name...)
}
dst = append(dst, '=')
dst = strconv.AppendQuote(dst, label.Value)
if i < len(labels)-1 {
dst = append(dst, ',')
}
}
dst = append(dst, '}')
return dst
}

View File

@@ -5,7 +5,6 @@ import (
"fmt"
"math"
"os"
"reflect"
"sort"
"testing"
"time"
@@ -17,8 +16,6 @@ import (
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/notifier"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/remotewrite"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/templates"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/decimal"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/prompbmarshal"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promutils"
)
@@ -383,153 +380,6 @@ func TestGetResolveDuration(t *testing.T) {
f(2*time.Minute, 0, 1*time.Minute, 8*time.Minute)
}
func TestGetStaleSeries(t *testing.T) {
ts := time.Now()
e := &executor{
previouslySentSeriesToRW: make(map[uint64]map[string][]prompbmarshal.Label),
}
f := func(r Rule, labels, expLabels [][]prompbmarshal.Label) {
t.Helper()
var tss []prompbmarshal.TimeSeries
for _, l := range labels {
tss = append(tss, newTimeSeriesPB([]float64{1}, []int64{ts.Unix()}, l))
}
staleS := e.getStaleSeries(r, tss, ts)
if staleS == nil && expLabels == nil {
return
}
if len(staleS) != len(expLabels) {
t.Fatalf("expected to get %d stale series, got %d",
len(expLabels), len(staleS))
}
for i, exp := range expLabels {
got := staleS[i]
if !reflect.DeepEqual(exp, got.Labels) {
t.Fatalf("expected to get labels: \n%v;\ngot instead: \n%v",
exp, got.Labels)
}
if len(got.Samples) != 1 {
t.Fatalf("expected to have 1 sample; got %d", len(got.Samples))
}
if !decimal.IsStaleNaN(got.Samples[0].Value) {
t.Fatalf("expected sample value to be %v; got %v", decimal.StaleNaN, got.Samples[0].Value)
}
}
}
// warn: keep in mind, that executor holds the state, so sequence of f calls matters
// single series
f(&AlertingRule{RuleID: 1},
[][]prompbmarshal.Label{toPromLabels(t, "__name__", "job:foo", "job", "foo")},
nil)
f(&AlertingRule{RuleID: 1},
[][]prompbmarshal.Label{toPromLabels(t, "__name__", "job:foo", "job", "foo")},
nil)
f(&AlertingRule{RuleID: 1},
nil,
[][]prompbmarshal.Label{toPromLabels(t, "__name__", "job:foo", "job", "foo")})
f(&AlertingRule{RuleID: 1},
nil,
nil)
// multiple series
f(&AlertingRule{RuleID: 1},
[][]prompbmarshal.Label{
toPromLabels(t, "__name__", "job:foo", "job", "foo"),
toPromLabels(t, "__name__", "job:foo", "job", "bar"),
},
nil)
f(&AlertingRule{RuleID: 1},
[][]prompbmarshal.Label{toPromLabels(t, "__name__", "job:foo", "job", "bar")},
[][]prompbmarshal.Label{toPromLabels(t, "__name__", "job:foo", "job", "foo")})
f(&AlertingRule{RuleID: 1},
[][]prompbmarshal.Label{toPromLabels(t, "__name__", "job:foo", "job", "bar")},
nil)
f(&AlertingRule{RuleID: 1},
nil,
[][]prompbmarshal.Label{toPromLabels(t, "__name__", "job:foo", "job", "bar")})
// multiple rules and series
f(&AlertingRule{RuleID: 1},
[][]prompbmarshal.Label{
toPromLabels(t, "__name__", "job:foo", "job", "foo"),
toPromLabels(t, "__name__", "job:foo", "job", "bar"),
},
nil)
f(&AlertingRule{RuleID: 2},
[][]prompbmarshal.Label{
toPromLabels(t, "__name__", "job:foo", "job", "foo"),
toPromLabels(t, "__name__", "job:foo", "job", "bar"),
},
nil)
f(&AlertingRule{RuleID: 1},
[][]prompbmarshal.Label{toPromLabels(t, "__name__", "job:foo", "job", "bar")},
[][]prompbmarshal.Label{toPromLabels(t, "__name__", "job:foo", "job", "foo")})
f(&AlertingRule{RuleID: 1},
[][]prompbmarshal.Label{toPromLabels(t, "__name__", "job:foo", "job", "bar")},
nil)
}
func TestPurgeStaleSeries(t *testing.T) {
ts := time.Now()
labels := toPromLabels(t, "__name__", "job:foo", "job", "foo")
tss := []prompbmarshal.TimeSeries{newTimeSeriesPB([]float64{1}, []int64{ts.Unix()}, labels)}
f := func(curRules, newRules, expStaleRules []Rule) {
t.Helper()
e := &executor{
previouslySentSeriesToRW: make(map[uint64]map[string][]prompbmarshal.Label),
}
// seed executor with series for
// current rules
for _, rule := range curRules {
e.getStaleSeries(rule, tss, ts)
}
e.purgeStaleSeries(newRules)
if len(e.previouslySentSeriesToRW) != len(expStaleRules) {
t.Fatalf("expected to get %d stale series, got %d",
len(expStaleRules), len(e.previouslySentSeriesToRW))
}
for _, exp := range expStaleRules {
if _, ok := e.previouslySentSeriesToRW[exp.ID()]; !ok {
t.Fatalf("expected to have rule %d; got nil instead", exp.ID())
}
}
}
f(nil, nil, nil)
f(
nil,
[]Rule{&AlertingRule{RuleID: 1}},
nil,
)
f(
[]Rule{&AlertingRule{RuleID: 1}},
nil,
nil,
)
f(
[]Rule{&AlertingRule{RuleID: 1}},
[]Rule{&AlertingRule{RuleID: 2}},
nil,
)
f(
[]Rule{&AlertingRule{RuleID: 1}, &AlertingRule{RuleID: 2}},
[]Rule{&AlertingRule{RuleID: 2}},
[]Rule{&AlertingRule{RuleID: 2}},
)
f(
[]Rule{&AlertingRule{RuleID: 1}, &AlertingRule{RuleID: 2}},
[]Rule{&AlertingRule{RuleID: 1}, &AlertingRule{RuleID: 2}},
[]Rule{&AlertingRule{RuleID: 1}, &AlertingRule{RuleID: 2}},
)
}
func TestFaultyNotifier(t *testing.T) {
fq := &datasource.FakeQuerier{}
fq.Add(metricWithValueAndLabels(t, 1, "__name__", "foo", "job", "bar"))
@@ -580,8 +430,7 @@ func TestFaultyRW(t *testing.T) {
}
e := &executor{
Rw: &remotewrite.Client{},
previouslySentSeriesToRW: make(map[uint64]map[string][]prompbmarshal.Label),
Rw: &remotewrite.Client{},
}
err := e.exec(context.Background(), r, time.Now(), 0, 10)

View File

@@ -1,36 +0,0 @@
package rule
import (
"fmt"
"testing"
"time"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/prompbmarshal"
)
func BenchmarkGetStaleSeries(b *testing.B) {
ts := time.Now()
n := 100
payload := make([]prompbmarshal.TimeSeries, 0, n)
for i := 0; i < n; i++ {
s := fmt.Sprintf("%d", i)
labels := toPromLabels(b,
"__name__", "foo", ""+
"instance", s,
"job", s,
"state", s,
)
payload = append(payload, newTimeSeriesPB([]float64{1}, []int64{ts.Unix()}, labels))
}
e := &executor{
previouslySentSeriesToRW: make(map[uint64]map[string][]prompbmarshal.Label),
}
ar := &AlertingRule{RuleID: 1}
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
e.getStaleSeries(ar, payload, ts)
}
}

View File

@@ -3,16 +3,17 @@ package rule
import (
"context"
"fmt"
"sort"
"strings"
"time"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/config"
"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/logger"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logstorage"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/prompbmarshal"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promrelabel"
)
// RecordingRule is a Rule that supposed
@@ -34,6 +35,8 @@ type RecordingRule struct {
// during evaluations
state *ruleState
lastEvaluation map[string]struct{}
metrics *recordingRuleMetrics
}
@@ -113,7 +116,7 @@ func (rr *RecordingRule) execRange(ctx context.Context, start, end time.Time) ([
var tss []prompbmarshal.TimeSeries
for _, s := range res.Data {
ts := rr.toTimeSeries(s)
key := stringifyLabels(ts)
key := stringifyLabels(ts.Labels)
if _, ok := duplicates[key]; ok {
return nil, fmt.Errorf("original metric %v; resulting labels %q: %w", s.Labels, key, errDuplicate)
}
@@ -155,28 +158,47 @@ func (rr *RecordingRule) exec(ctx context.Context, ts time.Time, limit int) ([]p
return nil, curState.Err
}
duplicates := make(map[string]struct{}, len(qMetrics))
curEvaluation := make(map[string]struct{}, len(qMetrics))
lastEvaluation := rr.lastEvaluation
var tss []prompbmarshal.TimeSeries
for _, r := range qMetrics {
ts := rr.toTimeSeries(r)
key := stringifyLabels(ts)
if _, ok := duplicates[key]; ok {
key := stringifyLabels(ts.Labels)
if _, ok := curEvaluation[key]; ok {
curState.Err = fmt.Errorf("original metric %v; resulting labels %q: %w", r, key, errDuplicate)
return nil, curState.Err
}
duplicates[key] = struct{}{}
curEvaluation[key] = struct{}{}
delete(lastEvaluation, key)
tss = append(tss, ts)
}
// check for stale time series
for k := range lastEvaluation {
tss = append(tss, prompbmarshal.TimeSeries{
Labels: stringToLabels(k),
Samples: []prompbmarshal.Sample{
{Value: decimal.StaleNaN, Timestamp: ts.UnixNano() / 1e6},
}})
}
rr.lastEvaluation = curEvaluation
return tss, nil
}
func stringifyLabels(ts prompbmarshal.TimeSeries) string {
labels := ts.Labels
if len(labels) > 1 {
sort.Slice(labels, func(i, j int) bool {
return labels[i].Name < labels[j].Name
})
func stringToLabels(s string) []prompbmarshal.Label {
labels := strings.Split(s, ",")
rLabels := make([]prompbmarshal.Label, 0, len(labels))
for i := range labels {
if label := strings.Split(labels[i], "="); len(label) == 2 {
rLabels = append(rLabels, prompbmarshal.Label{
Name: label[0],
Value: label[1],
})
}
}
return rLabels
}
func stringifyLabels(labels []prompbmarshal.Label) string {
b := strings.Builder{}
for i, l := range labels {
b.WriteString(l.Name)
@@ -190,19 +212,27 @@ func stringifyLabels(ts prompbmarshal.TimeSeries) string {
}
func (rr *RecordingRule) toTimeSeries(m datasource.Metric) prompbmarshal.TimeSeries {
labels := make(map[string]string)
for _, l := range m.Labels {
labels[l.Name] = l.Value
if preN := promrelabel.GetLabelByName(m.Labels, "__name__"); preN != nil {
preN.Value = rr.Name
} else {
m.Labels = append(m.Labels, prompbmarshal.Label{
Name: "__name__",
Value: rr.Name,
})
}
labels["__name__"] = rr.Name
// override existing labels with configured ones
for k, v := range rr.Labels {
if _, ok := labels[k]; ok && labels[k] != v {
labels[fmt.Sprintf("exported_%s", k)] = labels[k]
for k := range rr.Labels {
prevLabel := promrelabel.GetLabelByName(m.Labels, k)
if prevLabel != nil && prevLabel.Value != rr.Labels[k] {
// Rename the prevLabel to "exported_" + label.Name
prevLabel.Name = fmt.Sprintf("exported_%s", prevLabel.Name)
}
labels[k] = v
m.Labels = append(m.Labels, prompbmarshal.Label{
Name: k,
Value: rr.Labels[k],
})
}
return newTimeSeries(m.Values, m.Timestamps, labels)
ts := newTimeSeries(m.Values, m.Timestamps, m.Labels)
return ts
}
// updateWith copies all significant fields.

View File

@@ -9,59 +9,131 @@ import (
"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"
)
func TestRecordingRule_Exec(t *testing.T) {
f := func(rule *RecordingRule, metrics []datasource.Metric, tssExpected []prompbmarshal.TimeSeries) {
ts, _ := time.Parse(time.RFC3339, "2024-10-29T00:00:00Z")
const defaultStep = 5 * time.Millisecond
f := func(rule *RecordingRule, steps [][]datasource.Metric, tssExpected [][]prompbmarshal.TimeSeries) {
t.Helper()
fq := &datasource.FakeQuerier{}
fq.Add(metrics...)
rule.q = fq
rule.state = &ruleState{
entries: make([]StateEntry, 10),
}
tss, err := rule.exec(context.TODO(), time.Now(), 0)
if err != nil {
t.Fatalf("unexpected RecordingRule.exec error: %s", err)
}
if err := compareTimeSeries(t, tssExpected, tss); err != nil {
t.Fatalf("timeseries missmatch: %s", err)
for i, step := range steps {
fq.Reset()
fq.Add(step...)
rule.q = fq
rule.state = &ruleState{
entries: make([]StateEntry, 10),
}
tss, err := rule.exec(context.TODO(), ts, 0)
if err != nil {
t.Fatalf("fail to test rule %s: unexpected error: %s", rule.Name, err)
}
if err := compareTimeSeries(t, tssExpected[i], tss); err != nil {
t.Fatalf("fail to test rule %s: time series mismatch on step %d: %s", rule.Name, i, err)
}
ts = ts.Add(defaultStep)
}
}
timestamp := time.Now()
f(&RecordingRule{
Name: "foo",
}, []datasource.Metric{
}, [][]datasource.Metric{{
metricWithValueAndLabels(t, 10, "__name__", "bar"),
}, []prompbmarshal.TimeSeries{
newTimeSeries([]float64{10}, []int64{timestamp.UnixNano()}, map[string]string{
"__name__": "foo",
}}, [][]prompbmarshal.TimeSeries{{
newTimeSeries([]float64{10}, []int64{ts.UnixNano()}, []prompbmarshal.Label{
{
Name: "__name__",
Value: "foo",
},
}),
})
}})
f(&RecordingRule{
Name: "foobarbaz",
}, []datasource.Metric{
metricWithValueAndLabels(t, 1, "__name__", "foo", "job", "foo"),
metricWithValueAndLabels(t, 2, "__name__", "bar", "job", "bar"),
metricWithValueAndLabels(t, 3, "__name__", "baz", "job", "baz"),
}, []prompbmarshal.TimeSeries{
newTimeSeries([]float64{1}, []int64{timestamp.UnixNano()}, map[string]string{
"__name__": "foobarbaz",
"job": "foo",
}),
newTimeSeries([]float64{2}, []int64{timestamp.UnixNano()}, map[string]string{
"__name__": "foobarbaz",
"job": "bar",
}),
newTimeSeries([]float64{3}, []int64{timestamp.UnixNano()}, map[string]string{
"__name__": "foobarbaz",
"job": "baz",
}),
}, [][]datasource.Metric{
{
metricWithValueAndLabels(t, 1, "__name__", "foo", "job", "foo"),
metricWithValueAndLabels(t, 2, "__name__", "bar", "job", "bar"),
},
{
metricWithValueAndLabels(t, 10, "__name__", "foo", "job", "foo"),
},
{
metricWithValueAndLabels(t, 10, "__name__", "foo", "job", "bar"),
},
}, [][]prompbmarshal.TimeSeries{
{
newTimeSeries([]float64{1}, []int64{ts.UnixNano()}, []prompbmarshal.Label{
{
Name: "__name__",
Value: "foobarbaz",
},
{
Name: "job",
Value: "foo",
},
}),
newTimeSeries([]float64{2}, []int64{ts.UnixNano()}, []prompbmarshal.Label{
{
Name: "__name__",
Value: "foobarbaz",
},
{
Name: "job",
Value: "bar",
},
}),
},
{
newTimeSeries([]float64{10}, []int64{ts.Add(defaultStep).UnixNano()}, []prompbmarshal.Label{
{
Name: "__name__",
Value: "foobarbaz",
},
{
Name: "job",
Value: "foo",
},
}),
// stale time series
newTimeSeries([]float64{decimal.StaleNaN}, []int64{ts.Add(defaultStep).UnixNano()}, []prompbmarshal.Label{
{
Name: "__name__",
Value: "foobarbaz",
},
{
Name: "job",
Value: "bar",
},
}),
},
{
newTimeSeries([]float64{10}, []int64{ts.Add(2 * defaultStep).UnixNano()}, []prompbmarshal.Label{
{
Name: "__name__",
Value: "foobarbaz",
},
{
Name: "job",
Value: "bar",
},
}),
newTimeSeries([]float64{decimal.StaleNaN}, []int64{ts.Add(2 * defaultStep).UnixNano()}, []prompbmarshal.Label{
{
Name: "__name__",
Value: "foobarbaz",
},
{
Name: "job",
Value: "foo",
},
}),
},
})
f(&RecordingRule{
@@ -69,22 +141,44 @@ func TestRecordingRule_Exec(t *testing.T) {
Labels: map[string]string{
"source": "test",
},
}, []datasource.Metric{
}, [][]datasource.Metric{{
metricWithValueAndLabels(t, 2, "__name__", "foo", "job", "foo"),
metricWithValueAndLabels(t, 1, "__name__", "bar", "job", "bar", "source", "origin"),
}, []prompbmarshal.TimeSeries{
newTimeSeries([]float64{2}, []int64{timestamp.UnixNano()}, map[string]string{
"__name__": "job:foo",
"job": "foo",
"source": "test",
}}, [][]prompbmarshal.TimeSeries{{
newTimeSeries([]float64{2}, []int64{ts.UnixNano()}, []prompbmarshal.Label{
{
Name: "__name__",
Value: "job:foo",
},
{
Name: "job",
Value: "foo",
},
{
Name: "source",
Value: "test",
},
}),
newTimeSeries([]float64{1}, []int64{timestamp.UnixNano()}, map[string]string{
"__name__": "job:foo",
"job": "bar",
"source": "test",
"exported_source": "origin",
}),
})
newTimeSeries([]float64{1}, []int64{ts.UnixNano()},
[]prompbmarshal.Label{
{
Name: "__name__",
Value: "job:foo",
},
{
Name: "job",
Value: "bar",
},
{
Name: "source",
Value: "test",
},
{
Name: "exported_source",
Value: "origin",
},
}),
}})
}
func TestRecordingRule_ExecRange(t *testing.T) {
@@ -110,9 +204,13 @@ func TestRecordingRule_ExecRange(t *testing.T) {
}, []datasource.Metric{
metricWithValuesAndLabels(t, []float64{10, 20, 30}, "__name__", "bar"),
}, []prompbmarshal.TimeSeries{
newTimeSeries([]float64{10, 20, 30}, []int64{timestamp.UnixNano(), timestamp.UnixNano(), timestamp.UnixNano()}, map[string]string{
"__name__": "foo",
}),
newTimeSeries([]float64{10, 20, 30}, []int64{timestamp.UnixNano(), timestamp.UnixNano(), timestamp.UnixNano()},
[]prompbmarshal.Label{
{
Name: "__name__",
Value: "foo",
},
}),
})
f(&RecordingRule{
@@ -122,18 +220,36 @@ func TestRecordingRule_ExecRange(t *testing.T) {
metricWithValuesAndLabels(t, []float64{2, 3}, "__name__", "bar", "job", "bar"),
metricWithValuesAndLabels(t, []float64{4, 5, 6}, "__name__", "baz", "job", "baz"),
}, []prompbmarshal.TimeSeries{
newTimeSeries([]float64{1}, []int64{timestamp.UnixNano()}, map[string]string{
"__name__": "foobarbaz",
"job": "foo",
newTimeSeries([]float64{1}, []int64{timestamp.UnixNano()}, []prompbmarshal.Label{
{
Name: "__name__",
Value: "foobarbaz",
},
{
Name: "job",
Value: "foo",
},
}),
newTimeSeries([]float64{2, 3}, []int64{timestamp.UnixNano(), timestamp.UnixNano()}, map[string]string{
"__name__": "foobarbaz",
"job": "bar",
newTimeSeries([]float64{2, 3}, []int64{timestamp.UnixNano(), timestamp.UnixNano()}, []prompbmarshal.Label{
{
Name: "__name__",
Value: "foobarbaz",
},
{
Name: "job",
Value: "bar",
},
}),
newTimeSeries([]float64{4, 5, 6},
[]int64{timestamp.UnixNano(), timestamp.UnixNano(), timestamp.UnixNano()}, map[string]string{
"__name__": "foobarbaz",
"job": "baz",
[]int64{timestamp.UnixNano(), timestamp.UnixNano(), timestamp.UnixNano()}, []prompbmarshal.Label{
{
Name: "__name__",
Value: "foobarbaz",
},
{
Name: "job",
Value: "baz",
},
}),
})
@@ -146,16 +262,35 @@ func TestRecordingRule_ExecRange(t *testing.T) {
metricWithValueAndLabels(t, 2, "__name__", "foo", "job", "foo"),
metricWithValueAndLabels(t, 1, "__name__", "bar", "job", "bar"),
}, []prompbmarshal.TimeSeries{
newTimeSeries([]float64{2}, []int64{timestamp.UnixNano()}, map[string]string{
"__name__": "job:foo",
"job": "foo",
"source": "test",
}),
newTimeSeries([]float64{1}, []int64{timestamp.UnixNano()}, map[string]string{
"__name__": "job:foo",
"job": "bar",
"source": "test",
newTimeSeries([]float64{2}, []int64{timestamp.UnixNano()}, []prompbmarshal.Label{
{
Name: "__name__",
Value: "job:foo",
},
{
Name: "job",
Value: "foo",
},
{
Name: "source",
Value: "test",
},
}),
newTimeSeries([]float64{1}, []int64{timestamp.UnixNano()},
[]prompbmarshal.Label{
{
Name: "__name__",
Value: "job:foo",
},
{
Name: "job",
Value: "bar",
},
{
Name: "source",
Value: "test",
},
}),
})
}

View File

@@ -8,6 +8,7 @@ import (
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/datasource"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/notifier"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/decimal"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/prompbmarshal"
)
@@ -87,7 +88,7 @@ func metricWithLabels(t *testing.T, labels ...string) datasource.Metric {
}
m := datasource.Metric{Values: []float64{1}, Timestamps: []int64{1}}
for i := 0; i < len(labels); i += 2 {
m.Labels = append(m.Labels, datasource.Label{
m.Labels = append(m.Labels, prompbmarshal.Label{
Name: labels[i],
Value: labels[i+1],
})
@@ -95,21 +96,6 @@ func metricWithLabels(t *testing.T, labels ...string) datasource.Metric {
return m
}
func toPromLabels(t testing.TB, labels ...string) []prompbmarshal.Label {
t.Helper()
if len(labels) == 0 || len(labels)%2 != 0 {
t.Fatalf("expected to get even number of labels")
}
var ls []prompbmarshal.Label
for i := 0; i < len(labels); i += 2 {
ls = append(ls, prompbmarshal.Label{
Name: labels[i],
Value: labels[i+1],
})
}
return ls
}
func compareTimeSeries(t *testing.T, a, b []prompbmarshal.TimeSeries) error {
t.Helper()
if len(a) != len(b) {
@@ -122,7 +108,7 @@ func compareTimeSeries(t *testing.T, a, b []prompbmarshal.TimeSeries) error {
}
for i, exp := range expTS.Samples {
got := gotTS.Samples[i]
if got.Value != exp.Value {
if got.Value != exp.Value && (!decimal.IsStaleNaN(got.Value) || !decimal.IsStaleNaN(exp.Value)) {
return fmt.Errorf("expected value %.2f; got %.2f", exp.Value, got.Value)
}
// timestamp validation isn't always correct for now.

View File

@@ -9,10 +9,14 @@ import (
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/datasource"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/prompbmarshal"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promrelabel"
)
func newTimeSeries(values []float64, timestamps []int64, labels map[string]string) prompbmarshal.TimeSeries {
// newTimeSeries first sorts given labels, then returns new time series.
func newTimeSeries(values []float64, timestamps []int64, labels []prompbmarshal.Label) prompbmarshal.TimeSeries {
promrelabel.SortLabels(labels)
ts := prompbmarshal.TimeSeries{
Labels: labels,
Samples: make([]prompbmarshal.Sample, len(values)),
}
for i := range values {
@@ -21,34 +25,6 @@ func newTimeSeries(values []float64, timestamps []int64, labels map[string]strin
Timestamp: time.Unix(timestamps[i], 0).UnixNano() / 1e6,
}
}
keys := make([]string, 0, len(labels))
for k := range labels {
keys = append(keys, k)
}
sort.Strings(keys) // make order deterministic
for _, key := range keys {
ts.Labels = append(ts.Labels, prompbmarshal.Label{
Name: key,
Value: labels[key],
})
}
return ts
}
// newTimeSeriesPB creates prompbmarshal.TimeSeries with given
// values, timestamps and labels.
// It expects that labels are already sorted.
func newTimeSeriesPB(values []float64, timestamps []int64, labels []prompbmarshal.Label) prompbmarshal.TimeSeries {
ts := prompbmarshal.TimeSeries{
Samples: make([]prompbmarshal.Sample, len(values)),
}
for i := range values {
ts.Samples[i] = prompbmarshal.Sample{
Value: values[i],
Timestamp: time.Unix(timestamps[i], 0).UnixNano() / 1e6,
}
}
ts.Labels = labels
return ts
}

View File

@@ -169,6 +169,8 @@ func GetWithFuncs(funcs textTpl.FuncMap) (*textTpl.Template, error) {
if err != nil {
return nil, err
}
// Clone() doesn't copy tpl Options, so we set them manually
tmpl = tmpl.Option("missingkey=zero")
return tmpl.Funcs(funcs), nil
}

View File

@@ -67,6 +67,7 @@ type UserInfo struct {
URLPrefix *URLPrefix `yaml:"url_prefix,omitempty"`
DiscoverBackendIPs *bool `yaml:"discover_backend_ips,omitempty"`
URLMaps []URLMap `yaml:"url_map,omitempty"`
DumpRequestOnErrors bool `yaml:"dump_request_on_errors,omitempty"`
HeadersConf HeadersConf `yaml:",inline"`
MaxConcurrentRequests int `yaml:"max_concurrent_requests,omitempty"`
DefaultURL *URLPrefix `yaml:"default_url,omitempty"`
@@ -462,17 +463,12 @@ func getLeastLoadedBackendURL(bus []*backendURL, atomicCounter *atomic.Uint32) *
// Slow path - select other backend urls.
n := atomicCounter.Add(1) - 1
buMin := bus[n%uint32(len(bus))]
for i := uint32(0); i < uint32(len(bus)); i++ {
idx := (n + i) % uint32(len(bus))
bu := bus[idx]
if bu.isBroken() {
continue
}
if buMin.isBroken() {
// verify that buMin isn't set as broken
buMin = bu
}
if bu.concurrentRequests.Load() == 0 {
// Fast path - return the backend with zero concurrently executed requests.
// Do not use CompareAndSwap() instead of Load(), since it is much slower on systems with many CPU cores.
@@ -482,12 +478,13 @@ func getLeastLoadedBackendURL(bus []*backendURL, atomicCounter *atomic.Uint32) *
}
// Slow path - return the backend with the minimum number of concurrently executed requests.
buMin := bus[n%uint32(len(bus))]
minRequests := buMin.concurrentRequests.Load()
for _, bu := range bus {
if bu.isBroken() {
continue
}
if n := bu.concurrentRequests.Load(); n < minRequests {
if n := bu.concurrentRequests.Load(); n < minRequests || buMin.isBroken() {
buMin = bu
minRequests = n
}
@@ -864,22 +861,23 @@ func (ui *UserInfo) initURLs() error {
loadBalancingPolicy := *defaultLoadBalancingPolicy
dropSrcPathPrefixParts := 0
discoverBackendIPs := *discoverBackendIPsGlobal
if ui.RetryStatusCodes != nil {
retryStatusCodes = ui.RetryStatusCodes
}
if ui.LoadBalancingPolicy != "" {
loadBalancingPolicy = ui.LoadBalancingPolicy
}
if ui.DropSrcPathPrefixParts != nil {
dropSrcPathPrefixParts = *ui.DropSrcPathPrefixParts
}
if ui.DiscoverBackendIPs != nil {
discoverBackendIPs = *ui.DiscoverBackendIPs
}
if ui.URLPrefix != nil {
if err := ui.URLPrefix.sanitizeAndInitialize(); err != nil {
return err
}
if ui.RetryStatusCodes != nil {
retryStatusCodes = ui.RetryStatusCodes
}
if ui.LoadBalancingPolicy != "" {
loadBalancingPolicy = ui.LoadBalancingPolicy
}
if ui.DropSrcPathPrefixParts != nil {
dropSrcPathPrefixParts = *ui.DropSrcPathPrefixParts
}
if ui.DiscoverBackendIPs != nil {
discoverBackendIPs = *ui.DiscoverBackendIPs
}
ui.URLPrefix.retryStatusCodes = retryStatusCodes
ui.URLPrefix.dropSrcPathPrefixParts = dropSrcPathPrefixParts
ui.URLPrefix.discoverBackendIPs = discoverBackendIPs

View File

@@ -22,26 +22,26 @@ users:
# - or http://default2:8888/unsupported_url_handler?request_path=/non/existing/path
#
# Regular expressions are allowed in `src_paths` entries.
- username: "foobar"
url_map:
- src_paths:
- "/api/v1/query"
- "/api/v1/query_range"
- "/api/v1/label/[^/]+/values"
url_prefix:
- "http://vmselect1:8481/select/42/prometheus"
- "http://vmselect2:8481/select/42/prometheus"
- src_paths: ["/api/v1/write"]
url_prefix: "http://vminsert:8480/insert/42/prometheus"
headers:
- "X-Scope-OrgID: abc"
- username: "foobar"
ip_filters:
deny_list: [127.0.0.1]
default_url:
- "http://default1:8888/unsupported_url_handler"
- "http://default2:8888/unsupported_url_handler"
url_map:
- src_paths:
- "/api/v1/query"
- "/api/v1/query_range"
- "/api/v1/label/[^/]+/values"
url_prefix:
- "http://vmselect1:8481/select/42/prometheus"
- "http://vmselect2:8481/select/42/prometheus"
- src_paths: ["/api/v1/write"]
url_prefix: "http://vminsert:8480/insert/42/prometheus"
headers:
- "X-Scope-OrgID: abc"
default_url:
- "http://default1:8888/unsupported_url_handler"
- "http://default2:8888/unsupported_url_handler"
ip_filters:
allow_list: ["1.2.3.0/24", "127.0.0.1"]
deny_list:
- 10.1.0.1
- 10.1.0.1

View File

@@ -61,6 +61,9 @@ var (
"See https://docs.victoriametrics.com/vmauth/#backend-tls-setup")
backendTLSServerName = flag.String("backend.TLSServerName", "", "Optional TLS ServerName, which must be sent to HTTPS backend. "+
"See https://docs.victoriametrics.com/vmauth/#backend-tls-setup")
dryRun = flag.Bool("dryRun", false, "Whether to check only config files without running vmauth. The auth configuration file is validated. The -auth.config flag must be specified.")
removeXFFHTTPHeaderValue = flag.Bool(`removeXFFHTTPHeaderValue`, false, "Whether to remove the X-Forwarded-For HTTP header value from client requests before forwarding them to the backend. "+
"Recommended when vmauth is exposed to the internet.")
)
func main() {
@@ -71,6 +74,16 @@ func main() {
buildinfo.Init()
logger.Init()
if *dryRun {
if len(*authConfigPath) == 0 {
logger.Fatalf("missing required `-auth.config` command-line flag")
}
if _, err := reloadAuthConfig(); err != nil {
logger.Fatalf("failed to parse %q: %s", *authConfigPath, err)
}
return
}
listenAddrs := *httpListenAddrs
if len(listenAddrs) == 0 {
listenAddrs = []string{":8427"}
@@ -123,6 +136,12 @@ func requestHandler(w http.ResponseWriter, r *http.Request) bool {
ui := getUserInfoByAuthTokens(ats)
if ui == nil {
uu := authConfig.Load().UnauthorizedUser
if uu != nil {
processUserRequest(w, r, uu)
return true
}
invalidAuthTokenRequests.Inc()
if *logInvalidAuthTokens {
err := fmt.Errorf("cannot authorize request with auth tokens %q", ats)
@@ -192,7 +211,11 @@ func processRequest(w http.ResponseWriter, r *http.Request, ui *UserInfo) {
return
}
missingRouteRequests.Inc()
httpserver.Errorf(w, r, "missing route for %s", u.String())
var di string
if ui.DumpRequestOnErrors {
di = debugInfo(u, r.Header)
}
httpserver.Errorf(w, r, "missing route for %q%s", u.String(), di)
return
}
up, hc = ui.DefaultURL, ui.HeadersConf
@@ -371,7 +394,7 @@ func sanitizeRequestHeaders(r *http.Request) *http.Request {
// X-Forwarded-For information as a comma+space
// separated list and fold multiple headers into one.
prior := req.Header["X-Forwarded-For"]
if len(prior) > 0 {
if len(prior) > 0 && !*removeXFFHTTPHeaderValue {
clientIP = strings.Join(prior, ", ") + ", " + clientIP
}
req.Header.Set("X-Forwarded-For", clientIP)
@@ -644,3 +667,14 @@ func (rtb *readTrackingBody) Close() error {
return nil
}
func debugInfo(u *url.URL, h http.Header) string {
s := &strings.Builder{}
fmt.Fprintf(s, " (host: %q; ", u.Host)
fmt.Fprintf(s, "path: %q; ", u.Path)
fmt.Fprintf(s, "args: %q; ", u.Query().Encode())
fmt.Fprint(s, "headers:")
_ = h.WriteSubset(s, nil)
fmt.Fprint(s, ")")
return s.String()
}

View File

@@ -90,6 +90,20 @@ User-Agent: vmauth
X-Forwarded-For: 12.34.56.78, 42.2.3.84`
f(cfgStr, requestURL, backendHandler, responseExpected)
// routing of all failed to authorize requests to unauthorized_user (issue #7543)
cfgStr = `
unauthorized_user:
url_prefix: "{BACKEND}/foo"
keep_original_host: true`
requestURL = "http://foo:invalid-secret@some-host.com/abc/def"
backendHandler = func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "requested_url=http://%s%s", r.Host, r.URL)
}
responseExpected = `
statusCode=200
requested_url=http://some-host.com/foo/abc/def`
f(cfgStr, requestURL, backendHandler, responseExpected)
// keep_original_host
cfgStr = `
unauthorized_user:
@@ -346,7 +360,27 @@ unauthorized_user:
}
responseExpected = `
statusCode=400
remoteAddr: "42.2.3.84:6789, X-Forwarded-For: 12.34.56.78"; requestURI: /abc?de=fg; missing route for http://some-host.com/abc?de=fg`
remoteAddr: "42.2.3.84:6789, X-Forwarded-For: 12.34.56.78"; requestURI: /abc?de=fg; missing route for "http://some-host.com/abc?de=fg"`
f(cfgStr, requestURL, backendHandler, responseExpected)
// missing default_url and default url_prefix for unauthorized user with dump_request_on_errors enabled
cfgStr = `
unauthorized_user:
dump_request_on_errors: true
url_map:
- src_paths: ["/foo/.+"]
url_prefix: {BACKEND}/x-foo/`
requestURL = "http://some-host.com/abc?de=fg"
backendHandler = func(_ http.ResponseWriter, _ *http.Request) {
panic(fmt.Errorf("backend handler shouldn't be called"))
}
responseExpected = `
statusCode=400
remoteAddr: "42.2.3.84:6789, X-Forwarded-For: 12.34.56.78"; requestURI: /abc?de=fg; missing route for "http://some-host.com/abc?de=fg" (host: "some-host.com"; path: "/abc"; args: "de=fg"; headers:Connection: Some-Header,Other-Header
Pass-Header: abc
Some-Header: foobar
X-Forwarded-For: 12.34.56.78
)`
f(cfgStr, requestURL, backendHandler, responseExpected)
// missing default_url and default url_prefix for unauthorized user when there are configs for authorized users

View File

@@ -187,6 +187,10 @@ func TestCreateTargetURLSuccess(t *testing.T) {
RetryStatusCodes: []int{},
DropSrcPathPrefixParts: intp(0),
},
{
SrcPaths: getRegexs([]string{"/metrics"}),
URLPrefix: mustParseURL("http://metrics-server"),
},
},
URLPrefix: mustParseURL("http://default-server"),
HeadersConf: HeadersConf{
@@ -206,6 +210,35 @@ func TestCreateTargetURLSuccess(t *testing.T) {
"bb: aaa", "x: y", []int{502}, "least_loaded", 2)
f(ui, "https://foo-host/api/v1/write", "http://vminsert/0/prometheus/api/v1/write", "", "", []int{}, "least_loaded", 0)
f(ui, "https://foo-host/foo/bar/api/v1/query_range", "http://default-server/api/v1/query_range", "bb: aaa", "x: y", []int{502}, "least_loaded", 2)
f(ui, "https://foo-host/metrics", "http://metrics-server", "", "", []int{502}, "least_loaded", 2)
// Complex routing with `url_map` without global url_prefix
ui = &UserInfo{
URLMaps: []URLMap{
{
SrcPaths: getRegexs([]string{"/api/v1/write"}),
URLPrefix: mustParseURL("http://vminsert/0/prometheus"),
RetryStatusCodes: []int{},
DropSrcPathPrefixParts: intp(0),
},
{
SrcPaths: getRegexs([]string{"/metrics/a/b"}),
URLPrefix: mustParseURL("http://metrics-server"),
},
},
HeadersConf: HeadersConf{
RequestHeaders: []*Header{
mustNewHeader("'bb: aaa'"),
},
ResponseHeaders: []*Header{
mustNewHeader("'x: y'"),
},
},
RetryStatusCodes: []int{502},
DropSrcPathPrefixParts: intp(2),
}
f(ui, "https://foo-host/api/v1/write", "http://vminsert/0/prometheus/api/v1/write", "", "", []int{}, "least_loaded", 0)
f(ui, "https://foo-host/metrics/a/b", "http://metrics-server/b", "", "", []int{502}, "least_loaded", 2)
// Complex routing regexp paths in `url_map`
ui = &UserInfo{

View File

@@ -616,7 +616,7 @@ var (
},
&cli.BoolFlag{
Name: vmNativeDisableBinaryProtocol,
Usage: "Whether to use https://docs.victoriametrics.com/#how-to-export-data-in-json-line-format" +
Usage: "Whether to use https://docs.victoriametrics.com/#how-to-export-data-in-json-line-format " +
"instead of https://docs.victoriametrics.com/#how-to-export-data-in-native-format API." +
"Binary export/import API protocol implies less network and resource usage, as it transfers compressed binary data blocks." +
"Non-binary export/import API is less efficient, but supports deduplication if it is configured on vm-native-src-addr side.",

View File

@@ -266,7 +266,7 @@ func main() {
},
{
Name: "vm-native",
Usage: "Migrate time series between VictoriaMetrics installations via native binary format",
Usage: "Migrate time series between VictoriaMetrics installations",
Flags: mergeFlags(globalFlags, vmNativeFlags),
Before: beforeFn,
Action: func(c *cli.Context) error {

View File

@@ -1,348 +1,348 @@
package main
import (
"context"
"net/http"
"testing"
"time"
"github.com/prometheus/prometheus/prompb"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmctl/backoff"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmctl/barpool"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmctl/remoteread"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmctl/stepper"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmctl/testdata/servers_integration_test"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmctl/vm"
)
func TestRemoteRead(t *testing.T) {
barpool.Disable(true)
defer func() {
barpool.Disable(false)
}()
defer func() { isSilent = false }()
var testCases = []struct {
name string
remoteReadConfig remoteread.Config
vmCfg vm.Config
start string
end string
numOfSamples int64
numOfSeries int64
rrp remoteReadProcessor
chunk string
remoteReadSeries func(start, end, numOfSeries, numOfSamples int64) []*prompb.TimeSeries
expectedSeries []vm.TimeSeries
}{
{
name: "step minute on minute time range",
remoteReadConfig: remoteread.Config{Addr: "", LabelName: "__name__", LabelValue: ".*"},
vmCfg: vm.Config{Addr: "", Concurrency: 1},
start: "2022-11-26T11:23:05+02:00",
end: "2022-11-26T11:24:05+02:00",
numOfSamples: 2,
numOfSeries: 3,
chunk: stepper.StepMinute,
remoteReadSeries: remote_read_integration.GenerateRemoteReadSeries,
expectedSeries: []vm.TimeSeries{
{
Name: "vm_metric_1",
LabelPairs: []vm.LabelPair{{Name: "job", Value: "0"}},
Timestamps: []int64{1669454585000, 1669454615000},
Values: []float64{0, 0},
},
{
Name: "vm_metric_1",
LabelPairs: []vm.LabelPair{{Name: "job", Value: "1"}},
Timestamps: []int64{1669454585000, 1669454615000},
Values: []float64{100, 100},
},
{
Name: "vm_metric_1",
LabelPairs: []vm.LabelPair{{Name: "job", Value: "2"}},
Timestamps: []int64{1669454585000, 1669454615000},
Values: []float64{200, 200},
},
},
},
{
name: "step month on month time range",
remoteReadConfig: remoteread.Config{Addr: "", LabelName: "__name__", LabelValue: ".*"},
vmCfg: vm.Config{Addr: "", Concurrency: 1,
Transport: http.DefaultTransport.(*http.Transport)},
start: "2022-09-26T11:23:05+02:00",
end: "2022-11-26T11:24:05+02:00",
numOfSamples: 2,
numOfSeries: 3,
chunk: stepper.StepMonth,
remoteReadSeries: remote_read_integration.GenerateRemoteReadSeries,
expectedSeries: []vm.TimeSeries{
{
Name: "vm_metric_1",
LabelPairs: []vm.LabelPair{{Name: "job", Value: "0"}},
Timestamps: []int64{1664184185000},
Values: []float64{0},
},
{
Name: "vm_metric_1",
LabelPairs: []vm.LabelPair{{Name: "job", Value: "1"}},
Timestamps: []int64{1664184185000},
Values: []float64{100},
},
{
Name: "vm_metric_1",
LabelPairs: []vm.LabelPair{{Name: "job", Value: "2"}},
Timestamps: []int64{1664184185000},
Values: []float64{200},
},
{
Name: "vm_metric_1",
LabelPairs: []vm.LabelPair{{Name: "job", Value: "0"}},
Timestamps: []int64{1666819415000},
Values: []float64{0},
},
{
Name: "vm_metric_1",
LabelPairs: []vm.LabelPair{{Name: "job", Value: "1"}},
Timestamps: []int64{1666819415000},
Values: []float64{100},
},
{
Name: "vm_metric_1",
LabelPairs: []vm.LabelPair{{Name: "job", Value: "2"}},
Timestamps: []int64{1666819415000},
Values: []float64{200}},
},
},
}
for _, tt := range testCases {
t.Run(tt.name, func(t *testing.T) {
ctx := context.Background()
remoteReadServer := remote_read_integration.NewRemoteReadServer(t)
defer remoteReadServer.Close()
remoteWriteServer := remote_read_integration.NewRemoteWriteServer(t)
defer remoteWriteServer.Close()
tt.remoteReadConfig.Addr = remoteReadServer.URL()
rr, err := remoteread.NewClient(tt.remoteReadConfig)
if err != nil {
t.Fatalf("error create remote read client: %s", err)
}
start, err := time.Parse(time.RFC3339, tt.start)
if err != nil {
t.Fatalf("Error parse start time: %s", err)
}
end, err := time.Parse(time.RFC3339, tt.end)
if err != nil {
t.Fatalf("Error parse end time: %s", err)
}
rrs := tt.remoteReadSeries(start.Unix(), end.Unix(), tt.numOfSeries, tt.numOfSamples)
remoteReadServer.SetRemoteReadSeries(rrs)
remoteWriteServer.ExpectedSeries(tt.expectedSeries)
tt.vmCfg.Addr = remoteWriteServer.URL()
b, err := backoff.New(10, 1.8, time.Second*2)
if err != nil {
t.Fatalf("failed to create backoff: %s", err)
}
tt.vmCfg.Backoff = b
importer, err := vm.NewImporter(ctx, tt.vmCfg)
if err != nil {
t.Fatalf("failed to create VM importer: %s", err)
}
defer importer.Close()
rmp := remoteReadProcessor{
src: rr,
dst: importer,
filter: remoteReadFilter{
timeStart: &start,
timeEnd: &end,
chunk: tt.chunk,
},
cc: 1,
isVerbose: false,
}
err = rmp.run(ctx)
if err != nil {
t.Fatalf("failed to run remote read processor: %s", err)
}
})
}
}
func TestSteamRemoteRead(t *testing.T) {
barpool.Disable(true)
defer func() {
barpool.Disable(false)
}()
defer func() { isSilent = false }()
var testCases = []struct {
name string
remoteReadConfig remoteread.Config
vmCfg vm.Config
start string
end string
numOfSamples int64
numOfSeries int64
rrp remoteReadProcessor
chunk string
remoteReadSeries func(start, end, numOfSeries, numOfSamples int64) []*prompb.TimeSeries
expectedSeries []vm.TimeSeries
}{
{
name: "step minute on minute time range",
remoteReadConfig: remoteread.Config{Addr: "", LabelName: "__name__", LabelValue: ".*", UseStream: true},
vmCfg: vm.Config{Addr: "", Concurrency: 1},
start: "2022-11-26T11:23:05+02:00",
end: "2022-11-26T11:24:05+02:00",
numOfSamples: 2,
numOfSeries: 3,
chunk: stepper.StepMinute,
remoteReadSeries: remote_read_integration.GenerateRemoteReadSeries,
expectedSeries: []vm.TimeSeries{
{
Name: "vm_metric_1",
LabelPairs: []vm.LabelPair{{Name: "job", Value: "0"}},
Timestamps: []int64{1669454585000, 1669454615000},
Values: []float64{0, 0},
},
{
Name: "vm_metric_1",
LabelPairs: []vm.LabelPair{{Name: "job", Value: "1"}},
Timestamps: []int64{1669454585000, 1669454615000},
Values: []float64{100, 100},
},
{
Name: "vm_metric_1",
LabelPairs: []vm.LabelPair{{Name: "job", Value: "2"}},
Timestamps: []int64{1669454585000, 1669454615000},
Values: []float64{200, 200},
},
},
},
{
name: "step month on month time range",
remoteReadConfig: remoteread.Config{Addr: "", LabelName: "__name__", LabelValue: ".*", UseStream: true},
vmCfg: vm.Config{Addr: "", Concurrency: 1},
start: "2022-09-26T11:23:05+02:00",
end: "2022-11-26T11:24:05+02:00",
numOfSamples: 2,
numOfSeries: 3,
chunk: stepper.StepMonth,
remoteReadSeries: remote_read_integration.GenerateRemoteReadSeries,
expectedSeries: []vm.TimeSeries{
{
Name: "vm_metric_1",
LabelPairs: []vm.LabelPair{{Name: "job", Value: "0"}},
Timestamps: []int64{1664184185000},
Values: []float64{0},
},
{
Name: "vm_metric_1",
LabelPairs: []vm.LabelPair{{Name: "job", Value: "1"}},
Timestamps: []int64{1664184185000},
Values: []float64{100},
},
{
Name: "vm_metric_1",
LabelPairs: []vm.LabelPair{{Name: "job", Value: "2"}},
Timestamps: []int64{1664184185000},
Values: []float64{200},
},
{
Name: "vm_metric_1",
LabelPairs: []vm.LabelPair{{Name: "job", Value: "0"}},
Timestamps: []int64{1666819415000},
Values: []float64{0},
},
{
Name: "vm_metric_1",
LabelPairs: []vm.LabelPair{{Name: "job", Value: "1"}},
Timestamps: []int64{1666819415000},
Values: []float64{100},
},
{
Name: "vm_metric_1",
LabelPairs: []vm.LabelPair{{Name: "job", Value: "2"}},
Timestamps: []int64{1666819415000},
Values: []float64{200}},
},
},
}
for _, tt := range testCases {
t.Run(tt.name, func(t *testing.T) {
ctx := context.Background()
remoteReadServer := remote_read_integration.NewRemoteReadStreamServer(t)
defer remoteReadServer.Close()
remoteWriteServer := remote_read_integration.NewRemoteWriteServer(t)
defer remoteWriteServer.Close()
tt.remoteReadConfig.Addr = remoteReadServer.URL()
rr, err := remoteread.NewClient(tt.remoteReadConfig)
if err != nil {
t.Fatalf("error create remote read client: %s", err)
}
start, err := time.Parse(time.RFC3339, tt.start)
if err != nil {
t.Fatalf("Error parse start time: %s", err)
}
end, err := time.Parse(time.RFC3339, tt.end)
if err != nil {
t.Fatalf("Error parse end time: %s", err)
}
rrs := tt.remoteReadSeries(start.Unix(), end.Unix(), tt.numOfSeries, tt.numOfSamples)
remoteReadServer.InitMockStorage(rrs)
remoteWriteServer.ExpectedSeries(tt.expectedSeries)
tt.vmCfg.Addr = remoteWriteServer.URL()
b, err := backoff.New(10, 1.8, time.Second*2)
if err != nil {
t.Fatalf("failed to create backoff: %s", err)
}
tt.vmCfg.Backoff = b
importer, err := vm.NewImporter(ctx, tt.vmCfg)
if err != nil {
t.Fatalf("failed to create VM importer: %s", err)
}
defer importer.Close()
rmp := remoteReadProcessor{
src: rr,
dst: importer,
filter: remoteReadFilter{
timeStart: &start,
timeEnd: &end,
chunk: tt.chunk,
},
cc: 1,
isVerbose: false,
}
err = rmp.run(ctx)
if err != nil {
t.Fatalf("failed to run remote read processor: %s", err)
}
})
}
}
// import (
// "context"
// "net/http"
// "testing"
// "time"
//
// "github.com/prometheus/prometheus/prompb"
//
// "github.com/VictoriaMetrics/VictoriaMetrics/app/vmctl/backoff"
// "github.com/VictoriaMetrics/VictoriaMetrics/app/vmctl/barpool"
// "github.com/VictoriaMetrics/VictoriaMetrics/app/vmctl/remoteread"
// "github.com/VictoriaMetrics/VictoriaMetrics/app/vmctl/stepper"
// "github.com/VictoriaMetrics/VictoriaMetrics/app/vmctl/testdata/servers_integration_test"
// "github.com/VictoriaMetrics/VictoriaMetrics/app/vmctl/vm"
// )
//
// func TestRemoteRead(t *testing.T) {
// barpool.Disable(true)
// defer func() {
// barpool.Disable(false)
// }()
// defer func() { isSilent = false }()
//
// var testCases = []struct {
// name string
// remoteReadConfig remoteread.Config
// vmCfg vm.Config
// start string
// end string
// numOfSamples int64
// numOfSeries int64
// rrp remoteReadProcessor
// chunk string
// remoteReadSeries func(start, end, numOfSeries, numOfSamples int64) []*prompb.TimeSeries
// expectedSeries []vm.TimeSeries
// }{
// {
// name: "step minute on minute time range",
// remoteReadConfig: remoteread.Config{Addr: "", LabelName: "__name__", LabelValue: ".*"},
// vmCfg: vm.Config{Addr: "", Concurrency: 1},
// start: "2022-11-26T11:23:05+02:00",
// end: "2022-11-26T11:24:05+02:00",
// numOfSamples: 2,
// numOfSeries: 3,
// chunk: stepper.StepMinute,
// remoteReadSeries: remote_read_integration.GenerateRemoteReadSeries,
// expectedSeries: []vm.TimeSeries{
// {
// Name: "vm_metric_1",
// LabelPairs: []vm.LabelPair{{Name: "job", Value: "0"}},
// Timestamps: []int64{1669454585000, 1669454615000},
// Values: []float64{0, 0},
// },
// {
// Name: "vm_metric_1",
// LabelPairs: []vm.LabelPair{{Name: "job", Value: "1"}},
// Timestamps: []int64{1669454585000, 1669454615000},
// Values: []float64{100, 100},
// },
// {
// Name: "vm_metric_1",
// LabelPairs: []vm.LabelPair{{Name: "job", Value: "2"}},
// Timestamps: []int64{1669454585000, 1669454615000},
// Values: []float64{200, 200},
// },
// },
// },
// {
// name: "step month on month time range",
// remoteReadConfig: remoteread.Config{Addr: "", LabelName: "__name__", LabelValue: ".*"},
// vmCfg: vm.Config{Addr: "", Concurrency: 1,
// Transport: http.DefaultTransport.(*http.Transport)},
// start: "2022-09-26T11:23:05+02:00",
// end: "2022-11-26T11:24:05+02:00",
// numOfSamples: 2,
// numOfSeries: 3,
// chunk: stepper.StepMonth,
// remoteReadSeries: remote_read_integration.GenerateRemoteReadSeries,
// expectedSeries: []vm.TimeSeries{
// {
// Name: "vm_metric_1",
// LabelPairs: []vm.LabelPair{{Name: "job", Value: "0"}},
// Timestamps: []int64{1664184185000},
// Values: []float64{0},
// },
// {
// Name: "vm_metric_1",
// LabelPairs: []vm.LabelPair{{Name: "job", Value: "1"}},
// Timestamps: []int64{1664184185000},
// Values: []float64{100},
// },
// {
// Name: "vm_metric_1",
// LabelPairs: []vm.LabelPair{{Name: "job", Value: "2"}},
// Timestamps: []int64{1664184185000},
// Values: []float64{200},
// },
// {
// Name: "vm_metric_1",
// LabelPairs: []vm.LabelPair{{Name: "job", Value: "0"}},
// Timestamps: []int64{1666819415000},
// Values: []float64{0},
// },
// {
// Name: "vm_metric_1",
// LabelPairs: []vm.LabelPair{{Name: "job", Value: "1"}},
// Timestamps: []int64{1666819415000},
// Values: []float64{100},
// },
// {
// Name: "vm_metric_1",
// LabelPairs: []vm.LabelPair{{Name: "job", Value: "2"}},
// Timestamps: []int64{1666819415000},
// Values: []float64{200}},
// },
// },
// }
//
// for _, tt := range testCases {
// t.Run(tt.name, func(t *testing.T) {
// ctx := context.Background()
// remoteReadServer := remote_read_integration.NewRemoteReadServer(t)
// defer remoteReadServer.Close()
// remoteWriteServer := remote_read_integration.NewRemoteWriteServer(t)
// defer remoteWriteServer.Close()
//
// tt.remoteReadConfig.Addr = remoteReadServer.URL()
//
// rr, err := remoteread.NewClient(tt.remoteReadConfig)
// if err != nil {
// t.Fatalf("error create remote read client: %s", err)
// }
//
// start, err := time.Parse(time.RFC3339, tt.start)
// if err != nil {
// t.Fatalf("Error parse start time: %s", err)
// }
//
// end, err := time.Parse(time.RFC3339, tt.end)
// if err != nil {
// t.Fatalf("Error parse end time: %s", err)
// }
//
// rrs := tt.remoteReadSeries(start.Unix(), end.Unix(), tt.numOfSeries, tt.numOfSamples)
//
// remoteReadServer.SetRemoteReadSeries(rrs)
// remoteWriteServer.ExpectedSeries(tt.expectedSeries)
//
// tt.vmCfg.Addr = remoteWriteServer.URL()
//
// b, err := backoff.New(10, 1.8, time.Second*2)
// if err != nil {
// t.Fatalf("failed to create backoff: %s", err)
// }
// tt.vmCfg.Backoff = b
//
// importer, err := vm.NewImporter(ctx, tt.vmCfg)
// if err != nil {
// t.Fatalf("failed to create VM importer: %s", err)
// }
// defer importer.Close()
//
// rmp := remoteReadProcessor{
// src: rr,
// dst: importer,
// filter: remoteReadFilter{
// timeStart: &start,
// timeEnd: &end,
// chunk: tt.chunk,
// },
// cc: 1,
// isVerbose: false,
// }
//
// err = rmp.run(ctx)
// if err != nil {
// t.Fatalf("failed to run remote read processor: %s", err)
// }
// })
// }
// }
//
// func TestSteamRemoteRead(t *testing.T) {
// barpool.Disable(true)
// defer func() {
// barpool.Disable(false)
// }()
// defer func() { isSilent = false }()
//
// var testCases = []struct {
// name string
// remoteReadConfig remoteread.Config
// vmCfg vm.Config
// start string
// end string
// numOfSamples int64
// numOfSeries int64
// rrp remoteReadProcessor
// chunk string
// remoteReadSeries func(start, end, numOfSeries, numOfSamples int64) []*prompb.TimeSeries
// expectedSeries []vm.TimeSeries
// }{
// {
// name: "step minute on minute time range",
// remoteReadConfig: remoteread.Config{Addr: "", LabelName: "__name__", LabelValue: ".*", UseStream: true},
// vmCfg: vm.Config{Addr: "", Concurrency: 1},
// start: "2022-11-26T11:23:05+02:00",
// end: "2022-11-26T11:24:05+02:00",
// numOfSamples: 2,
// numOfSeries: 3,
// chunk: stepper.StepMinute,
// remoteReadSeries: remote_read_integration.GenerateRemoteReadSeries,
// expectedSeries: []vm.TimeSeries{
// {
// Name: "vm_metric_1",
// LabelPairs: []vm.LabelPair{{Name: "job", Value: "0"}},
// Timestamps: []int64{1669454585000, 1669454615000},
// Values: []float64{0, 0},
// },
// {
// Name: "vm_metric_1",
// LabelPairs: []vm.LabelPair{{Name: "job", Value: "1"}},
// Timestamps: []int64{1669454585000, 1669454615000},
// Values: []float64{100, 100},
// },
// {
// Name: "vm_metric_1",
// LabelPairs: []vm.LabelPair{{Name: "job", Value: "2"}},
// Timestamps: []int64{1669454585000, 1669454615000},
// Values: []float64{200, 200},
// },
// },
// },
// {
// name: "step month on month time range",
// remoteReadConfig: remoteread.Config{Addr: "", LabelName: "__name__", LabelValue: ".*", UseStream: true},
// vmCfg: vm.Config{Addr: "", Concurrency: 1},
// start: "2022-09-26T11:23:05+02:00",
// end: "2022-11-26T11:24:05+02:00",
// numOfSamples: 2,
// numOfSeries: 3,
// chunk: stepper.StepMonth,
// remoteReadSeries: remote_read_integration.GenerateRemoteReadSeries,
// expectedSeries: []vm.TimeSeries{
// {
// Name: "vm_metric_1",
// LabelPairs: []vm.LabelPair{{Name: "job", Value: "0"}},
// Timestamps: []int64{1664184185000},
// Values: []float64{0},
// },
// {
// Name: "vm_metric_1",
// LabelPairs: []vm.LabelPair{{Name: "job", Value: "1"}},
// Timestamps: []int64{1664184185000},
// Values: []float64{100},
// },
// {
// Name: "vm_metric_1",
// LabelPairs: []vm.LabelPair{{Name: "job", Value: "2"}},
// Timestamps: []int64{1664184185000},
// Values: []float64{200},
// },
// {
// Name: "vm_metric_1",
// LabelPairs: []vm.LabelPair{{Name: "job", Value: "0"}},
// Timestamps: []int64{1666819415000},
// Values: []float64{0},
// },
// {
// Name: "vm_metric_1",
// LabelPairs: []vm.LabelPair{{Name: "job", Value: "1"}},
// Timestamps: []int64{1666819415000},
// Values: []float64{100},
// },
// {
// Name: "vm_metric_1",
// LabelPairs: []vm.LabelPair{{Name: "job", Value: "2"}},
// Timestamps: []int64{1666819415000},
// Values: []float64{200}},
// },
// },
// }
//
// for _, tt := range testCases {
// t.Run(tt.name, func(t *testing.T) {
// ctx := context.Background()
// remoteReadServer := remote_read_integration.NewRemoteReadStreamServer(t)
// defer remoteReadServer.Close()
// remoteWriteServer := remote_read_integration.NewRemoteWriteServer(t)
// defer remoteWriteServer.Close()
//
// tt.remoteReadConfig.Addr = remoteReadServer.URL()
//
// rr, err := remoteread.NewClient(tt.remoteReadConfig)
// if err != nil {
// t.Fatalf("error create remote read client: %s", err)
// }
//
// start, err := time.Parse(time.RFC3339, tt.start)
// if err != nil {
// t.Fatalf("Error parse start time: %s", err)
// }
//
// end, err := time.Parse(time.RFC3339, tt.end)
// if err != nil {
// t.Fatalf("Error parse end time: %s", err)
// }
//
// rrs := tt.remoteReadSeries(start.Unix(), end.Unix(), tt.numOfSeries, tt.numOfSamples)
//
// remoteReadServer.InitMockStorage(rrs)
// remoteWriteServer.ExpectedSeries(tt.expectedSeries)
//
// tt.vmCfg.Addr = remoteWriteServer.URL()
//
// b, err := backoff.New(10, 1.8, time.Second*2)
// if err != nil {
// t.Fatalf("failed to create backoff: %s", err)
// }
//
// tt.vmCfg.Backoff = b
// importer, err := vm.NewImporter(ctx, tt.vmCfg)
// if err != nil {
// t.Fatalf("failed to create VM importer: %s", err)
// }
// defer importer.Close()
//
// rmp := remoteReadProcessor{
// src: rr,
// dst: importer,
// filter: remoteReadFilter{
// timeStart: &start,
// timeEnd: &end,
// chunk: tt.chunk,
// },
// cc: 1,
// isVerbose: false,
// }
//
// err = rmp.run(ctx)
// if err != nil {
// t.Fatalf("failed to run remote read processor: %s", err)
// }
// })
// }
// }

View File

@@ -15,8 +15,10 @@ import (
"github.com/VictoriaMetrics/VictoriaMetrics/lib/bytesutil"
"github.com/gogo/protobuf/proto"
"github.com/golang/snappy"
"github.com/prometheus/prometheus/config"
"github.com/prometheus/prometheus/prompb"
"github.com/prometheus/prometheus/storage/remote"
"github.com/prometheus/prometheus/tsdb/chunkenc"
)
@@ -238,7 +240,7 @@ func processStreamResponse(body io.ReadCloser, callback StreamCallback) error {
bb := bbPool.Get()
defer func() { bbPool.Put(bb) }()
stream := remote.NewChunkedReader(body, remote.DefaultChunkedReadLimit, bb.B)
stream := remote.NewChunkedReader(body, config.DefaultChunkedReadLimit, bb.B)
for {
res := &prompb.ChunkedReadResponse{}
err := stream.NextProto(res)

View File

@@ -1,368 +1,368 @@
package remote_read_integration
import (
"context"
"fmt"
"io"
"net/http"
"net/http/httptest"
"strconv"
"strings"
"testing"
"github.com/gogo/protobuf/proto"
"github.com/golang/snappy"
"github.com/prometheus/prometheus/model/labels"
"github.com/prometheus/prometheus/prompb"
"github.com/prometheus/prometheus/storage/remote"
"github.com/prometheus/prometheus/tsdb/chunks"
)
const (
maxBytesInFrame = 1024 * 1024
)
type RemoteReadServer struct {
server *httptest.Server
series []*prompb.TimeSeries
storage *MockStorage
}
// NewRemoteReadServer creates a remote read server. It exposes a single endpoint and responds with the
// passed series based on the request to the read endpoint. It returns a server which should be closed after
// being used.
func NewRemoteReadServer(t *testing.T) *RemoteReadServer {
rrs := &RemoteReadServer{
series: make([]*prompb.TimeSeries, 0),
}
rrs.server = httptest.NewServer(rrs.getReadHandler(t))
return rrs
}
// Close closes the server.
func (rrs *RemoteReadServer) Close() {
rrs.server.Close()
}
func (rrs *RemoteReadServer) URL() string {
return rrs.server.URL
}
func (rrs *RemoteReadServer) SetRemoteReadSeries(series []*prompb.TimeSeries) {
rrs.series = append(rrs.series, series...)
}
func (rrs *RemoteReadServer) getReadHandler(t *testing.T) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !validateReadHeaders(t, r) {
t.Fatalf("invalid read headers")
}
compressed, err := io.ReadAll(r.Body)
if err != nil {
t.Fatalf("error read body: %s", err)
}
reqBuf, err := snappy.Decode(nil, compressed)
if err != nil {
t.Fatalf("error decode compressed data:%s", err)
}
var req prompb.ReadRequest
if err := proto.Unmarshal(reqBuf, &req); err != nil {
t.Fatalf("error unmarshal read request: %s", err)
}
resp := &prompb.ReadResponse{
Results: make([]*prompb.QueryResult, len(req.Queries)),
}
for i, r := range req.Queries {
startTs := r.StartTimestampMs
endTs := r.EndTimestampMs
ts := make([]*prompb.TimeSeries, len(rrs.series))
for i, s := range rrs.series {
var samples []prompb.Sample
for _, sample := range s.Samples {
if sample.Timestamp >= startTs && sample.Timestamp < endTs {
samples = append(samples, sample)
}
}
var series prompb.TimeSeries
if len(samples) > 0 {
series.Labels = s.Labels
series.Samples = samples
}
ts[i] = &series
}
resp.Results[i] = &prompb.QueryResult{Timeseries: ts}
data, err := proto.Marshal(resp)
if err != nil {
t.Fatalf("error marshal response: %s", err)
}
compressed = snappy.Encode(nil, data)
w.Header().Set("Content-Type", "application/x-protobuf")
w.Header().Set("Content-Encoding", "snappy")
w.WriteHeader(http.StatusOK)
if _, err := w.Write(compressed); err != nil {
t.Fatalf("snappy encode error: %s", err)
}
}
})
}
func NewRemoteReadStreamServer(t *testing.T) *RemoteReadServer {
rrs := &RemoteReadServer{
series: make([]*prompb.TimeSeries, 0),
}
rrs.server = httptest.NewServer(rrs.getStreamReadHandler(t))
return rrs
}
func (rrs *RemoteReadServer) InitMockStorage(series []*prompb.TimeSeries) {
rrs.storage = NewMockStorage(series)
}
func (rrs *RemoteReadServer) getStreamReadHandler(t *testing.T) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !validateStreamReadHeaders(t, r) {
t.Fatalf("invalid read headers")
}
f, ok := w.(http.Flusher)
if !ok {
t.Fatalf("internal http.ResponseWriter does not implement http.Flusher interface")
}
stream := remote.NewChunkedWriter(w, f)
data, err := io.ReadAll(r.Body)
if err != nil {
t.Fatalf("error read body: %s", err)
}
decodedData, err := snappy.Decode(nil, data)
if err != nil {
t.Fatalf("error decode compressed data:%s", err)
}
var req prompb.ReadRequest
if err := proto.Unmarshal(decodedData, &req); err != nil {
t.Fatalf("error unmarshal read request: %s", err)
}
var chks []prompb.Chunk
ctx := context.Background()
for idx, r := range req.Queries {
startTs := r.StartTimestampMs
endTs := r.EndTimestampMs
var matchers []*labels.Matcher
cb := func() (int64, error) { return 0, nil }
c := remote.NewSampleAndChunkQueryableClient(rrs.storage, nil, matchers, true, cb)
q, err := c.ChunkQuerier(startTs, endTs)
if err != nil {
t.Fatalf("error init chunk querier: %s", err)
}
ss := q.Select(ctx, false, nil, matchers...)
var iter chunks.Iterator
for ss.Next() {
series := ss.At()
iter = series.Iterator(iter)
labels := remote.MergeLabels(labelsToLabelsProto(series.Labels()), nil)
frameBytesLeft := maxBytesInFrame
for _, lb := range labels {
frameBytesLeft -= lb.Size()
}
isNext := iter.Next()
for isNext {
chunk := iter.At()
if chunk.Chunk == nil {
t.Fatalf("error found not populated chunk returned by SeriesSet at ref: %v", chunk.Ref)
}
chks = append(chks, prompb.Chunk{
MinTimeMs: chunk.MinTime,
MaxTimeMs: chunk.MaxTime,
Type: prompb.Chunk_Encoding(chunk.Chunk.Encoding()),
Data: chunk.Chunk.Bytes(),
})
frameBytesLeft -= chks[len(chks)-1].Size()
// We are fine with minor inaccuracy of max bytes per frame. The inaccuracy will be max of full chunk size.
isNext = iter.Next()
if frameBytesLeft > 0 && isNext {
continue
}
resp := &prompb.ChunkedReadResponse{
ChunkedSeries: []*prompb.ChunkedSeries{
{Labels: labels, Chunks: chks},
},
QueryIndex: int64(idx),
}
b, err := proto.Marshal(resp)
if err != nil {
t.Fatalf("error marshal response: %s", err)
}
if _, err := stream.Write(b); err != nil {
t.Fatalf("error write to stream: %s", err)
}
chks = chks[:0]
rrs.storage.Reset()
}
if err := iter.Err(); err != nil {
t.Fatalf("error iterate over chunk series: %s", err)
}
}
}
})
}
func validateReadHeaders(t *testing.T, r *http.Request) bool {
if r.Method != http.MethodPost {
t.Fatalf("got %q method, expected %q", r.Method, http.MethodPost)
}
if r.Header.Get("Content-Encoding") != "snappy" {
t.Fatalf("got %q content encoding header, expected %q", r.Header.Get("Content-Encoding"), "snappy")
}
if r.Header.Get("Content-Type") != "application/x-protobuf" {
t.Fatalf("got %q content type header, expected %q", r.Header.Get("Content-Type"), "application/x-protobuf")
}
remoteReadVersion := r.Header.Get("X-Prometheus-Remote-Read-Version")
if remoteReadVersion == "" {
t.Fatalf("got empty prometheus remote read header")
}
if !strings.HasPrefix(remoteReadVersion, "0.1.") {
t.Fatalf("wrong remote version defined")
}
return true
}
func validateStreamReadHeaders(t *testing.T, r *http.Request) bool {
if r.Method != http.MethodPost {
t.Fatalf("got %q method, expected %q", r.Method, http.MethodPost)
}
if r.Header.Get("Content-Encoding") != "snappy" {
t.Fatalf("got %q content encoding header, expected %q", r.Header.Get("Content-Encoding"), "snappy")
}
if r.Header.Get("Content-Type") != "application/x-streamed-protobuf; proto=prometheus.ChunkedReadResponse" {
t.Fatalf("got %q content type header, expected %q", r.Header.Get("Content-Type"), "application/x-streamed-protobuf; proto=prometheus.ChunkedReadResponse")
}
remoteReadVersion := r.Header.Get("X-Prometheus-Remote-Read-Version")
if remoteReadVersion == "" {
t.Fatalf("got empty prometheus remote read header")
}
if !strings.HasPrefix(remoteReadVersion, "0.1.") {
t.Fatalf("wrong remote version defined")
}
return true
}
func GenerateRemoteReadSeries(start, end, numOfSeries, numOfSamples int64) []*prompb.TimeSeries {
var ts []*prompb.TimeSeries
j := 0
for i := 0; i < int(numOfSeries); i++ {
if i%3 == 0 {
j++
}
timeSeries := prompb.TimeSeries{
Labels: []prompb.Label{
{Name: labels.MetricName, Value: fmt.Sprintf("vm_metric_%d", j)},
{Name: "job", Value: strconv.Itoa(i)},
},
}
ts = append(ts, &timeSeries)
}
for i := range ts {
ts[i].Samples = generateRemoteReadSamples(i, start, end, numOfSamples)
}
return ts
}
func generateRemoteReadSamples(idx int, startTime, endTime, numOfSamples int64) []prompb.Sample {
samples := make([]prompb.Sample, 0)
delta := (endTime - startTime) / numOfSamples
t := startTime
for t != endTime {
v := 100 * int64(idx)
samples = append(samples, prompb.Sample{
Timestamp: t * 1000,
Value: float64(v),
})
t = t + delta
}
return samples
}
type MockStorage struct {
query *prompb.Query
store []*prompb.TimeSeries
}
func NewMockStorage(series []*prompb.TimeSeries) *MockStorage {
return &MockStorage{store: series}
}
func (ms *MockStorage) Read(_ context.Context, query *prompb.Query) (*prompb.QueryResult, error) {
if ms.query != nil {
return nil, fmt.Errorf("expected only one call to remote client got: %v", query)
}
ms.query = query
q := &prompb.QueryResult{Timeseries: make([]*prompb.TimeSeries, 0, len(ms.store))}
for _, s := range ms.store {
var samples []prompb.Sample
for _, sample := range s.Samples {
if sample.Timestamp >= query.StartTimestampMs && sample.Timestamp < query.EndTimestampMs {
samples = append(samples, sample)
}
}
var series prompb.TimeSeries
if len(samples) > 0 {
series.Labels = s.Labels
series.Samples = samples
}
q.Timeseries = append(q.Timeseries, &series)
}
return q, nil
}
func (ms *MockStorage) Reset() {
ms.query = nil
}
func labelsToLabelsProto(labels labels.Labels) []prompb.Label {
result := make([]prompb.Label, 0, len(labels))
for _, l := range labels {
result = append(result, prompb.Label{
Name: l.Name,
Value: l.Value,
})
}
return result
}
// import (
// "context"
// "fmt"
// "io"
// "net/http"
// "net/http/httptest"
// "strconv"
// "strings"
// "testing"
//
// "github.com/gogo/protobuf/proto"
// "github.com/golang/snappy"
// "github.com/prometheus/prometheus/model/labels"
// "github.com/prometheus/prometheus/prompb"
// "github.com/prometheus/prometheus/storage/remote"
// "github.com/prometheus/prometheus/tsdb/chunks"
// )
//
// const (
// maxBytesInFrame = 1024 * 1024
// )
//
// type RemoteReadServer struct {
// server *httptest.Server
// series []*prompb.TimeSeries
// storage *MockStorage
// }
//
// // NewRemoteReadServer creates a remote read server. It exposes a single endpoint and responds with the
// // passed series based on the request to the read endpoint. It returns a server which should be closed after
// // being used.
// func NewRemoteReadServer(t *testing.T) *RemoteReadServer {
// rrs := &RemoteReadServer{
// series: make([]*prompb.TimeSeries, 0),
// }
// rrs.server = httptest.NewServer(rrs.getReadHandler(t))
// return rrs
// }
//
// // Close closes the server.
// func (rrs *RemoteReadServer) Close() {
// rrs.server.Close()
// }
//
// func (rrs *RemoteReadServer) URL() string {
// return rrs.server.URL
// }
//
// func (rrs *RemoteReadServer) SetRemoteReadSeries(series []*prompb.TimeSeries) {
// rrs.series = append(rrs.series, series...)
// }
//
// func (rrs *RemoteReadServer) getReadHandler(t *testing.T) http.Handler {
// return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// if !validateReadHeaders(t, r) {
// t.Fatalf("invalid read headers")
// }
//
// compressed, err := io.ReadAll(r.Body)
// if err != nil {
// t.Fatalf("error read body: %s", err)
// }
//
// reqBuf, err := snappy.Decode(nil, compressed)
// if err != nil {
// t.Fatalf("error decode compressed data:%s", err)
// }
//
// var req prompb.ReadRequest
// if err := proto.Unmarshal(reqBuf, &req); err != nil {
// t.Fatalf("error unmarshal read request: %s", err)
// }
//
// resp := &prompb.ReadResponse{
// Results: make([]*prompb.QueryResult, len(req.Queries)),
// }
//
// for i, r := range req.Queries {
// startTs := r.StartTimestampMs
// endTs := r.EndTimestampMs
// ts := make([]*prompb.TimeSeries, len(rrs.series))
// for i, s := range rrs.series {
// var samples []prompb.Sample
// for _, sample := range s.Samples {
// if sample.Timestamp >= startTs && sample.Timestamp < endTs {
// samples = append(samples, sample)
// }
// }
// var series prompb.TimeSeries
// if len(samples) > 0 {
// series.Labels = s.Labels
// series.Samples = samples
// }
// ts[i] = &series
// }
//
// resp.Results[i] = &prompb.QueryResult{Timeseries: ts}
// data, err := proto.Marshal(resp)
// if err != nil {
// t.Fatalf("error marshal response: %s", err)
// }
//
// compressed = snappy.Encode(nil, data)
//
// w.Header().Set("Content-Type", "application/x-protobuf")
// w.Header().Set("Content-Encoding", "snappy")
// w.WriteHeader(http.StatusOK)
//
// if _, err := w.Write(compressed); err != nil {
// t.Fatalf("snappy encode error: %s", err)
// }
// }
// })
// }
//
// func NewRemoteReadStreamServer(t *testing.T) *RemoteReadServer {
// rrs := &RemoteReadServer{
// series: make([]*prompb.TimeSeries, 0),
// }
// rrs.server = httptest.NewServer(rrs.getStreamReadHandler(t))
// return rrs
// }
//
// func (rrs *RemoteReadServer) InitMockStorage(series []*prompb.TimeSeries) {
// rrs.storage = NewMockStorage(series)
// }
//
// func (rrs *RemoteReadServer) getStreamReadHandler(t *testing.T) http.Handler {
// return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// if !validateStreamReadHeaders(t, r) {
// t.Fatalf("invalid read headers")
// }
//
// f, ok := w.(http.Flusher)
// if !ok {
// t.Fatalf("internal http.ResponseWriter does not implement http.Flusher interface")
// }
//
// stream := remote.NewChunkedWriter(w, f)
//
// data, err := io.ReadAll(r.Body)
// if err != nil {
// t.Fatalf("error read body: %s", err)
// }
//
// decodedData, err := snappy.Decode(nil, data)
// if err != nil {
// t.Fatalf("error decode compressed data:%s", err)
// }
//
// var req prompb.ReadRequest
// if err := proto.Unmarshal(decodedData, &req); err != nil {
// t.Fatalf("error unmarshal read request: %s", err)
// }
//
// var chks []prompb.Chunk
// ctx := context.Background()
// for idx, r := range req.Queries {
// startTs := r.StartTimestampMs
// endTs := r.EndTimestampMs
//
// var matchers []*labels.Matcher
// cb := func() (int64, error) { return 0, nil }
//
// c := remote.NewSampleAndChunkQueryableClient(rrs.storage, nil, matchers, true, cb)
//
// q, err := c.ChunkQuerier(startTs, endTs)
// if err != nil {
// t.Fatalf("error init chunk querier: %s", err)
// }
//
// ss := q.Select(ctx, false, nil, matchers...)
// var iter chunks.Iterator
// for ss.Next() {
// series := ss.At()
// iter = series.Iterator(iter)
// labels := remote.MergeLabels(labelsToLabelsProto(series.Labels()), nil)
//
// frameBytesLeft := maxBytesInFrame
// for _, lb := range labels {
// frameBytesLeft -= lb.Size()
// }
//
// isNext := iter.Next()
//
// for isNext {
// chunk := iter.At()
//
// if chunk.Chunk == nil {
// t.Fatalf("error found not populated chunk returned by SeriesSet at ref: %v", chunk.Ref)
// }
//
// chks = append(chks, prompb.Chunk{
// MinTimeMs: chunk.MinTime,
// MaxTimeMs: chunk.MaxTime,
// Type: prompb.Chunk_Encoding(chunk.Chunk.Encoding()),
// Data: chunk.Chunk.Bytes(),
// })
//
// frameBytesLeft -= chks[len(chks)-1].Size()
//
// // We are fine with minor inaccuracy of max bytes per frame. The inaccuracy will be max of full chunk size.
// isNext = iter.Next()
// if frameBytesLeft > 0 && isNext {
// continue
// }
//
// resp := &prompb.ChunkedReadResponse{
// ChunkedSeries: []*prompb.ChunkedSeries{
// {Labels: labels, Chunks: chks},
// },
// QueryIndex: int64(idx),
// }
//
// b, err := proto.Marshal(resp)
// if err != nil {
// t.Fatalf("error marshal response: %s", err)
// }
//
// if _, err := stream.Write(b); err != nil {
// t.Fatalf("error write to stream: %s", err)
// }
// chks = chks[:0]
// rrs.storage.Reset()
// }
// if err := iter.Err(); err != nil {
// t.Fatalf("error iterate over chunk series: %s", err)
// }
// }
// }
// })
// }
//
// func validateReadHeaders(t *testing.T, r *http.Request) bool {
// if r.Method != http.MethodPost {
// t.Fatalf("got %q method, expected %q", r.Method, http.MethodPost)
// }
// if r.Header.Get("Content-Encoding") != "snappy" {
// t.Fatalf("got %q content encoding header, expected %q", r.Header.Get("Content-Encoding"), "snappy")
// }
// if r.Header.Get("Content-Type") != "application/x-protobuf" {
// t.Fatalf("got %q content type header, expected %q", r.Header.Get("Content-Type"), "application/x-protobuf")
// }
//
// remoteReadVersion := r.Header.Get("X-Prometheus-Remote-Read-Version")
// if remoteReadVersion == "" {
// t.Fatalf("got empty prometheus remote read header")
// }
// if !strings.HasPrefix(remoteReadVersion, "0.1.") {
// t.Fatalf("wrong remote version defined")
// }
//
// return true
// }
//
// func validateStreamReadHeaders(t *testing.T, r *http.Request) bool {
// if r.Method != http.MethodPost {
// t.Fatalf("got %q method, expected %q", r.Method, http.MethodPost)
// }
// if r.Header.Get("Content-Encoding") != "snappy" {
// t.Fatalf("got %q content encoding header, expected %q", r.Header.Get("Content-Encoding"), "snappy")
// }
// if r.Header.Get("Content-Type") != "application/x-streamed-protobuf; proto=prometheus.ChunkedReadResponse" {
// t.Fatalf("got %q content type header, expected %q", r.Header.Get("Content-Type"), "application/x-streamed-protobuf; proto=prometheus.ChunkedReadResponse")
// }
//
// remoteReadVersion := r.Header.Get("X-Prometheus-Remote-Read-Version")
// if remoteReadVersion == "" {
// t.Fatalf("got empty prometheus remote read header")
// }
// if !strings.HasPrefix(remoteReadVersion, "0.1.") {
// t.Fatalf("wrong remote version defined")
// }
// return true
// }
//
// func GenerateRemoteReadSeries(start, end, numOfSeries, numOfSamples int64) []*prompb.TimeSeries {
// var ts []*prompb.TimeSeries
// j := 0
// for i := 0; i < int(numOfSeries); i++ {
// if i%3 == 0 {
// j++
// }
//
// timeSeries := prompb.TimeSeries{
// Labels: []prompb.Label{
// {Name: labels.MetricName, Value: fmt.Sprintf("vm_metric_%d", j)},
// {Name: "job", Value: strconv.Itoa(i)},
// },
// }
//
// ts = append(ts, &timeSeries)
// }
//
// for i := range ts {
// ts[i].Samples = generateRemoteReadSamples(i, start, end, numOfSamples)
// }
//
// return ts
// }
//
// func generateRemoteReadSamples(idx int, startTime, endTime, numOfSamples int64) []prompb.Sample {
// samples := make([]prompb.Sample, 0)
// delta := (endTime - startTime) / numOfSamples
//
// t := startTime
// for t != endTime {
// v := 100 * int64(idx)
// samples = append(samples, prompb.Sample{
// Timestamp: t * 1000,
// Value: float64(v),
// })
// t = t + delta
// }
//
// return samples
// }
//
// type MockStorage struct {
// query *prompb.Query
// store []*prompb.TimeSeries
// }
//
// func NewMockStorage(series []*prompb.TimeSeries) *MockStorage {
// return &MockStorage{store: series}
// }
//
// func (ms *MockStorage) Read(_ context.Context, query *prompb.Query) (*prompb.QueryResult, error) {
// if ms.query != nil {
// return nil, fmt.Errorf("expected only one call to remote client got: %v", query)
// }
// ms.query = query
//
// q := &prompb.QueryResult{Timeseries: make([]*prompb.TimeSeries, 0, len(ms.store))}
// for _, s := range ms.store {
// var samples []prompb.Sample
// for _, sample := range s.Samples {
// if sample.Timestamp >= query.StartTimestampMs && sample.Timestamp < query.EndTimestampMs {
// samples = append(samples, sample)
// }
// }
// var series prompb.TimeSeries
// if len(samples) > 0 {
// series.Labels = s.Labels
// series.Samples = samples
// }
//
// q.Timeseries = append(q.Timeseries, &series)
// }
// return q, nil
// }
//
// func (ms *MockStorage) Reset() {
// ms.query = nil
// }
//
// func labelsToLabelsProto(labels labels.Labels) []prompb.Label {
// result := make([]prompb.Label, 0, len(labels))
// for _, l := range labels {
// result = append(result, prompb.Label{
// Name: l.Name,
// Value: l.Value,
// })
// }
// return result
// }

View File

@@ -17,7 +17,7 @@ import (
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmctl/vm"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmselect/promql"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmstorage"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/prompb"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/prompbmarshal"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/storage"
)
@@ -214,15 +214,15 @@ func processFlags() {
func fillStorage(series []vm.TimeSeries) error {
var mrs []storage.MetricRow
for _, series := range series {
var labels []prompb.Label
var labels []prompbmarshal.Label
for _, lp := range series.LabelPairs {
labels = append(labels, prompb.Label{
labels = append(labels, prompbmarshal.Label{
Name: lp.Name,
Value: lp.Value,
})
}
if series.Name != "" {
labels = append(labels, prompb.Label{
labels = append(labels, prompbmarshal.Label{
Name: "__name__",
Value: series.Name,
})

View File

@@ -8,7 +8,7 @@ import (
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmstorage"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/bytesutil"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/httpserver"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/prompb"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/prompbmarshal"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/slicesutil"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/storage"
)
@@ -30,7 +30,7 @@ type InsertCtx struct {
func (ctx *InsertCtx) Reset(rowsLen int) {
labels := ctx.Labels
for i := range labels {
labels[i] = prompb.Label{}
labels[i] = prompbmarshal.Label{}
}
ctx.Labels = labels[:0]
@@ -51,7 +51,7 @@ func cleanMetricRow(mr *storage.MetricRow) {
mr.MetricNameRaw = nil
}
func (ctx *InsertCtx) marshalMetricNameRaw(prefix []byte, labels []prompb.Label) []byte {
func (ctx *InsertCtx) marshalMetricNameRaw(prefix []byte, labels []prompbmarshal.Label) []byte {
start := len(ctx.metricNamesBuf)
ctx.metricNamesBuf = append(ctx.metricNamesBuf, prefix...)
ctx.metricNamesBuf = storage.MarshalMetricNameRaw(ctx.metricNamesBuf, labels)
@@ -60,7 +60,7 @@ func (ctx *InsertCtx) marshalMetricNameRaw(prefix []byte, labels []prompb.Label)
}
// WriteDataPoint writes (timestamp, value) with the given prefix and labels into ctx buffer.
func (ctx *InsertCtx) WriteDataPoint(prefix []byte, labels []prompb.Label, timestamp int64, value float64) error {
func (ctx *InsertCtx) WriteDataPoint(prefix []byte, labels []prompbmarshal.Label, timestamp int64, value float64) error {
metricNameRaw := ctx.marshalMetricNameRaw(prefix, labels)
return ctx.addRow(metricNameRaw, timestamp, value)
}
@@ -68,7 +68,7 @@ func (ctx *InsertCtx) WriteDataPoint(prefix []byte, labels []prompb.Label, times
// WriteDataPointExt writes (timestamp, value) with the given metricNameRaw and labels into ctx buffer.
//
// It returns metricNameRaw for the given labels if len(metricNameRaw) == 0.
func (ctx *InsertCtx) WriteDataPointExt(metricNameRaw []byte, labels []prompb.Label, timestamp int64, value float64) ([]byte, error) {
func (ctx *InsertCtx) WriteDataPointExt(metricNameRaw []byte, labels []prompbmarshal.Label, timestamp int64, value float64) ([]byte, error) {
if len(metricNameRaw) == 0 {
metricNameRaw = ctx.marshalMetricNameRaw(nil, labels)
}
@@ -106,7 +106,7 @@ func (ctx *InsertCtx) AddLabelBytes(name, value []byte) {
// Do not skip labels with empty name, since they are equal to __name__.
return
}
ctx.Labels = append(ctx.Labels, prompb.Label{
ctx.Labels = append(ctx.Labels, prompbmarshal.Label{
// Do not copy name and value contents for performance reasons.
// This reduces GC overhead on the number of objects and allocations.
Name: bytesutil.ToUnsafeString(name),
@@ -124,7 +124,7 @@ func (ctx *InsertCtx) AddLabel(name, value string) {
// Do not skip labels with empty name, since they are equal to __name__.
return
}
ctx.Labels = append(ctx.Labels, prompb.Label{
ctx.Labels = append(ctx.Labels, prompbmarshal.Label{
// Do not copy name and value contents for performance reasons.
// This reduces GC overhead on the number of objects and allocations.
Name: name,

View File

@@ -4,7 +4,7 @@ import (
"flag"
"sort"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/prompb"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/prompbmarshal"
)
var sortLabels = flag.Bool("sortLabels", false, `Whether to sort labels for incoming samples before writing them to storage. `+
@@ -19,7 +19,7 @@ func (ctx *InsertCtx) SortLabelsIfNeeded() {
}
}
type sortedLabels []prompb.Label
type sortedLabels []prompbmarshal.Label
func (sl *sortedLabels) Len() int { return len(*sl) }
func (sl *sortedLabels) Less(i, j int) bool {

View File

@@ -9,7 +9,6 @@ import (
"github.com/VictoriaMetrics/VictoriaMetrics/app/vminsert/common"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vminsert/relabel"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/bytesutil"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/prompb"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/prompbmarshal"
parserCommon "github.com/VictoriaMetrics/VictoriaMetrics/lib/protoparser/common"
parser "github.com/VictoriaMetrics/VictoriaMetrics/lib/protoparser/influx"
@@ -150,7 +149,7 @@ type pushCtx struct {
Common common.InsertCtx
metricNameBuf []byte
metricGroupBuf []byte
originLabels []prompb.Label
originLabels []prompbmarshal.Label
}
func (ctx *pushCtx) reset() {
@@ -160,7 +159,7 @@ func (ctx *pushCtx) reset() {
originLabels := ctx.originLabels
for i := range originLabels {
originLabels[i] = prompb.Label{}
originLabels[i] = prompbmarshal.Label{}
}
ctx.originLabels = originLabels[:0]
}

View File

@@ -8,7 +8,6 @@ import (
"github.com/VictoriaMetrics/VictoriaMetrics/lib/fasttime"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/procutil"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/prompb"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/prompbmarshal"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promrelabel"
"github.com/VictoriaMetrics/metrics"
@@ -108,7 +107,7 @@ func (ctx *Ctx) Reset() {
// ApplyRelabeling applies relabeling to the given labels and returns the result.
//
// The returned labels are valid until the next call to ApplyRelabeling.
func (ctx *Ctx) ApplyRelabeling(labels []prompb.Label) []prompb.Label {
func (ctx *Ctx) ApplyRelabeling(labels []prompbmarshal.Label) []prompbmarshal.Label {
pcs := pcsGlobal.Load()
if pcs.Len() == 0 && !*usePromCompatibleNaming {
// There are no relabeling rules.
@@ -159,7 +158,7 @@ func (ctx *Ctx) ApplyRelabeling(labels []prompb.Label) []prompb.Label {
name = ""
}
value := label.Value
dst = append(dst, prompb.Label{
dst = append(dst, prompbmarshal.Label{
Name: name,
Value: value,
})

View File

@@ -14,7 +14,7 @@ import (
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmstorage"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/bufferedwriter"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/httputils"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/prompb"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/prompbmarshal"
graphiteparser "github.com/VictoriaMetrics/VictoriaMetrics/lib/protoparser/graphite"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/storage"
"github.com/VictoriaMetrics/metrics"
@@ -95,7 +95,7 @@ func registerMetrics(startTime time.Time, w http.ResponseWriter, r *http.Request
_ = deadline // TODO: use the deadline as in the cluster branch
paths := r.Form["path"]
var row graphiteparser.Row
var labels []prompb.Label
var labels []prompbmarshal.Label
var b []byte
var tagsPool []graphiteparser.Tag
mrs := make([]storage.MetricRow, len(paths))
@@ -122,12 +122,12 @@ func registerMetrics(startTime time.Time, w http.ResponseWriter, r *http.Request
canonicalPaths[i] = string(b)
// Convert parsed metric and tags to labels.
labels = append(labels[:0], prompb.Label{
labels = append(labels[:0], prompbmarshal.Label{
Name: "__name__",
Value: row.Metric,
})
for _, tag := range row.Tags {
labels = append(labels, prompb.Label{
labels = append(labels, prompbmarshal.Label{
Name: tag.Key,
Value: tag.Value,
})

View File

@@ -9398,6 +9398,18 @@ func TestExecSuccess(t *testing.T) {
resultExpected := []netstorage.Result{r1, r2, r3, r4}
f(q, resultExpected)
})
t.Run("nan^any", func(t *testing.T) {
t.Parallel()
q := `(hour(time()*1e4) == 4)^1`
r := netstorage.Result{
MetricName: metricNameExpected,
Values: []float64{nan, nan, nan, 4, nan, nan},
Timestamps: timestampsExpected,
}
resultExpected := []netstorage.Result{r}
f(q, resultExpected)
})
}
func TestExecError(t *testing.T) {

View File

@@ -37,7 +37,7 @@ type panelSettings struct {
Unit string `json:"unit,omitempty"`
Expr []string `json:"expr"`
Alias []string `json:"alias,omitempty"`
ShowLegend bool `json:"showLegend,omitempty"`
ShowLegend *bool `json:"showLegend"`
Width int `json:"width,omitempty"`
}
@@ -107,6 +107,17 @@ func collectDashboardsSettings(path string) ([]byte, error) {
if err != nil {
return nil, fmt.Errorf("cannot parse file %s: %w", filePath, err)
}
for i := range ds.Rows {
for j := range ds.Rows[i].Panels {
// Set default value for ShowLegend = true if it is not specified
if ds.Rows[i].Panels[j].ShowLegend == nil {
defaultValue := true
ds.Rows[i].Panels[j].ShowLegend = &defaultValue
}
}
}
if len(ds.Rows) > 0 {
dss = append(dss, ds)
}

View File

@@ -1,13 +1,13 @@
{
"files": {
"main.css": "./static/css/main.d781989c.css",
"main.js": "./static/js/main.7ec4e6eb.js",
"main.css": "./static/css/main.b1929c64.css",
"main.js": "./static/js/main.a7d57628.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.d781989c.css",
"static/js/main.7ec4e6eb.js"
"static/css/main.b1929c64.css",
"static/js/main.a7d57628.js"
]
}

View File

@@ -1 +1 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="./favicon.svg"/><link rel="apple-touch-icon" href="./favicon.svg"/><link rel="mask-icon" href="./favicon.svg" color="#000000"><meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=5"/><meta name="theme-color" content="#000000"/><meta name="description" content="Explore and troubleshoot your VictoriaMetrics data"/><link rel="manifest" href="./manifest.json"/><title>vmui</title><script src="./dashboards/index.js" type="module"></script><meta name="twitter:card" content="summary"><meta name="twitter:title" content="UI for VictoriaMetrics"><meta name="twitter:site" content="@https://victoriametrics.com/"><meta name="twitter:description" content="Explore and troubleshoot your VictoriaMetrics data"><meta name="twitter:image" content="./preview.jpg"><meta property="og:type" content="website"><meta property="og:title" content="UI for VictoriaMetrics"><meta property="og:url" content="https://victoriametrics.com/"><meta property="og:description" content="Explore and troubleshoot your VictoriaMetrics data"><script defer="defer" src="./static/js/main.7ec4e6eb.js"></script><link href="./static/css/main.d781989c.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="./favicon.svg"/><link rel="apple-touch-icon" href="./favicon.svg"/><link rel="mask-icon" href="./favicon.svg" color="#000000"><meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=5"/><meta name="theme-color" content="#000000"/><meta name="description" content="Explore and troubleshoot your VictoriaMetrics data"/><link rel="manifest" href="./manifest.json"/><title>vmui</title><script src="./dashboards/index.js" type="module"></script><meta name="twitter:card" content="summary"><meta name="twitter:title" content="UI for VictoriaMetrics"><meta name="twitter:site" content="@https://victoriametrics.com/"><meta name="twitter:description" content="Explore and troubleshoot your VictoriaMetrics data"><meta name="twitter:image" content="./preview.jpg"><meta property="og:type" content="website"><meta property="og:title" content="UI for VictoriaMetrics"><meta property="og:url" content="https://victoriametrics.com/"><meta property="og:description" content="Explore and troubleshoot your VictoriaMetrics data"><script defer="defer" src="./static/js/main.a7d57628.js"></script><link href="./static/css/main.b1929c64.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -127,15 +127,27 @@ DashboardRow:
<br/>
PanelSettings:
| Name | Type | Description |
|:------------|:----------:|--------------------------------------------------------------------------------------:|
| expr* | `string[]` | Data source queries |
| alias | `string[]` | Expression alias. Matched by index in array |
| title | `string` | Panel title |
| description | `string` | Additional information about the panel |
| unit | `string` | Y-axis unit |
| showLegend | `boolean` | If `false`, the legend hide. Default value - `true` |
| width | `number` | The number of columns the panel uses.<br/> From 1 (minimum width) to 12 (full width). |
| Name | Type | Description |
|:------------|:----------:|---------------------------------------------------------------------------------------------------------------:|
| expr* | `string[]` | Data source queries |
| alias | `string[]` | An array of aliases for each expression in `expr`. See [Template Support in alias](#template-support-in-alias) |
| title | `string` | Panel title |
| description | `string` | Additional information about the panel |
| unit | `string` | Y-axis unit |
| showLegend | `boolean` | If `false`, the legend hide. Default value - `true` |
| width | `number` | The number of columns the panel uses.<br/> From 1 (minimum width) to 12 (full width). |
### Template Support in `alias`
To create more readable metric names in the legend, you can use constructions like `{{label_name}}`, where `label_name`
is the label's name.
If the label exists in the metric, its value will be substituted in the template.
If the label is missing, the legend will use the default name.
**Example:**
Metric: `metric{foo="bar",baz="qux"}`
Alias: `{{foo}} - {{baz}}`
Legend: `bar - qux`
### Example json

View File

@@ -20,6 +20,7 @@ module.exports = {
"@typescript-eslint"
],
"rules": {
"@typescript-eslint/no-unused-vars": ["warn", { "varsIgnorePattern": "^_" }],
"react/jsx-closing-bracket-location": [1, "line-aligned"],
"react/jsx-max-props-per-line":[1, { "maximum": 1 }],
"react/jsx-first-prop-new-line": [1, "multiline"],

View File

@@ -17,6 +17,7 @@ import ActiveQueries from "./pages/ActiveQueries";
import QueryAnalyzer from "./pages/QueryAnalyzer";
import DownsamplingFilters from "./pages/DownsamplingFilters";
import RetentionFilters from "./pages/RetentionFilters";
import RawQueryPage from "./pages/RawQueryPage";
const App: FC = () => {
const [loadedTheme, setLoadedTheme] = useState(false);
@@ -36,6 +37,10 @@ const App: FC = () => {
path={router.home}
element={<CustomPanel/>}
/>
<Route
path={router.rawQuery}
element={<RawQueryPage/>}
/>
<Route
path={router.metrics}
element={<ExploreMetrics/>}

View File

@@ -5,3 +5,13 @@ export const getQueryRangeUrl = (server: string, query: string, period: TimePara
export const getQueryUrl = (server: string, query: string, period: TimeParams, nocache: boolean, queryTracing: boolean): string =>
`${server}/api/v1/query?query=${encodeURIComponent(query)}&time=${period.end}&step=${period.step}${nocache ? "&nocache=1" : ""}${queryTracing ? "&trace=1" : ""}`;
export const getExportDataUrl = (server: string, query: string, period: TimeParams, reduceMemUsage: boolean): string => {
const params = new URLSearchParams({
"match[]": query,
start: period.start.toString(),
end: period.end.toString(),
});
if (reduceMemUsage) params.set("reduce_mem_usage", "1");
return `${server}/api/v1/export?${params}`;
};

View File

@@ -15,6 +15,11 @@ export interface InstantMetricResult extends MetricBase {
values?: [number, string][]
}
export interface ExportMetricResult extends MetricBase {
values: number[];
timestamps: number[];
}
export interface TracingData {
message: string;
duration_msec: number;

View File

@@ -56,19 +56,23 @@ const LegendItem: FC<LegendItemProps> = ({ legend, onChange, isHeatmap, isAnomal
)}
<div className="vm-legend-item-info">
<span className="vm-legend-item-info__label">
{legend.freeFormFields["__name__"]}
{!!freeFormFields.length && <>&#123;</>}
{freeFormFields.map((f, i) => (
<span
className="vm-legend-item-info__free-fields"
key={f.key}
onClick={createHandlerCopy(f.freeField)}
title="copy to clipboard"
>
{f.freeField}{i + 1 < freeFormFields.length && ","}
</span>
))}
{!!freeFormFields.length && <>&#125;</>}
{legend.hasAlias ? legend.label : (
<>
{legend.freeFormFields["__name__"]}
{!!freeFormFields.length && <>&#123;</>}
{freeFormFields.map((f, i) => (
<span
className="vm-legend-item-info__free-fields"
key={f.key}
onClick={createHandlerCopy(f.freeField)}
title="copy to clipboard"
>
{f.freeField}{i + 1 < freeFormFields.length && ","}
</span>
))}
{!!freeFormFields.length && <>&#125;</>}
</>
)}
</span>
</div>
{!isHeatmap && showStats && (

View File

@@ -20,13 +20,17 @@ const AdditionalSettingsControls: FC<Props & {isMobile?: boolean}> = ({ isMobile
const { autocomplete } = useQueryState();
const queryDispatch = useQueryDispatch();
const { nocache, isTracingEnabled } = useCustomPanelState();
const { nocache, isTracingEnabled, reduceMemUsage } = useCustomPanelState();
const customPanelDispatch = useCustomPanelDispatch();
const onChangeCache = () => {
customPanelDispatch({ type: "TOGGLE_NO_CACHE" });
};
const onChangeReduceMemUsage = () => {
customPanelDispatch({ type: "TOGGLE_REDUCE_MEM_USAGE" });
};
const onChangeQueryTracing = () => {
customPanelDispatch({ type: "TOGGLE_QUERY_TRACING" });
};
@@ -67,12 +71,22 @@ const AdditionalSettingsControls: FC<Props & {isMobile?: boolean}> = ({ isMobile
/>
</Tooltip>
)}
<Switch
label={"Disable cache"}
value={nocache}
onChange={onChangeCache}
fullWidth={isMobile}
/>
{!hideButtons?.disableCache && (
<Switch
label={"Disable cache"}
value={nocache}
onChange={onChangeCache}
fullWidth={isMobile}
/>
)}
{!hideButtons?.reduceMemUsage && (
<Switch
label={"Disable deduplication"}
value={reduceMemUsage}
onChange={onChangeReduceMemUsage}
fullWidth={isMobile}
/>
)}
{!hideButtons?.traceQuery && (
<Switch
label={"Trace query"}

View File

@@ -23,6 +23,7 @@ export interface QueryEditorProps {
stats?: QueryStats;
label: string;
disabled?: boolean
includeFunctions?: boolean;
}
const QueryEditor: FC<QueryEditorProps> = ({
@@ -35,7 +36,8 @@ const QueryEditor: FC<QueryEditorProps> = ({
error,
stats,
label,
disabled = false
disabled = false,
includeFunctions = true
}) => {
const { autocompleteQuick } = useQueryState();
const { isMobile } = useDeviceDetect();
@@ -143,6 +145,7 @@ const QueryEditor: FC<QueryEditorProps> = ({
anchorEl={autocompleteAnchorEl}
caretPosition={caretPosition}
hasHelperText={Boolean(warning || error)}
includeFunctions={includeFunctions}
onSelect={handleSelect}
onFoundOptions={handleChangeFoundOptions}
/>

View File

@@ -11,6 +11,7 @@ interface QueryEditorAutocompleteProps {
anchorEl: React.RefObject<HTMLElement>;
caretPosition: [number, number]; // [start, end]
hasHelperText: boolean;
includeFunctions: boolean;
onSelect: (val: string, caretPosition: number) => void;
onFoundOptions: (val: AutocompleteOptions[]) => void;
}
@@ -20,11 +21,12 @@ const QueryEditorAutocomplete: FC<QueryEditorAutocompleteProps> = ({
anchorEl,
caretPosition,
hasHelperText,
includeFunctions,
onSelect,
onFoundOptions
}) => {
const [offsetPos, setOffsetPos] = useState({ top: 0, left: 0 });
const metricsqlFunctions = useGetMetricsQL();
const metricsqlFunctions = useGetMetricsQL(includeFunctions);
const values = useMemo(() => {
if (caretPosition[0] !== caretPosition[1]) return { beforeCursor: value, afterCursor: "" };

View File

@@ -0,0 +1,105 @@
import React from "react";
import { ArrowDownIcon } from "../Icons";
import { useMemo } from "preact/compat";
import classNames from "classnames";
import "./style.scss";
interface PaginationProps {
currentPage: number;
totalItems: number;
itemsPerPage: number;
onPageChange: (page: number) => void;
maxVisiblePages?: number;
}
const Pagination: React.FC<PaginationProps> = ({
currentPage,
totalItems,
itemsPerPage,
onPageChange,
maxVisiblePages = 10
}) => {
const totalPages = Math.ceil(totalItems / itemsPerPage);
const handlePageChange = (page: number) => {
if (page < 1 || page > totalPages) return;
onPageChange(page);
};
const pages = useMemo(() => {
const pages = [];
if (totalPages <= maxVisiblePages) {
for (let i = 1; i <= totalPages; i++) {
pages.push(i);
}
} else {
const startPage = Math.max(1, currentPage - Math.floor(maxVisiblePages / 2));
const endPage = Math.min(totalPages, startPage + maxVisiblePages - 1);
if (startPage > 1) {
pages.push(1);
if (startPage > 2) {
pages.push("...");
}
}
for (let i = startPage; i <= endPage; i++) {
pages.push(i);
}
if (endPage < totalPages) {
if (endPage < totalPages - 1) {
pages.push("...");
}
pages.push(totalPages);
}
}
return pages;
}, [totalPages, currentPage, maxVisiblePages]);
const handleClickNav = (stepPage: number) => () => {
handlePageChange(currentPage + stepPage);
};
const handleClickPage = (page: number | string) => () => {
if (typeof page === "number") {
handlePageChange(page);
}
};
if (pages.length <= 1) return null;
return (
<div className="vm-pagination">
<button
className="vm-pagination__page vm-pagination__arrow vm-pagination__arrow_prev"
onClick={handleClickNav(-1)}
disabled={currentPage === 1}
>
<ArrowDownIcon/>
</button>
{pages.map((page, index) => (
<button
key={index}
onClick={handleClickPage(page)}
className={classNames({
"vm-pagination__page": true,
"vm-pagination__page_active": currentPage === page,
"vm-pagination__page_disabled": page === "..."
})}
disabled={page === "..."}
>
{page}
</button>
))}
<button
className="vm-pagination__page vm-pagination__arrow vm-pagination__arrow_next"
onClick={handleClickNav(1)}
disabled={currentPage === totalPages}
>
<ArrowDownIcon/>
</button>
</div>
);
};
export default Pagination;

View File

@@ -1,52 +0,0 @@
import React, { FC } from "preact/compat";
import Button from "../../Button/Button";
import { ArrowDownIcon } from "../../Icons";
import "./style.scss";
import useDeviceDetect from "../../../../hooks/useDeviceDetect";
import classNames from "classnames";
interface PaginationControlProps {
page: number;
length: number;
limit: number;
onChange: (page: number) => void;
}
const PaginationControl: FC<PaginationControlProps> = ({ page, length, limit, onChange }) => {
const { isMobile } = useDeviceDetect();
const handleChangePage = (step: number) => () => {
onChange(+page + step);
window.scrollTo(0, 0);
};
return (
<div
className={classNames({
"vm-pagination": true,
"vm-pagination_mobile": isMobile
})}
>
{page > 1 && (
<Button
variant={"text"}
onClick={handleChangePage(-1)}
startIcon={<div className="vm-pagination__icon vm-pagination__icon_prev"><ArrowDownIcon/></div>}
>
Previous
</Button>
)}
{length >= limit && (
<Button
variant={"text"}
onClick={handleChangePage(1)}
endIcon={<div className="vm-pagination__icon vm-pagination__icon_next"><ArrowDownIcon/></div>}
>
Next
</Button>
)}
</div>
);
};
export default PaginationControl;

View File

@@ -1,24 +0,0 @@
@use "src/styles/variables" as *;
.vm-pagination {
position: sticky;
right: 0;
display: flex;
justify-content: flex-end;
gap: $padding-small;
padding: $padding-global 0 0;
&_mobile {
padding: $padding-global 0;
}
&__icon {
&_prev {
transform: rotate(90deg);
}
&_next {
transform: rotate(-90deg);
}
}
}

View File

@@ -0,0 +1,66 @@
@use "../../../styles/variables" as *;
.vm-pagination {
position: sticky;
left: 0;
display: flex;
justify-content: center;
gap: $padding-small;
padding: $padding-global 0;
font-size: $font-size;
&_mobile {
padding: $padding-global 0;
}
&__page {
display: flex;
align-items: center;
justify-content: center;
height: 30px;
min-width: 30px;
color: $color-text;
padding: 0 $padding-small;
border-radius: $border-radius-small;
transition: background-color 0.3s;
border: 1px solid transparent;
cursor: pointer;
&_active {
background-color: $color-primary;
color: $color-primary-text;
}
&_disabled {
cursor: default;
pointer-events: none;
color: $color-text-disabled;
}
&:hover {
background-color: $color-hover-black;
}
}
&__arrow {
svg {
max-width: $font-size;
max-height: $font-size;
}
&:disabled {
color: $color-text-disabled;
cursor: default;
pointer-events: none;
}
&_prev {
transform: rotate(90deg);
}
&_next {
transform: rotate(-90deg);
}
}
}

View File

@@ -32,8 +32,7 @@ const Table = <T extends object>({ rows, columns, defaultOrderBy, defaultOrderDi
const sortedList = useMemo(() => {
const { startIndex, endIndex } = paginationOffset;
return stableSort(rows as [], getComparator(orderDir, orderBy)).slice(startIndex, endIndex);
},
[rows, orderBy, orderDir, paginationOffset]);
}, [rows, orderBy, orderDir, paginationOffset]);
const createSortHandler = (key: keyof T) => () => {
setOrderDir((prev) => prev === "asc" && orderBy === key ? "desc" : "asc");

View File

@@ -180,7 +180,7 @@ const GraphView: FC<GraphViewProps> = ({
if (isAnomalyView) {
setHideSeries(legend.map(s => s.label || "").slice(1));
}
}, [data, timezone, isHistogram]);
}, [data, timezone, isHistogram, currentStep]);
useEffect(() => {
const tempLegend: LegendItemType[] = [];

View File

@@ -12,17 +12,7 @@ export interface JsonViewProps {
const JsonView: FC<JsonViewProps> = ({ data }) => {
const copyToClipboard = useCopyToClipboard();
const formattedJson = useMemo(() => {
const space = " ";
const values = data.map(item => {
if (Object.keys(item).length === 1) {
return JSON.stringify(item);
} else {
return JSON.stringify(item, null, space.length);
}
}).join(",\n").replace(/^/gm, `${space}`);
return `[\n${values}\n]`;
}, [data]);
const formattedJson = useMemo(() => JSON.stringify(data, null, 2), [data]);
const handlerCopy = async () => {
await copyToClipboard(formattedJson, "Formatted JSON has been copied");

View File

@@ -64,7 +64,7 @@ const useLineTooltip = ({ u, metrics, series, unit, isAnomalyView }: LineTooltip
title: groups.size > 1 && !isAnomalyView ? `Query ${group}` : "",
dates: [date ? dayjs(date * 1000).tz().format(DATE_FULL_TIMEZONE_FORMAT) : "-"],
value: formatPrettyNumber(value, min, max),
info: getMetricName(metricItem),
info: getMetricName(metricItem, seriesItem),
statsFormatted: seriesItem?.statsFormatted,
marker: `${seriesItem?.stroke}`,
};

View File

@@ -48,7 +48,7 @@ const processGroups = (groups: NodeListOf<Element>): AutocompleteOptions[] => {
}).filter(Boolean) as AutocompleteOptions[];
};
const useGetMetricsQL = () => {
const useGetMetricsQL = (includeFunctions: boolean) => {
const { metricsQLFunctions } = useQueryState();
const queryDispatch = useQueryDispatch();
@@ -60,6 +60,7 @@ const useGetMetricsQL = () => {
};
useEffect(() => {
if (!includeFunctions || metricsQLFunctions.length) return;
const fetchMarkdown = async () => {
try {
const resp = await fetch(MetricsQL);
@@ -70,12 +71,10 @@ const useGetMetricsQL = () => {
console.error("Error fetching or processing the MetricsQL.md file:", e);
}
};
if (metricsQLFunctions.length) return;
fetchMarkdown();
}, []);
return metricsQLFunctions;
return includeFunctions ? metricsQLFunctions : [];
};
export default useGetMetricsQL;

View File

@@ -17,7 +17,11 @@ export const displayTypeTabs: DisplayTab[] = [
{ value: DisplayType.table, icon: <TableIcon/>, label: "Table", prometheusCode: 1 }
];
export const DisplayTypeSwitch: FC = () => {
interface Props {
tabFilter?: (tab: DisplayTab) => boolean
}
export const DisplayTypeSwitch: FC<Props> = ({ tabFilter }) => {
const { displayType } = useCustomPanelState();
const dispatch = useCustomPanelDispatch();
@@ -26,10 +30,12 @@ export const DisplayTypeSwitch: FC = () => {
dispatch({ type: "SET_DISPLAY_TYPE", payload: newValue as DisplayType ?? displayType });
};
const items = displayTypeTabs.filter(tabFilter ?? (() => true));
return (
<Tabs
activeItem={displayType}
items={displayTypeTabs}
items={items}
onChange={handleChange}
/>
);

View File

@@ -15,6 +15,8 @@ import Alert from "../../../components/Main/Alert/Alert";
import qs from "qs";
import Popper from "../../../components/Main/Popper/Popper";
import helperText from "./helperText";
import { Link } from "react-router-dom";
import router from "../../../router";
type Props = {
fetchUrl?: string[];
@@ -125,6 +127,15 @@ const DownloadReport: FC<Props> = ({ fetchUrl }) => {
setStepHelper(0);
}, [openHelper]);
const RawQueryLink = () => (
<Link
className="vm-link vm-link_underlined vm-link_colored"
to={router.rawQuery}
>
Raw Query
</Link>
);
return (
<>
<Tooltip title={"Export query"}>
@@ -165,6 +176,10 @@ const DownloadReport: FC<Props> = ({ fetchUrl }) => {
label={"Include query trace"}
/>
</div>
<Alert variant="info">
If confused with the query results,
try viewing the raw samples for selected series in <RawQueryLink/> tab.
</Alert>
</div>
{error && <Alert variant="error">{error}</Alert>}
<div className="vm-download-report__buttons">

View File

@@ -4,14 +4,15 @@
display: grid;
gap: $padding-large;
padding-top: calc($padding-large - $padding-global);
min-width: 400px;
width: 700px;
max-width: 100%;
&-settings {
display: grid;
gap: $padding-global;
textarea {
min-height: 100px;
min-height: 200px;
}
}

View File

@@ -31,7 +31,9 @@ export interface QueryConfiguratorProps {
setQueryErrors: Dispatch<SetStateAction<string[]>>;
setHideError: Dispatch<SetStateAction<boolean>>;
stats: QueryStats[];
label?: string;
isLoading?: boolean;
includeFunctions?: boolean;
onHideQuery?: (queries: number[]) => void
onRunQuery: () => void;
abortFetch?: () => void;
@@ -41,6 +43,8 @@ export interface QueryConfiguratorProps {
autocomplete?: boolean;
traceQuery?: boolean;
anomalyConfig?: boolean;
disableCache?: boolean;
reduceMemUsage?: boolean;
}
}
@@ -49,7 +53,9 @@ const QueryConfigurator: FC<QueryConfiguratorProps> = ({
setQueryErrors,
setHideError,
stats,
label,
isLoading,
includeFunctions = true,
onHideQuery,
onRunQuery,
abortFetch,
@@ -216,8 +222,9 @@ const QueryConfigurator: FC<QueryConfiguratorProps> = ({
onArrowDown={createHandlerArrow(1, i)}
onEnter={handleRunQuery}
onChange={createHandlerChangeQuery(i)}
label={`Query ${stateQuery.length > 1 ? i + 1 : ""}`}
label={`${label || "Query"} ${stateQuery.length > 1 ? i + 1 : ""}`}
disabled={hideQuery.includes(i)}
includeFunctions={includeFunctions}
/>
{onHideQuery && (
<Tooltip title={hideQuery.includes(i) ? "Enable query" : "Disable query"}>

View File

@@ -72,6 +72,16 @@ export const useSetQueryParams = () => {
newSearchParams.set(`${group}.tenantID`, tenantId);
}
});
// Remove extra parameters that exceed the request size
const maxIndex = query.length - 1;
Array.from(newSearchParams.keys()).forEach(key => {
const match = key.match(/^g(\d+)\./);
if (match && parseInt(match[1], 10) > maxIndex) {
newSearchParams.delete(key);
}
});
if (isEqualURLSearchParams(newSearchParams, searchParams) || !newSearchParams.size) return;
setSearchParams(newSearchParams);
}, [tenantId, displayType, query, duration, relativeTime, date, step, customStep]);

View File

@@ -85,6 +85,7 @@ const CustomPanel: FC = () => {
onHideQuery={handleHideQuery}
onRunQuery={handleRunQuery}
abortFetch={abortFetch}
hideButtons={{ reduceMemUsage: true }}
/>
<CustomPanelTraces
traces={traces}

View File

@@ -87,7 +87,14 @@ const ExploreAnomaly: FC = () => {
setHideError={setHideError}
stats={queryStats}
onRunQuery={handleRunQuery}
hideButtons={{ addQuery: true, prettify: false, autocomplete: false, traceQuery: true, anomalyConfig: true }}
hideButtons={{
addQuery: true,
prettify: false,
autocomplete: false,
traceQuery: true,
anomalyConfig: true,
reduceMemUsage: true,
}}
/>
{isLoading && <Spinner/>}
{(!hideError && error) && <Alert variant="error">{error}</Alert>}

View File

@@ -54,7 +54,7 @@ const ExploreLogs: FC = () => {
fetchLogs(newPeriod).then((isSuccess) => {
isSuccess && !hideChart && fetchLogHits(newPeriod);
}).catch(e => e);
setSearchParamsFromKeys( {
setSearchParamsFromKeys({
query,
"g0.range_input": duration,
"g0.end_input": newPeriod.date,

View File

@@ -1,22 +1,23 @@
import React, { FC, useState, useMemo, useRef } from "preact/compat";
import JsonView from "../../../components/Views/JsonView/JsonView";
import { CodeIcon, ListIcon, TableIcon } from "../../../components/Main/Icons";
import Tabs from "../../../components/Main/Tabs/Tabs";
import "./style.scss";
import classNames from "classnames";
import useDeviceDetect from "../../../hooks/useDeviceDetect";
import { Logs } from "../../../api/types";
import dayjs from "dayjs";
import { useTimeState } from "../../../state/time/TimeStateContext";
import useStateSearchParams from "../../../hooks/useStateSearchParams";
import useSearchParamsFromObject from "../../../hooks/useSearchParamsFromObject";
import TableSettings from "../../../components/Table/TableSettings/TableSettings";
import useBoolean from "../../../hooks/useBoolean";
import TableLogs from "./TableLogs";
import GroupLogs from "../GroupLogs/GroupLogs";
import { DATE_TIME_FORMAT } from "../../../constants/date";
import { marked } from "marked";
import JsonView from "../../../components/Views/JsonView/JsonView";
import LineLoader from "../../../components/Main/LineLoader/LineLoader";
import SelectLimit from "../../../components/Main/Pagination/SelectLimit/SelectLimit";
const MemoizedTableLogs = React.memo(TableLogs);
const MemoizedGroupLogs = React.memo(GroupLogs);
const MemoizedJsonView = React.memo(JsonView);
export interface ExploreLogBodyProps {
data: Logs[];
@@ -37,38 +38,35 @@ const tabs = [
const ExploreLogsBody: FC<ExploreLogBodyProps> = ({ data, isLoading }) => {
const { isMobile } = useDeviceDetect();
const { timezone } = useTimeState();
const { setSearchParamsFromKeys } = useSearchParamsFromObject();
const groupSettingsRef = useRef<HTMLDivElement>(null);
const [activeTab, setActiveTab] = useStateSearchParams(DisplayType.group, "view");
const [displayColumns, setDisplayColumns] = useState<string[]>([]);
const [rowsPerPage, setRowsPerPage] = useStateSearchParams(1000, "rows_per_page");
const { value: tableCompact, toggle: toggleTableCompact } = useBoolean(false);
const logs = useMemo(() => data.map((item) => ({
...item,
_vmui_time: item._time ? dayjs(item._time).tz().format(`${DATE_TIME_FORMAT}.SSS`) : "",
_vmui_data: JSON.stringify(item, null, 2),
_vmui_markdown: item._msg ? marked(item._msg.replace(/```/g, "\n```\n")) as string : ""
})) as Logs[], [data, timezone]);
const columns = useMemo(() => {
if (!logs?.length) return [];
const hideColumns = ["_vmui_data", "_vmui_time", "_vmui_markdown"];
if (!data?.length) return [];
const keys = new Set<string>();
for (const item of logs) {
for (const item of data) {
for (const key in item) {
keys.add(key);
}
}
return Array.from(keys).filter((col) => !hideColumns.includes(col));
}, [logs]);
return Array.from(keys);
}, [data]);
const handleChangeTab = (view: string) => {
setActiveTab(view as DisplayType);
setSearchParamsFromKeys({ view });
};
const handleSetRowsPerPage = (limit: number) => {
setRowsPerPage(limit);
setSearchParamsFromKeys({ rows_per_page: limit });
};
return (
<div
className={classNames({
@@ -97,6 +95,10 @@ const ExploreLogsBody: FC<ExploreLogBodyProps> = ({ data, isLoading }) => {
</div>
{activeTab === DisplayType.table && (
<div className="vm-explore-logs-body-header__settings">
<SelectLimit
limit={rowsPerPage}
onChange={handleSetRowsPerPage}
/>
<TableSettings
columns={columns}
selectedColumns={displayColumns}
@@ -124,22 +126,22 @@ const ExploreLogsBody: FC<ExploreLogBodyProps> = ({ data, isLoading }) => {
{!!data.length && (
<>
{activeTab === DisplayType.table && (
<TableLogs
logs={logs}
<MemoizedTableLogs
logs={data}
displayColumns={displayColumns}
tableCompact={tableCompact}
columns={columns}
rowsPerPage={Number(rowsPerPage)}
/>
)}
{activeTab === DisplayType.group && (
<GroupLogs
logs={logs}
columns={columns}
<MemoizedGroupLogs
logs={data}
settingsRef={groupSettingsRef}
/>
)}
{activeTab === DisplayType.json && (
<JsonView data={data}/>
<MemoizedJsonView data={data}/>
)}
</>
)}

View File

@@ -1,58 +1,94 @@
import React, { FC, useMemo } from "preact/compat";
import React, { FC, useMemo, useRef, useState } from "preact/compat";
import "./style.scss";
import Table from "../../../components/Table/Table";
import { Logs } from "../../../api/types";
import Pagination from "../../../components/Main/Pagination/Pagination";
import { useEffect } from "react";
interface TableLogsProps {
logs: Logs[];
displayColumns: string[];
tableCompact: boolean;
columns: string[];
rowsPerPage: number;
}
const TableLogs: FC<TableLogsProps> = ({ logs, displayColumns, tableCompact, columns }) => {
const getColumnClass = (key: string) => {
switch (key) {
case "_time":
return "vm-table-cell_logs-time";
case "_vmui_data":
return "vm-table-cell_logs vm-table-cell_pre";
default:
return "vm-table-cell_logs";
}
};
const getColumnClass = (key: string) => {
switch (key) {
case "_time":
return "vm-table-cell_logs-time";
default:
return "vm-table-cell_logs";
}
};
const compactColumns = [{
key: "_vmui_data",
title: "Data",
className: "vm-table-cell_logs vm-table-cell_pre"
}];
const TableLogs: FC<TableLogsProps> = ({ logs, displayColumns, tableCompact, columns, rowsPerPage }) => {
const containerRef = useRef<HTMLDivElement>(null);
const [page, setPage] = useState(1);
const rows = useMemo(() => {
return logs.map((log) => {
const _vmui_data = JSON.stringify(log, null, 2);
return { ...log, _vmui_data };
}) as Logs[];
}, [logs]);
const tableColumns = useMemo(() => {
if (tableCompact) {
return [{
key: "_vmui_data",
title: "Data",
className: getColumnClass("_vmui_data")
}];
}
return columns.map((key) => ({
key: key as keyof Logs,
title: key,
className: getColumnClass(key),
}));
}, [tableCompact, columns]);
}, [columns]);
const filteredColumns = useMemo(() => {
if (tableCompact) return tableColumns;
if (tableCompact) return compactColumns;
if (!displayColumns?.length) return [];
return tableColumns.filter(c => displayColumns.includes(c.key as string));
}, [tableColumns, displayColumns, tableCompact]);
const paginationOffset = useMemo(() => {
const startIndex = (page - 1) * rowsPerPage;
const endIndex = startIndex + rowsPerPage;
return { startIndex, endIndex };
}, [page, rowsPerPage]);
const handlePageChange = (newPage: number) => {
setPage(newPage);
if (containerRef.current) {
const y = containerRef.current.getBoundingClientRect().top + window.scrollY - 50;
window.scrollTo({ top: y });
}
};
useEffect(() => {
setPage(1);
}, [logs, rowsPerPage]);
return (
<>
<Table
rows={logs}
columns={filteredColumns}
defaultOrderBy={"_time"}
defaultOrderDir={"desc"}
copyToClipboard={"_vmui_data"}
paginationOffset={{ startIndex: 0, endIndex: Infinity }}
<div ref={containerRef}>
<Table
rows={rows}
columns={filteredColumns}
defaultOrderBy={"_time"}
defaultOrderDir={"desc"}
copyToClipboard={"_vmui_data"}
paginationOffset={paginationOffset}
/>
</div>
<Pagination
currentPage={page}
totalItems={rows.length}
itemsPerPage={rowsPerPage}
onPageChange={handlePageChange}
/>
</>
);

View File

@@ -20,13 +20,12 @@ import { getStreamPairs } from "../../../utils/logs";
const WITHOUT_GROUPING = "No Grouping";
interface TableLogsProps {
interface Props {
logs: Logs[];
columns: string[];
settingsRef: React.RefObject<HTMLElement>;
}
const GroupLogs: FC<TableLogsProps> = ({ logs, settingsRef }) => {
const GroupLogs: FC<Props> = ({ logs, settingsRef }) => {
const { isDarkTheme } = useAppState();
const copyToClipboard = useCopyToClipboard();
const [searchParams, setSearchParams] = useSearchParams();
@@ -46,19 +45,21 @@ const GroupLogs: FC<TableLogsProps> = ({ logs, settingsRef }) => {
const expandAll = useMemo(() => expandGroups.every(Boolean), [expandGroups]);
const logsKeys = useMemo(() => {
const excludeKeys = ["_msg", "_time", "_vmui_time", "_vmui_data", "_vmui_markdown"];
const excludeKeys = ["_msg", "_time"];
const uniqKeys = Array.from(new Set(logs.map(l => Object.keys(l)).flat()));
const keys = [WITHOUT_GROUPING, ...uniqKeys.filter(k => !excludeKeys.includes(k))];
return [WITHOUT_GROUPING, ...uniqKeys.filter(k => !excludeKeys.includes(k))];
}, [logs]);
if (!searchKey) return keys;
const filteredLogsKeys = useMemo(() => {
if (!searchKey) return logsKeys;
try {
const regexp = new RegExp(searchKey, "i");
const found = keys.filter((item) => regexp.test(item));
return found.sort((a,b) => (a.match(regexp)?.index || 0) - (b.match(regexp)?.index || 0));
return logsKeys.filter(item => regexp.test(item))
.sort((a, b) => (a.match(regexp)?.index || 0) - (b.match(regexp)?.index || 0));
} catch (e) {
return [];
}
}, [logs, searchKey]);
}, [logsKeys, searchKey]);
const groupData = useMemo(() => {
return groupByMultipleKeys(logs, [groupBy]).map((item) => {
@@ -94,16 +95,15 @@ const GroupLogs: FC<TableLogsProps> = ({ logs, settingsRef }) => {
const handleToggleExpandAll = useCallback(() => {
setExpandGroups(new Array(groupData.length).fill(!expandAll));
}, [expandAll]);
}, [expandAll, groupData.length]);
const handleChangeExpand = (i: number) => (value: boolean) => {
const handleChangeExpand = useCallback((i: number) => (value: boolean) => {
setExpandGroups((prev) => {
const newExpandGroups = [...prev];
newExpandGroups[i] = value;
return newExpandGroups;
});
};
}, []);
useEffect(() => {
if (copied === null) return;
@@ -170,7 +170,7 @@ const GroupLogs: FC<TableLogsProps> = ({ logs, settingsRef }) => {
<Tooltip title={expandAll ? "Collapse All" : "Expand All"}>
<Button
variant="text"
startIcon={expandAll ? <CollapseIcon/> : <ExpandIcon/> }
startIcon={expandAll ? <CollapseIcon/> : <ExpandIcon/>}
onClick={handleToggleExpandAll}
ariaLabel={expandAll ? "Collapse All" : "Expand All"}
/>
@@ -179,7 +179,7 @@ const GroupLogs: FC<TableLogsProps> = ({ logs, settingsRef }) => {
<div ref={optionsButtonRef}>
<Button
variant="text"
startIcon={<StorageIcon/> }
startIcon={<StorageIcon/>}
onClick={toggleOpenOptions}
ariaLabel={"Group by"}
/>
@@ -201,7 +201,7 @@ const GroupLogs: FC<TableLogsProps> = ({ logs, settingsRef }) => {
type="search"
/>
</div>
{logsKeys.map(id => (
{filteredLogsKeys.map(id => (
<div
className={classNames({
"vm-list-item": true,

View File

@@ -0,0 +1,54 @@
import React, { FC, memo, useCallback, useEffect, useState } from "preact/compat";
import Tooltip from "../../../components/Main/Tooltip/Tooltip";
import Button from "../../../components/Main/Button/Button";
import { CopyIcon } from "../../../components/Main/Icons";
import useCopyToClipboard from "../../../hooks/useCopyToClipboard";
interface Props {
field: string;
value: string;
}
const GroupLogsFieldRow: FC<Props> = ({ field, value }) => {
const copyToClipboard = useCopyToClipboard();
const [copied, setCopied] = useState<boolean>(false);
const handleCopy = useCallback(async () => {
if (copied) return;
try {
await copyToClipboard(`${field}: "${value}"`);
setCopied(true);
} catch (e) {
console.error(e);
}
}, [copied, copyToClipboard]);
useEffect(() => {
if (copied === null) return;
const timeout = setTimeout(() => setCopied(false), 2000);
return () => clearTimeout(timeout);
}, [copied]);
return (
<tr className="vm-group-logs-row-fields-item">
<td className="vm-group-logs-row-fields-item-controls">
<div className="vm-group-logs-row-fields-item-controls__wrapper">
<Tooltip title={copied ? "Copied" : "Copy to clipboard"}>
<Button
variant="text"
color="gray"
size="small"
startIcon={<CopyIcon/>}
onClick={handleCopy}
ariaLabel="copy to clipboard"
/>
</Tooltip>
</div>
</td>
<td className="vm-group-logs-row-fields-item__key">{field}</td>
<td className="vm-group-logs-row-fields-item__value">{value}</td>
</tr>
);
};
export default memo(GroupLogsFieldRow);

View File

@@ -1,13 +1,15 @@
import React, { FC, useEffect, useMemo, useState } from "preact/compat";
import React, { FC, memo, useMemo } from "preact/compat";
import { Logs } from "../../../api/types";
import "./style.scss";
import useBoolean from "../../../hooks/useBoolean";
import Button from "../../../components/Main/Button/Button";
import Tooltip from "../../../components/Main/Tooltip/Tooltip";
import { ArrowDownIcon, CopyIcon } from "../../../components/Main/Icons";
import useCopyToClipboard from "../../../hooks/useCopyToClipboard";
import { ArrowDownIcon } from "../../../components/Main/Icons";
import classNames from "classnames";
import { useLogsState } from "../../../state/logsPanel/LogsStateContext";
import dayjs from "dayjs";
import { DATE_TIME_FORMAT } from "../../../constants/date";
import { useTimeState } from "../../../state/time/TimeStateContext";
import GroupLogsFieldRow from "./GroupLogsFieldRow";
import { marked } from "marked";
interface Props {
log: Logs;
@@ -20,40 +22,31 @@ const GroupLogsItem: FC<Props> = ({ log }) => {
} = useBoolean(false);
const { markdownParsing } = useLogsState();
const { timezone } = useTimeState();
const excludeKeys = ["_msg", "_vmui_time", "_vmui_data", "_vmui_markdown"];
const fields = Object.entries(log).filter(([key]) => !excludeKeys.includes(key));
const formattedTime = useMemo(() => {
if (!log._time) return "";
return dayjs(log._time).tz().format(`${DATE_TIME_FORMAT}.SSS`);
}, [log._time, timezone]);
const formattedMarkdown = useMemo(() => {
if (!markdownParsing || !log._msg) return "";
return marked(log._msg.replace(/```/g, "\n```\n")) as string;
}, [log._msg, markdownParsing]);
const fields = useMemo(() => Object.entries(log).filter(([key]) => key !== "_msg"), [log]);
const hasFields = fields.length > 0;
const displayMessage = useMemo(() => {
if (log._msg) return log._msg;
if (!hasFields) return;
const dataObject = fields.reduce<{[key: string]: string}>((obj, [key, value]) => {
const dataObject = fields.reduce<{ [key: string]: string }>((obj, [key, value]) => {
obj[key] = value;
return obj;
}, {});
return JSON.stringify(dataObject);
}, [log, fields, hasFields]);
const copyToClipboard = useCopyToClipboard();
const [copied, setCopied] = useState<number | null>(null);
const createCopyHandler = (copyValue: string, rowIndex: number) => async () => {
if (copied === rowIndex) return;
try {
await copyToClipboard(copyValue);
setCopied(rowIndex);
} catch (e) {
console.error(e);
}
};
useEffect(() => {
if (copied === null) return;
const timeout = setTimeout(() => setCopied(null), 2000);
return () => clearTimeout(timeout);
}, [copied]);
return (
<div className="vm-group-logs-row">
<div
@@ -74,10 +67,10 @@ const GroupLogsItem: FC<Props> = ({ log }) => {
<div
className={classNames({
"vm-group-logs-row-content__time": true,
"vm-group-logs-row-content__time_missing": !log._vmui_time
"vm-group-logs-row-content__time_missing": !formattedTime
})}
>
{log._vmui_time || "timestamp missing"}
{formattedTime || "timestamp missing"}
</div>
<div
className={classNames({
@@ -85,7 +78,7 @@ const GroupLogsItem: FC<Props> = ({ log }) => {
"vm-group-logs-row-content__msg_empty-msg": !log._msg,
"vm-group-logs-row-content__msg_missing": !displayMessage
})}
dangerouslySetInnerHTML={markdownParsing && log._vmui_markdown ? { __html: log._vmui_markdown } : undefined}
dangerouslySetInnerHTML={(markdownParsing && formattedMarkdown) ? { __html: formattedMarkdown } : undefined}
>
{displayMessage || "-"}
</div>
@@ -94,28 +87,12 @@ const GroupLogsItem: FC<Props> = ({ log }) => {
<div className="vm-group-logs-row-fields">
<table>
<tbody>
{fields.map(([key, value], i) => (
<tr
{fields.map(([key, value]) => (
<GroupLogsFieldRow
key={key}
className="vm-group-logs-row-fields-item"
>
<td className="vm-group-logs-row-fields-item-controls">
<div className="vm-group-logs-row-fields-item-controls__wrapper">
<Tooltip title={copied === i ? "Copied" : "Copy to clipboard"}>
<Button
variant="text"
color="gray"
size="small"
startIcon={<CopyIcon/>}
onClick={createCopyHandler(`${key}: "${value}"`, i)}
ariaLabel="copy to clipboard"
/>
</Tooltip>
</div>
</td>
<td className="vm-group-logs-row-fields-item__key">{key}</td>
<td className="vm-group-logs-row-fields-item__value">{value}</td>
</tr>
field={key}
value={value}
/>
))}
</tbody>
</table>
@@ -125,4 +102,4 @@ const GroupLogsItem: FC<Props> = ({ log }) => {
);
};
export default GroupLogsItem;
export default memo(GroupLogsItem);

View File

@@ -9,7 +9,7 @@ export const useFetchLogs = (server: string, query: string, limit: number) => {
const [searchParams] = useSearchParams();
const [logs, setLogs] = useState<Logs[]>([]);
const [isLoading, setIsLoading] = useState<{[key: number]: boolean;}>([]);
const [isLoading, setIsLoading] = useState<{ [key: number]: boolean }>({});
const [error, setError] = useState<ErrorTypes | string>();
const abortControllerRef = useRef(new AbortController());
@@ -33,8 +33,9 @@ export const useFetchLogs = (server: string, query: string, limit: number) => {
const parseLineToJSON = (line: string): Logs | null => {
try {
return JSON.parse(line);
return line && JSON.parse(line);
} catch (e) {
console.error(`Failed to parse "${line}" to JSON\n`, e);
return null;
}
};
@@ -56,23 +57,25 @@ export const useFetchLogs = (server: string, query: string, limit: number) => {
if (!response.ok || !response.body) {
setError(text);
setLogs([]);
setIsLoading(prev => ({ ...prev, [id]: false }));
return false;
}
const lines = text.split("\n").filter(line => line).slice(0, limit);
const data = lines.map(parseLineToJSON).filter(line => line) as Logs[];
const data = text.split("\n", limit).map(parseLineToJSON).filter(line => line) as Logs[];
setLogs(data);
setIsLoading(prev => ({ ...prev, [id]: false }));
return true;
} catch (e) {
setIsLoading(prev => ({ ...prev, [id]: false }));
if (e instanceof Error && e.name !== "AbortError") {
setError(String(e));
console.error(e);
setLogs([]);
}
return false;
} finally {
setIsLoading(prev => {
// Remove the `id` key from `isLoading` when its value becomes `false`
const { [id]: _, ...rest } = prev;
return rest;
});
}
}, [url, query, limit, searchParams]);

View File

@@ -0,0 +1,152 @@
import { Dispatch, SetStateAction, useCallback, useEffect, useMemo, useRef, useState } from "preact/compat";
import { MetricBase, MetricResult, ExportMetricResult } from "../../../api/types";
import { ErrorTypes, SeriesLimits } from "../../../types";
import { useQueryState } from "../../../state/query/QueryStateContext";
import { useTimeState } from "../../../state/time/TimeStateContext";
import { useAppState } from "../../../state/common/StateContext";
import { useCustomPanelState } from "../../../state/customPanel/CustomPanelStateContext";
import { isValidHttpUrl } from "../../../utils/url";
import { getExportDataUrl } from "../../../api/query-range";
interface FetchQueryParams {
hideQuery?: number[];
showAllSeries?: boolean;
}
interface FetchQueryReturn {
fetchUrl?: string[],
isLoading: boolean,
data?: MetricResult[],
error?: ErrorTypes | string,
queryErrors: (ErrorTypes | string)[],
setQueryErrors: Dispatch<SetStateAction<string[]>>,
warning?: string,
abortFetch: () => void
}
const parseLineToJSON = (line: string): ExportMetricResult | null => {
try {
return JSON.parse(line);
} catch (e) {
return null;
}
};
export const useFetchExport = ({ hideQuery, showAllSeries }: FetchQueryParams): FetchQueryReturn => {
const { query } = useQueryState();
const { period } = useTimeState();
const { displayType, reduceMemUsage, seriesLimits: stateSeriesLimits } = useCustomPanelState();
const { serverUrl } = useAppState();
const [isLoading, setIsLoading] = useState(false);
const [data, setData] = useState<MetricResult[]>();
const [error, setError] = useState<ErrorTypes | string>();
const [queryErrors, setQueryErrors] = useState<string[]>([]);
const [warning, setWarning] = useState<string>();
const abortControllerRef = useRef(new AbortController());
const fetchUrl = useMemo(() => {
setError("");
setQueryErrors([]);
if (!period) return;
if (!serverUrl) {
setError(ErrorTypes.emptyServer);
} else if (query.every(q => !q.trim())) {
setQueryErrors(query.map(() => ErrorTypes.validQuery));
} else if (isValidHttpUrl(serverUrl)) {
const updatedPeriod = { ...period };
return query.map(q => getExportDataUrl(serverUrl, q, updatedPeriod, reduceMemUsage));
} else {
setError(ErrorTypes.validServer);
}
}, [serverUrl, period, hideQuery, reduceMemUsage]);
const fetchData = useCallback(async ( { fetchUrl, stateSeriesLimits, showAllSeries }: {
fetchUrl: string[];
stateSeriesLimits: SeriesLimits;
showAllSeries?: boolean;
}) => {
abortControllerRef.current.abort();
abortControllerRef.current = new AbortController();
const { signal } = abortControllerRef.current;
setIsLoading(true);
try {
const tempData: MetricBase[] = [];
const seriesLimit = showAllSeries ? Infinity : +stateSeriesLimits[displayType] || Infinity;
let counter = 1;
let totalLength = 0;
for await (const url of fetchUrl) {
const isHideQuery = hideQuery?.includes(counter - 1);
if (isHideQuery) {
setQueryErrors(prev => [...prev, ""]);
counter++;
continue;
}
const response = await fetch(url, { signal });
const text = await response.text();
if (!response.ok || !response.body) {
tempData.push({ metric: {}, values: [], group: counter } as MetricBase);
setError(text);
setQueryErrors(prev => [...prev, `${text}`]);
} else {
setQueryErrors(prev => [...prev, ""]);
const freeTempSize = seriesLimit - tempData.length;
const lines = text.split("\n").filter(line => line);
const lineLimited = lines.slice(0, freeTempSize).sort();
lineLimited.forEach((line: string) => {
const jsonLine = parseLineToJSON(line);
if (!jsonLine) return;
tempData.push({
group: counter,
metric: jsonLine.metric,
values: jsonLine.values.map((value, index) => [(jsonLine.timestamps[index]/1000), value]),
} as MetricBase);
});
totalLength += lines.length;
}
counter++;
}
const limitText = `Showing ${tempData.length} series out of ${totalLength} series due to performance reasons. Please narrow down the query, so it returns less series`;
setWarning(totalLength > seriesLimit ? limitText : "");
setData(tempData as MetricResult[]);
setIsLoading(false);
} catch (e) {
setIsLoading(false);
if (e instanceof Error && e.name !== "AbortError") {
setError(error);
console.error(e);
}
}
}, [displayType, hideQuery]);
const abortFetch = useCallback(() => {
abortControllerRef.current.abort();
setData([]);
}, [abortControllerRef]);
useEffect(() => {
if (!fetchUrl?.length) return;
const timer = setTimeout(fetchData, 400, { fetchUrl, stateSeriesLimits, showAllSeries });
return () => {
abortControllerRef.current?.abort();
clearTimeout(timer);
};
}, [fetchUrl, stateSeriesLimits, showAllSeries]);
return {
fetchUrl,
isLoading,
data,
error,
queryErrors,
setQueryErrors,
warning,
abortFetch,
};
};

View File

@@ -0,0 +1,162 @@
import React, { FC, useState } from "preact/compat";
import LineLoader from "../../components/Main/LineLoader/LineLoader";
import { useCustomPanelState } from "../../state/customPanel/CustomPanelStateContext";
import { useQueryState } from "../../state/query/QueryStateContext";
import "../CustomPanel/style.scss";
import Alert from "../../components/Main/Alert/Alert";
import classNames from "classnames";
import useDeviceDetect from "../../hooks/useDeviceDetect";
import { useRef } from "react";
import CustomPanelTabs from "../CustomPanel/CustomPanelTabs";
import { DisplayTypeSwitch } from "../CustomPanel/DisplayTypeSwitch";
import QueryConfigurator from "../CustomPanel/QueryConfigurator/QueryConfigurator";
import WarningLimitSeries from "../CustomPanel/WarningLimitSeries/WarningLimitSeries";
import { useFetchExport } from "./hooks/useFetchExport";
import { useSetQueryParams } from "../CustomPanel/hooks/useSetQueryParams";
import { DisplayType } from "../../types";
import Hyperlink from "../../components/Main/Hyperlink/Hyperlink";
import { CloseIcon } from "../../components/Main/Icons";
import Button from "../../components/Main/Button/Button";
const RawSamplesLink = () => (
<Hyperlink
href="https://docs.victoriametrics.com/keyconcepts/#raw-samples"
underlined
>
raw samples
</Hyperlink>
);
const QueryDataLink = () => (
<Hyperlink
underlined
href="https://docs.victoriametrics.com/keyconcepts/#query-data"
>
Query API
</Hyperlink>
);
const TimeSeriesSelectorLink = () => (
<Hyperlink
underlined
href="https://docs.victoriametrics.com/keyconcepts/#filtering"
>
time series selector
</Hyperlink>
);
const RawQueryPage: FC = () => {
useSetQueryParams();
const { isMobile } = useDeviceDetect();
const { displayType } = useCustomPanelState();
const { query } = useQueryState();
const [hideQuery, setHideQuery] = useState<number[]>([]);
const [hideError, setHideError] = useState(!query[0]);
const [showAllSeries, setShowAllSeries] = useState(false);
const [showPageDescription, setShowPageDescription] = useState(true);
const {
data,
error,
isLoading,
warning,
queryErrors,
setQueryErrors,
abortFetch,
} = useFetchExport({ hideQuery, showAllSeries });
const controlsRef = useRef<HTMLDivElement>(null);
const showError = !hideError && error;
const handleHideQuery = (queries: number[]) => {
setHideQuery(queries);
};
const handleRunQuery = () => {
setHideError(false);
};
const handleHidePageDescription = () => {
setShowPageDescription(false);
};
return (
<div
className={classNames({
"vm-custom-panel": true,
"vm-custom-panel_mobile": isMobile,
})}
>
<QueryConfigurator
label={"Time series selector"}
queryErrors={!hideError ? queryErrors : []}
setQueryErrors={setQueryErrors}
setHideError={setHideError}
stats={[]}
isLoading={isLoading}
onHideQuery={handleHideQuery}
onRunQuery={handleRunQuery}
abortFetch={abortFetch}
hideButtons={{ traceQuery: true, disableCache: true }}
includeFunctions={false}
/>
{showPageDescription && (
<Alert variant="info">
<div className="vm-explore-metrics-header-description">
<p>
This page provides a dedicated view for querying and displaying <RawSamplesLink/> from VictoriaMetrics.
It expects only <TimeSeriesSelectorLink/> as a query argument.
Users often assume that the <QueryDataLink/> returns data exactly as stored,
but data samples and timestamps may be modified by the API.
</p>
<Button
variant="text"
size="small"
startIcon={<CloseIcon/>}
onClick={handleHidePageDescription}
ariaLabel="close tips"
/>
</div>
</Alert>
)}
{showError && <Alert variant="error">{error}</Alert>}
{warning && (
<WarningLimitSeries
warning={warning}
query={query}
onChange={setShowAllSeries}
/>
)}
<div
className={classNames({
"vm-custom-panel-body": true,
"vm-custom-panel-body_mobile": isMobile,
"vm-block": true,
"vm-block_mobile": isMobile,
})}
>
{isLoading && <LineLoader/>}
<div
className="vm-custom-panel-body-header"
ref={controlsRef}
>
<div className="vm-custom-panel-body-header__tabs">
<DisplayTypeSwitch tabFilter={(tab) => (tab.value !== DisplayType.table)}/>
</div>
</div>
<CustomPanelTabs
graphData={data}
liveData={data}
isHistogram={false}
displayType={displayType}
controlsRef={controlsRef}
/>
</div>
</div>
);
};
export default RawQueryPage;

View File

@@ -15,6 +15,7 @@ const router = {
icons: "/icons",
anomaly: "/anomaly",
query: "/query",
rawQuery: "/raw-query",
downsamplingDebug: "/downsampling-filters-debug",
retentionDebug: "/retention-filters-debug",
};
@@ -45,11 +46,15 @@ const routerOptionsDefault = {
}
};
export const routerOptions: {[key: string]: RouterOptions} = {
export const routerOptions: { [key: string]: RouterOptions } = {
[router.home]: {
title: "Query",
...routerOptionsDefault
},
[router.rawQuery]: {
title: "Raw query",
...routerOptionsDefault
},
[router.metrics]: {
title: "Explore Prometheus metrics",
header: {

View File

@@ -68,6 +68,7 @@ export const getDefaultNavigation = ({
showAlertLink,
}: NavigationConfig): NavigationItem[] => [
{ value: router.home },
{ value: router.rawQuery },
{ label: "Explore", submenu: getExploreNav() },
{ label: "Tools", submenu: getToolsNav(isEnterpriseLicense) },
{ value: router.dashboards, hide: !showPredefinedDashboards },

View File

@@ -10,6 +10,7 @@ export interface CustomPanelState {
isTracingEnabled: boolean;
seriesLimits: SeriesLimits
tableCompact: boolean;
reduceMemUsage: boolean;
}
export type CustomPanelAction =
@@ -18,6 +19,7 @@ export type CustomPanelAction =
| { type: "TOGGLE_NO_CACHE"}
| { type: "TOGGLE_QUERY_TRACING" }
| { type: "TOGGLE_TABLE_COMPACT" }
| { type: "TOGGLE_REDUCE_MEM_USAGE"}
export const getInitialDisplayType = () => {
const queryTab = getQueryStringValue("g0.tab", 0) as string;
@@ -33,6 +35,7 @@ export const initialCustomPanelState: CustomPanelState = {
isTracingEnabled: false,
seriesLimits: limitsStorage ? JSON.parse(limitsStorage) : DEFAULT_MAX_SERIES,
tableCompact: getFromStorage("TABLE_COMPACT") as boolean || false,
reduceMemUsage: false
};
export function reducer(state: CustomPanelState, action: CustomPanelAction): CustomPanelState {
@@ -65,6 +68,12 @@ export function reducer(state: CustomPanelState, action: CustomPanelAction): Cus
...state,
tableCompact: !state.tableCompact
};
case "TOGGLE_REDUCE_MEM_USAGE":
saveToStorage("TABLE_COMPACT", !state.reduceMemUsage);
return {
...state,
reduceMemUsage: !state.reduceMemUsage
};
default:
throw new Error();
}

View File

@@ -23,6 +23,7 @@ export interface SeriesItem extends Series {
median: number;
forecast?: ForecastType | null;
forecastGroup?: string;
hasAlias?: boolean;
}
export interface HideSeriesArgs {
@@ -45,6 +46,7 @@ export interface LegendItemType {
freeFormFields: {[key: string]: string};
statsFormatted: SeriesItemStatsFormatted;
median: number
hasAlias: boolean;
}
export interface BarSeriesItem {

View File

@@ -2,16 +2,23 @@ import { MetricBase } from "../api/types";
export const getNameForMetric = (result: MetricBase, alias?: string, showQueryNum = true): string => {
const { __name__, ...freeFormFields } = result.metric;
const name = alias || `${showQueryNum ? `[Query ${result.group}] ` : ""}${__name__ || ""}`;
if (Object.keys(freeFormFields).length == 0) {
if (!name) {
return "value";
}
return name;
const queryPrefix = showQueryNum ? `[Query ${result.group}] ` : "";
if (alias) {
return alias.replace(/\{\{(\w+)}}/g, (_, key) => result.metric[key] || "");
}
return `${name}{${Object.entries(freeFormFields).map(e =>
`${e[0]}=${JSON.stringify(e[1])}`
).join(", ")}}`;
const name = `${queryPrefix}${__name__ || ""}`;
if (Object.keys(freeFormFields).length === 0) {
return name || "value";
}
const fieldsString = Object.entries(freeFormFields)
.map(([key, value]) => `${key}=${JSON.stringify(value)}`)
.join(", ");
return `${name}{${fieldsString}}`;
};
export const promValueToNumber = (s: string): number => {

View File

@@ -1,5 +1,6 @@
import uPlot from "uplot";
import { MetricResult } from "../../api/types";
import { SeriesItem } from "../../types";
export const formatTicks = (u: uPlot, ticks: number[], unit = ""): string[] => {
const min = ticks[0];
@@ -53,7 +54,11 @@ export const getDashLine = (group: number): number[] => {
return group <= 1 ? [] : [group*4, group*1.2];
};
export const getMetricName = (metricItem: MetricResult) => {
export const getMetricName = (metricItem: MetricResult, seriesItem: SeriesItem) => {
if (seriesItem?.hasAlias && seriesItem?.label) {
return seriesItem.label;
}
const metric = metricItem?.metric || {};
const labelNames = Object.keys(metric).filter(x => x != "__name__");
const labels = labelNames.map(key => `${key}=${JSON.stringify(metric[key])}`);

View File

@@ -42,10 +42,12 @@ export const getSeriesItemContext = (data: MetricResult[], hideSeries: string[],
return (d: MetricResult, i: number): SeriesItem => {
const metricInfo = isAnomalyUI ? isForecast(data[i].metric) : null;
const label = isAnomalyUI ? metricInfo?.group || "" : getNameForMetric(d, alias[d.group - 1]);
const aliasValue = alias[d.group - 1];
const label = isAnomalyUI ? metricInfo?.group || "" : getNameForMetric(d, aliasValue);
return {
label,
hasAlias: Boolean(aliasValue),
dash: getDashSeries(metricInfo),
width: getWidthSeries(metricInfo),
stroke: getStrokeSeries({ metricInfo, label, isAnomalyUI, colorState }),
@@ -88,6 +90,7 @@ export const getLegendItem = (s: SeriesItem, group: number): LegendItemType => (
freeFormFields: s.freeFormFields,
statsFormatted: s.statsFormatted,
median: s.median,
hasAlias: s.hasAlias || false,
});
export const getHideSeries = ({ hideSeries, legend, metaKey, series, isAnomalyView }: HideSeriesArgs): string[] => {
@@ -185,7 +188,27 @@ const getPointsSeries = (metricInfo: ForecastMetricInfo | null): uPlotSeries.Poi
if (isAnomalyMetric) {
return { size: 8, width: 4, space: 0 };
}
return { size: 4.2, width: 1.4 };
return {
size: 4,
width: 0,
show: true,
filter: filterPoints,
};
};
const filterPoints = (self: uPlot, seriesIdx: number): number[] | null => {
const data = self.data[seriesIdx];
const indices = [];
for (let i = 0; i < data.length; i++) {
const prev = data[i - 1];
const next = data[i + 1];
if (prev === null && next === null) {
indices.push(i);
}
}
return indices;
};
type GetStrokeSeriesArgs = {

View File

@@ -1,6 +1,7 @@
package apptest
import (
"bytes"
"io"
"net/http"
"net/url"
@@ -36,13 +37,13 @@ func (c *Client) CloseConnections() {
// the response body to the caller.
func (c *Client) Get(t *testing.T, url string, wantStatusCode int) string {
t.Helper()
return c.do(t, http.MethodGet, url, "", "", wantStatusCode)
return c.do(t, http.MethodGet, url, "", nil, wantStatusCode)
}
// 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, data string, wantStatusCode int) string {
func (c *Client) Post(t *testing.T, url, contentType string, data []byte, wantStatusCode int) string {
t.Helper()
return c.do(t, http.MethodPost, url, contentType, data, wantStatusCode)
}
@@ -52,16 +53,16 @@ func (c *Client) Post(t *testing.T, url, contentType, data string, wantStatusCod
// 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 {
t.Helper()
return c.Post(t, url, "application/x-www-form-urlencoded", data.Encode(), wantStatusCode)
return c.Post(t, url, "application/x-www-form-urlencoded", []byte(data.Encode()), wantStatusCode)
}
// 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, data string, wantStatusCode int) string {
func (c *Client) do(t *testing.T, method, url, contentType string, data []byte, wantStatusCode int) string {
t.Helper()
req, err := http.NewRequest(method, url, strings.NewReader(data))
req, err := http.NewRequest(method, url, bytes.NewReader(data))
if err != nil {
t.Fatalf("could not create a HTTP request: %v", err)
}
@@ -128,3 +129,31 @@ func (app *ServesMetrics) GetMetric(t *testing.T, metricName string) float64 {
t.Fatalf("metic not found: %s", metricName)
return 0
}
// GetMetricsByPrefix retrieves the values of all metrics that start with given
// prefix.
func (app *ServesMetrics) GetMetricsByPrefix(t *testing.T, prefix string) []float64 {
t.Helper()
values := []float64{}
metrics := app.cli.Get(t, app.metricsURL, http.StatusOK)
for _, metric := range strings.Split(metrics, "\n") {
if !strings.HasPrefix(metric, prefix) {
continue
}
parts := strings.Split(metric, " ")
if len(parts) < 2 {
t.Fatalf("unexpected record format: got %q, want metric name and value separated by a space", metric)
}
value, err := strconv.ParseFloat(parts[len(parts)-1], 64)
if err != nil {
t.Fatalf("could not parse metric value %s: %v", metric, err)
}
values = append(values, value)
}
return values
}

View File

@@ -3,27 +3,83 @@ package apptest
import (
"encoding/json"
"fmt"
"net/url"
"slices"
"strconv"
"strings"
"testing"
"time"
pb "github.com/VictoriaMetrics/VictoriaMetrics/lib/prompbmarshal"
)
// PrometheusQuerier contains methods available to Prometheus-like HTTP API for Querying
type PrometheusQuerier interface {
PrometheusAPIV1Query(t *testing.T, query, time, step string, opts QueryOpts) *PrometheusAPIV1QueryResponse
PrometheusAPIV1QueryRange(t *testing.T, query, start, end, step string, opts QueryOpts) *PrometheusAPIV1QueryResponse
PrometheusAPIV1Export(t *testing.T, query string, opts QueryOpts) *PrometheusAPIV1QueryResponse
PrometheusAPIV1Query(t *testing.T, query string, opts QueryOpts) *PrometheusAPIV1QueryResponse
PrometheusAPIV1QueryRange(t *testing.T, query string, opts QueryOpts) *PrometheusAPIV1QueryResponse
PrometheusAPIV1Series(t *testing.T, matchQuery string, opts QueryOpts) *PrometheusAPIV1SeriesResponse
}
// PrometheusWriter contains methods available to Prometheus-like HTTP API for Writing new data
type PrometheusWriter interface {
PrometheusAPIV1Write(t *testing.T, records []pb.TimeSeries, opts QueryOpts)
PrometheusAPIV1ImportPrometheus(t *testing.T, records []string, opts QueryOpts)
}
// StorageFlusher defines a method that forces the flushing of data inserted
// into the storage, so it becomes available for searching immediately.
type StorageFlusher interface {
ForceFlush(t *testing.T)
}
// PrometheusWriteQuerier encompasses the methods for writing, flushing and
// querying the data.
type PrometheusWriteQuerier interface {
PrometheusWriter
PrometheusQuerier
StorageFlusher
}
// QueryOpts contains various params used for querying or ingesting data
type QueryOpts struct {
Tenant string
Timeout string
Tenant string
Timeout string
Start string
End string
Time string
Step string
ExtraFilters []string
ExtraLabels []string
}
func (qos *QueryOpts) asURLValues() url.Values {
uv := make(url.Values)
addNonEmpty := func(name string, values ...string) {
for _, value := range values {
if len(value) == 0 {
continue
}
uv.Add(name, value)
}
}
addNonEmpty("start", qos.Start)
addNonEmpty("end", qos.End)
addNonEmpty("time", qos.Time)
addNonEmpty("step", qos.Step)
addNonEmpty("timeout", qos.Timeout)
addNonEmpty("extra_label", qos.ExtraLabels...)
addNonEmpty("extra_filters", qos.ExtraFilters...)
return uv
}
// getTenant returns tenant with optional default value
func (qos *QueryOpts) getTenant() string {
if qos.Tenant == "" {
return "0"
}
return qos.Tenant
}
// PrometheusAPIV1QueryResponse is an inmemory representation of the
@@ -40,7 +96,7 @@ func NewPrometheusAPIV1QueryResponse(t *testing.T, s string) *PrometheusAPIV1Que
res := &PrometheusAPIV1QueryResponse{}
if err := json.Unmarshal([]byte(s), res); err != nil {
t.Fatalf("could not unmarshal query response: %v", err)
t.Fatalf("could not unmarshal query response data=\n%s\n: %v", string(s), err)
}
return res
}
@@ -81,7 +137,7 @@ func NewSample(t *testing.T, timeStr string, value float64) *Sample {
// UnmarshalJSON populates the sample fields from a JSON string.
func (s *Sample) UnmarshalJSON(b []byte) error {
var (
ts int64
ts float64
v string
)
raw := []any{&ts, &v}
@@ -91,7 +147,7 @@ func (s *Sample) UnmarshalJSON(b []byte) error {
if got, want := len(raw), 2; got != want {
return fmt.Errorf("unexpected number of fields: got %d, want %d (raw sample: %s)", got, want, string(b))
}
s.Timestamp = ts
s.Timestamp = int64(ts)
var err error
s.Value, err = strconv.ParseFloat(v, 64)
if err != nil {
@@ -115,7 +171,23 @@ func NewPrometheusAPIV1SeriesResponse(t *testing.T, s string) *PrometheusAPIV1Se
res := &PrometheusAPIV1SeriesResponse{}
if err := json.Unmarshal([]byte(s), res); err != nil {
t.Fatalf("could not unmarshal series response: %v", err)
t.Fatalf("could not unmarshal series response data:\n%s\n err: %v", string(s), err)
}
return res
}
// Sort sorts the response data.
func (r *PrometheusAPIV1SeriesResponse) Sort() {
str := func(m map[string]string) string {
s := []string{}
for k, v := range m {
s = append(s, k+v)
}
slices.Sort(s)
return strings.Join(s, "")
}
slices.SortFunc(r.Data, func(a, b map[string]string) int {
return strings.Compare(str(a), str(b))
})
}

View File

@@ -1,9 +1,12 @@
package apptest
import (
"fmt"
"testing"
"time"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/fs"
"github.com/google/go-cmp/cmp"
)
// TestCase holds the state and defines clean-up procedure common for all test
@@ -51,6 +54,17 @@ func (tc *TestCase) Stop() {
}
}
// MustStartDefaultVmsingle is a test helper function that starts an instance of
// vmsingle with defaults suitable for most tests.
func (tc *TestCase) MustStartDefaultVmsingle() *Vmsingle {
tc.t.Helper()
return tc.MustStartVmsingle("vmsingle", []string{
"-storageDataPath=" + tc.Dir() + "/vmsingle",
"-retentionPeriod=100y",
})
}
// MustStartVmsingle is a test helper function that starts an instance of
// vmsingle and fails the test if the app fails to start.
func (tc *TestCase) MustStartVmsingle(instance string, flags []string) *Vmsingle {
@@ -103,6 +117,125 @@ func (tc *TestCase) MustStartVminsert(instance string, flags []string) *Vminsert
return app
}
type vmcluster struct {
*Vminsert
*Vmselect
vmstorages []*Vmstorage
}
func (c *vmcluster) ForceFlush(t *testing.T) {
for _, s := range c.vmstorages {
s.ForceFlush(t)
}
}
// MustStartDefaultCluster is a typical cluster configuration suitable for most
// tests.
//
// The cluster consists of two vmstorages, one vminsert and one vmselect, no
// data replication.
//
// Such configuration is suitable for tests that don't verify the
// cluster-specific behavior (such as sharding, replication, or multilevel
// vmselect) but instead just need a typical cluster configuration to verify
// some business logic (such as API surface, or MetricsQL). Such cluster
// tests usually come paired with corresponding vmsingle tests.
func (tc *TestCase) MustStartDefaultCluster() PrometheusWriteQuerier {
tc.t.Helper()
vmstorage1 := tc.MustStartVmstorage("vmstorage-1", []string{
"-storageDataPath=" + tc.Dir() + "/vmstorage-1",
"-retentionPeriod=100y",
})
vmstorage2 := tc.MustStartVmstorage("vmstorage-2", []string{
"-storageDataPath=" + tc.Dir() + "/vmstorage-2",
"-retentionPeriod=100y",
})
vminsert := tc.MustStartVminsert("vminsert", []string{
"-storageNode=" + vmstorage1.VminsertAddr() + "," + vmstorage2.VminsertAddr(),
})
vmselect := tc.MustStartVmselect("vmselect", []string{
"-storageNode=" + vmstorage1.VmselectAddr() + "," + vmstorage2.VmselectAddr(),
})
return &vmcluster{vminsert, vmselect, []*Vmstorage{vmstorage1, vmstorage2}}
}
func (tc *TestCase) addApp(app Stopper) {
tc.startedApps = append(tc.startedApps, app)
}
// AssertOptions hold the assertion params, such as got and wanted values as
// well as the message that should be included into the assertion error message
// in case of failure.
//
// In VictoriaMetrics (especially the cluster version) the inserted data does
// not become visible for querying right away. Therefore, the first comparisons
// may fail. AssertOptions allow to configure how many times the actual result
// must be retrieved and compared with the expected one and for long to wait
// between the retries. If these two params (`Retries` and `Period`) are not
// set, the default values will be used.
//
// If it is known that the data is available, then the retry functionality can
// be disabled by setting the `DoNotRetry` field.
//
// AssertOptions are used by the TestCase.Assert() method, and this method uses
// cmp.Diff() from go-cmp package for comparing got and wanted values.
// AssertOptions, therefore, allows to pass cmp.Options to cmp.Diff() via
// `CmpOpts` field.
//
// Finally the `FailNow` field controls whether the assertion should fail using
// `testing.T.Errorf()` or `testing.T.Fatalf()`.
type AssertOptions struct {
Msg string
Got func() any
Want any
CmpOpts []cmp.Option
DoNotRetry bool
Retries int
Period time.Duration
FailNow bool
}
// Assert compares the actual result with the expected one possibly multiple
// times in order to account for the fact that the inserted data does not become
// available for querying right away (especially in cluster version of
// VictoriaMetrics).
func (tc *TestCase) Assert(opts *AssertOptions) {
tc.t.Helper()
const (
defaultRetries = 20
defaultPeriod = 100 * time.Millisecond
)
if opts.DoNotRetry {
opts.Retries = 1
opts.Period = 0
} else {
if opts.Retries <= 0 {
opts.Retries = defaultRetries
}
if opts.Period <= 0 {
opts.Period = defaultPeriod
}
}
var diff string
for range opts.Retries {
diff = cmp.Diff(opts.Want, opts.Got(), opts.CmpOpts...)
if diff == "" {
return
}
time.Sleep(opts.Period)
}
msg := fmt.Sprintf("%s (-want, +got):\n%s", opts.Msg, diff)
if opts.FailNow {
tc.t.Fatal(msg)
} else {
tc.t.Error(msg)
}
}

View File

@@ -1,11 +1,11 @@
package tests
import (
"testing"
"github.com/VictoriaMetrics/VictoriaMetrics/apptest"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"testing"
"time"
)
// Data used in examples in
@@ -28,127 +28,180 @@ var docData = []string{
}
// TestSingleKeyConceptsQuery verifies cases from https://docs.victoriametrics.com/keyconcepts/#query-data
// for vm-single.
func TestSingleKeyConceptsQuery(t *testing.T) {
tc := apptest.NewTestCase(t)
defer tc.Stop()
vmsingle := tc.MustStartVmsingle("vmsingle", []string{
"-storageDataPath=" + tc.Dir() + "/vmstorage",
"-retentionPeriod=100y",
})
sut := tc.MustStartDefaultVmsingle()
opts := apptest.QueryOpts{Timeout: "5s"}
// Insert example data from documentation.
vmsingle.PrometheusAPIV1ImportPrometheus(t, docData, opts)
vmsingle.ForceFlush(t)
testInstantQuery(t, vmsingle, opts)
testRangeQuery(t, vmsingle, opts)
testKeyConceptsQueryData(t, sut)
}
// TestClusterKeyConceptsQuery verifies cases from https://docs.victoriametrics.com/keyconcepts/#query-data
func TestClusterKeyConceptsQuery(t *testing.T) {
// TestClusterKeyConceptsQueryData verifies cases from https://docs.victoriametrics.com/keyconcepts/#query-data
// for vm-cluster.
func TestClusterKeyConceptsQueryData(t *testing.T) {
tc := apptest.NewTestCase(t)
defer tc.Stop()
// Set up the following cluster configuration:
//
// - two vmstorage instances
// - vminsert points to the two vmstorages, its replication setting
// is off which means it will only shard the incoming data across the two
// vmstorages.
// - vmselect points to the two vmstorages and is expected to query both
// vmstorages and build the full result out of the two partial results.
sut := tc.MustStartDefaultCluster()
vmstorage1 := tc.MustStartVmstorage("vmstorage-1", []string{
"-storageDataPath=" + tc.Dir() + "/vmstorage-1",
"-retentionPeriod=100y",
})
vmstorage2 := tc.MustStartVmstorage("vmstorage-2", []string{
"-storageDataPath=" + tc.Dir() + "/vmstorage-2",
"-retentionPeriod=100y",
})
vminsert := tc.MustStartVminsert("vminsert", []string{
"-storageNode=" + vmstorage1.VminsertAddr() + "," + vmstorage2.VminsertAddr(),
})
vmselect := tc.MustStartVmselect("vmselect", []string{
"-storageNode=" + vmstorage1.VmselectAddr() + "," + vmstorage2.VmselectAddr(),
})
opts := apptest.QueryOpts{Timeout: "5s", Tenant: "0"}
// Insert example data from documentation.
vminsert.PrometheusAPIV1ImportPrometheus(t, docData, opts)
time.Sleep(2 * time.Second)
vmstorage1.ForceFlush(t)
vmstorage2.ForceFlush(t)
testInstantQuery(t, vmselect, opts)
testRangeQuery(t, vmselect, opts)
testKeyConceptsQueryData(t, sut)
}
// vmsingleInstantQuery verifies the statements made in the
// `Instant query` section of the VictoriaMetrics documentation. See:
// testClusterKeyConceptsQuery verifies cases from https://docs.victoriametrics.com/keyconcepts/#query-data
func testKeyConceptsQueryData(t *testing.T, sut apptest.PrometheusWriteQuerier) {
// Insert example data from documentation.
sut.PrometheusAPIV1ImportPrometheus(t, docData, apptest.QueryOpts{})
sut.ForceFlush(t)
testInstantQuery(t, sut)
testRangeQuery(t, sut)
testRangeQueryIsEquivalentToManyInstantQueries(t, sut)
}
// testInstantQuery verifies the statements made in the `Instant query` section
// of the VictoriaMetrics documentation. See:
// https://docs.victoriametrics.com/keyconcepts/#instant-query
func testInstantQuery(t *testing.T, q apptest.PrometheusQuerier, opts apptest.QueryOpts) {
// Get the value of the foo_bar time series at 2022-05-10Z08:03:00Z with the
func testInstantQuery(t *testing.T, q apptest.PrometheusQuerier) {
// Get the value of the foo_bar time series at 2022-05-10T08:03:00Z with the
// step of 5m and timeout 5s. There is no sample at exactly this timestamp.
// Therefore, VictoriaMetrics will search for the nearest sample within the
// [time-5m..time] interval.
got := q.PrometheusAPIV1Query(t, "foo_bar", "2022-05-10T08:03:00.000Z", "5m", opts)
got := q.PrometheusAPIV1Query(t, "foo_bar", apptest.QueryOpts{Time: "2022-05-10T08:03:00.000Z", Step: "5m"})
want := apptest.NewPrometheusAPIV1QueryResponse(t, `{"data":{"result":[{"metric":{"__name__":"foo_bar"},"value":[1652169780,"3"]}]}}`)
opt := cmpopts.IgnoreFields(apptest.PrometheusAPIV1QueryResponse{}, "Status", "Data.ResultType")
if diff := cmp.Diff(want, got, opt); diff != "" {
t.Errorf("unexpected response (-want, +got):\n%s", diff)
}
// Get the value of the foo_bar time series at 2022-05-10Z08:18:00Z with the
// Get the value of the foo_bar time series at 2022-05-10T08:18:00Z with the
// step of 1m and timeout 5s. There is no sample at this timestamp.
// Therefore, VictoriaMetrics will search for the nearest sample within the
// [time-1m..time] interval. Since the nearest sample is 2m away and the
// step is 1m, then the VictoriaMetrics must return empty response.
got = q.PrometheusAPIV1Query(t, "foo_bar", "2022-05-10T08:18:00.000Z", "1m", opts)
got = q.PrometheusAPIV1Query(t, "foo_bar", apptest.QueryOpts{Time: "2022-05-10T08:18:00.000Z", Step: "1m"})
if len(got.Data.Result) > 0 {
t.Errorf("unexpected response: got non-empty result, want empty result:\n%v", got)
}
}
// vmsingleRangeQuery verifies the statements made in the
// `Range query` section of the VictoriaMetrics documentation. See:
// testRangeQuery verifies the statements made in the `Range query` section of
// the VictoriaMetrics documentation. See:
// https://docs.victoriametrics.com/keyconcepts/#range-query
func testRangeQuery(t *testing.T, q apptest.PrometheusQuerier, opts apptest.QueryOpts) {
// Get the values of the foo_bar time series for
// [2022-05-10Z07:59:00Z..2022-05-10Z08:17:00Z] time interval with the step
// of 1m and timeout 5s.
got := q.PrometheusAPIV1QueryRange(t, "foo_bar", "2022-05-10T07:59:00.000Z", "2022-05-10T08:17:00.000Z", "1m", opts)
want := apptest.NewPrometheusAPIV1QueryResponse(t, `{"data": {"result": [{"metric": {"__name__": "foo_bar"}, "values": []}]}}`)
s := make([]*apptest.Sample, 17)
// Sample for 2022-05-10T07:59:00Z is missing because the time series has
// samples only starting from 8:00.
s[0] = apptest.NewSample(t, "2022-05-10T08:00:00Z", 1)
s[1] = apptest.NewSample(t, "2022-05-10T08:01:00Z", 2)
s[2] = apptest.NewSample(t, "2022-05-10T08:02:00Z", 3)
s[3] = apptest.NewSample(t, "2022-05-10T08:03:00Z", 3)
s[4] = apptest.NewSample(t, "2022-05-10T08:04:00Z", 5)
s[5] = apptest.NewSample(t, "2022-05-10T08:05:00Z", 5)
s[6] = apptest.NewSample(t, "2022-05-10T08:06:00Z", 5.5)
s[7] = apptest.NewSample(t, "2022-05-10T08:07:00Z", 5.5)
s[8] = apptest.NewSample(t, "2022-05-10T08:08:00Z", 4)
s[9] = apptest.NewSample(t, "2022-05-10T08:09:00Z", 4)
// Sample for 2022-05-10T08:10:00Z is missing because there is no sample
// within the [8:10 - 1m .. 8:10] interval.
s[10] = apptest.NewSample(t, "2022-05-10T08:11:00Z", 3.5)
s[11] = apptest.NewSample(t, "2022-05-10T08:12:00Z", 3.25)
s[12] = apptest.NewSample(t, "2022-05-10T08:13:00Z", 3)
s[13] = apptest.NewSample(t, "2022-05-10T08:14:00Z", 2)
s[14] = apptest.NewSample(t, "2022-05-10T08:15:00Z", 1)
s[15] = apptest.NewSample(t, "2022-05-10T08:16:00Z", 4)
s[16] = apptest.NewSample(t, "2022-05-10T08:17:00Z", 4)
want.Data.Result[0].Samples = s
opt := cmpopts.IgnoreFields(apptest.PrometheusAPIV1QueryResponse{}, "Status", "Data.ResultType")
if diff := cmp.Diff(want, got, opt); diff != "" {
t.Errorf("unexpected response (-want, +got):\n%s", diff)
func testRangeQuery(t *testing.T, q apptest.PrometheusQuerier) {
f := func(start, end, step string, wantSamples []*apptest.Sample) {
t.Helper()
got := q.PrometheusAPIV1QueryRange(t, "foo_bar", apptest.QueryOpts{Start: start, End: end, Step: step})
want := apptest.NewPrometheusAPIV1QueryResponse(t, `{"data": {"result": [{"metric": {"__name__": "foo_bar"}, "values": []}]}}`)
want.Data.Result[0].Samples = wantSamples
opt := cmpopts.IgnoreFields(apptest.PrometheusAPIV1QueryResponse{}, "Status", "Data.ResultType")
if diff := cmp.Diff(want, got, opt); diff != "" {
t.Errorf("unexpected response (-want, +got):\n%s", diff)
}
}
// Verify the statement that the query result for
// [2022-05-10T07:59:00Z..2022-05-10T08:17:00Z] time range and 1m step will
// contain 17 points.
f("2022-05-10T07:59:00.000Z", "2022-05-10T08:17:00.000Z", "1m", []*apptest.Sample{
// Sample for 2022-05-10T07:59:00Z is missing because the time series has
// samples only starting from 8:00.
apptest.NewSample(t, "2022-05-10T08:00:00Z", 1),
apptest.NewSample(t, "2022-05-10T08:01:00Z", 2),
apptest.NewSample(t, "2022-05-10T08:02:00Z", 3),
apptest.NewSample(t, "2022-05-10T08:03:00Z", 3),
apptest.NewSample(t, "2022-05-10T08:04:00Z", 5),
apptest.NewSample(t, "2022-05-10T08:05:00Z", 5),
apptest.NewSample(t, "2022-05-10T08:06:00Z", 5.5),
apptest.NewSample(t, "2022-05-10T08:07:00Z", 5.5),
apptest.NewSample(t, "2022-05-10T08:08:00Z", 4),
apptest.NewSample(t, "2022-05-10T08:09:00Z", 4),
// Sample for 2022-05-10T08:10:00Z is missing because there is no sample
// within the [8:10 - 1m .. 8:10] interval.
apptest.NewSample(t, "2022-05-10T08:11:00Z", 3.5),
apptest.NewSample(t, "2022-05-10T08:12:00Z", 3.25),
apptest.NewSample(t, "2022-05-10T08:13:00Z", 3),
apptest.NewSample(t, "2022-05-10T08:14:00Z", 2),
apptest.NewSample(t, "2022-05-10T08:15:00Z", 1),
apptest.NewSample(t, "2022-05-10T08:16:00Z", 4),
apptest.NewSample(t, "2022-05-10T08:17:00Z", 4),
})
// Verify the statement that a query is executed at start, start+step,
// start+2*step, …, step+N*step timestamps, where N is the whole number
// of steps that fit between start and end.
f("2022-05-10T08:00:01.000Z", "2022-05-10T08:02:00.000Z", "1m", []*apptest.Sample{
apptest.NewSample(t, "2022-05-10T08:00:01Z", 1),
apptest.NewSample(t, "2022-05-10T08:01:01Z", 2),
})
// Verify the statement that a query is executed at start, start+step,
// start+2*step, …, end timestamps, when end = start + N*step.
f("2022-05-10T08:00:00.000Z", "2022-05-10T08:02:00.000Z", "1m", []*apptest.Sample{
apptest.NewSample(t, "2022-05-10T08:00:00Z", 1),
apptest.NewSample(t, "2022-05-10T08:01:00Z", 2),
apptest.NewSample(t, "2022-05-10T08:02:00Z", 3),
})
// If the step isnt set, then it defaults to 5m (5 minutes).
f("2022-05-10T07:59:00.000Z", "2022-05-10T08:17:00.000Z", "", []*apptest.Sample{
// Sample for 2022-05-10T07:59:00Z is missing because the time series has
// samples only starting from 8:00.
apptest.NewSample(t, "2022-05-10T08:04:00Z", 5),
apptest.NewSample(t, "2022-05-10T08:09:00Z", 4),
apptest.NewSample(t, "2022-05-10T08:14:00Z", 2),
})
}
// testRangeQueryIsEquivalentToManyInstantQueries verifies the statement made in
// the `Range query` section of the VictoriaMetrics documentation that a range
// query is actually an instant query executed 1 + (start-end)/step times on the
// time range from start to end. See:
// https://docs.victoriametrics.com/keyconcepts/#range-query
func testRangeQueryIsEquivalentToManyInstantQueries(t *testing.T, q apptest.PrometheusQuerier) {
f := func(timestamp string, want *apptest.Sample) {
t.Helper()
gotInstant := q.PrometheusAPIV1Query(t, "foo_bar", apptest.QueryOpts{Time: timestamp, Step: "1m"})
if want == nil {
if got, want := len(gotInstant.Data.Result), 0; got != want {
t.Errorf("unexpected instant result size: got %d, want %d", got, want)
}
} else {
got := gotInstant.Data.Result[0].Sample
if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("unexpected instant sample (-want, +got):\n%s", diff)
}
}
}
rangeRes := q.PrometheusAPIV1QueryRange(t, "foo_bar", apptest.QueryOpts{
Start: "2022-05-10T07:59:00.000Z",
End: "2022-05-10T08:17:00.000Z",
Step: "1m",
})
rangeSamples := rangeRes.Data.Result[0].Samples
f("2022-05-10T07:59:00.000Z", nil)
f("2022-05-10T08:00:00.000Z", rangeSamples[0])
f("2022-05-10T08:01:00.000Z", rangeSamples[1])
f("2022-05-10T08:02:00.000Z", rangeSamples[2])
f("2022-05-10T08:03:00.000Z", rangeSamples[3])
f("2022-05-10T08:04:00.000Z", rangeSamples[4])
f("2022-05-10T08:05:00.000Z", rangeSamples[5])
f("2022-05-10T08:06:00.000Z", rangeSamples[6])
f("2022-05-10T08:07:00.000Z", rangeSamples[7])
f("2022-05-10T08:08:00.000Z", rangeSamples[8])
f("2022-05-10T08:09:00.000Z", rangeSamples[9])
f("2022-05-10T08:10:00.000Z", nil)
f("2022-05-10T08:11:00.000Z", rangeSamples[10])
f("2022-05-10T08:12:00.000Z", rangeSamples[11])
f("2022-05-10T08:13:00.000Z", rangeSamples[12])
f("2022-05-10T08:14:00.000Z", rangeSamples[13])
f("2022-05-10T08:15:00.000Z", rangeSamples[14])
f("2022-05-10T08:16:00.000Z", rangeSamples[15])
f("2022-05-10T08:17:00.000Z", rangeSamples[16])
}

View File

@@ -0,0 +1,130 @@
package tests
import (
"fmt"
"testing"
"time"
"github.com/VictoriaMetrics/VictoriaMetrics/apptest"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/decimal"
pb "github.com/VictoriaMetrics/VictoriaMetrics/lib/prompbmarshal"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
)
func millis(s string) int64 {
t, err := time.Parse(time.RFC3339, s)
if err != nil {
panic(fmt.Sprintf("could not parse time %q: %v", s, err))
}
return t.UnixMilli()
}
var staleNaNsData = func() []pb.TimeSeries {
return []pb.TimeSeries{
{
Labels: []pb.Label{
{
Name: "__name__",
Value: "metric",
},
},
Samples: []pb.Sample{
{
Value: 1,
Timestamp: millis("2024-01-01T00:01:00Z"),
},
{
Value: decimal.StaleNaN,
Timestamp: millis("2024-01-01T00:02:00Z"),
},
},
},
}
}()
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{})
sut.ForceFlush(t)
var got, want *apptest.PrometheusAPIV1QueryResponse
cmpOptions := []cmp.Option{
cmpopts.IgnoreFields(apptest.PrometheusAPIV1QueryResponse{}, "Status", "Data.ResultType"),
cmpopts.EquateNaNs(),
}
// Verify that instant query returns the first point.
got = sut.PrometheusAPIV1Query(t, "metric", apptest.QueryOpts{
Step: "5m",
Time: "2024-01-01T00:01:00.000Z",
})
want = apptest.NewPrometheusAPIV1QueryResponse(t, `{"data": {"result": [{"metric": {"__name__": "metric"}}]}}`)
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)
}
// Verify that instant query does not return stale NaN.
got = sut.PrometheusAPIV1Query(t, "metric", apptest.QueryOpts{
Step: "5m",
Time: "2024-01-01T00:02:00.000Z",
})
want = apptest.NewPrometheusAPIV1QueryResponse(t, `{"data": {"result": []}}`)
// Empty response, stale NaN is not included into response
if diff := cmp.Diff(want, got, cmpOptions...); diff != "" {
t.Errorf("unexpected response (-want, +got):\n%s", diff)
}
// Verify that instant query with default rollup function returns stale NaN
// while it must not.
// See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/5806
got = sut.PrometheusAPIV1Query(t, "metric[2m]", apptest.QueryOpts{
Step: "5m",
Time: "2024-01-01T00:02:00.000Z",
})
want = apptest.NewPrometheusAPIV1QueryResponse(t, `{"data": {"result": [{"metric": {"__name__": "metric"}, "values": []}]}}`)
s := make([]*apptest.Sample, 2)
s[0] = apptest.NewSample(t, "2024-01-01T00:01:00Z", 1)
s[1] = apptest.NewSample(t, "2024-01-01T00:02:00Z", decimal.StaleNaN)
want.Data.Result[0].Samples = s
if diff := cmp.Diff(want, got, cmpOptions...); diff != "" {
t.Errorf("unexpected response (-want, +got):\n%s", diff)
}
// Verify that exported data contains stale NaN.
got = sut.PrometheusAPIV1Export(t, `{__name__="metric"}`, apptest.QueryOpts{
Start: "2024-01-01T00:01:00.000Z",
End: "2024-01-01T00:02:00.000Z",
})
want = apptest.NewPrometheusAPIV1QueryResponse(t, `{"data": {"result": [{"metric": {"__name__": "metric"}, "values": []}]}}`)
s = make([]*apptest.Sample, 2)
s[0] = apptest.NewSample(t, "2024-01-01T00:01:00Z", 1)
s[1] = apptest.NewSample(t, "2024-01-01T00:02:00Z", decimal.StaleNaN)
want.Data.Result[0].Samples = s
if diff := cmp.Diff(want, got, cmpOptions...); diff != "" {
t.Errorf("unexpected response (-want, +got):\n%s", diff)
}
}

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