Compare commits

..

191 Commits

Author SHA1 Message Date
Zakhar Bessarab
5af6b108e0 app/vmselect/netstorage: make linter happy
Signed-off-by: Zakhar Bessarab <z.bessarab@victoriametrics.com>
2025-01-27 20:38:00 +04:00
Zakhar Bessarab
ac602476dc app/vmselect/netstorage: apply review fixes feedback
Signed-off-by: Zakhar Bessarab <z.bessarab@victoriametrics.com>
2025-01-27 20:35:21 +04:00
Zakhar Bessarab
0a1638acd5 app/vmselect/netstorage: fix spelling
Signed-off-by: Zakhar Bessarab <z.bessarab@victoriametrics.com>
2025-01-27 20:35:21 +04:00
Zakhar Bessarab
11bb4ef7a7 app/vmselect: enforce memory limits for label/label-values/series APIs
Respect limit set via `search.maxMemoryPerQuery` at `/api/v1/labels`, `/api/v1/label/{}/values` and `/api/v1/series` endpoints.

Signed-off-by: Zakhar Bessarab <z.bessarab@victoriametrics.com>
2025-01-27 20:35:17 +04:00
Zakhar Bessarab
35ddfc5d62 docs/{vmauth,VictoriaLogs}: add examples of using headers per-user
### Describe Your Changes
- add example a generic example to vmauth docs
- add an multi-tenancy usage example to VictoriaLogs docs
### Checklist

The following checks are **mandatory**:

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

Signed-off-by: Zakhar Bessarab <z.bessarab@victoriametrics.com>
(cherry picked from commit de144899b2)
2025-01-27 20:16:03 +04:00
Aliaksandr Valialkin
d60238d917 lib/storage: open per-month partitions in parallel
This should reduce the time needed for opening the storage with retentions exceeding a few months.

While at at, limit the concurrency of opening partitions in parallel to the number of available CPU cores,
since higher concurrency may increase RAM usage and CPU usage without performance improvements
if opening a single partition is CPU-bound task.

This is a follow-up for 17988942ab
2025-01-27 16:12:25 +01:00
Aliaksandr Valialkin
bca61bdd35 lib/filestream: use correct formatting option for error type in the error message 2025-01-27 15:24:11 +01:00
f41gh7
6168ab3b3d docs/changelog: mention lts releases
Signed-off-by: f41gh7 <nik@victoriametrics.com>
2025-01-27 11:06:12 +01:00
f41gh7
3fc6de40cf docs: point apps versions to the latest releases
v1.110.0
v1.102.11
v1.97.16

Signed-off-by: f41gh7 <nik@victoriametrics.com>
2025-01-27 11:06:11 +01:00
f41gh7
08404f2169 CHANGELOG.md: cut v1.110.0 release 2025-01-27 11:06:11 +01:00
Aliaksandr Valialkin
c51282d698 lib/logstorage: open per-day partitions in parallel during startup
This significantly reduces startup times when the storage contains large partitions over many days.
2025-01-27 00:35:35 +01:00
Aliaksandr Valialkin
b01b02b40c lib/logstorage: optimize unmarshalColumnNames a bit
This should reduce the time needed for opening a large storage with many partitions,
which contain logs with big number of fields (aka wide events).

Thanks to @kiriklo for the initial idea at the pull request https://github.com/VictoriaMetrics/VictoriaMetrics/pull/8061

Updates https://github.com/VictoriaMetrics/VictoriaMetrics/issues/7937
2025-01-27 00:14:42 +01:00
Aliaksandr Valialkin
dbe8e9df09 lib/logstorage: improve error message by adding a link with the explanation why VictoriaLogs ignores logs with the size exceeding 2MB
Updates https://github.com/VictoriaMetrics/VictoriaMetrics/issues/7972
Updates https://github.com/VictoriaMetrics/VictoriaMetrics/pull/7984
2025-01-26 22:53:15 +01:00
Aliaksandr Valialkin
4f9e51e106 lib/logstorage: block_stat pipe: return the path to the part where the block is stored 2025-01-26 22:53:15 +01:00
Aliaksandr Valialkin
a0ca7e9eea docs/VictoriaLogs: small updates 2025-01-26 22:07:45 +01:00
Aliaksandr Valialkin
ee69dd028a lib/{fs,filestream}: unconditionally disable fsync in tests
Use the testing.Testing() function in order to determine whether the code runs in test.
This allows running tests and fast speed without the need to specify DISABLE_FSYNC_FOR_TESTING
environment variable.

This is a follow-up for the commit 334cd92a6c
Updates https://github.com/VictoriaMetrics/VictoriaMetrics/pull/6871
2025-01-26 22:07:44 +01:00
Aliaksandr Valialkin
4908f98ee2 lib/logstorage: remove unneeded code after 202eb429a7
readerWithStats isn't used when reading column names from file
2025-01-26 22:07:44 +01:00
Aliaksandr Valialkin
c252d2651b docs/VictoriaLogs/sql-to-logsql.md: pay attention to the fact that stats() pipe at LogsQL has better usability than GROUP BY at SQL 2025-01-26 22:07:44 +01:00
Aliaksandr Valialkin
6a92e3f348 deployment/docker: update VictoriaLogs from v1.7.0-victorialogs to v1.8.0-victorialogs
See https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.8.0-victorialogs
2025-01-26 22:07:43 +01:00
Aliaksandr Valialkin
96246c0e04 docs/VictoriaLogs/CHANGELOG.md: cut v1.8.0-victorialogs release 2025-01-26 22:06:47 +01:00
Aliaksandr Valialkin
7ece9ac198 app/vlselect/vmui: run make vmui-logs-update after the commit 87739bbbef 2025-01-26 22:06:46 +01:00
Aliaksandr Valialkin
9754f4b298 docs/VictoriaLogs/CHANGELOG.md: move the changes from 87739bbbef to the correct place
Updates https://github.com/VictoriaMetrics/VictoriaMetrics/pull/7750
2025-01-26 22:06:46 +01:00
Yury Molodov
70d7f4aba9 vmui/logs: update hits chart legend (#7750)
### Describe Your Changes

- Added the `fields_limit` parameter for the `hits` query to limit the
number of returned fields. This reduces the response size and decreases
memory usage.

#### Legend Menu:
A context menu has been added for legend items, which includes:  
1. Metric name
2. **Copy _stream name** - copies the full stream name in the format
`{field1="value1", ..., fieldN="valueN"}`.
3. **Add _stream to filter** - adds the full stream value to the current
filter:
   `_stream: {field1="value1", ..., fieldN="valueN"} AND (old_expr)`.  
4. **Exclude _stream from filter** - excludes the stream from the
current filter:
`(NOT _stream: {field1="value1", ..., fieldN="valueN"}) AND (old_expr)`.
5. List of fields with options:  
   - Copy as `field: "value"`.  
   - Add to filter: `field: "value" AND (old_expr)`.  
   - Exclude from filter: `-field: "value" AND (old_expr)`.  
6. Total number of hits for the stream.  

Related issue: #7552

<details>
  <summary>UI Demo - Legend Menu</summary>
  
<img width="400"
src="https://github.com/user-attachments/assets/ee1954b2-fdce-44b4-a2dc-aa73096a5414"/>
<img width="400"
src="https://github.com/user-attachments/assets/19d71f04-c207-4143-a176-c5f221592e3d"/>

</details>

---

#### Legend:
1. Displays the total number of hits for the stream.  
2. Added hints below the legend for total hits and graph interactions.
3. Click behavior is now the same as in other vmui charts:  
   - `click` - shows only the selected series.  
   - `click + ctrl/cmd` - hides the selected series.  
   
<details>
  <summary>UI Demo - Legend</summary>

before: 
<img
src="https://github.com/user-attachments/assets/18270842-0c39-4f63-bcda-da62e15c3c73"/>

after:
<img
src="https://github.com/user-attachments/assets/351cad3a-f763-4b1d-b3be-b569b5472a7c"/>
  
</details>

---

#### Tooltip:  
1. The `other` label is moved to the end, and others are sorted by
value.
2. Values are aligned to the right.  
3. Labels are truncated and always shown in a single line for better
readability; the full name is available in the legend.

<details>
  <summary>UI Demo - Tooltip</summary>
  
| before     | after        |
|----------|----------|
| <img
src="https://github.com/user-attachments/assets/adccff38-e2e6-46e4-a69e-21381982489c"/>
| <img
src="https://github.com/user-attachments/assets/81008897-d816-4aed-92cb-749ea7e0ff1e"/>
|
  
</details>

---

#### Group View (tab Group):
Groups are now sorted by the number of records in descending order.

<details>
  <summary>UI Demo - Group View</summary>

before: 
<img width="800"
src="https://github.com/user-attachments/assets/15b4ca72-7e5d-421f-913b-c5ff22c340cb"/>

after:
<img width="800"
src="https://github.com/user-attachments/assets/32ff627b-6f30-4195-bfe7-8c9b4aa11f6b"/>
  
</details>
2025-01-26 22:06:13 +01:00
Aliaksandr Valialkin
93ea7c5cf6 docs/VictoriaLogs/CHANGELOG.md: typo fix after ad6c587494: ignore_global_time_range -> ignore_global_time_filter 2025-01-26 22:05:37 +01:00
Aliaksandr Valialkin
fea934936b lib/logstorage: properly propagate extra filters to all the subqueries
The purpose of extra filters ( https://docs.victoriametrics.com/victorialogs/querying/#extra-filters )
is to limit the subset of logs, which can be queried. For example, it is expected that all the queries
with `extra_filters={tenant=123}` can access only logs, which contain `123` value for the `tenant` field.

Previously this wasn't the case, since the provided extra filters weren't applied to subqueries.
For example, the following query could be used to select all the logs outside `tenant=123`, for any `extra_filters` arg:

    * | union({tenant!=123})

This commit fixes this by propagating extra filters to all the subqueries.

While at it, this commit also properly propagates [start, end] time range filter from HTTP querying APIs
into all the subqueries, since this is what most users expect. This behaviour can be overriden on per-subquery
basis with the `options(ignore_global_time_filter=true)` option - see https://docs.victoriametrics.com/victorialogs/logsql/#query-options

Also properly apply apply optimizations across all the subqueries. Previously the optimizations at Query.optimize()
function were applied only to the top-level query.
2025-01-26 22:05:05 +01:00
Aliaksandr Valialkin
7b62086609 lib: consistently use logger.Panicf("BUG: ...") for logging programming bugs
logger.Fatalf("BUG: ...") complicates investigating the bug, since it doesn't show the call stack,
which led to the bug. So it is better to consistently use logger.Panicf("BUG: ...") for logging programming bugs.
2025-01-24 16:40:50 +01:00
Aliaksandr Valialkin
9c23c61a2e docs/VictoriaLogs/LogsQL.md: show how to unroll the returned histogram buckets into separate rows at histogram pipe docs 2025-01-24 16:39:47 +01:00
Aliaksandr Valialkin
fb68ba3745 docs/VictoriaLogs/sql-to-logsql.md: show how to substitute complex SQL query with top pipe 2025-01-24 16:39:46 +01:00
f41gh7
43772b9869 make vmui-update 2025-01-24 14:23:49 +01:00
Nikolay
cab5cf3c4c app/vmselect: fixes panic data race at query tracing
Previously, NewChild elements of querytracer could be referenced by concurrent
storageNode goroutines. After earlier return ( if search.skipSlowReplicas is set), it is
possible, that tracer objects could be still in-use by concurrent workers.
  It may cause panics and data races. Most probable case is when parent tracer is finished, but children
still could write data to itself via Donef() method. It triggers read-write data race at trace
formatting.

This commit adds a new methods to the querytracer package, that allows to
create children not referenced by parent and add it to the parent later.

 Orphaned child must be registered at the parent, when goroutine returns. It's done synchronously by the single caller  via finishQueryTracer call. 
If child didn't finished work and reference for it is used by concurrent goroutine, new child must be created instead with
context message.
 It prevents panics and possible data races.

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

---------

Signed-off-by: f41gh7 <nik@victoriametrics.com>
Co-authored-by: Zakhar Bessarab <z.bessarab@victoriametrics.com>
Co-authored-by: Roman Khavronenko <roman@victoriametrics.com>
2025-01-24 13:55:32 +01:00
Roman Khavronenko
f52736eb98 docs: add avaialbe_version notion to new -search.maxDeleteDuration (#8142)
Follow-up for
4574958e2e

### Describe Your Changes

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

### Checklist

The following checks are **mandatory**:

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

Signed-off-by: hagen1778 <roman@victoriametrics.com>
2025-01-24 13:46:03 +01:00
Yury Molodov
50a37ee07d vmui: save column settings in URL #7662 (#7979)
### Describe Your Changes

Added saving column settings in the URL for the table view. See #7662

---------

Signed-off-by: hagen1778 <roman@victoriametrics.com>
Co-authored-by: hagen1778 <roman@victoriametrics.com>
(cherry picked from commit f0d55a1c25)
2025-01-24 09:51:50 +01:00
Yury Molodov
7a13358cd2 vmui: fix issue with query execution and line breaks in query editor
This commit fixes incorrect behaviour when pressing `Enter` did not execute the query, and
`Shift+Enter` did not insert a new line.

- The issue occurred when autocomplete was disabled.
- This problem affected the query editor in both the VictoriaMetrics UI
and VictoriaLogs UI.

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

(cherry picked from commit f31dece58d)
2025-01-24 09:51:50 +01:00
Dmytro Kozlov
327f1835d1 deployment/docker: upgraded Grafana plugins to the latest versions
Upgraded Grafana plugins to the latest versions

(cherry picked from commit a6951b8b14)
2025-01-24 09:51:49 +01:00
Zakhar Bessarab
aef93b1889 app/vmselect/prometheus: fix panic when performing delete with "multitenant" auth token
Initially delete_series API wasn't implemented for mulitenant auth token.

 This commit fixes it and properly handle delete series requests for mulitenant auth token.
It also adds integration tests for this case.

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

Introduced at v1.104.0 release:
https://github.com/VictoriaMetrics/VictoriaMetrics/issues/1434
---------

Signed-off-by: Zakhar Bessarab <z.bessarab@victoriametrics.com>
Co-authored-by: f41gh7 <nik@victoriametrics.com>
2025-01-24 08:32:50 +01:00
Zakhar Bessarab
5f56375564 app/vmselect/prometheus: prevent panic when using "multitenant" at /api/v1/series/count requests
Adding support of multi-tenant reads to /api/v1/series/count would
require introducing a breaking change to a `netstorage` RPC, so
currently vmselect will explicitly deny these requests.

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

Introduced at v1.104.0 release:
https://github.com/VictoriaMetrics/VictoriaMetrics/issues/1434

Signed-off-by: Zakhar Bessarab <z.bessarab@victoriametrics.com>
2025-01-24 07:58:54 +01:00
Phuong Le
3ada13dd48 lib/logstorage: remove redundant error check 2025-01-24 07:52:52 +01:00
Aliaksandr Valialkin
2aeba2b2cc docs/VictoriaLogs/sql-to-logsql.md: add a guide on how to convert SQL to LogsQL 2025-01-24 07:52:52 +01:00
Aliaksandr Valialkin
755a0bb3d9 docs/VictoriaLogs/README.md: add a link to VictoriaLogs playground 2025-01-24 07:52:52 +01:00
Aliaksandr Valialkin
9c8fc93553 docs/VictoriaLogs/LogsQL.md: mention that field pipe can be used for improving query performance 2025-01-24 07:52:52 +01:00
Aliaksandr Valialkin
45cc9974ab lib/logstorage: inherit query options by nested queries
This is a follow-up for b620b5cff5
2025-01-24 07:52:51 +01:00
hagen1778
ea3a7c3528 deployment: reflect metrics datasource ID change
See https://github.com/VictoriaMetrics/victoriametrics-datasource

Signed-off-by: hagen1778 <roman@victoriametrics.com>
2025-01-24 07:52:51 +01:00
Roman Khavronenko
9261da53a0 app/vmselect/promql: respect staleness in removeCounterResets (#8073)
See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/8072

### Describe Your Changes

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

### Checklist

The following checks are **mandatory**:

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

---------

Signed-off-by: hagen1778 <roman@victoriametrics.com>
Co-authored-by: Aliaksandr Valialkin <valyala@victoriametrics.com>
2025-01-24 07:52:51 +01:00
Zhu Jiekun
3ca0ca1f45 docs/vmagent: estimating the message size and rate when using Kafka
When using Kafka, it's important to estimate the message rate and size
before applying for resources.

This commit  explains why and how to use remote write metrics to
do the evaluation.
2025-01-24 07:52:51 +01:00
Jose Gómez-Sellés
e1852e8aa1 doc: fix typo in vmsingle k8s monitoring guide (#8120)
As reported by a user, the value in [VictoriaMetrics Single helm
chart](https://github.com/VictoriaMetrics/helm-charts/blob/master/charts/victoria-metrics-single/values.yaml#L420)
and actual example, should be "enabled", not "enable". This commit fixes
it.


### Describe Your Changes

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

### Checklist

The following checks are **mandatory**:

- [x] My change adheres [VictoriaMetrics contributing
guidelines](https://docs.victoriametrics.com/contributing/).
2025-01-24 07:52:51 +01:00
Aliaksandr Valialkin
bfd198a4d5 lib/logstorage: add hash pipe for calculating hash over the given log field
This pipe may be useful for sharding log entries among hash buckets.
2025-01-24 07:52:51 +01:00
Aliaksandr Valialkin
0a586ecfd8 lib/logstorage: add an ability to set query concurrency on a per-query basis
This is done via 'options(concurrency=N)' prefix for the query.
For example, the following query is executed on at most 4 CPU cores:

    options(concurrency=4) _time:1d | count_uniq(user_id)

This allows reducing RAM and CPU usage at the cost of longer query execution times,
since by default every query is executed in parallel on all the available CPU cores.

See https://docs.victoriametrics.com/victorialogs/logsql/#query-options
2025-01-24 07:52:50 +01:00
Aliaksandr Valialkin
fb311d3ad5 lib/logstorage: always pass the current timestamp to newLexer()
Also always initialize Query.timestamp with the timestamp from the lexer.

This should avoid potential problems with relative timestamps inside inner queries.
For example, the `_time:1h` filter in the following query is correctly executed
relative to the current timestamp:

   foo:in(_time:1h | keep foo)
2025-01-24 07:52:50 +01:00
Aliaksandr Valialkin
159b821a52 lib/logstorage: simplify the caller side of addNewItem() function 2025-01-24 07:52:50 +01:00
Daria Karavaieva
e7c806e5c3 docs/vmanomaly: format & fix docs (#8122)
### Describe Your Changes

- fix formatting in Presets
- change integration guide config according to new format + optimisation
of prophet
- minor fixes

### Checklist

The following checks are **mandatory**:

- [x] My change adheres [VictoriaMetrics contributing
guidelines](https://docs.victoriametrics.com/contributing/).
2025-01-24 07:52:49 +01:00
Daria Karavaieva
06d0afd79d docs/vmanomaly: prod ready config example (#8107)
### Describe Your Changes

- provide prod ready config examples for vmanomaly
- minor formating fix

### Checklist

The following checks are **mandatory**:

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

(cherry picked from commit f932deb47a)
2025-01-22 13:50:59 +01:00
Zhu Jiekun
98acb06a2e docs: add curl file upload example to csv import doc (#8113)
### Describe Your Changes

Provide `curl` file upload example to csv import related section.

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

### Checklist

The following checks are **mandatory**:

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

(cherry picked from commit 77f446d095)
2025-01-22 13:50:59 +01:00
Github Actions
2676d9baa8 Automatic update helm docs from VictoriaMetrics/helm-charts@b26141b (#8116)
Automated changes by
[create-pull-request](https://github.com/peter-evans/create-pull-request)
GitHub action

Signed-off-by: Github Actions <133988544+victoriametrics-bot@users.noreply.github.com>
Co-authored-by: f41gh7 <18450869+f41gh7@users.noreply.github.com>
(cherry picked from commit 5f8810fc8d)
2025-01-22 13:50:58 +01:00
Nikolay
af4446725d app/vmauth: remove readTrackingBody pool (#8104)
Sync.Pool for readTrackingBody was added in order to reduce potential
load on garbage collector. But golang net/http standard library does not
allow to reuse request body, since it closes body asynchronously after
return. Related issue: https://github.com/golang/go/issues/51907

This commit removes sync.Pool in order to fix potential panic and data
race at requests processing.

 Affected releases:
- all releases after v1.97.7

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

Signed-off-by: f41gh7 <nik@victoriametrics.com>
Co-authored-by: Roman Khavronenko <roman@victoriametrics.com>
(cherry picked from commit 80ead7cfa4)
2025-01-22 13:33:38 +01:00
Zakhar Bessarab
e77493b5b9 docs/guides/guide-vmcluster-multiple-retention-setup: fix some typos (#8112)
### Describe Your Changes

- fix typo in text
- fix typos in schema, re-create schema and upload source

Current doc:
https://docs.victoriametrics.com/guides/guide-vmcluster-multiple-retention-setup/

Here is an updated schema preview:

![image](https://github.com/user-attachments/assets/a65613c0-f3f6-4df5-bc70-fd70f1677386)

### Checklist

The following checks are **mandatory**:

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

Signed-off-by: Zakhar Bessarab <z.bessarab@victoriametrics.com>
(cherry picked from commit 8772288bd6)
2025-01-22 13:33:37 +01:00
Fred Navruzov
2a259398e0 docs/vmanomaly: release v1.19.1 (#8108)
### Describe Your Changes

docs/vmanomaly: release v1.19.1

### Checklist

The following checks are **mandatory**:

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

(cherry picked from commit 299d66fd98)
2025-01-22 13:33:34 +01:00
Zakhar Bessarab
e3054bbcf8 lib/license: trim whitespaces before license key validation
Validation expects license key to be present without any addition whitespaces, so having a trailing whitespaces would lead to verification failure.

Signed-off-by: Zakhar Bessarab <z.bessarab@victoriametrics.com>
2025-01-22 12:07:28 +04:00
Dmytro Kozlov
bb3ecf450f deployment/docker: update datasource plugins version to the latest release (#8106)
### Describe Your Changes

Updated plugins to the latest versions

### Checklist

The following checks are **mandatory**:

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

(cherry picked from commit 77218c5848)
2025-01-21 20:40:54 +04:00
hagen1778
d28a245a20 docs: move changelog line to the corresponding section
Signed-off-by: hagen1778 <roman@victoriametrics.com>
(cherry picked from commit 9e6fc9269d)
2025-01-21 20:32:31 +04:00
hagen1778
dfec66a0fd docs: rm misleading default value for -maxConcurrentInserts
See https://github.com/VictoriaMetrics/VictoriaMetrics/pull/5494
and https://github.com/VictoriaMetrics/VictoriaMetrics/pull/5494#issuecomment-2603067137

Signed-off-by: hagen1778 <roman@victoriametrics.com>
(cherry picked from commit 661f9fc3e2)
2025-01-21 12:51:36 +01:00
Andrii Chubatiuk
49694efc02 lib/protoparser/opentelemetry: do not drop histogram buckets, when sum is absent (#8054)
Despite requirement in OpenTelemetry spec that histograms should contain
sum, [OpenTelemetry collector promremotewrite
translator](37c8044abf/pkg/translator/prometheusremotewrite/helper.go (L222))
and [Prometheus OpenTelemetry
parsing](d52e689a20/storage/remote/otlptranslator/prometheusremotewrite/helper.go (L264))
skip only sum if it's absent. Our current implementation drops buckets
if sum is absent, which causes issues for users, that are expecting a
similar to Prometheus behaviour

### Describe Your Changes

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

### Checklist

The following checks are **mandatory**:

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

---------

Co-authored-by: Roman Khavronenko <roman@victoriametrics.com>
(cherry picked from commit 2adb5fe014)
2025-01-21 12:51:36 +01:00
Github Actions
2ab56f1615 Automatic update operator docs from VictoriaMetrics/operator@5c7959f (#8102)
Automated changes by
[create-pull-request](https://github.com/peter-evans/create-pull-request)
GitHub action

Signed-off-by: Github Actions <133988544+victoriametrics-bot@users.noreply.github.com>
Co-authored-by: f41gh7 <18450869+f41gh7@users.noreply.github.com>
(cherry picked from commit ce917a4cc3)
2025-01-21 12:51:35 +01:00
Github Actions
7d69cb57e2 Automatic update helm docs from VictoriaMetrics/helm-charts@21bf09b (#8098)
Automated changes by
[create-pull-request](https://github.com/peter-evans/create-pull-request)
GitHub action

Signed-off-by: Github Actions <133988544+victoriametrics-bot@users.noreply.github.com>
Co-authored-by: zekker6 <1367798+zekker6@users.noreply.github.com>
(cherry picked from commit b3de1c029c)
2025-01-21 12:51:35 +01:00
Fred Navruzov
9c982e60db docs/vmanomaly: release v1.19.0 (#8097)
### Describe Your Changes

Updated `vmanomaly` docs to release v1.19.0. Also, slight improvements
on other pages, like FAQ

### Checklist

The following checks are **mandatory**:

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

(cherry picked from commit 461c7a5ad7)
2025-01-21 12:51:35 +01:00
Aliaksandr Valialkin
3de20efbfd deployment/docker: update Go builder from Go1.23.4 to Go1.23.5
See https://github.com/golang/go/issues?q=milestone%3AGo1.23.5+label%3ACherryPickApproved

(cherry picked from commit 489631b227)
2025-01-21 12:51:35 +01:00
Aliaksandr Valialkin
e7e4aa4ce3 deployment/docker: update VictoriaLogs from v1.6.1-victorialogs to v1.7.0-victorialogs
(cherry picked from commit e78ff0dc2a)
2025-01-21 12:51:34 +01:00
Aliaksandr Valialkin
489bf3415f app/vlselect/vmui: run make vmui-logs-update after 17b3f24a37
(cherry picked from commit ab4d9f6213)
2025-01-21 12:51:34 +01:00
Aliaksandr Valialkin
2f12e8b692 docs/VictoriaLogs/CHANGELOG.md: cut v1.7.0-victorialogs
(cherry picked from commit 81c313fd89)
2025-01-21 12:51:34 +01:00
Aliaksandr Valialkin
e0a6011f51 vendor/github.com/VictoriaMetrics/metricsql: update from v0.81.1 to v0.82.0
This introduces the ability to copy-n-paste queries with $__interval and $__rate_interval
placeholders into VictoriaMetrics - these placeholders are automatically replaced with 1i,
which equals to the `step` query arg value passed to /api/v1/query_range and /api/v1/query.

(cherry picked from commit e9de665289)
2025-01-21 12:51:34 +01:00
Aliaksandr Valialkin
5095833357 lib/logstorage: add ability to execute INNER JOIN with join pipe
(cherry picked from commit bfbe06e912)
2025-01-21 12:51:33 +01:00
Aliaksandr Valialkin
a85bce0d68 docs/VictoriaLogs/LogsQL.md: clarify docs about LogsQL pipes a bit
(cherry picked from commit 71a7d0db4a)
2025-01-21 12:51:33 +01:00
Aliaksandr Valialkin
df6a371615 docs/VictoriaLogs/FAQ.md: add questions on how to determine the number of unique log streams and unique field values
(cherry picked from commit e8748e4747)
2025-01-21 12:51:33 +01:00
hagen1778
72a1758297 deployment: bump vm-datasource version to v0.11.0
Signed-off-by: hagen1778 <roman@victoriametrics.com>
(cherry picked from commit ad3a5be097)
2025-01-20 13:49:13 +01:00
Yury Molodov
72a78d89a6 vmui/logs: improvements to grouped view in logs (#7815)
### Describe Your Changes

This PR introduces improvements for the `Group view` tab:

1. **Reduced text size and improved styles**
   Related issue: #7479
   <details>
   <summary>Demo UI</summary>

<img
src="https://github.com/user-attachments/assets/c96aed95-e668-49a5-af20-778580e8a884"/>

   </details>

2. **Added the ability to select a field to display instead of `_msg`**
   You can select fields to display in the field list or in the settings
   Related issue: #7419
   <details>
   <summary>Demo UI</summary>

<img width="600"
src="https://github.com/user-attachments/assets/b5ae1433-bf40-4151-986b-ba057791dcee"/>
<img width="600"
src="https://github.com/user-attachments/assets/2c3d8c5c-1543-43ee-8241-8fd4d4b99b1d"/>

   </details>

3. **Added date format customization**
   <details>
   <summary>Demo UI</summary>

<img width="400"
src="https://github.com/user-attachments/assets/b095adfd-cbe3-45a6-bd7d-dc574d3edfd1"/>
<img width="400"
src="https://github.com/user-attachments/assets/0e8b0fbc-2c01-40b7-b9b8-7ef04d21ef9b"/>

   </details>

4. **Added an option to display log messages in a single line**
   <details>
   <summary>Demo UI</summary>

<img width="400"
src="https://github.com/user-attachments/assets/43d64a57-19c9-4b15-9009-13b8545d906c"/>
<img
src="https://github.com/user-attachments/assets/eaada2b1-9474-4496-ac39-b38332e637c1"/>

   </details>

5. **Added an option to display group labels in a single line**
   <details>
   <summary>Demo UI</summary>

<img width="400"
src="https://github.com/user-attachments/assets/05be097a-7b19-4e7b-9cf4-181fd802728b"/>
<img
src="https://github.com/user-attachments/assets/d4405fa6-3829-4713-8537-11edc6da5c4f"/>
<img
src="https://github.com/user-attachments/assets/90d2c48d-ba3b-4051-a645-22e7c8945dcb"/>

   </details>

7. **Settings indicator for modified defaults**
A red marker is displayed on the settings icon if the default settings
are changed. Hovering over it shows all the modified parameters.
   <details>
   <summary>Demo UI</summary>

<img
src="https://github.com/user-attachments/assets/19fbaf9b-684b-4cac-ad80-2db5e63804e9"/>

   </details>

(cherry picked from commit 17b3f24a37)
2025-01-20 13:49:13 +01:00
Zhu Jiekun
21f6d84b4b docs: update docs for *authKey, add authKey to HTTP 401 resp body (#7971)
### Describe Your Changes

optimize for
https://github.com/VictoriaMetrics/VictoriaMetrics/issues/6226

for user who set `*AuthKey` flag, they will receive new response in
body:
```go
// query arg not set
The provided authKey '' doesn't match -search.resetCacheAuthKey

// incorrect query arg
The provided authKey '5dxd71hsz==' doesn't match -search.resetCacheAuthKey
```

previously, they receive:
```
The provided authKey doesn't match -search.resetCacheAuthKey
```

### Checklist

The following checks are **mandatory**:

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

(cherry picked from commit 1f0b03aebe)
Signed-off-by: hagen1778 <roman@victoriametrics.com>
2025-01-20 13:49:12 +01:00
Github Actions
9cf08f500a Automatic update operator docs from VictoriaMetrics/operator@7da9289 (#8093)
Automated changes by
[create-pull-request](https://github.com/peter-evans/create-pull-request)
GitHub action

Signed-off-by: Github Actions <133988544+victoriametrics-bot@users.noreply.github.com>
Co-authored-by: f41gh7 <18450869+f41gh7@users.noreply.github.com>
(cherry picked from commit fc8710c071)
2025-01-20 13:42:24 +01:00
Yury Molodov
e72ecc10c6 vmui/logs: update default graph to bars with color fill #7101 (#8060)
### Describe Your Changes

Updated the default graph type in the hits panel to bars with color fill
for better readability. Removed options for lines, stepped lines, and
points.

Ref issue: #7101

<img
src="https://github.com/user-attachments/assets/62522c97-7e5e-426a-b597-8457b2360f7e"
width="400"/>

(cherry picked from commit a7f36eef0e)
2025-01-20 13:42:24 +01:00
Zakhar Bessarab
46f9076ade docs/release-guide: update helm release guide (#8090)
### Describe Your Changes

Release procedure was updated on helm charts side, update documented
procedure with up-to-date steps for the release.

### Checklist

The following checks are **mandatory**:

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

Signed-off-by: Zakhar Bessarab <z.bessarab@victoriametrics.com>
(cherry picked from commit 54ab08d839)
2025-01-20 13:42:24 +01:00
Zakhar Bessarab
b80a93146f {docs,deployment}: use explicit version tag instead of "latest" (#8089)
### Describe Your Changes

Using latest leads to inconsistent results between runs and might also
lead to data corruption since "latest" can be updated by any development
build.

### Checklist

The following checks are **mandatory**:

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

Signed-off-by: Zakhar Bessarab <z.bessarab@victoriametrics.com>
(cherry picked from commit a3ea6d9e61)
2025-01-20 13:42:23 +01:00
Github Actions
35606eefe3 Automatic update helm docs from VictoriaMetrics/helm-charts@ba82ea6 (#8087)
Automated changes by
[create-pull-request](https://github.com/peter-evans/create-pull-request)
GitHub action

Signed-off-by: Github Actions <133988544+victoriametrics-bot@users.noreply.github.com>
Co-authored-by: AndrewChubatiuk <3162380+AndrewChubatiuk@users.noreply.github.com>
(cherry picked from commit f19c760f4f)
2025-01-20 13:14:12 +04:00
Aliaksandr Valialkin
9798ce0276 docs/VictoriaLogs/FAQ.md: add answers on how to detect the most frequently seen logs and how to get field names in the selected logs
(cherry picked from commit 86e74de9db)
2025-01-20 13:14:10 +04:00
Aliaksandr Valialkin
aa37295358 docs/VictoriaLogs/FAQ.md: add an answer on how to determine log fields, which occupy the most of the disk space and how to deal with such log fields
(cherry picked from commit 4d4253ee17)
2025-01-20 13:14:08 +04:00
Github Actions
d2e5751515 Automatic update helm docs from VictoriaMetrics/helm-charts@139a32b (#8077)
Automated changes by
[create-pull-request](https://github.com/peter-evans/create-pull-request)
GitHub action

Signed-off-by: Github Actions <133988544+victoriametrics-bot@users.noreply.github.com>
Co-authored-by: zekker6 <1367798+zekker6@users.noreply.github.com>
(cherry picked from commit 8c7b5d22c9)
2025-01-17 19:46:55 +04:00
Zakhar Bessarab
0eef0b860a docs: update reference to the latest release - v1.109.1
### Describe Your Changes

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

### Checklist

The following checks are **mandatory**:

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

Signed-off-by: Zakhar Bessarab <z.bessarab@victoriametrics.com>
(cherry picked from commit 513f5da5de)
2025-01-17 19:46:53 +04:00
Zakhar Bessarab
bd82d2e726 deployment/docker: update reference to the latest release - v1.109.1 (#8078)
### Describe Your Changes

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

### Checklist

The following checks are **mandatory**:

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

Signed-off-by: Zakhar Bessarab <z.bessarab@victoriametrics.com>
(cherry picked from commit fb4d545555)
2025-01-17 19:46:52 +04:00
hagen1778
522c912f35 docs: mention v1.109.1 as recommendation to update
Signed-off-by: hagen1778 <roman@victoriametrics.com>
(cherry picked from commit abaf8574a8)
2025-01-17 15:30:27 +01:00
hagen1778
691303ee36 docs: re-purpose b26a68641c as bugfix
Signed-off-by: hagen1778 <roman@victoriametrics.com>
(cherry picked from commit f346b5aaaa)
2025-01-17 15:30:27 +01:00
hagen1778
374232ee49 docs: move change 4574958e2e to #tip section
Signed-off-by: hagen1778 <roman@victoriametrics.com>
(cherry picked from commit 31398cc739)
2025-01-17 15:30:27 +01:00
Hui Wang
cc24696ade vmselect: add -search.maxDeleteDuration to limit the duration of th… (#8039)
…e `/api/v1/admin/tsdb/delete_series` call

Previously, it is limited by `-search.maxQueryDuration`, and can be
small for delete calls.

part of https://github.com/VictoriaMetrics/VictoriaMetrics/issues/7857.

(cherry picked from commit 4574958e2e)
Signed-off-by: hagen1778 <roman@victoriametrics.com>
2025-01-17 15:30:26 +01:00
Konstantin Shalygin
4b00cc2e46 vmui: web: fixed favicon name (#7632)
### Describe Your Changes

Seems `favicon-32x32.png` removed in #6972

Fixes this:

```shell
√ build % go build -x -buildmode="pie" -trimpath -mod="readonly" -modcacherw -ldflags "-linkmode external"
WORK=/var/folders/mj/r83hky151tzbqsvml0hxts_w0000gn/T/go-build2937230855
main.go:13:12: pattern favicon-32x32.png: no matching files found
```

### Checklist

The following checks are **mandatory**:

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

(cherry picked from commit d623105ef4)
2025-01-17 15:29:17 +01:00
Daria Karavaieva
57cb3dd537 docs/vmanomaly: fix column width (#8059)
### Describe Your Changes

Fixing column widht in tables for components`sections: reader, writer,
monitoring and scheduler

### Checklist

The following checks are **mandatory**:

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

(cherry picked from commit aac5cd8574)
2025-01-17 15:29:17 +01:00
Github Actions
fe57d0ce2f Automatic update operator docs from VictoriaMetrics/operator@e9d1493 (#8074)
Automated changes by
[create-pull-request](https://github.com/peter-evans/create-pull-request)
GitHub action

Signed-off-by: Github Actions <133988544+victoriametrics-bot@users.noreply.github.com>
Co-authored-by: f41gh7 <18450869+f41gh7@users.noreply.github.com>
(cherry picked from commit d3c02b8f5d)
2025-01-17 15:29:17 +01:00
Roman Khavronenko
c66e986cd7 fix testsapp/vlselect/logsql: follow-up after 2eb15cf30c964a9ad86f296… (#8075)
…9cbb1518e71fba36b

This fixes tests failing
https://github.com/VictoriaMetrics/VictoriaMetrics/actions/runs/12827319722/job/35769125733?pr=8073#step:5:197

### Describe Your Changes

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

### Checklist

The following checks are **mandatory**:

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

Signed-off-by: hagen1778 <roman@victoriametrics.com>
(cherry picked from commit 7f252c1800)
2025-01-17 15:29:17 +01:00
Zakhar Bessarab
6b551c82fb docs/changelog/CHANGELOG.md: cut v1.109.1
Signed-off-by: Zakhar Bessarab <z.bessarab@victoriametrics.com>
2025-01-17 14:37:13 +04:00
Aliaksandr Valialkin
73eddc4da9 deployment/docker: update VictoriaLogs Docker image from v1.6.0-victorialogs to v1.6.1-victorialogs
See https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.6.1-victorialogs

(cherry picked from commit 0f7b853a88)
2025-01-17 13:26:55 +04:00
Aliaksandr Valialkin
7562348c1e docs/VictoriaLogs/CHANGELOG.md: cut v1.6.1-victorialogs
(cherry picked from commit 70f0a974b8)
2025-01-17 13:26:54 +04:00
Aliaksandr Valialkin
cb6f69b3ee lib/logstorage: merge top-level _stream:{...} filters in the query
This should improve performance of queries, which contain multiple top-level _stream:{...} filters.

This should help the case described at https://github.com/VictoriaMetrics/VictoriaMetrics/issues/8037#issuecomment-2595854592

(cherry picked from commit 2eb15cf30c)
2025-01-17 13:26:52 +04:00
Aliaksandr Valialkin
9f0ce93433 lib/logstorage: add a test for union pipe
This is a follow-up for f27e120aeb

(cherry picked from commit 499f0b9588)
2025-01-17 13:26:50 +04:00
Aliaksandr Valialkin
403dbd6047 lib/logstorage: properly pass tenantIDs list to initStreamFilters
Previously an empty tenantIDs list was mistakenly passed to initStreamFilters
when the query already contained top-level stream filter.

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

(cherry picked from commit 43d615ae87)
2025-01-17 13:26:48 +04:00
Zakhar Bessarab
140f0f302e lib/license/online: remove delay before failing if all online verification attempts have failed (#816)
Enable components to fail faster in case all verification attempts have failed. Currently, there will be a final sleep before returning an error.

Signed-off-by: Zakhar Bessarab <z.bessarab@victoriametrics.com>
2025-01-16 20:40:28 +04:00
Mathias Palmersheim
35f102c6ea added retention filters to Victorialogs roadmap (#8020)
### Describe Your Changes

Fixes #8019 and adds retention filters to victorialogs roadmap as an
enterprise feature

### Checklist

The following checks are **mandatory**:

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

(cherry picked from commit 45bfe1f44c)
2025-01-16 17:07:39 +01:00
Aliaksandr Valialkin
1e3da83acc go.mod: stick cloud.google.com/go/storage to v1.43.0 until https://github.com/googleapis/google-cloud-go/issues/11448 is fixed
This returns back vmbackup and vmrestore binary sizes from 60MB to 40MB.

See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/8008
See https://github.com/googleapis/google-cloud-go/issues/11448

Thanks to @f41gh7 for the investigation of the issue.

(cherry picked from commit 58d2c18423)
2025-01-16 17:07:39 +01:00
hagen1778
5a35ae4565 docs: add changelog line for 7d2a6764e7
Signed-off-by: hagen1778 <roman@victoriametrics.com>
(cherry picked from commit feeda42560)
2025-01-16 17:07:39 +01:00
Roman Khavronenko
3dfcbf3229 app/vmselect/promql: limit staleness detection for increase/increase_pure/delta (#8052)
`doInternal` has adaptive staleness detection mechanism. It is
calculated using timestamp distance between samples in selected list of
samples. It is dynamic because VM can store signals from many sources
with different samples resolution. And while it works for most of cases,
there are edge cases for rollup functions that are comparing values
between windows: increase, increase_pure, delta.

The edge case 1.
There was a gap between series because of the missed scrape or two. In
this case staleness will trigger and increase-like functions will assume
the value they need to compare with is 0. In result, this could produce
spikes for a flappy data - see
https://github.com/VictoriaMetrics/VictoriaMetrics/issues/894 This
problem was solved by introducing a `realPrevValue` field -
1f19c167a4.
It stores the closest real sample value on selected interval and is used
for comparison when samples start after the gap.

The edge case 2.
`realPrevValue` doesn't respect staleness interval. In result, even if
gap between samples is huge (hours), the increase-like functions will
not consider it as a new series that started from 0. See
https://github.com/VictoriaMetrics/VictoriaMetrics/pull/8002.

Covering both edge cases is tricky, because `realPrevValue` has to
respect and not respect the staleness interval in the same time. In
other words, it should be able to ignore periodic missing gaps, but
reset if the gap is too big. While "too big gap" can't be figured out
empirically, I suggest using `-search.maxStalenessInterval` for this
purpose. If `-search.maxStalenessInterval` is set to 0 (default), then
`realPrevValue` ignores staleness interval. If
`-search.maxStalenessInterval` is > 0, then `realPrevValue` respects it
as a staleness interval.

### Checklist

The following checks are **mandatory**:

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

---------

Signed-off-by: hagen1778 <roman@victoriametrics.com>
(cherry picked from commit 7d2a6764e7)
2025-01-16 17:07:38 +01:00
Aliaksandr Valialkin
7af44f5e11 docs/VictoriaLogs/LogsQL.md: use top pipe in examples instead of stats by (...) count() | sort (...) limit N
`top` pipe is shorter and easier to understand

(cherry picked from commit 1645542a8a)
2025-01-16 17:07:38 +01:00
Aliaksandr Valialkin
6010daa536 docs/VictoriaLogs/logsql-examples.md: replace last pipe with first pipe, since it is easier to understand
(cherry picked from commit 151eb1e4b6)
2025-01-16 17:07:38 +01:00
Aliaksandr Valialkin
a48dccdde5 docs/VictoriaLogs/LogsQL.md: use proper backticks around hexnumencode:
(cherry picked from commit 5e4de8e860)
2025-01-16 17:07:37 +01:00
Aliaksandr Valialkin
c66520253e docs/VictoriaLogs: typo fixes for block_stats pipe docs
(cherry picked from commit 6312d3bbba)
2025-01-16 17:07:37 +01:00
Aliaksandr Valialkin
506ca64514 docs/VictoriaLogs/LogsQL.md: add missing they word
(cherry picked from commit d2bede6b51)
2025-01-16 17:07:37 +01:00
Aliaksandr Valialkin
37a9d38877 docs/VictoriaLogs/LogsQL.md: fix incorrect url to VictoriaMetrics histogram buckets
This is a follow-up for d2a791bef3

(cherry picked from commit 5ca5069fc4)
2025-01-16 17:07:36 +01:00
Aliaksandr Valialkin
d9da20769c deployment/docker: update VictoriaLogs Docker images from v1.5.0-victorialogs to v1.6.0-victorialogs
See https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.6.0-victorialogs

(cherry picked from commit 8a3c460f63)
2025-01-16 17:07:36 +01:00
Aliaksandr Valialkin
d8102b98fb docs/VictoriaLogs/CHANGELOG.md: cut v1.6.0-victorialogs release
(cherry picked from commit ca653a515c)
2025-01-16 17:07:35 +01:00
Aliaksandr Valialkin
faf1bcdbdf lib/logstorage: make golangci-lint happy after f27e120aeb
(cherry picked from commit e5b4cf33bf)
2025-01-16 17:07:35 +01:00
Aliaksandr Valialkin
a9c47ecb81 deployment/docker: update Alpine Docker image from 3.21.0 to 3.21.2
See https://alpinelinux.org/posts/Alpine-3.21.1-released.html and https://alpinelinux.org/posts/Alpine-3.18.11-3.19.6-3.20.5-3.21.2-released.html

(cherry picked from commit e24a8f2088)
2025-01-16 17:07:34 +01:00
Aliaksandr Valialkin
7cbfe32d7e lib/logstorage: add union pipe, which allows uniting results from multiple queries
(cherry picked from commit f27e120aeb)
2025-01-16 17:07:34 +01:00
Aliaksandr Valialkin
cb454aca03 lib/logstorage: properly drop temporary directories created by filter* tests
(cherry picked from commit ee1ce90501)
2025-01-16 17:07:34 +01:00
Aliaksandr Valialkin
0deab437e9 lib/logstorage: math pipe: add rand() function
(cherry picked from commit 47fe8cf3be)
2025-01-16 17:07:33 +01:00
Daria Karavaieva
32cc73dc13 docs/vmanomaly: fix parameters field width (#8025)
### Describe Your Changes

- fix parameters width in Components section - reader, wtiter,
monitoring, scheduler

### Checklist

The following checks are **mandatory**:

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

(cherry picked from commit 5813aa6602)
2025-01-16 17:07:33 +01:00
Aliaksandr Valialkin
f4b08b70d2 lib/logstorage: improve performance of unique pipe for integer columns with big number of unique values
(cherry picked from commit b4f4ece162)
2025-01-16 17:07:32 +01:00
Aliaksandr Valialkin
bc9ff8e40e lib/logstorage: improve performance when applying math calculations for _time, const and dict values
(cherry picked from commit bb00f7529f)
2025-01-16 17:07:32 +01:00
Roman Khavronenko
8426f984ae docs: mention regression in v1.109.0 (#8046)
See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/8045

### Checklist

The following checks are **mandatory**:

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

---------

Signed-off-by: hagen1778 <roman@victoriametrics.com>
(cherry picked from commit ad3bd11334)
2025-01-16 17:07:32 +01:00
Github Actions
6db3646460 Automatic update helm docs from VictoriaMetrics/helm-charts@d8e5f03 (#8035)
Automated changes by
[create-pull-request](https://github.com/peter-evans/create-pull-request)
GitHub action

Signed-off-by: Github Actions <133988544+victoriametrics-bot@users.noreply.github.com>
Co-authored-by: AndrewChubatiuk <3162380+AndrewChubatiuk@users.noreply.github.com>
(cherry picked from commit 875c6663ef)
2025-01-16 17:07:31 +01:00
Zakhar Bessarab
ba5f272ee4 docs: update references to v1.109.0 (#8034)
### Describe Your Changes

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

### Checklist

The following checks are **mandatory**:

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

Signed-off-by: Zakhar Bessarab <z.bessarab@victoriametrics.com>
(cherry picked from commit b48b7c454a)
2025-01-16 17:07:31 +01:00
Zakhar Bessarab
004a324050 deployment/docker: update to v1.109.0 (#8032)
### Describe Your Changes

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

### Checklist

The following checks are **mandatory**:

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

Signed-off-by: Zakhar Bessarab <z.bessarab@victoriametrics.com>
(cherry picked from commit f523348b3f)
2025-01-16 17:07:31 +01:00
Zakhar Bessarab
4475c56ead docs/changelog: add missing entry for #7182 (#8030)
### Describe Your Changes

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

### Checklist

The following checks are **mandatory**:

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

Signed-off-by: Zakhar Bessarab <z.bessarab@victoriametrics.com>
(cherry picked from commit 63bf1e008f)
2025-01-16 17:07:31 +01:00
Zakhar Bessarab
f8937d0338 docs/lts-releases: update links to latest releases (#8028)
### Describe Your Changes

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

### Checklist

The following checks are **mandatory**:

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

Signed-off-by: Zakhar Bessarab <z.bessarab@victoriametrics.com>
(cherry picked from commit 419ac10c60)
2025-01-16 17:07:30 +01:00
Dmytro Kozlov
b1eecb4dac cloud: update email images for cloud (#8024)
### Describe Your Changes

Updated email images with new support email

### Checklist

The following checks are **mandatory**:

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

(cherry picked from commit d631d2c100)
2025-01-14 14:29:52 +01:00
Zakhar Bessarab
37be3848b2 docs/release-guide: move testing on sandbox step before pushing tags (#8026)
### Describe Your Changes

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

### Checklist

The following checks are **mandatory**:

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

Signed-off-by: Zakhar Bessarab <z.bessarab@victoriametrics.com>
(cherry picked from commit 89431458bf)
2025-01-14 14:29:52 +01:00
Zakhar Bessarab
638848f450 docs/changelog: port LTS changes, update release date (#8027)
### Describe Your Changes

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

### Checklist

The following checks are **mandatory**:

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

---------

Signed-off-by: Zakhar Bessarab <z.bessarab@victoriametrics.com>
Co-authored-by: Roman Khavronenko <roman@victoriametrics.com>
(cherry picked from commit d8d0c0ac01)
2025-01-14 14:29:51 +01:00
Roman Khavronenko
e1abd66b9b docs: cut new changelog doc for 2024 (#8023)
Cutting new changelod doc reduces the size of the current's year
changelog and improves navigation for users.

### Describe Your Changes

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

### Checklist

The following checks are **mandatory**:

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

Signed-off-by: hagen1778 <roman@victoriametrics.com>
(cherry picked from commit c0f5699bad)
2025-01-14 14:29:50 +01:00
Nikolay
158f78976e lib/storage: reduce test suite batch size (#8022)
Commit eef6943084 added new test
functions. Which checks various cases for metricName registration at
data ingestion.
Initial dataset size had 4 batches with 100 rows each. It works fine at
machines with 5GB+ memory.
But i386 architecture supports only 4GB of memory per process.

Due to given limitations, batch size should be reduced to 3 batches and
30 rows. It keeps the same
test funtionality, but reduces overall memory usage to ~3GB.

Signed-off-by: f41gh7 <nik@victoriametrics.com>
(cherry picked from commit 277fdd1070)
2025-01-14 14:29:50 +01:00
Roman Khavronenko
2406480722 lib/opentlemetry: throttle log messages during parsing (#8021)
Samples parsing is a hot path. Bad client could easily overwhelm
receiver with bad or unsupported data. So it is better to throttle such
messages.

Follow-up after
b26a68641c

### Describe Your Changes

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

### Checklist

The following checks are **mandatory**:

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

Signed-off-by: hagen1778 <roman@victoriametrics.com>
(cherry picked from commit d290efb849)
2025-01-14 14:29:50 +01:00
chenlujjj
dc10b26306 lib/opentelemetry: log the metric name of unsupported metrics (#8018)
To resolve:
https://github.com/VictoriaMetrics/VictoriaMetrics/issues/8009
Log the name of unsupported metrics.

(cherry picked from commit b26a68641c)
2025-01-14 14:29:49 +01:00
Aliaksandr Valialkin
0c7b7c9404 lib/logstorage: make golangci-lint happy after the commit d2a791bef3
(cherry picked from commit b88cda5c41)
2025-01-14 14:29:49 +01:00
Aliaksandr Valialkin
e1c044f326 lib/logstorage: add histogram stats function for calculating histogram buckets over numeric fields
(cherry picked from commit d2a791bef3)
2025-01-14 14:29:48 +01:00
Aliaksandr Valialkin
46ee68683b lib/logstorage: top pipe: allow mixing the order of hits and rank suffixes
(cherry picked from commit 99516a5730)
2025-01-14 14:29:48 +01:00
Aliaksandr Valialkin
a944ce3f34 lib/logstorage: do not copy pipeTopkProcessorShard when obtaining parition keys
(cherry picked from commit aecc86c390)
2025-01-14 14:29:48 +01:00
Aliaksandr Valialkin
dfbd418b1b app/vlogscli: typo fix, which could result in incomplete results in compact mode
(cherry picked from commit 500b54f5aa)
2025-01-14 14:29:47 +01:00
Aliaksandr Valialkin
dbb1007b43 lib/logstorage: track integer field values in integer map for top N (int_field)
This reduces memory usage by up to 2x for the map used for tracking hits.
This also reduces CPU usage for tracking integer fields.

(cherry picked from commit cc29692e27)
2025-01-14 14:29:47 +01:00
Aliaksandr Valialkin
3775998a82 lib/logstorage: avoid callback overhead at visitValuesReadonly
Process values in batches instead of passing every value in the callback.
This improves performance of reading the encoded values from storage by up to 50%.

(cherry picked from commit f018aa33cb)
2025-01-14 14:29:47 +01:00
Daria Karavaieva
5923a5d0e7 docs/vmanomaly: fix table rendering (#8005)
### Describe Your Changes

- fix table rendering on writer and scheduler pages

### Checklist

The following checks are **mandatory**:

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

(cherry picked from commit 92b6475fa6)
2025-01-14 14:29:46 +01:00
Andrii Chubatiuk
284a5f3c5f docs: added search resupts page for docs.victoriametrics.com (#8017)
### Describe Your Changes

added search page required for docs site

### Checklist

The following checks are **mandatory**:

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

(cherry picked from commit bda3546cfd)
2025-01-14 14:29:46 +01:00
Artem Navoiev
d0e435dd20 docs: cloud use separate support email for cloud (#8013)
### Describe Your Changes

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

### Checklist

The following checks are **mandatory**:

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

Signed-off-by: Artem Navoiev <tenmozes@gmail.com>
(cherry picked from commit 2691cdefe3)
2025-01-14 14:29:46 +01:00
Github Actions
ebe0d36fa0 Automatic update helm docs from VictoriaMetrics/helm-charts@457951a (#8014)
Automated changes by
[create-pull-request](https://github.com/peter-evans/create-pull-request)
GitHub action

Signed-off-by: Github Actions <133988544+victoriametrics-bot@users.noreply.github.com>
Co-authored-by: f41gh7 <18450869+f41gh7@users.noreply.github.com>
(cherry picked from commit 93b8aa5c9d)
2025-01-14 14:29:46 +01:00
Aliaksandr Valialkin
fab8260899 docs/VictoriaLogs/README.md: add "Profiling" chapter
This chapter is needed for referring from Github issues when CPU or memory profiles are needed to be collected
in order to investigate issues with high CPU and/or RAM usage at VictoriaLogs.
2025-01-13 09:44:35 +01:00
Aliaksandr Valialkin
b40d096291 LICENSE: update the current year from 2024 to 2025 2025-01-13 09:44:34 +01:00
Aliaksandr Valialkin
12f6df291c deployment/docker: update VictoriaLogs Docker image tag from v1.4.0-victorialogs to v1.5.0-victorialogs
See https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.5.0-victorialogs
2025-01-13 07:49:14 +01:00
Aliaksandr Valialkin
78df661c9f docs/VictoriaLogs/CHANGELOG.md: cut v1.5.0-victorialogs release 2025-01-13 07:49:13 +01:00
Roman Khavronenko
9de0b8a165 make: bump golangci-lint to v1.63.4 (
New version has additional checks and reduced resource consumption, so
it doesn't timeout for our internal repos.

To make linter happy, I addressed "redefinition of the built-in
function" lint error.

----
Signed-off-by: hagen1778 <roman@victoriametrics.com>
2025-01-13 07:23:21 +01:00
Aliaksandr Valialkin
c4b3def5b0 lib/logstorage: format pipe: add frequently used formatters
- url encoding / decoding with <urlencode:field> and <urldecode:field>
- base64 encoding / decoding with <base64encode:field> and <base64decode:field>
- hex encoding / decoding with <hexencode:field> and <hexdecode:field>
- hex encoding for integers with <hexnumencode:field> and <hexnumdecode:field>
2025-01-13 07:23:20 +01:00
Aliaksandr Valialkin
262ccf7963 lib/logstorage: explicitly pass statsFunc to statsProcessor methods
This allows reducing the state of every statsProcessor by removing pointer to the corresponding statsFunc.
For example, this reduces statsCountProcessor size by 2x.
2025-01-13 07:23:20 +01:00
Aliaksandr Valialkin
a2003de567 lib/logstorage: stats pipe: stop finalizeStats() as soon as the query is canceled
Previoysly finalizeStats() for some functions such as count_uniq() could run for long periods
of time after the query is canceled, since stopCh wan't propagated to finalizeStats().
2025-01-13 07:23:20 +01:00
Aliaksandr Valialkin
1ac4516919 lib/logstorage: stats pipe: use integer group keys if stats by(...) contains a single field with integer values
This reduces memory usage and improves performance, since access to a map with integer keys
is faster than access to a map with string keys.
2025-01-13 07:23:20 +01:00
Aliaksandr Valialkin
c17149cb0c lib/logstorage: count_uniq and count_uniq_hash stats functions: avoid converting integer values to strings
Prevsiously integer values were converted to strings before being passed to `updateState()` function at `count_uniq`
and `count_uniq_hash`. Later such values are converted back to integers in order to track them via integer map of unique values.

This commit avoids the int -> string -> int conversion. Instead, it passes integers directly to the integer map of unique values.
This improves performance of `count_uniq` and `count_uniq_hash` functions even further.
2025-01-13 07:23:20 +01:00
Aliaksandr Valialkin
9ff6128102 lib/logstorage: add value_type filter to LogsQL
This filter can be used when debugging and exploring logs in order to understand better
which value types are used for storing the particular log fields.

The `value_type` filter complements `block_stats` pipe.
2025-01-13 07:23:19 +01:00
Aliaksandr Valialkin
8efd21519d lib/logstorage: run make fmt after e610edf045 2025-01-13 07:23:19 +01:00
Aliaksandr Valialkin
fcc692d00e lib/logstorage: improve performance for math pipe
- Pass the calculated results to the next pipe in float64 columns.
  Previously the results were converted to string columns. This could slow down further calculations.

- Use custom optimized logic for processing numeric columns, which are passed to math pipe.
  Previously all the input columns were converted to string and then converted to float64
  before math pipe calculations.

- Initialize the newly added columns at blockResult as soon as they are added.
  This improves performance when big number of columns are calculated by math pipe.
2025-01-13 07:23:19 +01:00
Aliaksandr Valialkin
6b59561f96 lib/logstorage: track integer values in integer maps when counting the number of unique values at count_uniq stats function
Previously integer values were tracked in string maps. Now every input value is parsed as integer.
On success the parsed integer is tracked via specialized maps, which hold only integers.
This reduces CPU usage and memory usage in general case.
2025-01-13 07:23:19 +01:00
Aliaksandr Valialkin
2e8c678e19 lib/logstorage: avoid copying column name inside blockSearch.getColumnHeader() and blockSearch.getConstColumnValue()
Use the column name attached to the corresponding part. The lifetime of this column name exceed the blockSearch lifetime,
so it is safe using it here.

This is a follow-up for 8d968acd0a
2025-01-13 07:23:19 +01:00
Aliaksandr Valialkin
0b2b29c3d1 lib/logstorage: automatically detect columns with int64 values and store them as packed 8-byte int64 values
Previously columns with negative int64 values were stored either as float64 or string
depending on whether the negative int64 values are bigger or smaller than -2^53.
If the integer values are smaller than -2^53, then they are stored as string, since float64 cannot
hold such values without precision loss. Now such values are stored as int64.
This should improve compression ratio and query performance over columns with negative int64 values.
2025-01-13 07:23:18 +01:00
Aliaksandr Valialkin
e95b491221 lib/logstorage: make sure that the automatic conversion of field values to float64 is lossless
Previously field values could be automatically converted to float64 with precision loss.
This could lead to unexpected results when querying such field values.
For example, "10007199254740992" was incorrectly represented as 10007199254740993.
This commit prevents from such lossy conversions when storing field values.

While at it, prevent from int64 overflow at tryParseBytes and tryParseDuration functions,
which are used for parsing constants in queries for byte sizes and durations.
Now these functions return 1<<63-1 (the maximum int64 value) for constants exceeding
this value. Previously they could return arbitrary garbage for such constants.
2025-01-13 07:23:18 +01:00
Aliaksandr Valialkin
2e61cff968 app/vlinsert/insertutils: avoid excess copying of lines at LineReader.buf
1. Do not copy every line from LineReader.buf to LineReader.Line - just refer the line at LineReader.buf.
2. Do not copy the next found line to the beginning of LineReader.buf - just track the next line start index with LineReader.bufOffset.

This reduces memory copying when many lines are read into LineReader.buf by a single read() syscall.
2025-01-13 07:23:18 +01:00
Roman Khavronenko
0614eb97a5 app/vmselect/promql: account for staleness when populating realPrevValue (#8002)
When vmselect process a rollup function it fetches all the raw samples
on requested `start-end` interval of the query. It then loops through
the raw samples, picks the range of the samples based on provided `step`
interval and invokes a rollup function for each of the picked ranges of
samples.

During this processing, vmselect always populates the `realPrevValue`
field with the closest previous raw sample value before the picked range
of samples. This `realPrevValue` is used by rollup functions like
increase_pure or delta to decide whether the counter change happened or
not. For example, we get the counter value == 1. If we've seen this
counter before and its value was also 1 - then no change happened. If we
didn't see it before, then this counter should have started with value=0
and we need to account for `1-0=1` change. All this is required to deal
with situations when scrapes are missing or `step` is too small.

However, vmselect doesn't check how "old" is the `realPrevValue`. In
other words, it doesn't respect the staleness interval when picking it.
In result, depending on the `start` and `end` params, vmselect can use
`realPrevValue` which is a couple of hours old and is unlikely to be a
temporary scrape fail. In result, some increases can be incorrectly
ingnored by vmselect.

This change makes sure that vmselect doesn't populate `realPrevValue`
with samples that are older than staleness interval.

### Describe Your Changes

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

### Checklist

The following checks are **mandatory**:

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


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

To reproduce, create a dataset with one metric `foo` which has samples
with value=1 on interval of couple of hours and resolution 15s, and a
gap for an hour in the middle:
<img width="769" alt="image"
src="https://github.com/user-attachments/assets/a39b2740-b741-45f8-ad18-093b7c57c3b3"
/>

Then run `increase(foo[1m])` expression on this time range (disable
cache):
<img width="1472" alt="image"
src="https://github.com/user-attachments/assets/463cece1-f359-4c75-a96c-60092a31cab2"
/>

In result, there will be one increase on the beginning of the series.
And no increase after the gap. Then change the time range so it starts
in the middle of the gap:
<img width="1505" alt="image"
src="https://github.com/user-attachments/assets/f4a460c3-9fd1-4ec7-ab47-15e716ec1019"
/>

Now, there is an increase>0 because the `realPrevValue` wasn't
populated. This is wrong, because it hides the increase of the series.

With the fix, the original increase query on full time range should show
2 increases:
<img width="1492" alt="image"
src="https://github.com/user-attachments/assets/aa9d8a6b-7b22-41f6-9eb9-83b3113a6982"
/>

Signed-off-by: hagen1778 <roman@victoriametrics.com>
2025-01-13 07:23:17 +01:00
Aliaksandr Valialkin
f29406f853 vendor: run make vendor-update
Add exclude google.golang.org/grpc/stats/opentelemetry v0.0.0-20240907200651-3ffb98b2c93a to go.mod
according to https://github.com/googleapis/google-cloud-go/issues/11283#issuecomment-2558515586 .

This fixes the following strange issue on `make vendor-update`:

cloud.google.com/go/storage imports
        google.golang.org/grpc/stats/opentelemetry: ambiguous import: found package google.golang.org/grpc/stats/opentelemetry in multiple modules:
        google.golang.org/grpc v1.69.0 (/go/pkg/mod/google.golang.org/grpc@v1.69.0/stats/opentelemetry)
        google.golang.org/grpc/stats/opentelemetry v0.0.0-20240907200651-3ffb98b2c93a (/go/pkg/mod/google.golang.org/grpc/stats/opentelemetry@v0.0.0-20240907200651-3ffb98b2c93a)
2025-01-12 03:23:17 +01:00
f41gh7
49faf1b395 docs/changelog: removes duplicate record for maxIngestionRate feature
Signed-off-by: f41gh7 <nik@victoriametrics.com>
2025-01-10 19:17:29 +04:00
Github Actions
89da24ed8e Automatic update operator docs from VictoriaMetrics/operator@43f5554 (#7923)
Automated changes by
[create-pull-request](https://github.com/peter-evans/create-pull-request)
GitHub action

Signed-off-by: Github Actions <133988544+victoriametrics-bot@users.noreply.github.com>
Co-authored-by: AndrewChubatiuk <3162380+AndrewChubatiuk@users.noreply.github.com>
(cherry picked from commit 5afbee5f6f)
2025-01-10 19:16:09 +04:00
Github Actions
08bbd86e92 Automatic update helm docs from VictoriaMetrics/helm-charts@1f33c21 (#7929)
Automated changes by
[create-pull-request](https://github.com/peter-evans/create-pull-request)
GitHub action

Signed-off-by: Github Actions <133988544+victoriametrics-bot@users.noreply.github.com>
Co-authored-by: f41gh7 <18450869+f41gh7@users.noreply.github.com>
(cherry picked from commit 51459196f9)
2025-01-10 19:14:45 +04:00
Dima Shur
07bb605682 docs: changed typo in label (enterpriSe instead of enterpriZe) (#7925)
### Describe Your Changes

Fixed typo in contributing.md (enterpriZe -> enterpriSe in the label
name)

### Checklist

The following checks are **mandatory**:

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

(cherry picked from commit 7941877233)
2025-01-10 19:13:51 +04:00
Zakhar Bessarab
dd933fe52c docs/CHANGELOG.md: cut v1.109.0
Signed-off-by: Zakhar Bessarab <z.bessarab@victoriametrics.com>
2025-01-10 18:57:58 +04:00
Zakhar Bessarab
357ae06c37 app/{vmselect,vlselect}: run make vmui-update vmui-logs-update
Signed-off-by: Zakhar Bessarab <z.bessarab@victoriametrics.com>
2025-01-10 18:02:30 +04:00
Nikolay
ca5e105ada lib/storage: add a hint for merge about type of parts in merge (#7998)
Hint allows to choose type of cache to be used for index search:
- in-memory parts are storing recently ingested samples and should use
main cache. This improves ingestion speed and cache hit ration for
queries accessing recently ingested samples.
- merges of file parts is performed in background, using a separate
cache allows avoiding pollution of the main cache with irrelevant
entries.

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

---------

Signed-off-by: f41gh7 <nik@victoriametrics.com>
(cherry picked from commit e9f86af7f5)
2025-01-10 17:45:20 +04:00
Nikolay
688f667919 lib/storage: make finalDedup schedule interval configurable
This commit makes configurable interval for checking if final dedup
process for the historical data should be started. It allows to spread
resource utilisation for multiple vmstorage/vmsingle instances in time.
Since final dedup may add additional preasure on disk, backup systems
and make cluster less stable. Storage unconditionally adds 25% jitter to
the provided value, it should simplify configuration management at
Kubernetes ecosystem. Because Kubernetes application pods must have the
same configuration.

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

---------

Signed-off-by: f41gh7 <nik@victoriametrics.com>
Co-authored-by: Roman Khavronenko <roman@victoriametrics.com>
(cherry picked from commit 9ada784983)
2025-01-10 16:56:08 +04:00
f41gh7
221bbb6381 github/workflows: set GOGC=10 for unit tests
It reduces memory usage during tests execution. It makes tests execution more reliable.
Since it sometimes crashes with OOM at small github runners.

Signed-off-by: f41gh7 <nik@victoriametrics.com>
(cherry picked from commit a83ee2b3f1)
2025-01-10 16:51:26 +04:00
Github Actions
c7484ec9bb Automatic update helm docs from VictoriaMetrics/helm-charts@117506e (#7996)
Automated changes by
[create-pull-request](https://github.com/peter-evans/create-pull-request)
GitHub action

Signed-off-by: Github Actions <133988544+victoriametrics-bot@users.noreply.github.com>
Co-authored-by: AndrewChubatiuk <3162380+AndrewChubatiuk@users.noreply.github.com>

(cherry picked from commit 2564f10d98)
Signed-off-by: Zakhar Bessarab <z.bessarab@victoriametrics.com>
2025-01-10 16:51:19 +04:00
Andrii Chubatiuk
ea434aedac victorialogs: ugraded datadog extension version in compose to one which supports custom endpoint configuration (#7989)
recently new datadog extension was released, where custom endpoint
configuration was added

### Describe Your Changes

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

### Checklist

The following checks are **mandatory**:

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

(cherry picked from commit 0871770634)
2025-01-10 16:51:10 +04:00
Zakhar Bessarab
441f9efa5d app/vmalert/notifier: fix rendering of Alertmanager notification body
commitL  c7fc0d0d2f  enabled skipping alerts
in case there is no labels present for an alert. This made clause which
was adding a comma for the JSON list incorrect as it is not possible to
determine if the next alert will be skipped or not.

This fix renders all alert labels in advance allowing properly format
JSON payload for Alertmanager notification.

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

Signed-off-by: Zakhar Bessarab <z.bessarab@victoriametrics.com>

(cherry picked from commit 51b21dfd57)
Signed-off-by: Zakhar Bessarab <z.bessarab@victoriametrics.com>
2025-01-10 16:51:06 +04:00
Zhu Jiekun
3c1282ae7c lib/promscrape: add Marathon service discovery
This commit adds support for [Marathon](https://mesosphere.github.io/marathon/)
service discovery to the scrape configuration.

The following flag is introduced:
```
  -promscrape.marathonSDCheckInterval duration
          Interval for checking for changes in Marathon service discovery. This works only if marathon_sd_configs is configured in '-promscrape.config' file. See https://docs.victoriametrics.com/sd_configs.html#marathon_sd_configs for details  (default 30s)
```

The service discovery could be config like:
```yaml
scrape_configs:
- job_name: marathon_job
  marathon_sd_configs:
      servers:
        - "..."
        - "..."
```
See:
[b555d94d1a/docs/sd_configs.md (marathon_sd_configs))

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

---------

Co-authored-by: Zakhar Bessarab <z.bessarab@victoriametrics.com>
(cherry picked from commit 276989716f)
2025-01-10 16:50:39 +04:00
Artem Fetishev
7620da0355 Update VictoriaLogs FAQ: add a section about max log record length (#7984)
### Describe Your Changes

There has been a question in our public Slack on whether the length
limit of a log record is going to be changed. See:
https://victoriametrics.slack.com/archives/C05UNTPAEDN/p1736156255119689

This PR documents the max length and explains why it has been chosen.
This FAQ section could serve as an answer to more questions like this.

### Checklist

The following checks are **mandatory**:

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

---------

Signed-off-by: Artem Fetishev <rtm@victoriametrics.com>
Co-authored-by: Aliaksandr Valialkin <valyala@victoriametrics.com>
(cherry picked from commit 6cb3c0cac8)
2025-01-10 16:50:35 +04:00
cuiweiyuan
56f98e74f5 chore: fix function name in comment (#7926)
### Describe Your Changes

 fix function name in comment

### Checklist

The following checks are **mandatory**:

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

Signed-off-by: cuiweiyuan <cuiweiyuan@aliyun.com>
(cherry picked from commit d064e14933)
2025-01-10 16:50:32 +04:00
Afolabi Badmos
355397be48 vmauth: fix bug in discovering ipv6 addresses (#7955)
### Describe Your Changes

Fixes error in `vmauth` when discovering ipv6 addresses.

`vmauth` attempts to [slice till
`:`](https://github.com/VictoriaMetrics/VictoriaMetrics/blob/master/app/vmauth/auth_config.go#L397)
in the discovered addresses without accounting for ipv6. This causes it
to fail in ipv6 only environments.

```sh
$ nslookup vmselect.ns.svc.cluster.local

...
Name: vmselect.ns.svc.cluster.local
Address: 2600:dead:beef:dead:beef::8
```

```sh
$ kubectl logs -f vmauth

...
error: dial tcp: lookup 2600: no such host
```

### Checklist

The following checks are **mandatory**:

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

---------

Co-authored-by: f41gh7 <nik@victoriametrics.com>
(cherry picked from commit 77b0fcfdd9)
2025-01-10 16:50:29 +04:00
Yury Molodov
803a264a5b vmui/logs: add autocomplete support for LogsQL (#6949)
### Describe Your Changes

This pull request adds support for autocomplete in LogsQL queries. The
new feature provides suggestions for field names, field values, and pipe
names as you type.

---------

Co-authored-by: Aliaksandr Valialkin <valyala@victoriametrics.com>
(cherry picked from commit ee7fe11fd2)
2025-01-10 16:50:24 +04:00
Andrii Chubatiuk
9e4c4c57a5 docs: make badges in docs clickable (#7960)
### Describe Your Changes

added links to badges and made them clickable at
docs.victoriametrics.com

### Checklist

The following checks are **mandatory**:

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

(cherry picked from commit 4c26fb6fe5)
2025-01-10 16:50:17 +04:00
Github Actions
2a0aec2079 Automatic update operator docs from VictoriaMetrics/operator@028ab8b (#7981)
Automated changes by
[create-pull-request](https://github.com/peter-evans/create-pull-request)
GitHub action

Signed-off-by: Github Actions <133988544+victoriametrics-bot@users.noreply.github.com>
Co-authored-by: f41gh7 <18450869+f41gh7@users.noreply.github.com>
(cherry picked from commit fc135094b3)
2025-01-10 16:50:15 +04:00
Roman Khavronenko
3582e13d22 docs: mention publicly available playgrounds (#7977)
The point of the new section is to highlight publicly available
playgrounds for users. All of them were mentioned in other parts of the
documentation, but we didn't have all of them in one place before.

### Describe Your Changes

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

### Checklist

The following checks are **mandatory**:

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

Signed-off-by: hagen1778 <roman@victoriametrics.com>
(cherry picked from commit 5d42f21abd)
2025-01-10 16:50:12 +04:00
hagen1778
0db40bf8cc docs: rm extra new lines as they bring no value
Signed-off-by: hagen1778 <roman@victoriametrics.com>
(cherry picked from commit 28eeabded1)
2025-01-10 16:50:09 +04:00
hagen1778
79a327428d docs: make vmui related pages below the vmui parent page
This change only updates the hierarchy of pages within the readme.

Signed-off-by: hagen1778 <roman@victoriametrics.com>
(cherry picked from commit b6910cfff7)
2025-01-10 16:50:07 +04:00
Zhu Jiekun
ef9e75748a docs: clarify extra resource is needed for downsampling/retention filter (#7974)
### Describe Your Changes

clarify extra resource is needed when downsampling with filter(s) or
retention filter(s) is applied

### Checklist

The following checks are **mandatory**:

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

(cherry picked from commit 8938ef398c)
2025-01-10 16:50:04 +04:00
hagen1778
76e92067fc docs: fix markdown typos
Signed-off-by: hagen1778 <roman@victoriametrics.com>
(cherry picked from commit df2b75fa81)
2025-01-10 16:49:59 +04:00
hagen1778
5afa4b1a58 docs: fix markdown typo
Signed-off-by: hagen1778 <roman@victoriametrics.com>
(cherry picked from commit 857734c66c)
2025-01-10 16:49:07 +04:00
Github Actions
a23d37b084 Automatic update helm docs from VictoriaMetrics/helm-charts@7004727 (#7958)
Automated changes by
[create-pull-request](https://github.com/peter-evans/create-pull-request)
GitHub action

Signed-off-by: Github Actions <133988544+victoriametrics-bot@users.noreply.github.com>
Co-authored-by: AndrewChubatiuk <3162380+AndrewChubatiuk@users.noreply.github.com>

(cherry picked from commit bedc0c0f8f)
Signed-off-by: Zakhar Bessarab <z.bessarab@victoriametrics.com>
2025-01-10 16:49:03 +04:00
Zakhar Bessarab
8f8b29355d app/vmselect/promql: set tenant information for numbers
Since
44b071296d
`evalNumber` function no longer updating MetricName tenancy information.
This leads to mismatch in metric names between the query result and
evaluated number for all tenants other than 0:0.

For example, query `count(up) or 0` will return different results for
tenants 0:0 and 1:1 (assuming up is present for both tenants):
- tenant 0:0 - will only contain result of `count(up)`
- tenant 1:1 - will return both `count(up)` and `0` since metric names
will not be matched

This restores setting of tenancy information for metric name for
single-tenant queries.

Related issue:

 https://github.com/VictoriaMetrics/VictoriaMetrics/issues/7987
---
Signed-off-by: Zakhar Bessarab <z.bessarab@victoriametrics.com>
2025-01-10 09:57:58 +01:00
1649 changed files with 39308 additions and 419826 deletions

View File

@@ -85,7 +85,7 @@ jobs:
restore-keys: go-artifacts-${{ runner.os }}-${{ matrix.scenario }}-
- name: Run tests
run: make ${{ matrix.scenario}}
run: GOGC=10 make ${{ matrix.scenario}}
- name: Publish coverage
uses: codecov/codecov-action@v5

View File

@@ -175,7 +175,7 @@
END OF TERMS AND CONDITIONS
Copyright 2019-2024 VictoriaMetrics, Inc.
Copyright 2019-2025 VictoriaMetrics, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.

View File

@@ -204,19 +204,19 @@ check-all: fmt vet golangci-lint govulncheck
clean-checkers: remove-golangci-lint remove-govulncheck
test:
DISABLE_FSYNC_FOR_TESTING=1 go test ./lib/... ./app/...
go test ./lib/... ./app/...
test-race:
DISABLE_FSYNC_FOR_TESTING=1 go test -race ./lib/... ./app/...
go test -race ./lib/... ./app/...
test-pure:
DISABLE_FSYNC_FOR_TESTING=1 CGO_ENABLED=0 go test ./lib/... ./app/...
CGO_ENABLED=0 go test ./lib/... ./app/...
test-full:
DISABLE_FSYNC_FOR_TESTING=1 go test -coverprofile=coverage.txt -covermode=atomic ./lib/... ./app/...
go test -coverprofile=coverage.txt -covermode=atomic ./lib/... ./app/...
test-full-386:
DISABLE_FSYNC_FOR_TESTING=1 GOARCH=386 go test -coverprofile=coverage.txt -covermode=atomic ./lib/... ./app/...
GOARCH=386 go test -coverprofile=coverage.txt -covermode=atomic ./lib/... ./app/...
integration-test: all
go test ./apptest/... -skip="^TestSingle.*"
@@ -258,7 +258,7 @@ golangci-lint: install-golangci-lint
golangci-lint run
install-golangci-lint:
which golangci-lint || curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(shell go env GOPATH)/bin v1.60.3
which golangci-lint || curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(shell go env GOPATH)/bin v1.63.4
remove-golangci-lint:
rm -rf `which golangci-lint`

View File

@@ -1,12 +1,14 @@
# VictoriaMetrics
[![Latest Release](https://img.shields.io/github/release/VictoriaMetrics/VictoriaMetrics.svg?style=flat-square)](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/latest)
[![Docker Pulls](https://img.shields.io/docker/pulls/victoriametrics/victoria-metrics.svg?maxAge=604800)](https://hub.docker.com/r/victoriametrics/victoria-metrics)
[![Slack](https://img.shields.io/badge/join%20slack-%23victoriametrics-brightgreen.svg)](https://slack.victoriametrics.com/)
[![GitHub license](https://img.shields.io/github/license/VictoriaMetrics/VictoriaMetrics.svg)](https://github.com/VictoriaMetrics/VictoriaMetrics/blob/master/LICENSE)
[![Go Report](https://goreportcard.com/badge/github.com/VictoriaMetrics/VictoriaMetrics)](https://goreportcard.com/report/github.com/VictoriaMetrics/VictoriaMetrics)
[![Build Status](https://github.com/VictoriaMetrics/VictoriaMetrics/workflows/main/badge.svg)](https://github.com/VictoriaMetrics/VictoriaMetrics/actions)
[![codecov](https://codecov.io/gh/VictoriaMetrics/VictoriaMetrics/branch/master/graph/badge.svg)](https://codecov.io/gh/VictoriaMetrics/VictoriaMetrics)
![Latest Release](https://img.shields.io/github/v/release/VictoriaMetrics/VictoriaMetrics?sort=semver&label=&filter=!*-victorialogs&logo=github&labelColor=gray&color=gray&link=https%3A%2F%2Fgithub.com%2FVictoriaMetrics%2FVictoriaMetrics%2Freleases%2Flatest)
![Docker Pulls](https://img.shields.io/docker/pulls/victoriametrics/victoria-metrics?label=&logo=docker&logoColor=white&labelColor=2496ED&color=2496ED&link=https%3A%2F%2Fhub.docker.com%2Fr%2Fvictoriametrics%2Fvictoria-metrics)
![Go Report](https://goreportcard.com/badge/github.com/VictoriaMetrics/VictoriaMetrics?link=https%3A%2F%2Fgoreportcard.com%2Freport%2Fgithub.com%2FVictoriaMetrics%2FVictoriaMetrics)
![Build Status](https://github.com/VictoriaMetrics/VictoriaMetrics/workflows/main/badge.svg?link=https%3A%2F%2Fgithub.com%2FVictoriaMetrics%2FVictoriaMetrics%2Factions)
![codecov](https://codecov.io/gh/VictoriaMetrics/VictoriaMetrics/branch/master/graph/badge.svg?link=https%3A%2F%2Fcodecov.io%2Fgh%2FVictoriaMetrics%2FVictoriaMetrics)
![License](https://img.shields.io/github/license/VictoriaMetrics/VictoriaMetrics?labelColor=green&label=&link=https%3A%2F%2Fgithub.com%2FVictoriaMetrics%2FVictoriaMetrics%2Fblob%2Fmaster%2FLICENSE)
![Slack](https://img.shields.io/badge/Join-4A154B?logo=slack&link=https%3A%2F%2Fslack.victoriametrics.com)
![X](https://img.shields.io/twitter/follow/VictoriaMetrics?style=flat&label=Follow&color=black&logo=x&labelColor=black&link=https%3A%2F%2Fx.com%2FVictoriaMetrics)
![Reddit](https://img.shields.io/reddit/subreddit-subscribers/VictoriaMetrics?style=flat&label=Join&labelColor=red&logoColor=white&logo=reddit&link=https%3A%2F%2Fwww.reddit.com%2Fr%2FVictoriaMetrics)
<picture>
<source srcset="docs/logo_white.webp" media="(prefers-color-scheme: dark)">

View File

@@ -15,6 +15,8 @@ import (
// LineReader reads newline-delimited lines from the underlying reader
type LineReader struct {
// Line contains the next line read after the call to NextLine
//
// The Line contents is valid until the next call to NextLine.
Line []byte
// name is the LineReader name
@@ -26,6 +28,9 @@ type LineReader struct {
// buf is a buffer for reading the next line
buf []byte
// bufOffset is the offset at buf to read the next line from
bufOffset int
// err is the last error when reading data from r
err error
@@ -51,26 +56,27 @@ func NewLineReader(name string, r io.Reader) *LineReader {
// Check for Err in this case.
func (lr *LineReader) NextLine() bool {
for {
if len(lr.buf) == 0 {
if lr.bufOffset >= len(lr.buf) {
if lr.err != nil || lr.eofReached {
return false
}
if !lr.readMoreData() {
return false
}
if len(lr.buf) == 0 && lr.eofReached {
if lr.bufOffset >= len(lr.buf) && lr.eofReached {
return false
}
}
if n := bytes.IndexByte(lr.buf, '\n'); n >= 0 {
lr.Line = append(lr.Line[:0], lr.buf[:n]...)
lr.buf = append(lr.buf[:0], lr.buf[n+1:]...)
buf := lr.buf[lr.bufOffset:]
if n := bytes.IndexByte(buf, '\n'); n >= 0 {
lr.Line = buf[:n]
lr.bufOffset += n + 1
return true
}
if lr.eofReached {
lr.Line = append(lr.Line[:0], lr.buf...)
lr.buf = lr.buf[:0]
lr.Line = buf
lr.bufOffset += len(buf)
return true
}
if !lr.readMoreData() {
@@ -88,6 +94,11 @@ func (lr *LineReader) Err() error {
}
func (lr *LineReader) readMoreData() bool {
if lr.bufOffset > 0 {
lr.buf = append(lr.buf[:0], lr.buf[lr.bufOffset:]...)
lr.bufOffset = 0
}
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)

View File

@@ -176,7 +176,7 @@ func writeCompactObject(w io.Writer, fields []logstorage.Field) error {
_, err := fmt.Fprintf(w, "%s\n", fields[0].Value)
return err
}
if len(fields) == 2 && fields[0].Name == "_time" || fields[1].Name == "_time" {
if len(fields) == 2 && (fields[0].Name == "_time" || fields[1].Name == "_time") {
// Write _time\tfieldValue as is
if fields[0].Name == "_time" {
_, err := fmt.Fprintf(w, "%s\t%s\n", fields[0].Value, fields[1].Value)

View File

@@ -45,6 +45,8 @@ var (
"see https://docs.victoriametrics.com/victorialogs/keyconcepts/#data-model")
u64FieldsPerLog = flag.Int("u64FieldsPerLog", 1, "The number of fields with uint64 values to generate per each log entry; "+
"see https://docs.victoriametrics.com/victorialogs/keyconcepts/#data-model")
i64FieldsPerLog = flag.Int("i64FieldsPerLog", 1, "The number of fields with int64 values to generate per each log entry; "+
"see https://docs.victoriametrics.com/victorialogs/keyconcepts/#data-model")
floatFieldsPerLog = flag.Int("floatFieldsPerLog", 1, "The number of fields with float64 values to generate per each log entry; "+
"see https://docs.victoriametrics.com/victorialogs/keyconcepts/#data-model")
ipFieldsPerLog = flag.Int("ipFieldsPerLog", 1, "The number of fields with IPv4 values to generate per each log entry; "+
@@ -254,6 +256,9 @@ func generateLogsAtTimestamp(bw *bufio.Writer, workerID int, ts int64, firstStre
for j := 0; j < *u64FieldsPerLog; j++ {
fmt.Fprintf(bw, `,"u64_%d":"%d"`, j, rand.Uint64())
}
for j := 0; j < *i64FieldsPerLog; j++ {
fmt.Fprintf(bw, `,"i64_%d":"%d"`, j, int64(rand.Uint64()))
}
for j := 0; j < *floatFieldsPerLog; j++ {
fmt.Fprintf(bw, `,"float_%d":"%v"`, j, math.Round(10_000*rand.Float64())/1000)
}

View File

@@ -688,13 +688,13 @@ func ProcessStatsQueryRangeRequest(ctx context.Context, w http.ResponseWriter, r
m := make(map[string]*statsSeries)
var mLock sync.Mutex
timestamp := q.GetTimestamp()
writeBlock := func(_ uint, timestamps []int64, columns []logstorage.BlockColumn) {
clonedColumnNames := make([]string, len(columns))
for i, c := range columns {
clonedColumnNames[i] = strings.Clone(c.Name)
}
for i := range timestamps {
timestamp := q.GetTimestamp()
labels := make([]logstorage.Field, 0, len(byFields))
for j, c := range columns {
if c.Name == "_time" {

View File

@@ -28,7 +28,7 @@ func TestParseExtraFilters_Success(t *testing.T) {
// LogsQL filter
f(`foobar`, `foobar`)
f(`foo:bar`, `foo:bar`)
f(`foo:(bar or baz) error _time:5m {"foo"=bar,baz="z"}`, `(foo:bar or foo:baz) error _time:5m {foo="bar",baz="z"}`)
f(`foo:(bar or baz) error _time:5m {"foo"=bar,baz="z"}`, `{foo="bar",baz="z"} (foo:bar or foo:baz) error _time:5m`)
}
func TestParseExtraFilters_Failure(t *testing.T) {
@@ -77,7 +77,7 @@ func TestParseExtraStreamFilters_Success(t *testing.T) {
// LogsQL filter
f(`foobar`, `foobar`)
f(`foo:bar`, `foo:bar`)
f(`foo:(bar or baz) error _time:5m {"foo"=bar,baz="z"}`, `(foo:bar or foo:baz) error _time:5m {foo="bar",baz="z"}`)
f(`foo:(bar or baz) error _time:5m {"foo"=bar,baz="z"}`, `{foo="bar",baz="z"} (foo:bar or foo:baz) error _time:5m`)
}
func TestParseExtraStreamFilters_Failure(t *testing.T) {

View File

@@ -1,13 +1,12 @@
{
"files": {
"main.css": "./static/css/main.fa83344e.css",
"main.js": "./static/js/main.8ad2bc1f.js",
"main.css": "./static/css/main.02a1c6cb.css",
"main.js": "./static/js/main.55c8060b.js",
"static/js/685.f772060c.chunk.js": "./static/js/685.f772060c.chunk.js",
"static/media/MetricsQL.md": "./static/media/MetricsQL.a00044c91d9781cf8557.md",
"index.html": "./index.html"
},
"entrypoints": [
"static/css/main.fa83344e.css",
"static/js/main.8ad2bc1f.js"
"static/css/main.02a1c6cb.css",
"static/js/main.55c8060b.js"
]
}

View File

@@ -1 +1 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="./favicon.svg"/><link rel="apple-touch-icon" href="./favicon.svg"/><link rel="mask-icon" href="./favicon.svg" color="#000000"><meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=5"/><meta name="theme-color" content="#000000"/><meta name="description" content="Explore your log data with VictoriaLogs UI"/><link rel="manifest" href="./manifest.json"/><title>UI for VictoriaLogs</title><meta name="twitter:card" content="summary"><meta name="twitter:title" content="UI for VictoriaLogs"><meta name="twitter:site" content="@https://victoriametrics.com/products/victorialogs/"><meta name="twitter:description" content="Explore your log data with VictoriaLogs UI"><meta name="twitter:image" content="./preview.jpg"><meta property="og:type" content="website"><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 defer="defer" src="./static/js/main.8ad2bc1f.js"></script><link href="./static/css/main.fa83344e.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="./favicon.svg"/><link rel="apple-touch-icon" href="./favicon.svg"/><link rel="mask-icon" href="./favicon.svg" color="#000000"><meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=5"/><meta name="theme-color" content="#000000"/><meta name="description" content="Explore your log data with VictoriaLogs UI"/><link rel="manifest" href="./manifest.json"/><title>UI for VictoriaLogs</title><meta name="twitter:card" content="summary"><meta name="twitter:title" content="UI for VictoriaLogs"><meta name="twitter:site" content="@https://victoriametrics.com/products/victorialogs/"><meta name="twitter:description" content="Explore your log data with VictoriaLogs UI"><meta name="twitter:image" content="./preview.jpg"><meta property="og:type" content="website"><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 defer="defer" src="./static/js/main.55c8060b.js"></script><link href="./static/css/main.02a1c6cb.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -160,8 +160,8 @@ func (m *manager) update(ctx context.Context, groupsCfg []config.Group, restore
// it is important to call InterruptEval before the update, because cancel fn
// can be re-assigned during the update.
item.old.InterruptEval()
go func(old *rule.Group, new *rule.Group) {
old.UpdateWith(new)
go func(oldGroup *rule.Group, newGroup *rule.Group) {
oldGroup.UpdateWith(newGroup)
wg.Done()
}(item.old, item.new)
}

View File

@@ -13,6 +13,7 @@ import (
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/utils"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/httputils"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promauth"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/prompbmarshal"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promrelabel"
)
@@ -69,7 +70,17 @@ func (am *AlertManager) Send(ctx context.Context, alerts []Alert, headers map[st
func (am *AlertManager) send(ctx context.Context, alerts []Alert, headers map[string]string) error {
b := &bytes.Buffer{}
writeamRequest(b, alerts, am.argFunc, am.relabelConfigs)
alertsToSend := alerts[:0]
lblss := make([][]prompbmarshal.Label, 0, len(alerts))
for _, a := range alerts {
lbls := a.applyRelabelingIfNeeded(am.relabelConfigs)
if len(lbls) == 0 {
continue
}
alertsToSend = append(alertsToSend, a)
lblss = append(lblss, lbls)
}
writeamRequest(b, alertsToSend, am.argFunc, lblss)
req, err := http.NewRequest(http.MethodPost, am.addr.String(), b)
if err != nil {

View File

@@ -1,15 +1,14 @@
{% import (
"time"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promrelabel"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/prompbmarshal"
) %}
{% stripspace %}
{% func amRequest(alerts []Alert, generatorURL func(Alert) string, relabelCfg *promrelabel.ParsedConfigs) %}
{% func amRequest(alerts []Alert, generatorURL func(Alert) string, lblss [][]prompbmarshal.Label) %}
[
{% for i, alert := range alerts %}
{% code lbls := alert.applyRelabelingIfNeeded(relabelCfg) %}
{% if len(lbls) == 0 %} {% continue %} {% endif %}
{% code lbls := lblss[i] %}
{
"startsAt":{%q= alert.Start.Format(time.RFC3339Nano) %},
"generatorURL": {%q= generatorURL(alert) %},

View File

@@ -8,7 +8,7 @@ package notifier
import (
"time"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promrelabel"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/prompbmarshal"
)
//line app/vmalert/notifier/alertmanager_request.qtpl:8
@@ -25,122 +25,116 @@ var (
)
//line app/vmalert/notifier/alertmanager_request.qtpl:8
func streamamRequest(qw422016 *qt422016.Writer, alerts []Alert, generatorURL func(Alert) string, relabelCfg *promrelabel.ParsedConfigs) {
func streamamRequest(qw422016 *qt422016.Writer, alerts []Alert, generatorURL func(Alert) string, lblss [][]prompbmarshal.Label) {
//line app/vmalert/notifier/alertmanager_request.qtpl:8
qw422016.N().S(`[`)
//line app/vmalert/notifier/alertmanager_request.qtpl:10
for i, alert := range alerts {
//line app/vmalert/notifier/alertmanager_request.qtpl:11
lbls := alert.applyRelabelingIfNeeded(relabelCfg)
lbls := lblss[i]
//line app/vmalert/notifier/alertmanager_request.qtpl:12
if len(lbls) == 0 {
//line app/vmalert/notifier/alertmanager_request.qtpl:12
continue
//line app/vmalert/notifier/alertmanager_request.qtpl:12
}
//line app/vmalert/notifier/alertmanager_request.qtpl:12
//line app/vmalert/notifier/alertmanager_request.qtpl:11
qw422016.N().S(`{"startsAt":`)
//line app/vmalert/notifier/alertmanager_request.qtpl:14
//line app/vmalert/notifier/alertmanager_request.qtpl:13
qw422016.N().Q(alert.Start.Format(time.RFC3339Nano))
//line app/vmalert/notifier/alertmanager_request.qtpl:14
//line app/vmalert/notifier/alertmanager_request.qtpl:13
qw422016.N().S(`,"generatorURL":`)
//line app/vmalert/notifier/alertmanager_request.qtpl:15
//line app/vmalert/notifier/alertmanager_request.qtpl:14
qw422016.N().Q(generatorURL(alert))
//line app/vmalert/notifier/alertmanager_request.qtpl:15
//line app/vmalert/notifier/alertmanager_request.qtpl:14
qw422016.N().S(`,`)
//line app/vmalert/notifier/alertmanager_request.qtpl:16
//line app/vmalert/notifier/alertmanager_request.qtpl:15
if !alert.End.IsZero() {
//line app/vmalert/notifier/alertmanager_request.qtpl:16
//line app/vmalert/notifier/alertmanager_request.qtpl:15
qw422016.N().S(`"endsAt":`)
//line app/vmalert/notifier/alertmanager_request.qtpl:17
//line app/vmalert/notifier/alertmanager_request.qtpl:16
qw422016.N().Q(alert.End.Format(time.RFC3339Nano))
//line app/vmalert/notifier/alertmanager_request.qtpl:17
//line app/vmalert/notifier/alertmanager_request.qtpl:16
qw422016.N().S(`,`)
//line app/vmalert/notifier/alertmanager_request.qtpl:18
//line app/vmalert/notifier/alertmanager_request.qtpl:17
}
//line app/vmalert/notifier/alertmanager_request.qtpl:18
//line app/vmalert/notifier/alertmanager_request.qtpl:17
qw422016.N().S(`"labels": {`)
//line app/vmalert/notifier/alertmanager_request.qtpl:20
//line app/vmalert/notifier/alertmanager_request.qtpl:19
ll := len(lbls)
//line app/vmalert/notifier/alertmanager_request.qtpl:21
//line app/vmalert/notifier/alertmanager_request.qtpl:20
for idx, l := range lbls {
//line app/vmalert/notifier/alertmanager_request.qtpl:22
//line app/vmalert/notifier/alertmanager_request.qtpl:21
qw422016.N().Q(l.Name)
//line app/vmalert/notifier/alertmanager_request.qtpl:22
//line app/vmalert/notifier/alertmanager_request.qtpl:21
qw422016.N().S(`:`)
//line app/vmalert/notifier/alertmanager_request.qtpl:22
//line app/vmalert/notifier/alertmanager_request.qtpl:21
qw422016.N().Q(l.Value)
//line app/vmalert/notifier/alertmanager_request.qtpl:22
//line app/vmalert/notifier/alertmanager_request.qtpl:21
if idx != ll-1 {
//line app/vmalert/notifier/alertmanager_request.qtpl:22
//line app/vmalert/notifier/alertmanager_request.qtpl:21
qw422016.N().S(`,`)
//line app/vmalert/notifier/alertmanager_request.qtpl:22
//line app/vmalert/notifier/alertmanager_request.qtpl:21
}
//line app/vmalert/notifier/alertmanager_request.qtpl:23
//line app/vmalert/notifier/alertmanager_request.qtpl:22
}
//line app/vmalert/notifier/alertmanager_request.qtpl:23
//line app/vmalert/notifier/alertmanager_request.qtpl:22
qw422016.N().S(`},"annotations": {`)
//line app/vmalert/notifier/alertmanager_request.qtpl:26
//line app/vmalert/notifier/alertmanager_request.qtpl:25
c := len(alert.Annotations)
//line app/vmalert/notifier/alertmanager_request.qtpl:27
//line app/vmalert/notifier/alertmanager_request.qtpl:26
for k, v := range alert.Annotations {
//line app/vmalert/notifier/alertmanager_request.qtpl:28
//line app/vmalert/notifier/alertmanager_request.qtpl:27
c = c - 1
//line app/vmalert/notifier/alertmanager_request.qtpl:29
//line app/vmalert/notifier/alertmanager_request.qtpl:28
qw422016.N().Q(k)
//line app/vmalert/notifier/alertmanager_request.qtpl:29
//line app/vmalert/notifier/alertmanager_request.qtpl:28
qw422016.N().S(`:`)
//line app/vmalert/notifier/alertmanager_request.qtpl:29
//line app/vmalert/notifier/alertmanager_request.qtpl:28
qw422016.N().Q(v)
//line app/vmalert/notifier/alertmanager_request.qtpl:29
//line app/vmalert/notifier/alertmanager_request.qtpl:28
if c > 0 {
//line app/vmalert/notifier/alertmanager_request.qtpl:29
//line app/vmalert/notifier/alertmanager_request.qtpl:28
qw422016.N().S(`,`)
//line app/vmalert/notifier/alertmanager_request.qtpl:29
//line app/vmalert/notifier/alertmanager_request.qtpl:28
}
//line app/vmalert/notifier/alertmanager_request.qtpl:30
//line app/vmalert/notifier/alertmanager_request.qtpl:29
}
//line app/vmalert/notifier/alertmanager_request.qtpl:30
//line app/vmalert/notifier/alertmanager_request.qtpl:29
qw422016.N().S(`}}`)
//line app/vmalert/notifier/alertmanager_request.qtpl:33
//line app/vmalert/notifier/alertmanager_request.qtpl:32
if i != len(alerts)-1 {
//line app/vmalert/notifier/alertmanager_request.qtpl:33
//line app/vmalert/notifier/alertmanager_request.qtpl:32
qw422016.N().S(`,`)
//line app/vmalert/notifier/alertmanager_request.qtpl:33
//line app/vmalert/notifier/alertmanager_request.qtpl:32
}
//line app/vmalert/notifier/alertmanager_request.qtpl:34
//line app/vmalert/notifier/alertmanager_request.qtpl:33
}
//line app/vmalert/notifier/alertmanager_request.qtpl:34
//line app/vmalert/notifier/alertmanager_request.qtpl:33
qw422016.N().S(`]`)
//line app/vmalert/notifier/alertmanager_request.qtpl:36
//line app/vmalert/notifier/alertmanager_request.qtpl:35
}
//line app/vmalert/notifier/alertmanager_request.qtpl:36
func writeamRequest(qq422016 qtio422016.Writer, alerts []Alert, generatorURL func(Alert) string, relabelCfg *promrelabel.ParsedConfigs) {
//line app/vmalert/notifier/alertmanager_request.qtpl:36
//line app/vmalert/notifier/alertmanager_request.qtpl:35
func writeamRequest(qq422016 qtio422016.Writer, alerts []Alert, generatorURL func(Alert) string, lblss [][]prompbmarshal.Label) {
//line app/vmalert/notifier/alertmanager_request.qtpl:35
qw422016 := qt422016.AcquireWriter(qq422016)
//line app/vmalert/notifier/alertmanager_request.qtpl:36
streamamRequest(qw422016, alerts, generatorURL, relabelCfg)
//line app/vmalert/notifier/alertmanager_request.qtpl:36
//line app/vmalert/notifier/alertmanager_request.qtpl:35
streamamRequest(qw422016, alerts, generatorURL, lblss)
//line app/vmalert/notifier/alertmanager_request.qtpl:35
qt422016.ReleaseWriter(qw422016)
//line app/vmalert/notifier/alertmanager_request.qtpl:36
//line app/vmalert/notifier/alertmanager_request.qtpl:35
}
//line app/vmalert/notifier/alertmanager_request.qtpl:36
func amRequest(alerts []Alert, generatorURL func(Alert) string, relabelCfg *promrelabel.ParsedConfigs) string {
//line app/vmalert/notifier/alertmanager_request.qtpl:36
//line app/vmalert/notifier/alertmanager_request.qtpl:35
func amRequest(alerts []Alert, generatorURL func(Alert) string, lblss [][]prompbmarshal.Label) string {
//line app/vmalert/notifier/alertmanager_request.qtpl:35
qb422016 := qt422016.AcquireByteBuffer()
//line app/vmalert/notifier/alertmanager_request.qtpl:36
writeamRequest(qb422016, alerts, generatorURL, relabelCfg)
//line app/vmalert/notifier/alertmanager_request.qtpl:36
//line app/vmalert/notifier/alertmanager_request.qtpl:35
writeamRequest(qb422016, alerts, generatorURL, lblss)
//line app/vmalert/notifier/alertmanager_request.qtpl:35
qs422016 := string(qb422016.B)
//line app/vmalert/notifier/alertmanager_request.qtpl:36
//line app/vmalert/notifier/alertmanager_request.qtpl:35
qt422016.ReleaseByteBuffer(qb422016)
//line app/vmalert/notifier/alertmanager_request.qtpl:36
//line app/vmalert/notifier/alertmanager_request.qtpl:35
return qs422016
//line app/vmalert/notifier/alertmanager_request.qtpl:36
//line app/vmalert/notifier/alertmanager_request.qtpl:35
}

View File

@@ -105,6 +105,16 @@ func TestAlertManager_Send(t *testing.T) {
if r.Header.Get(headerKey) != "bar" {
t.Fatalf("expected header %q to be set to %q; got %q instead", headerKey, "bar", r.Header.Get(headerKey))
}
case 4:
var a []struct {
Labels map[string]string `json:"labels"`
}
if err := json.NewDecoder(r.Body).Decode(&a); err != nil {
t.Fatalf("can not unmarshal data into alert %s", err)
}
if len(a) != 1 {
t.Fatalf("expected 1 alert in array got %d", len(a))
}
}
})
srv := httptest.NewServer(mux)
@@ -168,7 +178,20 @@ func TestAlertManager_Send(t *testing.T) {
t.Fatalf("unexpected error %s", err)
}
if c != 3 {
t.Fatalf("expected 3 calls(count from zero) to server got %d", c)
if err := am.Send(context.Background(), []Alert{
{
Name: "alert1",
Labels: map[string]string{"rule": "test"},
},
{
Name: "alert2",
Labels: map[string]string{},
},
}, map[string]string{}); err != nil {
t.Fatalf("unexpected error %s", err)
}
if c != 4 {
t.Fatalf("expected 4 calls(count from zero) to server got %d", c)
}
}

View File

@@ -443,8 +443,8 @@ func (g *Group) Start(ctx context.Context, nts func() []notifier.Notifier, rw re
}
// UpdateWith inserts new group to updateCh
func (g *Group) UpdateWith(new *Group) {
g.updateCh <- new
func (g *Group) UpdateWith(newGroup *Group) {
g.updateCh <- newGroup
}
// DeepCopy returns a deep copy of group

View File

@@ -7,11 +7,13 @@ import (
"flag"
"fmt"
"math"
"net"
"net/http"
"net/url"
"os"
"regexp"
"sort"
"strconv"
"strings"
"sync"
"sync/atomic"
@@ -348,6 +350,7 @@ func (up *URLPrefix) discoverBackendAddrsIfNeeded() {
hostToAddrs := make(map[string][]string)
for _, bu := range up.busOriginal {
host := bu.Hostname()
port := bu.Port()
if hostToAddrs[host] != nil {
// ips for the given host have been already discovered
continue
@@ -364,7 +367,11 @@ func (up *URLPrefix) discoverBackendAddrsIfNeeded() {
} else {
resolvedAddrs = make([]string, len(addrs))
for i, addr := range addrs {
resolvedAddrs[i] = fmt.Sprintf("%s:%d", addr.Target, addr.Port)
hostPort := port
if hostPort == "" && addr.Port > 0 {
hostPort = strconv.FormatUint(uint64(addr.Port), 10)
}
resolvedAddrs[i] = net.JoinHostPort(addr.Target, hostPort)
}
}
} else {
@@ -375,7 +382,7 @@ func (up *URLPrefix) discoverBackendAddrsIfNeeded() {
} else {
resolvedAddrs = make([]string, len(addrs))
for i, addr := range addrs {
resolvedAddrs[i] = addr.String()
resolvedAddrs[i] = net.JoinHostPort(addr.String(), port)
}
}
}
@@ -389,17 +396,9 @@ func (up *URLPrefix) discoverBackendAddrsIfNeeded() {
var busNew []*backendURL
for _, bu := range up.busOriginal {
host := bu.Hostname()
port := bu.Port()
for _, addr := range hostToAddrs[host] {
buCopy := *bu
buCopy.Host = addr
if port != "" {
if n := strings.IndexByte(buCopy.Host, ':'); n >= 0 {
// Drop the discovered port and substitute it the port specified in bu.
buCopy.Host = buCopy.Host[:n]
}
buCopy.Host += ":" + port
}
busNew = append(busNew, &backendURL{
url: &buCopy,
})

View File

@@ -3,12 +3,14 @@ package main
import (
"bytes"
"fmt"
"net"
"net/url"
"testing"
"gopkg.in/yaml.v2"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/netutil"
)
func TestParseAuthConfigFailure(t *testing.T) {
@@ -799,6 +801,75 @@ func TestBrokenBackend(t *testing.T) {
}
}
func TestDiscoverBackendIPsWithIPV6(t *testing.T) {
f := func(actualUrl, expectedUrl string) {
t.Helper()
up := mustParseURL(actualUrl)
up.discoverBackendIPs = true
up.loadBalancingPolicy = "least_loaded"
up.discoverBackendAddrsIfNeeded()
pbus := up.bus.Load()
bus := *pbus
if len(bus) != 1 {
t.Fatalf("expected url list to be of size 1; got %d instead", len(bus))
}
got := bus[0].url.Host
if got != expectedUrl {
t.Fatalf(`expected url to be %q; got %q instead`, expectedUrl, bus[0].url.Host)
}
}
// Discover backendURL with SRV hostnames
customResolver := &fakeResolver{
Resolver: &net.Resolver{},
// SRV records must return hostname
// not an IP address
lookupSRVResults: map[string][]*net.SRV{
"_vmselect._tcp.selectwithport.": {
{
Target: "vmselect.local",
Port: 8481,
},
},
"_vmselect._tcp.selectwoport.": {
{
Target: "vmselect.local",
},
},
},
lookupIPAddrResults: map[string][]net.IPAddr{
"vminsert.local": {
{
IP: net.ParseIP("10.0.10.13"),
},
},
"ipv6.vminsert.local": {
{
IP: net.ParseIP("2607:f8b0:400a:80b::200e"),
},
},
},
}
origResolver := netutil.Resolver
netutil.Resolver = customResolver
defer func() {
netutil.Resolver = origResolver
}()
f("http://srv+_vmselect._tcp.selectwithport.:8080", "vmselect.local:8080")
f("http://srv+_vmselect._tcp.selectwithport.:", "vmselect.local:8481")
f("http://srv+_vmselect._tcp.selectwoport.:8080", "vmselect.local:8080")
f("http://srv+_vmselect._tcp.selectwoport.", "vmselect.local:")
f("http://vminsert.local:8080", "10.0.10.13:8080")
f("http://vminsert.local", "10.0.10.13:")
f("http://ipv6.vminsert.local:8080", "[2607:f8b0:400a:80b::200e]:8080")
f("http://ipv6.vminsert.local", "[2607:f8b0:400a:80b::200e]:")
}
func getRegexs(paths []string) []*Regex {
var sps []*Regex
for _, path := range paths {

View File

@@ -216,8 +216,7 @@ func processRequest(w http.ResponseWriter, r *http.Request, ui *UserInfo) {
isDefault = true
}
rtb := getReadTrackingBody(r.Body, maxRequestBodySizeToRetry.IntN())
defer putReadTrackingBody(rtb)
rtb := newReadTrackingBody(r.Body, maxRequestBodySizeToRetry.IntN())
r.Body = rtb
maxAttempts := up.getBackendsCount()
@@ -553,22 +552,11 @@ type readTrackingBody struct {
bufComplete bool
}
func (rtb *readTrackingBody) reset() {
rtb.maxBodySize = 0
rtb.r = nil
rtb.buf = rtb.buf[:0]
rtb.readBuf = nil
rtb.cannotRetry = false
rtb.bufComplete = false
}
func getReadTrackingBody(r io.ReadCloser, maxBodySize int) *readTrackingBody {
v := readTrackingBodyPool.Get()
if v == nil {
v = &readTrackingBody{}
}
rtb := v.(*readTrackingBody)
func newReadTrackingBody(r io.ReadCloser, maxBodySize int) *readTrackingBody {
// do not use sync.Pool there
// since http.RoundTrip may still use request body after return
// See this issue for details https://github.com/VictoriaMetrics/VictoriaMetrics/issues/8051
rtb := &readTrackingBody{}
if maxBodySize < 0 {
maxBodySize = 0
}
@@ -591,13 +579,6 @@ func (r *zeroReader) Close() error {
return nil
}
func putReadTrackingBody(rtb *readTrackingBody) {
rtb.reset()
readTrackingBodyPool.Put(rtb)
}
var readTrackingBodyPool sync.Pool
// Read implements io.Reader interface.
func (rtb *readTrackingBody) Read(p []byte) (int, error) {
if len(rtb.readBuf) > 0 {

View File

@@ -181,7 +181,7 @@ unauthorized_user:
}
responseExpected = `
statusCode=401
The provided authKey doesn't match -reloadAuthKey`
Expected to receive non-empty authKey when -reloadAuthKey is set`
f(cfgStr, requestURL, backendHandler, responseExpected)
if err := reloadAuthKey.Set(origAuthKey); err != nil {
t.Fatalf("unexpected error: %s", err)
@@ -531,8 +531,7 @@ func TestReadTrackingBody_RetrySuccess(t *testing.T) {
f := func(s string, maxBodySize int) {
t.Helper()
rtb := getReadTrackingBody(io.NopCloser(bytes.NewBufferString(s)), maxBodySize)
defer putReadTrackingBody(rtb)
rtb := newReadTrackingBody(io.NopCloser(bytes.NewBufferString(s)), maxBodySize)
if !rtb.canRetry() {
t.Fatalf("canRetry() must return true before reading anything")
@@ -567,8 +566,7 @@ func TestReadTrackingBody_RetrySuccessPartialRead(t *testing.T) {
t.Helper()
// Check the case with partial read
rtb := getReadTrackingBody(io.NopCloser(bytes.NewBufferString(s)), maxBodySize)
defer putReadTrackingBody(rtb)
rtb := newReadTrackingBody(io.NopCloser(bytes.NewBufferString(s)), maxBodySize)
for i := 0; i < len(s); i++ {
buf := make([]byte, i)
@@ -617,8 +615,7 @@ func TestReadTrackingBody_RetryFailureTooBigBody(t *testing.T) {
f := func(s string, maxBodySize int) {
t.Helper()
rtb := getReadTrackingBody(io.NopCloser(bytes.NewBufferString(s)), maxBodySize)
defer putReadTrackingBody(rtb)
rtb := newReadTrackingBody(io.NopCloser(bytes.NewBufferString(s)), maxBodySize)
if !rtb.canRetry() {
t.Fatalf("canRetry() must return true before reading anything")
@@ -667,8 +664,7 @@ func TestReadTrackingBody_RetryFailureZeroOrNegativeMaxBodySize(t *testing.T) {
f := func(s string, maxBodySize int) {
t.Helper()
rtb := getReadTrackingBody(io.NopCloser(bytes.NewBufferString(s)), maxBodySize)
defer putReadTrackingBody(rtb)
rtb := newReadTrackingBody(io.NopCloser(bytes.NewBufferString(s)), maxBodySize)
if !rtb.canRetry() {
t.Fatalf("canRetry() must return true before reading anything")

View File

@@ -40,15 +40,15 @@ type filter struct {
labelValue string
}
func (f filter) inRange(min, max int64) bool {
func (f filter) inRange(minV, maxV int64) bool {
fmin, fmax := f.min, f.max
if min == 0 {
fmin = min
if minV == 0 {
fmin = minV
}
if fmax == 0 {
fmax = max
fmax = maxV
}
return min <= fmax && fmin <= max
return minV <= fmax && fmin <= maxV
}
// NewClient creates and validates new Client
@@ -59,13 +59,13 @@ func NewClient(cfg Config) (*Client, error) {
return nil, fmt.Errorf("failed to open snapshot %q: %s", cfg.Snapshot, err)
}
c := &Client{DBReadOnly: db}
min, max, err := parseTime(cfg.Filter.TimeMin, cfg.Filter.TimeMax)
minTime, maxTime, err := parseTime(cfg.Filter.TimeMin, cfg.Filter.TimeMax)
if err != nil {
return nil, fmt.Errorf("failed to parse time in filter: %s", err)
}
c.filter = filter{
min: min,
max: max,
min: minTime,
max: maxTime,
label: cfg.Filter.Label,
labelValue: cfg.Filter.LabelValue,
}

View File

@@ -186,16 +186,8 @@ func (ctx *InsertCtx) GetStorageNodeIdx(at *auth.Token, labels []prompbmarshal.L
h := xxhash.Sum64(buf)
ctx.labelsBuf = buf
// Exclude long-broken storage nodes.
var excludeIdxs []int
if !*dropSamplesOnOverload {
for i := range ctx.snb.sns {
if ctx.snb.sns[i].isExcluded() {
excludeIdxs = append(excludeIdxs, i)
}
}
}
idx := ctx.snb.nodesHash.getNodeIdx(h, excludeIdxs)
// Do not exclude unavailable storage nodes in order to properly account for rerouted rows in storageNode.push().
idx := ctx.snb.nodesHash.getNodeIdx(h, nil)
return idx
}

View File

@@ -31,7 +31,7 @@ var (
replicationFactor = flag.Int("replicationFactor", 1, "Replication factor for the ingested data, i.e. how many copies to make among distinct -storageNode instances. "+
"Note that vmselect must run with -dedup.minScrapeInterval=1ms for data de-duplication when replicationFactor is greater than 1. "+
"Higher values for -dedup.minScrapeInterval at vmselect is OK")
_ = flag.Bool("disableRerouting", true, "This option is deprecated and has no effect. See also -disableReroutingOnUnavailable and -dropSamplesOnOverload.")
disableRerouting = flag.Bool("disableRerouting", true, "Whether to disable re-routing when some of vmstorage nodes accept incoming data at slower speed compared to other storage nodes. Disabled re-routing limits the ingestion rate by the slowest vmstorage node. On the other side, disabled re-routing minimizes the number of active time series in the cluster during rolling restarts and during spikes in series churn rate. See also -disableReroutingOnUnavailable and -dropSamplesOnOverload")
dropSamplesOnOverload = flag.Bool("dropSamplesOnOverload", false, "Whether to drop incoming samples if the destination vmstorage node is overloaded and/or unavailable. This prioritizes cluster availability over consistency, e.g. the cluster continues accepting all the ingested samples, but some of them may be dropped if vmstorage nodes are temporarily unavailable and/or overloaded. The drop of samples happens before the replication, so it's not recommended to use this flag with -replicationFactor enabled.")
vmstorageDialTimeout = flag.Duration("vmstorageDialTimeout", 3*time.Second, "Timeout for establishing RPC connections from vminsert to vmstorage. "+
"See also -vmstorageUserTimeout")
@@ -44,7 +44,6 @@ var (
"On the other side, disabled re-routing minimizes the number of active time series in the cluster "+
"during rolling restarts and during spikes in series churn rate. "+
"See also -disableRerouting")
rerouteDelay = flag.Duration("rerouteDelay", 20*time.Second, "The maximum time the system waits for vmstorage nodes to become available before re-routing the data to other vmstorage nodes, minimum value is 1s and rounding to seconds")
)
var errStorageReadOnly = errors.New("storage node is read only")
@@ -53,16 +52,6 @@ func (sn *storageNode) isReady() bool {
return !sn.isBroken.Load() && !sn.isReadOnly.Load()
}
func (sn *storageNode) isExcluded() bool {
return (sn.isBroken.Load() && fasttime.UnixTimestamp()-sn.brokenAt.Load() > uint64(*rerouteDelay/time.Second)) || sn.isReadOnly.Load()
}
func (sn *storageNode) setBroken(isBroken bool) {
if !sn.isBroken.Swap(isBroken) && isBroken {
sn.brokenAt.Store(fasttime.UnixTimestamp())
}
}
// push pushes buf to sn internal bufs.
//
// This function doesn't block on fast path.
@@ -119,35 +108,15 @@ again:
sn.brCond.Wait()
goto again
}
sn.brLock.Unlock()
// Reroute buf to healthy vmstorage nodes if the current node is broken for too long.
timeoutAt := uint64(*rerouteDelay/time.Second) + sn.brokenAt.Load()
if timeoutAt <= fasttime.UnixTimestamp() || sn.isReadOnly.Load() {
sn.brLock.Unlock()
rowsProcessed, err := rerouteRowsToReadyStorageNodes(snb, sn, buf)
rows -= rowsProcessed
if err != nil {
return fmt.Errorf("%d rows dropped because the current vsmtorage is unavailable and %w", rows, err)
}
return nil
// The vmstorage node isn't ready for data processing. Re-route buf to healthy vmstorage nodes even if disableRerouting is set.
rowsProcessed, err := rerouteRowsToReadyStorageNodes(snb, sn, buf)
rows -= rowsProcessed
if err != nil {
return fmt.Errorf("%d rows dropped because the current vsmtorage is unavailable and %w", rows, err)
}
// Wait for the vmstorage node to change its state to ready, or timeout.
// sn.brCond.Wait() will be woken up at ~200ms intervals by the health checker.
waitLoop:
for sn.isBroken.Load() && timeoutAt > fasttime.UnixTimestamp() {
sn.brCond.Wait()
select {
case <-sn.stopCh:
break waitLoop
default:
}
}
goto again
return nil
}
if len(sn.br.buf)+len(buf) <= maxBufSizePerStorageNode {
@@ -157,20 +126,17 @@ again:
sn.brLock.Unlock()
return nil
}
// Slow path: the buf contents doesn't fit sn.buf, so try re-routing it to other vmstorage nodes.
if len(sns) == 1 {
if *disableRerouting || len(sns) == 1 {
sn.brCond.Wait()
goto again
}
sn.brLock.Unlock()
rowsProcessed, err := rerouteRowsToFreeStorageNodes(snb, sn, buf)
rows -= rowsProcessed
if err != nil {
return fmt.Errorf("%d rows dropped because the current vmstorage buf is full and %w", rows, err)
}
return nil
}
@@ -228,8 +194,7 @@ func (sn *storageNode) run(snb *storageNodesBucket, snIdx int) {
continue
}
// Send br to replicas storage nodes starting from snIdx.
usedStorageNodes := make(map[*storageNode]struct{}, replicas)
for !trySendBufToStorages(snb, &br, snIdx, replicas, usedStorageNodes) {
for !sendBufToReplicasNonblocking(snb, &br, snIdx, replicas) {
d := timeutil.AddJitterToDuration(time.Millisecond * 200)
t := timerpool.Get(d)
select {
@@ -246,25 +211,9 @@ func (sn *storageNode) run(snb *storageNodesBucket, snIdx int) {
}
}
func trySendBufToStorages(snb *storageNodesBucket, br *bufRows, snIdx, replicas int, usedStorageNodes map[*storageNode]struct{}) bool {
func sendBufToReplicasNonblocking(snb *storageNodesBucket, br *bufRows, snIdx, replicas int) bool {
usedStorageNodes := make(map[*storageNode]struct{}, replicas)
sns := snb.sns
// If the current storage node is broken, wait for it to be ready or timeout
if sns[snIdx].isBroken.Load() {
timeoutAt := uint64(*rerouteDelay/time.Second) + sns[snIdx].brokenAt.Load()
if timeoutAt > fasttime.UnixTimestamp() {
return false
}
}
if *dropSamplesOnOverload {
return tryReplicateBufToStorages(sns, br, snIdx, replicas, usedStorageNodes)
}
return tryReplicateBufToStoragesUntilExhausted(sns, br, snIdx, replicas, usedStorageNodes)
}
func tryReplicateBufToStoragesUntilExhausted(sns []*storageNode, br *bufRows, snIdx, replicas int, usedStorageNodes map[*storageNode]struct{}) bool {
for i := 0; i < replicas; i++ {
idx := snIdx + i
attempts := 0
@@ -306,40 +255,6 @@ func tryReplicateBufToStoragesUntilExhausted(sns []*storageNode, br *bufRows, sn
return true
}
func tryReplicateBufToStorages(sns []*storageNode, br *bufRows, snIdx, replicas int, usedStorageNodes map[*storageNode]struct{}) bool {
previousSuccessLen := len(usedStorageNodes)
for i := 0; i < replicas; i++ {
idx := snIdx + i
for {
if idx >= len(sns) {
idx %= len(sns)
}
sn := sns[idx]
idx++
if _, ok := usedStorageNodes[sn]; ok {
continue
}
if !sn.sendBufRowsNonblocking(br) {
continue
}
usedStorageNodes[sn] = struct{}{}
break
}
}
if _, ok := usedStorageNodes[sns[snIdx]]; !ok {
cannotReplicateLogger.Warnf("cannot push %d bytes with %d rows to degraded node %s, %d/%d nodes are replicated", len(br.buf), br.rows, sns[snIdx].dialer.Addr(), len(usedStorageNodes), replicas)
return false
} else if previousSuccessLen != len(usedStorageNodes) && len(usedStorageNodes) < replicas {
rowsIncompletelyReplicatedTotal.Add(br.rows)
incompleteReplicationLogger.Warnf("dropping %d rows (%d bytes) as cannot make a copy #%d out of %d copies according to -replicationFactor=%d, since a part of storage nodes is temporarily unavailable", br.rows, len(br.buf), len(usedStorageNodes), replicas, *replicationFactor)
return true
}
return true
}
var (
cannotReplicateLogger = logger.WithThrottler("cannotReplicateDataBecauseNoStorageNodes", 5*time.Second)
incompleteReplicationLogger = logger.WithThrottler("incompleteReplication", 5*time.Second)
@@ -355,7 +270,7 @@ func (sn *storageNode) checkHealth() {
}
bc, err := sn.dial()
if err != nil {
sn.setBroken(true)
sn.isBroken.Store(true)
sn.brCond.Broadcast()
if sn.lastDialErr == nil {
// Log the error only once.
@@ -367,7 +282,7 @@ func (sn *storageNode) checkHealth() {
logger.Infof("successfully dialed -storageNode=%q", sn.dialer.Addr())
sn.lastDialErr = nil
sn.bc = bc
sn.setBroken(false)
sn.isBroken.Store(false)
sn.brCond.Broadcast()
}
@@ -409,7 +324,7 @@ func (sn *storageNode) sendBufRowsNonblocking(br *bufRows) bool {
cannotCloseStorageNodeConnLogger.Warnf("cannot close connection to storageNode %q: %s", sn.dialer.Addr(), err)
}
sn.bc = nil
sn.setBroken(true)
sn.isBroken.Store(true)
sn.brCond.Broadcast()
sn.connectionErrors.Inc()
return false
@@ -498,7 +413,6 @@ type storageNode struct {
// isBroken is set to true if the given vmstorage node is temporarily unhealthy.
// In this case the data is re-routed to the remaining healthy vmstorage nodes.
isBroken atomic.Bool
brokenAt atomic.Uint64
// isReadOnly is set to true if the given vmstorage node is read only
// In this case the data is re-routed to the remaining healthy vmstorage nodes.
@@ -668,11 +582,6 @@ func initStorageNodes(unsortedAddrs []string, hashSeed uint64) *storageNodesBuck
maxBufSizePerStorageNode = consts.MaxInsertPacketSizeForVMInsert
}
*rerouteDelay = (*rerouteDelay).Round(time.Second)
if *rerouteDelay < time.Second {
*rerouteDelay = time.Second
}
metrics.RegisterSet(ms)
var wg sync.WaitGroup
snb := &storageNodesBucket{
@@ -740,6 +649,17 @@ func rerouteRowsToReadyStorageNodes(snb *storageNodesBucket, snSource *storageNo
// re-generate idxsExclude list, since sn must be put there.
idxsExclude = getNotReadyStorageNodeIdxsBlocking(snb, idxsExclude[:0])
}
if *disableRerouting {
if !sn.sendBufMayBlock(rowBuf) {
return rowsProcessed, fmt.Errorf("graceful shutdown started")
}
rowsProcessed++
if sn != snSource {
snSource.rowsReroutedFromHere.Inc()
sn.rowsReroutedToHere.Inc()
}
continue
}
again:
if sn.trySendBuf(rowBuf, 1) {
rowsProcessed++
@@ -774,6 +694,9 @@ func rerouteRowsToReadyStorageNodes(snb *storageNodesBucket, snSource *storageNo
// It is expected than *disableRerouting isn't set when calling this function.
// It is expected that len(snb.sns) >= 2
func rerouteRowsToFreeStorageNodes(snb *storageNodesBucket, snSource *storageNode, src []byte) (int, error) {
if *disableRerouting {
logger.Panicf("BUG: disableRerouting must be disabled when calling rerouteRowsToFreeStorageNodes")
}
sns := snb.sns
if len(sns) < 2 {
logger.Panicf("BUG: the number of storage nodes is too small for calling rerouteRowsToFreeStorageNodes: %d", len(sns))
@@ -879,6 +802,23 @@ func (sn *storageNode) trySendBuf(buf []byte, rows int) bool {
return sent
}
func (sn *storageNode) sendBufMayBlock(buf []byte) bool {
sn.brLock.Lock()
for len(sn.br.buf)+len(buf) > maxBufSizePerStorageNode {
select {
case <-sn.stopCh:
sn.brLock.Unlock()
return false
default:
}
sn.brCond.Wait()
}
sn.br.buf = append(sn.br.buf, buf...)
sn.br.rows++
sn.brLock.Unlock()
return true
}
func (sn *storageNode) readOnlyChecker() {
d := timeutil.AddJitterToDuration(time.Second * 30)
ticker := time.NewTicker(d)
@@ -924,7 +864,7 @@ func (sn *storageNode) checkReadOnlyMode() {
cannotCloseStorageNodeConnLogger.Warnf("cannot close connection to storageNode %q: %s", sn.dialer.Addr(), err)
}
sn.bc = nil
sn.setBroken(true)
sn.isBroken.Store(true)
sn.brCond.Broadcast()
sn.connectionErrors.Inc()
}

View File

@@ -98,13 +98,13 @@ func aggrMin(values []float64) float64 {
if pos < 0 {
return nan
}
min := values[pos]
minV := values[pos]
for _, v := range values[pos+1:] {
if !math.IsNaN(v) && v < min {
min = v
if !math.IsNaN(v) && v < minV {
minV = v
}
}
return min
return minV
}
func aggrMax(values []float64) float64 {
@@ -112,13 +112,13 @@ func aggrMax(values []float64) float64 {
if pos < 0 {
return nan
}
max := values[pos]
maxV := values[pos]
for _, v := range values[pos+1:] {
if !math.IsNaN(v) && v > max {
max = v
if !math.IsNaN(v) && v > maxV {
maxV = v
}
}
return max
return maxV
}
func aggrDiff(values []float64) float64 {
@@ -177,12 +177,12 @@ func aggrCount(values []float64) float64 {
}
func aggrRange(values []float64) float64 {
min := aggrMin(values)
if math.IsNaN(min) {
minV := aggrMin(values)
if math.IsNaN(minV) {
return nan
}
max := aggrMax(values)
return max - min
maxV := aggrMax(values)
return maxV - minV
}
func aggrMultiply(values []float64) float64 {

View File

@@ -2594,17 +2594,17 @@ func transformMinMax(ec *evalConfig, fe *graphiteql.FuncExpr) (nextSeriesFunc, e
}
f := nextSeriesConcurrentWrapper(nextSeries, func(s *series) (*series, error) {
values := s.Values
min := aggrMin(values)
if math.IsNaN(min) {
min = 0
minV := aggrMin(values)
if math.IsNaN(minV) {
minV = 0
}
max := aggrMax(values)
if math.IsNaN(max) {
max = 0
maxV := aggrMax(values)
if math.IsNaN(maxV) {
maxV = 0
}
vRange := max - min
vRange := maxV - minV
for i, v := range values {
v = (v - min) / vRange
v = (v - minV) / vRange
if math.IsInf(v, 0) {
v = 0
}
@@ -2975,9 +2975,9 @@ func transformRemoveAbovePercentile(ec *evalConfig, fe *graphiteql.FuncExpr) (ne
}
f := nextSeriesConcurrentWrapper(nextSeries, func(s *series) (*series, error) {
values := s.Values
max := aggrFunc(values)
maxV := aggrFunc(values)
for i, v := range values {
if v > max {
if v > maxV {
values[i] = nan
}
}
@@ -3035,9 +3035,9 @@ func transformRemoveBelowPercentile(ec *evalConfig, fe *graphiteql.FuncExpr) (ne
}
f := nextSeriesConcurrentWrapper(nextSeries, func(s *series) (*series, error) {
values := s.Values
min := aggrFunc(values)
minV := aggrFunc(values)
for i, v := range values {
if v < min {
if v < minV {
values[i] = nan
}
}
@@ -4514,11 +4514,11 @@ func transformOffsetToZero(ec *evalConfig, fe *graphiteql.FuncExpr) (nextSeriesF
}
f := nextSeriesConcurrentWrapper(nextSeries, func(s *series) (*series, error) {
values := s.Values
min := aggrMin(values)
minV := aggrMin(values)
for i, v := range values {
values[i] = v - min
values[i] = v - minV
}
s.Tags["offsetToZero"] = fmt.Sprintf("%g", min)
s.Tags["offsetToZero"] = fmt.Sprintf("%g", minV)
s.Name = fmt.Sprintf("offsetToZero(%s)", s.Name)
s.expr = fe
s.pathExpression = s.Name
@@ -4567,29 +4567,29 @@ func transformPerSecond(ec *evalConfig, fe *graphiteql.FuncExpr) (nextSeriesFunc
return f, nil
}
func nonNegativeDelta(curr, prev, max, min float64) (float64, float64) {
if !math.IsNaN(max) && curr > max {
func nonNegativeDelta(currV, prevV, maxV, minV float64) (float64, float64) {
if !math.IsNaN(maxV) && currV > maxV {
return nan, nan
}
if !math.IsNaN(min) && curr < min {
if !math.IsNaN(minV) && currV < minV {
return nan, nan
}
if math.IsNaN(curr) || math.IsNaN(prev) {
return nan, curr
if math.IsNaN(currV) || math.IsNaN(prevV) {
return nan, currV
}
if curr >= prev {
return curr - prev, curr
if currV >= prevV {
return currV - prevV, currV
}
if !math.IsNaN(max) {
if math.IsNaN(min) {
min = float64(0)
if !math.IsNaN(maxV) {
if math.IsNaN(minV) {
minV = float64(0)
}
return max + 1 + curr - prev - min, curr
return maxV + 1 + currV - prevV - minV, currV
}
if !math.IsNaN(min) {
return curr - min, curr
if !math.IsNaN(minV) {
return currV - minV, currV
}
return nan, curr
return nan, currV
}
// See https://graphite.readthedocs.io/en/stable/functions.html#graphite.render.functions.threshold
@@ -4941,8 +4941,8 @@ func transformSortByMinima(ec *evalConfig, fe *graphiteql.FuncExpr) (nextSeriesF
}
// Filter out series with all the values smaller than 0
f := nextSeriesConcurrentWrapper(nextSeries, func(s *series) (*series, error) {
max := aggrMax(s.Values)
if math.IsNaN(max) || max <= 0 {
maxV := aggrMax(s.Values)
if math.IsNaN(maxV) || maxV <= 0 {
return nil, nil
}
return s, nil

View File

@@ -52,8 +52,8 @@ var (
"limit is reached; see also -search.maxQueryDuration")
minScrapeInterval = flag.Duration("dedup.minScrapeInterval", 0, "Leave only the last sample in every time series per each discrete interval "+
"equal to -dedup.minScrapeInterval > 0. See https://docs.victoriametrics.com/#deduplication for details")
deleteAuthKey = flagutil.NewPassword("deleteAuthKey", "authKey for metrics' deletion via /prometheus/api/v1/admin/tsdb/delete_series and /graphite/tags/delSeries")
resetCacheAuthKey = flagutil.NewPassword("search.resetCacheAuthKey", "Optional authKey for resetting rollup cache via /internal/resetRollupResultCache call")
deleteAuthKey = flagutil.NewPassword("deleteAuthKey", "authKey for metrics' deletion via /prometheus/api/v1/admin/tsdb/delete_series and /graphite/tags/delSeries. It could be passed via authKey query arg. It overrides -httpAuth.*")
resetCacheAuthKey = flagutil.NewPassword("search.resetCacheAuthKey", "Optional authKey for resetting rollup cache via /internal/resetRollupResultCache call. It could be passed via authKey query arg. It overrides -httpAuth.*")
logSlowQueryDuration = flag.Duration("search.logSlowQueryDuration", 5*time.Second, "Log queries with execution time exceeding this value. Zero disables slow query logging. "+
"See also -search.logQueryMemoryUsage")
vmalertProxyURL = flag.String("vmalert.proxyURL", "", "Optional URL for proxying requests to vmalert. For example, if -vmalert.proxyURL=http://vmalert:8880 , then alerting API requests such as /api/v1/rules from Grafana will be proxied to http://vmalert:8880/api/v1/rules")
@@ -63,6 +63,10 @@ var (
clusternativeListenAddr = flag.String("clusternativeListenAddr", "", "TCP address to listen for requests from other vmselect nodes in multi-level cluster setup. "+
"See https://docs.victoriametrics.com/cluster-victoriametrics/#multi-level-cluster-setup . Usually :8401 should be set to match default vmstorage port for vmselect. Disabled work if empty")
maxMemoryPerQuery = flagutil.NewBytes("search.maxMemoryPerQuery", 0, "The maximum amounts of memory a single query may consume. "+
"Queries requiring more memory are rejected. The total memory limit for concurrently executed queries can be estimated "+
"as -search.maxMemoryPerQuery multiplied by -search.maxConcurrentRequests . "+
"See also -search.logQueryMemoryUsage")
)
var slowQueries = metrics.NewCounter(`vm_slow_queries_total`)
@@ -116,6 +120,10 @@ func main() {
netstorage.InitTmpBlocksDir("")
promql.InitRollupResultCache("")
}
promql.SetMaxMemoryPerQuery(maxMemoryPerQuery.N)
netstorage.SetMaxMemoryUsagePerQuery(maxMemoryPerQuery.N)
concurrencyLimitCh = make(chan struct{}, *maxConcurrentRequests)
initVMAlertProxy()
var vmselectapiServer *vmselectapi.Server

View File

@@ -0,0 +1,73 @@
package netstorage
import (
"fmt"
"sync"
)
var (
maxMemoryUsagePerQuery int64
)
// getMaxMemoryUsagePerQuery returns the maximum memory usage per query.
func getMaxMemoryUsagePerQuery() int64 {
return maxMemoryUsagePerQuery
}
// SetMaxMemoryUsagePerQuery sets the maximum memory usage per query.
func SetMaxMemoryUsagePerQuery(v int64) {
maxMemoryUsagePerQuery = v
}
// memoryLimiter tracks and limits memory usage for operations
type memoryLimiter struct {
maxSize uint64
mu sync.Mutex
usage uint64
}
func newMemoryLimiter() *memoryLimiter {
maxSize := uint64(getMaxMemoryUsagePerQuery())
return &memoryLimiter{
maxSize: maxSize,
}
}
func (ml *memoryLimiter) AddBytes(n uint64) error {
if ml.maxSize <= 0 {
return nil
}
ml.mu.Lock()
ok := n <= ml.maxSize && ml.maxSize-n >= ml.usage
if ok {
ml.usage += n
}
ml.mu.Unlock()
if !ok {
return &limitExceededErr{
err: fmt.Errorf("cannot allocate %d bytes; max allowed memory usage is %d bytes", n, ml.maxSize),
}
}
return nil
}
// AddStrings accounts for and limits memory usage of string slices
func (ml *memoryLimiter) AddStrings(v []string) error {
if len(v) == 0 {
return nil
}
// Account for:
// - Slice header (24 bytes)
// - String headers (16 bytes each)
// - String data
overhead := uint64(24 + (16 * len(v)))
dataSize := uint64(0)
for _, s := range v {
dataSize += uint64(len(s))
}
return ml.AddBytes(overhead + dataSize)
}

View File

@@ -906,10 +906,14 @@ func LabelNames(qt *querytracer.Tracer, denyPartialResponse bool, sq *storage.Se
return nil, false, err
}
sns := getStorageNodes()
ml := newMemoryLimiter()
snr := startStorageNodesRequest(qt, sns, denyPartialResponse, func(qt *querytracer.Tracer, _ uint, sn *storageNode) any {
return execSearchQuery(qt, sq, func(qt *querytracer.Tracer, requestData []byte, _ storage.TenantToken) any {
sn.labelNamesRequests.Inc()
labelNames, err := sn.getLabelNames(qt, requestData, maxLabelNames, deadline)
if err == nil {
err = ml.AddStrings(labelNames)
}
if err != nil {
sn.labelNamesErrors.Inc()
err = fmt.Errorf("cannot get labels from vmstorage %s: %w", sn.connPool.Addr(), err)
@@ -1023,14 +1027,14 @@ func LabelValues(qt *querytracer.Tracer, denyPartialResponse bool, labelName str
case "vm_project_id":
idx = 1
default:
logger.Fatalf("BUG: unexpected labeName=%q", labelName)
logger.Fatalf("BUG: unexpected labelName=%q", labelName)
}
labelValues := make([]string, 0, len(tenants))
for _, t := range tenants {
s := strings.Split(t, ":")
if len(s) != 2 {
logger.Fatalf("BUG: unexpected tenant received from storage: %q", t)
logger.Panicf("BUG: unexpected tenant received from storage: %q", t)
}
labelValues = append(labelValues, s[idx])
@@ -1049,11 +1053,15 @@ func LabelValues(qt *querytracer.Tracer, denyPartialResponse bool, labelName str
if err != nil {
return nil, false, err
}
ml := newMemoryLimiter()
sns := getStorageNodes()
snr := startStorageNodesRequest(qt, sns, denyPartialResponse, func(qt *querytracer.Tracer, _ uint, sn *storageNode) any {
return execSearchQuery(qt, sq, func(qt *querytracer.Tracer, requestData []byte, _ storage.TenantToken) any {
sn.labelValuesRequests.Inc()
labelValues, err := sn.getLabelValues(qt, labelName, requestData, maxLabelValues, deadline)
if err == nil {
err = ml.AddStrings(labelValues)
}
if err != nil {
sn.labelValuesErrors.Inc()
err = fmt.Errorf("cannot get label values from vmstorage %s: %w", sn.connPool.Addr(), err)
@@ -1194,10 +1202,14 @@ func TagValueSuffixes(qt *querytracer.Tracer, accountID, projectID uint32, denyP
suffixes []string
err error
}
ml := newMemoryLimiter()
sns := getStorageNodes()
snr := startStorageNodesRequest(qt, sns, denyPartialResponse, func(qt *querytracer.Tracer, _ uint, sn *storageNode) any {
sn.tagValueSuffixesRequests.Inc()
suffixes, err := sn.getTagValueSuffixes(qt, accountID, projectID, tr, tagKey, tagValuePrefix, delimiter, maxSuffixes, deadline)
if err == nil {
err = ml.AddStrings(suffixes)
}
if err != nil {
sn.tagValueSuffixesErrors.Inc()
err = fmt.Errorf("cannot get tag value suffixes for timeRange=%s, tagKey=%q, tagValuePrefix=%q, delimiter=%c from vmstorage %s: %w",
@@ -1695,11 +1707,15 @@ func SearchMetricNames(qt *querytracer.Tracer, denyPartialResponse bool, sq *sto
if err != nil {
return nil, false, err
}
ml := newMemoryLimiter()
sns := getStorageNodes()
snr := startStorageNodesRequest(qt, sns, denyPartialResponse, func(qt *querytracer.Tracer, _ uint, sn *storageNode) any {
return execSearchQuery(qt, sq, func(qt *querytracer.Tracer, requestData []byte, t storage.TenantToken) any {
sn.searchMetricNamesRequests.Inc()
metricNames, err := sn.processSearchMetricNames(qt, requestData, deadline)
if err == nil {
err = ml.AddStrings(metricNames)
}
if sq.IsMultiTenant {
// TODO: (@f41gh7) this function could produce duplicate labels
// if original metricName already have tenant labels
@@ -1951,8 +1967,10 @@ func populateSqTenantTokensIfNeeded(sq *storage.SearchQuery) error {
type storageNodesRequest struct {
denyPartialResponse bool
resultsCh chan rpcResult
qts map[*querytracer.Tracer]struct{}
sns []*storageNode
qt *querytracer.Tracer
// query tracers to storageAddresses mapping
qts map[*querytracer.Tracer]string
sns []*storageNode
}
type rpcResult struct {
@@ -1965,15 +1983,21 @@ func startStorageNodesRequest(qt *querytracer.Tracer, sns []*storageNode, denyPa
f func(qt *querytracer.Tracer, workerID uint, sn *storageNode) any,
) *storageNodesRequest {
resultsCh := make(chan rpcResult, len(sns))
qts := make(map[*querytracer.Tracer]struct{}, len(sns))
qts := make(map[*querytracer.Tracer]string, len(sns))
for idx, sn := range sns {
qtChild := qt.NewChild("rpc at vmstorage %s", sn.connPool.Addr())
qts[qtChild] = struct{}{}
// Do not use qt.NewChild.
// StorageNodesRequest may be finished before goroutine returns.
// Caller must register tracker manually with finishQueryTracer after goroutine returns result.
// It ensures that tracker is no longer referenced by any concurrent goroutines.
//
// See this issue: https://github.com/VictoriaMetrics/VictoriaMetrics/issues/8114
qtOrphan := querytracer.NewOrphan(qt, "rpc at vmstorage %s", sn.connPool.Addr())
qts[qtOrphan] = sn.connPool.Addr()
go func(workerID uint, sn *storageNode) {
data := f(qtChild, workerID, sn)
data := f(qtOrphan, workerID, sn)
resultsCh <- rpcResult{
data: data,
qt: qtChild,
qt: qtOrphan,
group: sn.group,
}
}(uint(idx), sn)
@@ -1981,14 +2005,21 @@ func startStorageNodesRequest(qt *querytracer.Tracer, sns []*storageNode, denyPa
return &storageNodesRequest{
denyPartialResponse: denyPartialResponse,
resultsCh: resultsCh,
qt: qt,
qts: qts,
sns: sns,
}
}
func (snr *storageNodesRequest) finishQueryTracers(msg string) {
for qt := range snr.qts {
snr.finishQueryTracer(qt, msg)
for qt, storageAddr := range snr.qts {
// since qt cannot be used concurrently,
// replace child still referenced by concurrent storageNode goroutine
// with local child that belongs to current goroutine.
// Add reason msg why it was done.
cancelledQt := snr.qt.NewChild("rpc at vmstorage: %s: %s", storageAddr, msg)
cancelledQt.Done()
delete(snr.qts, qt)
}
}
@@ -1999,6 +2030,7 @@ func (snr *storageNodesRequest) finishQueryTracer(qt *querytracer.Tracer, msg st
qt.Donef("%s", msg)
}
delete(snr.qts, qt)
snr.qt.AddChild(qt)
}
func (snr *storageNodesRequest) collectAllResults(f func(result any) error) error {

View File

@@ -507,10 +507,15 @@ func DeleteHandler(startTime time.Time, at *auth.Token, r *http.Request) error {
if err != nil {
return err
}
cp.deadline = searchutils.GetDeadlineForDelete(r, startTime)
if !cp.IsDefaultTimeRange() {
return fmt.Errorf("start=%d and end=%d args aren't supported. Remove these args from the query in order to delete all the matching metrics", cp.start, cp.end)
}
sq := storage.NewSearchQuery(at.AccountID, at.ProjectID, cp.start, cp.end, cp.filterss, *maxDeleteSeries)
sq, err := getSearchQuery(nil, at, cp, *maxDeleteSeries)
if err != nil {
return err
}
deletedCount, err := netstorage.DeleteSeries(nil, sq, cp.deadline)
if err != nil {
return fmt.Errorf("cannot delete time series: %w", err)
@@ -753,7 +758,9 @@ var labelsDuration = metrics.NewSummary(`vm_request_duration_seconds{path="/api/
// SeriesCountHandler processes /api/v1/series/count request.
func SeriesCountHandler(startTime time.Time, at *auth.Token, w http.ResponseWriter, r *http.Request) error {
defer seriesCountDuration.UpdateDuration(startTime)
if at == nil {
return fmt.Errorf("multi-tenant request to /api/v1/series/count is not supported")
}
deadline := searchutils.GetDeadlineForStatusRequest(r, startTime)
denyPartialResponse := httputils.GetDenyPartialResponse(r)
n, isPartial, err := netstorage.SeriesCount(nil, at.AccountID, at.ProjectID, denyPartialResponse, deadline)

View File

@@ -295,13 +295,13 @@ func aggrFuncMin(tss []*timeseries) []*timeseries {
}
dst := tss[0]
for i := range dst.Values {
min := dst.Values[i]
minV := dst.Values[i]
for _, ts := range tss {
if math.IsNaN(min) || ts.Values[i] < min {
min = ts.Values[i]
if math.IsNaN(minV) || ts.Values[i] < minV {
minV = ts.Values[i]
}
}
dst.Values[i] = min
dst.Values[i] = minV
}
return tss[:1]
}
@@ -313,13 +313,13 @@ func aggrFuncMax(tss []*timeseries) []*timeseries {
}
dst := tss[0]
for i := range dst.Values {
max := dst.Values[i]
maxV := dst.Values[i]
for _, ts := range tss {
if math.IsNaN(max) || ts.Values[i] > max {
max = ts.Values[i]
if math.IsNaN(maxV) || ts.Values[i] > maxV {
maxV = ts.Values[i]
}
}
dst.Values[i] = max
dst.Values[i] = maxV
}
return tss[:1]
}
@@ -793,7 +793,7 @@ func fillNaNsAtIdx(idx int, k float64, tss []*timeseries) {
}
}
func getIntK(k float64, max int) int {
func getIntK(k float64, maxV int) int {
if math.IsNaN(k) {
return 0
}
@@ -801,38 +801,38 @@ func getIntK(k float64, max int) int {
if kn < 0 {
return 0
}
if kn > max {
return max
if kn > maxV {
return maxV
}
return kn
}
func minValue(values []float64) float64 {
min := nan
for len(values) > 0 && math.IsNaN(min) {
min = values[0]
minV := nan
for len(values) > 0 && math.IsNaN(minV) {
minV = values[0]
values = values[1:]
}
for _, v := range values {
if !math.IsNaN(v) && v < min {
min = v
if !math.IsNaN(v) && v < minV {
minV = v
}
}
return min
return minV
}
func maxValue(values []float64) float64 {
max := nan
for len(values) > 0 && math.IsNaN(max) {
max = values[0]
maxV := nan
for len(values) > 0 && math.IsNaN(maxV) {
maxV = values[0]
values = values[1:]
}
for _, v := range values {
if !math.IsNaN(v) && v > max {
max = v
if !math.IsNaN(v) && v > maxV {
maxV = v
}
}
return max
return maxV
}
func avgValue(values []float64) float64 {

View File

@@ -36,10 +36,7 @@ var (
"See https://docs.victoriametrics.com/#backfilling . See also -search.resetRollupResultCacheOnStartup")
maxPointsSubqueryPerTimeseries = flag.Int("search.maxPointsSubqueryPerTimeseries", 100e3, "The maximum number of points per series, which can be generated by subquery. "+
"See https://valyala.medium.com/prometheus-subqueries-in-victoriametrics-9b1492b720b3")
maxMemoryPerQuery = flagutil.NewBytes("search.maxMemoryPerQuery", 0, "The maximum amounts of memory a single query may consume. "+
"Queries requiring more memory are rejected. The total memory limit for concurrently executed queries can be estimated "+
"as -search.maxMemoryPerQuery multiplied by -search.maxConcurrentRequests . "+
"See also -search.logQueryMemoryUsage")
logQueryMemoryUsage = flagutil.NewBytes("search.logQueryMemoryUsage", 0, "Log query and increment vm_memory_intensive_queries_total metric each time "+
"the query requires more memory than specified by this flag. "+
"This may help detecting and optimizing heavy queries. Query logging is disabled by default. "+
@@ -52,6 +49,20 @@ var (
"A larger value makes the pushed-down filter more complex but fewer time series will be returned. This flag is useful when selective label contains numerous values, for example `instance`, and storage resources are abundant.")
)
var (
maxMemoryPerQuery int64
)
// SetMaxMemoryPerQuery sets the maximum memory allowed per query.
func SetMaxMemoryPerQuery(v int64) {
maxMemoryPerQuery = v
}
// getMaxMemoryPerQuery returns the maximum memory allowed per query.
func getMaxMemoryPerQuery() int64 {
return maxMemoryPerQuery
}
// The minimum number of points per timeseries for enabling time rounding.
// This improves cache hit ratio for frequently requested queries over
// big time ranges.
@@ -1793,7 +1804,7 @@ func evalRollupFuncNoCache(qt *querytracer.Tracer, ec *EvalConfig, funcName stri
"the query selects %d time series and generates %d points across all the time series; try reducing the number of selected time series",
ec.QuotedRemoteAddr, requestURI, expr.AppendString(nil), rollupMemorySize, maxMemory, timeseriesLen*len(rcs), rollupPoints)
}
if maxMemory := int64(maxMemoryPerQuery.N); maxMemory > 0 && rollupMemorySize > maxMemory {
if maxMemory := int64(getMaxMemoryPerQuery()); maxMemory > 0 && rollupMemorySize > maxMemory {
rss.Cancel()
err := fmt.Errorf("not enough memory for processing %s, which returns %d data points across %d time series with %d points in each time series "+
"according to -search.maxMemoryPerQuery=%d; requested memory: %d bytes; "+
@@ -1986,6 +1997,10 @@ func evalNumber(ec *EvalConfig, n float64) []*timeseries {
for i := range timestamps {
values[i] = n
}
if !ec.IsMultiTenant {
ts.MetricName.AccountID = ec.AuthTokens[0].AccountID
ts.MetricName.ProjectID = ec.AuthTokens[0].ProjectID
}
ts.Values = values
ts.Timestamps = timestamps
return []*timeseries{&ts}

View File

@@ -374,8 +374,8 @@ func getRollupConfigs(funcName string, rf rollupFunc, expr metricsql.Expr, start
preFunc := func(_ []float64, _ []int64) {}
funcName = strings.ToLower(funcName)
if rollupFuncsRemoveCounterResets[funcName] {
preFunc = func(values []float64, _ []int64) {
removeCounterResets(values)
preFunc = func(values []float64, timestamps []int64) {
removeCounterResets(values, timestamps, lookbackDelta)
}
}
samplesScannedPerCall := rollupFuncsSamplesScannedPerCall[funcName]
@@ -487,8 +487,8 @@ func getRollupConfigs(funcName string, rf rollupFunc, expr metricsql.Expr, start
for _, aggrFuncName := range aggrFuncNames {
if rollupFuncsRemoveCounterResets[aggrFuncName] {
// There is no need to save the previous preFunc, since it is either empty or the same.
preFunc = func(values []float64, _ []int64) {
removeCounterResets(values)
preFunc = func(values []float64, timestamps []int64) {
removeCounterResets(values, timestamps, lookbackDelta)
}
}
rf := rollupAggrFuncs[aggrFuncName]
@@ -521,7 +521,8 @@ type rollupFuncArg struct {
// Timestamps for values.
timestamps []int64
// Real value preceding values without restrictions on staleness interval.
// Real value preceding values.
// Is populated if preceding value is within the -search.maxStalenessInterval (rc.LookbackDelta).
realPrevValue float64
// Real value which goes after values.
@@ -769,10 +770,18 @@ func (rc *rollupConfig) doInternal(dstValues []float64, tsm *timeseriesMap, valu
}
rfa.values = values[i:j]
rfa.timestamps = timestamps[i:j]
rfa.realPrevValue = nan
if i > 0 {
rfa.realPrevValue = values[i-1]
} else {
rfa.realPrevValue = nan
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
// 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 {
rfa.realPrevValue = prevValue
}
}
if j < len(values) {
rfa.realNextValue = values[j]
@@ -896,7 +905,7 @@ func getMaxPrevInterval(scrapeInterval int64) int64 {
return scrapeInterval + scrapeInterval/8
}
func removeCounterResets(values []float64) {
func removeCounterResets(values []float64, timestamps []int64, maxStalenessInterval int64) {
// There is no need in handling NaNs here, since they are impossible
// on values from vmstorage.
if len(values) == 0 {
@@ -915,6 +924,16 @@ func removeCounterResets(values []float64) {
correction += prevValue
}
}
if i > 0 && maxStalenessInterval > 0 {
gap := timestamps[i] - timestamps[i-1]
if gap > maxStalenessInterval {
// reset correction if gap between samples exceeds staleness interval
// see https://github.com/VictoriaMetrics/VictoriaMetrics/issues/8072
correction = 0
prevValue = v
continue
}
}
prevValue = v
values[i] = v + correction
// Check again, there could be precision error in float operations,
@@ -1687,9 +1706,9 @@ func rollupRateOverSum(rfa *rollupFuncArg) float64 {
}
func rollupRange(rfa *rollupFuncArg) float64 {
max := rollupMax(rfa)
min := rollupMin(rfa)
return max - min
maxV := rollupMax(rfa)
minV := rollupMin(rfa)
return maxV - minV
}
func rollupSum2(rfa *rollupFuncArg) float64 {
@@ -2197,38 +2216,38 @@ func rollupClose(rfa *rollupFuncArg) float64 {
func rollupHigh(rfa *rollupFuncArg) float64 {
values := getCandlestickValues(rfa)
max := getFirstValueForCandlestick(rfa)
if math.IsNaN(max) {
maxV := getFirstValueForCandlestick(rfa)
if math.IsNaN(maxV) {
if len(values) == 0 {
return nan
}
max = values[0]
maxV = values[0]
values = values[1:]
}
for _, v := range values {
if v > max {
max = v
if v > maxV {
maxV = v
}
}
return max
return maxV
}
func rollupLow(rfa *rollupFuncArg) float64 {
values := getCandlestickValues(rfa)
min := getFirstValueForCandlestick(rfa)
if math.IsNaN(min) {
minV := getFirstValueForCandlestick(rfa)
if math.IsNaN(minV) {
if len(values) == 0 {
return nan
}
min = values[0]
minV = values[0]
values = values[1:]
}
for _, v := range values {
if v < min {
min = v
if v < minV {
minV = v
}
}
return min
return minV
}
func rollupModeOverTime(rfa *rollupFuncArg) float64 {

View File

@@ -5,6 +5,8 @@ import (
"testing"
"github.com/VictoriaMetrics/metricsql"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/decimal"
)
var (
@@ -115,31 +117,49 @@ func TestRollupIderivDuplicateTimestamps(t *testing.T) {
}
func TestRemoveCounterResets(t *testing.T) {
removeCounterResets(nil)
removeCounterResets(nil, nil, 0)
values := append([]float64{}, testValues...)
removeCounterResets(values)
timestamps := append([]int64{}, testTimestamps...)
removeCounterResets(values, timestamps, 0)
valuesExpected := []float64{123, 157, 167, 188, 221, 255, 320, 332, 364, 396, 398, 398}
testRowsEqual(t, values, testTimestamps, valuesExpected, testTimestamps)
// removeCounterResets doesn't expect negative values, so it doesn't work properly with them.
values = []float64{-100, -200, -300, -400}
removeCounterResets(values)
valuesExpected = []float64{-100, -100, -100, -100}
timestampsExpected := []int64{0, 1, 2, 3}
removeCounterResets(values, timestampsExpected, 0)
valuesExpected = []float64{-100, -100, -100, -100}
testRowsEqual(t, values, timestampsExpected, valuesExpected, timestampsExpected)
// verify how partial counter reset is handled.
// See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/2787
values = []float64{100, 95, 120, 119, 139, 50}
removeCounterResets(values)
valuesExpected = []float64{100, 100, 125, 125, 145, 195}
timestampsExpected = []int64{0, 1, 2, 3, 4, 5}
removeCounterResets(values, timestampsExpected, 0)
valuesExpected = []float64{100, 100, 125, 125, 145, 195}
testRowsEqual(t, values, timestampsExpected, valuesExpected, timestampsExpected)
// verify that staleness interval is respected during resets
// see https://github.com/VictoriaMetrics/VictoriaMetrics/issues/8072
values = []float64{10, 12, 14, 4, 6, 8, 6, 8, 4, 6}
timestamps = []int64{10, 20, 30, 60, 70, 80, 90, 100, 120, 130}
valuesExpected = []float64{10, 12, 14, 4, 6, 8, 14, 16, 4, 6}
removeCounterResets(values, timestamps, 10)
testRowsEqual(t, values, timestamps, valuesExpected, timestamps)
// verify that staleness is respected if there was no counter reset
// but correction was made previously
values = []float64{10, 12, 2, 4}
timestamps = []int64{10, 20, 30, 60}
valuesExpected = []float64{10, 12, 14, 4}
removeCounterResets(values, timestamps, 10)
testRowsEqual(t, values, timestamps, valuesExpected, timestamps)
// verify results always increase monotonically with possible float operations precision error
values = []float64{34.094223, 2.7518, 2.140669, 0.044878, 1.887095, 2.546569, 2.490149, 0.045, 0.035684, 0.062454, 0.058296}
removeCounterResets(values)
timestampsExpected = []int64{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
removeCounterResets(values, timestampsExpected, 0)
var prev float64
for i, v := range values {
if v < prev {
@@ -164,7 +184,7 @@ func TestDeltaValues(t *testing.T) {
// remove counter resets
values = append([]float64{}, testValues...)
removeCounterResets(values)
removeCounterResets(values, testTimestamps, 0)
deltaValues(values)
valuesExpected = []float64{34, 10, 21, 33, 34, 65, 12, 32, 32, 2, 0, 0}
testRowsEqual(t, values, testTimestamps, valuesExpected, testTimestamps)
@@ -186,7 +206,7 @@ func TestDerivValues(t *testing.T) {
// remove counter resets
values = append([]float64{}, testValues...)
removeCounterResets(values)
removeCounterResets(values, testTimestamps, 0)
derivValues(values, testTimestamps)
valuesExpected = []float64{3400, 1111.111111111111, 1750, 2538.4615384615386, 3090.909090909091, 3611.1111111111113,
6000, 1882.3529411764705, 1777.7777777777778, 400, 0, 0}
@@ -217,7 +237,7 @@ func testRollupFunc(t *testing.T, funcName string, args []any, vExpected float64
rfa.timestamps = append(rfa.timestamps, testTimestamps...)
rfa.window = rfa.timestamps[len(rfa.timestamps)-1] - rfa.timestamps[0]
if rollupFuncsRemoveCounterResets[funcName] {
removeCounterResets(rfa.values)
removeCounterResets(rfa.values, rfa.timestamps, 0)
}
for i := 0; i < 5; i++ {
v := rf(&rfa)
@@ -1587,3 +1607,229 @@ func TestRollupDelta(t *testing.T) {
f(1, nan, nan, nil, 0)
f(100, nan, nan, nil, 0)
}
func TestRollupDeltaWithStaleness(t *testing.T) {
// there is a gap between samples in the dataset below
timestamps := []int64{0, 15000, 30000, 70000}
values := []float64{1, 1, 1, 1}
// if step > gap, then delta will always respect value before gap
t.Run("step>gap", func(t *testing.T) {
rc := rollupConfig{
Func: rollupDelta,
Start: 0,
End: 70000,
Step: 45000,
Window: 0,
MaxPointsPerSeries: 1e4,
}
rc.Timestamps = rc.getTimestamps()
gotValues, samplesScanned := rc.Do(nil, values, timestamps)
if samplesScanned != 7 {
t.Fatalf("expecting 8 samplesScanned from rollupConfig.Do; got %d", samplesScanned)
}
valuesExpected := []float64{1, 0}
timestampsExpected := []int64{0, 45e3}
testRowsEqual(t, gotValues, rc.Timestamps, valuesExpected, timestampsExpected)
})
// even if LookbackDelta < gap
t.Run("step>gap;LookbackDelta<gap", func(t *testing.T) {
rc := rollupConfig{
Func: rollupDelta,
Start: 0,
End: 70000,
Step: 45000,
LookbackDelta: 10e3,
Window: 0,
MaxPointsPerSeries: 1e4,
}
rc.Timestamps = rc.getTimestamps()
gotValues, samplesScanned := rc.Do(nil, values, timestamps)
if samplesScanned != 7 {
t.Fatalf("expecting 8 samplesScanned from rollupConfig.Do; got %d", samplesScanned)
}
valuesExpected := []float64{1, 0}
timestampsExpected := []int64{0, 45e3}
testRowsEqual(t, gotValues, rc.Timestamps, valuesExpected, timestampsExpected)
})
// if step < gap and LookbackDelta==0 then delta will always respect value before gap
// as LookbackDelta=0 ignores staleness
t.Run("step<gap;LookbackDelta=0", func(t *testing.T) {
rc := rollupConfig{
Func: rollupDelta,
Start: 0,
End: 70000,
Step: 10000,
LookbackDelta: 0,
Window: 0,
MaxPointsPerSeries: 1e4,
}
rc.Timestamps = rc.getTimestamps()
gotValues, samplesScanned := rc.Do(nil, values, timestamps)
if samplesScanned != 8 {
t.Fatalf("expecting 8 samplesScanned from rollupConfig.Do; got %d", samplesScanned)
}
valuesExpected := []float64{1, 0, 0, 0, 0, 0, 0, 0}
timestampsExpected := []int64{0, 10e3, 20e3, 30e3, 40e3, 50e3, 60e3, 70e3}
testRowsEqual(t, gotValues, rc.Timestamps, valuesExpected, timestampsExpected)
})
// if step < gap and LookbackDelta>0 then delta will respect value before gap
// only if it is not stale according to LookbackDelta
t.Run("step<gap;LookbackDelta>0", func(t *testing.T) {
rc := rollupConfig{
Func: rollupDelta,
Start: 0,
End: 70000,
Step: 10000,
Window: 0,
MaxPointsPerSeries: 1e4,
LookbackDelta: 30e3,
}
rc.Timestamps = rc.getTimestamps()
gotValues, samplesScanned := rc.Do(nil, values, timestamps)
if samplesScanned != 8 {
t.Fatalf("expecting 8 samplesScanned from rollupConfig.Do; got %d", samplesScanned)
}
valuesExpected := []float64{1, 0, 0, 0, 0, 0, 0, 1}
timestampsExpected := []int64{0, 10e3, 20e3, 30e3, 40e3, 50e3, 60e3, 70e3}
testRowsEqual(t, gotValues, rc.Timestamps, valuesExpected, timestampsExpected)
})
// there is a staleness marker between samples in the dataset below
timestamps = []int64{0, 10000, 20000, 30000, 40000}
values = []float64{1, 1, 1, decimal.StaleNaN, 1}
t.Run("staleness marker", func(t *testing.T) {
rc := rollupConfig{
Func: rollupDelta,
Start: 0,
End: 40000,
Step: 10000,
Window: 0,
MaxPointsPerSeries: 1e4,
}
rc.Timestamps = rc.getTimestamps()
gotValues, samplesScanned := rc.Do(nil, values, timestamps)
if samplesScanned != 10 {
t.Fatalf("expecting 10 samplesScanned from rollupConfig.Do; got %d", samplesScanned)
}
valuesExpected := []float64{1, 0, 0, nan, 1}
timestampsExpected := []int64{0, 10e3, 20e3, 30e3, 40e3}
testRowsEqual(t, gotValues, rc.Timestamps, valuesExpected, timestampsExpected)
})
}
func TestRollupIncreasePureWithStaleness(t *testing.T) {
// there is a gap between samples in the dataset below
timestamps := []int64{0, 15000, 30000, 70000}
values := []float64{1, 1, 1, 1}
// if step > gap, then delta will always respect value before gap
t.Run("step>gap", func(t *testing.T) {
rc := rollupConfig{
Func: rollupIncreasePure,
Start: 0,
End: 70000,
Step: 45000,
Window: 0,
MaxPointsPerSeries: 1e4,
}
rc.Timestamps = rc.getTimestamps()
gotValues, samplesScanned := rc.Do(nil, values, timestamps)
if samplesScanned != 7 {
t.Fatalf("expecting 8 samplesScanned from rollupConfig.Do; got %d", samplesScanned)
}
valuesExpected := []float64{1, 0}
timestampsExpected := []int64{0, 45e3}
testRowsEqual(t, gotValues, rc.Timestamps, valuesExpected, timestampsExpected)
})
// even if LookbackDelta < gap
t.Run("step>gap;LookbackDelta<gap", func(t *testing.T) {
rc := rollupConfig{
Func: rollupIncreasePure,
Start: 0,
End: 70000,
Step: 45000,
LookbackDelta: 10e3,
Window: 0,
MaxPointsPerSeries: 1e4,
}
rc.Timestamps = rc.getTimestamps()
gotValues, samplesScanned := rc.Do(nil, values, timestamps)
if samplesScanned != 7 {
t.Fatalf("expecting 8 samplesScanned from rollupConfig.Do; got %d", samplesScanned)
}
valuesExpected := []float64{1, 0}
timestampsExpected := []int64{0, 45e3}
testRowsEqual(t, gotValues, rc.Timestamps, valuesExpected, timestampsExpected)
})
// if step < gap and LookbackDelta==0 then delta will always respect value before gap
// as LookbackDelta=0 ignores staleness
t.Run("step<gap;LookbackDelta=0", func(t *testing.T) {
rc := rollupConfig{
Func: rollupIncreasePure,
Start: 0,
End: 70000,
Step: 10000,
LookbackDelta: 0,
Window: 0,
MaxPointsPerSeries: 1e4,
}
rc.Timestamps = rc.getTimestamps()
gotValues, samplesScanned := rc.Do(nil, values, timestamps)
if samplesScanned != 8 {
t.Fatalf("expecting 8 samplesScanned from rollupConfig.Do; got %d", samplesScanned)
}
valuesExpected := []float64{1, 0, 0, 0, 0, 0, 0, 0}
timestampsExpected := []int64{0, 10e3, 20e3, 30e3, 40e3, 50e3, 60e3, 70e3}
testRowsEqual(t, gotValues, rc.Timestamps, valuesExpected, timestampsExpected)
})
// if step < gap and LookbackDelta>0 then delta will respect value before gap
// only if it is not stale according to LookbackDelta
t.Run("step<gap;LookbackDelta>0", func(t *testing.T) {
rc := rollupConfig{
Func: rollupIncreasePure,
Start: 0,
End: 70000,
Step: 10000,
Window: 0,
MaxPointsPerSeries: 1e4,
LookbackDelta: 30e3,
}
rc.Timestamps = rc.getTimestamps()
gotValues, samplesScanned := rc.Do(nil, values, timestamps)
if samplesScanned != 8 {
t.Fatalf("expecting 8 samplesScanned from rollupConfig.Do; got %d", samplesScanned)
}
valuesExpected := []float64{1, 0, 0, 0, 0, 0, 0, 1}
timestampsExpected := []int64{0, 10e3, 20e3, 30e3, 40e3, 50e3, 60e3, 70e3}
testRowsEqual(t, gotValues, rc.Timestamps, valuesExpected, timestampsExpected)
})
// there is a staleness marker between samples in the dataset below
timestamps = []int64{0, 10000, 20000, 30000, 40000}
values = []float64{1, 1, 1, decimal.StaleNaN, 1}
t.Run("staleness marker", func(t *testing.T) {
rc := rollupConfig{
Func: rollupIncreasePure,
Start: 0,
End: 40000,
Step: 10000,
Window: 0,
MaxPointsPerSeries: 1e4,
}
rc.Timestamps = rc.getTimestamps()
gotValues, samplesScanned := rc.Do(nil, values, timestamps)
if samplesScanned != 10 {
t.Fatalf("expecting 10 samplesScanned from rollupConfig.Do; got %d", samplesScanned)
}
valuesExpected := []float64{1, 0, 0, nan, 1}
timestampsExpected := []int64{0, 10e3, 20e3, 30e3, 40e3}
testRowsEqual(t, gotValues, rc.Timestamps, valuesExpected, timestampsExpected)
})
}

View File

@@ -15,6 +15,7 @@ import (
var (
maxExportDuration = flag.Duration("search.maxExportDuration", time.Hour*24*30, "The maximum duration for /api/v1/export call")
maxDeleteDuration = flag.Duration("search.maxDeleteDuration", time.Minute*5, "The maximum duration for /api/v1/admin/tsdb/delete_series call")
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")
maxStatusRequestDuration = flag.Duration("search.maxStatusRequestDuration", time.Minute*5, "The maximum duration for /api/v1/status/* requests")
maxLabelsAPIDuration = flag.Duration("search.maxLabelsAPIDuration", time.Second*5, "The maximum duration for /api/v1/labels, /api/v1/label/.../values and /api/v1/series requests. "+
@@ -58,6 +59,12 @@ func GetDeadlineForLabelsAPI(r *http.Request, startTime time.Time) Deadline {
return getDeadlineWithMaxDuration(r, startTime, dMax, "-search.maxLabelsAPIDuration")
}
// GetDeadlineForDelete returns deadline for the given request to /api/v1/admin/tsdb/delete_series.
func GetDeadlineForDelete(r *http.Request, startTime time.Time) Deadline {
dMax := maxDeleteDuration.Milliseconds()
return getDeadlineWithMaxDuration(r, startTime, dMax, "-search.maxDeleteDuration")
}
func getDeadlineWithMaxDuration(r *http.Request, startTime time.Time, dMax int64, flagHint string) Deadline {
d, err := httputils.GetDuration(r, "timeout", 0)
if err != nil {

View File

@@ -1,13 +1,13 @@
{
"files": {
"main.css": "./static/css/main.876c56b7.css",
"main.js": "./static/js/main.caf36c39.js",
"main.css": "./static/css/main.af583aad.css",
"main.js": "./static/js/main.1413b18d.js",
"static/js/685.f772060c.chunk.js": "./static/js/685.f772060c.chunk.js",
"static/media/MetricsQL.md": "./static/media/MetricsQL.a00044c91d9781cf8557.md",
"index.html": "./index.html"
},
"entrypoints": [
"static/css/main.876c56b7.css",
"static/js/main.caf36c39.js"
"static/css/main.af583aad.css",
"static/js/main.1413b18d.js"
]
}

View File

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -69,12 +69,19 @@ var (
minFreeDiskSpaceBytes = flagutil.NewBytes("storage.minFreeDiskSpaceBytes", 10e6, "The minimum free disk space at -storageDataPath after which the storage stops accepting new data")
finalDedupScheduleInterval = flag.Duration("storage.finalDedupScheduleCheckInterval", time.Hour, "The interval for checking when final deduplication process should be started."+
"Storage unconditionally adds 25% jitter to the interval value on each check evaluation."+
" Changing the interval to the bigger values may delay downsampling, deduplication for historical data."+
" See also https://docs.victoriametrics.com/#deduplication")
cacheSizeStorageTSID = flagutil.NewBytes("storage.cacheSizeStorageTSID", 0, "Overrides max size for storage/tsid cache. "+
"See https://docs.victoriametrics.com/single-server-victoriametrics/#cache-tuning")
cacheSizeIndexDBIndexBlocks = flagutil.NewBytes("storage.cacheSizeIndexDBIndexBlocks", 0, "Overrides max size for indexdb/indexBlocks cache. "+
"See https://docs.victoriametrics.com/single-server-victoriametrics/#cache-tuning")
cacheSizeIndexDBDataBlocks = flagutil.NewBytes("storage.cacheSizeIndexDBDataBlocks", 0, "Overrides max size for indexdb/dataBlocks cache. "+
"See https://docs.victoriametrics.com/single-server-victoriametrics/#cache-tuning")
cacheSizeIndexDBDataBlocksSparse = flagutil.NewBytes("storage.cacheSizeIndexDBDataBlocksSparse", 0, "Overrides max size for indexdb/dataBlocksSparse cache. "+
"See https://docs.victoriametrics.com/single-server-victoriametrics/#cache-tuning")
cacheSizeIndexDBTagFilters = flagutil.NewBytes("storage.cacheSizeIndexDBTagFilters", 0, "Overrides max size for indexdb/tagFiltersToMetricIDs cache. "+
"See https://docs.victoriametrics.com/single-server-victoriametrics/#cache-tuning")
)
@@ -103,8 +110,13 @@ func main() {
storage.SetFreeDiskSpaceLimit(minFreeDiskSpaceBytes.N)
storage.SetTSIDCacheSize(cacheSizeStorageTSID.IntN())
storage.SetTagFiltersCacheSize(cacheSizeIndexDBTagFilters.IntN())
if *finalDedupScheduleInterval < time.Hour {
logger.Fatalf("-storage.finalDedupScheduleCheckInterval cannot be smaller than 1 hour; got %s", *finalDedupScheduleInterval)
}
storage.SetFinalDedupScheduleInterval(*finalDedupScheduleInterval)
mergeset.SetIndexBlocksCacheSize(cacheSizeIndexDBIndexBlocks.IntN())
mergeset.SetDataBlocksCacheSize(cacheSizeIndexDBDataBlocks.IntN())
mergeset.SetDataBlocksSparseCacheSize(cacheSizeIndexDBDataBlocksSparse.IntN())
if retentionPeriod.Duration() < 24*time.Hour {
logger.Fatalf("-retentionPeriod cannot be smaller than a day; got %s", retentionPeriod)
@@ -518,6 +530,7 @@ func writeStorageMetrics(w io.Writer, strg *storage.Storage) {
metrics.WriteGaugeUint64(w, `vm_cache_entries{type="storage/next_day_metric_ids"}`, m.NextDayMetricIDCacheSize)
metrics.WriteGaugeUint64(w, `vm_cache_entries{type="storage/indexBlocks"}`, tm.IndexBlocksCacheSize)
metrics.WriteGaugeUint64(w, `vm_cache_entries{type="indexdb/dataBlocks"}`, idbm.DataBlocksCacheSize)
metrics.WriteGaugeUint64(w, `vm_cache_entries{type="indexdb/dataBlocksSparse"}`, idbm.DataBlocksSparseCacheSize)
metrics.WriteGaugeUint64(w, `vm_cache_entries{type="indexdb/indexBlocks"}`, idbm.IndexBlocksCacheSize)
metrics.WriteGaugeUint64(w, `vm_cache_entries{type="indexdb/tagFiltersToMetricIDs"}`, idbm.TagFiltersToMetricIDsCacheSize)
metrics.WriteGaugeUint64(w, `vm_cache_entries{type="storage/regexps"}`, uint64(storage.RegexpCacheSize()))
@@ -529,6 +542,7 @@ func writeStorageMetrics(w io.Writer, strg *storage.Storage) {
metrics.WriteGaugeUint64(w, `vm_cache_size_bytes{type="storage/metricName"}`, m.MetricNameCacheSizeBytes)
metrics.WriteGaugeUint64(w, `vm_cache_size_bytes{type="storage/indexBlocks"}`, tm.IndexBlocksCacheSizeBytes)
metrics.WriteGaugeUint64(w, `vm_cache_size_bytes{type="indexdb/dataBlocks"}`, idbm.DataBlocksCacheSizeBytes)
metrics.WriteGaugeUint64(w, `vm_cache_size_bytes{type="indexdb/dataBlocksSparse"}`, idbm.DataBlocksSparseCacheSizeBytes)
metrics.WriteGaugeUint64(w, `vm_cache_size_bytes{type="indexdb/indexBlocks"}`, idbm.IndexBlocksCacheSizeBytes)
metrics.WriteGaugeUint64(w, `vm_cache_size_bytes{type="storage/date_metricID"}`, m.DateMetricIDCacheSizeBytes)
metrics.WriteGaugeUint64(w, `vm_cache_size_bytes{type="storage/hour_metric_ids"}`, m.HourMetricIDCacheSizeBytes)
@@ -543,6 +557,7 @@ func writeStorageMetrics(w io.Writer, strg *storage.Storage) {
metrics.WriteGaugeUint64(w, `vm_cache_size_max_bytes{type="storage/metricName"}`, m.MetricNameCacheSizeMaxBytes)
metrics.WriteGaugeUint64(w, `vm_cache_size_max_bytes{type="storage/indexBlocks"}`, tm.IndexBlocksCacheSizeMaxBytes)
metrics.WriteGaugeUint64(w, `vm_cache_size_max_bytes{type="indexdb/dataBlocks"}`, idbm.DataBlocksCacheSizeMaxBytes)
metrics.WriteGaugeUint64(w, `vm_cache_size_max_bytes{type="indexdb/dataBlocksSparse"}`, idbm.DataBlocksSparseCacheSizeMaxBytes)
metrics.WriteGaugeUint64(w, `vm_cache_size_max_bytes{type="indexdb/indexBlocks"}`, idbm.IndexBlocksCacheSizeMaxBytes)
metrics.WriteGaugeUint64(w, `vm_cache_size_max_bytes{type="indexdb/tagFiltersToMetricIDs"}`, idbm.TagFiltersToMetricIDsCacheSizeMaxBytes)
metrics.WriteGaugeUint64(w, `vm_cache_size_max_bytes{type="storage/regexps"}`, uint64(storage.RegexpCacheMaxSizeBytes()))
@@ -553,6 +568,7 @@ func writeStorageMetrics(w io.Writer, strg *storage.Storage) {
metrics.WriteCounterUint64(w, `vm_cache_requests_total{type="storage/metricName"}`, m.MetricNameCacheRequests)
metrics.WriteCounterUint64(w, `vm_cache_requests_total{type="storage/indexBlocks"}`, tm.IndexBlocksCacheRequests)
metrics.WriteCounterUint64(w, `vm_cache_requests_total{type="indexdb/dataBlocks"}`, idbm.DataBlocksCacheRequests)
metrics.WriteCounterUint64(w, `vm_cache_requests_total{type="indexdb/dataBlocksSparse"}`, idbm.DataBlocksSparseCacheRequests)
metrics.WriteCounterUint64(w, `vm_cache_requests_total{type="indexdb/indexBlocks"}`, idbm.IndexBlocksCacheRequests)
metrics.WriteCounterUint64(w, `vm_cache_requests_total{type="indexdb/tagFiltersToMetricIDs"}`, idbm.TagFiltersToMetricIDsCacheRequests)
metrics.WriteCounterUint64(w, `vm_cache_requests_total{type="storage/regexps"}`, storage.RegexpCacheRequests())
@@ -563,6 +579,7 @@ func writeStorageMetrics(w io.Writer, strg *storage.Storage) {
metrics.WriteCounterUint64(w, `vm_cache_misses_total{type="storage/metricName"}`, m.MetricNameCacheMisses)
metrics.WriteCounterUint64(w, `vm_cache_misses_total{type="storage/indexBlocks"}`, tm.IndexBlocksCacheMisses)
metrics.WriteCounterUint64(w, `vm_cache_misses_total{type="indexdb/dataBlocks"}`, idbm.DataBlocksCacheMisses)
metrics.WriteCounterUint64(w, `vm_cache_misses_total{type="indexdb/dataBlocksSparse"}`, idbm.DataBlocksSparseCacheMisses)
metrics.WriteCounterUint64(w, `vm_cache_misses_total{type="indexdb/indexBlocks"}`, idbm.IndexBlocksCacheMisses)
metrics.WriteCounterUint64(w, `vm_cache_misses_total{type="indexdb/tagFiltersToMetricIDs"}`, idbm.TagFiltersToMetricIDsCacheMisses)
metrics.WriteCounterUint64(w, `vm_cache_misses_total{type="storage/regexps"}`, storage.RegexpCacheMisses())

View File

@@ -1,4 +1,4 @@
FROM golang:1.23.4 AS build-web-stage
FROM golang:1.23.5 AS build-web-stage
COPY build /build
WORKDIR /build
@@ -6,7 +6,7 @@ COPY web/ /build/
RUN GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -o web-amd64 github.com/VictoriMetrics/vmui/ && \
GOOS=windows GOARCH=amd64 CGO_ENABLED=0 go build -o web-windows github.com/VictoriMetrics/vmui/
FROM alpine:3.21.0
FROM alpine:3.21.2
USER root
COPY --from=build-web-stage /build/web-amd64 /app/web

View File

@@ -1,3 +1,6 @@
import uPlot from "uplot";
import { ReactNode } from "react";
export interface MetricBase {
group: number;
metric: {
@@ -6,13 +9,13 @@ export interface MetricBase {
}
export interface MetricResult extends MetricBase {
values: [number, string][]
values: [number, string][];
}
export interface InstantMetricResult extends MetricBase {
value?: [number, string]
values?: [number, string][]
value?: [number, string];
values?: [number, string][];
}
export interface ExportMetricResult extends MetricBase {
@@ -44,9 +47,23 @@ export interface LogHits {
timestamps: string[];
values: number[];
total?: number;
fields: {
[key: string]: string;
};
fields: { [key: string]: string; };
_isOther: boolean;
}
export interface LegendLogHits {
label: string;
total: number;
totalHits: number;
isOther: boolean;
fields: { [key: string]: string; };
stroke?: uPlot.Series.Stroke;
}
export interface LegendLogHitsMenu {
title: string;
icon?: ReactNode;
handler?: () => void;
}
export interface ReportMetaData {
@@ -56,3 +73,8 @@ export interface ReportMetaData {
comment: string;
params: Record<string, string>;
}
export interface LogsFiledValues {
value: string;
hits: number;
}

View File

@@ -1,22 +1,23 @@
import React, { FC, useMemo, useRef, useState } from "preact/compat";
import React, { FC, useCallback, useMemo, useRef, useState } from "preact/compat";
import "./style.scss";
import "uplot/dist/uPlot.min.css";
import useElementSize from "../../../hooks/useElementSize";
import uPlot, { AlignedData } from "uplot";
import { useEffect } from "react";
import useBarHitsOptions from "./hooks/useBarHitsOptions";
import useBarHitsOptions, { getLabelFromLogHit } from "./hooks/useBarHitsOptions";
import BarHitsTooltip from "./BarHitsTooltip/BarHitsTooltip";
import { TimeParams } from "../../../types";
import usePlotScale from "../../../hooks/uplot/usePlotScale";
import useReadyChart from "../../../hooks/uplot/useReadyChart";
import useZoomChart from "../../../hooks/uplot/useZoomChart";
import classNames from "classnames";
import { LogHits } from "../../../api/types";
import { LegendLogHits, LogHits } from "../../../api/types";
import { addSeries, delSeries, setBand } from "../../../utils/uplot";
import { GraphOptions, GRAPH_STYLES } from "./types";
import BarHitsOptions from "./BarHitsOptions/BarHitsOptions";
import stack from "../../../utils/uplot/stack";
import BarHitsLegend from "./BarHitsLegend/BarHitsLegend";
import { calculateTotalHits, sortLogHits } from "../../../utils/logs";
interface Props {
logHits: LogHits[];
@@ -57,6 +58,29 @@ const BarHitsChart: FC<Props> = ({ logHits, data: _data, period, setPeriod, onAp
graphOptions
});
const prepareLegend = useCallback((hits: LogHits[], totalHits: number): LegendLogHits[] => {
return hits.map((hit) => {
const label = getLabelFromLogHit(hit);
const legendItem: LegendLogHits = {
label,
isOther: hit._isOther,
fields: hit.fields,
total: hit.total || 0,
totalHits,
stroke: series.find((s) => s.label === label)?.stroke,
};
return legendItem;
}).sort(sortLogHits("total"));
}, [series]);
const legendDetails: LegendLogHits[] = useMemo(() => {
const totalHits = calculateTotalHits(logHits);
return prepareLegend(logHits, totalHits);
}, [logHits, prepareLegend]);
useEffect(() => {
if (!uPlotInst) return;
delSeries(uPlotInst);
@@ -121,6 +145,7 @@ const BarHitsChart: FC<Props> = ({ logHits, data: _data, period, setPeriod, onAp
<BarHitsLegend
uPlotInst={uPlotInst}
onApplyFilter={onApplyFilter}
legendDetails={legendDetails}
/>
)}
</div>

View File

@@ -1,83 +1,53 @@
import React, { FC, useCallback, useEffect, useState } from "preact/compat";
import React, { FC, useEffect, useState } from "preact/compat";
import uPlot, { Series } from "uplot";
import "./style.scss";
import "../../Line/Legend/style.scss";
import classNames from "classnames";
import { MouseEvent } from "react";
import { isMacOs } from "../../../../utils/detect-device";
import Tooltip from "../../../Main/Tooltip/Tooltip";
import { getStreamPairs } from "../../../../utils/logs";
import BarHitsLegendItem from "./BarHitsLegendItem";
import { LegendLogHits } from "../../../../api/types";
interface Props {
uPlotInst: uPlot;
legendDetails: LegendLogHits[];
onApplyFilter: (value: string) => void;
}
const BarHitsLegend: FC<Props> = ({ uPlotInst, onApplyFilter }) => {
const BarHitsLegend: FC<Props> = ({ uPlotInst, legendDetails, onApplyFilter }) => {
const [series, setSeries] = useState<Series[]>([]);
const [pairs, setPairs] = useState<string[][]>([]);
const totalHits = legendDetails[0]?.totalHits || 0;
const updateSeries = useCallback(() => {
const series = uPlotInst.series.filter(s => s.scale !== "x");
setSeries(series);
setPairs(series.map(s => getStreamPairs(s.label || "")));
const getSeries = () => {
return uPlotInst.series.filter(s => s.scale !== "x");
};
const handleRedrawGraph = () => {
uPlotInst.redraw();
setSeries(getSeries());
};
useEffect(() => {
setSeries(getSeries());
}, [uPlotInst]);
const handleClickByValue = (value: string) => (e: MouseEvent<HTMLDivElement>) => {
const metaKey = e.metaKey || e.ctrlKey;
if (!metaKey) return;
onApplyFilter(`{${value}}` || "");
updateSeries();
uPlotInst.redraw();
};
const handleClickByStream = (target: Series) => (e: MouseEvent<HTMLDivElement>) => {
const metaKey = e.metaKey || e.ctrlKey;
if (metaKey) return;
target.show = !target.show;
updateSeries();
uPlotInst.redraw();
};
useEffect(updateSeries, [uPlotInst]);
return (
<div className="vm-bar-hits-legend">
{series.map((s, i) => (
<Tooltip
key={s.label}
title={(
<ul className="vm-bar-hits-legend-info">
<li>Click to {s.show ? "hide" : "show"} the _stream.</li>
<li>{isMacOs() ? "Cmd" : "Ctrl"} + Click to filter by the _stream.</li>
</ul>
)}
>
<div
className={classNames({
"vm-bar-hits-legend-item": true,
"vm-bar-hits-legend-item_hide": !s.show,
})}
onClick={handleClickByStream(s)}
>
<div
className="vm-bar-hits-legend-item__marker"
style={{ backgroundColor: `${(s?.stroke as () => string)?.()}` }}
/>
<div className="vm-bar-hits-legend-item-pairs">
{pairs[i].map(value => (
<span
className="vm-bar-hits-legend-item-pairs__value"
key={value}
onClick={handleClickByValue(value)}
>
{value}
</span>
))}
</div>
</div>
</Tooltip>
{legendDetails.map((legend) => (
<BarHitsLegendItem
key={legend.label}
legend={legend}
series={series}
onRedrawGraph={handleRedrawGraph}
onApplyFilter={onApplyFilter}
/>
))}
<div className="vm-bar-hits-legend-info">
<div>
Total hits: <b>{totalHits.toLocaleString("en-US")}</b>
</div>
<div>
<code>L-Click</code> toggles visibility.&nbsp;
<code>R-Click</code> opens menu.
</div>
</div>
</div>
);
};

View File

@@ -0,0 +1,92 @@
import React, { FC, useMemo, useRef, useState } from "preact/compat";
import classNames from "classnames";
import { Series } from "uplot";
import { MouseEvent } from "react";
import { LegendLogHits } from "../../../../api/types";
import { getStreamPairs } from "../../../../utils/logs";
import { formatNumberShort } from "../../../../utils/math";
import Popper from "../../../Main/Popper/Popper";
import useBoolean from "../../../../hooks/useBoolean";
import LegendHitsMenu from "../LegendHitsMenu/LegendHitsMenu";
interface Props {
legend: LegendLogHits;
series: Series[];
onRedrawGraph: () => void;
onApplyFilter: (value: string) => void;
}
const BarHitsLegendItem: FC<Props> = ({ legend, series, onRedrawGraph, onApplyFilter }) => {
const {
value: openContextMenu,
setTrue: handleOpenContextMenu,
setFalse: handleCloseContextMenu,
} = useBoolean(false);
const legendRef = useRef<HTMLDivElement>(null);
const [clickPosition, setClickPosition] = useState<{ top: number; left: number } | null>(null);
const targetSeries = useMemo(() => series.find(s => s.label === legend.label), [series]);
const fields = useMemo(() => getStreamPairs(legend.label), [legend.label]);
const label = fields.join(", ");
const totalShortFormatted = formatNumberShort(legend.total);
const handleClickByStream = (e: MouseEvent<HTMLDivElement>) => {
if (!targetSeries) return;
if (e.metaKey || e.ctrlKey) {
targetSeries.show = !targetSeries.show;
} else {
const isOnlyTargetVisible = series.every(s => s === targetSeries || !s.show);
series.forEach(s => {
s.show = isOnlyTargetVisible || (s === targetSeries);
});
}
onRedrawGraph();
};
const handleContextMenu = (e: MouseEvent<HTMLDivElement>) => {
e.preventDefault();
setClickPosition({ top: e.clientY, left: e.clientX });
handleOpenContextMenu();
};
return (
<div
ref={legendRef}
className={classNames({
"vm-bar-hits-legend-item": true,
"vm-bar-hits-legend-item_other": legend.isOther,
"vm-bar-hits-legend-item_hide": !targetSeries?.show,
})}
onClick={handleClickByStream}
onContextMenu={handleContextMenu}
>
<div
className="vm-bar-hits-legend-item__marker"
style={{ backgroundColor: `${legend.stroke}` }}
/>
<div className="vm-bar-hits-legend-item__label">{label}</div>
<span className="vm-bar-hits-legend-item__total">({totalShortFormatted})</span>
<Popper
placement="fixed"
open={openContextMenu}
buttonRef={legendRef}
placementPosition={clickPosition}
onClose={handleCloseContextMenu}
>
<LegendHitsMenu
legend={legend}
fields={fields}
onApplyFilter={onApplyFilter}
onClose={handleCloseContextMenu}
/>
</Popper>
</div>
);
};
export default BarHitsLegendItem;

View File

@@ -3,16 +3,16 @@
.vm-bar-hits-legend {
display: flex;
flex-wrap: wrap;
gap: $padding-small;
padding: 0 $padding-small $padding-small;
color: $color-text;
&-item {
display: grid;
grid-template-columns: auto 1fr;
max-width: 50%;
display: flex;
align-items: center;
gap: $padding-small;
font-size: 12px;
padding: 0 $padding-small;
font-size: $font-size-small;
padding: $padding-small $padding-global;
border-radius: $border-radius-small;
cursor: pointer;
transition: 0.2s;
@@ -27,34 +27,44 @@
}
&__marker {
width: 14px;
min-width: 14px;
max-width: 14px;
height: 14px;
border: $color-background-block;
}
&-pairs {
display: flex;
gap: $padding-small;
&__label {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
&__value {
padding: $padding-small 0;
&:hover {
text-decoration: underline;
}
&:after {
content: ",";
}
&:last-child:after {
content: "";
}
}
&__total {
color: $color-text-secondary;
font-style: italic;
grid-column: 2;
}
}
&-info {
list-style-position: inside;
flex-grow: 1;
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
padding-top: $padding-small;
color: $color-text-secondary;
font-size: $font-size-small;
code {
display: inline-block;
padding: calc($padding-small / 2) $padding-small;
font-size: $font-size-small;
text-align: center;
background-color: $color-background-body;
background-repeat: repeat-x;
border: $border-divider;
border-radius: 4px;
}
}
}

View File

@@ -5,7 +5,6 @@ import "./style.scss";
import useStateSearchParams from "../../../../hooks/useStateSearchParams";
import { useSearchParams } from "react-router-dom";
import Button from "../../../Main/Button/Button";
import classNames from "classnames";
import { SettingsIcon, VisibilityIcon, VisibilityOffIcon } from "../../../Main/Icons";
import Tooltip from "../../../Main/Tooltip/Tooltip";
import Popper from "../../../Main/Popper/Popper";
@@ -24,27 +23,20 @@ const BarHitsOptions: FC<Props> = ({ onChange }) => {
setFalse: handleCloseOptions,
} = useBoolean(false);
const [graphStyle, setGraphStyle] = useStateSearchParams(GRAPH_STYLES.LINE_STEPPED, "graph");
const [stacked, setStacked] = useStateSearchParams(false, "stacked");
const [fill, setFill] = useStateSearchParams(false, "fill");
const [fill, setFill] = useStateSearchParams("true", "fill");
const [hideChart, setHideChart] = useStateSearchParams(false, "hide_chart");
const options: GraphOptions = useMemo(() => ({
graphStyle,
graphStyle: GRAPH_STYLES.BAR,
stacked,
fill,
fill: fill === "true",
hideChart,
}), [graphStyle, stacked, fill, hideChart]);
const handleChangeGraphStyle = (val: string) => () => {
setGraphStyle(val as GRAPH_STYLES);
searchParams.set("graph", val);
setSearchParams(searchParams);
};
}), [stacked, fill, hideChart]);
const handleChangeFill = (val: boolean) => {
setFill(val);
val ? searchParams.set("fill", "true") : searchParams.delete("fill");
setFill(`${val}`);
searchParams.set("fill", `${val}`);
setSearchParams(searchParams);
};
@@ -97,21 +89,6 @@ const BarHitsOptions: FC<Props> = ({ onChange }) => {
title={"Graph settings"}
>
<div className="vm-bar-hits-options-settings">
<div className="vm-bar-hits-options-settings-item vm-bar-hits-options-settings-item_list">
<p className="vm-bar-hits-options-settings-item__title">Graph style:</p>
{Object.values(GRAPH_STYLES).map(style => (
<div
key={style}
className={classNames({
"vm-list-item": true,
"vm-list-item_active": graphStyle === style,
})}
onClick={handleChangeGraphStyle(style)}
>
{style}
</div>
))}
</div>
<div className="vm-bar-hits-options-settings-item">
<Switch
label={"Stacked"}
@@ -122,7 +99,7 @@ const BarHitsOptions: FC<Props> = ({ onChange }) => {
<div className="vm-bar-hits-options-settings-item">
<Switch
label={"Fill"}
value={fill}
value={fill === "true"}
onChange={handleChangeFill}
/>
</div>

View File

@@ -11,12 +11,12 @@
&-settings {
display: grid;
align-items: flex-start;
gap: $padding-global;
min-width: 200px;
gap: $padding-global;
padding-bottom: $padding-global;
&-item {
border-bottom: $border-divider;
padding: 0 $padding-global $padding-global;
padding: 0 $padding-global;
&_list {
padding: 0;

View File

@@ -5,6 +5,7 @@ import { DATE_TIME_FORMAT } from "../../../../constants/date";
import classNames from "classnames";
import "./style.scss";
import "../../ChartTooltip/style.scss";
import { sortLogHits } from "../../../../utils/logs";
interface Props {
data: AlignedData;
@@ -26,7 +27,7 @@ const BarHitsTooltip: FC<Props> = ({ data, focusDataIdx, uPlotInst }) => {
const tooltipItems = values.map((value, i) => {
const targetSeries = series[i + 1];
const stroke = (targetSeries?.stroke as () => string)?.();
const label = targetSeries?.label || "other";
const label = targetSeries?.label;
const show = targetSeries?.show;
return {
label,
@@ -34,7 +35,7 @@ const BarHitsTooltip: FC<Props> = ({ data, focusDataIdx, uPlotInst }) => {
value,
show
};
}).filter(item => item.value > 0 && item.show).sort((a, b) => b.value - a.value);
}).filter(item => item.value > 0 && item.show).sort(sortLogHits("value"));
const point = {
top: tooltipItems[0] ? uPlotInst?.valToPos?.(tooltipItems[0].value, "y") || 0 : 0,
@@ -104,16 +105,19 @@ const BarHitsTooltip: FC<Props> = ({ data, focusDataIdx, uPlotInst }) => {
className="vm-chart-tooltip-data__marker"
style={{ background: item.stroke }}
/>
<p>
{item.label}: <b>{item.value}</b>
<p className="vm-bar-hits-tooltip-item">
<span className="vm-bar-hits-tooltip-item__label">{item.label}</span>
<span>{item.value.toLocaleString("en-US")}</span>
</p>
</div>
))}
</div>
{tooltipData.values.length > 1 && (
<div className="vm-chart-tooltip-data">
<p>
Total records: <b>{tooltipData.total}</b>
<span/>
<p className="vm-bar-hits-tooltip-item">
<span className="vm-bar-hits-tooltip-item__label">Total</span>
<span>{tooltipData.total.toLocaleString("en-US")}</span>
</p>
</div>
)}

View File

@@ -9,4 +9,19 @@
opacity: 1;
pointer-events: auto;
}
&-item {
display: grid;
grid-template-columns: 1fr auto;
align-items: center;
gap: $padding-global;
max-width: 100%;
&__label {
display: inline-block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
}

View File

@@ -0,0 +1,50 @@
import React, { FC } from "preact/compat";
import "./style.scss";
import { LegendLogHits } from "../../../../api/types";
import LegendHitsMenuStats from "./LegendHitsMenuStats";
import LegendHitsMenuBase from "./LegendHitsMenuBase";
import LegendHitsMenuRow from "./LegendHitsMenuRow";
import LegendHitsMenuFields from "./LegendHitsMenuFields";
import { LOGS_LIMIT_HITS } from "../../../../constants/logs";
const otherDescription = `aggregated results for fields not in the top ${LOGS_LIMIT_HITS}`;
interface Props {
legend: LegendLogHits;
fields: string[];
onApplyFilter: (value: string) => void;
onClose: () => void;
}
const LegendHitsMenu: FC<Props> = ({ legend, fields, onApplyFilter, onClose }) => {
return (
<div className="vm-legend-hits-menu">
<div className="vm-legend-hits-menu-section">
<LegendHitsMenuRow
className="vm-legend-hits-menu-row_info"
title={legend.isOther ? otherDescription : legend.label}
/>
</div>
{!legend.isOther && (
<LegendHitsMenuBase
legend={legend}
onApplyFilter={onApplyFilter}
onClose={onClose}
/>
)}
{!legend.isOther && (
<LegendHitsMenuFields
fields={fields}
onApplyFilter={onApplyFilter}
onClose={onClose}
/>
)}
<LegendHitsMenuStats legend={legend}/>
</div>
);
};
export default LegendHitsMenu;

View File

@@ -0,0 +1,64 @@
import React, { FC } from "preact/compat";
import LegendHitsMenuRow from "./LegendHitsMenuRow";
import useCopyToClipboard from "../../../../hooks/useCopyToClipboard";
import { CopyIcon, FilterIcon, FilterOffIcon } from "../../../Main/Icons";
import { LegendLogHits, LegendLogHitsMenu } from "../../../../api/types";
import { LOGS_GROUP_BY } from "../../../../constants/logs";
interface Props {
legend: LegendLogHits;
onApplyFilter: (value: string) => void;
onClose: () => void;
}
const LegendHitsMenuBase: FC<Props> = ({ legend, onApplyFilter, onClose }) => {
const copyToClipboard = useCopyToClipboard();
const handleAddStreamToFilter = () => {
onApplyFilter(`${LOGS_GROUP_BY}: ${legend.label}`);
onClose();
};
const handleExcludeStreamToFilter = () => {
onApplyFilter(`(NOT ${LOGS_GROUP_BY}: ${legend.label})`);
onClose();
};
const handlerCopyLabel = async () => {
await copyToClipboard(legend.label, `${legend.label} has been copied`);
onClose();
};
const options: LegendLogHitsMenu[] = [
{
title: `Copy ${LOGS_GROUP_BY} name`,
icon: <CopyIcon/>,
handler: handlerCopyLabel,
},
{
title: `Add ${LOGS_GROUP_BY} to filter`,
icon: <FilterIcon/>,
handler: handleAddStreamToFilter,
},
{
title: `Exclude ${LOGS_GROUP_BY} to filter`,
icon: <FilterOffIcon/>,
handler: handleExcludeStreamToFilter,
}
];
return (
<div className="vm-legend-hits-menu-section">
{options.map(({ icon, title, handler }) => (
<LegendHitsMenuRow
key={title}
iconStart={icon}
title={title}
handler={handler}
/>
))}
</div>
);
};
export default LegendHitsMenuBase;

View File

@@ -0,0 +1,74 @@
import React, { FC, useMemo } from "preact/compat";
import LegendHitsMenuRow from "./LegendHitsMenuRow";
import { CopyIcon, FilterIcon, FilterOffIcon } from "../../../Main/Icons";
import { convertToFieldFilter } from "../../../../utils/logs";
import { LegendLogHitsMenu } from "../../../../api/types";
import useCopyToClipboard from "../../../../hooks/useCopyToClipboard";
interface Props {
fields: string[];
onApplyFilter: (value: string) => void;
onClose: () => void;
}
const LegendHitsMenuFields: FC<Props> = ({ fields, onApplyFilter, onClose }) => {
const copyToClipboard = useCopyToClipboard();
const handleCopy = (field: string) => async () => {
await copyToClipboard(field, `${field} has been copied`);
onClose();
};
const handleAddToFilter = (field: string) => () => {
onApplyFilter(field);
onClose();
};
const handleExcludeToFilter = (field: string) => () => {
onApplyFilter(`-${field}`);
onClose();
};
const generateFieldMenu = (field: string): LegendLogHitsMenu[] => {
return [
{
title: "Copy",
icon: <CopyIcon/>,
handler: handleCopy(field),
},
{
title: "Add to filter",
icon: <FilterIcon/>,
handler: handleAddToFilter(field),
},
{
title: "Exclude to filter",
icon: <FilterOffIcon/>,
handler: handleExcludeToFilter(field),
}
];
};
const fieldsWithMenu: LegendLogHitsMenu[] = useMemo(() => {
return fields.map(field => {
const title = convertToFieldFilter(field);
return {
title,
submenu: generateFieldMenu(title),
};
});
}, [fields]);
return (
<div className="vm-legend-hits-menu-section">
{fieldsWithMenu?.map((field) => (
<LegendHitsMenuRow
key={field.title}
{...field}
/>
))}
</div>
);
};
export default LegendHitsMenuFields;

View File

@@ -0,0 +1,116 @@
import React, { FC, useRef, useState } from "preact/compat";
import classNames from "classnames";
import { ReactNode, useEffect } from "react";
import Tooltip from "../../../Main/Tooltip/Tooltip";
import { LegendLogHitsMenu } from "../../../../api/types";
import { ArrowDropDownIcon } from "../../../Main/Icons";
import useClickOutside from "../../../../hooks/useClickOutside";
interface Props {
title: string | ReactNode;
handler?: () => void;
iconStart?: ReactNode;
iconEnd?: ReactNode;
className?: string;
submenu?: LegendLogHitsMenu[];
}
const LegendHitsMenuRow: FC<Props> = ({ title, handler, iconStart, iconEnd, className, submenu }) => {
const containerRef = useRef<HTMLDivElement>(null);
const titleRef = useRef<HTMLDivElement>(null);
const submenuRef = useRef<HTMLDivElement>(null);
const [isOverflownTitle, setIsOverflownTitle] = useState(false);
const [openSubmenu, setOpenSubmenu] = useState(false);
const [posSubmenuLeft, setPosSubmenuLeft] = useState(false);
const hasSubmenu = !!submenu?.length;
const handleToggleContextMenu = () => {
setOpenSubmenu(prev => !prev);
};
const handleCloseContextMenu = () => {
setOpenSubmenu(false);
};
const handleClick = () => {
handler && handler();
hasSubmenu && handleToggleContextMenu();
};
useEffect(() => {
if (!titleRef.current) return;
setIsOverflownTitle(titleRef.current.scrollWidth > titleRef.current.clientWidth);
}, [title, titleRef]);
useEffect(() => {
requestAnimationFrame(() => {
if (!openSubmenu || !submenuRef.current) {
setPosSubmenuLeft(false);
return;
}
const { left, width } = submenuRef.current.getBoundingClientRect();
setPosSubmenuLeft(left + width > window.innerWidth);
});
}, [submenuRef, openSubmenu]);
useClickOutside(containerRef, handleCloseContextMenu);
const titleContent = (
<div
ref={titleRef}
className="vm-legend-hits-menu-row__title"
>
{title}
</div>
);
return (
<div
ref={containerRef}
className={classNames({
"vm-legend-hits-menu-row": true,
"vm-legend-hits-menu-row_interactive": !!handler || hasSubmenu,
[`${className}`]: className
})}
onClick={handleClick}
>
{iconStart && <div className="vm-legend-hits-menu-row__icon">{iconStart}</div>}
{isOverflownTitle ? (<Tooltip title={title}>{titleContent}</Tooltip>) : titleContent}
{iconEnd && !hasSubmenu && <div className="vm-legend-hits-menu-row__icon">{iconEnd}</div>}
{hasSubmenu && (
<div className="vm-legend-hits-menu-row__icon vm-legend-hits-menu-row__icon_drop">
<ArrowDropDownIcon/>
</div>
)}
{openSubmenu && submenu && (
<div
ref={submenuRef}
className={classNames({
"vm-legend-hits-menu": true,
"vm-legend-hits-menu_submenu": true,
"vm-legend-hits-menu_submenu_left": posSubmenuLeft
})}
>
<div className="vm-legend-hits-menu-section">
{submenu.map(({ icon, title, handler }) => (
<LegendHitsMenuRow
key={title}
iconStart={icon}
title={title}
handler={handler}
/>
))}
</div>
</div>
)}
</div>
);
};
export default LegendHitsMenuRow;

View File

@@ -0,0 +1,23 @@
import React, { FC } from "preact/compat";
import { LegendLogHits } from "../../../../api/types";
interface Props {
legend: LegendLogHits;
}
const LegendHitsMenuStats: FC<Props> = ({ legend }) => {
const totalFormatted = legend.total.toLocaleString("en-US");
const percentage = Math.round((legend.total / legend.totalHits) * 100);
return (
<div className="vm-legend-hits-menu-section">
<div className="vm-legend-hits-menu-row">
<div className="vm-legend-hits-menu-row__title">
Total: {totalFormatted} ({percentage}%)
</div>
</div>
</div>
);
};
export default LegendHitsMenuStats;

View File

@@ -0,0 +1,178 @@
@use "src/styles/variables" as *;
.vm-legend-hits-menu {
min-width: 160px;
z-index: 1;
&_submenu {
position: absolute;
top: calc(-1 * $padding-small);
background-color: $color-background-block;
left: calc(100% + ($padding-small / 2));
box-shadow: $box-shadow-popper;
border-radius: $border-radius-small;
animation: vm-submenu-show 150ms cubic-bezier(0.280, 0.840, 0.2, 1);
transform-origin: top left;
&_left {
left: auto;
right: calc(100% + ($padding-small / 2));
transform-origin: top right;
}
}
&-section {
border-bottom: $border-divider;
&:last-child {
border-bottom: none;
}
}
&-row {
position: relative;
display: flex;
gap: $padding-small;
align-items: center;
justify-content: flex-start;
padding: 0 $padding-global;
transition: background-color 0.3s;
color: $color-text;
&_interactive {
cursor: pointer;
&:hover {
background-color: rgba(0, 0, 0, 0.05);
}
}
&_info {
font-size: $font-size-small;
font-weight: 500;
padding-block: $padding-small;
}
&_info &__icon {
color: $color-info;
}
&__icon {
display: flex;
align-items: center;
justify-content: center;
width: 14px;
height: 14px;
&_drop {
transform: rotate(-90deg);
}
}
&__title {
flex-grow: 1;
padding: $padding-global 0;
position: relative;
max-width: 400px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
&-other-list {
width: 80vw;
height: 80vh;
overflow: auto;
&__search {
position: sticky;
top: 0;
padding: $padding-small 0;
background-color: $color-background-block;
border-bottom: $border-divider;
z-index: 2;
}
&-row {
border-bottom: $border-divider;
&_header {
border-bottom: none;
position: sticky;
top: 65px;
background-color: $color-background-block;
z-index: 1;
width: 100%;
&:after {
content: '';
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 1px;
border-bottom: $border-divider;
}
}
}
&-cell {
padding: calc($padding-small / 2) 0;
text-align: left;
&_header {
padding: $padding-small;
font-weight: 500;
}
&_number {
padding: $padding-small;
text-align: right;
font-variant-numeric: tabular-nums;
}
&_fields {
width: 100%;
}
}
&-fields {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: flex-start;
&__field {
padding: calc($padding-small / 2) $padding-small;
border-radius: $border-radius-small;
transition: background-color 0.3s;
&:hover {
background-color: $color-hover-black;
}
&:not(:last-child) {
&:after {
content: ',';
}
}
}
}
&-actions {
display: flex;
align-items: center;
justify-content: center;
}
}
}
@keyframes vm-submenu-show {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}

View File

@@ -36,6 +36,14 @@ interface UseGetBarHitsOptionsArgs {
graphOptions: GraphOptions;
}
export const OTHER_HITS_LABEL = "other";
export const getLabelFromLogHit = (logHit: LogHits) => {
if (logHit?._isOther) return OTHER_HITS_LABEL;
const fields = Object.values(logHit?.fields || {});
return fields.map((value) => value || "\"\"").join(", ");
};
const useBarHitsOptions = ({
data,
logHits,
@@ -59,12 +67,12 @@ const useBarHitsOptions = ({
let colorN = 0;
return data.map((_d, i) => {
if (i === 0) return {}; // 0 index is xAxis(timestamps)
const fields = Object.values(logHits?.[i - 1]?.fields || {});
const label = fields.map((value) => value || "\"\"").join(", ");
const color = getCssVariable(label ? seriesColors[colorN] : "color-log-hits-bar-0");
if (label) colorN++;
const target = logHits?.[i - 1];
const label = getLabelFromLogHit(target);
const color = getCssVariable(target?._isOther ? "color-log-hits-bar-0" : seriesColors[colorN]);
if (!target?._isOther) colorN++;
return {
label: label || "other",
label,
width: strokeWidth[graphOptions.graphStyle],
spanGaps: true,
stroke: color,

View File

@@ -32,6 +32,11 @@ $chart-tooltip-y: -1 * ($padding-global + $chart-tooltip-half-icon);
max-width: calc(100vw/3);
}
&_hits &-data {
display: grid;
grid-template-columns: $font-size 1fr;
}
&_sticky {
pointer-events: auto;
z-index: 99;
@@ -90,6 +95,8 @@ $chart-tooltip-y: -1 * ($padding-global + $chart-tooltip-half-icon);
}
&__marker {
min-width: $font-size;
max-width: $font-size;
width: $font-size;
height: $font-size;
border: 1px solid rgba($color-white, 0.5);

View File

@@ -0,0 +1,154 @@
import React, { FC, useCallback, useEffect, useMemo, useState } from "preact/compat";
import Autocomplete, { AutocompleteOptions } from "../../../Main/Autocomplete/Autocomplete";
import { AUTOCOMPLETE_LIMITS } from "../../../../constants/queryAutocomplete";
import { QueryEditorAutocompleteProps } from "../QueryEditor";
import { getContextData, splitLogicalParts } from "./parser";
import { ContextType, LogicalPart, LogicalPartType } from "./types";
import { useFetchLogsQLOptions } from "./useFetchLogsQLOptions";
import { pipeList } from "./pipes";
const LogsQueryEditorAutocomplete: FC<QueryEditorAutocompleteProps> = ({
value,
anchorEl,
caretPosition,
hasHelperText,
onSelect,
onFoundOptions
}) => {
const [offsetPos, setOffsetPos] = useState({ top: 0, left: 0 });
const fullValue = useMemo(() => {
if (caretPosition[0] !== caretPosition[1]) return { valueBeforeCursor: value, valueAfterCursor: "" };
const valueBeforeCursor = value.substring(0, caretPosition[0]);
const valueAfterCursor = value.substring(caretPosition[1]);
return { valueBeforeCursor, valueAfterCursor };
}, [value, caretPosition]);
const logicalParts = useMemo(() => {
return splitLogicalParts(value);
}, [value]);
const contextData = useMemo(() => {
if (caretPosition[0] !== caretPosition[1]) return;
const part = logicalParts.find(p => caretPosition[0] >= p.position[0] && caretPosition[0] <= p.position[1]);
if (!part) return;
const cursorStartPosition = caretPosition[0] - part.position[0];
return {
...part,
...getContextData(part, cursorStartPosition)
};
}, [logicalParts, caretPosition]);
const { fieldNames, fieldValues, loading } = useFetchLogsQLOptions(contextData);
const options = useMemo(() => {
switch (contextData?.contextType) {
case ContextType.FilterName:
case ContextType.FilterUnknown:
return fieldNames;
case ContextType.FilterValue:
return fieldValues;
case ContextType.PipeName:
return pipeList;
default:
return [];
}
}, [contextData, fieldNames, fieldValues]);
const getUpdatedValue = (insertValue: string, logicalParts: LogicalPart[], id?: number) => {
return logicalParts.reduce((acc, part) => {
const value = part.id === id ? insertValue : part.value;
const separator = part.type === LogicalPartType.Pipe ? " | " : " ";
return `${acc}${separator}${value}`;
}, "").trim();
};
const getModifyInsert = (insert: string, contextType: ContextType, value = "", insertType?: string) => {
let modifiedInsert = insert;
if (insertType === ContextType.FilterName) {
modifiedInsert += ":";
} else if (contextType === ContextType.FilterValue) {
const insertWithQuotes = value.startsWith("_stream:") ? modifiedInsert : `"${modifiedInsert}"`;
modifiedInsert = `${contextData?.filterName || ""}:${insertWithQuotes}`;
}
return modifiedInsert;
};
const handleSelect = useCallback((insert: string, item: AutocompleteOptions) => {
const {
id,
contextType = ContextType.FilterUnknown,
value = "",
position = [0, 0]
} = contextData || {};
const insertValue = getModifyInsert(insert, contextType, value, item.type);
const newValue = getUpdatedValue(insertValue, logicalParts, id);
const updatedPosition = (position[0] || 1) + insertValue.length + (item.type === ContextType.PipeName ? 1 : 0);
onSelect(newValue, updatedPosition);
}, [contextData, logicalParts]);
useEffect(() => {
if (!anchorEl.current) {
setOffsetPos({ top: 0, left: 0 });
return;
}
const element = anchorEl.current.querySelector("textarea") || anchorEl.current;
const style = window.getComputedStyle(element);
const fontSize = `${style.getPropertyValue("font-size")}`;
const fontFamily = `${style.getPropertyValue("font-family")}`;
const lineHeight = parseInt(`${style.getPropertyValue("line-height")}`);
const span = document.createElement("div");
span.style.font = `${fontSize} ${fontFamily}`;
span.style.padding = style.getPropertyValue("padding");
span.style.lineHeight = `${lineHeight}px`;
span.style.width = `${element.offsetWidth}px`;
span.style.maxWidth = `${element.offsetWidth}px`;
span.style.whiteSpace = style.getPropertyValue("white-space");
span.style.overflowWrap = style.getPropertyValue("overflow-wrap");
const marker = document.createElement("span");
span.appendChild(document.createTextNode(fullValue.valueBeforeCursor || ""));
span.appendChild(marker);
span.appendChild(document.createTextNode(fullValue.valueAfterCursor || ""));
document.body.appendChild(span);
const spanRect = span.getBoundingClientRect();
const markerRect = marker.getBoundingClientRect();
const leftOffset = markerRect.left - spanRect.left;
const topOffset = markerRect.bottom - spanRect.bottom - (hasHelperText ? lineHeight : 0);
setOffsetPos({ top: topOffset, left: leftOffset });
span.remove();
marker.remove();
}, [anchorEl, caretPosition, hasHelperText, fullValue]);
return (
<>
<Autocomplete
loading={loading}
disabledFullScreen
value={contextData?.valueContext || ""}
options={options}
anchor={anchorEl}
minLength={0}
offset={offsetPos}
onSelect={handleSelect}
onFoundOptions={onFoundOptions}
maxDisplayResults={{
limit: AUTOCOMPLETE_LIMITS.displayResults,
message: "Please, specify the query more precisely."
}}
/>
</>
);
};
export default LogsQueryEditorAutocomplete;

View File

@@ -0,0 +1,117 @@
import { ContextData, ContextType, LogicalPart, LogicalPartPosition, LogicalPartType } from "./types";
import { pipeList } from "./pipes";
const BUILDER_OPERATORS = ["AND", "OR", "NOT"];
const PIPE_NAMES = pipeList.map(p => p.value);
export const splitLogicalParts = (expr: string) => {
// Replace spaces around the colon (:) with just the colon, removing the spaces
const input = expr; //.replace(/\s*:\s*/g, ":");
const parts: LogicalPart[] = [];
let currentPart = "";
let isPipePart = false;
const quotes = ["'", "\"", "`"];
let insideQuotes = false;
let expectedQuote = "";
const openBrackets = ["(", "[", "{"];
const closeBrackets = [")", "]", "}"];
const brackets = [...openBrackets, ...closeBrackets];
let insideBrackets = 0;
let startIndex = 0;
for (let i = 0; i < input.length; i++) {
const char = input[i];
// Check if the current character is a quote
if (quotes.includes(char)) {
const isClosedQuote: boolean = insideQuotes && (char === expectedQuote);
insideQuotes = !isClosedQuote;
expectedQuote = isClosedQuote ? "" : char;
}
// Check if the current character is a bracket
if (!insideQuotes && brackets.includes(char)) {
const dir = openBrackets.includes(char) ? 1 : -1;
insideBrackets += dir;
}
// Check if the current character is a pipe
if ((!insideQuotes && !insideBrackets && char === "|")) {
isPipePart = true;
const countStartSpaces = currentPart.match(/^ */)?.[0].length || 0;
const countEndSpaces = currentPart.match(/ *$/)?.[0].length || 0;
pushPart(currentPart, true, [startIndex + countStartSpaces, i - countEndSpaces - 1], parts);
currentPart = "";
startIndex = i + 1;
continue;
}
// Check if the current character is a space
if (!isPipePart && !insideQuotes && !insideBrackets && char === " ") {
const nextStr = input.slice(i).replace(/^\s*/, "");
const prevStr = input.slice(0, i).replace(/\s*$/, "");
if (!nextStr.startsWith(":") && !prevStr.endsWith(":")) {
pushPart(currentPart, false, [startIndex, i - 1], parts);
currentPart = "";
startIndex = i + 1;
continue;
}
}
currentPart += char;
}
// push the last part
pushPart(currentPart, isPipePart, [startIndex, input.length], parts);
return parts;
};
const pushPart = (currentPart: string, isPipePart: boolean, position: LogicalPartPosition, parts: LogicalPart[]) => {
const trimmedPart = currentPart.trim();
if (!trimmedPart) return;
const isOperator = BUILDER_OPERATORS.includes(trimmedPart.toUpperCase());
parts.push({
id: parts.length,
value: trimmedPart,
position,
type: isPipePart
? LogicalPartType.Pipe
: isOperator ? LogicalPartType.Operator : LogicalPartType.Filter,
});
};
export const getContextData = (part: LogicalPart, cursorPos: number) => {
const valueBeforeCursor = part.value.substring(0, cursorPos);
const valueAfterCursor = part.value.substring(cursorPos);
const metaData: ContextData = {
valueBeforeCursor,
valueAfterCursor,
valueContext: part.value,
contextType: ContextType.Unknown,
};
if (part.type === LogicalPartType.Filter) {
const noColon = !valueBeforeCursor.includes(":") && !valueAfterCursor.includes(":");
if (noColon) {
metaData.contextType = ContextType.FilterUnknown;
} else if (valueBeforeCursor.includes(":")) {
const [filterName, filterValue] = valueBeforeCursor.split(":");
metaData.contextType = ContextType.FilterValue;
metaData.filterName = filterName;
metaData.valueContext = filterValue;
} else {
metaData.contextType = ContextType.FilterName;
}
} else if (part.type === LogicalPartType.Pipe) {
const valueStartWithPipe = PIPE_NAMES.some(p => part.value.startsWith(p));
metaData.contextType = valueStartWithPipe ? ContextType.PipeValue : ContextType.PipeName;
}
metaData.valueContext = metaData.valueContext.replace(/^["']|["']$/g, "");
return metaData;
};

View File

@@ -0,0 +1,130 @@
import React from "react";
import { ContextType } from "./types";
import { FunctionIcon } from "../../../Main/Icons";
const docsUrl = "https://docs.victoriametrics.com/victorialogs/logsql";
const classLink = "vm-link vm-link_colored";
const prepareDescription = (text: string): string => {
const replaceClass = `$1 target="_blank" class="${classLink}" $2`;
const replaceHref = `$1 $2${docsUrl}#`;
return text
.replace(/(<a) (href=")#/gm, replaceHref)
.replace(/(<a) (href="[^"]+")/gm, replaceClass);
};
export const pipeList = [
{
"value": "copy",
"description": "<a href=\"#copy-pipe\"><code>copy</code></a> copies <a href=\"https://docs.victoriametrics.com/victorialogs/keyconcepts/#data-model\">log fields</a>."
},
{
"value": "delete",
"description": "<a href=\"#delete-pipe\"><code>delete</code></a> deletes <a href=\"https://docs.victoriametrics.com/victorialogs/keyconcepts/#data-model\">log fields</a>."
},
{
"value": "drop_empty_fields",
"description": "<a href=\"#drop_empty_fields-pipe\"><code>drop_empty_fields</code></a> drops <a href=\"https://docs.victoriametrics.com/victorialogs/keyconcepts/#data-model\">log fields</a> with empty values."
},
{
"value": "extract",
"description": "<a href=\"#extract-pipe\"><code>extract</code></a> extracts the specified text into the given log fields."
},
{
"value": "extract_regexp",
"description": "<a href=\"#extract_regexp-pipe\"><code>extract_regexp</code></a> extracts the specified text into the given log fields via <a href=\"https://github.com/google/re2/wiki/Syntax\" rel=\"external\" target=\"_blank\">RE2 regular expressions</a>."
},
{
"value": "field_names",
"description": "<a href=\"#field_names-pipe\"><code>field_names</code></a> returns all the names of <a href=\"https://docs.victoriametrics.com/victorialogs/keyconcepts/#data-model\">log fields</a>."
},
{
"value": "field_values",
"description": "<a href=\"#field_values-pipe\"><code>field_values</code></a> returns all the values for the given <a href=\"https://docs.victoriametrics.com/victorialogs/keyconcepts/#data-model\">log field</a>."
},
{
"value": "fields",
"description": "<a href=\"#fields-pipe\"><code>fields</code></a> selects the given set of <a href=\"https://docs.victoriametrics.com/victorialogs/keyconcepts/#data-model\">log fields</a>."
},
{
"value": "filter",
"description": "<a href=\"#filter-pipe\"><code>filter</code></a> applies additional <a href=\"#filters\">filters</a> to results."
},
{
"value": "format",
"description": "<a href=\"#format-pipe\"><code>format</code></a> formats output field from input <a href=\"https://docs.victoriametrics.com/victorialogs/keyconcepts/#data-model\">log fields</a>."
},
{
"value": "limit",
"description": "<a href=\"#limit-pipe\"><code>limit</code></a> limits the number selected logs."
},
{
"value": "math",
"description": "<a href=\"#math-pipe\"><code>math</code></a> performs mathematical calculations over <a href=\"https://docs.victoriametrics.com/victorialogs/keyconcepts/#data-model\">log fields</a>."
},
{
"value": "offset",
"description": "<a href=\"#offset-pipe\"><code>offset</code></a> skips the given number of selected logs."
},
{
"value": "pack_json",
"description": "<a href=\"#pack_json-pipe\"><code>pack_json</code></a> packs <a href=\"https://docs.victoriametrics.com/victorialogs/keyconcepts/#data-model\">log fields</a> into JSON object."
},
{
"value": "pack_logfmt",
"description": "<a href=\"#pack_logfmt-pipe\"><code>pack_logfmt</code></a> packs <a href=\"https://docs.victoriametrics.com/victorialogs/keyconcepts/#data-model\">log fields</a> into <a href=\"https://brandur.org/logfmt\" rel=\"external\" target=\"_blank\">logfmt</a> message."
},
{
"value": "rename",
"description": "<a href=\"#rename-pipe\"><code>rename</code></a> renames <a href=\"https://docs.victoriametrics.com/victorialogs/keyconcepts/#data-model\">log fields</a>."
},
{
"value": "replace",
"description": "<a href=\"#replace-pipe\"><code>replace</code></a> replaces substrings in the specified <a href=\"https://docs.victoriametrics.com/victorialogs/keyconcepts/#data-model\">log fields</a>."
},
{
"value": "replace_regexp",
"description": "<a href=\"#replace_regexp-pipe\"><code>replace_regexp</code></a> updates <a href=\"https://docs.victoriametrics.com/victorialogs/keyconcepts/#data-model\">log fields</a> with regular expressions."
},
{
"value": "sort",
"description": "<a href=\"#sort-pipe\"><code>sort</code></a> sorts logs by the given <a href=\"https://docs.victoriametrics.com/victorialogs/keyconcepts/#data-model\">fields</a>."
},
{
"value": "stats",
"description": "<a href=\"#stats-pipe\"><code>stats</code></a> calculates various stats over the selected logs."
},
{
"value": "stream_context",
"description": "<a href=\"#stream_context-pipe\"><code>stream_context</code></a> allows selecting surrounding logs in front and after the matching logs\nper each <a href=\"/victorialogs/keyconcepts/#stream-fields\">log stream</a>."
},
{
"value": "top",
"description": "<a href=\"#top-pipe\"><code>top</code></a> returns top <code>N</code> field sets with the maximum number of matching logs."
},
{
"value": "uniq",
"description": "<a href=\"#uniq-pipe\"><code>uniq</code></a> returns unique log entires."
},
{
"value": "unpack_json",
"description": "<a href=\"#unpack_json-pipe\"><code>unpack_json</code></a> unpacks JSON messages from <a href=\"https://docs.victoriametrics.com/victorialogs/keyconcepts/#data-model\">log fields</a>."
},
{
"value": "unpack_logfmt",
"description": "<a href=\"#unpack_logfmt-pipe\"><code>unpack_logfmt</code></a> unpacks <a href=\"https://brandur.org/logfmt\" rel=\"external\" target=\"_blank\">logfmt</a> messages from <a href=\"https://docs.victoriametrics.com/victorialogs/keyconcepts/#data-model\">log fields</a>."
},
{
"value": "unpack_syslog",
"description": "<a href=\"#unpack_syslog-pipe\"><code>unpack_syslog</code></a> unpacks <a href=\"https://en.wikipedia.org/wiki/Syslog\" rel=\"external\" target=\"_blank\">syslog</a> messages from <a href=\"https://docs.victoriametrics.com/victorialogs/keyconcepts/#data-model\">log fields</a>."
},
{
"value": "unroll",
"description": "<a href=\"#unroll-pipe\"><code>unroll</code></a> unrolls JSON arrays from <a href=\"https://docs.victoriametrics.com/victorialogs/keyconcepts/#data-model\">log fields</a>."
}
].map(item => ({
...item,
type: ContextType.PipeName,
icon: <FunctionIcon/>,
description: prepareDescription(item.description),
}));

View File

@@ -0,0 +1,31 @@
export enum LogicalPartType {
Filter = "Filter",
Pipe = "Pipe",
Operator = "Operator",
}
export type LogicalPartPosition = [start: number, end: number];
export interface LogicalPart {
id: number;
value: string;
type: LogicalPartType;
position: LogicalPartPosition;
}
export interface ContextData {
valueBeforeCursor: string;
valueAfterCursor: string;
contextType: ContextType;
valueContext: string;
filterName?: string;
}
export enum ContextType {
FilterName = "FilterName",
FilterUnknown = "FilterUnknown",
FilterValue = "FilterValue",
PipeName = "Pipes",
PipeValue = "PipeValue",
Unknown = "Unknown",
}

View File

@@ -0,0 +1,137 @@
import React, { useEffect, useState, useRef, Dispatch, SetStateAction } from "preact/compat";
import dayjs from "dayjs";
import { ContextData, ContextType } from "./types";
import { FunctionIcon, LabelIcon, MetricIcon, ValueIcon } from "../../../Main/Icons";
import { AutocompleteOptions } from "../../../Main/Autocomplete/Autocomplete";
import { useAppState } from "../../../../state/common/StateContext";
import { useTimeState } from "../../../../state/time/TimeStateContext";
import { useCallback } from "react";
import { AUTOCOMPLETE_LIMITS } from "../../../../constants/queryAutocomplete";
import { LogsFiledValues } from "../../../../api/types";
import { useLogsDispatch, useLogsState } from "../../../../state/logsPanel/LogsStateContext";
type FetchDataArgs = {
urlSuffix: string;
setter: Dispatch<SetStateAction<AutocompleteOptions[]>>
type: ContextType;
params?: URLSearchParams;
}
const icons = {
[ContextType.FilterName]: <MetricIcon/>,
[ContextType.FilterUnknown]: <MetricIcon/>,
[ContextType.FilterValue]: <ValueIcon/>,
[ContextType.PipeName]: <FunctionIcon/>,
[ContextType.PipeValue]: <LabelIcon/>,
[ContextType.Unknown]: <ValueIcon/>
};
export const useFetchLogsQLOptions = (contextData?: ContextData) => {
const { serverUrl } = useAppState();
const { period: { start, end } } = useTimeState();
const { autocompleteCache } = useLogsState();
const dispatch = useLogsDispatch();
const [loading, setLoading] = useState(false);
const [fieldNames, setFieldNames] = useState<AutocompleteOptions[]>([]);
const [fieldValues, setFieldValues] = useState<AutocompleteOptions[]>([]);
const abortControllerRef = useRef(new AbortController());
const getQueryParams = useCallback((params?: Record<string, string>) => {
const startDay = dayjs(start * 1000).startOf("day").valueOf() / 1000;
const endDay = dayjs(end * 1000).endOf("day").valueOf() / 1000;
return new URLSearchParams({
...(params || {}),
limit: `${AUTOCOMPLETE_LIMITS.queryLimit}`,
start: `${startDay}`,
end: `${endDay}`
});
}, [start, end]);
const processData = (values: LogsFiledValues[], type: ContextType): AutocompleteOptions[] => {
return values.map(v => ({
value: v.value,
type: `${type}`,
icon: icons[type]
}));
};
const fetchData = async ({ urlSuffix, setter, type, params }: FetchDataArgs) => {
// if (!value && type === TypeData.metric) return;
abortControllerRef.current.abort();
abortControllerRef.current = new AbortController();
const { signal } = abortControllerRef.current;
const key = `${urlSuffix}?${params?.toString()}`;
setLoading(true);
try {
const cachedData = autocompleteCache.get(key);
if (cachedData) {
setter(processData(cachedData, type));
setLoading(false);
return;
}
const response = await fetch(`${serverUrl}/select/logsql/${urlSuffix}?${params}`, { signal });
if (response.ok) {
const data = await response.json();
const value = (data?.values || []) as LogsFiledValues[];
setter(value ? processData(value, type) : []);
dispatch({ type: "SET_AUTOCOMPLETE_CACHE", payload: { key, value } });
}
setLoading(false);
} catch (e) {
if (e instanceof Error && e.name !== "AbortError") {
dispatch({ type: "SET_AUTOCOMPLETE_CACHE", payload: { key, value: [] } });
setLoading(false);
console.error(e);
}
}
};
// fetch field names
useEffect(() => {
const validContexts = [ContextType.FilterName, ContextType.FilterUnknown];
const isInvalidContext = !validContexts.includes(contextData?.contextType || ContextType.Unknown);
if (!serverUrl || isInvalidContext) {
return;
}
setFieldNames([]);
fetchData({
urlSuffix: "field_names",
setter: setFieldNames,
type: ContextType.FilterName,
params: getQueryParams({ query: "*" })
});
return () => abortControllerRef.current?.abort();
}, [serverUrl, contextData]);
// fetch field values
useEffect(() => {
const isInvalidContext = contextData?.contextType !== ContextType.FilterValue;
if (!serverUrl || isInvalidContext || !contextData?.filterName) {
return;
}
setFieldValues([]);
fetchData({
urlSuffix: "field_values",
setter: setFieldValues,
type: ContextType.FilterValue,
params: getQueryParams({ query: "*", field: contextData.filterName })
});
return () => abortControllerRef.current?.abort();
}, [serverUrl, contextData]);
return {
fieldNames,
fieldValues,
loading,
};
};

View File

@@ -2,7 +2,6 @@ import React, { FC, useEffect, useRef, useState } from "preact/compat";
import { KeyboardEvent } from "react";
import { ErrorTypes } from "../../../types";
import TextField from "../../Main/TextField/TextField";
import QueryEditorAutocomplete from "./QueryEditorAutocomplete";
import "./style.scss";
import { QueryStats } from "../../../api/types";
import { partialWarning, seriesFetchedWarning } from "./warningText";
@@ -11,6 +10,16 @@ import useDeviceDetect from "../../../hooks/useDeviceDetect";
import { useQueryState } from "../../../state/query/QueryStateContext";
import debounce from "lodash.debounce";
export interface QueryEditorAutocompleteProps {
value: string;
anchorEl: React.RefObject<HTMLInputElement>;
caretPosition: [number, number]; // [start, end]
hasHelperText: boolean;
includeFunctions: boolean;
onSelect: (val: string, caretPosition: number) => void;
onFoundOptions: (val: AutocompleteOptions[]) => void;
}
export interface QueryEditorProps {
onChange: (query: string) => void;
onEnter: () => void;
@@ -19,6 +28,7 @@ export interface QueryEditorProps {
value: string;
oneLiner?: boolean;
autocomplete: boolean;
autocompleteEl?: FC<QueryEditorAutocompleteProps>;
error?: ErrorTypes | string;
stats?: QueryStats;
label: string;
@@ -33,6 +43,7 @@ const QueryEditor: FC<QueryEditorProps> = ({
onArrowUp,
onArrowDown,
autocomplete,
autocompleteEl: AutocompleteEl,
error,
stats,
label,
@@ -47,7 +58,7 @@ const QueryEditor: FC<QueryEditorProps> = ({
const [caretPositionInput, setCaretPositionInput] = useState<[number, number]>([0, 0]);
const autocompleteAnchorEl = useRef<HTMLInputElement>(null);
const [showAutocomplete, setShowAutocomplete] = useState(autocomplete);
const [showAutocomplete, setShowAutocomplete] = useState(!!AutocompleteEl);
const debouncedSetShowAutocomplete = useRef(debounce(setShowAutocomplete, 500)).current;
const warning = [
@@ -113,7 +124,7 @@ const QueryEditor: FC<QueryEditorProps> = ({
};
useEffect(() => {
setOpenAutocomplete(autocomplete);
setOpenAutocomplete(!!AutocompleteEl && autocompleteQuick);
}, [autocompleteQuick]);
useEffect(() => {
@@ -140,8 +151,8 @@ const QueryEditor: FC<QueryEditorProps> = ({
inputmode={"search"}
caretPosition={caretPositionInput}
/>
{showAutocomplete && autocomplete && (
<QueryEditorAutocomplete
{showAutocomplete && autocomplete && AutocompleteEl && (
<AutocompleteEl
value={value}
anchorEl={autocompleteAnchorEl}
caretPosition={caretPositionAutocomplete}

View File

@@ -1,20 +1,11 @@
import React, { FC, useState, useEffect, useMemo, useCallback } from "preact/compat";
import Autocomplete, { AutocompleteOptions } from "../../Main/Autocomplete/Autocomplete";
import Autocomplete from "../../Main/Autocomplete/Autocomplete";
import { useFetchQueryOptions } from "../../../hooks/useFetchQueryOptions";
import { escapeRegexp, hasUnclosedQuotes } from "../../../utils/regexp";
import useGetMetricsQL from "../../../hooks/useGetMetricsQL";
import { QueryContextType } from "../../../types";
import { AUTOCOMPLETE_LIMITS } from "../../../constants/queryAutocomplete";
interface QueryEditorAutocompleteProps {
value: string;
anchorEl: React.RefObject<HTMLElement>;
caretPosition: [number, number]; // [start, end]
hasHelperText: boolean;
includeFunctions: boolean;
onSelect: (val: string, caretPosition: number) => void;
onFoundOptions: (val: AutocompleteOptions[]) => void;
}
import { QueryEditorAutocompleteProps } from "./QueryEditor";
const QueryEditorAutocomplete: FC<QueryEditorAutocompleteProps> = ({
value,

View File

@@ -0,0 +1,246 @@
import React, { FC, useMemo, useState } from "preact/compat";
import useBoolean from "../../../hooks/useBoolean";
import { RestartIcon, SettingsIcon } from "../../Main/Icons";
import Button from "../../Main/Button/Button";
import Modal from "../../Main/Modal/Modal";
import Tooltip from "../../Main/Tooltip/Tooltip";
import { Logs } from "../../../api/types";
import Select from "../../Main/Select/Select";
import { useSearchParams } from "react-router-dom";
import "./style.scss";
import Switch from "../../Main/Switch/Switch";
import TextField from "../../Main/TextField/TextField";
import dayjs from "dayjs";
import Hyperlink from "../../Main/Hyperlink/Hyperlink";
import {
LOGS_DISPLAY_FIELDS,
LOGS_GROUP_BY,
LOGS_DATE_FORMAT,
LOGS_URL_PARAMS,
WITHOUT_GROUPING
} from "../../../constants/logs";
const {
GROUP_BY,
NO_WRAP_LINES,
COMPACT_GROUP_HEADER,
DISPLAY_FIELDS,
DATE_FORMAT
} = LOGS_URL_PARAMS;
const title = "Group view settings";
interface Props {
logs: Logs[];
}
const GroupLogsConfigurators: FC<Props> = ({ logs }) => {
const [searchParams, setSearchParams] = useSearchParams();
const groupBy = searchParams.get(GROUP_BY) || LOGS_GROUP_BY;
const noWrapLines = searchParams.get(NO_WRAP_LINES) === "true";
const compactGroupHeader = searchParams.get(COMPACT_GROUP_HEADER) === "true";
const displayFieldsString = searchParams.get(DISPLAY_FIELDS) || "";
const displayFields = displayFieldsString ? displayFieldsString.split(",") : [];
const [dateFormat, setDateFormat] = useState(searchParams.get(DATE_FORMAT) || LOGS_DATE_FORMAT);
const [errorFormat, setErrorFormat] = useState("");
const isGroupChanged = groupBy !== LOGS_GROUP_BY;
const isDisplayFieldsChanged = displayFields.length > 0;
const isTimeChanged = searchParams.get(DATE_FORMAT) !== LOGS_DATE_FORMAT;
const hasChanges = [
isGroupChanged,
isDisplayFieldsChanged,
noWrapLines,
compactGroupHeader,
isTimeChanged
].some(Boolean);
const logsKeys = useMemo(() => {
const excludeKeys = ["_msg", "_time"];
const uniqKeys = Array.from(new Set(logs.map(l => Object.keys(l)).flat()));
return uniqKeys.filter(k => !excludeKeys.includes(k));
}, [logs]);
const {
value: openModal,
toggle: toggleOpen,
setFalse: handleClose,
} = useBoolean(false);
const handleSelectGroupBy = (key: string) => {
searchParams.set(GROUP_BY, key);
setSearchParams(searchParams);
};
const handleSelectDisplayField = (value: string) => {
const prev = displayFields;
const newDisplayFields = prev.includes(value) ? prev.filter(v => v !== value) : [...prev, value];
searchParams.set(DISPLAY_FIELDS, newDisplayFields.join(","));
setSearchParams(searchParams);
};
const handleResetDisplayFields = () => {
searchParams.delete(DISPLAY_FIELDS);
setSearchParams(searchParams);
};
const toggleWrapLines = () => {
searchParams.set(NO_WRAP_LINES, String(!noWrapLines));
setSearchParams(searchParams);
};
const toggleCompactGroupHeader = () => {
searchParams.set(COMPACT_GROUP_HEADER, String(!compactGroupHeader));
setSearchParams(searchParams);
};
const handleChangeDateFormat = (format: string) => {
const date = new Date();
if (!dayjs(date, format, true).isValid()) {
setErrorFormat("Invalid date format");
}
setDateFormat(format);
};
const handleSaveAndClose = () => {
searchParams.set(DATE_FORMAT, dateFormat);
setSearchParams(searchParams);
handleClose();
};
const tooltipContent = () => {
if (!hasChanges) return title;
return (
<div className="vm-group-logs-configurator__tooltip">
<p>{title}</p>
<hr/>
<ul>
{isGroupChanged && <li>Group by <code>{`"${groupBy}"`}</code></li>}
{isDisplayFieldsChanged && <li>Display fields: {displayFields.length || 1}</li>}
{noWrapLines && <li>Single-line text is enabled</li>}
{compactGroupHeader && <li>Compact group header is enabled</li>}
{isTimeChanged && <li>Date format: <code>{dateFormat}</code></li>}
</ul>
</div>
);
};
return (
<>
<div className="vm-group-logs-configurator-button">
<Tooltip title={tooltipContent()}>
<Button
variant="text"
startIcon={<SettingsIcon/>}
onClick={toggleOpen}
ariaLabel={title}
/>
</Tooltip>
{hasChanges && <span className="vm-group-logs-configurator-button__marker"/>}
</div>
{openModal && (
<Modal
title={title}
onClose={handleSaveAndClose}
>
<div className="vm-group-logs-configurator">
<div className="vm-group-logs-configurator-item">
<Select
value={groupBy}
list={[WITHOUT_GROUPING, ...logsKeys]}
label="Group by field"
placeholder="Group by field"
onChange={handleSelectGroupBy}
searchable
/>
<Tooltip title={"Reset grouping"}>
<Button
variant="text"
color="primary"
startIcon={<RestartIcon/>}
onClick={() => handleSelectGroupBy(LOGS_GROUP_BY)}
/>
</Tooltip>
<span className="vm-group-logs-configurator-item__info">
Select a field to group logs by (default: <code>{LOGS_GROUP_BY}</code>).
</span>
</div>
<div className="vm-group-logs-configurator-item">
<Select
value={displayFields}
list={logsKeys}
label="Display fields"
placeholder="Display fields"
onChange={handleSelectDisplayField}
searchable
/>
<Tooltip title={"Clear fields"}>
<Button
variant="text"
color="primary"
startIcon={<RestartIcon/>}
onClick={handleResetDisplayFields}
/>
</Tooltip>
<span className="vm-group-logs-configurator-item__info">
Select fields to display instead of the message (default: <code>{LOGS_DISPLAY_FIELDS}</code>).
</span>
</div>
<div className="vm-group-logs-configurator-item">
<TextField
autofocus
label="Date format"
value={dateFormat}
onChange={handleChangeDateFormat}
error={errorFormat}
/>
<Tooltip title={"Reset format"}>
<Button
variant="text"
color="primary"
startIcon={<RestartIcon/>}
onClick={() => setDateFormat(LOGS_DATE_FORMAT)}
/>
</Tooltip>
<span className="vm-group-logs-configurator-item__info vm-group-logs-configurator-item__info_input">
Set the date format (e.g., <code>YYYY-MM-DD HH:mm:ss</code>).
Learn more in <Hyperlink
href="https://day.js.org/docs/en/display/format"
>this documentation</Hyperlink>. <br/>
Your current date format: <code>{dayjs().format(dateFormat || LOGS_DATE_FORMAT)}</code>
</span>
</div>
<div className="vm-group-logs-configurator-item">
<Switch
value={noWrapLines}
onChange={toggleWrapLines}
label="Single-line message"
/>
<span className="vm-group-logs-configurator-item__info">
Displays message in a single line and truncates it with an ellipsis if it exceeds the available space
</span>
</div>
<div className="vm-group-logs-configurator-item">
<Switch
value={compactGroupHeader}
onChange={toggleCompactGroupHeader}
label="Compact group header"
/>
<span className="vm-group-logs-configurator-item__info">
Shows group headers in one line with a &quot;+N more&quot; badge for extra fields.
</span>
</div>
</div>
</Modal>
)}
</>
);
};
export default GroupLogsConfigurators;

View File

@@ -0,0 +1,48 @@
@use "src/styles/variables" as *;
.vm-group-logs-configurator {
display: grid;
gap: calc($padding-large * 2);
padding: $padding-global 0;
width: 600px;
&-item {
display: grid;
grid-template-columns: 1fr 31px;
align-items: center;
justify-content: stretch;
gap: 0 $padding-small;
&__info {
margin-top: $padding-small;
grid-column: 1/span 2;
font-size: $font-size-small;
color: $color-text-secondary;
line-height: 130%;
&_input {
margin-top: 0;
}
}
}
&-button {
position: relative;
&__marker {
position: absolute;
top: 6px;
left: 6px;
width: 5px;
height: 5px;
border-radius: 50%;
background-color: $color-secondary;
}
}
&__tooltip {
ul {
list-style-position: inside;
}
}
}

View File

@@ -30,6 +30,10 @@ const Accordion: FC<AccordionProps> = ({
onChange && onChange(isOpen);
}, [isOpen]);
useEffect(() => {
setIsOpen(defaultExpanded);
}, [defaultExpanded]);
return (
<>
<header

View File

@@ -28,7 +28,7 @@ interface AutocompleteProps {
offset?: {top: number, left: number}
maxDisplayResults?: {limit: number, message?: string}
loading?: boolean;
onSelect: (val: string) => void
onSelect: (val: string, item: AutocompleteOptions) => void
onOpenAutocomplete?: (val: boolean) => void
onFoundOptions?: (val: AutocompleteOptions[]) => void
onChangeWrapperRef?: (elementRef: React.RefObject<HTMLElement>) => void
@@ -97,9 +97,9 @@ const Autocomplete: FC<AutocompleteProps> = ({
return noOptionsText && !foundOptions.length;
}, [noOptionsText,foundOptions]);
const createHandlerSelect = (item: string) => () => {
const createHandlerSelect = (item: AutocompleteOptions) => () => {
if (disabled) return;
onSelect(item);
onSelect(item.value, item);
if (!selected) handleCloseAutocomplete();
};
@@ -141,7 +141,7 @@ const Autocomplete: FC<AutocompleteProps> = ({
if (key === "Enter") {
const item = foundOptions[focusOption.index];
item && onSelect(item.value);
item && onSelect(item.value, item);
if (!selected) handleCloseAutocomplete();
}
@@ -206,7 +206,7 @@ const Autocomplete: FC<AutocompleteProps> = ({
})}
id={`$autocomplete$${option.value}`}
key={`${i}${option.value}`}
onClick={createHandlerSelect(option.value)}
onClick={createHandlerSelect(option)}
onMouseEnter={createHandlerMouseEnter(i)}
onMouseLeave={handlerMouseLeave}
>

View File

@@ -581,3 +581,45 @@ export const CommentIcon = () => (
></path>
</svg>
);
export const FilterIcon = () => (
<svg
viewBox="0 0 24 24"
fill="currentColor"
>
<path
d="M4.25 5.61C6.27 8.2 10 13 10 13v6c0 .55.45 1 1 1h2c.55 0 1-.45 1-1v-6s3.72-4.8 5.74-7.39c.51-.66.04-1.61-.79-1.61H5.04c-.83 0-1.3.95-.79 1.61"
></path>
</svg>
);
export const FilterOffIcon = () => (
<svg
viewBox="0 0 24 24"
fill="currentColor"
>
<path
d="M19.79 5.61C20.3 4.95 19.83 4 19 4H6.83l7.97 7.97zM2.81 2.81 1.39 4.22 10 13v6c0 .55.45 1 1 1h2c.55 0 1-.45 1-1v-2.17l5.78 5.78 1.41-1.41z"
></path>
</svg>
);
export const OpenNewIcon = () => (
<svg
viewBox="0 0 24 24"
fill="currentColor"
>
<path
d="M19 19H5V5h7V3H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2v-7h-2zM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3z"
></path>
</svg>
);
export const ModalIcon = () => (
<svg
viewBox="0 0 24 24"
fill="currentColor"
>
<path d="M19 4H5c-1.11 0-2 .9-2 2v12c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V6c0-1.1-.89-2-2-2m0 14H5V8h14z"></path>
</svg>
);

View File

@@ -67,11 +67,11 @@ const Modal: FC<ModalProps> = ({
})}
onMouseDown={onClose}
>
<div className="vm-modal-content">
<div
className="vm-modal-content-header"
onMouseDown={handleMouseDown}
>
<div
className="vm-modal-content"
onMouseDown={handleMouseDown}
>
<div className="vm-modal-content-header">
{title && (
<div className="vm-modal-content-header__title">
{title}
@@ -91,7 +91,6 @@ const Modal: FC<ModalProps> = ({
{/* tabIndex to fix Ctrl-A */}
<div
className="vm-modal-content-body"
onMouseDown={handleMouseDown}
tabIndex={0}
>
{children}

View File

@@ -15,9 +15,10 @@ interface PopperProps {
open: boolean
onClose: () => void
buttonRef: React.RefObject<HTMLElement>
placement?: "bottom-right" | "bottom-left" | "top-left" | "top-right"
placement?: "bottom-right" | "bottom-left" | "top-left" | "top-right" | "fixed"
placementPosition?: { top: number, left: number } | null
animation?: string
offset?: {top: number, left: number}
offset?: { top: number, left: number }
clickOutside?: boolean,
fullWidth?: boolean
title?: string
@@ -29,6 +30,7 @@ const Popper: FC<PopperProps> = ({
children,
buttonRef,
placement = "bottom-left",
placementPosition,
open = false,
onClose,
offset = { top: 6, left: 0 },
@@ -92,13 +94,18 @@ const Popper: FC<PopperProps> = ({
if (needAlignRight) position.left = buttonPos.right - popperSize.width;
if (needAlignTop) position.top = buttonPos.top - popperSize.height - offsetTop;
const { innerWidth, innerHeight } = window;
const margin = 20;
if (placement === "fixed" && placementPosition) {
position.top = Math.max(placementPosition.top + offset.top, 0);
position.left = Math.max(placementPosition.left + offset.left, 0);
return position;
}
const isOverflowBottom = (position.top + popperSize.height + margin) > innerHeight;
const isOverflowTop = (position.top - margin) < 0;
const isOverflowRight = (position.left + popperSize.width + margin) > innerWidth;
const isOverflowLeft = (position.left - margin) < 0;
const { innerWidth, innerHeight } = window;
const isOverflowBottom = (position.top + popperSize.height) > innerHeight;
const isOverflowTop = (position.top) < 0;
const isOverflowRight = (position.left + popperSize.width) > innerWidth;
const isOverflowLeft = (position.left) < 0;
if (isOverflowBottom) position.top = buttonPos.top - popperSize.height - offsetTop;
if (isOverflowTop) position.top = buttonPos.height + buttonPos.top + offsetTop;
@@ -106,11 +113,11 @@ const Popper: FC<PopperProps> = ({
if (isOverflowLeft) position.left = buttonPos.left + offsetLeft;
if (fullWidth) position.width = `${buttonPos.width}px`;
if (position.top < 0) position.top = 20;
if (position.left < 0) position.left = 20;
if (position.top < 0) position.top = 0;
if (position.left < 0) position.left = 0;
return position;
},[buttonRef, placement, isOpen, children, fullWidth]);
}, [buttonRef, placement, isOpen, children, fullWidth]);
const handleClickClose = (e: ReactMouseEvent<HTMLButtonElement, MouseEvent>) => {
e.stopPropagation();
@@ -131,10 +138,10 @@ const Popper: FC<PopperProps> = ({
if (!popperRef.current || !isOpen || (isMobile && !disabledFullScreen)) return;
const { right, width } = popperRef.current.getBoundingClientRect();
if (right > window.innerWidth) {
const left = window.innerWidth - 20 - width;
popperRef.current.style.left = left < window.innerWidth ? "0" : `${left}px`;
const left = window.innerWidth - width;
popperRef.current.style.left = `${left}px`;
}
}, [isOpen, popperRef]);
}, [isOpen, popperRef, placementPosition]);
const handlePopstate = useCallback(() => {
if (isOpen && isMobile && !disabledFullScreen) {

View File

@@ -11,7 +11,7 @@
border-radius: $border-radius-small;
&_open {
z-index: 101;
z-index: 100;
opacity: 1;
transform-origin: top center;
animation: vm-slider 150ms cubic-bezier(0.280, 0.840, 0.420, 1.1);

View File

@@ -33,9 +33,9 @@
align-items: center;
justify-content: center;
background-color: $color-hover-black;
padding: 2px 2px 2px 6px;
padding: 2px 2px 2px $padding-small;
border-radius: $border-radius-small;
font-size: $font-size;
font-size: $font-size-small;
line-height: $font-size;
max-width: 100%;

View File

@@ -11,7 +11,7 @@ import useBoolean from "../../../hooks/useBoolean";
import TextField from "../../Main/TextField/TextField";
import { KeyboardEvent, useState } from "react";
import Modal from "../../Main/Modal/Modal";
import { getFromStorage, removeFromStorage, saveToStorage } from "../../../utils/storage";
import { useSearchParams } from "react-router-dom";
const title = "Table settings";
@@ -30,6 +30,8 @@ const TableSettings: FC<TableSettingsProps> = ({
onChangeColumns,
toggleTableCompact
}) => {
const [searchParams, setSearchParams] = useSearchParams();
const buttonRef = useRef<HTMLDivElement>(null);
const {
@@ -38,11 +40,6 @@ const TableSettings: FC<TableSettingsProps> = ({
setFalse: handleClose,
} = useBoolean(false);
const {
value: saveColumns,
toggle: toggleSaveColumns,
} = useBoolean(Boolean(getFromStorage("TABLE_COLUMNS")));
const [searchColumn, setSearchColumn] = useState("");
const [indexFocusItem, setIndexFocusItem] = useState(-1);
@@ -60,15 +57,34 @@ const TableSettings: FC<TableSettingsProps> = ({
return filteredColumns.every(col => selectedColumns.includes(col));
}, [selectedColumns, filteredColumns]);
const handleChangeDisplayColumns = (displayColumns: string[]) => {
onChangeColumns(displayColumns);
const updatedParams = new URLSearchParams(searchParams.toString());
const isAllCheck = displayColumns.length === columns.length;
if (isAllCheck) {
updatedParams.delete("columns");
} else {
updatedParams.set("columns", displayColumns.map(encodeURIComponent).join(","));
}
setSearchParams(updatedParams);
};
const handleChange = (key: string) => {
onChangeColumns(selectedColumns.includes(key) ? selectedColumns.filter(col => col !== key) : [...selectedColumns, key]);
const displayColumns = selectedColumns.includes(key)
? selectedColumns.filter(col => col !== key)
: [...selectedColumns, key];
handleChangeDisplayColumns(displayColumns);
};
const toggleAllColumns = () => {
if (isAllChecked) {
onChangeColumns(selectedColumns.filter(col => !filteredColumns.includes(col)));
handleChangeDisplayColumns(selectedColumns.filter(col => !filteredColumns.includes(col)));
} else {
onChangeColumns(filteredColumns);
handleChangeDisplayColumns(filteredColumns);
}
};
@@ -95,22 +111,16 @@ const TableSettings: FC<TableSettingsProps> = ({
};
useEffect(() => {
if (arrayEquals(columns, selectedColumns) || saveColumns) return;
if (arrayEquals(columns, selectedColumns) || searchParams.has("columns")) return;
onChangeColumns(columns);
}, [columns]);
useEffect(() => {
if (!saveColumns) {
removeFromStorage(["TABLE_COLUMNS"]);
} else if (selectedColumns.length) {
saveToStorage("TABLE_COLUMNS", selectedColumns.join(","));
}
}, [saveColumns, selectedColumns]);
useEffect(() => {
const saveColumns = getFromStorage("TABLE_COLUMNS") as string;
if (!saveColumns) return;
onChangeColumns(saveColumns.split(","));
const hasColumns = searchParams.has("columns");
if (!hasColumns) return;
const columnsParam = searchParams.get("columns") || "";
const columnsArray = columnsParam.split(",").map(decodeURIComponent).filter(Boolean);
onChangeColumns(columnsArray);
}, []);
return (
@@ -183,19 +193,6 @@ const TableSettings: FC<TableSettingsProps> = ({
</div>
))}
</div>
<div className="vm-table-settings-modal-preserve">
<Checkbox
checked={saveColumns}
onChange={toggleSaveColumns}
label={"Preserve column settings"}
disabled={tableCompact}
color={"primary"}
/>
<p className="vm-table-settings-modal-preserve__info">
This label indicates that when the checkbox is activated,
the current column configurations will not be reset.
</p>
</div>
</div>
</div>
<div className="vm-table-settings-modal-section">

View File

@@ -3,6 +3,7 @@
.vm-table-settings {
&-modal {
.vm-modal-content-body {
min-width: clamp(300px, 600px, 90vw);
padding: 0;
}
@@ -83,16 +84,5 @@
}
}
}
&-preserve {
padding: $padding-global;
&__info {
padding-top: $padding-small;
font-size: $font-size-small;
color: $color-text-secondary;
line-height: 130%;
}
}
}
}

View File

@@ -1,2 +1,22 @@
import { DATE_TIME_FORMAT } from "./date";
export const LOGS_ENTRIES_LIMIT = 50;
export const LOGS_BARS_VIEW = 100;
export const LOGS_LIMIT_HITS = 5;
// "Ungrouped" is a string that is used as a value for the "groupBy" parameter.
export const WITHOUT_GROUPING = "Ungrouped";
// Default values for the logs configurators.
export const LOGS_GROUP_BY = "_stream";
export const LOGS_DISPLAY_FIELDS = "_msg";
export const LOGS_DATE_FORMAT = `${DATE_TIME_FORMAT}.SSS`;
// URL parameters for the logs page.
export const LOGS_URL_PARAMS = {
GROUP_BY: "groupBy",
DISPLAY_FIELDS: "displayFields",
NO_WRAP_LINES: "noWrapLines",
COMPACT_GROUP_HEADER: "compactGroupHeader",
DATE_FORMAT: "dateFormat",
};

View File

@@ -20,7 +20,7 @@ const useClickOutside = <T extends HTMLElement = HTMLElement>(
handler(event); // Call the handler only if the click is outside of the element passed.
}, [ref, handler]);
useEventListener("mousedown", listener);
useEventListener("mouseup", listener);
useEventListener("touchstart", listener);
};

View File

@@ -25,6 +25,7 @@ import { QueryStats } from "../../../api/types";
import { usePrettifyQuery } from "./hooks/usePrettifyQuery";
import QueryHistory from "../QueryHistory/QueryHistory";
import AnomalyConfig from "../../../components/ExploreAnomaly/AnomalyConfig";
import QueryEditorAutocomplete from "../../../components/Configurators/QueryEditor/QueryEditorAutocomplete";
export interface QueryConfiguratorProps {
queryErrors: string[];
@@ -216,6 +217,7 @@ const QueryConfigurator: FC<QueryConfiguratorProps> = ({
<QueryEditor
value={stateQuery[i]}
autocomplete={!hideButtons?.autocomplete && (autocomplete || autocompleteQuick)}
autocompleteEl={QueryEditorAutocomplete}
error={queryErrors[i]}
stats={stats[i]}
onArrowUp={createHandlerArrow(-1, i)}

View File

@@ -69,7 +69,7 @@ const ExploreLogs: FC = () => {
};
const handleApplyFilter = (val: string) => {
setQuery(prev => `_stream: ${val === "other" ? "{}" : val} AND (${prev})`);
setQuery(prev => `${val} AND (${prev})`);
setIsUpdatingQuery(true);
};

View File

@@ -6,6 +6,9 @@ import useDeviceDetect from "../../../hooks/useDeviceDetect";
import Button from "../../../components/Main/Button/Button";
import QueryEditor from "../../../components/Configurators/QueryEditor/QueryEditor";
import TextField from "../../../components/Main/TextField/TextField";
import LogsQueryEditorAutocomplete from "../../../components/Configurators/QueryEditor/LogsQL/LogsQueryEditorAutocomplete";
import { useQueryDispatch, useQueryState } from "../../../state/query/QueryStateContext";
import Switch from "../../../components/Main/Switch/Switch";
export interface ExploreLogHeaderProps {
query: string;
@@ -27,6 +30,8 @@ const ExploreLogsHeader: FC<ExploreLogHeaderProps> = ({
onRun,
}) => {
const { isMobile } = useDeviceDetect();
const { autocomplete } = useQueryState();
const queryDispatch = useQueryDispatch();
const [errorLimit, setErrorLimit] = useState("");
const [limitInput, setLimitInput] = useState(limit);
@@ -42,6 +47,10 @@ const ExploreLogsHeader: FC<ExploreLogHeaderProps> = ({
}
};
const onChangeAutocomplete = () => {
queryDispatch({ type: "TOGGLE_AUTOCOMPLETE" });
};
useEffect(() => {
setLimitInput(limit);
}, [limit]);
@@ -57,7 +66,8 @@ const ExploreLogsHeader: FC<ExploreLogHeaderProps> = ({
<div className="vm-explore-logs-header-top">
<QueryEditor
value={query}
autocomplete={false}
autocomplete={autocomplete}
autocompleteEl={LogsQueryEditorAutocomplete}
onArrowUp={() => null}
onArrowDown={() => null}
onEnter={onRun}
@@ -75,7 +85,14 @@ const ExploreLogsHeader: FC<ExploreLogHeaderProps> = ({
/>
</div>
<div className="vm-explore-logs-header-bottom">
<div className="vm-explore-logs-header-bottom-contols"></div>
<div className="vm-explore-logs-header-bottom-contols">
<Switch
label={"Autocomplete"}
value={autocomplete}
onChange={onChangeAutocomplete}
fullWidth={isMobile}
/>
</div>
<div className="vm-explore-logs-header-bottom-helpful">
<a
className="vm-link vm-link_with-icon"

View File

@@ -26,6 +26,9 @@
}
&-contols {
display: flex;
align-items: center;
justify-content: flex-start;
flex-grow: 1;
}

View File

@@ -1,24 +1,19 @@
import React, { FC, useCallback, useEffect, useMemo, useRef } from "preact/compat";
import { MouseEvent, useState } from "react";
import React, { FC, useCallback, useEffect, useMemo } from "preact/compat";
import { useState } from "react";
import "./style.scss";
import { Logs } from "../../../api/types";
import Accordion from "../../../components/Main/Accordion/Accordion";
import { groupByMultipleKeys } from "../../../utils/array";
import Tooltip from "../../../components/Main/Tooltip/Tooltip";
import useCopyToClipboard from "../../../hooks/useCopyToClipboard";
import GroupLogsItem from "./GroupLogsItem";
import { useAppState } from "../../../state/common/StateContext";
import classNames from "classnames";
import Button from "../../../components/Main/Button/Button";
import { CollapseIcon, ExpandIcon, StorageIcon } from "../../../components/Main/Icons";
import Popper from "../../../components/Main/Popper/Popper";
import TextField from "../../../components/Main/TextField/TextField";
import useBoolean from "../../../hooks/useBoolean";
import useStateSearchParams from "../../../hooks/useStateSearchParams";
import { CollapseIcon, ExpandIcon } from "../../../components/Main/Icons";
import { useSearchParams } from "react-router-dom";
import { getStreamPairs } from "../../../utils/logs";
const WITHOUT_GROUPING = "No Grouping";
import GroupLogsConfigurators
from "../../../components/LogsConfigurators/GroupLogsConfigurators/GroupLogsConfigurators";
import GroupLogsHeader from "./GroupLogsHeader";
import { LOGS_DISPLAY_FIELDS, LOGS_GROUP_BY, LOGS_URL_PARAMS, WITHOUT_GROUPING } from "../../../constants/logs";
interface Props {
logs: Logs[];
@@ -26,73 +21,31 @@ interface Props {
}
const GroupLogs: FC<Props> = ({ logs, settingsRef }) => {
const { isDarkTheme } = useAppState();
const copyToClipboard = useCopyToClipboard();
const [searchParams, setSearchParams] = useSearchParams();
const [searchParams] = useSearchParams();
const [expandGroups, setExpandGroups] = useState<boolean[]>([]);
const [groupBy, setGroupBy] = useStateSearchParams("_stream", "groupBy");
const [copied, setCopied] = useState<string | null>(null);
const [searchKey, setSearchKey] = useState("");
const optionsButtonRef = useRef<HTMLDivElement>(null);
const {
value: openOptions,
toggle: toggleOpenOptions,
setFalse: handleCloseOptions,
} = useBoolean(false);
const groupBy = searchParams.get(LOGS_URL_PARAMS.GROUP_BY) || LOGS_GROUP_BY;
const displayFieldsString = searchParams.get(LOGS_URL_PARAMS.DISPLAY_FIELDS) || LOGS_DISPLAY_FIELDS;
const displayFields = displayFieldsString.split(",");
const expandAll = useMemo(() => expandGroups.every(Boolean), [expandGroups]);
const logsKeys = useMemo(() => {
const excludeKeys = ["_msg", "_time"];
const uniqKeys = Array.from(new Set(logs.map(l => Object.keys(l)).flat()));
return [WITHOUT_GROUPING, ...uniqKeys.filter(k => !excludeKeys.includes(k))];
}, [logs]);
const filteredLogsKeys = useMemo(() => {
if (!searchKey) return logsKeys;
try {
const regexp = new RegExp(searchKey, "i");
return logsKeys.filter(item => regexp.test(item))
.sort((a, b) => (a.match(regexp)?.index || 0) - (b.match(regexp)?.index || 0));
} catch (e) {
return [];
}
}, [logsKeys, searchKey]);
const groupData = useMemo(() => {
return groupByMultipleKeys(logs, [groupBy]).map((item) => {
const streamValue = item.values[0]?.[groupBy] || "";
const pairs = getStreamPairs(streamValue);
// values sorting by time
const values = item.values.sort((a,b) => new Date(b._time).getTime() - new Date(a._time).getTime());
const values = item.values.sort((a, b) => new Date(b._time).getTime() - new Date(a._time).getTime());
return {
keys: item.keys,
keysString: item.keys.join(""),
values,
pairs,
};
}).sort((a, b) => a.keysString.localeCompare(b.keysString)); // groups sorting
}).sort((a, b) => b.values.length - a.values.length); // groups sorting
}, [logs, groupBy]);
const handleClickByPair = (value: string) => async (e: MouseEvent<HTMLDivElement>) => {
e.stopPropagation();
const isKeyValue = /(.+)?=(".+")/.test(value);
const copyValue = isKeyValue ? `${value.replace(/=/, ": ")}` : `${groupBy}: "${value}"`;
const isCopied = await copyToClipboard(copyValue);
if (isCopied) {
setCopied(value);
}
};
const handleSelectGroupBy = (key: string) => () => {
setGroupBy(key);
searchParams.set("groupBy", key);
setSearchParams(searchParams);
handleCloseOptions();
};
const handleToggleExpandAll = useCallback(() => {
setExpandGroups(new Array(groupData.length).fill(!expandAll));
}, [expandAll, groupData.length]);
@@ -105,11 +58,6 @@ const GroupLogs: FC<Props> = ({ logs, settingsRef }) => {
});
}, []);
useEffect(() => {
if (copied === null) return;
const timeout = setTimeout(() => setCopied(null), 2000);
return () => clearTimeout(timeout);
}, [copied]);
useEffect(() => {
setExpandGroups(new Array(groupData.length).fill(true));
@@ -124,38 +72,16 @@ const GroupLogs: FC<Props> = ({ logs, settingsRef }) => {
key={item.keysString}
>
<Accordion
key={String(expandGroups[i])}
defaultExpanded={expandGroups[i]}
onChange={handleChangeExpand(i)}
title={groupBy !== WITHOUT_GROUPING && (
<div className="vm-group-logs-section-keys">
<span className="vm-group-logs-section-keys__title">Group by <code>{groupBy}</code>:</span>
{item.pairs.map((pair) => (
<Tooltip
title={copied === pair ? "Copied" : "Copy to clipboard"}
key={`${item.keysString}_${pair}`}
placement={"top-center"}
>
<div
className={classNames({
"vm-group-logs-section-keys__pair": true,
"vm-group-logs-section-keys__pair_dark": isDarkTheme
})}
onClick={handleClickByPair(pair)}
>
{pair}
</div>
</Tooltip>
))}
<span className="vm-group-logs-section-keys__count">{item.values.length} entries</span>
</div>
)}
title={groupBy !== WITHOUT_GROUPING && <GroupLogsHeader group={item}/>}
>
<div className="vm-group-logs-section-rows">
{item.values.map((value) => (
<GroupLogsItem
key={`${value._msg}${value._time}`}
log={value}
displayFields={displayFields}
/>
))}
</div>
@@ -175,47 +101,7 @@ const GroupLogs: FC<Props> = ({ logs, settingsRef }) => {
ariaLabel={expandAll ? "Collapse All" : "Expand All"}
/>
</Tooltip>
<Tooltip title={"Group by"}>
<div ref={optionsButtonRef}>
<Button
variant="text"
startIcon={<StorageIcon/>}
onClick={toggleOpenOptions}
ariaLabel={"Group by"}
/>
</div>
</Tooltip>
{
<Popper
open={openOptions}
placement="bottom-right"
onClose={handleCloseOptions}
buttonRef={optionsButtonRef}
>
<div className="vm-list vm-group-logs-header-keys">
<div className="vm-group-logs-header-keys__search">
<TextField
label="Search key"
value={searchKey}
onChange={setSearchKey}
type="search"
/>
</div>
{filteredLogsKeys.map(id => (
<div
className={classNames({
"vm-list-item": true,
"vm-list-item_active": id === groupBy
})}
key={id}
onClick={handleSelectGroupBy(id)}
>
{id}
</div>
))}
</div>
</Popper>
}
<GroupLogsConfigurators logs={logs}/>
</div>
), settingsRef.current)}
</>

View File

@@ -1,8 +1,10 @@
import React, { FC, memo, useCallback, useEffect, useState } from "preact/compat";
import Tooltip from "../../../components/Main/Tooltip/Tooltip";
import Button from "../../../components/Main/Button/Button";
import { CopyIcon } from "../../../components/Main/Icons";
import { CopyIcon, StorageIcon, VisibilityIcon } from "../../../components/Main/Icons";
import useCopyToClipboard from "../../../hooks/useCopyToClipboard";
import { useSearchParams } from "react-router-dom";
import { LOGS_GROUP_BY, LOGS_URL_PARAMS } from "../../../constants/logs";
interface Props {
field: string;
@@ -11,8 +13,17 @@ interface Props {
const GroupLogsFieldRow: FC<Props> = ({ field, value }) => {
const copyToClipboard = useCopyToClipboard();
const [searchParams, setSearchParams] = useSearchParams();
const [copied, setCopied] = useState<boolean>(false);
const groupBy = searchParams.get(LOGS_URL_PARAMS.GROUP_BY) || LOGS_GROUP_BY;
const displayFieldsString = searchParams.get(LOGS_URL_PARAMS.DISPLAY_FIELDS) || "";
const displayFields = displayFieldsString ? displayFieldsString.split(",") : [];
const isSelectedField = displayFields.includes(field);
const isGroupByField = groupBy === field;
const handleCopy = useCallback(async () => {
if (copied) return;
try {
@@ -23,6 +34,18 @@ const GroupLogsFieldRow: FC<Props> = ({ field, value }) => {
}
}, [copied, copyToClipboard]);
const handleSelectDisplayField = () => {
const prev = displayFields;
const newDisplayFields = prev.includes(field) ? prev.filter(v => v !== field) : [...prev, field];
searchParams.set(LOGS_URL_PARAMS.DISPLAY_FIELDS, newDisplayFields.join(","));
setSearchParams(searchParams);
};
const handleSelectGroupBy = () => {
isGroupByField ? searchParams.delete(LOGS_URL_PARAMS.GROUP_BY) : searchParams.set(LOGS_URL_PARAMS.GROUP_BY, field);
setSearchParams(searchParams);
};
useEffect(() => {
if (copied === null) return;
const timeout = setTimeout(() => setCopied(false), 2000);
@@ -35,6 +58,7 @@ const GroupLogsFieldRow: FC<Props> = ({ field, value }) => {
<div className="vm-group-logs-row-fields-item-controls__wrapper">
<Tooltip title={copied ? "Copied" : "Copy to clipboard"}>
<Button
className="vm-group-logs-row-fields-item-controls__button"
variant="text"
color="gray"
size="small"
@@ -43,6 +67,34 @@ const GroupLogsFieldRow: FC<Props> = ({ field, value }) => {
ariaLabel="copy to clipboard"
/>
</Tooltip>
<Tooltip
key={`${field}_${isSelectedField}_${isGroupByField}`}
title={isSelectedField ? "Hide this field" : "Show this field instead of the message"}
>
<Button
className="vm-group-logs-row-fields-item-controls__button"
variant="text"
color={isSelectedField ? "secondary" : "gray"}
size="small"
startIcon={isSelectedField ? <VisibilityIcon/> : <VisibilityIcon/>}
onClick={handleSelectDisplayField}
ariaLabel="copy to clipboard"
/>
</Tooltip>
<Tooltip
key={`${field}_${isSelectedField}_${isGroupByField}`}
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"
/>
</Tooltip>
</div>
</td>
<td className="vm-group-logs-row-fields-item__key">{field}</td>

View File

@@ -0,0 +1,127 @@
import React, { FC, useCallback, useEffect, useRef } from "preact/compat";
import classNames from "classnames";
import { useSearchParams } from "react-router-dom";
import { MouseEvent, useState } from "react";
import { useAppState } from "../../../state/common/StateContext";
import { Logs } from "../../../api/types";
import useEventListener from "../../../hooks/useEventListener";
import Popper from "../../../components/Main/Popper/Popper";
import useBoolean from "../../../hooks/useBoolean";
import GroupLogsHeaderItem from "./GroupLogsHeaderItem";
import { LOGS_GROUP_BY, LOGS_URL_PARAMS } from "../../../constants/logs";
interface Props {
group: {
keys: string[]
keysString: string
values: Logs[]
pairs: string[]
};
}
const GroupLogsHeader: FC<Props> = ({ group }) => {
const { isDarkTheme } = useAppState();
const [searchParams] = useSearchParams();
const containerRef = useRef<HTMLDivElement>(null);
const moreRef = useRef<HTMLDivElement>(null);
const {
value: openMore,
toggle: handleToggleMore,
setFalse: handleCloseMore,
} = useBoolean(false);
const [hideParisCount, setHideParisCount] = useState<number>(0);
const groupBy = searchParams.get(LOGS_URL_PARAMS.GROUP_BY) || LOGS_GROUP_BY;
const compactGroupHeader = searchParams.get(LOGS_URL_PARAMS.COMPACT_GROUP_HEADER) === "true";
const pairs = group.pairs;
const hideAboveIndex = pairs.length - hideParisCount - 1;
const handleClickMore = (e: MouseEvent<HTMLDivElement>) => {
e.stopPropagation();
handleToggleMore();
};
const calcVisiblePairsCount = useCallback(() => {
if (!compactGroupHeader || !containerRef.current) {
setHideParisCount(0);
return;
}
const container = containerRef.current;
const containerSize = container.getBoundingClientRect();
const selector = ".vm-group-logs-section-keys__pair:not(.vm-group-logs-section-keys__pair_more)";
const children = Array.from(container.querySelectorAll(selector));
let count = 0;
for (const child of children) {
const { right } = (child as HTMLElement).getBoundingClientRect();
if ((right + 220) > containerSize.width) {
count++;
}
}
setHideParisCount(count);
}, [compactGroupHeader, containerRef]);
useEffect(calcVisiblePairsCount, [group.pairs, compactGroupHeader, containerRef]);
useEventListener("resize", calcVisiblePairsCount);
return (
<div
className={classNames({
"vm-group-logs-section-keys": true,
"vm-group-logs-section-keys_compact": compactGroupHeader,
})}
ref={containerRef}
>
<span className="vm-group-logs-section-keys__title">Group by <code>{groupBy}</code>:</span>
{pairs.map((pair, i) => (
<GroupLogsHeaderItem
key={`${group.keysString}_${pair}`}
pair={pair}
isHide={hideParisCount ? i > hideAboveIndex : false}
/>
))}
{hideParisCount > 0 && (
<>
<div
className={classNames({
"vm-group-logs-section-keys__pair": true,
"vm-group-logs-section-keys__pair_more": true,
"vm-group-logs-section-keys__pair_dark": isDarkTheme
})}
ref={moreRef}
onClick={handleClickMore}
>
+{hideParisCount} more
</div>
<Popper
open={openMore}
buttonRef={moreRef}
placement="bottom-left"
onClose={handleCloseMore}
>
<div className="vm-group-logs-section-keys vm-group-logs-section-keys_popper">
{pairs.slice(hideAboveIndex + 1).map((pair) => (
<GroupLogsHeaderItem
key={`${group.keysString}_${pair}`}
pair={pair}
/>
))}
</div>
</Popper>
</>
)}
<span className="vm-group-logs-section-keys__count">{group.values.length} entries</span>
</div>
)
;
};
export default GroupLogsHeader;

View File

@@ -0,0 +1,59 @@
import React, { FC, useEffect } from "preact/compat";
import { useAppState } from "../../../state/common/StateContext";
import Tooltip from "../../../components/Main/Tooltip/Tooltip";
import classNames from "classnames";
import { MouseEvent, useState } from "react";
import useCopyToClipboard from "../../../hooks/useCopyToClipboard";
import { useSearchParams } from "react-router-dom";
import { LOGS_GROUP_BY, LOGS_URL_PARAMS } from "../../../constants/logs";
import { convertToFieldFilter } from "../../../utils/logs";
interface Props {
pair: string;
isHide?: boolean;
}
const GroupLogsHeaderItem: FC<Props> = ({ pair, isHide }) => {
const { isDarkTheme } = useAppState();
const copyToClipboard = useCopyToClipboard();
const [searchParams] = useSearchParams();
const [copied, setCopied] = useState<string | null>(null);
const groupBy = searchParams.get(LOGS_URL_PARAMS.GROUP_BY) || LOGS_GROUP_BY;
const handleClickByPair = (value: string) => async (e: MouseEvent<HTMLDivElement>) => {
e.stopPropagation();
const copyValue = convertToFieldFilter(value, groupBy);
const isCopied = await copyToClipboard(copyValue);
if (isCopied) {
setCopied(value);
}
};
useEffect(() => {
if (copied === null) return;
const timeout = setTimeout(() => setCopied(null), 2000);
return () => clearTimeout(timeout);
}, [copied]);
return (
<Tooltip
title={copied === pair ? "Copied" : "Copy to clipboard"}
placement={"top-center"}
>
<div
className={classNames({
"vm-group-logs-section-keys__pair": true,
"vm-group-logs-section-keys__pair_hide": isHide,
"vm-group-logs-section-keys__pair_dark": isDarkTheme
})}
onClick={handleClickByPair(pair)}
>
{pair}
</div>
</Tooltip>
);
};
export default GroupLogsHeaderItem;

View File

@@ -6,28 +6,34 @@ import { ArrowDownIcon } from "../../../components/Main/Icons";
import classNames from "classnames";
import { useLogsState } from "../../../state/logsPanel/LogsStateContext";
import dayjs from "dayjs";
import { DATE_TIME_FORMAT } from "../../../constants/date";
import { useTimeState } from "../../../state/time/TimeStateContext";
import GroupLogsFieldRow from "./GroupLogsFieldRow";
import { marked } from "marked";
import { useSearchParams } from "react-router-dom";
import { LOGS_DATE_FORMAT, LOGS_URL_PARAMS } from "../../../constants/logs";
interface Props {
log: Logs;
displayFields?: string[];
}
const GroupLogsItem: FC<Props> = ({ log }) => {
const GroupLogsItem: FC<Props> = ({ log, displayFields = ["_msg"] }) => {
const {
value: isOpenFields,
toggle: toggleOpenFields,
} = useBoolean(false);
const [searchParams] = useSearchParams();
const { markdownParsing } = useLogsState();
const { timezone } = useTimeState();
const noWrapLines = searchParams.get(LOGS_URL_PARAMS.NO_WRAP_LINES) === "true";
const dateFormat = searchParams.get(LOGS_URL_PARAMS.DATE_FORMAT) || LOGS_DATE_FORMAT;
const formattedTime = useMemo(() => {
if (!log._time) return "";
return dayjs(log._time).tz().format(`${DATE_TIME_FORMAT}.SSS`);
}, [log._time, timezone]);
return dayjs(log._time).tz().format(dateFormat);
}, [log._time, timezone, dateFormat]);
const formattedMarkdown = useMemo(() => {
if (!markdownParsing || !log._msg) return "";
@@ -38,6 +44,14 @@ const GroupLogsItem: FC<Props> = ({ log }) => {
const hasFields = fields.length > 0;
const displayMessage = useMemo(() => {
if (displayFields.length) {
return displayFields.filter(field => log[field]).map((field, i) => (
<span
className="vm-group-logs-row-content__sub-msg"
key={field + i}
>{log[field]}</span>
));
}
if (log._msg) return log._msg;
if (!hasFields) return;
const dataObject = fields.reduce<{ [key: string]: string }>((obj, [key, value]) => {
@@ -45,7 +59,7 @@ const GroupLogsItem: FC<Props> = ({ log }) => {
return obj;
}, {});
return JSON.stringify(dataObject);
}, [log, fields, hasFields]);
}, [log, fields, hasFields, displayFields]);
return (
<div className="vm-group-logs-row">
@@ -76,7 +90,8 @@ const GroupLogsItem: FC<Props> = ({ log }) => {
className={classNames({
"vm-group-logs-row-content__msg": true,
"vm-group-logs-row-content__msg_empty-msg": !log._msg,
"vm-group-logs-row-content__msg_missing": !displayMessage
"vm-group-logs-row-content__msg_missing": !displayMessage,
"vm-group-logs-row-content__msg_single-line": noWrapLines,
})}
dangerouslySetInnerHTML={(markdownParsing && formattedMarkdown) ? { __html: formattedMarkdown } : undefined}
>

View File

@@ -1,5 +1,7 @@
@use "src/styles/variables" as *;
$font-size-logs: var(--font-size-logs, $font-size-small);
.vm-group-logs {
margin-top: calc(-1 * $padding-medium);
@@ -19,22 +21,44 @@
}
&-section {
border-bottom: $border-divider;
&-keys {
position: relative;
display: flex;
align-items: center;
flex-wrap: wrap;
gap: $padding-small;
border-bottom: $border-divider;
padding: $padding-small 0;
padding: $padding-small 120px $padding-small 0;
font-size: $font-size-logs;
&_compact {
flex-wrap: nowrap;
overflow: hidden;
}
&_popper {
display: flex;
flex-wrap: nowrap;
flex-direction: column;
align-items: flex-start;
justify-content: flex-start;
padding: $padding-global;
max-height: 400px;
overflow: auto;
}
&__title {
font-weight: bold;
white-space: nowrap;
code {
font-family: monospace;
&:before {
content: "\"";
}
&:after {
content: "\"";
}
@@ -42,19 +66,35 @@
}
&__count {
position: absolute;
top: auto;
right: 0;
flex-grow: 1;
text-align: right;
font-size: $font-size-small;
font-size: $font-size-logs;
color: $color-text-secondary;
padding-right: calc($padding-large * 3);
}
&__pair {
order: 0;
padding: calc($padding-global / 2) $padding-global;
background-color: lighten($color-tropical-blue, 6%);
color: darken($color-dodger-blue, 20%);
border-radius: $border-radius-medium;
transition: background-color 0.3s ease-in, transform 0.1s ease-in, opacity 0.3s ease-in;
white-space: nowrap;
&_hide {
order: 2;
visibility: hidden;
opacity: 0;
pointer-events: none;
}
&_more {
order: 1;
}
&:hover {
background-color: $color-tropical-blue;
@@ -84,13 +124,19 @@
&-row {
position: relative;
border-bottom: $border-divider;
&:last-child {
margin-bottom: $padding-small;
}
&-content {
position: relative;
display: grid;
grid-template-columns: auto minmax(180px, max-content) 1fr;
padding: $padding-global 0;
grid-template-columns: auto max-content 1fr;
padding: calc($padding-small / 4) 0;
font-size: $font-size-logs;
font-variant-numeric: tabular-nums;
line-height: 1.3;
cursor: pointer;
transition: background-color 0.2s ease-in;
@@ -116,8 +162,7 @@
display: flex;
align-items: flex-start;
justify-content: flex-end;
margin-right: $padding-small;
line-height: 1;
padding: 0 $padding-global 0 $padding-small;
white-space: nowrap;
&_missing {
@@ -130,7 +175,12 @@
&__msg {
font-family: $font-family-monospace;
overflow-wrap: anywhere;
line-height: 1.1;
&_single-line {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
&_empty-msg {
overflow: hidden;
@@ -158,7 +208,7 @@
border-radius: $border-radius-small;
tab-size: 4;
font-variant-ligatures: none;
margin: calc($padding-small/4) 0;
margin: calc($padding-small / 4) 0;
}
p {
@@ -171,7 +221,7 @@
}
code {
font-size: $font-size-small;
font-size: $font-size-logs;
padding: calc($padding-small / 4) calc($padding-small / 2);
}
@@ -194,25 +244,35 @@
blockquote {
border-left: 4px solid $color-hover-black;
margin: calc($padding-small/2) $padding-small;
padding: calc($padding-small/2) $padding-small;
margin: calc($padding-small / 2) $padding-small;
padding: calc($padding-small / 2) $padding-small;
}
ul, ol {
list-style-position: inside;
}
/* end styles for markdown */
}
&__sub-msg {
padding-right: $padding-global;
}
}
&-fields {
position: relative;
grid-row: 2;
padding: $padding-small 0;
margin-bottom: $padding-small;
margin: $padding-small 0 $padding-small calc($padding-global * 2);
border: $border-divider;
border-radius: $border-radius-small;
overflow: auto;
max-height: 300px;
resize: vertical;
font-family: $font-family-monospace;
font-size: $font-size-logs;
font-variant-numeric: tabular-nums;
&-item {
border-radius: $border-radius-small;
@@ -223,19 +283,26 @@
}
&-controls {
padding: 0;
padding: 0 calc($padding-small / 2);
&__wrapper {
display: flex;
align-items: center;
justify-content: center;
}
&__button.vm-button_small {
width: 22px;
height: 22px;
min-height: 22px;
}
}
&__key,
&__value {
vertical-align: top;
padding: calc($padding-small / 2) $padding-global;
line-height: $font-size;
padding: calc($padding-small / 2);
}
&__key {

View File

@@ -4,6 +4,8 @@ import { ErrorTypes, TimeParams } from "../../../types";
import { LogHits } from "../../../api/types";
import { useSearchParams } from "react-router-dom";
import { getHitsTimeParams } from "../../../utils/logs";
import { LOGS_GROUP_BY, LOGS_LIMIT_HITS } from "../../../constants/logs";
import { isEmptyObject } from "../../../utils/object";
export const useFetchLogHits = (server: string, query: string) => {
const [searchParams] = useSearchParams();
@@ -30,46 +32,12 @@ export const useFetchLogHits = (server: string, query: string) => {
step: `${step}ms`,
start: start.toISOString(),
end: end.toISOString(),
field: "_stream" // In the future, this field can be made configurable
fields_limit: `${LOGS_LIMIT_HITS}`,
field: LOGS_GROUP_BY,
})
};
};
const accumulateHits = (resultHit: LogHits, hit: LogHits) => {
resultHit.total = (resultHit.total || 0) + (hit.total || 0);
hit.timestamps.forEach((timestamp, i) => {
const index = resultHit.timestamps.findIndex(t => t === timestamp);
if (index === -1) {
resultHit.timestamps.push(timestamp);
resultHit.values.push(hit.values[i]);
} else {
resultHit.values[index] += hit.values[i];
}
});
return resultHit;
};
const getHitsWithTop = (hits: LogHits[]) => {
const topN = 5;
const defaultHit = { fields: {}, timestamps: [], values: [], total: 0 };
const hitsByTotal = hits.sort((a, b) => (b.total || 0) - (a.total || 0));
const result = [];
const otherHits: LogHits = hitsByTotal.slice(topN).reduce(accumulateHits, defaultHit);
if (otherHits.total) {
result.push(otherHits);
}
const topHits: LogHits[] = hitsByTotal.slice(0, topN);
if (topHits.length) {
result.push(...topHits);
}
return result;
};
const fetchLogHits = useCallback(async (period: TimeParams) => {
abortControllerRef.current.abort();
abortControllerRef.current = new AbortController();
@@ -98,7 +66,7 @@ export const useFetchLogHits = (server: string, query: string) => {
setError(error);
}
setLogHits(!hits ? [] : getHitsWithTop(hits));
setLogHits(hits.map(hit => ({ ...hit, _isOther: isEmptyObject(hit.fields) })));
} catch (e) {
if (e instanceof Error && e.name !== "AbortError") {
setError(String(e));

View File

@@ -1,15 +1,20 @@
import { getFromStorage, saveToStorage } from "../../utils/storage";
import { LogsFiledValues } from "../../api/types";
import { AUTOCOMPLETE_LIMITS } from "../../constants/queryAutocomplete";
export interface LogsState {
markdownParsing: boolean;
autocompleteCache: Map<string, LogsFiledValues[]>;
}
export type LogsAction =
| { type: "SET_MARKDOWN_PARSING", payload: boolean }
| { type: "SET_AUTOCOMPLETE_CACHE", payload: { key: string, value: LogsFiledValues[] } }
export const initialLogsState: LogsState = {
markdownParsing: getFromStorage("LOGS_MARKDOWN") === "true",
autocompleteCache: new Map<string, LogsFiledValues[]>(),
};
export function reducer(state: LogsState, action: LogsAction): LogsState {
@@ -20,6 +25,18 @@ export function reducer(state: LogsState, action: LogsAction): LogsState {
...state,
markdownParsing: action.payload
};
case "SET_AUTOCOMPLETE_CACHE": {
if (state.autocompleteCache.size >= AUTOCOMPLETE_LIMITS.cacheLimit) {
const firstKey = state.autocompleteCache.keys().next().value;
state.autocompleteCache.delete(firstKey);
}
state.autocompleteCache.set(action.payload.key, action.payload.value);
return {
...state,
autocompleteCache: state.autocompleteCache,
};
}
default:
throw new Error();
}

View File

@@ -1,6 +1,8 @@
import { TimeParams } from "../types";
import dayjs from "dayjs";
import { LOGS_BARS_VIEW } from "../constants/logs";
import { LOGS_BARS_VIEW, LOGS_GROUP_BY } from "../constants/logs";
import { LogHits } from "../api/types";
import { OTHER_HITS_LABEL } from "../components/Chart/BarHitsChart/hooks/useBarHitsOptions";
export const getStreamPairs = (value: string): string[] => {
const pairs = /^{.+}$/.test(value) ? value.slice(1, -1).split(",") : [value];
@@ -14,3 +16,27 @@ export const getHitsTimeParams = (period: TimeParams) => {
const step = Math.ceil(totalSeconds / LOGS_BARS_VIEW) || 1;
return { start, end, step };
};
export const convertToFieldFilter = (value: string, field = LOGS_GROUP_BY) => {
const isKeyValue = /(.+)?=(".+")/.test(value);
if (isKeyValue) {
return value.replace(/=/, ": ");
}
return `${field}: "${value}"`;
};
export const calculateTotalHits = (hits: LogHits[]): number => {
return hits.reduce((acc, item) => acc + (item.total || 0), 0);
};
export const sortLogHits = <T extends { label?: string }>(key: keyof T) => (a: T, b: T): number => {
if (a.label === OTHER_HITS_LABEL) return 1;
if (b.label === OTHER_HITS_LABEL) return -1;
const aValue = a[key] as unknown as number;
const bValue = b[key] as unknown as number;
return bValue - aValue;
};

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