Compare commits

...

147 Commits

Author SHA1 Message Date
Zakhar Bessarab
1d6340fe83 deps: go mod vendor after 1334eee4
Signed-off-by: Zakhar Bessarab <z.bessarab@victoriametrics.com>
2025-06-10 18:29:25 +04:00
Zakhar Bessarab
1334eee433 make/tools: switch only qtc to "go tool"
Restore other tools to migrate step by step.

Signed-off-by: Zakhar Bessarab <z.bessarab@victoriametrics.com>
2025-06-10 18:28:31 +04:00
Zakhar Bessarab
2d67e14d59 make/tools: switch tooling to use go tool for external tools
Go 1.24 added first class support for management of tools via built-in `go tool` commands - https://tip.golang.org/doc/go1.24#tools

Switching tools management to go tooling addresses the following:
- version drift - previously make commands did not verify specific version of the tool
- global installation of dependencies - dependencies are now well scoped and do not require any globally installed parts

This commit sets up tooling to use `go tool` pattern for the same make targets as previously so there are no changes in workflow needed for existing users.

Signed-off-by: Zakhar Bessarab <z.bessarab@victoriametrics.com>
2025-06-06 15:04:44 +04:00
Aliaksandr Valialkin
539498058e lib/atomicutil: add CacheLineSize const equal to the size of CPU cache line, and use this const for padding against false sharing across the code base
This should reduce the waste of memory on the padding from 128 bytes to 64 bytes on GOARCH=amd64,
while preserving bigger padding for platforms with bigger cache line sizes.

See https://stackoverflow.com/questions/68320687/why-are-most-cache-line-sizes-designed-to-be-64-byte-instead-of-32-128byte-now

Thanks to @tIGO for the hint
2025-06-06 10:21:40 +02:00
Peter Gervai
b3d22403eb docs: update LogsQL "field pipe" typo (#9037)
### Describe Your Changes

Typo? It's called "fields" pipe, not "field".

### Checklist

The following checks are **mandatory**:

- [x] My change adheres to [VictoriaMetrics contributing
guidelines](https://docs.victoriametrics.com/victoriametrics/contributing/).
2025-06-06 09:55:06 +02:00
hagen1778
0fce51e3b4 docs: prettify the changelog
Signed-off-by: hagen1778 <roman@victoriametrics.com>
2025-06-06 09:52:02 +02:00
hagen1778
9e118fe1ee docs: rm 11831-victoria-metrics-cluster-ig1-version dashboard
Remove the community-provided dashboard as it remains without updates
for a few years already. Recommending it may hurt user's experience.

Signed-off-by: hagen1778 <roman@victoriametrics.com>
2025-06-06 09:44:37 +02:00
Zakhar Bessarab
3553c60399 app/vmctl: enable dual-stack mode by default (#9119)
Dual stack mode is disabled by default in order to avoid accidentally
exposing components via IPv6 networks.

vmctl does not expose any endpoints and does not allow using default Go
flags as it is using `urfave/cli` lib.

This commit enables IPv6 support by default since there is no security
risks related to network configuration and this make vmctl easier to use
with default configuration.

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

Signed-off-by: Zakhar Bessarab <z.bessarab@victoriametrics.com>
2025-06-06 09:40:26 +02:00
Dmitry Ponomaryov
9b54bd6e8d app/vlinsert: add logging of skipped bytes for log lines exceeding insert.maxLineSizeBytes (#9082)
### Describe Your Changes

This change adds logging of the number of skipped bytes when a log line
exceeds the configured `insert.maxLineSizeBytes`.

it helps diagnose and tune systems dealing with oversized log records by
showing how much to increase the parameter for the log to fit in
storage.

### Checklist

The following checks are **mandatory**:

- [x] My change adheres to [VictoriaMetrics contributing
guidelines](https://docs.victoriametrics.com/victoriametrics/contributing/#pull-request-checklist).

---------

Signed-off-by: Dmitry Ponomaryov <iamhalje@gmail.com>
Co-authored-by: Aliaksandr Valialkin <valyala@victoriametrics.com>
2025-06-06 09:08:57 +02:00
Artur Minchukou
a90edc71c7 app/vmui/logs: optimize live tailing performance by limiting logs to 200 and notifying users (#9083)
### Describe Your Changes

Added a log limit if the 200 logs per second limit is reached and a
notification for the user asking them to add a filter to the query

### Checklist

The following checks are **mandatory**:

- [ ] My change adheres to [VictoriaMetrics contributing
guidelines](https://docs.victoriametrics.com/victoriametrics/contributing/#pull-request-checklist).
2025-06-06 09:02:43 +02:00
Nils K
83deddc84c Fix typo of journald in CLI flag (#9112)
### Describe Your Changes

Fix swapped letters

### Checklist

The following checks are **mandatory**:

- [x] My change adheres to [VictoriaMetrics contributing
guidelines](https://docs.victoriametrics.com/victoriametrics/contributing/#pull-request-checklist).

Signed-off-by: Septatrix <24257556+septatrix@users.noreply.github.com>
2025-06-06 08:59:16 +02:00
Jose Gómez-Sellés
434cb7028c docs/cloud: add explore data page (#9113)
### Describe Your Changes

This PR adds the explore section to the docs. It emphasizes on
explaining and linking assets for VMUI and
MetricsQL

### Checklist

The following checks are **mandatory**:

- [x] My change adheres to [VictoriaMetrics contributing
guidelines](https://docs.victoriametrics.com/victoriametrics/contributing/#pull-request-checklist).
2025-06-06 08:53:59 +02:00
Zakhar Bessarab
107b6517b7 lib/netutil/netutil: fix strings index check
### Describe Your Changes

Properly check precense of `/`, previously it was 
ignoring a case where "/" would be at the beginning of the string.

This is a follow-up for 00712b18

---------

Signed-off-by: Zakhar Bessarab <z.bessarab@victoriametrics.com>
2025-06-06 09:01:50 +04:00
Fred Navruzov
f68f5b3113 docs/vmanomaly - missing updates for v1.23.0 (#9114)
### Describe Your Changes

Some of the missing doc updates after 1.23.0 release

### Checklist

The following checks are **mandatory**:

- [x] My change adheres to [VictoriaMetrics contributing
guidelines](https://docs.victoriametrics.com/victoriametrics/contributing/#pull-request-checklist).
2025-06-05 20:21:52 +02:00
Aliaksandr Valialkin
d49b4a7550 docs/victorialogs/cluster.md: added missing -storageNode option in the example on how to disable /insert/* requests at vlselect
This is a follow-up for 41558066db

Updates https://github.com/VictoriaMetrics/VictoriaMetrics/issues/9061
2025-06-05 18:44:17 +02:00
Aliaksandr Valialkin
bd8b4eb78b app/vmauth: add tests for the case when url_prefix ends with / and the requested path starts with /
This is needed for verifying https://github.com/VictoriaMetrics/VictoriaMetrics/issues/9096
2025-06-05 18:37:53 +02:00
Phuong Le
41558066db vlselect/vlinsert: allow disabling the vlinsert and vlselect endpoints (#9067)
## Problem

In vlcluster evel setups, components like vlselect can still accept and
forward /insert requests. The lack of strict endpoint control increases
the risk of human error and undermines deployment security boundaries.

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

## Fix

Add flags to disable the vlinsert and vlselect endpoints. The
`-insert.disable` flag also disables the internalinsert endpoint.
Similarly for vlselect.

---------

Co-authored-by: Aliaksandr Valialkin <valyala@victoriametrics.com>
2025-06-05 18:21:29 +02:00
Fred Navruzov
af064ca65a docs/vmanomaly - release v1.23.0 (#9111)
### Describe Your Changes

Docs update for vmanomaly v1.23.0 release

### Checklist

The following checks are **mandatory**:

- [x] My change adheres to [VictoriaMetrics contributing
guidelines](https://docs.victoriametrics.com/victoriametrics/contributing/#pull-request-checklist).
2025-06-05 17:43:58 +02:00
Nikolay
eced71a96d lib/storage: properly load metric_usage_tracker file content
Previously, if metric_usage_tracker file was corrupted. It prevented
VictoriaMetrics from start and required manual action. Corruption may
happen in various reasons, such as unclean shutdown of the process.

 This commit changes panic into error message, in the same way as other
caches do.

Related issue:
https://github.com/VictoriaMetrics/VictoriaMetrics/issues/9074
2025-06-05 14:07:31 +02:00
Aliaksandr Valialkin
23fd269ccf lib/{storage,mergeset}: reduce the multi-CPU contention on global stats vars, which are updated during background merge
Background merge updates the global stats on the number of merged / deleted items. This may result in slowdown
when multiple goroutines update these global stats at frequent rate, since every goroutine must fetch the actual value
for the updated stats from slow memory on every update. It is much faster to count the needed stats locally per every goroutine
and then periodically updating the global stats (once per ~second).

Thanks to @tIGO for the intial implementation of this idea at https://github.com/VictoriaMetrics/VictoriaMetrics/pull/8683/files#diff-95e28ae911944708f94f3bb31fa9ba8bc185dedc23ae6fb02a272c34b8f83244

This should help improving scalability of background merges on multi-CPU systems.
See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/8682
2025-06-05 12:24:03 +02:00
Aliaksandr Valialkin
1f5d02e059 lib: make sure that frequently updated global counters are padded in order to protect from false sharing issues on multi-CPU systems
Go linker packs global variables close to each other in the memory. This may lead to false sharing (https://en.wikipedia.org/wiki/False_sharing)
among these variables if frequently updated vars are put close to mostly read-only vars like described
at https://github.com/VictoriaMetrics/VictoriaMetrics/issues/8682 .

This commit adds padding to frequently updated global vars. This guarantees that these variables are put into distinct CPU cache lines
comparing to the rest of global variables. See https://github.com/VictoriaMetrics/VictoriaMetrics/pull/8683#issuecomment-2943254119

Thanks to @tIGO for the intial attempt to fix the issue at https://github.com/VictoriaMetrics/VictoriaMetrics/pull/8683
2025-06-05 11:40:20 +02:00
Zakhar Bessarab
690aaf7d2d app/vmselect/netstorage/tenant_cache: fix inconsistent fetching of tenants list
Previously, any case when cache returned items was skipping lookup of
tenants at vmstorage nodes. This leaded to inconsistent results for
cases when cache contained items to cover only some part of requested
time range.

Fix this by forcing a cache item to cover full requested time range.
This forces cache hits to always be "full hits".

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

Target branch for this PR is another PR related to the same issue -
https://github.com/VictoriaMetrics/VictoriaMetrics/pull/9048, this is in
order to avoid additional rebasing/merge as this PR will conflict with
cluster branch after initial PR merge. GH will change target for this pr
to cluster brance once #9048 will be merged.

---------

Signed-off-by: Zakhar Bessarab <z.bessarab@victoriametrics.com>
2025-06-05 11:09:32 +04:00
f41gh7
1e0f7f0d28 app/vmgateway: add support of mTLS for read and write backends
This commit introduces new flags for mTLS configuration:

```
  -read.tlsCAFile
  -read.tlsCertFile
  -read.tlsInsecureSkipVerify
  -read.tlsKeyFile
  -read.tlsServerName

  -write.tlsCAFile
  -write.tlsCertFile
  -write.tlsInsecureSkipVerify
  -write.tlsKeyFile
  -write.tlsServerName
```

Related issue:
https://github.com/VictoriaMetrics/VictoriaMetrics/issues/8841
2025-06-04 19:36:39 +02:00
Andrii Chubatiuk
c7a16e1df6 app/vmgateway: added more select routes
This commits adds additional vmselect routes.
Such as `/static`, `/api/v1/status/metric_names_stats` and others.

 In addition it properly redirects `/vmui` and `/vmalert` access endpoints requests. Such endpoints require to preserve trailing `/`. Previously it was omitted and redirect requests failed.

Related issue:
https://github.com/VictoriaMetrics/VictoriaMetrics/issues/9003
2025-06-04 19:36:28 +02:00
Dmytro Kozlov
2cb909022f apptest/vmctl: migrate vmctl test for the prometheus migration process (#9047)
Moved Prometheus migration process test to the apptest folder, where all
integration tests are held

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

Other tests will be migrated one by one.
2025-06-04 16:40:26 +02:00
Zakhar Bessarab
fe70b963e4 app/vmselect/netstorage: allow disabling cache for list of tenants
Properly respect passing `nocache=1` or using `search.disableCache` when
executing a query. Also allow disabling tenant cache separately in order
to make debugging easier.

Related: https://github.com/VictoriaMetrics/VictoriaMetrics/issues/9042

The following checks are **mandatory**:

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

---------

Signed-off-by: Zakhar Bessarab <z.bessarab@victoriametrics.com>
2025-06-04 17:05:39 +04:00
Zakhar Bessarab
9bb726751c deployment/docker: do not update stable tag
### Describe Your Changes

Stop updating `:stable` tags as those are exactly the same as `:latest`
but not used by default by docker/podman and other commands.

### Checklist

The following checks are **mandatory**:

- [x] My change adheres to [VictoriaMetrics contributing
guidelines](https://docs.victoriametrics.com/victoriametrics/contributing/#pull-request-checklist).

---------

Signed-off-by: Zakhar Bessarab <z.bessarab@victoriametrics.com>
2025-06-04 16:57:40 +04:00
Hui Wang
3c85ffb1e6 docs: minor fixes (#9090) 2025-06-04 10:04:40 +02:00
hagen1778
65cb6468ac docs: typo fix
Signed-off-by: hagen1778 <roman@victoriametrics.com>
2025-06-04 10:03:27 +02:00
Robin Hayer
8e645ea708 lib/workingsetcache: log error when restoring cache from file (#8952)
### What this PR does

log error returned by `fastcache.LoadFromFile` before falling back to
creating a new cache instance. this improves observability and helps
detect problems like file corruption or permission issues early.

this replaces `fastcache.LoadFromFileOrNew` with a custom function
`loadFromFileOrNewWithLog` that explicitly logs errors encountered
during cache restoration.

---

### Related Issue

Closes #8934

---

### Test Plan

- manually tested by simulating a missing file scenario  
- ensured expected log output on cache load failure  
- verified normal cache creation fallback path  

---

### Changelog

log error when cache fails to restore from file during workingsetcache
initialization (#8934)

---

### Checklist

- [x] Signed commits  
- [x] Follows coding and commit message conventions  
- [x] Tested manually  
- [x] Scope limited to relevant change  
- [x] Changelog entry added

Co-authored-by: Robin Hayer <rshayer95@gmail.com>
Co-authored-by: Roman Khavronenko <hagen1778@gmail.com>
2025-06-04 09:56:24 +02:00
Vadim Alekseev
b95bdb5781 app/vlselect: set missing Authorization header (#9089)
### Describe Your Changes

Set missing `Authorization` header when querying a storage node and
Basic Auth is enabled.

See: #9080 

### Checklist

The following checks are **mandatory**:

- [X] My change adheres to [VictoriaMetrics contributing
guidelines](https://docs.victoriametrics.com/victoriametrics/contributing/#pull-request-checklist).
2025-06-04 08:57:06 +02:00
Aliaksandr Valialkin
5ecc5770c2 deployment/docker/Makefile: properly publish multi-architecture Docker images at latest and stable tags
The previous approach was assigning only the current architecture image to the `latest` and `stable` tags.

This is a follow-up for 02c03793b3

Thanks to @zekker6 for the initial attempt to address this issue at https://github.com/VictoriaMetrics/VictoriaMetrics/pull/9088 .
This attempt was using a third-party component - skopeo , which must be installed manually.
This complicates the usage of the `make publish-latest` command.

The new approach, which is implemented in this commit, is to use the standard `docker buildx imagetool create` command
for creating `latest` and `stable` tags, which contain images for all the architectures from the source tag.
See https://docs.docker.com/reference/cli/docker/buildx/imagetools/create/

Updates https://github.com/VictoriaMetrics/VictoriaMetrics/issues/7336
2025-06-04 08:46:17 +02:00
Aliaksandr Valialkin
02c03793b3 Makefile: add TAG=v1.x.y make publish-latest command for publishing latest and stable Docker image tags from the given TAG
Add the step for running this command after publishing Docker images during the release process.
See docs/victoriametrics/Release-Guide.md

This commit resolves the issue with the missing `latest` and `stable` tags after the https://github.com/VictoriaMetrics/VictoriaMetrics/issues/7336
This also resolve issues with accidental publishing of incorrect Docker images under the `latest` and `stable` tags.
It is very easy to fix incorrectly published `latest` and `stable` tags by re-running the `TAG=v1.x.y make publish-latest` command,
which updates the `latest` and `stable` tags, so they point to the given TAG=v1.x.y.
2025-06-03 18:02:07 +02:00
hagen1778
c74c4b24d7 docs: fix broken image in victoriametrics-cloud docs
bug was introduced in 07be0c6129

Signed-off-by: hagen1778 <roman@victoriametrics.com>
2025-06-03 16:07:37 +02:00
hagen1778
07be0c6129 docs: fix broken links in victoriametrics-cloud docs
Signed-off-by: hagen1778 <roman@victoriametrics.com>
2025-06-03 11:56:27 +02:00
hagen1778
826c408e0e docs: fix broken links in victorialogs docs
Signed-off-by: hagen1778 <roman@victoriametrics.com>
2025-06-03 11:52:15 +02:00
hagen1778
913b64d9b5 docs: fix broken links in victoriametrics docs
Signed-off-by: hagen1778 <roman@victoriametrics.com>
2025-06-03 11:49:51 +02:00
hagen1778
6b76dead5a docs: fix broken links in vmanomaly docs
Signed-off-by: hagen1778 <roman@victoriametrics.com>
2025-06-03 11:44:10 +02:00
hagen1778
41991edb34 docs: follow the same approach for assets linking as in other docs
Signed-off-by: hagen1778 <roman@victoriametrics.com>
2025-06-03 11:37:58 +02:00
hagen1778
eb7c21bde5 docs: fix typos and reference errors in k8s monitoring guide
See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/9069

Signed-off-by: hagen1778 <roman@victoriametrics.com>
2025-06-03 11:32:35 +02:00
Roman Khavronenko
3cc8013dd9 docs: add guideline for merging PRs (#9066)
### 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 to [VictoriaMetrics contributing
guidelines](https://docs.victoriametrics.com/victoriametrics/contributing/).

---------

Signed-off-by: hagen1778 <roman@victoriametrics.com>
Co-authored-by: Max Kotliar <kotlyar.maksim@gmail.com>
2025-06-03 11:12:22 +02:00
Hui Wang
1209f33c6d doc: clarify ingested metric usage more (#9075)
### 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 to [VictoriaMetrics contributing
guidelines](https://docs.victoriametrics.com/victoriametrics/contributing/).
2025-06-03 11:05:57 +02:00
maegpankey
3c87e361ba docs: fix various typos and grammar in FAQ #9072 (#9073)
### Describe Your Changes

Fixed grammatical and phrasing issues in first half of FAQ docs.

### Checklist

The following checks are **mandatory**:

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

---------

Co-authored-by: Roman Khavronenko <hagen1778@gmail.com>
2025-06-03 11:05:19 +02:00
Aliaksandr Valialkin
f5c9c5bf01 lib/logstorage: allow using prefix filters on log fields in some LogsQL pipes
This should simplify working with big number of log fields in LogsQL queries.
Examples:

- `... | keep foo*` leaves only fields starting with `foo` prefix
- `... | rm foo*` removes all the fields starting with `foo` prefix
- `... | mv foo* bar*` replaces `foo` prefix with `bar` prefix in log fields
- `... | sum(foo*)` sums all the log fields starting with `foo` prefix
2025-06-02 22:41:57 +02:00
Aliaksandr Valialkin
7712a34ba6 docs/victorialogs/CHANGELOG.md: typo fix - use the proper link to v1.18.0-victorialogs release 2025-06-02 21:47:04 +02:00
Aliaksandr Valialkin
d890bf52fe docs/victorialogs/CHANGELOG.md: add missing closing brace 2025-06-02 21:45:08 +02:00
Aliaksandr Valialkin
f52478dac7 deployment: update VictoriaLogs Docker image tag from v1.23.2-victorialogs to v1.23.3-victorialogs
See https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.23.3-victorialogs
2025-06-02 21:43:53 +02:00
Aliaksandr Valialkin
bcc2c85e53 docs/victorialogs/CHANGELOG.md: cut the release v1.23.3-victorialogs 2025-06-02 21:38:15 +02:00
Aliaksandr Valialkin
001f9218b1 app/vlselect: properly sort results for /select/logsql/query with limit query arg and for /select/logsql/tail
The DataBlock.GetTimestamps() was returning a slice of strings, which belong to the DataBlock.
These strings are changed whenever the DataBlock is re-used for the next block.
So these strings couldn't be assigned to logRow.timestamp and to tailProcessor.lastTimestamps,
which outlive the DataBlock. The commit aa8c18fc9f5d44091d7ca92be6935eeaf3b85d7f broke this assumption,
which triggered the following bugs:

1. The bug, which could return incorrectly sorted results from /select/logsql/query when the 'limit' query arg is passed to it.
   The endpoint must return the last 'limit' log entries on the selected time range in this case, and these log entries
   must be sorted by _time.

2. The bug, which could return incorrect results from /select/logsql/tail (e.g. it could incorrectly skip some matching logs,
   it could return the same logs multiple times and it could return out-of-order logs without proper sorting by _time).

The solution is to return parsed timestamps from the DataBlock.GetTimestamps() function, so they could be safely
used by the caller without worries that they could be changed while in use.
2025-06-02 21:34:02 +02:00
Aliaksandr Valialkin
f7fc897f85 docs/victorialogs: add a link to the post from the user who migrated from 27-node Elasticsearch to a single-node VictoriaMetrics
The link is https://aus.social/@phs/114583927679254536
2025-06-02 19:18:15 +02:00
Aliaksandr Valialkin
e58b512305 docs/victorialogs/data-ingestion/DataDogAgent.md: add commonly used alias in the Internet for this page - https://docs.victoriametrics.com/victorialogs/data-ingestion/datadog/
The https://docs.victoriametrics.com/victorialogs/data-ingestion/datadog/ shows in Google Analytics report for 404 pages.
2025-06-02 18:19:06 +02:00
Aliaksandr Valialkin
d33efbbd95 app/vlselect: drop all the pipes from LogsQL query passed to HTTP querying APIs used in auto-suggestion
Auto-suggestion expects field names and values from the real logs stored in the database.
It doesn't expect field names and values created by pipes.

See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/9068#issuecomment-2931275012
2025-06-02 17:53:23 +02:00
Zhu Jiekun
23cb0475e9 vmselect: remove tenant info when exporting data in native format
Fix https://github.com/VictoriaMetrics/VictoriaMetrics/issues/9016.

Data will carry `vm_account_id` and `vm_project_id` labels when
exporting with native export API in cluster.

These labels could be treated as normal labels and be imported to
victoriametrics cluster, hence inconsistent with the source metrics
data.

e.g.:
1. source data: `{__name__="metrics_test"}`.
2. exported data: `{__name__="metrics_test", vm_account_id="0",
vm_project_id="0"}`.
3. re-imported data: `{__name__="metrics_test", vm_account_id="0",
vm_project_id="0", vm_account_id="0", vm_project_id="0"}`.
4. query result for MetricsQL `metrics_test{}`:
`{__name__="metrics_test", vm_account_id="0", vm_project_id="0"}`.
5. expect query result: `{__name__="metrics_test"}`

In VictoriaMetrics cluster, `vm_account_id` and `vm_project_id` label
are only useful when doing multi-tenant export/import. So they should be
remove if the export URL is not for multi-tenant.

This pull request:
- properly remove tenant info when exporting data in native format.

Note:
- Commit 67514c37ef23c22b91638e80e30504be23fa8dc1 is for apptest and
need to be cherry pick to master branch cc @rtm0 .

The following checks are **mandatory**:

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

---------

Signed-off-by: Zakhar Bessarab <z.bessarab@victoriametrics.com>
Co-authored-by: Roman Khavronenko <roman@victoriametrics.com>
Co-authored-by: Zakhar Bessarab <z.bessarab@victoriametrics.com>
2025-06-02 19:17:44 +04:00
Nick Yang
3d3fcf8fcb docs/contribution: fix makefile target typo
### Describe Your Changes

`tests-full` (plural) target doesn't exist, but test (singular) does

discovered while working through unrelated PR

### Checklist

The following checks are **mandatory**:

- [x] My change adheres to [VictoriaMetrics contributing
guidelines](https://docs.victoriametrics.com/victoriametrics/contributing/).
2025-06-02 19:00:18 +04:00
Zakhar Bessarab
d99e3e52f3 lib/backup: add support of object metadata configuration
Add an option to configure metadata of objects when uploading backups.
For AWS S3 also support using object tagging.

Using metadata of objects is useful in order to get extended reports
about bucket content and billing details. It is also useful when
performing queries to bucket content based on metadata.

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

---------

Signed-off-by: Zakhar Bessarab <z.bessarab@victoriametrics.com>
Co-authored-by: Roman Khavronenko <roman@victoriametrics.com>
2025-06-02 18:52:05 +04:00
Aliaksandr Valialkin
bbcfc0ce59 vendor: run make vendor-update 2025-06-02 16:10:26 +02:00
Aliaksandr Valialkin
d9ac6867cb vendor: update github.com/valyala/gozstd from v1.21.2 to v1.22.0
This updates upstream zstd from v1.5.6 to v1.5.7 . See https://github.com/facebook/zstd/releases/tag/v1.5.7
2025-06-02 15:52:54 +02:00
Zakhar Bessarab
00712b184b app/netstorage: improve validation for address provided at storageNode
Previously, address was always parsed as "host:port" and added port if
it was missing. This leaded to hard to understand errors in case address
was provided in "http://host:port" format.

Improve error validation in order to provide more precise error message
in case of invalid address format.

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

Previous error message: `cannot dial storageNode
"http://localhost:8488": dial tcp4: address http://localhost:8488: too
many colons in address` and vminsert continue running.
Current error message: `cannot normalize
-storageNode="http://localhost:8480": invalid address
"http://localhost:8480"; expected format: host:port` and vminsert exists
with error status code.

---------

Signed-off-by: Zakhar Bessarab <z.bessarab@victoriametrics.com>
2025-06-02 13:29:15 +04:00
Zakhar Bessarab
30ca617960 lib/promrelabel: follow up for aef59d9
Sync quick-template to add missing comma for the resulting JSON.

Signed-off-by: Zakhar Bessarab <z.bessarab@victoriametrics.com>
2025-06-02 11:22:01 +04:00
f41gh7
aba5205896 lib/storage: properly apply retentionFilter changes
Previously, if the value of rentetionFilter was changed within the same
retention, storage didn't start background merge for historical data.

 This commits changes this behaviour by writing applied
filters into metadata.json. For backward-compatibility it reads content
of appliedRetention.txt file. It should prevent from triggering
background merge on storage update. If needed, manually remove appliedRetention.txt file from
storage/data/PART folder and remove storage.

 Also, it properly applies retentionFilter for data back-filling.
Previously, it was ignored and data outside of retention could be
ingested.

 In addition, it changes scheduling of historical merges.
Instead 2 separate background processes, storage launches a single
thread. It reduces CPU resource and disk IO resources usage.

Related issues:
https://github.com/VictoriaMetrics/VictoriaMetrics/issues/8885
https://github.com/VictoriaMetrics/VictoriaMetrics/issues/4592
2025-05-30 19:38:05 +02:00
Zakhar Bessarab
aef59d9281 lib/promrelabel: prevent panic caused by invalid label name or value in debug interface
### Describe Your Changes

Previously, invalid label name or value could cause a panic of vmselect
or vmsingle as it was using MustNewLabelsFromString which was added for
usage in tests only.

Fix this by properly handling and propagating error to user interface if
there is any.

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

Signed-off-by: Zakhar Bessarab <z.bessarab@victoriametrics.com>
2025-05-30 19:22:48 +02:00
Zakhar Bessarab
b1582b3012 app/vmbackupmanager: verify backup availability when creating a restore mark
Previously, restore mark could be create to a backup which does not exist or incomplete. This would lead to a crash when attempting to perform restore later on.

This commit adds verification of backup availability and completion to prevent such issues from happening. It also adds a verification bypass mechanism for cases when user wants to create a restore mark which is not currently available.

related issues:
https://github.com/VictoriaMetrics/VictoriaMetrics/issues/5361
https://github.com/VictoriaMetrics/VictoriaMetrics/issues/8771
2025-05-30 15:20:44 +02:00
f41gh7
dd769d87c0 app/vmbackupmanager: add support for user-defined timezone for backup scheduling
This is useful in order to create backups at midnight in timezone specific to the user allowing to make sure backups are taken at off-peak hours of operations.

See these issues for details:
- https://github.com/VictoriaMetrics/VictoriaMetrics/issues/6707
- https://github.com/VictoriaMetrics/VictoriaMetrics/issues/3950

Signed-off-by: Zakhar Bessarab <z.bessarab@victoriametrics.com>
2025-05-30 15:20:32 +02:00
Zhu Jiekun
febe9a2882 vmselect: dynamic concurrent dial limit
- dynamically adjusts the concurrent dial limit between 8 and 64 based
on the `-search.maxConcurrentRequests`.
- goroutines now have the chance to access available connections while
awaiting the dial limit token.

Related PR:
https://github.com/VictoriaMetrics/VictoriaMetrics/pull/8922
2025-05-30 15:14:07 +02:00
Aliaksandr Valialkin
337ccd7c62 deployment: update VictoriaLogs Docker image tag from v1.23.1-victorialogs to v1.23.2-victorialogs
See https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.23.2-victorialogs
2025-05-30 00:25:23 +02:00
Aliaksandr Valialkin
c9789b3c18 docs/victorialogs/CHANGELOG.md: cut v1.23.2-victorialogs release 2025-05-30 00:21:40 +02:00
Aliaksandr Valialkin
c9db487613 app/vlselect/vmui: run make vmui-logs-update after the commit 51fdd885ea 2025-05-30 00:20:23 +02:00
Aliaksandr Valialkin
77fffb4dc7 deployment: update VictoriaLogs Docker image tag from v1.23.0-victorialogs to v1.23.1-victorialogs
See https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.23.1-victorialogs
2025-05-30 00:05:26 +02:00
Aliaksandr Valialkin
8701ec0968 docs/victorialogs/CHANGELOG.md: cut v1.23.1-victorialogs release 2025-05-29 23:58:00 +02:00
Aliaksandr Valialkin
94f3302aca lib/logstorage: properly handle stats pipe in multi-level cluster setup when a vlselect queries another vlselect, which, in turn, queries vlstorage or another vlselect
The intermediate `vlselect` should properly proxy the `stats` state from the lower-level nodes to the upper-level `vlselect`.
Previously it was finalizing the state instead of proxying it to the upper-level `vlselect, so the upper-level `vlselect`
couldn't read it.

Fix this by introducing `proxy` mode for `stats` pipe. This mode accepts state from lower-level node, aggregates the state
and then proxies it to the upper node.

Thanks to @AndrewChubatiuk for the initial attempt to fix this issue at https://github.com/VictoriaMetrics/VictoriaMetrics/pull/9023 .
Thanks to @func25 for the idea with introduction of a new `proxy` mode for `stats` pipe at https://github.com/VictoriaMetrics/VictoriaMetrics/pull/9023/files#r2107735835 ,
which has been implemented in this commit. This approach results in less code changes comparing to the approach
taken at https://github.com/VictoriaMetrics/VictoriaMetrics/pull/9023

Updates https://github.com/VictoriaMetrics/VictoriaMetrics/issues/8815
2025-05-29 20:59:05 +02:00
f41gh7
16909a2b6b lib/storage/downsampling: revert pre-filtering optimisation for downsampling rules
Skipping downsampling rules with filters based on the timestamp and offset leads to unexpected behaviour in case both rules with and without filters are present.

For example, with the following configuration: `-downsampling.period='{__name__="foo"}:60d:2m,7d:4m'`
The user would expect `foo` metrics to be downsampled only after 60d to 2m intervals. But actually pre-filter would skip scoped rule and use global rule after 7d with 4m interval.

Related issue:
https://github.com/VictoriaMetrics/VictoriaMetrics/issues/8969
2025-05-29 19:13:14 +02:00
Yury Molodov
51fdd885ea vmui/logs: fix query trigger when chart is hidden (#9006)
### Describe Your Changes

Fix an issue where queries were not triggered when relative time was
selected and the chart was hidden.
Related issue: #8983 

### Checklist

The following checks are **mandatory**:

- [ ] My change adheres to [VictoriaMetrics contributing
guidelines](https://docs.victoriametrics.com/victoriametrics/contributing/).
2025-05-29 15:43:50 +02:00
Yury Molodov
a213f5a423 vmui/logs: add sort pipe handling (#9004)
### Describe Your Changes

* UI now respects the `sort by` pipe in queries — if it's present, the
order returned by the server is preserved. Related issue: #8660.
* If no `sort by` pipe is used, logs are reversed on the client to show
the newest entries first (since VictoriaLogs returns them in ascending
time order — [see this in the
code](https://github.com/VictoriaMetrics/VictoriaMetrics/blob/master/app/vlselect/logsql/logsql.go#L1047)).
* Removed redundant client-side time-based sorting logic.

Additionally:

* Log record fields are now sorted alphabetically in UI selectors such
as **Group by field**, **Display fields**, and **Customize columns**.
Related issue: #8438.

### Checklist

The following checks are **mandatory**:

- [ ] My change adheres to [VictoriaMetrics contributing
guidelines](https://docs.victoriametrics.com/victoriametrics/contributing/).
2025-05-29 15:41:02 +02:00
Roman Khavronenko
3a812a8b28 apptest: run tests in parallel to imrpove testing speed (#9050)
This change reduces integration tests time from 90s to 30s:
1. before
https://github.com/VictoriaMetrics/VictoriaMetrics/actions/runs/15321154610/job/43105211053?pr=9048#step:5:2
2. after
https://github.com/VictoriaMetrics/VictoriaMetrics/actions/runs/15324035886/job/43114340500#step:5:2

Signed-off-by: hagen1778 <roman@victoriametrics.com>
(cherry picked from commit 70f8c60c96)
2025-05-29 15:38:35 +02:00
hagen1778
4375699013 docs: move change 53a6bbfdf8
Move change 53a6bbfdf8 to the actual
release. Before, it was mistakenly merged to prev release.

Re-classify change from BUGFIX to FEATURE due to following reasons:
* the risk of facing this issue is low, as it reveals itself only for short staleness intervals
* it slightly changes increase_pure logic in a good way. But it is still a change, not bugfix.

Signed-off-by: hagen1778 <roman@victoriametrics.com>
2025-05-29 11:40:26 +02:00
Roman Khavronenko
53a6bbfdf8 app/vmselect/promql: detect staleness between real timestamps for increase, increase_pure or delta (#9000)
This change has effect only if one of the flags below are set:
`-search.maxLookback`, `-search.setLookbackToStep` or
`-search.maxStalenessInterval`

These flags instruct query engine to ignore data points outside of the
look-behind window if these data points are beyond the staleness
interval.

This logic is used for `removeCounterResets` function, and in functions
`increase`, `increase_pure` or `delta`. The bug described in
https://github.com/VictoriaMetrics/VictoriaMetrics/issues/8935 hit the
corner case when `removeCounterResets` detected the stale series and
`increase` did not.

The reason why staleness detection failed for `increase` is that
`removeCounterResets` calculates interval between real data points. And
`realPrevValue` (that is used by those functions) calculates the
difference between look-behind window start and previous data point.
Which, at smaller gaps or smaller staleness intervals, could affect
staleness detection and make it different to `removeCounterResets`.

This change makes `realPrevValue` to acocunt for staleness between first
data point in captured look-behind window and previous data point.

-------

While there, also updated `increase_pure` logic. It was changed in
https://github.com/VictoriaMetrics/VictoriaMetrics/pull/1381 without
good explanation. Turns out, that `increase_pure` always compared last
value on the interval with value before the interval. While other
increase or delta functions did compare it with first data point on
interval, and only if it is missing - with the realPrevValue.

This change makes `increase_pure` logic consistent with other similar
function. The reason why it is not a separate PR is because tests
started to fail once `realPrevValue` callculation logic changed and
there were no good solution to isolate this change.

### 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 to [VictoriaMetrics contributing
guidelines](https://docs.victoriametrics.com/victoriametrics/contributing/).

---------

Signed-off-by: hagen1778 <roman@victoriametrics.com>
Co-authored-by: Max Kotliar <kotlyar.maksim@gmail.com>
2025-05-29 11:23:46 +02:00
Hui Wang
897f1b97e3 docs: fix cmd-line flag default values in description (#9008)
follow up
b9f080321c
2025-05-29 11:19:13 +02:00
Hui Wang
309f1898b3 alerts: fix the alerting rule ScrapePoolHasNoTargets (#9045)
as it may cause false positive in [sharding
mode](https://docs.victoriametrics.com/victoriametrics/vmagent/#scraping-big-number-of-targets)

related https://github.com/VictoriaMetrics/helm-charts/issues/2200
2025-05-29 11:17:52 +02:00
Alexander Marshalov
8998526384 docs/vmcloud: add info about api go client to the docs (#9040)
### 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 to [VictoriaMetrics contributing
guidelines](https://docs.victoriametrics.com/victoriametrics/contributing/).
2025-05-29 00:32:23 +02:00
Aliaksandr Valialkin
e55e2a4274 docs/victoriametrics/Cluster-VictoriaMetrics.md: update description for the -downsampling.period command-line flag after the commit 7dbfe1e5474b69cd1ab7cc8b5e936b6dc62d5f71 2025-05-29 00:13:45 +02:00
Aliaksandr Valialkin
29ec5d2898 all: consistently end docs.victoriametrics.com urls with /
Urls to docs.victoriametrics.com, which do not end with `/`, are working, but they lead to an unnecessary redirect to /index.html url,
which breaks backwards navigation. For example, https://docs.victoriametrics.com/victoriametrics/integrations/prometheus
redirects to https://docs.victoriametrics.com/victoriametrics/integrations/prometheus/index.html .

So it is better to consistently end all the urls to docs.victoriametrics.com with `/` in order to prevent
the unnecessary redirect and preserve backwards navigation. E.g. https://docs.victoriametrics.com/victoriametrics/integrations/prometheus
is replaced with https://docs.victoriametrics.com/victoriametrics/integrations/prometheus/ , etc.

This is a follow-up for commits starting from 6ec422160b
2025-05-29 00:05:30 +02:00
Aliaksandr Valialkin
adef9693af docs/victoriametrics-cloud: consistently use absolute links to VictoriaMetrics Cloud docs after the commit cddf36af43 2025-05-29 00:05:30 +02:00
Aliaksandr Valialkin
8f01ac42a8 docs/victoriametrics/integrations/prometheus.md: add an alias /data-ingestion/prometheus/ , since it is already used all over the Internet after the commit a46d554f74
The link to https://docs.victoriametrics.com/victoriametrics/data-ingestion/prometheus/ became borken after the commit 7d199d1d83,
which renamed the link to https://docs.victoriametrics.com/victoriametrics/integrations/prometheus .
2025-05-29 00:05:29 +02:00
Vadim Alekseev
8223a5235f deployment/logs-benchmark: fix URLs to benchmark data (#9030)
### Describe Your Changes

When downloading archives for benchmarks, an error appears saying that
the archive was placed in a new path.

The error could have been prevented by providing the `-L (--location)`
flag that would tell curl to follow the redirect, so in addition to
updating the paths, this flag was added.

### Checklist

The following checks are **mandatory**:

- [X] My change adheres to [VictoriaMetrics contributing
guidelines](https://docs.victoriametrics.com/victoriametrics/contributing/).
2025-05-28 16:39:06 +02:00
Cheyi Lin
fe5f2bd5d7 dashboards: fix newline escape in panel descriptions (#9036)
### Describe Your Changes

Fix extra newline escape characters in panel descriptions.

### Checklist

The following checks are **mandatory**:

- [x] My change adheres to [VictoriaMetrics contributing
guidelines](https://docs.victoriametrics.com/victoriametrics/contributing/).
2025-05-28 16:37:46 +02:00
Aliaksandr Valialkin
00075ac4ee deployment: update VictoriaLogs Docker image tag from v1.22.2-victorialogs to v1.23.0-victorialogs
See https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.23.0-victorialogs
2025-05-28 14:26:55 +02:00
Aliaksandr Valialkin
3f39946f99 docs/victorialogs/CHANGELOG.md: cut v1.23.0-victorialogs release 2025-05-28 14:20:21 +02:00
Aliaksandr Valialkin
1ddfd55e51 docs/victorialogs/logsql-examples.md: add an example how to get duration since the last seen log, which matches the given filter
This is a follow-up for 5bb012b67b

Updates https://github.com/VictoriaMetrics/VictoriaMetrics/issues/9013
2025-05-28 14:04:14 +02:00
Phuong Le
5bb012b67b logsql: math now() (#9014)
Resolves https://github.com/VictoriaMetrics/VictoriaMetrics/issues/9013
2025-05-28 13:43:23 +02:00
Phuong Le
78fb987bef vlstorage: automatically recover missing parts.json files on startup (#9007)
Fixes
[#8873](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/8873).

Automatically recover missing `parts.json` files on startup.
VictoriaLogs now scans existing part directories and recreates missing
`parts.json` files instead of crashing. This aligns with
VictoriaMetrics' approach.

---------

Co-authored-by: Aliaksandr Valialkin <valyala@victoriametrics.com>
2025-05-28 13:19:05 +02:00
Aliaksandr Valialkin
a0084dc223 docs/victorialogs/LogsQL.md: remove superflouos "returns" word 2025-05-27 15:56:41 +02:00
Phuong Le
f5ffbb4e00 logsql: Remove redundant suffix logic (#9022)
1. Add `!lex.isEnd()` to prevent an infinite loop. Although the current
code doesn't trigger this bug, it's a latent issue that could occur if
someone modifies the callers or adds new code paths without proper stop
tokens.
2025-05-27 14:00:37 +02:00
Jose Gómez-Sellés
13d2b0b558 docs/cloud: adapt integrations (#9032)
This PR improves integrations docs in 2 areas:
- Background set to white, avoiding issues when changing to dark mode.
- Height set to avoid blank spaces

### 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 to [VictoriaMetrics contributing
guidelines](https://docs.victoriametrics.com/victoriametrics/contributing/).
2025-05-27 10:59:47 +02:00
Evgeny
b83b2bae3b fix for multiple users running tests (testStoragePath ownership issue) (#9015)
### Describe Your Changes

When multiple users run tests on the same instance, the first user
creating a folder will own the testStoragePath, which can lead to issues
accessing this folder for other users. This change will allow us to
create unique folders per user.

```
% ls -ld /usr/tmp/vmalert-unittest/
drwxr-xr-x 2 some_user users 4096 May 12 17:22 /usr/tmp/vmalert-unittest/
...
2025-05-20T13:56:16.488Z        panic   lib/fs/fs.go:132        FATAL: cannot create directory: mkdir /usr/tmp/vmalert-unittest/1747749376488491648: permission denied
panic: FATAL: cannot create directory: mkdir /usr/tmp/vmalert-unittest/1747749376488491648: permission denied
```

### Checklist

The following checks are **mandatory**:

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

---------

Co-authored-by: Hui Wang <haley@victoriametrics.com>
2025-05-27 15:09:56 +08:00
Artem Fetishev
ef84c16f37 Bump VictoriaMetrics version mentioned in docs
Signed-off-by: Artem Fetishev <rtm@victoriametrics.com>
2025-05-26 13:49:56 +02:00
Artem Fetishev
47391fea3b deployment/docker: Bump VictoriaMetrics version
Signed-off-by: Artem Fetishev <rtm@victoriametrics.com>
2025-05-26 13:39:08 +02:00
Artem Fetishev
afce8bc320 docs: bump last LTS versions
Signed-off-by: Artem Fetishev <rtm@victoriametrics.com>
2025-05-26 13:34:53 +02:00
Artem Fetishev
5d18cd3416 docs/CHANGELOG.md: update changelog with LTS release notes
Signed-off-by: Artem Fetishev <rtm@victoriametrics.com>
2025-05-26 13:30:19 +02:00
hagen1778
65e0b3b86f deployment/dashboards: update PendingDataPoints description
Mention that data is flushed every 5s, not 2s.
Hence, expected pending data points is x5.

Signed-off-by: hagen1778 <roman@victoriametrics.com>
2025-05-26 10:58:48 +02:00
Artem Fetishev
aa3171cf4b docs/CHANGELOG.md: cut v1.118.0
Signed-off-by: Artem Fetishev <rtm@victoriametrics.com>
2025-05-23 14:40:22 +02:00
Artem Fetishev
1754ac53cd make vmui-update and make vmui-logs-update
Signed-off-by: Artem Fetishev <rtm@victoriametrics.com>
2025-05-23 11:21:53 +02:00
hagen1778
5c6af65e48 deployment/dashboards: add panels for PSI metrics
Add panels for CPU, IO and Memory pressure to vmalert, vmagent,
VictoriaMetrics single/cluster and VictoriaLogs single/cluster dashboards.

https://github.com/VictoriaMetrics/VictoriaMetrics/issues/8966
Signed-off-by: hagen1778 <roman@victoriametrics.com>
2025-05-22 16:54:52 +02:00
Phuong Le
d8871f56ba lib/logstorage/parse: fix incorrect endTime in AddTimeFilter (#8991)
Fixes https://github.com/VictoriaMetrics/VictoriaMetrics/issues/8985

When using `AddTimeFilter`, it creates a string representation with the
exact same timestamps but doesn't transform the internal end value. This
is different from the `parseFilterTime` function, which makes the
behavior of these two paths different.
2025-05-22 16:45:12 +02:00
Zakhar Bessarab
4d6bc3b5df app/vmselect/prometheus: follow-up after 60e253b
Prevent panic on instant queries when `-search.queryStats.lastQueriesCount=0` is set.

Signed-off-by: hagen1778 <roman@victoriametrics.com>
2025-05-22 15:50:02 +02:00
Zakhar Bessarab
60e253b387 app/vmselect/prometheus: prevent panic when querying with disabled query stats tracking (#8974)
Previously, querying with disabled tracking would lead to loading nil
value of execution duration. `qs.SeriesFetched` does not need similar
handling as it uses atomic.Int64 and has default value anyway.

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

Signed-off-by: Zakhar Bessarab <z.bessarab@victoriametrics.com>
2025-05-22 15:38:15 +02:00
dependabot[bot]
127d6972ac build(deps): bump react-router and react-router-dom in /app/vmui/packages/vmui (#8859)
Bumps
[react-router](https://github.com/remix-run/react-router/tree/HEAD/packages/react-router)
to 7.5.3 and updates ancestor dependency
[react-router-dom](https://github.com/remix-run/react-router/tree/HEAD/packages/react-router-dom).
These dependencies need to be updated together.

Updates `react-router` from 7.5.0 to 7.5.3
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/remix-run/react-router/releases">react-router's
releases</a>.</em></p>
<blockquote>
<h2>v7.5.3</h2>
<p>See the changelog for release notes: <a
href="https://github.com/remix-run/react-router/blob/main/CHANGELOG.md#v753">https://github.com/remix-run/react-router/blob/main/CHANGELOG.md#v753</a></p>
<h2>v7.5.2</h2>
<p>See the changelog for release notes: <a
href="https://github.com/remix-run/react-router/blob/main/CHANGELOG.md#v752">https://github.com/remix-run/react-router/blob/main/CHANGELOG.md#v752</a></p>
<h2>v7.5.1</h2>
<p>See the changelog for release notes: <a
href="https://github.com/remix-run/react-router/blob/main/CHANGELOG.md#v751">https://github.com/remix-run/react-router/blob/main/CHANGELOG.md#v751</a></p>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/remix-run/react-router/blob/main/packages/react-router/CHANGELOG.md">react-router's
changelog</a>.</em></p>
<blockquote>
<h2>7.5.3</h2>
<h3>Patch Changes</h3>
<ul>
<li>Fix bug where bubbled action errors would result in
<code>loaderData</code> being cleared at the handling
<code>ErrorBoundary</code> route (<a
href="https://redirect.github.com/remix-run/react-router/pull/13476">#13476</a>)</li>
<li>Handle redirects from <code>clientLoader.hydrate</code> initial load
executions (<a
href="https://redirect.github.com/remix-run/react-router/pull/13477">#13477</a>)</li>
</ul>
<h2>7.5.2</h2>
<h3>Patch Changes</h3>
<ul>
<li>
<p>Update Single Fetch to also handle the 204 redirects used in
<code>?_data</code> requests in Remix v2 (<a
href="https://redirect.github.com/remix-run/react-router/pull/13364">#13364</a>)</p>
<ul>
<li>This allows applications to return a redirect on <code>.data</code>
requests from outside the scope of React Router (i.e., an
<code>express</code>/<code>hono</code> middleware)</li>
<li>⚠️ Please note that doing so relies on implementation details that
are subject to change without a SemVer major release</li>
<li>This is primarily done to ease upgrading to Single Fetch for
existing Remix v2 applications, but the recommended way to handle this
is redirecting from a route middleware</li>
</ul>
</li>
<li>
<p>Adjust approach for Prerendering/SPA Mode via headers (<a
href="https://redirect.github.com/remix-run/react-router/pull/13453">#13453</a>)</p>
</li>
</ul>
<h2>7.5.1</h2>
<h3>Patch Changes</h3>
<ul>
<li>
<p>Fix single fetch bug where no revalidation request would be made when
navigating upwards to a reused parent route (<a
href="https://redirect.github.com/remix-run/react-router/pull/13253">#13253</a>)</p>
</li>
<li>
<p>When using the object-based <code>route.lazy</code> API, the
<code>HydrateFallback</code> and <code>hydrateFallbackElement</code>
properties are now skipped when lazy loading routes after hydration. (<a
href="https://redirect.github.com/remix-run/react-router/pull/13376">#13376</a>)</p>
<p>If you move the code for these properties into a separate file, you
can use this optimization to avoid downloading unused hydration code.
For example:</p>
<pre lang="ts"><code>createBrowserRouter([
  {
    path: &quot;/show/:showId&quot;,
    lazy: {
loader: async () =&gt; (await
import(&quot;./show.loader.js&quot;)).loader,
Component: async () =&gt; (await
import(&quot;./show.component.js&quot;)).Component,
      HydrateFallback: async () =&gt;
(await import(&quot;./show.hydrate-fallback.js&quot;)).HydrateFallback,
    },
  },
]);
</code></pre>
</li>
<li>
<p>Properly revalidate prerendered paths when param values change (<a
href="https://redirect.github.com/remix-run/react-router/pull/13380">#13380</a>)</p>
</li>
<li>
<p>UNSTABLE: Add a new <code>unstable_runClientMiddleware</code>
argument to <code>dataStrategy</code> to enable middleware execution in
custom <code>dataStrategy</code> implementations (<a
href="https://redirect.github.com/remix-run/react-router/pull/13395">#13395</a>)</p>
</li>
<li>
<p>UNSTABLE: Add better error messaging when <code>getLoadContext</code>
is not updated to return a <code>Map</code>&quot; (<a
href="https://redirect.github.com/remix-run/react-router/pull/13242">#13242</a>)</p>
</li>
<li>
<p>Do not automatically add <code>null</code> to
<code>staticHandler.query()</code> <code>context.loaderData</code> if
routes do not have loaders (<a
href="https://redirect.github.com/remix-run/react-router/pull/13223">#13223</a>)</p>
</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="9a41029f58"><code>9a41029</code></a>
chore: Update version for release (<a
href="https://github.com/remix-run/react-router/tree/HEAD/packages/react-router/issues/13482">#13482</a>)</li>
<li><a
href="945295b711"><code>945295b</code></a>
chore: Update version for release (pre) (<a
href="https://github.com/remix-run/react-router/tree/HEAD/packages/react-router/issues/13479">#13479</a>)</li>
<li><a
href="501d554cba"><code>501d554</code></a>
Handle redirects from hydrating clientLoaders (<a
href="https://github.com/remix-run/react-router/tree/HEAD/packages/react-router/issues/13477">#13477</a>)</li>
<li><a
href="2a128f1b91"><code>2a128f1</code></a>
Fix cleared loaderData bug on thrown action errors (<a
href="https://github.com/remix-run/react-router/tree/HEAD/packages/react-router/issues/13476">#13476</a>)</li>
<li><a
href="5819e0c45d"><code>5819e0c</code></a>
chore: Update version for release (<a
href="https://github.com/remix-run/react-router/tree/HEAD/packages/react-router/issues/13456">#13456</a>)</li>
<li><a
href="d0cac3395f"><code>d0cac33</code></a>
chore: Update version for release (pre) (<a
href="https://github.com/remix-run/react-router/tree/HEAD/packages/react-router/issues/13454">#13454</a>)</li>
<li><a
href="c84302972a"><code>c843029</code></a>
Adjust approach for prerendering/SPA mode via headers (<a
href="https://github.com/remix-run/react-router/tree/HEAD/packages/react-router/issues/13453">#13453</a>)</li>
<li><a
href="8e4963faec"><code>8e4963f</code></a>
Restore handling of 204 &quot;soft&quot; redirects on data requests (<a
href="https://github.com/remix-run/react-router/tree/HEAD/packages/react-router/issues/13364">#13364</a>)</li>
<li><a
href="ed77157ed5"><code>ed77157</code></a>
update session documentation links (<a
href="https://github.com/remix-run/react-router/tree/HEAD/packages/react-router/issues/13448">#13448</a>)</li>
<li><a
href="4281172339"><code>4281172</code></a>
Missed refactor updates</li>
<li>Additional commits viewable in <a
href="https://github.com/remix-run/react-router/commits/react-router@7.5.3/packages/react-router">compare
view</a></li>
</ul>
</details>
<br />

Updates `react-router-dom` from 7.5.0 to 7.5.3
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/remix-run/react-router/releases">react-router-dom's
releases</a>.</em></p>
<blockquote>
<h2>react-router-dom-v5-compat@6.4.0-pre.15</h2>
<h3>Patch Changes</h3>
<ul>
<li>Updated dependencies
<ul>
<li>react-router@6.4.0-pre.15</li>
<li>react-router-dom@6.4.0-pre.15</li>
</ul>
</li>
</ul>
<h2>react-router-dom-v5-compat@6.4.0-pre.11</h2>
<h3>Patch Changes</h3>
<ul>
<li>Updated dependencies
<ul>
<li>react-router@6.4.0-pre.11</li>
<li>react-router-dom@6.4.0-pre.11</li>
</ul>
</li>
</ul>
<h2>react-router-dom-v5-compat@6.4.0-pre.10</h2>
<h3>Patch Changes</h3>
<ul>
<li>Updated dependencies
<ul>
<li>react-router@6.4.0-pre.10</li>
<li>react-router-dom@6.4.0-pre.10</li>
</ul>
</li>
</ul>
<h2>react-router-dom-v5-compat@6.4.0-pre.9</h2>
<h3>Patch Changes</h3>
<ul>
<li>Updated dependencies
<ul>
<li>react-router@6.4.0-pre.9</li>
<li>react-router-dom@6.4.0-pre.9</li>
</ul>
</li>
</ul>
<h2>react-router-dom-v5-compat@6.4.0-pre.8</h2>
<h3>Patch Changes</h3>
<ul>
<li>Updated dependencies
<ul>
<li>react-router@6.4.0-pre.8</li>
<li>react-router-dom@6.4.0-pre.8</li>
</ul>
</li>
</ul>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/remix-run/react-router/blob/main/packages/react-router-dom/CHANGELOG.md">react-router-dom's
changelog</a>.</em></p>
<blockquote>
<h2>7.5.3</h2>
<h3>Patch Changes</h3>
<ul>
<li>Updated dependencies:
<ul>
<li><code>react-router@7.5.3</code></li>
</ul>
</li>
</ul>
<h2>7.5.2</h2>
<h3>Patch Changes</h3>
<ul>
<li>Updated dependencies:
<ul>
<li><code>react-router@7.5.2</code></li>
</ul>
</li>
</ul>
<h2>7.5.1</h2>
<h3>Patch Changes</h3>
<ul>
<li>Updated dependencies:
<ul>
<li><code>react-router@7.5.1</code></li>
</ul>
</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="9a41029f58"><code>9a41029</code></a>
chore: Update version for release (<a
href="https://github.com/remix-run/react-router/tree/HEAD/packages/react-router-dom/issues/13482">#13482</a>)</li>
<li><a
href="945295b711"><code>945295b</code></a>
chore: Update version for release (pre) (<a
href="https://github.com/remix-run/react-router/tree/HEAD/packages/react-router-dom/issues/13479">#13479</a>)</li>
<li><a
href="5819e0c45d"><code>5819e0c</code></a>
chore: Update version for release (<a
href="https://github.com/remix-run/react-router/tree/HEAD/packages/react-router-dom/issues/13456">#13456</a>)</li>
<li><a
href="d0cac3395f"><code>d0cac33</code></a>
chore: Update version for release (pre) (<a
href="https://github.com/remix-run/react-router/tree/HEAD/packages/react-router-dom/issues/13454">#13454</a>)</li>
<li><a
href="5dd7c1580f"><code>5dd7c15</code></a>
chore: Update version for release (<a
href="https://github.com/remix-run/react-router/tree/HEAD/packages/react-router-dom/issues/13422">#13422</a>)</li>
<li><a
href="6ce4a79774"><code>6ce4a79</code></a>
chore: Update version for release (pre) (<a
href="https://github.com/remix-run/react-router/tree/HEAD/packages/react-router-dom/issues/13412">#13412</a>)</li>
<li>See full diff in <a
href="https://github.com/remix-run/react-router/commits/react-router-dom@7.5.3/packages/react-router-dom">compare
view</a></li>
</ul>
</details>
<br />


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)
You can disable automated security fix PRs for this repo from the
[Security Alerts
page](https://github.com/VictoriaMetrics/VictoriaMetrics/network/alerts).

</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-22 15:24:52 +02:00
Andrii Chubatiuk
6d7d22f3e6 deployment/docker/victorialogs: add grafana alloy setup (#8908)
### 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 to [VictoriaMetrics contributing
guidelines](https://docs.victoriametrics.com/victoriametrics/contributing/).

---------

Signed-off-by: hagen1778 <roman@victoriametrics.com>
Co-authored-by: hagen1778 <roman@victoriametrics.com>
2025-05-22 15:24:29 +02:00
Komei Kamiya
6a4757ad06 deployment: fix compose.yml for victorialogs fluentbit otlp demo (#8987)
### Describe Your Changes

compose.yml does not exist, only compose-base.yml

### Checklist

The following checks are **mandatory**:

- [x] My change adheres to [VictoriaMetrics contributing
guidelines](https://docs.victoriametrics.com/victoriametrics/contributing/).
2025-05-22 15:00:05 +02:00
Dima Shur
ff0632c01e docs: changed typo in histogram_bucket - metricsQL query was in wrong place (#8999)
### Describe Your Changes

MetricsQL code was in wrong place - moved it 5 lines higher

### Checklist

The following checks are **mandatory**:

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

---------

Signed-off-by: hagen1778 <roman@victoriametrics.com>
Co-authored-by: hagen1778 <roman@victoriametrics.com>
2025-05-22 14:49:10 +02:00
Phuong Le
134501bf99 docs/relabeling: improve readability (#8633)
This commit re-fines the relabeling cookbok and moves all
relabeling related docs to the same page. 
It also removes duplicated information from vmagent readme.

Signed-off-by: hagen1778 <roman@victoriametrics.com>
Co-authored-by: hagen1778 <roman@victoriametrics.com>
2025-05-22 14:46:06 +02:00
Hui Wang
faa3943a25 deployment-docker: fix vmalert -remoteWrite.url arg (#8986)
vmalert auto adds `/api/v1/write` if `remoteWrite.disablePathAppend` is not specified
2025-05-22 10:47:12 +08:00
Artem Fetishev
b30f4ca12a Release-Guide.md: Update link to enterprise release guide and remove Public Announcement section (#8978)
The link to enterprise release guide now points to the doc in
enterprise-single-node branch instead of enterprise master. This is
because we don't use enterprise master.

Additionally the `Public Announcement` section has been removed because
we don't make public announcements for releases.

Signed-off-by: Artem Fetishev <rtm@victoriametrics.com>
2025-05-21 11:16:17 +02:00
Zakhar Bessarab
208515dc38 lib/promscrape/discovery/k8s: fix inconsistency with Prometheus for endpointslice discovery
Add missing labels for:
- node name
- endpoint "serving" and "terminating" states

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

Signed-off-by: Zakhar Bessarab <z.bessarab@victoriametrics.com>
2025-05-19 17:28:53 +04:00
Jose Gómez-Sellés
9de3e80a4f Docs/cloud: complete deployments section (#8928)
### Describe Your Changes

As planned, we are adding a more structured way of explaining
deployments and tiers.
In this PR the following changes are added:
- The previous tiering section is moved under the deployments
placeholder
- Explanations for users to pick single or cluster 
- Explanations for different parameters
- Re-styling of the docs to look more appealing
- Reorder some hanging docs so the sections are more clearly presented
to the users

### Checklist

The following checks are **mandatory**:

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

---------

Co-authored-by: Alexander Marshalov <_@marshalov.org>
2025-05-19 10:15:45 +02:00
hagen1778
63d8d0c5ac docs: remove link to #filtering as it has conflicts in vmctl doc
Apparently, when the doc was created for vmctl the anchor conflicts
weren't accounted for or weren't a thing yet. Now, various migration modes
have conflicting anchors. This should be addressed in follow-up commits.

Signed-off-by: hagen1778 <roman@victoriametrics.com>
2025-05-16 21:59:26 +02:00
hagen1778
6b485c4e46 docs: revise Prometheus migration section in vmctl
Improve wording and instructions. It has been a while since the last
time we updated it (more than 4 years!).

Signed-off-by: hagen1778 <roman@victoriametrics.com>
2025-05-16 21:47:45 +02:00
GitHub Snyk Bot
723e56ac50 [Snyk] Security upgrade vite from 6.2.6 to 6.2.7 (#8866)
![snyk-top-banner](https://res.cloudinary.com/snyk/image/upload/r-d/scm-platform/snyk-pull-requests/pr-banner-default.svg)

### Snyk has created this PR to fix 1 vulnerabilities in the npm
dependencies of this project.

#### Snyk changed the following file(s):

- `app/vmui/packages/vmui/package.json`
- `app/vmui/packages/vmui/package-lock.json`




#### Vulnerabilities that will be fixed with an upgrade:

|  | Issue | Score | 

:-------------------------:|:-------------------------|:-------------------------
![medium
severity](https://res.cloudinary.com/snyk/image/upload/w_20,h_20/v1561977819/icon/m.png
'medium severity') | Directory Traversal
<br/>[SNYK-JS-VITE-9919777](https://snyk.io/vuln/SNYK-JS-VITE-9919777) |
&nbsp;&nbsp;**693**&nbsp;&nbsp;




---

> [!IMPORTANT]
>
> - Check the changes in this PR to ensure they won't cause issues with
your project.
> - Max score is 1000. Note that the real score may have changed since
the PR was raised.
> - This PR was automatically created by Snyk using the credentials of a
real user.

---

**Note:** _You are seeing this because you or someone else with access
to this repository has authorized Snyk to open fix PRs._

For more information: <img
src="https://api.segment.io/v1/pixel/track?data=eyJ3cml0ZUtleSI6InJyWmxZcEdHY2RyTHZsb0lYd0dUcVg4WkFRTnNCOUEwIiwiYW5vbnltb3VzSWQiOiI1ZTlmZjBkYy01MjQ0LTQzMGYtYTc3MS0yMTY1MjdjN2Q1NjEiLCJldmVudCI6IlBSIHZpZXdlZCIsInByb3BlcnRpZXMiOnsicHJJZCI6IjVlOWZmMGRjLTUyNDQtNDMwZi1hNzcxLTIxNjUyN2M3ZDU2MSJ9fQ=="
width="0" height="0"/>
🧐 [View latest project
report](https://app.snyk.io/org/victoriametrics/project/69d9ccbb-b5b1-492f-9f8c-9032dcaf021e?utm_source&#x3D;github&amp;utm_medium&#x3D;referral&amp;page&#x3D;fix-pr)
📜 [Customise PR
templates](https://docs.snyk.io/scan-using-snyk/pull-requests/snyk-fix-pull-or-merge-requests/customize-pr-templates?utm_source=github&utm_content=fix-pr-template)
🛠 [Adjust project
settings](https://app.snyk.io/org/victoriametrics/project/69d9ccbb-b5b1-492f-9f8c-9032dcaf021e?utm_source&#x3D;github&amp;utm_medium&#x3D;referral&amp;page&#x3D;fix-pr/settings)
📚 [Read about Snyk's upgrade
logic](https://docs.snyk.io/scan-with-snyk/snyk-open-source/manage-vulnerabilities/upgrade-package-versions-to-fix-vulnerabilities?utm_source=github&utm_content=fix-pr-template)

---

**Learn how to fix vulnerabilities with free interactive lessons:**

🦉 [Directory
Traversal](https://learn.snyk.io/lesson/directory-traversal/?loc&#x3D;fix-pr)

[//]: #
'snyk:metadata:{"customTemplate":{"variablesUsed":[],"fieldsUsed":[]},"dependencies":[{"name":"vite","from":"6.2.6","to":"6.2.7"}],"env":"prod","issuesToFix":["SNYK-JS-VITE-9919777"],"prId":"5e9ff0dc-5244-430f-a771-216527c7d561","prPublicId":"5e9ff0dc-5244-430f-a771-216527c7d561","packageManager":"npm","priorityScoreList":[693],"projectPublicId":"69d9ccbb-b5b1-492f-9f8c-9032dcaf021e","projectUrl":"https://app.snyk.io/org/victoriametrics/project/69d9ccbb-b5b1-492f-9f8c-9032dcaf021e?utm_source=github&utm_medium=referral&page=fix-pr","prType":"fix","templateFieldSources":{"branchName":"default","commitMessage":"default","description":"default","title":"default"},"templateVariants":["updated-fix-title","priorityScore"],"type":"auto","upgrade":["SNYK-JS-VITE-9919777"],"vulns":["SNYK-JS-VITE-9919777"],"patch":[],"isBreakingChange":false,"remediationStrategy":"vuln"}'

Co-authored-by: snyk-bot <snyk-bot@snyk.io>
2025-05-16 16:07:28 +02:00
Andrii Chubatiuk
0a4f7e0958 app/vmui: add command to run vmui with victoriametrics playground env (#8927)
### Describe Your Changes

added command to run vmui locally backed by vm playground

### Checklist

The following checks are **mandatory**:

- [ ] My change adheres to [VictoriaMetrics contributing
guidelines](https://docs.victoriametrics.com/victoriametrics/contributing/).
2025-05-16 16:07:07 +02:00
Artur Minchukou
9eb6796bad app/vmui/logs: properly escape special characters in field values shown in autocomplete suggestions
### Describe Your Changes

Related issue: #8925 

Properly escaped special characters in field values shown in
autocomplete suggestions.

### Checklist

The following checks are **mandatory**:

- [ ] My change adheres to [VictoriaMetrics contributing
guidelines](https://docs.victoriametrics.com/victoriametrics/contributing/).
2025-05-16 16:02:20 +02:00
Artem Fetishev
1916f5be4b lib/storage: make the test pass on systems with 1 CPU (#8949)
The following test produces duplicate per-day index records on a system
with 1 CPU even when data inserted sequentially:

```
GOEXPERIMENT=synctest taskset -c 0 go test ./lib/storage -run=TestStorageAddRowsForVariousDataPatternsConcurrently/perDayIndexes/serial/sameBatchMetrics/sameRowMetrics/sameBatchDates/diffRowDates
```

See: #8654

Make this test pass by relaxing got and want data equality requirement
if the number of CPUs is 1. This is temporary until one insertion corner
case is fixed:
https://github.com/VictoriaMetrics/VictoriaMetrics/issues/8948

### 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 to [VictoriaMetrics contributing
guidelines](https://docs.victoriametrics.com/victoriametrics/contributing/).

Signed-off-by: Artem Fetishev <rtm@victoriametrics.com>
2025-05-16 15:54:11 +02:00
hagen1778
4207cb8450 docs: add trailing / to links
See comment 266bceaffd (r157230824)

Signed-off-by: hagen1778 <roman@victoriametrics.com>
2025-05-16 15:46:37 +02:00
Artur Minchukou
231bfcf4cf app/vmui: add live tailing tab to Victoria Logs (#8882)
### Describe Your Changes

Related issue: #7046 

Changes:
- add JSX automatic runtime import of React, you don't need to import
React anymore in JSX files.
 - add unused imports eslint rule
- add headers to ProcessLiveTailRequest to enable client-side connection
setup
- refactor ExploreLogs: divided the component into several separate
components for better readability and code maintenance
 - add live tailing tab to VictoriaLogs

short demo:


https://github.com/user-attachments/assets/3e5f57ee-8e72-4835-9fc6-35c6f38bc9ef



### Checklist

The following checks are **mandatory**:

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

---------

Signed-off-by: hagen1778 <roman@victoriametrics.com>
Co-authored-by: hagen1778 <roman@victoriametrics.com>
2025-05-16 15:37:55 +02:00
Max Kotliar
03ceeb7211 docs: capitalize "Enterprise" in VictoriaMetrics Enterprise phrase (#8950)
### Describe Your Changes

Capitalize "Enterprise" in VictoriaMetrics Enterprise phrase

### Checklist

The following checks are **mandatory**:

- [ ] My change adheres to [VictoriaMetrics contributing
guidelines](https://docs.victoriametrics.com/victoriametrics/contributing/).
2025-05-16 15:17:01 +02:00
Max Kotliar
118a322aa4 docs/victoriametrics/changelog: Improve relabel bug fix changelog message (#8951)
### Describe Your Changes

 Improve relabel bug fix changelog message

### Checklist

The following checks are **mandatory**:

- [ ] My change adheres to [VictoriaMetrics contributing
guidelines](https://docs.victoriametrics.com/victoriametrics/contributing/).
2025-05-16 15:13:05 +02:00
Hui Wang
454ad7a1b4 alerts: improve disk full estimation (#8955)
enhance alerting rule `DiskRunsOutOfSpaceIn3Days` and
`NodeBecomesReadonlyIn3Days` to account for
[deduplication](https://docs.victoriametrics.com/#deduplication) and
[indexDB](https://docs.victoriametrics.com/#indexdb) when calculating
disk consumption rate.

And try bring the `Storage full ETA` panel back.

---------

Signed-off-by: hagen1778 <roman@victoriametrics.com>
Co-authored-by: hagen1778 <roman@victoriametrics.com>
2025-05-16 15:11:12 +02:00
hagen1778
266bceaffd docs: demote links used for backward compatibility
Move anchors that were kept only for backward-compatibility reasons
to the bottom of the document. So they don't take extra space in main doc.

Demote anchor level to `h6`, so docs engine will stop rendering in the
right navigation column.

In this way, we still keep the backward compatibility: old links will
continue working. And free space occupied by these link in the main doc.
Thanks to @makasim for the idea.

Signed-off-by: hagen1778 <roman@victoriametrics.com>
2025-05-16 15:06:55 +02:00
Hui Wang
60322ed491 vmalert: drop duplicate labels when they appear in both the recording… (#8957)
… rule label spec and the expression result

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

---------

Signed-off-by: hagen1778 <roman@victoriametrics.com>
Co-authored-by: hagen1778 <roman@victoriametrics.com>
2025-05-16 13:47:54 +02:00
Andrii Chubatiuk
18f4c1f646 app/vlinsert: return HTTP 202 on successfully ingested Datadog logs (#8958)
### Describe Your Changes

fixes https://github.com/VictoriaMetrics/VictoriaMetrics/issues/8956

### Checklist

The following checks are **mandatory**:

- [ ] My change adheres to [VictoriaMetrics contributing
guidelines](https://docs.victoriametrics.com/victoriametrics/contributing/).
2025-05-16 13:38:46 +02:00
hagen1778
45e6491a8e docs: mention where known issues were resolved
* mention which release got the bugfix
* unify `known issues` message

Signed-off-by: hagen1778 <roman@victoriametrics.com>
2025-05-16 13:35:35 +02:00
f41gh7
0108d5777c docs: release follow-up
* mention new app versions
* add lts releases changelog
2025-05-15 17:40:59 +02:00
Max Kotliar
4fabb459aa changelog: add advice to skip affected versions
Add a note to skip the latest releases, because of the bug in vmagent.
Fixed in https://github.com/VictoriaMetrics/VictoriaMetrics/pull/8941
2025-05-15 17:33:49 +02:00
f41gh7
75623173d4 make linter happy after e2ddf2ba52
Signed-off-by: f41gh7 <nik@victoriametrics.com>
2025-05-15 12:51:56 +02:00
f41gh7
f26f84cc4f CHANGELOG.md: cut v1.117.1 release 2025-05-15 12:30:49 +02:00
Andrii Chubatiuk
d68d0b67ca app/vmagent: fixed typo at relabel config reloading
Commit 3b84f45e0a introduce a typo at `relabelConfigs.IsSet` function. It incorrectly returned value if relabeling configuration is set or not.
As a result, vmagent was not able to properly perform relabel configuration reload.
And incorrectly exposed metrics for reload configuration.

Related issue:
https://github.com/VictoriaMetrics/helm-charts/issues/2119
2025-05-15 12:26:39 +02:00
smallpath
e2ddf2ba52 lib/promauth: properly reload config on headers changes
Previously, headers hash calculation had a typo, instead of `hash.Write` - `hash.Sum` method was used.
It discards any previous writes and as a result digest always had the same value. It prevented from proper config reload.

 This commit fixes this typo and properly calculates headers digest. 

Related issue:
https://github.com/VictoriaMetrics/VictoriaMetrics/issues/8931
2025-05-15 12:14:22 +02:00
Phuong Le
21c06e86db net/http: close the body
Close the body to avoid leaking goroutines
2025-05-14 11:07:54 +02:00
Andrii Chubatiuk
93eaea9754 lib/protoparser/datadogsketches: fixed duplications in datadog sketches aggregations
Previously, due to bug at parsing logic sketches values were duplicated.

This commit properly parsers sketches and correctly converts it to time series labels.

Related issue:
https://github.com/VictoriaMetrics/VictoriaMetrics/issues/8836
2025-05-14 10:50:39 +02:00
Vadim Alekseev
d1d8be8d9b app/vlinsert: handle nested fields in the otel ingestion handle
Previously, OTL attributes fields with KeyValue type were ingested as a single json formated field. It complicates requests and requires extra effort at query time.

 This commit adds support for handling nested fields to match the behavior of
other handlers, such as `/jsonline`. KeyValue attribute will be converted into separate field.


Related issue:
https://github.com/VictoriaMetrics/VictoriaMetrics/issues/8862
2025-05-14 10:46:16 +02:00
Zakhar Bessarab
b9b0b73d29 make/docker: stop updating :latest and :stable tags
Related issue:
https://github.com/VictoriaMetrics/VictoriaMetrics/issues/7336
2025-05-14 10:43:07 +02:00
Hui Wang
6dfd7fb518 docs: minor fix
https://docs.victoriametrics.com/victoriametrics/cluster-victoriametrics/#how-to-delete-time-series
doesn't exist.
2025-05-14 10:42:11 +02:00
Artem Fetishev
fde196c86e docs/Release-Guide.md: Fix formatting and outdated instructions (#8932)
Signed-off-by: Artem Fetishev <rtm@victoriametrics.com>
2025-05-13 13:47:35 +02:00
hagen1778
6c08fae64f docs: mention how to set alert source example in docker
Signed-off-by: hagen1778 <roman@victoriametrics.com>
2025-05-12 22:59:47 +02:00
Hui Wang
cdaf83247c docker-deployment: update vmalert external.alert.source flag (#8926)
follow up
4de8113f39
2025-05-12 22:58:45 +02:00
Artem Fetishev
c1911a00de docs: Bump VictoriaMetrics version mentioned in docs
Signed-off-by: Artem Fetishev <rtm@victoriametrics.com>
2025-05-12 14:40:10 +02:00
Artem Fetishev
56e3568492 deployment: Bump VictoriaMetrics version at deployment/docker/*.yml
Signed-off-by: Artem Fetishev <rtm@victoriametrics.com>
2025-05-12 14:28:12 +02:00
Artem Fetishev
19d3c44391 docs: bump last LTS versions
Signed-off-by: Artem Fetishev <rtm@victoriametrics.com>
2025-05-12 12:34:38 +02:00
Artem Fetishev
ab15ffcf3a docs/CHANGELOG.md: update changelog with LTS release notes
Signed-off-by: Artem Fetishev <rtm@victoriametrics.com>
2025-05-12 12:30:29 +02:00
878 changed files with 41121 additions and 101764 deletions

View File

@@ -6,4 +6,4 @@ Please provide a brief description of the changes you made. Be as specific as po
The following checks are **mandatory**:
- [ ] My change adheres to [VictoriaMetrics contributing guidelines](https://docs.victoriametrics.com/victoriametrics/contributing/).
- [ ] My change adheres to [VictoriaMetrics contributing guidelines](https://docs.victoriametrics.com/victoriametrics/contributing/#pull-request-checklist).

View File

@@ -5,7 +5,6 @@ MAKE_PARALLEL := $(MAKE) -j $(MAKE_CONCURRENCY)
DATEINFO_TAG ?= $(shell date -u +'%Y%m%d-%H%M%S')
BUILDINFO_TAG ?= $(shell echo $$(git describe --long --all | tr '/' '-')$$( \
git diff-index --quiet HEAD -- || echo '-dirty-'$$(git diff-index -u HEAD | openssl sha1 | cut -d' ' -f2 | cut -c 1-8)))
LATEST_TAG ?= latest
PKG_TAG ?= $(shell git tag -l --points-at HEAD)
ifeq ($(PKG_TAG),)
@@ -196,12 +195,31 @@ vmutils-crossbuild: \
vmutils-openbsd-amd64 \
vmutils-windows-amd64
publish-latest:
PKG_TAG=$(TAG) APP_NAME=victoria-metrics $(MAKE) publish-via-docker-latest && \
PKG_TAG=$(TAG) APP_NAME=vmagent $(MAKE) publish-via-docker-latest && \
PKG_TAG=$(TAG) APP_NAME=vmalert $(MAKE) publish-via-docker-latest && \
PKG_TAG=$(TAG) APP_NAME=vmalert-tool $(MAKE) publish-via-docker-latest && \
PKG_TAG=$(TAG) APP_NAME=vmauth $(MAKE) publish-via-docker-latest && \
PKG_TAG=$(TAG) APP_NAME=vmbackup $(MAKE) publish-via-docker-latest && \
PKG_TAG=$(TAG) APP_NAME=vmrestore $(MAKE) publish-via-docker-latest && \
PKG_TAG=$(TAG) APP_NAME=vmctl $(MAKE) publish-via-docker-latest && \
PKG_TAG=$(TAG)-cluster APP_NAME=vminsert $(MAKE) publish-via-docker-latest && \
PKG_TAG=$(TAG)-cluster APP_NAME=vmselect $(MAKE) publish-via-docker-latest && \
PKG_TAG=$(TAG)-cluster APP_NAME=vmstorage $(MAKE) publish-via-docker-latest && \
PKG_TAG=$(TAG)-enterprise APP_NAME=vmgateway $(MAKE) publish-via-docker-latest
PKG_TAG=$(TAG)-enterprise APP_NAME=vmbackupmanager $(MAKE) publish-via-docker-latest
publish-victoria-logs-latest:
PKG_TAG=$(TAG) APP_NAME=victoria-logs $(MAKE) publish-via-docker-latest
PKG_TAG=$(TAG) APP_NAME=vlogscli $(MAKE) publish-via-docker-latest
publish-release:
rm -rf bin/*
git checkout $(TAG) && $(MAKE) release && LATEST_TAG=stable $(MAKE) publish && \
git checkout $(TAG)-cluster && $(MAKE) release && LATEST_TAG=cluster-stable $(MAKE) publish && \
git checkout $(TAG)-enterprise && $(MAKE) release && LATEST_TAG=enterprise-stable $(MAKE) publish && \
git checkout $(TAG)-enterprise-cluster && $(MAKE) release && LATEST_TAG=enterprise-cluster-stable $(MAKE) publish
git checkout $(TAG) && $(MAKE) release && $(MAKE) publish && \
git checkout $(TAG)-cluster && $(MAKE) release && $(MAKE) publish && \
git checkout $(TAG)-enterprise && $(MAKE) release && $(MAKE) publish && \
git checkout $(TAG)-enterprise-cluster && $(MAKE) release && $(MAKE) publish
release:
$(MAKE_PARALLEL) \
@@ -510,8 +528,6 @@ vet:
check-all: fmt vet golangci-lint govulncheck
clean-checkers: remove-golangci-lint remove-govulncheck
test:
GOEXPERIMENT=synctest go test ./lib/... ./app/...
@@ -527,7 +543,7 @@ test-full:
test-full-386:
GOEXPERIMENT=synctest GOARCH=386 go test -coverprofile=coverage.txt -covermode=atomic ./lib/... ./app/...
integration-test: victoria-metrics vmagent vmalert vmauth
integration-test: victoria-metrics vmagent vmalert vmauth vmctl
go test ./apptest/... -skip="^TestCluster.*"
benchmark:
@@ -556,12 +572,11 @@ app-local-goos-goarch:
app-local-windows-goarch:
CGO_ENABLED=0 GOOS=windows GOARCH=$(GOARCH) go build $(RACE) -ldflags "$(GO_BUILDINFO)" -o bin/$(APP_NAME)-windows-$(GOARCH)$(RACE).exe $(PKG_PREFIX)/app/$(APP_NAME)
quicktemplate-gen: install-qtc
qtc
install-qtc:
which qtc || go install github.com/valyala/quicktemplate/qtc@latest
quicktemplate-gen:
go tool qtc
golangci-lint:
GOEXPERIMENT=synctest go tool golangci-lint run
golangci-lint: install-golangci-lint
GOEXPERIMENT=synctest golangci-lint run

View File

@@ -40,16 +40,16 @@ VictoriaMetrics is optimized for timeseries data, even when old time series are
* **Easy to setup**: No dependencies, single [small binary](https://medium.com/@valyala/stripping-dependency-bloat-in-victoriametrics-docker-image-983fb5912b0d), configuration through command-line flags, but the default is also fine-tuned; backup and restore with [instant snapshots](https://medium.com/@valyala/how-victoriametrics-makes-instant-snapshots-for-multi-terabyte-time-series-data-e1f3fb0e0282).
* **Global query view**: Multiple Prometheus instances or any other data sources may ingest data into VictoriaMetrics and queried via a single query.
* **Various Protocols**: Support metric scraping, ingestion and backfilling in various protocol.
* [Prometheus exporters](https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/#how-to-scrape-prometheus-exporters-such-as-node-exporter), [Prometheus remote write API](https://docs.victoriametrics.com/victoriametrics/integrations/prometheus), [Prometheus exposition format](https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/#how-to-import-data-in-prometheus-exposition-format).
* [InfluxDB line protocol](https://docs.victoriametrics.com/victoriametrics/integrations/influxdb) over HTTP, TCP and UDP.
* [Prometheus exporters](https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/#how-to-scrape-prometheus-exporters-such-as-node-exporter), [Prometheus remote write API](https://docs.victoriametrics.com/victoriametrics/integrations/prometheus/), [Prometheus exposition format](https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/#how-to-import-data-in-prometheus-exposition-format).
* [InfluxDB line protocol](https://docs.victoriametrics.com/victoriametrics/integrations/influxdb/) over HTTP, TCP and UDP.
* [Graphite plaintext protocol](https://docs.victoriametrics.com/victoriametrics/integrations/graphite/#ingesting) with [tags](https://graphite.readthedocs.io/en/latest/tags.html#carbon).
* [OpenTSDB put message](https://docs.victoriametrics.com/victoriametrics/integrations/opentsdb#sending-data-via-telnet).
* [HTTP OpenTSDB /api/put requests](https://docs.victoriametrics.com/victoriametrics/integrations/opentsdb#sending-data-via-http).
* [OpenTSDB put message](https://docs.victoriametrics.com/victoriametrics/integrations/opentsdb/#sending-data-via-telnet).
* [HTTP OpenTSDB /api/put requests](https://docs.victoriametrics.com/victoriametrics/integrations/opentsdb/#sending-data-via-http).
* [JSON line format](https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/#how-to-import-data-in-json-line-format).
* [Arbitrary CSV data](https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/#how-to-import-csv-data).
* [Native binary format](https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/#how-to-import-data-in-native-format).
* [DataDog agent or DogStatsD](https://docs.victoriametrics.com/victoriametrics/integrations/datadog).
* [NewRelic infrastructure agent](https://docs.victoriametrics.com/victoriametrics/integrations/newrelic#sending-data-from-agent).
* [DataDog agent or DogStatsD](https://docs.victoriametrics.com/victoriametrics/integrations/datadog/).
* [NewRelic infrastructure agent](https://docs.victoriametrics.com/victoriametrics/integrations/newrelic/#sending-data-from-agent).
* [OpenTelemetry metrics format](https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/#sending-data-via-opentelemetry).
* **NFS-based storages**: Supports storing data on NFS-based storages such as Amazon EFS, Google Filestore.
* And many other features such as metrics relabeling, cardinality limiter, etc.

View File

@@ -95,6 +95,7 @@ func datadogLogsIngestion(w http.ResponseWriter, r *http.Request) bool {
// There is no need in updating v2LogsRequestDuration for request errors,
// since their timings are usually much smaller than the timing for successful request parsing.
v2LogsRequestDuration.UpdateDuration(startTime)
w.WriteHeader(http.StatusAccepted)
fmt.Fprintf(w, `{}`)
return true
}

View File

@@ -101,9 +101,11 @@ func (lr *LineReader) readMoreData() bool {
bufLen := len(lr.buf)
if bufLen >= MaxLineSizeBytes.IntN() {
logger.Warnf("%s: the line length exceeds -insert.maxLineSizeBytes=%d; skipping it; line contents=%q", lr.name, MaxLineSizeBytes.IntN(), lr.buf)
ok, skippedBytes := lr.skipUntilNextLine()
logger.Warnf("%s: the line length exceeds -insert.maxLineSizeBytes=%d; skipping it; total skipped bytes=%d",
lr.name, MaxLineSizeBytes.IntN(), skippedBytes)
tooLongLinesSkipped.Inc()
return lr.skipUntilNextLine()
return ok
}
lr.buf = slicesutil.SetLength(lr.buf, MaxLineSizeBytes.IntN())
@@ -121,26 +123,35 @@ func (lr *LineReader) readMoreData() bool {
var tooLongLinesSkipped = metrics.NewCounter("vl_too_long_lines_skipped_total")
func (lr *LineReader) skipUntilNextLine() bool {
func (lr *LineReader) skipUntilNextLine() (bool, int) {
// Initialize skipped bytes count with MaxLineSizeBytes because
// we've already read that many bytes without encountering a newline,
// indicating the line size exceeds the maximum allowed limit.
skipSizeBytes := MaxLineSizeBytes.IntN()
for {
lr.buf = slicesutil.SetLength(lr.buf, MaxLineSizeBytes.IntN())
n, err := lr.r.Read(lr.buf)
skipSizeBytes += n
lr.buf = lr.buf[:n]
if err != nil {
if errors.Is(err, io.EOF) {
lr.eofReached = true
lr.buf = lr.buf[:0]
return true
return true, skipSizeBytes
}
lr.err = fmt.Errorf("cannot skip the current line: %s", err)
return false
return false, skipSizeBytes
}
if n := bytes.IndexByte(lr.buf, '\n'); n >= 0 {
// Include skipped bytes before \n, including the newline itself.
skipSizeBytes += n + 1 - len(lr.buf)
// Include \n in the buf, so too long line is replaced with an empty line.
// This is needed for maintaining synchorinzation consistency between lines
// in protocols such as Elasticsearch bulk import.
lr.buf = append(lr.buf[:0], lr.buf[n:]...)
return true
return true, skipSizeBytes
}
}
}

View File

@@ -1,7 +1,6 @@
package internalinsert
import (
"flag"
"fmt"
"net/http"
"time"
@@ -18,17 +17,11 @@ import (
)
var (
disableInsert = flag.Bool("internalinsert.disable", false, "Whether to disable /internal/insert HTTP endpoint")
maxRequestSize = flagutil.NewBytes("internalinsert.maxRequestSize", 64*1024*1024, "The maximum size in bytes of a single request, which can be accepted at /internal/insert HTTP endpoint")
)
// RequestHandler processes /internal/insert requests.
func RequestHandler(w http.ResponseWriter, r *http.Request) {
if *disableInsert {
httpserver.Errorf(w, r, "requests to /internal/insert are disabled with -internalinsert.disable command-line flag")
return
}
startTime := time.Now()
if r.Method != "POST" {
w.WriteHeader(http.StatusMethodNotAllowed)

View File

@@ -1,6 +1,7 @@
package vlinsert
import (
"flag"
"fmt"
"net/http"
"strings"
@@ -13,6 +14,12 @@ import (
"github.com/VictoriaMetrics/VictoriaMetrics/app/vlinsert/loki"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vlinsert/opentelemetry"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vlinsert/syslog"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/httpserver"
)
var (
disableInsert = flag.Bool("insert.disable", false, "Whether to disable /insert/* HTTP endpoints")
disableInternal = flag.Bool("internalinsert.disable", false, "Whether to disable /internal/insert HTTP endpoint")
)
// Init initializes vlinsert
@@ -27,19 +34,31 @@ func Stop() {
// RequestHandler handles insert requests for VictoriaLogs
func RequestHandler(w http.ResponseWriter, r *http.Request) bool {
path := r.URL.Path
path := strings.ReplaceAll(r.URL.Path, "//", "/")
if strings.HasPrefix(path, "/insert/") {
if *disableInsert {
httpserver.Errorf(w, r, "requests to /insert/* are disabled with -insert.disable command-line flag")
return true
}
return insertHandler(w, r, path)
}
if path == "/internal/insert" {
if *disableInternal || *disableInsert {
httpserver.Errorf(w, r, "requests to /internal/insert are disabled with -internalinsert.disable or -insert.disable command-line flag")
return true
}
internalinsert.RequestHandler(w, r)
return true
}
if !strings.HasPrefix(path, "/insert/") {
// Skip requests, which do not start with /insert/, since these aren't our requests.
return false
}
return false
}
func insertHandler(w http.ResponseWriter, r *http.Request, path string) bool {
path = strings.TrimPrefix(path, "/insert")
path = strings.ReplaceAll(path, "//", "/")
switch path {
case "/jsonline":
@@ -69,7 +88,7 @@ func RequestHandler(w http.ResponseWriter, r *http.Request) bool {
case strings.HasPrefix(path, "/datadog/"):
path = strings.TrimPrefix(path, "/datadog")
return datadog.RequestHandler(path, w, r)
default:
return false
}
return false
}

View File

@@ -12,7 +12,6 @@ import (
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logstorage"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/protoparser/opentelemetry/pb"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/protoparser/protoparserutil"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/slicesutil"
"github.com/VictoriaMetrics/metrics"
)
@@ -53,7 +52,7 @@ func handleProtobuf(r *http.Request, w http.ResponseWriter) {
err = protoparserutil.ReadUncompressedData(r.Body, encoding, maxRequestSize, func(data []byte) error {
lmp := cp.NewLogMessageProcessor("opentelelemtry_protobuf", false)
useDefaultStreamFields := len(cp.StreamFields) == 0
err := pushProtobufRequest(data, lmp, useDefaultStreamFields)
err := pushProtobufRequest(data, lmp, cp.MsgFields, useDefaultStreamFields)
lmp.MustClose()
return err
})
@@ -75,7 +74,7 @@ var (
requestProtobufDuration = metrics.NewHistogram(`vl_http_request_duration_seconds{path="/insert/opentelemetry/v1/logs",format="protobuf"}`)
)
func pushProtobufRequest(data []byte, lmp insertutil.LogMessageProcessor, useDefaultStreamFields bool) error {
func pushProtobufRequest(data []byte, lmp insertutil.LogMessageProcessor, msgFields []string, useDefaultStreamFields bool) error {
var req pb.ExportLogsServiceRequest
if err := req.UnmarshalProtobuf(data); err != nil {
errorsTotal.Inc()
@@ -84,35 +83,31 @@ func pushProtobufRequest(data []byte, lmp insertutil.LogMessageProcessor, useDef
var commonFields []logstorage.Field
for _, rl := range req.ResourceLogs {
attributes := rl.Resource.Attributes
commonFields = slicesutil.SetLength(commonFields, len(attributes))
for i, attr := range attributes {
commonFields[i].Name = attr.Key
commonFields[i].Value = attr.Value.FormatString(true)
}
commonFields = commonFields[:0]
commonFields = appendKeyValues(commonFields, rl.Resource.Attributes, "")
commonFieldsLen := len(commonFields)
for _, sc := range rl.ScopeLogs {
commonFields = pushFieldsFromScopeLogs(&sc, commonFields[:commonFieldsLen], lmp, useDefaultStreamFields)
commonFields = pushFieldsFromScopeLogs(&sc, commonFields[:commonFieldsLen], lmp, msgFields, useDefaultStreamFields)
}
}
return nil
}
func pushFieldsFromScopeLogs(sc *pb.ScopeLogs, commonFields []logstorage.Field, lmp insertutil.LogMessageProcessor, useDefaultStreamFields bool) []logstorage.Field {
func pushFieldsFromScopeLogs(sc *pb.ScopeLogs, commonFields []logstorage.Field, lmp insertutil.LogMessageProcessor, msgFields []string, useDefaultStreamFields bool) []logstorage.Field {
fields := commonFields
for _, lr := range sc.LogRecords {
fields = fields[:len(commonFields)]
fields = append(fields, logstorage.Field{
Name: "_msg",
Value: lr.Body.FormatString(true),
})
for _, attr := range lr.Attributes {
if lr.Body.KeyValueList != nil {
fields = appendKeyValues(fields, lr.Body.KeyValueList.Values, "")
logstorage.RenameField(fields[len(commonFields):], msgFields, "_msg")
} else {
fields = append(fields, logstorage.Field{
Name: attr.Key,
Value: attr.Value.FormatString(true),
Name: "_msg",
Value: lr.Body.FormatString(true),
})
}
fields = appendKeyValues(fields, lr.Attributes, "")
if len(lr.TraceID) > 0 {
fields = append(fields, logstorage.Field{
Name: "trace_id",
@@ -138,3 +133,22 @@ func pushFieldsFromScopeLogs(sc *pb.ScopeLogs, commonFields []logstorage.Field,
}
return fields
}
func appendKeyValues(fields []logstorage.Field, kvs []*pb.KeyValue, parentField string) []logstorage.Field {
for _, attr := range kvs {
fieldName := attr.Key
if parentField != "" {
fieldName = parentField + "." + fieldName
}
if attr.Value.KeyValueList != nil {
fields = appendKeyValues(fields, attr.Value.KeyValueList.Values, fieldName)
} else {
fields = append(fields, logstorage.Field{
Name: fieldName,
Value: attr.Value.FormatString(true),
})
}
}
return fields
}

View File

@@ -16,7 +16,7 @@ func TestPushProtoOk(t *testing.T) {
pData := lr.MarshalProtobuf(nil)
tlp := &insertutil.TestLogMessageProcessor{}
if err := pushProtobufRequest(pData, tlp, false); err != nil {
if err := pushProtobufRequest(pData, tlp, nil, false); err != nil {
t.Fatalf("unexpected error: %s", err)
}
@@ -88,9 +88,9 @@ func TestPushProtoOk(t *testing.T) {
},
},
[]int64{1234, 1235, 1236},
`{"logger":"context","instance_id":"10","node_taints":"{\"role\":\"dev\",\"cluster_load_percent\":0.55}","_msg":"log-line-message","severity":"Trace"}
{"logger":"context","instance_id":"10","node_taints":"{\"role\":\"dev\",\"cluster_load_percent\":0.55}","_msg":"log-line-message-msg-2","severity":"Unspecified"}
{"logger":"context","instance_id":"10","node_taints":"{\"role\":\"dev\",\"cluster_load_percent\":0.55}","_msg":"log-line-message-msg-2","severity":"Unspecified"}`,
`{"logger":"context","instance_id":"10","node_taints.role":"dev","node_taints.cluster_load_percent":"0.55","_msg":"log-line-message","severity":"Trace"}
{"logger":"context","instance_id":"10","node_taints.role":"dev","node_taints.cluster_load_percent":"0.55","_msg":"log-line-message-msg-2","severity":"Unspecified"}
{"logger":"context","instance_id":"10","node_taints.role":"dev","node_taints.cluster_load_percent":"0.55","_msg":"log-line-message-msg-2","severity":"Unspecified"}`,
)
// multi-scope with resource attributes and multi-line
@@ -136,14 +136,71 @@ func TestPushProtoOk(t *testing.T) {
},
},
[]int64{1234, 1235, 2345, 2346, 2347, 2348, 3333},
`{"logger":"context","instance_id":"10","node_taints":"{\"role\":\"dev\",\"cluster_load_percent\":0.55}","_msg":"log-line-message","severity":"Trace"}
{"logger":"context","instance_id":"10","node_taints":"{\"role\":\"dev\",\"cluster_load_percent\":0.55}","_msg":"log-line-message-msg-2","severity":"Debug"}
`{"logger":"context","instance_id":"10","node_taints.role":"dev","node_taints.cluster_load_percent":"0.55","_msg":"log-line-message","severity":"Trace"}
{"logger":"context","instance_id":"10","node_taints.role":"dev","node_taints.cluster_load_percent":"0.55","_msg":"log-line-message-msg-2","severity":"Debug"}
{"_msg":"log-line-resource-scope-1-0-0","severity":"Info2"}
{"_msg":"log-line-resource-scope-1-0-1","severity":"Info2"}
{"_msg":"log-line-resource-scope-1-1-0","severity":"Info4"}
{"_msg":"log-line-resource-scope-1-1-1","trace_id":"1234","span_id":"45","severity":"Info4"}
{"_msg":"log-line-resource-scope-1-1-2","trace_id":"4bf92f3577b34da6a3ce929d0e0e4736","span_id":"00f067aa0ba902b7","severity":"Unspecified"}`,
)
// nested fields
f([]pb.ResourceLogs{
{
ScopeLogs: []pb.ScopeLogs{
{
LogRecords: []pb.LogRecord{
{
TimeUnixNano: 1234,
Body: pb.AnyValue{StringValue: ptrTo("nested fields")},
Attributes: []*pb.KeyValue{
{Key: "error", Value: &pb.AnyValue{KeyValueList: &pb.KeyValueList{Values: []*pb.KeyValue{
{
Key: "type",
Value: &pb.AnyValue{StringValue: ptrTo("document_parsing_exception")},
},
{
Key: "reason",
Value: &pb.AnyValue{StringValue: ptrTo("failed to parse field [_msg] of type [text]")},
},
{
Key: "caused_by",
Value: &pb.AnyValue{KeyValueList: &pb.KeyValueList{Values: []*pb.KeyValue{
{
Key: "type",
Value: &pb.AnyValue{StringValue: ptrTo("x_content_parse_exception")},
},
{
Key: "reason",
Value: &pb.AnyValue{StringValue: ptrTo("unexpected end-of-input in VALUE_STRING")},
},
{
Key: "caused_by",
Value: &pb.AnyValue{KeyValueList: &pb.KeyValueList{Values: []*pb.KeyValue{
{
Key: "type",
Value: &pb.AnyValue{StringValue: ptrTo("json_e_o_f_exception")},
},
{
Key: "reason",
Value: &pb.AnyValue{StringValue: ptrTo("eof")},
},
}}},
},
}}},
},
}}}},
},
},
},
},
},
},
}, []int64{1234},
`{"_msg":"nested fields","error.type":"document_parsing_exception","error.reason":"failed to parse field [_msg] of type [text]",`+
`"error.caused_by.type":"x_content_parse_exception","error.caused_by.reason":"unexpected end-of-input in VALUE_STRING",`+
`"error.caused_by.caused_by.type":"json_e_o_f_exception","error.caused_by.caused_by.reason":"eof","severity":"Unspecified"}`)
}
func ptrTo[T any](s T) *T {

View File

@@ -27,7 +27,7 @@ func benchmarkParseProtobufRequest(b *testing.B, streams, rows, labels int) {
b.RunParallel(func(pb *testing.PB) {
body := getProtobufBody(streams, rows, labels)
for pb.Next() {
if err := pushProtobufRequest(body, blp, false); err != nil {
if err := pushProtobufRequest(body, blp, nil, false); err != nil {
panic(fmt.Errorf("unexpected error: %w", err))
}
}

View File

@@ -206,6 +206,8 @@ func generateAndPushLogs(cfg *workerConfig, workerID int) {
if err != nil {
logger.Fatalf("cannot perform request to %q: %s", cfg.url, err)
}
defer resp.Body.Close()
if resp.StatusCode/100 != 2 {
logger.Fatalf("unexpected status code got from %q: %d; want 2xx", cfg.url, err)
}

View File

@@ -2,7 +2,6 @@ package internalselect
import (
"context"
"flag"
"fmt"
"net/http"
"strconv"
@@ -22,15 +21,8 @@ import (
"github.com/VictoriaMetrics/VictoriaMetrics/lib/netutil"
)
var disableSelect = flag.Bool("internalselect.disable", false, "Whether to disable /internal/select/* HTTP endpoints")
// RequestHandler processes requests to /internal/select/*
func RequestHandler(ctx context.Context, w http.ResponseWriter, r *http.Request) {
if *disableSelect {
httpserver.Errorf(w, r, "requests to /internal/select/* are disabled with -internalselect.disable command-line flag")
return
}
startTime := time.Now()
path := r.URL.Path

View File

@@ -55,7 +55,10 @@ func ProcessFacetsRequest(ctx context.Context, w http.ResponseWriter, r *http.Re
}
keepConstFields := httputil.GetBool(r, "keep_const_fields")
// Pipes must be dropped, since it is expected facets are obtained
// from the real logs stored in the database.
q.DropAllPipes()
q.AddFacetsPipe(limit, maxValuesPerField, maxValueLen, keepConstFields)
var mLock sync.Mutex
@@ -156,8 +159,10 @@ func ProcessHitsRequest(ctx context.Context, w http.ResponseWriter, r *http.Requ
fieldsLimit = 0
}
// Prepare the query for hits count.
// Pipes must be dropped, since it is expected hits are obtained
// from the real logs stored in the database.
q.DropAllPipes()
q.AddCountByTimePipe(int64(step), int64(offset), fields)
var mLock sync.Mutex
@@ -290,6 +295,10 @@ func ProcessFieldNamesRequest(ctx context.Context, w http.ResponseWriter, r *htt
return
}
// Pipes must be dropped, since it is expected field names are obtained
// from the real logs stored in the database.
q.DropAllPipes()
// Obtain field names for the given query
fieldNames, err := vlstorage.GetFieldNames(ctx, tenantIDs, q)
if err != nil {
@@ -329,6 +338,10 @@ func ProcessFieldValuesRequest(ctx context.Context, w http.ResponseWriter, r *ht
limit = 0
}
// Pipes must be dropped, since it is expected field values are obtained
// from the real logs stored in the database.
q.DropAllPipes()
// Obtain unique values for the given field
values, err := vlstorage.GetFieldValues(ctx, tenantIDs, q, fieldName, uint64(limit))
if err != nil {
@@ -351,6 +364,10 @@ func ProcessStreamFieldNamesRequest(ctx context.Context, w http.ResponseWriter,
return
}
// Pipes must be dropped, since it is expected stream field names are obtained
// from the real logs stored in the database.
q.DropAllPipes()
// Obtain stream field names for the given query
names, err := vlstorage.GetStreamFieldNames(ctx, tenantIDs, q)
if err != nil {
@@ -389,6 +406,10 @@ func ProcessStreamFieldValuesRequest(ctx context.Context, w http.ResponseWriter,
limit = 0
}
// Pipes must be dropped, since it is expected stream field values are obtained
// from the real logs stored in the database.
q.DropAllPipes()
// Obtain stream field values for the given query and the given fieldName
values, err := vlstorage.GetStreamFieldValues(ctx, tenantIDs, q, fieldName, uint64(limit))
if err != nil {
@@ -420,6 +441,10 @@ func ProcessStreamIDsRequest(ctx context.Context, w http.ResponseWriter, r *http
limit = 0
}
// Pipes must be dropped, since it is expected stream ids are obtained
// from the real logs stored in the database.
q.DropAllPipes()
// Obtain streamIDs for the given query
streamIDs, err := vlstorage.GetStreamIDs(ctx, tenantIDs, q, uint64(limit))
if err != nil {
@@ -451,6 +476,10 @@ func ProcessStreamsRequest(ctx context.Context, w http.ResponseWriter, r *http.R
limit = 0
}
// Pipes must be dropped, since it is expected stream are obtained
// from the real logs stored in the database.
q.DropAllPipes()
// Obtain streams for the given query
streams, err := vlstorage.GetStreams(ctx, tenantIDs, q, uint64(limit))
if err != nil {
@@ -514,6 +543,11 @@ func ProcessLiveTailRequest(ctx context.Context, w http.ResponseWriter, r *http.
if !ok {
logger.Panicf("BUG: it is expected that http.ResponseWriter (%T) supports http.Flusher interface", w)
}
w.Header().Set("Content-Type", "application/x-ndjson")
w.Header().Set("Access-Control-Allow-Origin", "*")
flusher.Flush()
qOrig := q
for {
q = qOrig.CloneWithTimeFilter(end, start, end)
@@ -546,7 +580,7 @@ var liveTailRequests = metrics.NewCounter(`vl_live_tailing_requests`)
const tailOffsetNsecs = 5e9
type logRow struct {
timestamp string
timestamp int64
fields []logstorage.Field
}
@@ -562,7 +596,7 @@ type tailProcessor struct {
mu sync.Mutex
perStreamRows map[string][]logRow
lastTimestamps map[string]string
lastTimestamps map[string]int64
err error
}
@@ -572,7 +606,7 @@ func newTailProcessor(cancel func()) *tailProcessor {
cancel: cancel,
perStreamRows: make(map[string][]logRow),
lastTimestamps: make(map[string]string),
lastTimestamps: make(map[string]int64),
}
}
@@ -589,7 +623,7 @@ func (tp *tailProcessor) writeBlock(_ uint, db *logstorage.DataBlock) {
}
// Make sure columns contain _time field, since it is needed for proper tail work.
timestamps, ok := db.GetTimestamps()
timestamps, ok := db.GetTimestamps(nil)
if !ok {
tp.err = fmt.Errorf("missing _time field")
tp.cancel()
@@ -1038,9 +1072,7 @@ func getLastNQueryResults(ctx context.Context, tenantIDs []logstorage.TenantID,
}
func getLastNRows(rows []logRow, limit int) []logRow {
sort.Slice(rows, func(i, j int) bool {
return rows[i].timestamp < rows[j].timestamp
})
sortLogRows(rows)
if len(rows) > limit {
rows = rows[len(rows)-limit:]
}
@@ -1065,7 +1097,7 @@ func getQueryResultsWithLimit(ctx context.Context, tenantIDs []logstorage.Tenant
clonedColumnNames[i] = strings.Clone(c.Name)
}
timestamps, ok := db.GetTimestamps()
timestamps, ok := db.GetTimestamps(nil)
if !ok {
missingTimeColumn.Store(true)
cancel()

View File

@@ -25,6 +25,9 @@ var (
maxQueueDuration = flag.Duration("search.maxQueueDuration", 10*time.Second, "The maximum time the search request waits for execution when -search.maxConcurrentRequests "+
"limit is reached; see also -search.maxQueryDuration")
maxQueryDuration = flag.Duration("search.maxQueryDuration", time.Second*30, "The maximum duration for query execution. It can be overridden to a smaller value on a per-query basis via 'timeout' query arg")
disableSelect = flag.Bool("select.disable", false, "Whether to disable /select/* HTTP endpoints")
disableInternal = flag.Bool("internalselect.disable", false, "Whether to disable /internal/select/* HTTP endpoints")
)
func getDefaultMaxConcurrentRequests() int {
@@ -71,13 +74,31 @@ var vmuiFileServer = http.FileServer(http.FS(vmuiFiles))
// RequestHandler handles select requests for VictoriaLogs
func RequestHandler(w http.ResponseWriter, r *http.Request) bool {
path := r.URL.Path
path := strings.ReplaceAll(r.URL.Path, "//", "/")
if !strings.HasPrefix(path, "/select/") && !strings.HasPrefix(path, "/internal/select/") {
// Skip requests, which do not start with /select/, since these aren't our requests.
return false
if strings.HasPrefix(path, "/select/") {
if *disableSelect {
httpserver.Errorf(w, r, "requests to /select/* are disabled with -select.disable command-line flag")
return true
}
return selectHandler(w, r, path)
}
path = strings.ReplaceAll(path, "//", "/")
if strings.HasPrefix(path, "/internal/select/") {
if *disableInternal || *disableSelect {
httpserver.Errorf(w, r, "requests to /internal/select/* are disabled with -internalselect.disable or -select.disable command-line flag")
return true
}
internalselect.RequestHandler(r.Context(), w, r)
return true
}
return false
}
func selectHandler(w http.ResponseWriter, r *http.Request, path string) bool {
ctx := r.Context()
if path == "/select/vmui" {
// VMUI access via incomplete url without `/` in the end. Redirect to complete url.
@@ -100,7 +121,6 @@ func RequestHandler(w http.ResponseWriter, r *http.Request) bool {
return true
}
ctx := r.Context()
if path == "/select/logsql/tail" {
logsqlTailRequests.Inc()
// Process live tailing request without timeout, since it is OK to run live tailing requests for very long time.
@@ -120,13 +140,6 @@ func RequestHandler(w http.ResponseWriter, r *http.Request) bool {
}
defer decRequestConcurrency()
if strings.HasPrefix(path, "/internal/select/") {
// Process internal request from vlselect without timeout (e.g. use ctx instead of ctxWithTimeout),
// since the timeout must be controlled by the vlselect.
internalselect.RequestHandler(ctx, w, r)
return true
}
ok := processSelectRequest(ctxWithTimeout, w, r, path)
if !ok {
return false

View File

@@ -66,8 +66,8 @@ or at your own [VictoriaMetrics instance](https://docs.victoriametrics.com/victo
The list of MetricsQL features on top of PromQL:
* Graphite-compatible filters can be passed via `{__graphite__="foo.*.bar"}` syntax.
See [these docs](https://docs.victoriametrics.com/victoriametrics/integrations/graphite#selecting-graphite-metrics).
VictoriaMetrics can be used as Graphite datasource in Grafana. See [these docs](https://docs.victoriametrics.com/victoriametrics/integrations/graphite#graphite-api-usage) for details.
See [these docs](https://docs.victoriametrics.com/victoriametrics/integrations/graphite/#selecting-graphite-metrics).
VictoriaMetrics can be used as Graphite datasource in Grafana. See [these docs](https://docs.victoriametrics.com/victoriametrics/integrations/graphite/#graphite-api-usage) for details.
See also [label_graphite_group](#label_graphite_group) function, which can be used for extracting the given groups from Graphite metric name.
* Lookbehind window in square brackets for [rollup functions](#rollup-functions) may be omitted. VictoriaMetrics automatically selects the lookbehind window
depending on the `step` query arg passed to [/api/v1/query_range](https://docs.victoriametrics.com/victoriametrics/keyconcepts/#range-query)

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -35,10 +35,10 @@
<meta property="og:title" content="UI for VictoriaLogs">
<meta property="og:url" content="https://victoriametrics.com/products/victorialogs/">
<meta property="og:description" content="Explore your log data with VictoriaLogs UI">
<script type="module" crossorigin src="./assets/index-DfcWONVQ.js"></script>
<link rel="modulepreload" crossorigin href="./assets/vendor-C-vZmbyg.js">
<script type="module" crossorigin src="./assets/index-BaRvaPfA.js"></script>
<link rel="modulepreload" crossorigin href="./assets/vendor-D8IJGiEn.js">
<link rel="stylesheet" crossorigin href="./assets/vendor-D1GxaB_c.css">
<link rel="stylesheet" crossorigin href="./assets/index-Brup_hCI.css">
<link rel="stylesheet" crossorigin href="./assets/index-C85_NB5q.css">
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>

View File

@@ -248,6 +248,9 @@ func (sn *storageNode) executeRequestAt(ctx context.Context, path string, args u
if err != nil {
logger.Panicf("BUG: unexpected error when creating a request: %s", err)
}
if err := sn.ac.SetHeaders(req, true); err != nil {
return nil, fmt.Errorf("cannot set auth headers for %q: %w", reqURL, err)
}
// send the request to the storage node
resp, err := sn.c.Do(req)

View File

@@ -22,10 +22,10 @@ var (
relabelConfigPathGlobal = flag.String("remoteWrite.relabelConfig", "", "Optional path to file with relabeling configs, which are applied "+
"to all the metrics before sending them to -remoteWrite.url. See also -remoteWrite.urlRelabelConfig. "+
"The path can point either to local file or to http url. "+
"See https://docs.victoriametrics.com/victoriametrics/vmagent/#relabeling")
"See https://docs.victoriametrics.com/victoriametrics/relabeling/")
relabelConfigPaths = flagutil.NewArrayString("remoteWrite.urlRelabelConfig", "Optional path to relabel configs for the corresponding -remoteWrite.url. "+
"See also -remoteWrite.relabelConfig. The path can point either to local file or to http url. "+
"See https://docs.victoriametrics.com/victoriametrics/vmagent/#relabeling")
"See https://docs.victoriametrics.com/victoriametrics/relabeling/")
usePromCompatibleNaming = flag.Bool("usePromCompatibleNaming", false, "Whether to replace characters unsupported by Prometheus with underscores "+
"in the ingested metric names and label names. For example, foo.bar{a.b='c'} is transformed into foo_bar{a_b='c'} during data ingestion if this flag is set. "+
@@ -121,7 +121,7 @@ type relabelConfigs struct {
}
func (rcs *relabelConfigs) isSet() bool {
if rcs != nil {
if rcs == nil {
return false
}
if rcs.global.Len() > 0 {

View File

@@ -13,7 +13,6 @@ import (
"path/filepath"
"reflect"
"sort"
"strconv"
"strings"
"syscall"
"time"
@@ -99,8 +98,11 @@ func UnitTest(files []string, disableGroupLabel bool, externalLabels []string, e
}()
}
// adding time.Now().UnixNano() to avoid possible file conflict when multiple processes run on a single host
storagePath = filepath.Join(os.TempDir(), testStoragePath, strconv.FormatInt(time.Now().UnixNano(), 10))
tmpFolder, err := os.MkdirTemp(os.TempDir(), testStoragePath)
if err != nil {
logger.Fatalf("failed to create tmp dir for tests: %v", err)
}
storagePath = tmpFolder
processFlags()
vminsert.Init()
vmselect.Init()

View File

@@ -258,12 +258,18 @@ func (rr *RecordingRule) toTimeSeries(m datasource.Metric) prompbmarshal.TimeSer
Value: rr.Name,
})
}
// add extra labels configured by user
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)
existingLabel := promrelabel.GetLabelByName(m.Labels, k)
if existingLabel != nil { // there is a conflict between extra and existing label
if existingLabel.Value == rr.Labels[k] {
// extra and existing labels are identical - do nothing
continue
}
// preserve existing label by adding "exported_" prefix
existingLabel.Name = fmt.Sprintf("exported_%s", existingLabel.Name)
}
// add extra label
m.Labels = append(m.Labels, prompbmarshal.Label{
Name: k,
Value: rr.Labels[k],

View File

@@ -168,6 +168,7 @@ func TestRecordingRule_Exec(t *testing.T) {
}, [][]datasource.Metric{{
metricWithValueAndLabels(t, 2, "__name__", "foo", "job", "foo"),
metricWithValueAndLabels(t, 1, "__name__", "bar", "job", "bar", "source", "origin"),
metricWithValueAndLabels(t, 1, "__name__", "baz", "job", "baz", "source", "test"),
}}, [][]prompbmarshal.TimeSeries{{
newTimeSeries([]float64{2}, []int64{ts.UnixNano()}, []prompbmarshal.Label{
{
@@ -202,6 +203,21 @@ func TestRecordingRule_Exec(t *testing.T) {
Value: "origin",
},
}),
newTimeSeries([]float64{1}, []int64{ts.UnixNano()},
[]prompbmarshal.Label{
{
Name: "__name__",
Value: "job:foo",
},
{
Name: "job",
Value: "baz",
},
{
Name: "source",
Value: "test",
},
}),
}})
}

View File

@@ -120,6 +120,9 @@ func normalizeURL(uOrig *url.URL) *url.URL {
u := *uOrig
// Prevent from attacks with using `..` in r.URL.Path
u.Path = path.Clean(u.Path)
if u.Path == "." {
u.Path = "/"
}
if !strings.HasSuffix(u.Path, "/") && strings.HasSuffix(uOrig.Path, "/") {
// The path.Clean() removes trailing slash.
// Return it back if needed.

View File

@@ -128,7 +128,40 @@ func TestCreateTargetURLSuccess(t *testing.T) {
// Simple routing with `url_prefix`
f(&UserInfo{
URLPrefix: mustParseURL("http://foo.bar"),
}, "", "http://foo.bar/.", "", "", nil, "least_loaded", 0)
}, "", "http://foo.bar", "", "", nil, "least_loaded", 0)
f(&UserInfo{
URLPrefix: mustParseURL("http://foo.bar"),
}, "/", "http://foo.bar", "", "", nil, "least_loaded", 0)
f(&UserInfo{
URLPrefix: mustParseURL("http://foo.bar"),
}, "http://aaa///", "http://foo.bar", "", "", nil, "least_loaded", 0)
f(&UserInfo{
URLPrefix: mustParseURL("http://foo.bar/"),
}, "/", "http://foo.bar/", "", "", nil, "least_loaded", 0)
f(&UserInfo{
URLPrefix: mustParseURL("http://foo.bar/"),
}, "/x", "http://foo.bar/x", "", "", nil, "least_loaded", 0)
f(&UserInfo{
URLPrefix: mustParseURL("http://foo.bar/"),
}, "/x/", "http://foo.bar/x/", "", "", nil, "least_loaded", 0)
f(&UserInfo{
URLPrefix: mustParseURL("http://foo.bar/"),
}, "http://abc///x/", "http://foo.bar/x/", "", "", nil, "least_loaded", 0)
f(&UserInfo{
URLPrefix: mustParseURL("http://foo.bar/"),
}, "http://foo//x", "http://foo.bar/x", "", "", nil, "least_loaded", 0)
f(&UserInfo{
URLPrefix: mustParseURL("http://foo.bar/baz"),
}, "", "http://foo.bar/baz", "", "", nil, "least_loaded", 0)
f(&UserInfo{
URLPrefix: mustParseURL("http://foo.bar/baz"),
}, "/", "http://foo.bar/baz", "", "", nil, "least_loaded", 0)
f(&UserInfo{
URLPrefix: mustParseURL("http://foo.bar/x/"),
}, "/abc", "http://foo.bar/x/abc", "", "", nil, "least_loaded", 0)
f(&UserInfo{
URLPrefix: mustParseURL("http://foo.bar/x/"),
}, "/abc/", "http://foo.bar/x/abc/", "", "", nil, "least_loaded", 0)
f(&UserInfo{
URLPrefix: mustParseURL("http://foo.bar"),
HeadersConf: HeadersConf{
@@ -149,6 +182,12 @@ func TestCreateTargetURLSuccess(t *testing.T) {
f(&UserInfo{
URLPrefix: mustParseURL("http://foo.bar"),
}, "a/b?c=d", "http://foo.bar/a/b?c=d", "", "", nil, "least_loaded", 0)
f(&UserInfo{
URLPrefix: mustParseURL("http://foo.bar"),
}, "/a/b?c=d", "http://foo.bar/a/b?c=d", "", "", nil, "least_loaded", 0)
f(&UserInfo{
URLPrefix: mustParseURL("http://foo.bar/"),
}, "/a/b?c=d", "http://foo.bar/a/b?c=d", "", "", nil, "least_loaded", 0)
f(&UserInfo{
URLPrefix: mustParseURL("https://sss:3894/x/y"),
}, "/z", "https://sss:3894/x/y/z", "", "", nil, "least_loaded", 0)

View File

@@ -19,6 +19,7 @@ import (
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmctl/barpool"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmctl/native"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmctl/remoteread"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/netutil"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmctl/influx"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmctl/opentsdb"
@@ -44,6 +45,7 @@ func main() {
if c.Bool(globalDisableProgressBar) {
barpool.Disable(true)
}
netutil.EnableIPv6()
return nil
}
app := &cli.App{

View File

@@ -1,215 +0,0 @@
package main
import (
"context"
"fmt"
"log"
"os"
"testing"
"time"
"github.com/prometheus/prometheus/model/labels"
"github.com/prometheus/prometheus/storage"
"github.com/prometheus/prometheus/tsdb"
"github.com/prometheus/prometheus/tsdb/chunkenc"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmctl/backoff"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmctl/barpool"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmctl/prometheus"
remote_read_integration "github.com/VictoriaMetrics/VictoriaMetrics/app/vmctl/testdata/servers_integration_test"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmctl/vm"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmselect/promql"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmstorage"
)
const (
testSnapshot = "./testdata/snapshots/20250118T124506Z-59d1b952d7eaf547"
blockData = "./testdata/snapshots/20250118T124506Z-59d1b952d7eaf547/01JHWQ445Y2P1TDYB05AEKD6MC"
)
// This test simulates close process if user abort it
func TestPrometheusProcessorRun(t *testing.T) {
f := func(startStr, endStr string, numOfSeries int, resultExpected []vm.TimeSeries) {
t.Helper()
dst := remote_read_integration.NewRemoteWriteServer(t)
defer func() {
dst.Close()
}()
dst.Series(resultExpected)
dst.ExpectedSeries(resultExpected)
if err := fillStorage(resultExpected); err != nil {
t.Fatalf("cannot fill storage: %s", err)
}
isSilent = true
defer func() { isSilent = false }()
bf, err := backoff.New(1, 1.8, time.Second*2)
if err != nil {
t.Fatalf("cannot create backoff: %s", err)
}
importerCfg := vm.Config{
Addr: dst.URL(),
Transport: nil,
Concurrency: 1,
Backoff: bf,
}
ctx := context.Background()
importer, err := vm.NewImporter(ctx, importerCfg)
if err != nil {
t.Fatalf("cannot create importer: %s", err)
}
defer importer.Close()
matchName := "__name__"
matchValue := ".*"
filter := prometheus.Filter{
TimeMin: startStr,
TimeMax: endStr,
Label: matchName,
LabelValue: matchValue,
}
runner, err := prometheus.NewClient(prometheus.Config{
Snapshot: testSnapshot,
Filter: filter,
})
if err != nil {
t.Fatalf("cannot create prometheus client: %s", err)
}
p := &prometheusProcessor{
cl: runner,
im: importer,
cc: 1,
}
if err := p.run(); err != nil {
t.Fatalf("run() error: %s", err)
}
collectedTs := dst.GetCollectedTimeSeries()
t.Logf("collected timeseries: %d; expected timeseries: %d", len(collectedTs), len(resultExpected))
if len(collectedTs) != len(resultExpected) {
t.Fatalf("unexpected number of collected time series; got %d; want %d", len(collectedTs), numOfSeries)
}
deleted, err := deleteSeries(matchName, matchValue)
if err != nil {
t.Fatalf("cannot delete series: %s", err)
}
if deleted != numOfSeries {
t.Fatalf("unexpected number of deleted series; got %d; want %d", deleted, numOfSeries)
}
}
processFlags()
vmstorage.Init(promql.ResetRollupResultCacheIfNeeded)
defer func() {
vmstorage.Stop()
if err := os.RemoveAll(storagePath); err != nil {
log.Fatalf("cannot remove %q: %s", storagePath, err)
}
}()
barpool.Disable(true)
defer func() {
barpool.Disable(false)
}()
b, err := tsdb.OpenBlock(nil, blockData, nil, nil)
if err != nil {
t.Fatalf("cannot open block: %s", err)
}
// timestamp is equal to minTime and maxTime from meta.json
ss, err := readBlock(b, 1737204082361, 1737204302539)
if err != nil {
t.Fatalf("cannot read block: %s", err)
}
resultExpected, err := prepareExpectedData(ss)
if err != nil {
t.Fatalf("cannot prepare expected data: %s", err)
}
f("2025-01-18T12:40:00Z", "2025-01-18T12:46:00Z", 2792, resultExpected)
}
func readBlock(b tsdb.BlockReader, timeMin int64, timeMax int64) (storage.SeriesSet, error) {
minTime, maxTime := b.Meta().MinTime, b.Meta().MaxTime
if timeMin != 0 {
minTime = timeMin
}
if timeMax != 0 {
maxTime = timeMax
}
q, err := tsdb.NewBlockQuerier(b, minTime, maxTime)
if err != nil {
return nil, err
}
matchName := "__name__"
matchValue := ".*"
ctx := context.Background()
ss := q.Select(ctx, false, nil, labels.MustNewMatcher(labels.MatchRegexp, matchName, matchValue))
return ss, nil
}
func prepareExpectedData(ss storage.SeriesSet) ([]vm.TimeSeries, error) {
var expectedSeriesSet []vm.TimeSeries
var it chunkenc.Iterator
for ss.Next() {
var name string
var labelPairs []vm.LabelPair
series := ss.At()
for _, label := range series.Labels() {
if label.Name == "__name__" {
name = label.Value
continue
}
labelPairs = append(labelPairs, vm.LabelPair{
Name: label.Name,
Value: label.Value,
})
}
if name == "" {
return nil, fmt.Errorf("failed to find `__name__` label in labelset for block")
}
var timestamps []int64
var values []float64
it = series.Iterator(it)
for {
typ := it.Next()
if typ == chunkenc.ValNone {
break
}
if typ != chunkenc.ValFloat {
// Skip unsupported values
continue
}
t, v := it.At()
timestamps = append(timestamps, t)
values = append(values, v)
}
if err := it.Err(); err != nil {
return nil, err
}
ts := vm.TimeSeries{
Name: name,
LabelPairs: labelPairs,
Timestamps: timestamps,
Values: values,
}
expectedSeriesSet = append(expectedSeriesSet, ts)
}
return expectedSeriesSet, nil
}

View File

@@ -1,17 +0,0 @@
{
"ulid": "01JHWQ445Y2P1TDYB05AEKD6MC",
"minTime": 1737204082361,
"maxTime": 1737204302539,
"stats": {
"numSamples": 60275,
"numSeries": 2792,
"numChunks": 2792
},
"compaction": {
"level": 1,
"sources": [
"01JHWQ445Y2P1TDYB05AEKD6MC"
]
},
"version": 1
}

View File

@@ -299,6 +299,8 @@ func (im *Importer) Ping() error {
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("bad status code: %d", resp.StatusCode)
}

View File

@@ -15,7 +15,7 @@ import (
)
var maxGraphiteSeries = flag.Int("search.maxGraphiteSeries", 300e3, "The maximum number of time series, which can be scanned during queries to Graphite Render API. "+
"See https://docs.victoriametrics.com/victoriametrics/integrations/graphite#render-api")
"See https://docs.victoriametrics.com/victoriametrics/integrations/graphite/#render-api")
type evalConfig struct {
startTime int64

View File

@@ -22,9 +22,9 @@ import (
var (
maxGraphiteTagKeysPerSearch = flag.Int("search.maxGraphiteTagKeys", 100e3, "The maximum number of tag keys returned from Graphite API, which returns tags. "+
"See https://docs.victoriametrics.com/victoriametrics/integrations/graphite#tags-api")
"See https://docs.victoriametrics.com/victoriametrics/integrations/graphite/#tags-api")
maxGraphiteTagValuesPerSearch = flag.Int("search.maxGraphiteTagValues", 100e3, "The maximum number of tag values returned from Graphite API, which returns tag values. "+
"See https://docs.victoriametrics.com/victoriametrics/integrations/graphite#tags-api")
"See https://docs.victoriametrics.com/victoriametrics/integrations/graphite/#tags-api")
)
// TagsDelSeriesHandler implements /tags/delSeries handler.

View File

@@ -32,9 +32,13 @@ See https://prometheus.io/docs/prometheus/latest/querying/api/#range-queries
{% code
// seriesFetched is string instead of int because of historical reasons.
// It cannot be converted to int without breaking backwards compatibility at vmalert :(
executionDuration := int64(0)
if ed := qs.ExecutionDuration.Load(); ed != nil {
executionDuration = ed.Milliseconds()
}
%}
"seriesFetched": "{%dl qs.SeriesFetched.Load() %}",
"executionTimeMsec": {%dl qs.ExecutionDuration.Load().Milliseconds() %}
"executionTimeMsec": {%dl executionDuration %}
}
{% code
qt.Printf("generate /api/v1/query_range response for series=%d, points=%d", seriesCount, pointsCount)

View File

@@ -64,91 +64,95 @@ func StreamQueryRangeResponse(qw422016 *qt422016.Writer, rs []netstorage.Result,
//line app/vmselect/prometheus/query_range_response.qtpl:33
// seriesFetched is string instead of int because of historical reasons.
// It cannot be converted to int without breaking backwards compatibility at vmalert :(
executionDuration := int64(0)
if ed := qs.ExecutionDuration.Load(); ed != nil {
executionDuration = ed.Milliseconds()
}
//line app/vmselect/prometheus/query_range_response.qtpl:35
//line app/vmselect/prometheus/query_range_response.qtpl:39
qw422016.N().S(`"seriesFetched": "`)
//line app/vmselect/prometheus/query_range_response.qtpl:36
qw422016.N().DL(qs.SeriesFetched.Load())
//line app/vmselect/prometheus/query_range_response.qtpl:36
qw422016.N().S(`","executionTimeMsec":`)
//line app/vmselect/prometheus/query_range_response.qtpl:37
qw422016.N().DL(qs.ExecutionDuration.Load().Milliseconds())
//line app/vmselect/prometheus/query_range_response.qtpl:37
qw422016.N().S(`}`)
//line app/vmselect/prometheus/query_range_response.qtpl:40
qw422016.N().DL(qs.SeriesFetched.Load())
//line app/vmselect/prometheus/query_range_response.qtpl:40
qw422016.N().S(`","executionTimeMsec":`)
//line app/vmselect/prometheus/query_range_response.qtpl:41
qw422016.N().DL(executionDuration)
//line app/vmselect/prometheus/query_range_response.qtpl:41
qw422016.N().S(`}`)
//line app/vmselect/prometheus/query_range_response.qtpl:44
qt.Printf("generate /api/v1/query_range response for series=%d, points=%d", seriesCount, pointsCount)
qtDone()
//line app/vmselect/prometheus/query_range_response.qtpl:43
//line app/vmselect/prometheus/query_range_response.qtpl:47
streamdumpQueryTrace(qw422016, qt)
//line app/vmselect/prometheus/query_range_response.qtpl:43
//line app/vmselect/prometheus/query_range_response.qtpl:47
qw422016.N().S(`}`)
//line app/vmselect/prometheus/query_range_response.qtpl:45
//line app/vmselect/prometheus/query_range_response.qtpl:49
}
//line app/vmselect/prometheus/query_range_response.qtpl:45
//line app/vmselect/prometheus/query_range_response.qtpl:49
func WriteQueryRangeResponse(qq422016 qtio422016.Writer, rs []netstorage.Result, qt *querytracer.Tracer, qtDone func(), qs *promql.QueryStats) {
//line app/vmselect/prometheus/query_range_response.qtpl:45
//line app/vmselect/prometheus/query_range_response.qtpl:49
qw422016 := qt422016.AcquireWriter(qq422016)
//line app/vmselect/prometheus/query_range_response.qtpl:45
//line app/vmselect/prometheus/query_range_response.qtpl:49
StreamQueryRangeResponse(qw422016, rs, qt, qtDone, qs)
//line app/vmselect/prometheus/query_range_response.qtpl:45
//line app/vmselect/prometheus/query_range_response.qtpl:49
qt422016.ReleaseWriter(qw422016)
//line app/vmselect/prometheus/query_range_response.qtpl:45
//line app/vmselect/prometheus/query_range_response.qtpl:49
}
//line app/vmselect/prometheus/query_range_response.qtpl:45
//line app/vmselect/prometheus/query_range_response.qtpl:49
func QueryRangeResponse(rs []netstorage.Result, qt *querytracer.Tracer, qtDone func(), qs *promql.QueryStats) string {
//line app/vmselect/prometheus/query_range_response.qtpl:45
//line app/vmselect/prometheus/query_range_response.qtpl:49
qb422016 := qt422016.AcquireByteBuffer()
//line app/vmselect/prometheus/query_range_response.qtpl:45
//line app/vmselect/prometheus/query_range_response.qtpl:49
WriteQueryRangeResponse(qb422016, rs, qt, qtDone, qs)
//line app/vmselect/prometheus/query_range_response.qtpl:45
//line app/vmselect/prometheus/query_range_response.qtpl:49
qs422016 := string(qb422016.B)
//line app/vmselect/prometheus/query_range_response.qtpl:45
//line app/vmselect/prometheus/query_range_response.qtpl:49
qt422016.ReleaseByteBuffer(qb422016)
//line app/vmselect/prometheus/query_range_response.qtpl:45
//line app/vmselect/prometheus/query_range_response.qtpl:49
return qs422016
//line app/vmselect/prometheus/query_range_response.qtpl:45
//line app/vmselect/prometheus/query_range_response.qtpl:49
}
//line app/vmselect/prometheus/query_range_response.qtpl:47
//line app/vmselect/prometheus/query_range_response.qtpl:51
func streamqueryRangeLine(qw422016 *qt422016.Writer, r *netstorage.Result) {
//line app/vmselect/prometheus/query_range_response.qtpl:47
//line app/vmselect/prometheus/query_range_response.qtpl:51
qw422016.N().S(`{"metric":`)
//line app/vmselect/prometheus/query_range_response.qtpl:49
//line app/vmselect/prometheus/query_range_response.qtpl:53
streammetricNameObject(qw422016, &r.MetricName)
//line app/vmselect/prometheus/query_range_response.qtpl:49
//line app/vmselect/prometheus/query_range_response.qtpl:53
qw422016.N().S(`,"values":`)
//line app/vmselect/prometheus/query_range_response.qtpl:50
//line app/vmselect/prometheus/query_range_response.qtpl:54
streamvaluesWithTimestamps(qw422016, r.Values, r.Timestamps)
//line app/vmselect/prometheus/query_range_response.qtpl:50
//line app/vmselect/prometheus/query_range_response.qtpl:54
qw422016.N().S(`}`)
//line app/vmselect/prometheus/query_range_response.qtpl:52
//line app/vmselect/prometheus/query_range_response.qtpl:56
}
//line app/vmselect/prometheus/query_range_response.qtpl:52
//line app/vmselect/prometheus/query_range_response.qtpl:56
func writequeryRangeLine(qq422016 qtio422016.Writer, r *netstorage.Result) {
//line app/vmselect/prometheus/query_range_response.qtpl:52
//line app/vmselect/prometheus/query_range_response.qtpl:56
qw422016 := qt422016.AcquireWriter(qq422016)
//line app/vmselect/prometheus/query_range_response.qtpl:52
//line app/vmselect/prometheus/query_range_response.qtpl:56
streamqueryRangeLine(qw422016, r)
//line app/vmselect/prometheus/query_range_response.qtpl:52
//line app/vmselect/prometheus/query_range_response.qtpl:56
qt422016.ReleaseWriter(qw422016)
//line app/vmselect/prometheus/query_range_response.qtpl:52
//line app/vmselect/prometheus/query_range_response.qtpl:56
}
//line app/vmselect/prometheus/query_range_response.qtpl:52
//line app/vmselect/prometheus/query_range_response.qtpl:56
func queryRangeLine(r *netstorage.Result) string {
//line app/vmselect/prometheus/query_range_response.qtpl:52
//line app/vmselect/prometheus/query_range_response.qtpl:56
qb422016 := qt422016.AcquireByteBuffer()
//line app/vmselect/prometheus/query_range_response.qtpl:52
//line app/vmselect/prometheus/query_range_response.qtpl:56
writequeryRangeLine(qb422016, r)
//line app/vmselect/prometheus/query_range_response.qtpl:52
//line app/vmselect/prometheus/query_range_response.qtpl:56
qs422016 := string(qb422016.B)
//line app/vmselect/prometheus/query_range_response.qtpl:52
//line app/vmselect/prometheus/query_range_response.qtpl:56
qt422016.ReleaseByteBuffer(qb422016)
//line app/vmselect/prometheus/query_range_response.qtpl:52
//line app/vmselect/prometheus/query_range_response.qtpl:56
return qs422016
//line app/vmselect/prometheus/query_range_response.qtpl:52
//line app/vmselect/prometheus/query_range_response.qtpl:56
}

View File

@@ -34,9 +34,13 @@ See https://prometheus.io/docs/prometheus/latest/querying/api/#instant-queries
{% code
// seriesFetched is string instead of int because of historical reasons.
// It cannot be converted to int without breaking backwards compatibility at vmalert :(
executionDuration := int64(0)
if ed := qs.ExecutionDuration.Load(); ed != nil {
executionDuration = ed.Milliseconds()
}
%}
"seriesFetched": "{%dl qs.SeriesFetched.Load() %}",
"executionTimeMsec": {%dl qs.ExecutionDuration.Load().Milliseconds() %}
"executionTimeMsec": {%dl executionDuration %}
}
{% code
qt.Printf("generate /api/v1/query response for series=%d", seriesCount)

View File

@@ -74,50 +74,54 @@ func StreamQueryResponse(qw422016 *qt422016.Writer, rs []netstorage.Result, qt *
//line app/vmselect/prometheus/query_response.qtpl:35
// seriesFetched is string instead of int because of historical reasons.
// It cannot be converted to int without breaking backwards compatibility at vmalert :(
executionDuration := int64(0)
if ed := qs.ExecutionDuration.Load(); ed != nil {
executionDuration = ed.Milliseconds()
}
//line app/vmselect/prometheus/query_response.qtpl:37
//line app/vmselect/prometheus/query_response.qtpl:41
qw422016.N().S(`"seriesFetched": "`)
//line app/vmselect/prometheus/query_response.qtpl:38
qw422016.N().DL(qs.SeriesFetched.Load())
//line app/vmselect/prometheus/query_response.qtpl:38
qw422016.N().S(`","executionTimeMsec":`)
//line app/vmselect/prometheus/query_response.qtpl:39
qw422016.N().DL(qs.ExecutionDuration.Load().Milliseconds())
//line app/vmselect/prometheus/query_response.qtpl:39
qw422016.N().S(`}`)
//line app/vmselect/prometheus/query_response.qtpl:42
qw422016.N().DL(qs.SeriesFetched.Load())
//line app/vmselect/prometheus/query_response.qtpl:42
qw422016.N().S(`","executionTimeMsec":`)
//line app/vmselect/prometheus/query_response.qtpl:43
qw422016.N().DL(executionDuration)
//line app/vmselect/prometheus/query_response.qtpl:43
qw422016.N().S(`}`)
//line app/vmselect/prometheus/query_response.qtpl:46
qt.Printf("generate /api/v1/query response for series=%d", seriesCount)
qtDone()
//line app/vmselect/prometheus/query_response.qtpl:45
//line app/vmselect/prometheus/query_response.qtpl:49
streamdumpQueryTrace(qw422016, qt)
//line app/vmselect/prometheus/query_response.qtpl:45
//line app/vmselect/prometheus/query_response.qtpl:49
qw422016.N().S(`}`)
//line app/vmselect/prometheus/query_response.qtpl:47
//line app/vmselect/prometheus/query_response.qtpl:51
}
//line app/vmselect/prometheus/query_response.qtpl:47
//line app/vmselect/prometheus/query_response.qtpl:51
func WriteQueryResponse(qq422016 qtio422016.Writer, rs []netstorage.Result, qt *querytracer.Tracer, qtDone func(), qs *promql.QueryStats) {
//line app/vmselect/prometheus/query_response.qtpl:47
//line app/vmselect/prometheus/query_response.qtpl:51
qw422016 := qt422016.AcquireWriter(qq422016)
//line app/vmselect/prometheus/query_response.qtpl:47
//line app/vmselect/prometheus/query_response.qtpl:51
StreamQueryResponse(qw422016, rs, qt, qtDone, qs)
//line app/vmselect/prometheus/query_response.qtpl:47
//line app/vmselect/prometheus/query_response.qtpl:51
qt422016.ReleaseWriter(qw422016)
//line app/vmselect/prometheus/query_response.qtpl:47
//line app/vmselect/prometheus/query_response.qtpl:51
}
//line app/vmselect/prometheus/query_response.qtpl:47
//line app/vmselect/prometheus/query_response.qtpl:51
func QueryResponse(rs []netstorage.Result, qt *querytracer.Tracer, qtDone func(), qs *promql.QueryStats) string {
//line app/vmselect/prometheus/query_response.qtpl:47
//line app/vmselect/prometheus/query_response.qtpl:51
qb422016 := qt422016.AcquireByteBuffer()
//line app/vmselect/prometheus/query_response.qtpl:47
//line app/vmselect/prometheus/query_response.qtpl:51
WriteQueryResponse(qb422016, rs, qt, qtDone, qs)
//line app/vmselect/prometheus/query_response.qtpl:47
//line app/vmselect/prometheus/query_response.qtpl:51
qs422016 := string(qb422016.B)
//line app/vmselect/prometheus/query_response.qtpl:47
//line app/vmselect/prometheus/query_response.qtpl:51
qt422016.ReleaseByteBuffer(qb422016)
//line app/vmselect/prometheus/query_response.qtpl:47
//line app/vmselect/prometheus/query_response.qtpl:51
return qs422016
//line app/vmselect/prometheus/query_response.qtpl:47
//line app/vmselect/prometheus/query_response.qtpl:51
}

View File

@@ -5,8 +5,10 @@ import (
"strings"
"unsafe"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmselect/netstorage"
"github.com/VictoriaMetrics/metricsql"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmselect/netstorage"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/atomicutil"
)
// callbacks for optimized incremental calculations for aggregate functions
@@ -66,9 +68,8 @@ var incrementalAggrFuncCallbacksMap = map[string]*incrementalAggrFuncCallbacks{
type incrementalAggrContextMap struct {
m map[string]*incrementalAggrContext
// The padding prevents false sharing on widespread platforms with
// 128 mod (cache line size) = 0 .
_ [128 - unsafe.Sizeof(map[string]*incrementalAggrContext{})%128]byte
// The padding prevents false sharing
_ [atomicutil.CacheLineSize - unsafe.Sizeof(map[string]*incrementalAggrContext{})%atomicutil.CacheLineSize]byte
}
type incrementalAggrFuncContext struct {

View File

@@ -17,6 +17,7 @@ import (
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmselect/netstorage"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmselect/searchutil"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/atomicutil"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/bytesutil"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/cgroup"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/decimal"
@@ -1885,9 +1886,8 @@ func doRollupForTimeseries(funcName string, keepMetricNames bool, rc *rollupConf
type timeseriesWithPadding struct {
tss []*timeseries
// The padding prevents false sharing on widespread platforms with
// 128 mod (cache line size) = 0 .
_ [128 - unsafe.Sizeof([]*timeseries{})%128]byte
// The padding prevents false sharing
_ [atomicutil.CacheLineSize - unsafe.Sizeof([]*timeseries{})%atomicutil.CacheLineSize]byte
}
type timeseriesByWorkerID struct {

View File

@@ -529,7 +529,7 @@ type rollupFuncArg struct {
timestamps []int64
// Real value preceding values.
// Is populated if preceding value is within the -search.maxStalenessInterval (rc.LookbackDelta).
// Is populated if preceding value is within the rc.LookbackDelta.
realPrevValue float64
// Real value which goes after values.
@@ -776,13 +776,18 @@ func (rc *rollupConfig) doInternal(dstValues []float64, tsm *timeseriesMap, valu
rfa.realPrevValue = nan
if i > 0 {
prevValue, prevTimestamp := values[i-1], timestamps[i-1]
// set realPrevValue if rc.LookbackDelta == 0
// or if distance between datapoint in prev interval and beginning of this interval
// set realPrevValue if rc.LookbackDelta == 0 or
// if distance between datapoint in prev interval and first datapoint in this interval
// doesn't exceed LookbackDelta.
// https://github.com/VictoriaMetrics/VictoriaMetrics/pull/1381
// https://github.com/VictoriaMetrics/VictoriaMetrics/issues/894
// https://github.com/VictoriaMetrics/VictoriaMetrics/issues/8045
if rc.LookbackDelta == 0 || (tStart-prevTimestamp) < rc.LookbackDelta {
// https://github.com/VictoriaMetrics/VictoriaMetrics/issues/8935
currTimestamp := tStart
if len(rfa.timestamps) > 0 {
currTimestamp = rfa.timestamps[0]
}
if rc.LookbackDelta == 0 || (currTimestamp-prevTimestamp) < rc.LookbackDelta {
rfa.realPrevValue = prevValue
}
}
@@ -1826,14 +1831,18 @@ func rollupIncreasePure(rfa *rollupFuncArg) float64 {
// There is no need in handling NaNs here, since they must be cleaned up
// before calling rollup funcs.
values := rfa.values
// restore to the real value because of potential staleness reset
prevValue := rfa.realPrevValue
prevValue := rfa.prevValue
if math.IsNaN(prevValue) {
if len(values) == 0 {
return nan
}
// Assume the counter starts from 0.
prevValue = 0
if !math.IsNaN(rfa.realPrevValue) {
// Assume that the value didn't change during the current gap
// if realPrevValue exists.
prevValue = rfa.realPrevValue
}
}
if len(values) == 0 {
// Assume the counter didn't change since prevValue.

View File

@@ -1719,6 +1719,33 @@ func TestRollupDeltaWithStaleness(t *testing.T) {
timestampsExpected := []int64{0, 10e3, 20e3, 30e3, 40e3}
testRowsEqual(t, gotValues, rc.Timestamps, valuesExpected, timestampsExpected)
})
t.Run("issue-8935", func(t *testing.T) {
// https://github.com/VictoriaMetrics/VictoriaMetrics/issues/8935
// below dataset has a gap that exceeds LookbackDelta.
// The step is picked in a way that on [60e3-90e3] window
// the prevValue will be NaN, but 60e3-55e3 still matches
// timestamp=10e3 and stores its value as realPrevValue.
// This results into delta=1-50=-49 increase result.
// The fix makes it to deduct LookbackDelta not from window start
// but from first captured data point in the window, so it becomes 70e3-55e3=15e3.
// And realPrevValue becomes NaN due to staleness detection.
timestamps = []int64{0, 10000, 70000, 80000}
values = []float64{50, 50, 1, 1}
rc := rollupConfig{
Func: rollupDelta,
Start: 0,
End: 90e3,
Step: 30e3,
LookbackDelta: 55e3,
MaxPointsPerSeries: 1e4,
}
rc.Timestamps = rc.getTimestamps()
gotValues, _ := rc.Do(nil, values, timestamps)
valuesExpected := []float64{0, 0, 0, 1}
timestampsExpected := []int64{0, 30e3, 60e3, 90e3}
testRowsEqual(t, gotValues, rc.Timestamps, valuesExpected, timestampsExpected)
})
}
func TestRollupIncreasePureWithStaleness(t *testing.T) {

View File

@@ -66,8 +66,8 @@ or at your own [VictoriaMetrics instance](https://docs.victoriametrics.com/victo
The list of MetricsQL features on top of PromQL:
* Graphite-compatible filters can be passed via `{__graphite__="foo.*.bar"}` syntax.
See [these docs](https://docs.victoriametrics.com/victoriametrics/integrations/graphite#selecting-graphite-metrics).
VictoriaMetrics can be used as Graphite datasource in Grafana. See [these docs](https://docs.victoriametrics.com/victoriametrics/integrations/graphite#graphite-api-usage) for details.
See [these docs](https://docs.victoriametrics.com/victoriametrics/integrations/graphite/#selecting-graphite-metrics).
VictoriaMetrics can be used as Graphite datasource in Grafana. See [these docs](https://docs.victoriametrics.com/victoriametrics/integrations/graphite/#graphite-api-usage) for details.
See also [label_graphite_group](#label_graphite_group) function, which can be used for extracting the given groups from Graphite metric name.
* Lookbehind window in square brackets for [rollup functions](#rollup-functions) may be omitted. VictoriaMetrics automatically selects the lookbehind window
depending on the `step` query arg passed to [/api/v1/query_range](https://docs.victoriametrics.com/victoriametrics/keyconcepts/#range-query)

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -36,10 +36,10 @@
<meta property="og:title" content="UI for VictoriaMetrics">
<meta property="og:url" content="https://victoriametrics.com/">
<meta property="og:description" content="Explore and troubleshoot your VictoriaMetrics data">
<script type="module" crossorigin src="./assets/index-C_-w5pCZ.js"></script>
<link rel="modulepreload" crossorigin href="./assets/vendor-C-vZmbyg.js">
<script type="module" crossorigin src="./assets/index-xmjGcv4-.js"></script>
<link rel="modulepreload" crossorigin href="./assets/vendor-D8IJGiEn.js">
<link rel="stylesheet" crossorigin href="./assets/vendor-D1GxaB_c.css">
<link rel="stylesheet" crossorigin href="./assets/index-Brup_hCI.css">
<link rel="stylesheet" crossorigin href="./assets/index-C85_NB5q.css">
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>

View File

@@ -6,6 +6,7 @@ import path from "node:path";
import { fileURLToPath } from "node:url";
import js from "@eslint/js";
import { FlatCompat } from "@eslint/eslintrc";
import unusedImports from "eslint-plugin-unused-imports";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
@@ -23,6 +24,7 @@ export default [...compat.extends(
plugins: {
react,
"@typescript-eslint": typescriptEslint,
"unused-imports": unusedImports,
},
languageOptions: {
@@ -59,7 +61,7 @@ export default [...compat.extends(
allowTernary: true
}],
"@typescript-eslint/no-unused-vars": ["warn", {
"@typescript-eslint/no-unused-vars": ["error", {
"argsIgnorePattern": "^_",
"caughtErrors": "none",
"caughtErrorsIgnorePattern": "^_",
@@ -67,6 +69,8 @@ export default [...compat.extends(
"varsIgnorePattern": "^_",
"ignoreRestSiblings": true
}],
"unused-imports/no-unused-imports": "error",
"react/jsx-closing-bracket-location": [1, "line-aligned"],
@@ -85,6 +89,7 @@ export default [...compat.extends(
quotes: ["error", "double"],
semi: ["error", "always"],
"react/prop-types": 0,
"react/react-in-jsx-scope": "off",
},
}];

View File

@@ -23,9 +23,9 @@
"preact": "^10.26.5",
"qs": "^6.14.0",
"react-input-mask": "^2.0.4",
"react-router-dom": "^7.5.0",
"react-router-dom": "^7.6.0",
"uplot": "^1.6.32",
"vite": "^6.2.6",
"vite": "^6.2.7",
"web-vitals": "^4.2.4"
},
"devDependencies": {
@@ -42,6 +42,7 @@
"cross-env": "^7.0.3",
"eslint": "^9.24.0",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-unused-imports": "^4.1.4",
"globals": "^16.0.0",
"http-proxy-middleware": "^3.0.5",
"jsdom": "^26.1.0",
@@ -2115,12 +2116,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/cookie": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz",
"integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==",
"license": "MIT"
},
"node_modules/@types/eslint": {
"version": "9.6.1",
"resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz",
@@ -4199,6 +4194,22 @@
"eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7"
}
},
"node_modules/eslint-plugin-unused-imports": {
"version": "4.1.4",
"resolved": "https://registry.npmjs.org/eslint-plugin-unused-imports/-/eslint-plugin-unused-imports-4.1.4.tgz",
"integrity": "sha512-YptD6IzQjDardkl0POxnnRBhU1OEePMV0nd6siHaRBbd+lyh6NAhFEobiznKU7kTsSsDeSD62Pe7kAM1b7dAZQ==",
"dev": true,
"license": "MIT",
"peerDependencies": {
"@typescript-eslint/eslint-plugin": "^8.0.0-0 || ^7.0.0 || ^6.0.0 || ^5.0.0",
"eslint": "^9.0.0 || ^8.0.0"
},
"peerDependenciesMeta": {
"@typescript-eslint/eslint-plugin": {
"optional": true
}
}
},
"node_modules/eslint-scope": {
"version": "8.3.0",
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.3.0.tgz",
@@ -6476,15 +6487,13 @@
"license": "MIT"
},
"node_modules/react-router": {
"version": "7.5.0",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.5.0.tgz",
"integrity": "sha512-estOHrRlDMKdlQa6Mj32gIks4J+AxNsYoE0DbTTxiMy2mPzZuWSDU+N85/r1IlNR7kGfznF3VCUlvc5IUO+B9g==",
"version": "7.6.0",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.6.0.tgz",
"integrity": "sha512-GGufuHIVCJDbnIAXP3P9Sxzq3UUsddG3rrI3ut1q6m0FI6vxVBF3JoPQ38+W/blslLH4a5Yutp8drkEpXoddGQ==",
"license": "MIT",
"dependencies": {
"@types/cookie": "^0.6.0",
"cookie": "^1.0.1",
"set-cookie-parser": "^2.6.0",
"turbo-stream": "2.4.0"
"set-cookie-parser": "^2.6.0"
},
"engines": {
"node": ">=20.0.0"
@@ -6500,12 +6509,12 @@
}
},
"node_modules/react-router-dom": {
"version": "7.5.0",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.5.0.tgz",
"integrity": "sha512-fFhGFCULy4vIseTtH5PNcY/VvDJK5gvOWcwJVHQp8JQcWVr85ENhJ3UpuF/zP1tQOIFYNRJHzXtyhU1Bdgw0RA==",
"version": "7.6.0",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.6.0.tgz",
"integrity": "sha512-DYgm6RDEuKdopSyGOWZGtDfSm7Aofb8CCzgkliTjtu/eDuB0gcsv6qdFhhi8HdtmA+KHkt5MfZ5K2PdzjugYsA==",
"license": "MIT",
"dependencies": {
"react-router": "7.5.0"
"react-router": "7.6.0"
},
"engines": {
"node": ">=20.0.0"
@@ -8012,12 +8021,6 @@
"devOptional": true,
"license": "0BSD"
},
"node_modules/turbo-stream": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/turbo-stream/-/turbo-stream-2.4.0.tgz",
"integrity": "sha512-FHncC10WpBd2eOmGwpmQsWLDoK4cqsA/UT/GqNoaKOQnT8uzhtCbg3EoUDMvqpOSAI0S26mr0rkjzbOO6S3v1g==",
"license": "ISC"
},
"node_modules/type-check": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
@@ -8204,9 +8207,9 @@
"license": "MIT"
},
"node_modules/vite": {
"version": "6.2.6",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.2.6.tgz",
"integrity": "sha512-9xpjNl3kR4rVDZgPNdTL0/c6ao4km69a/2ihNQbcANz8RuCOK3hQBmLSJf3bRKVQjVMda+YvizNE8AwvogcPbw==",
"version": "6.2.7",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.2.7.tgz",
"integrity": "sha512-qg3LkeuinTrZoJHHF94coSaTfIPyBYoywp+ys4qu20oSJFbKMYoIJo0FWJT9q6Vp49l6z9IsJRbHdcGtiKbGoQ==",
"license": "MIT",
"dependencies": {
"esbuild": "^0.25.0",

View File

@@ -20,14 +20,15 @@
"preact": "^10.26.5",
"qs": "^6.14.0",
"react-input-mask": "^2.0.4",
"react-router-dom": "^7.5.0",
"react-router-dom": "^7.6.0",
"uplot": "^1.6.32",
"vite": "^6.2.6",
"vite": "^6.2.7",
"web-vitals": "^4.2.4"
},
"scripts": {
"prestart": "npm run copy-metricsql-docs",
"start": "vite",
"start:playground": "cross-env PLAYGROUND=METRICS npm run start",
"start:logs": "vite --mode victorialogs",
"start:logs:playground": "cross-env PLAYGROUND=LOGS npm run start:logs",
"start:anomaly": "vite --mode vmanomaly",
@@ -66,6 +67,7 @@
"cross-env": "^7.0.3",
"eslint": "^9.24.0",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-unused-imports": "^4.1.4",
"globals": "^16.0.0",
"http-proxy-middleware": "^3.0.5",
"jsdom": "^26.1.0",

View File

@@ -66,8 +66,8 @@ or at your own [VictoriaMetrics instance](https://docs.victoriametrics.com/victo
The list of MetricsQL features on top of PromQL:
* Graphite-compatible filters can be passed via `{__graphite__="foo.*.bar"}` syntax.
See [these docs](https://docs.victoriametrics.com/victoriametrics/integrations/graphite#selecting-graphite-metrics).
VictoriaMetrics can be used as Graphite datasource in Grafana. See [these docs](https://docs.victoriametrics.com/victoriametrics/integrations/graphite#graphite-api-usage) for details.
See [these docs](https://docs.victoriametrics.com/victoriametrics/integrations/graphite/#selecting-graphite-metrics).
VictoriaMetrics can be used as Graphite datasource in Grafana. See [these docs](https://docs.victoriametrics.com/victoriametrics/integrations/graphite/#graphite-api-usage) for details.
See also [label_graphite_group](#label_graphite_group) function, which can be used for extracting the given groups from Graphite metric name.
* Lookbehind window in square brackets for [rollup functions](#rollup-functions) may be omitted. VictoriaMetrics automatically selects the lookbehind window
depending on the `step` query arg passed to [/api/v1/query_range](https://docs.victoriametrics.com/victoriametrics/keyconcepts/#range-query)

View File

@@ -69,7 +69,7 @@ const LogsQueryEditorAutocomplete: FC<QueryEditorAutocompleteProps> = ({
if (insertType === ContextType.FilterName) {
modifiedInsert += ":";
} else if (contextType === ContextType.FilterValue) {
const insertWithQuotes = value.startsWith("_stream:") ? modifiedInsert : `"${modifiedInsert}"`;
const insertWithQuotes = value.startsWith("_stream:") ? modifiedInsert : `${JSON.stringify(modifiedInsert)}`;
modifiedInsert = `${contextData?.filterName || ""}:${insertWithQuotes}`;
}

View File

@@ -0,0 +1,43 @@
import { hasSortPipe } from "./sort";
describe("hasSortPipe()", () => {
/** Queries that MUST be recognised as containing a sort/order pipe. */
const positive: string[] = [
// ───── basic usage ─────
"sort by (_time)",
"| sort by (_time)",
"|sort(_time) desc",
"| order by (foo desc)",
"_time:5m | sort by (_stream, _time)",
// ───── documented options ─────
"_time:1h | sort by (request_duration desc) limit 10",
"_time:1h | sort by (request_duration desc) partition by (host) limit 3",
"_time:5m | sort by (_time) rank as position",
// ───── whitespace / tabs ─────
"|\t sort\tby (host)",
// ───── no space after the pipe ─────
"foo|sort by (_time)",
];
/** Queries that MUST **not** be recognised (false positives). */
const negative: string[] = [
"", // empty
"error | sample 100", // no sort
"|sorted(field)", // 'sorted' ≠ 'sort'
"|sorter(field)", // 'sorter' ≠ 'sort'
"my_sort(field)", // function name
"| sorta by (field)", // 'sorta'
"foo | orderliness by (bar)", // 'orderliness' ≠ 'order'
];
it.each(positive)("detects pipe in ➜ %s", query => {
expect(hasSortPipe(query)).toBe(true);
});
it.each(negative)("does NOT detect pipe in ➜ %s", query => {
expect(hasSortPipe(query)).toBe(false);
});
});

View File

@@ -0,0 +1,5 @@
const hasSortPipeRe = /(?:^|\|)\s*(?:sort|order)\b/i;
export function hasSortPipe(query: string): boolean {
return hasSortPipeRe.test(query);
}

View File

@@ -1,4 +1,4 @@
import React, { FC, useMemo, useState } from "preact/compat";
import { FC, useMemo, useState } from "preact/compat";
import useBoolean from "../../../hooks/useBoolean";
import { RestartIcon, SettingsIcon } from "../../Main/Icons";
import Button from "../../Main/Button/Button";
@@ -62,7 +62,8 @@ const GroupLogsConfigurators: FC<Props> = ({ logs }) => {
].some(Boolean);
const logsKeys = useMemo(() => {
return Array.from(new Set(logs.map(l => Object.keys(l)).flat()));
const uniqueKeys = new Set(logs.map(l => Object.keys(l)).flat());
return Array.from(uniqueKeys).sort((a, b) => a.localeCompare(b));
}, [logs]);
const {

File diff suppressed because one or more lines are too long

View File

@@ -1,4 +1,4 @@
import React, { FC, useMemo, useRef } from "preact/compat";
import { FC, useMemo, useRef } from "preact/compat";
import { ArrowDropDownIcon } from "../../Icons";
import useBoolean from "../../../../hooks/useBoolean";
import Popper from "../../Popper/Popper";
@@ -10,11 +10,12 @@ interface SelectLimitProps {
limit: number | string;
allowUnlimited?: boolean;
onChange: (val: number) => void;
onOpenSelect?: () => void;
}
const defaultLimits = [10, 25, 50, 100, 250, 500, 1000];
const SelectLimit: FC<SelectLimitProps> = ({ limit, allowUnlimited, onChange }) => {
const SelectLimit: FC<SelectLimitProps> = ({ limit, allowUnlimited, onChange, onOpenSelect }) => {
const { isMobile } = useDeviceDetect();
const buttonRef = useRef<HTMLDivElement>(null);
@@ -28,6 +29,11 @@ const SelectLimit: FC<SelectLimitProps> = ({ limit, allowUnlimited, onChange })
setFalse: handleClose,
} = useBoolean(false);
const handleClickSelect = () => {
toggleOpenList();
if(!openList) onOpenSelect?.();
};
const handleChangeLimit = (n: number) => () => {
onChange(n);
handleClose();
@@ -37,7 +43,7 @@ const SelectLimit: FC<SelectLimitProps> = ({ limit, allowUnlimited, onChange })
<>
<div
className="vm-select-limits-button"
onClick={toggleOpenList}
onClick={handleClickSelect}
ref={buttonRef}
>
<div>

View File

@@ -1,4 +1,4 @@
import React, { FC, useEffect, useRef, useMemo } from "preact/compat";
import { FC, useEffect, useRef, useMemo } from "preact/compat";
import Button from "../../Main/Button/Button";
import { SearchIcon, SettingsIcon } from "../../Main/Icons";
import "./style.scss";
@@ -49,8 +49,8 @@ const TableSettings: FC<TableSettingsProps> = ({
const filteredColumns = useMemo(() => {
const allColumns = customColumns.concat(columns);
if (!searchColumn) return allColumns;
return allColumns.filter(col => col.includes(searchColumn));
const result = searchColumn ? allColumns.filter(col => col.includes(searchColumn)) : allColumns;
return result.sort((a, b) => a.localeCompare(b));
}, [columns, customColumns, searchColumn]);
const isAllChecked = useMemo(() => {

View File

@@ -24,6 +24,7 @@
&_sidebar {
display: grid;
grid-template-columns: 40px auto 1fr;
box-shadow: $color-background-body 0 1px 1px 0px;
}
&_mobile {

View File

@@ -1,4 +1,4 @@
import React, { FC, useMemo } from "preact/compat";
import { FC, useMemo } from "preact/compat";
import { useCallback } from "react";
import dayjs from "dayjs";
import DownloadButton from "../../../components/DownloadButton/DownloadButton";
@@ -7,10 +7,11 @@ import { downloadCSV, downloadJSON } from "../../../utils/file";
import { Logs } from "../../../api/types";
interface DownloadLogsButtonProps {
logs: Logs[];
/** Callback to get logs to download */
getLogs: () => Logs[];
}
const DownloadLogsButton: FC<DownloadLogsButtonProps> = ({ logs }) => {
const DownloadLogsButton: FC<DownloadLogsButtonProps> = ({ getLogs }) => {
const { fileExtensions, getDownloaderByExtension } = useMemo(() => {
const downloadFileOptions: {
extension: string;
@@ -39,12 +40,13 @@ const DownloadLogsButton: FC<DownloadLogsButtonProps> = ({ logs }) => {
return;
}
const logs = getLogs();
const downloader = getDownloaderByExtension(fileExtension);
if (downloader){
const timestamp = dayjs().utc().format(DATE_FILENAME_FORMAT);
downloader(logs, `vmui_logs_${timestamp}.${fileExtension}`);
}
}, [logs]);
}, [getLogs]);
return <DownloadButton
title={"Download logs"}
@@ -53,4 +55,4 @@ const DownloadLogsButton: FC<DownloadLogsButtonProps> = ({ logs }) => {
/>;
};
export default DownloadLogsButton;
export default DownloadLogsButton;

View File

@@ -1,4 +1,4 @@
import React, { FC, useEffect, useMemo, useState } from "preact/compat";
import { FC, useEffect, useMemo, useState } from "preact/compat";
import ExploreLogsBody from "./ExploreLogsBody/ExploreLogsBody";
import useStateSearchParams from "../../hooks/useStateSearchParams";
import useSearchParamsFromObject from "../../hooks/useSearchParamsFromObject";
@@ -18,6 +18,7 @@ import { useSearchParams } from "react-router-dom";
import { useQueryDispatch, useQueryState } from "../../state/query/QueryStateContext";
import { getUpdatedHistory } from "../../components/QueryHistory/utils";
import { useDebounceCallback } from "../../hooks/useDebounceCallback";
import usePrevious from "../../hooks/usePrevious";
const storageLimit = Number(getFromStorage("LOGS_LIMIT"));
const defaultLimit = isNaN(storageLimit) ? LOGS_ENTRIES_LIMIT : storageLimit;
@@ -30,6 +31,7 @@ const ExploreLogs: FC = () => {
const { setSearchParamsFromKeys } = useSearchParamsFromObject();
const [searchParams] = useSearchParams();
const hideChart = useMemo(() => searchParams.get("hide_chart"), [searchParams]);
const prevHideChart = usePrevious(hideChart);
const [limit, setLimit] = useStateSearchParams(defaultLimit, "limit");
const [query, setQuery] = useStateSearchParams("*", "query");
@@ -118,11 +120,10 @@ const ExploreLogs: FC = () => {
}, [query, isUpdatingQuery]);
useEffect(() => {
if (!hideChart) debouncedFetchLogs(period, true);
return () => {
debouncedFetchLogs.cancel?.();
};
}, [hideChart, period]);
if (!hideChart && prevHideChart) {
fetchLogHits(period);
}
}, [hideChart, prevHideChart, period]);
return (
<div className="vm-explore-logs">

View File

@@ -1,5 +1,5 @@
import React, { FC, useState, useMemo, useRef } from "preact/compat";
import { CodeIcon, ListIcon, TableIcon } from "../../../components/Main/Icons";
import { FC, useRef } from "preact/compat";
import { CodeIcon, ListIcon, TableIcon, PlayIcon } from "../../../components/Main/Icons";
import Tabs from "../../../components/Main/Tabs/Tabs";
import "./style.scss";
import classNames from "classnames";
@@ -7,18 +7,11 @@ import useDeviceDetect from "../../../hooks/useDeviceDetect";
import { Logs } from "../../../api/types";
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 JsonView from "../../../components/Views/JsonView/JsonView";
import LineLoader from "../../../components/Main/LineLoader/LineLoader";
import SelectLimit from "../../../components/Main/Pagination/SelectLimit/SelectLimit";
import DownloadLogsButton from "../DownloadLogsButton/DownloadLogsButton";
const MemoizedTableLogs = React.memo(TableLogs);
const MemoizedGroupLogs = React.memo(GroupLogs);
const MemoizedJsonView = React.memo(JsonView);
import GroupView from "./views/GroupView/GroupView";
import TableView from "./views/TableView/TableView";
import JsonView from "./views/JsonView/JsonView";
import LiveTailingView from "./views/LiveTailingView/LiveTailingView";
export interface ExploreLogBodyProps {
data: Logs[];
@@ -29,44 +22,28 @@ enum DisplayType {
group = "group",
table = "table",
json = "json",
liveTailing = "liveTailing",
}
const tabs = [
{ label: "Group", value: DisplayType.group, icon: <ListIcon/> },
{ label: "Table", value: DisplayType.table, icon: <TableIcon/> },
{ label: "JSON", value: DisplayType.json, icon: <CodeIcon/> },
{ label: "Group", value: DisplayType.group, icon: <ListIcon/>, Component: GroupView },
{ label: "Table", value: DisplayType.table, icon: <TableIcon/>, Component: TableView },
{ label: "JSON", value: DisplayType.json, icon: <CodeIcon/>, Component: JsonView },
{ label: "Live", value: DisplayType.liveTailing, icon: <PlayIcon/>, Component: LiveTailingView },
];
const ExploreLogsBody: FC<ExploreLogBodyProps> = ({ data, isLoading }) => {
const { isMobile } = useDeviceDetect();
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 columns = useMemo(() => {
if (!data?.length || activeTab !== DisplayType.table) return [];
const keys = new Set<string>();
for (const item of data) {
for (const key in item) {
keys.add(key);
}
}
return Array.from(keys);
}, [data, activeTab]);
const settingsRef = useRef<HTMLDivElement>(null);
const handleChangeTab = (view: string) => {
setActiveTab(view as DisplayType);
setSearchParamsFromKeys({ view });
};
const handleSetRowsPerPage = (limit: number) => {
setRowsPerPage(limit);
setSearchParamsFromKeys({ rows_per_page: limit });
};
const ActiveTabComponent = tabs.find(tab => tab.value === activeTab)?.Component;
return (
<div
@@ -95,39 +72,16 @@ const ExploreLogsBody: FC<ExploreLogBodyProps> = ({ data, isLoading }) => {
items={tabs}
onChange={handleChangeTab}
/>
<div className="vm-explore-logs-body-header__log-info">
Total logs returned: <b>{data.length}</b>
</div>
</div>
{activeTab === DisplayType.table && (
<div className="vm-explore-logs-body-header__settings">
<SelectLimit
limit={rowsPerPage}
onChange={handleSetRowsPerPage}
/>
<div className="vm-explore-logs-body-header__table-settings">
{data.length > 0 && <DownloadLogsButton logs={data} />}
<TableSettings
columns={columns}
selectedColumns={displayColumns}
onChangeColumns={setDisplayColumns}
tableCompact={tableCompact}
toggleTableCompact={toggleTableCompact}
/>
{activeTab !== DisplayType.liveTailing && (
<div className="vm-explore-logs-body-header__log-info">
Total logs returned: <b>{data.length}</b>
</div>
</div>
)}
{activeTab === DisplayType.group && (
<>
<div
className="vm-explore-logs-body-header__settings"
ref={groupSettingsRef}
/>
</>
)}
{activeTab === DisplayType.json && data.length > 0 && (
<DownloadLogsButton logs={data} />
)}
)}
</div>
<div
className="vm-explore-logs-body-header__settings"
ref={settingsRef}
/>
</div>
<div
@@ -136,29 +90,12 @@ const ExploreLogsBody: FC<ExploreLogBodyProps> = ({ data, isLoading }) => {
"vm-explore-logs-body__table_mobile": isMobile,
})}
>
{!data.length && <div className="vm-explore-logs-body__empty">No logs found</div>}
{!!data.length && (
<>
{activeTab === DisplayType.table && (
<MemoizedTableLogs
logs={data}
displayColumns={displayColumns}
tableCompact={tableCompact}
columns={columns}
rowsPerPage={Number(rowsPerPage)}
/>
)}
{activeTab === DisplayType.group && (
<MemoizedGroupLogs
logs={data}
settingsRef={groupSettingsRef}
/>
)}
{activeTab === DisplayType.json && (
<MemoizedJsonView data={data}/>
)}
</>
)}
{ActiveTabComponent &&
<ActiveTabComponent
data={data}
settingsRef={settingsRef}
/>
}
</div>
</div>
);

View File

@@ -4,7 +4,15 @@
position: relative;
&-header {
background-color: $color-background-block;
z-index: 1;
margin: -$padding-medium 0-$padding-medium 0;
position: sticky;
top: 0;
@media (max-width:1000px) {
top: 51px;
}
&_mobile {
margin: -$padding-global 0-$padding-global 0;
@@ -19,11 +27,6 @@
justify-content: flex-end;
}
&__table-settings {
display: flex;
flex-direction: row;
}
&__log-info {
flex-grow: 1;
text-align: right;

View File

@@ -0,0 +1,6 @@
import { Logs } from "../../../api/types";
export interface ViewProps {
data: Logs[];
settingsRef: React.RefObject<HTMLDivElement>;
}

View File

@@ -0,0 +1,21 @@
import React, { FC } from "preact/compat";
import GroupLogs from "../../../GroupLogs/GroupLogs";
import { ViewProps } from "../../types";
import EmptyLogs from "../components/EmptyLogs/EmptyLogs";
const MemoizedGroupLogs = React.memo(GroupLogs);
const GroupView: FC<ViewProps> = ({ data, settingsRef }) => {
if (!data.length) return <EmptyLogs />;
return (
<>
<MemoizedGroupLogs
logs={data}
settingsRef={settingsRef}
/>
</>
);
};
export default GroupView;

View File

@@ -0,0 +1,33 @@
import React, { FC } from "preact/compat";
import DownloadLogsButton from "../../../DownloadLogsButton/DownloadLogsButton";
import { createPortal } from "preact/compat";
import JsonViewComponent from "../../../../../components/Views/JsonView/JsonView";
import { ViewProps } from "../../types";
import EmptyLogs from "../components/EmptyLogs/EmptyLogs";
import { useCallback } from "react";
const MemoizedJsonView = React.memo(JsonViewComponent);
const JsonView: FC<ViewProps> = ({ data, settingsRef }) => {
const getLogs = useCallback(() => data, [data]);
const renderSettings = () => {
if (!settingsRef.current) return null;
return createPortal(
data.length > 0 && <DownloadLogsButton getLogs={getLogs} />,
settingsRef.current
);
};
if (!data.length) return <EmptyLogs />;
return (
<>
{renderSettings()}
<MemoizedJsonView data={data} />
</>
);
};
export default JsonView;

View File

@@ -0,0 +1,125 @@
import { FC, RefObject, useCallback, useRef } from "preact/compat";
import { createPortal } from "preact/compat";
import DownloadLogsButton from "../../../DownloadLogsButton/DownloadLogsButton";
import Button from "../../../../../components/Main/Button/Button";
import SelectLimit from "../../../../../components/Main/Pagination/SelectLimit/SelectLimit";
import { DeleteIcon, PauseIcon, PlayCircleOutlineIcon, SettingsIcon } from "../../../../../components/Main/Icons";
import Tooltip from "../../../../../components/Main/Tooltip/Tooltip";
import Modal from "../../../../../components/Main/Modal/Modal";
import Switch from "../../../../../components/Main/Switch/Switch";
import useBoolean from "../../../../../hooks/useBoolean";
import { Logs } from "../../../../../api/types";
interface LiveTailingSettingsProps {
settingsRef: RefObject<HTMLDivElement>;
rowsPerPage: number;
handleSetRowsPerPage: (limit: number) => void;
logs: Logs[];
isPaused: boolean;
handleResumeLiveTailing: () => void;
pauseLiveTailing: () => void;
clearLogs: () => void;
isCompactTailingNumber: boolean;
handleSetCompactTailing: (value: boolean) => void;
}
const LiveTailingSettings: FC<LiveTailingSettingsProps> = ({
settingsRef,
rowsPerPage,
handleSetRowsPerPage,
logs,
isPaused,
handleResumeLiveTailing,
pauseLiveTailing,
clearLogs,
isCompactTailingNumber,
handleSetCompactTailing
}) => {
const settingButtonRef = useRef<HTMLDivElement>(null);
const { value: isSettingsOpen, setFalse: closeSettings, setTrue: openSettings } = useBoolean(false);
const getLogs = useCallback(() => logs.map(({ _log_id, ...log }) => log), [logs]);
if (!settingsRef.current) return null;
return createPortal(
<div className="vm-live-tailing-view__settings">
<SelectLimit
limit={rowsPerPage}
onChange={handleSetRowsPerPage}
onOpenSelect={pauseLiveTailing}
/>
<div className="vm-live-tailing-view__settings-buttons">
{logs.length > 0 && <DownloadLogsButton getLogs={getLogs}/>}
{isPaused ? (
<Tooltip
title={"Resume live tailing"}
>
<Button
variant="text"
color="primary"
onClick={handleResumeLiveTailing}
startIcon={<PlayCircleOutlineIcon/>}
ariaLabel={"Resume live tailing"}
/>
</Tooltip>
) : (
<Tooltip
title={"Pause live tailing"}
>
<Button
variant="text"
color="primary"
onClick={pauseLiveTailing}
startIcon={<PauseIcon/>}
ariaLabel={"Pause live tailing"}
/>
</Tooltip>
)}
<Tooltip
title={"Clear logs"}
>
<Button
variant="text"
color="secondary"
onClick={clearLogs}
startIcon={<DeleteIcon/>}
ariaLabel={"Clear logs"}
/>
</Tooltip>
<Tooltip
title={"Settings"}
>
<Button
ref={settingButtonRef}
variant="text"
color="secondary"
onClick={openSettings}
startIcon={<SettingsIcon/>}
ariaLabel={"Settings"}
/>
</Tooltip>
{isSettingsOpen && <Modal
onClose={closeSettings}
title={"Live tailing settings"}
>
<div className="vm-live-tailing-view__settings-modal">
<div className={"vm-live-tailing-view__settings-modal-item"}>
<Switch
label={"Expandable Properties View"}
value={isCompactTailingNumber}
onChange={handleSetCompactTailing}
/>
<span className="vm-group-logs-configurator-item__info">
Switches log display to expandable properties view with additional visualization settings. Please note: when processing large volumes of data, it may increase system response time.
</span>
</div>
</div>
</Modal>}
</div>
</div>,
settingsRef.current
);
};
export default LiveTailingSettings;

View File

@@ -0,0 +1,151 @@
import { FC, useCallback, useEffect, useRef, useState } from "preact/compat";
import { ViewProps } from "../../types";
import useStateSearchParams from "../../../../../hooks/useStateSearchParams";
import useSearchParamsFromObject from "../../../../../hooks/useSearchParamsFromObject";
import "./style.scss";
import { useLiveTailingLogs } from "./useLiveTailingLogs";
import { LOGS_DISPLAY_FIELDS, LOGS_URL_PARAMS } from "../../../../../constants/logs";
import { useMemo } from "react";
import { useSearchParams } from "react-router-dom";
import throttle from "lodash/throttle";
import GroupLogsItem from "../../../GroupLogs/GroupLogsItem";
import LiveTailingSettings from "./LiveTailingSettings";
import Alert from "../../../../../components/Main/Alert/Alert";
import { isDecreasing } from "../../../../../utils/array";
const SCROLL_THRESHOLD = 100;
const scrollToBottom = () => window.scrollTo({
top: document.documentElement.scrollHeight,
behavior: "instant"
});
const throttledScrollToBottom = throttle(scrollToBottom, 200);
const LiveTailingView: FC<ViewProps> = ({ settingsRef }) => {
const containerRef = useRef<HTMLDivElement>(null);
const [isAtBottom, setIsAtBottom] = useState(true);
const [searchParams] = useSearchParams();
const { setSearchParamsFromKeys } = useSearchParamsFromObject();
const [rowsPerPage, setRowsPerPage] = useStateSearchParams(100, "rows_per_page");
const [query, _setQuery] = useStateSearchParams("*", "query");
const [isCompactTailingStr] = useStateSearchParams(0, "compact_tailing");
const isCompactTailingNumber = Boolean(Number(isCompactTailingStr));
const {
logs,
isPaused,
error,
startLiveTailing,
stopLiveTailing,
pauseLiveTailing,
resumeLiveTailing,
clearLogs,
isLimitedLogsPerUpdate
} = useLiveTailingLogs(query, rowsPerPage);
const displayFieldsString = searchParams.get(LOGS_URL_PARAMS.DISPLAY_FIELDS) || LOGS_DISPLAY_FIELDS;
const displayFields = useMemo(() => displayFieldsString.split(","), [displayFieldsString]);
const handleResumeLiveTailing = useCallback(() => {
throttledScrollToBottom();
resumeLiveTailing();
}, [resumeLiveTailing]);
const handleSetRowsPerPage = useCallback((limit: number) => {
setSearchParamsFromKeys({ rows_per_page: limit });
}, [setRowsPerPage, setSearchParamsFromKeys]);
const handleSetCompactTailing = useCallback((value: boolean) => {
setSearchParamsFromKeys({ compact_tailing: Number(value) });
}, [setSearchParamsFromKeys]);
useEffect(() => {
startLiveTailing();
return () => stopLiveTailing();
}, [startLiveTailing, stopLiveTailing]);
useEffect(() => {
const container = containerRef.current;
if (!container) return;
let prevScrollTop: number[] = [];
const handleScroll = () => {
const { scrollTop, scrollHeight, clientHeight } = document.documentElement;
const isBottom = Math.abs(scrollHeight - scrollTop - clientHeight) < SCROLL_THRESHOLD;
setIsAtBottom(isBottom);
prevScrollTop.push(scrollTop);
prevScrollTop = prevScrollTop.slice(-3);
const isMoveToTop = isDecreasing(prevScrollTop);
if (!isBottom && !isPaused && isMoveToTop) {
pauseLiveTailing();
}
};
document.addEventListener("scroll", handleScroll);
return () => document.removeEventListener("scroll", handleScroll);
}, [isPaused, pauseLiveTailing, resumeLiveTailing]);
useEffect(() => {
if (isAtBottom && !isPaused) {
throttledScrollToBottom();
}
}, [logs, isAtBottom]);
useEffect(() => {
handleResumeLiveTailing();
}, [rowsPerPage]);
if (error) {
return <div className="vm-live-tailing-view__error">{error}</div>;
}
return (
<>
<LiveTailingSettings
settingsRef={settingsRef}
rowsPerPage={rowsPerPage}
handleSetRowsPerPage={handleSetRowsPerPage}
logs={logs}
isPaused={isPaused}
handleResumeLiveTailing={handleResumeLiveTailing}
pauseLiveTailing={pauseLiveTailing}
clearLogs={clearLogs}
isCompactTailingNumber={isCompactTailingNumber}
handleSetCompactTailing={handleSetCompactTailing}
/>
<div
ref={containerRef}
className="vm-live-tailing-view__container"
>
{logs.length === 0
? (<div className="vm-live-tailing-view__empty">Waiting for logs...</div>)
: (<div className="vm-live-tailing-view__logs">
{logs.map(({ _log_id, ...log }, idx) =>
isCompactTailingNumber
? (
<GroupLogsItem
key={_log_id}
log={log}
onItemClick={pauseLiveTailing}
hideGroupButton={true}
displayFields={displayFields}
/>
) : (
<pre
key={idx}
className="vm-live-tailing-view__log-row"
>
{JSON.stringify(log)}
</pre>
)
)}
</div>
)}
</div>
{isLimitedLogsPerUpdate && (<Alert variant="warning">Too many logs per second detected. Large volumes of log data are difficult to process and may impact performance. We recommend adding filters to your query for better analysis and system performance.</Alert>)}
</>
);
};
export default LiveTailingView;

View File

@@ -0,0 +1,76 @@
@use "src/styles/variables" as *;
.vm-live-tailing-view {
&__settings {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
}
&__settings-modal {
max-width: 500px;
min-width: 300px;
}
&__settings-modal-item {
display: flex;
flex-direction: column;
gap: $padding-small;
}
&__settings-modal-item-info {
font-size: $font-size-small;
color: $color-text-secondary;
line-height: 130%;
}
&__settings-buttons {
display: flex;
align-items: center;
}
&__container {
width: 100%;
height: 100%;
overflow: auto;
min-height: 200px;
font-family: $font-family-monospace;
padding-bottom: $padding-medium;
}
&__empty {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
color: $color-text-secondary;
}
&__error {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
color: $color-error;
}
&__logs {
.vm-group-logs-row {
animation: highlight-fade 1s ease-out forwards;
}
}
&__log-row {
margin-top: $padding-small;
}
}
@keyframes highlight-fade {
0% {
background-color: $color-tropical-blue;
}
100% {
background-color: $color-background-block;
}
}

View File

@@ -0,0 +1,147 @@
import { act, renderHook } from "@testing-library/preact";
import { useLiveTailingLogs } from "./useLiveTailingLogs";
import { vi } from "vitest";
vi.mock("../../../../../state/common/StateContext", () => ({
useAppState: () => ({ serverUrl: "http://localhost:8080" }),
}));
vi.mock("../../../../../hooks/useTenant", () => ({
useTenant: () => ({}),
}));
// Mock dependencies
const mockFetch = vi.fn();
global.fetch = mockFetch;
const createMockStreamResponse = (logs: string[], sendCount: number = 1) => ({
ok: true,
body: new ReadableStream({
async start(controller) {
for (let i = 0; i < sendCount; i++) {
logs.forEach((log) => {
controller.enqueue(new TextEncoder().encode(log + "\n"));
});
await new Promise((resolve) => setTimeout(resolve, 1000));
}
controller.close();
},
}),
text: async () => logs.join("\n"),
});
describe("useLiveTailingLogs", () => {
afterEach(() => {
vi.restoreAllMocks();
vi.clearAllMocks();
});
it("should start live tailing and process logs", async () => {
const query = "*";
const limit = 10;
const { result } = renderHook(() => useLiveTailingLogs(query, limit));
mockFetch.mockResolvedValue(createMockStreamResponse(["{\"logs\":\"test log\"}"]));
await act(async () => {
const started = await result.current.startLiveTailing();
expect(started).toBe(true);
});
expect(mockFetch).toHaveBeenCalledTimes(1);
expect(mockFetch).toHaveBeenCalledWith(
"http://localhost:8080/select/logsql/tail",
expect.objectContaining({
method: "POST",
body: new URLSearchParams({
query: query.trim(),
}),
})
);
});
it("should pause and resume live tailing", () => {
const query = "*";
const limit = 10;
const { result } = renderHook(() => useLiveTailingLogs(query, limit));
act(() => {
result.current.pauseLiveTailing();
});
expect(result.current.isPaused).toBe(true);
act(() => {
result.current.resumeLiveTailing();
});
expect(result.current.isPaused).toBe(false);
});
it("should stop live tailing", async () => {
const query = "*";
const limit = 10;
const { result } = renderHook(() => useLiveTailingLogs(query, limit));
act(() => {
result.current.stopLiveTailing();
});
expect(result.current.logs).toHaveLength(0);
});
it("should clear logs", () => {
const query = "*";
const limit = 10;
const { result } = renderHook(() => useLiveTailingLogs(query, limit));
act(() => {
result.current.clearLogs();
});
expect(result.current.logs).toEqual([]);
});
it("should handle errors during live tailing", async () => {
const query = "*";
const limit = 10;
const { result } = renderHook(() => useLiveTailingLogs(query, limit));
mockFetch.mockRejectedValue(new Error("Network error"));
await act(async () => {
const started = await result.current.startLiveTailing();
expect(started).toBe(false);
});
expect(result.current.error).toBe("Error: Network error");
expect(result.current.logs).toHaveLength(0);
});
it("should process high load of logs incoming at 100k logs per second", async () => {
const query = "*";
const limit = 1000;
const logCount = 10000; // High log rate
const logs = Array.from({ length: logCount }, (_, i) => `{"log": "log message ${i}"}`);
const { result } = renderHook(() => useLiveTailingLogs(query, limit));
mockFetch.mockResolvedValue(createMockStreamResponse(logs, 7));
await act(async () => {
const started = await result.current.startLiveTailing();
expect(started).toBe(true);
});
// Wait for logs to process
await new Promise((resolve) => setTimeout(resolve, 7000));
// Verify logs are limited and processed correctly
expect(result.current.logs.length).toBeLessThanOrEqual(limit);
// After setting flag isLimitedLogsPerUpdate when more than 200 logs received 5 times in a row,
// we take only the last 200 logs, so we get 800 older logs (9200 - 9999) and 200 new logs (9800-9999)
expect(result.current.logs[0].log).toStrictEqual("log message 9200");
expect(result.current.logs[799].log).toStrictEqual("log message 9999");
expect(result.current.isLimitedLogsPerUpdate).toBeTruthy();
}, { timeout: 9000 });
});

View File

@@ -0,0 +1,269 @@
import { useCallback, useEffect, useRef, useState } from "preact/compat";
import { ErrorTypes } from "../../../../../types";
import { Logs } from "../../../../../api/types";
import { useAppState } from "../../../../../state/common/StateContext";
import useBoolean from "../../../../../hooks/useBoolean";
import { useTenant } from "../../../../../hooks/useTenant";
/**
* Defines the maximum number of consecutive times logs can be fetched above the threshold
* before showing a warning notification, and vice versa:
* - If logs are fetched above a threshold this many times in a row -> show warning
* - If warning is shown, it won't disappear until logs are fetched below a threshold
* this many times in a row
*
* This threshold helps optimize log display performance when dealing with large volumes of logs.
* If the threshold is consistently exceeded, users will be prompted to add filters to their query
* for better system performance and more focused log analysis.
*/
const MAX_ATTEMPTS_FETCH_LOGS_PER_SECOND = 5;
/**
* Defines the log's threshold, after which will be shown a warning notification
*/
const LOGS_THRESHOLD = 200;
const CONNECTION_TIMEOUT_MS = 5000;
const PROCESSING_INTERVAL_MS = 1000;
const createStreamProcessor = (
bufferRef: React.MutableRefObject<string>,
bufferLinesRef: React.MutableRefObject<string[]>,
setError: (error: string) => void,
restartTailing: () => Promise<boolean>
) => {
return async (reader: ReadableStreamDefaultReader<Uint8Array>) => {
let lastDataTime = Date.now();
const connectionCheckInterval = setInterval(() => {
const timeSinceLastData = Date.now() - lastDataTime;
if (timeSinceLastData > CONNECTION_TIMEOUT_MS) {
clearInterval(connectionCheckInterval);
restartTailing();
return;
}
}, CONNECTION_TIMEOUT_MS);
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
lastDataTime = Date.now();
const chunk = new TextDecoder().decode(value);
const lines = (bufferRef.current + chunk).split("\n");
bufferRef.current = lines.pop() || "";
bufferLinesRef.current = [...bufferLinesRef.current, ...lines];
}
} catch (e) {
if (e instanceof Error && e.name !== "AbortError") {
console.error("Stream processing error:", e);
setError(String(e));
}
} finally {
clearInterval(connectionCheckInterval);
}
};
};
const updateLimitModeTracking = (
linesCount: number,
attemptsFetchLimitRef: React.MutableRefObject<number>,
attemptsFetchLowRef: React.MutableRefObject<number>,
isLimitedLogsPerUpdate: boolean,
) => {
if (linesCount > LOGS_THRESHOLD) {
attemptsFetchLimitRef.current++;
attemptsFetchLowRef.current = 0;
} else {
attemptsFetchLowRef.current++;
attemptsFetchLimitRef.current = 0;
}
if (attemptsFetchLimitRef.current > MAX_ATTEMPTS_FETCH_LOGS_PER_SECOND) {
return true;
}
if (attemptsFetchLowRef.current > MAX_ATTEMPTS_FETCH_LOGS_PER_SECOND) {
return false;
}
return isLimitedLogsPerUpdate;
};
const parseLogLines = (lines: string[], counterRef: React.MutableRefObject<bigint>): Logs[] => {
return lines
.map(line => {
try {
const parsedLine = line && JSON.parse(line);
parsedLine._log_id = counterRef.current++;
return parsedLine;
} catch (e) {
console.error(`Failed to parse "${line}" to JSON\n`, e);
return null;
}
})
.filter(Boolean) as Logs[];
};
interface ProcessBufferedLogsParams {
lines: string[];
limit: number;
counterRef: React.MutableRefObject<bigint>;
attemptsFetchLimitRef: React.MutableRefObject<number>;
attemptsFetchLowRef: React.MutableRefObject<number>;
setIsLimitedLogsPerUpdate: (isLimited: boolean) => void;
setLogs: React.Dispatch<React.SetStateAction<Logs[]>>;
bufferLinesRef: React.MutableRefObject<string[]>;
isLimitedLogsPerUpdate: boolean;
}
const processBufferedLogs = ({
lines,
limit,
counterRef,
attemptsFetchLimitRef,
attemptsFetchLowRef,
setIsLimitedLogsPerUpdate,
setLogs,
bufferLinesRef,
isLimitedLogsPerUpdate
}: ProcessBufferedLogsParams) => {
const isLimitLogsMode = updateLimitModeTracking(lines.length, attemptsFetchLimitRef, attemptsFetchLowRef, isLimitedLogsPerUpdate);
const limitedLines = isLimitLogsMode && lines.length > LOGS_THRESHOLD ? lines.slice(-LOGS_THRESHOLD) : lines;
const newLogs = parseLogLines(limitedLines, counterRef);
setIsLimitedLogsPerUpdate(isLimitLogsMode);
setLogs(prevLogs => {
const combinedLogs = [...prevLogs, ...newLogs];
return combinedLogs.length > limit ? combinedLogs.slice(-limit) : combinedLogs;
});
bufferLinesRef.current = [];
};
export const useLiveTailingLogs = (query: string, limit: number) => {
const { serverUrl } = useAppState();
const [logs, setLogs] = useState<Logs[]>([]);
const { value: isPaused, setTrue: pauseLiveTailing, setFalse: resumeLiveTailing } = useBoolean(false);
const tenant = useTenant();
const [error, setError] = useState<ErrorTypes | string>();
const [isLimitedLogsPerUpdate, setIsLimitedLogsPerUpdate] = useState(false);
const counterRef = useRef<bigint>(0n);
const abortControllerRef = useRef(new AbortController());
const readerRef = useRef<ReadableStreamDefaultReader<Uint8Array> | null>(null);
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
const bufferRef = useRef<string>("");
const bufferLinesRef = useRef<string[]>([]);
const attemptsFetchLimitLogsPerSecondCountRef = useRef<number>(0);
const attemptsFetchLowLogsPerSecondCountRef = useRef<number>(0);
const stopLiveTailing = useCallback(() => {
if (readerRef.current) {
readerRef.current.cancel();
readerRef.current = null;
}
if (intervalRef.current) {
clearInterval(intervalRef.current);
}
if (bufferRef.current) {
bufferRef.current = "";
}
abortControllerRef.current.abort();
}, []);
const startLiveTailing = useCallback(async () => {
stopLiveTailing();
abortControllerRef.current = new AbortController();
const { signal } = abortControllerRef.current;
setError(undefined);
try {
const response = await fetch(`${serverUrl}/select/logsql/tail`, {
signal,
method: "POST",
headers: {
...tenant,
},
body: new URLSearchParams({
query: query.trim(),
})
});
if (!response.ok || !response.body) {
const text = await response.text();
setError(text);
setLogs([]);
return false;
}
const reader = response.body.getReader();
readerRef.current = reader;
const processStream = createStreamProcessor(
bufferRef,
bufferLinesRef,
setError,
startLiveTailing
);
processStream(reader);
return true;
} catch (e) {
if (e instanceof Error && e.name !== "AbortError") {
setError(String(e));
console.error(e);
setLogs([]);
}
return false;
}
}, [query, stopLiveTailing]);
useEffect(() => {
if (isPaused) {
const pauseTimerId = setInterval(() => {
if (bufferLinesRef.current.length > limit) {
bufferLinesRef.current = bufferLinesRef.current.slice(-limit);
}
}, PROCESSING_INTERVAL_MS);
return () => {
clearInterval(pauseTimerId);
};
}
const timerId = setInterval(() => {
const lines = bufferLinesRef.current;
processBufferedLogs({
lines,
limit,
counterRef,
attemptsFetchLimitRef: attemptsFetchLimitLogsPerSecondCountRef,
attemptsFetchLowRef: attemptsFetchLowLogsPerSecondCountRef,
setIsLimitedLogsPerUpdate,
isLimitedLogsPerUpdate,
setLogs,
bufferLinesRef
});
}, PROCESSING_INTERVAL_MS);
return () => clearInterval(timerId);
}, [limit, isPaused, isLimitedLogsPerUpdate]);
const clearLogs = useCallback(() => {
setLogs([]);
}, []);
return {
logs,
isPaused,
error,
startLiveTailing,
stopLiveTailing,
pauseLiveTailing,
resumeLiveTailing,
clearLogs,
isLimitedLogsPerUpdate
};
};

View File

@@ -0,0 +1,80 @@
import React, { FC, useMemo, useState } from "preact/compat";
import DownloadLogsButton from "../../../DownloadLogsButton/DownloadLogsButton";
import { createPortal } from "preact/compat";
import "./style.scss";
import { ViewProps } from "../../types";
import useBoolean from "../../../../../hooks/useBoolean";
import useStateSearchParams from "../../../../../hooks/useStateSearchParams";
import TableLogs from "../../TableLogs";
import SelectLimit from "../../../../../components/Main/Pagination/SelectLimit/SelectLimit";
import TableSettings from "../../../../../components/Table/TableSettings/TableSettings";
import useSearchParamsFromObject from "../../../../../hooks/useSearchParamsFromObject";
import EmptyLogs from "../components/EmptyLogs/EmptyLogs";
import { useCallback } from "react";
const MemoizedTableView = React.memo(TableLogs);
const TableView: FC<ViewProps> = ({ data, settingsRef }) => {
const { setSearchParamsFromKeys } = useSearchParamsFromObject();
const [displayColumns, setDisplayColumns] = useState<string[]>([]);
const [rowsPerPage, setRowsPerPage] = useStateSearchParams(100, "rows_per_page");
const { value: tableCompact, toggle: toggleTableCompact } = useBoolean(false);
const columns = useMemo(() => {
const keys = new Set<string>();
for (const item of data) {
for (const key in item) {
keys.add(key);
}
}
return Array.from(keys).sort((a,b) => a.localeCompare(b));
}, [data]);
const handleSetRowsPerPage = (limit: number) => {
setRowsPerPage(limit);
setSearchParamsFromKeys({ rows_per_page: limit });
};
const getLogs = useCallback(() => data, [data]);
const renderSettings = () => {
if (!settingsRef.current) return null;
return createPortal(
<div className="vm-table-view__settings">
<SelectLimit
limit={rowsPerPage}
onChange={handleSetRowsPerPage}
/>
<div className="vm-table-view__settings-buttons">
{data.length > 0 && <DownloadLogsButton getLogs={getLogs} />}
<TableSettings
columns={columns}
selectedColumns={displayColumns}
onChangeColumns={setDisplayColumns}
tableCompact={tableCompact}
toggleTableCompact={toggleTableCompact}
/>
</div>
</div>,
settingsRef.current
);
};
if (!data.length) return <EmptyLogs />;
return (
<>
{renderSettings()}
<MemoizedTableView
logs={data}
displayColumns={displayColumns}
tableCompact={tableCompact}
columns={columns}
rowsPerPage={Number(rowsPerPage)}
/>
</>
);
};
export default TableView;

View File

@@ -0,0 +1,10 @@
@use "src/styles/variables" as *;
.vm-table-view {
&__settings,
&__settings-buttons {
display: flex;
align-items: center;
gap: $padding-small;
}
}

View File

@@ -0,0 +1,10 @@
import { FC } from "preact/compat";
import "./style.scss";
const EmptyLogs: FC = () => {
return (
<div className="vm-explore-logs-body__empty">No logs found</div>
);
};
export default EmptyLogs;

View File

@@ -0,0 +1,12 @@
@use "src/styles/variables" as *;
.vm-explore-logs-body {
&__empty {
display: flex;
align-items: center;
justify-content: center;
min-height: 120px;
color: $color-text-disabled;
text-align: center;
}
}

View File

@@ -17,9 +17,9 @@ import Pagination from "../../../components/Main/Pagination/Pagination";
import SelectLimit from "../../../components/Main/Pagination/SelectLimit/SelectLimit";
import { usePaginateGroups } from "../hooks/usePaginateGroups";
import { GroupLogsType } from "../../../types";
import { getNanoTimestamp } from "../../../utils/time";
import useDeviceDetect from "../../../hooks/useDeviceDetect";
import DownloadLogsButton from "../DownloadLogsButton/DownloadLogsButton";
import { hasSortPipe } from "../../../components/Configurators/QueryEditor/LogsQL/utils/sort";
interface Props {
logs: Logs[];
@@ -30,6 +30,9 @@ const GroupLogs: FC<Props> = ({ logs, settingsRef }) => {
const { isMobile } = useDeviceDetect();
const [searchParams, setSearchParams] = useSearchParams();
const query = searchParams.get("query") || "";
const queryHasSort = hasSortPipe(query);
const [page, setPage] = useState(1);
const [expandGroups, setExpandGroups] = useState<boolean[]>([]);
@@ -47,15 +50,10 @@ const GroupLogs: FC<Props> = ({ logs, settingsRef }) => {
const streamValue = item.values[0]?.[groupBy] || "";
const pairs = getStreamPairs(streamValue);
// values sorting by time
const values = item.values.sort((a, b) => {
const aTimestamp = getNanoTimestamp(a._time);
const bTimestamp = getNanoTimestamp(b._time);
if (aTimestamp < bTimestamp) return 1;
if (aTimestamp > bTimestamp) return -1;
return 0;
});
// VictoriaLogs sends rows oldest → newest when the query has no `| sort` pipe,
// so we reverse the array to put the newest entries first.
// If a sort is already specified, keep the original order.
const values = queryHasSort ? item.values : item.values.toReversed();
return {
keys: item.keys,
@@ -64,8 +62,8 @@ const GroupLogs: FC<Props> = ({ logs, settingsRef }) => {
pairs,
total: values.length,
};
}).sort((a, b) => b.values.length - a.values.length); // groups sorting
}, [logs, groupBy]);
}).sort((a, b) => b.total - a.total); // groups sorting
}, [logs, groupBy, queryHasSort]);
const paginatedGroups = usePaginateGroups(groupData, page, rowsPerPage);
@@ -96,6 +94,8 @@ const GroupLogs: FC<Props> = ({ logs, settingsRef }) => {
window.scrollTo({ top: 0 });
};
const getLogs = useCallback(() => logs, [logs]);
useEffect(() => {
setExpandGroups(new Array(groupData.length).fill(!isMobile));
}, [groupData]);
@@ -162,7 +162,7 @@ const GroupLogs: FC<Props> = ({ logs, settingsRef }) => {
ariaLabel={expandAll ? "Collapse All" : "Expand All"}
/>
</Tooltip>
<DownloadLogsButton logs={logs} />
<DownloadLogsButton getLogs={getLogs}/>
<GroupLogsConfigurators logs={logs}/>
</div>
), settingsRef.current)}

View File

@@ -1,4 +1,4 @@
import React, { FC, memo, useCallback, useEffect, useState } from "preact/compat";
import { FC, memo, useCallback, useEffect, useState } from "preact/compat";
import Tooltip from "../../../components/Main/Tooltip/Tooltip";
import Button from "../../../components/Main/Button/Button";
import { CopyIcon, StorageIcon, VisibilityIcon } from "../../../components/Main/Icons";
@@ -9,9 +9,10 @@ import { LOGS_GROUP_BY, LOGS_URL_PARAMS } from "../../../constants/logs";
interface Props {
field: string;
value: string;
hideGroupButton?: boolean;
}
const GroupLogsFieldRow: FC<Props> = ({ field, value }) => {
const GroupLogsFieldRow: FC<Props> = ({ field, value, hideGroupButton }) => {
const copyToClipboard = useCopyToClipboard();
const [searchParams, setSearchParams] = useSearchParams();
@@ -75,20 +76,22 @@ const GroupLogsFieldRow: FC<Props> = ({ field, value }) => {
size="small"
startIcon={isSelectedField ? <VisibilityIcon/> : <VisibilityIcon/>}
onClick={handleSelectDisplayField}
ariaLabel="copy to clipboard"
/>
</Tooltip>
<Tooltip title={isGroupByField ? "Ungroup this field" : "Group by this field"}>
<Button
className="vm-group-logs-row-fields-item-controls__button"
variant="text"
color={isGroupByField ? "secondary" : "gray"}
size="small"
startIcon={<StorageIcon/>}
onClick={handleSelectGroupBy}
ariaLabel="copy to clipboard"
ariaLabel={isSelectedField ? "Hide this field" : "Show this field instead of the message"}
/>
</Tooltip>
{!hideGroupButton && (
<Tooltip title={isGroupByField ? "Ungroup this field" : "Group by this field"}>
<Button
className="vm-group-logs-row-fields-item-controls__button"
variant="text"
color={isGroupByField ? "secondary" : "gray"}
size="small"
startIcon={<StorageIcon/>}
onClick={handleSelectGroupBy}
ariaLabel={isGroupByField ? "Ungroup this field" : "Group by this field"}
/>
</Tooltip>
)}
</div>
</td>
<td className="vm-group-logs-row-fields-item__key">{field}</td>

View File

@@ -8,9 +8,10 @@ import { getFromStorage } from "../../../utils/storage";
interface Props {
log: Logs;
hideGroupButton?: boolean;
}
const GroupLogsFields: FC<Props> = ({ log }) => {
const GroupLogsFields: FC<Props> = ({ log, hideGroupButton }) => {
const sortedFields = useMemo(() => {
return Object.entries(log)
.sort(([aKey], [bKey]) => aKey.localeCompare(bKey));
@@ -41,6 +42,7 @@ const GroupLogsFields: FC<Props> = ({ log }) => {
key={key}
field={key}
value={value}
hideGroupButton={hideGroupButton}
/>
))}
</tbody>

View File

@@ -18,9 +18,11 @@ import GroupLogsFields from "./GroupLogsFields";
interface Props {
log: Logs;
displayFields?: string[];
hideGroupButton?: boolean;
onItemClick?: (log: Logs) => void;
}
const GroupLogsItem: FC<Props> = ({ log, displayFields = ["_msg"] }) => {
const GroupLogsItem: FC<Props> = ({ log, displayFields = ["_msg"], onItemClick, hideGroupButton }) => {
const {
value: isOpenFields,
toggle: toggleOpenFields,
@@ -75,6 +77,11 @@ const GroupLogsItem: FC<Props> = ({ log, displayFields = ["_msg"] }) => {
}
};
const handleClick = () => {
toggleOpenFields();
onItemClick?.(log);
};
useEventListener("storage", handleUpdateStage);
return (
@@ -84,7 +91,7 @@ const GroupLogsItem: FC<Props> = ({ log, displayFields = ["_msg"] }) => {
"vm-group-logs-row-content": true,
"vm-group-logs-row-content_interactive": !disabledHovers,
})}
onClick={toggleOpenFields}
onClick={handleClick}
>
{hasFields && (
<div
@@ -123,7 +130,10 @@ const GroupLogsItem: FC<Props> = ({ log, displayFields = ["_msg"] }) => {
))}
</div>
</div>
{hasFields && isOpenFields && <GroupLogsFields log={log}/>}
{hasFields && isOpenFields && <GroupLogsFields
hideGroupButton={hideGroupButton}
log={log}
/>}
</div>
);
};

View File

@@ -101,7 +101,7 @@ const Relabel: FC = () => {
<a
className="vm-link vm-link_with-icon"
target="_blank"
href="https://docs.victoriametrics.com/victoriametrics/vmagent/#relabeling"
href="https://docs.victoriametrics.com/victoriametrics/relabeling/"
rel="help noreferrer"
>
<WikiIcon/>
@@ -154,13 +154,13 @@ const Relabel: FC = () => {
<div className="vm-relabeling-steps-item__row">
<span>Input Labels:</span>
<code>
<pre dangerouslySetInnerHTML={{ __html: step.inLabels }}/>
<pre dangerouslySetInnerHTML={{ __html: step.errors?.inLabels || step.inLabels }}/>
</code>
</div>
<div className="vm-relabeling-steps-item__row">
<span>Output labels:</span>
<code>
<pre dangerouslySetInnerHTML={{ __html: step.outLabels }}/>
<pre dangerouslySetInnerHTML={{ __html: step.errors?.outLabels || step.outLabels }}/>
</code>
</div>
</div>

View File

@@ -138,6 +138,10 @@ export interface RelabelStep {
rule: string;
inLabels: string;
outLabels: string;
errors: {
inLabels: string;
outLabels: string;
}
}
export interface RelabelData {

View File

@@ -0,0 +1,36 @@
import { describe, expect, it } from "vitest";
import { isDecreasing } from "./array";
describe("isDecreasing", () => {
it("should return true for an array with strictly decreasing numbers", () => {
expect(isDecreasing([5, 4, 3, 2, 1])).toBe(true);
});
it("should return false for an array with increasing numbers", () => {
expect(isDecreasing([1, 2, 3, 4, 5])).toBe(false);
});
it("should return false for an array with equal consecutive numbers", () => {
expect(isDecreasing([5, 5, 4, 3, 2])).toBe(false);
});
it("should return false for an empty array", () => {
expect(isDecreasing([])).toBe(false);
});
it("should return false for an array with a single element", () => {
expect(isDecreasing([1])).toBe(false);
});
it("should return false for an array with both increasing and decreasing numbers", () => {
expect(isDecreasing([5, 3, 4, 2, 1])).toBe(false);
});
it("should return true for an array with negative strictly decreasing numbers", () => {
expect(isDecreasing([-1, -2, -3, -4])).toBe(true);
});
it("should return false for an array with a mix of positive and negative numbers that do not strictly decrease", () => {
expect(isDecreasing([3, 2, -1, -1])).toBe(false);
});
});

View File

@@ -1,4 +1,4 @@
export const arrayEquals = (a: (string|number)[], b: (string|number)[]) => {
export const arrayEquals = (a: (string | number)[], b: (string | number)[]) => {
return a.length === b.length && a.every((val, index) => val === b[index]);
};
@@ -17,3 +17,8 @@ export function groupByMultipleKeys<T>(items: T[], keys: (keyof T)[]): { keys: s
}));
}
export const isDecreasing = (arr: number[]): boolean => {
if (arr.length < 2) return false;
return arr.every((v, i) => i === 0 || v < arr[i - 1]);
};

View File

@@ -20,7 +20,9 @@
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"jsxImportSource": "preact",
"downlevelIteration": true,
"noUnusedLocals": true,
"paths": {
"react": ["./node_modules/preact/compat/"],
"react/jsx-runtime": ["./node_modules/preact/jsx-runtime"],

View File

@@ -8,6 +8,28 @@ const getProxy = (): Record<string, ProxyOptions> | undefined => {
const playground = process.env.PLAYGROUND;
switch (playground) {
case "METRICS": {
return {
"^/vmalert/.*": {
target: "https://play.victoriametrics.com",
changeOrigin: true,
configure: (proxy) => {
proxy.on("error", (err) => {
console.error("[proxy error]", err.message);
});
}
},
"^/api/.*": {
target: "https://play.victoriametrics.com/select/0/prometheus/",
changeOrigin: true,
configure: (proxy) => {
proxy.on("error", (err) => {
console.error("[proxy error]", err.message);
});
}
}
};
}
case "LOGS": {
return {
"^/select/.*": {

View File

@@ -31,6 +31,7 @@ type app struct {
binary string
flags []string
process *os.Process
wait bool
}
// appOptions holds the optional configuration of an app, such as default flags
@@ -38,6 +39,7 @@ type app struct {
type appOptions struct {
defaultFlags map[string]string
extractREs []*regexp.Regexp
wait bool
}
// startApp starts an instance of an app using the app binary file path and
@@ -73,6 +75,7 @@ func startApp(instance string, binary string, flags []string, opts *appOptions)
binary: binary,
flags: flags,
process: cmd.Process,
wait: opts.wait,
}
go app.processOutput("stdout", stdout, app.writeToStderr)
@@ -92,7 +95,11 @@ func startApp(instance string, binary string, flags []string, opts *appOptions)
return nil, nil, err
}
return app, extracts, nil
if app.wait {
err = cmd.Wait()
}
return app, extracts, err
}
// setDefaultFlags adds flags with default values to `flags` if it does not
@@ -112,9 +119,12 @@ func setDefaultFlags(flags []string, defaultFlags map[string]string) []string {
return flags
}
// stop sends the app process a SIGINT signal and waits until it terminates
// Stop sends the app process a SIGINT signal and waits until it terminates
// gracefully.
func (app *app) Stop() {
if app.wait {
return
}
if err := app.process.Signal(os.Interrupt); err != nil {
log.Fatalf("Could not send SIGINT signal to %s process: %v", app.instance, err)
}

View File

@@ -21,6 +21,7 @@ type PrometheusQuerier interface {
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
PrometheusAPIV1ExportNative(t *testing.T, query string, opts QueryOpts) []byte
}
// Writer contains methods for writing new data
@@ -29,6 +30,7 @@ type Writer interface {
PrometheusAPIV1Write(t *testing.T, records []pb.TimeSeries, opts QueryOpts)
PrometheusAPIV1ImportPrometheus(t *testing.T, records []string, opts QueryOpts)
PrometheusAPIV1ImportCSV(t *testing.T, records []string, opts QueryOpts)
PrometheusAPIV1ImportNative(t *testing.T, data []byte, opts QueryOpts)
// Graphit APIs
GraphiteWrite(t *testing.T, records []string, opts QueryOpts)

View File

@@ -7,8 +7,9 @@ import (
"testing"
"time"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/fs"
"github.com/google/go-cmp/cmp"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/fs"
)
// TestCase holds the state and defines clean-up procedure common for all test
@@ -27,6 +28,7 @@ type Stopper interface {
// NewTestCase creates a new test case.
func NewTestCase(t *testing.T) *TestCase {
t.Parallel()
return &TestCase{t, NewClient(), make(map[string]Stopper)}
}
@@ -189,7 +191,7 @@ func (tc *TestCase) MustStartVmauth(instance string, flags []string, configFileY
// MustStartDefaultCluster starts a typical cluster configuration with default
// flags.
func (tc *TestCase) MustStartDefaultCluster() PrometheusWriteQuerier {
func (tc *TestCase) MustStartDefaultCluster() *Vmcluster {
tc.t.Helper()
return tc.MustStartCluster(&ClusterOptions{
@@ -223,7 +225,7 @@ type ClusterOptions struct {
}
// MustStartCluster starts a typical cluster configuration with custom flags.
func (tc *TestCase) MustStartCluster(opts *ClusterOptions) PrometheusWriteQuerier {
func (tc *TestCase) MustStartCluster(opts *ClusterOptions) *Vmcluster {
tc.t.Helper()
opts.Vmstorage1Flags = append(opts.Vmstorage1Flags, []string{
@@ -251,6 +253,18 @@ func (tc *TestCase) MustStartCluster(opts *ClusterOptions) PrometheusWriteQuerie
return &Vmcluster{vminsert, vmselect, []*Vmstorage{vmstorage1, vmstorage2}}
}
// MustStartVmctl is a test helper function that starts an instance of vmctl
func (tc *TestCase) MustStartVmctl(instance string, flags []string) *Vmctl {
tc.t.Helper()
app, err := StartVmctl(instance, flags)
if err != nil {
tc.t.Fatalf("Could not start %s: %v", instance, err)
}
tc.addApp(instance, app)
return app
}
func (tc *TestCase) addApp(instance string, app Stopper) {
if _, alreadyStarted := tc.startedApps[instance]; alreadyStarted {
tc.t.Fatalf("%s has already been started", instance)

View File

@@ -0,0 +1,73 @@
package tests
import (
"os"
"testing"
at "github.com/VictoriaMetrics/VictoriaMetrics/apptest"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
)
func TestSingleExportImportNative(t *testing.T) {
os.RemoveAll(t.Name())
tc := at.NewTestCase(t)
defer tc.Stop()
sut := tc.MustStartDefaultVmsingle()
testExportImportNative(tc.T(), sut)
}
func TestClusterExportImportNative(t *testing.T) {
os.RemoveAll(t.Name())
tc := at.NewTestCase(t)
defer tc.Stop()
sut := tc.MustStartDefaultCluster()
testExportImportNative(tc.T(), sut)
}
// testExportImportNative test export and import in VictoriaMetrics native format.
// see: https://docs.victoriametrics.com/#how-to-import-data-in-native-format
func testExportImportNative(t *testing.T, sut at.PrometheusWriteQuerier) {
// create test data
sut.PrometheusAPIV1ImportPrometheus(t, []string{
`native_export_import 10 1707123456700`, // 2024-02-05T08:57:36.700Z
}, at.QueryOpts{
ExtraLabels: []string{"el1=elv1", "el2=elv2"},
})
sut.ForceFlush(t)
// export test data via native export API
exportResult := sut.PrometheusAPIV1ExportNative(t, "native_export_import", at.QueryOpts{
Start: "2024-02-05T08:50:00.700Z",
End: "2024-02-05T09:00:00.700Z",
})
// re-import test data via native import API
sut.PrometheusAPIV1ImportNative(t, exportResult, at.QueryOpts{})
sut.ForceFlush(t)
// check query result
got := sut.PrometheusAPIV1QueryRange(t, "native_export_import", at.QueryOpts{
Start: "2024-02-05T08:57:36.700Z",
End: "2024-02-05T08:57:36.700Z",
Step: "60s",
})
cmpOptions := []cmp.Option{
cmpopts.IgnoreFields(at.PrometheusAPIV1QueryResponse{}, "Status", "Data.ResultType"),
cmpopts.EquateNaNs(),
}
want := at.NewPrometheusAPIV1QueryResponse(t, `{"data": {"result": [{"metric": {"__name__": "native_export_import", "el1": "elv1", "el2":"elv2"}, "values": []}]}}`)
want.Data.Result[0].Samples = []*at.Sample{
at.NewSample(t, "2024-02-05T08:57:36.700Z", 10),
}
if diff := cmp.Diff(want, got, cmpOptions...); diff != "" {
t.Errorf("unexpected response (-want, +got):\n%s", diff)
}
}

View File

@@ -216,4 +216,15 @@ func TestClusterMultiTenantSelect(t *testing.T) {
t.Errorf("unexpected response (-want, +got):\n%s", diff)
}
if got := vmselect.GetIntMetric(t, `vm_cache_requests_total{type="multitenancy/tenants"}`); got != 0 {
t.Errorf("unexpected multitenancy tenants cache requests; got %d; want 0", got)
}
if got := vmselect.GetIntMetric(t, `vm_cache_misses_total{type="multitenancy/tenants"}`); got != 0 {
t.Errorf("unexpected multitenancy tenants cache misses; got %d; want 0", got)
}
if got := vmselect.GetIntMetric(t, `vm_cache_entries{type="multitenancy/tenants"}`); got != 0 {
t.Errorf("unexpected multitenancy tenants cache entries; got %d; want 0", got)
}
}

View File

@@ -5,8 +5,9 @@ import (
"regexp"
"testing"
at "github.com/VictoriaMetrics/VictoriaMetrics/apptest"
"github.com/google/go-cmp/cmp"
at "github.com/VictoriaMetrics/VictoriaMetrics/apptest"
)
// snapshotNameRE covers years 1970-2099.
@@ -104,7 +105,7 @@ func TestClusterSnapshots_CreateListDelete(t *testing.T) {
tc := at.NewTestCase(t)
defer tc.Stop()
sut := tc.MustStartDefaultCluster().(*at.Vmcluster)
sut := tc.MustStartDefaultCluster()
// Insert some data.
const numSamples = 1000

View File

@@ -0,0 +1,40 @@
{
"status": "success",
"data": {
"resultType": "matrix",
"result": [
{
"metric": {
"__name__": "vm_log_messages_total",
"job": "victoriametrics",
"instance": "victoriametrics:8428",
"app_version": "victoria-metrics-20250523-133235-tags-v1.118.0-0-gaa3171cf4b",
"level": "info",
"location": "VictoriaMetrics/lib/ingestserver/opentsdb/server.go:48"
},
"values": [
[
1748897918.112,
"1"
]
]
},
{
"metric": {
"__name__": "vm_log_messages_total",
"job": "victoriametrics",
"instance": "victoriametrics:8428",
"app_version": "victoria-metrics-20250523-133235-tags-v1.118.0-0-gaa3171cf4b",
"level": "info",
"location": "VictoriaMetrics/lib/ingestserver/opentsdb/server.go:59"
},
"values": [
[
1748897918.112,
"1"
]
]
}
]
}
}

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