Compare commits

..

2 Commits

Author SHA1 Message Date
Haley Wang
7d7d17d192 add changelog 2025-02-10 14:08:32 +08:00
Evgeny Kuzin
0a8b4281e5 fix race using the same list from 2 goroutines 2025-02-07 11:55:45 -05:00
366 changed files with 19742 additions and 12843 deletions

View File

@@ -90,7 +90,7 @@ jobs:
- name: Publish coverage
uses: codecov/codecov-action@v5
with:
files: ./coverage.txt
file: ./coverage.txt
integration-test:
name: integration-test

View File

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

View File

@@ -129,11 +129,6 @@ func handleJournald(r *http.Request, w http.ResponseWriter) {
return
}
// systemd starting release v258 will support compression, which starts working after negotiation: it expects supported compression
// algorithms list in Accept-Encoding response header in a format "<algorithm_1>[:<priority_1>][;<algorithm_2>:<priority_2>]"
// See https://github.com/systemd/systemd/pull/34822
w.Header().Set("Accept-Encoding", "zstd")
// update requestJournaldDuration only for successfully parsed requests
// There is no need in updating requestJournaldDuration for request errors,
// since their timings are usually much smaller than the timing for successful request parsing.

View File

@@ -51,13 +51,20 @@ func RequestHandler(w http.ResponseWriter, r *http.Request) {
lmp := cp.NewLogMessageProcessor("jsonline")
streamName := fmt.Sprintf("remoteAddr=%s, requestURI=%q", httpserver.GetQuotedRemoteAddr(r), r.RequestURI)
processStreamInternal(streamName, reader, cp.TimeField, cp.MsgFields, lmp)
err = processStreamInternal(streamName, reader, cp.TimeField, cp.MsgFields, lmp)
lmp.MustClose()
requestDuration.UpdateDuration(startTime)
if err != nil {
logger.Errorf("jsonline: %s", err)
} else {
// update requestDuration only for successfully parsed requests.
// There is no need in updating requestDuration for request errors,
// since their timings are usually much smaller than the timing for successful request parsing.
requestDuration.UpdateDuration(startTime)
}
}
func processStreamInternal(streamName string, r io.Reader, timeField string, msgFields []string, lmp insertutils.LogMessageProcessor) {
func processStreamInternal(streamName string, r io.Reader, timeField string, msgFields []string, lmp insertutils.LogMessageProcessor) error {
wcr := writeconcurrencylimiter.GetReader(r)
defer writeconcurrencylimiter.PutReader(wcr)
@@ -69,10 +76,10 @@ func processStreamInternal(streamName string, r io.Reader, timeField string, msg
wcr.DecConcurrency()
if err != nil {
errorsTotal.Inc()
logger.Warnf("jsonline: cannot read line #%d in /jsonline request: %s", n, err)
return fmt.Errorf("cannot read line #%d in /jsonline request: %s", n, err)
}
if !ok {
return
return nil
}
n++
}
@@ -89,17 +96,16 @@ func readLine(lr *insertutils.LineReader, timeField string, msgFields []string,
}
p := logstorage.GetJSONParser()
defer logstorage.PutJSONParser(p)
if err := p.ParseLogMessage(line); err != nil {
return true, fmt.Errorf("cannot parse json-encoded line: %w; line contents: %q", err, line)
return false, fmt.Errorf("cannot parse json-encoded log entry: %w", err)
}
ts, err := insertutils.ExtractTimestampFromFields(timeField, p.Fields)
if err != nil {
return true, fmt.Errorf("cannot get timestamp from json-encoded line: %w; line contents: %q", err, line)
return false, fmt.Errorf("cannot get timestamp: %w", err)
}
logstorage.RenameField(p.Fields, msgFields, "_msg")
lmp.AddRow(ts, p.Fields, nil)
logstorage.PutJSONParser(p)
return true, nil
}

View File

@@ -7,14 +7,16 @@ import (
"github.com/VictoriaMetrics/VictoriaMetrics/app/vlinsert/insertutils"
)
func TestProcessStreamInternal(t *testing.T) {
func TestProcessStreamInternal_Success(t *testing.T) {
f := func(data, timeField, msgField string, timestampsExpected []int64, resultExpected string) {
t.Helper()
msgFields := []string{msgField}
tlp := &insertutils.TestLogMessageProcessor{}
r := bytes.NewBufferString(data)
processStreamInternal("test", r, timeField, msgFields, tlp)
if err := processStreamInternal("test", r, timeField, msgFields, tlp); err != nil {
t.Fatalf("unexpected error: %s", err)
}
if err := tlp.Verify(timestampsExpected, resultExpected); err != nil {
t.Fatal(err)
@@ -43,37 +45,22 @@ func TestProcessStreamInternal(t *testing.T) {
resultExpected = `{"log.offset":"71770","log.file.path":"/var/log/auth.log","message":"foobar"}
{"message":"baz"}`
f(data, timeField, msgField, timestampsExpected, resultExpected)
}
func TestProcessStreamInternal_Failure(t *testing.T) {
f := func(data string) {
t.Helper()
tlp := &insertutils.TestLogMessageProcessor{}
r := bytes.NewBufferString(data)
if err := processStreamInternal("test", r, "time", nil, tlp); err == nil {
t.Fatalf("expecting non-nil error")
}
}
// invalid json
data = "foobar"
timeField = "@timestamp"
msgField = "aaa"
timestampsExpected = nil
resultExpected = ``
f(data, timeField, msgField, timestampsExpected, resultExpected)
f("foobar")
// invalid timestamp field
data = `{"time":"foobar"}`
timeField = "time"
msgField = "abc"
timestampsExpected = nil
resultExpected = ``
f(data, timeField, msgField, timestampsExpected, resultExpected)
// invalid lines among valid lines
data = `
dsfodmasd
{"time":"2023-06-06T04:48:11.735Z","log":{"offset":71770,"file":{"path":"/var/log/auth.log"}},"message":"foobar"}
invalid line
{"time":"2023-06-06T04:48:12.735+01:00","message":"baz"}
asbsdf
`
timeField = "time"
msgField = "message"
timestampsExpected = []int64{1686026891735000000, 1686023292735000000}
resultExpected = `{"log.offset":"71770","log.file.path":"/var/log/auth.log","_msg":"foobar"}
{"_msg":"baz"}`
f(data, timeField, msgField, timestampsExpected, resultExpected)
f(`{"time":"foobar"}`)
}

View File

@@ -126,18 +126,6 @@ func pushFieldsFromScopeLogs(sc *pb.ScopeLogs, commonFields []logstorage.Field,
Value: attr.Value.FormatString(),
})
}
if len(lr.TraceID) > 0 {
fields = append(fields, logstorage.Field{
Name: "trace_id",
Value: lr.TraceID,
})
}
if len(lr.SpanID) > 0 {
fields = append(fields, logstorage.Field{
Name: "span_id",
Value: lr.SpanID,
})
}
fields = append(fields, logstorage.Field{
Name: "severity",
Value: lr.FormatSeverity(),

View File

@@ -0,0 +1,12 @@
{
"files": {
"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",
"index.html": "./index.html"
},
"entrypoints": [
"static/css/main.02a1c6cb.css",
"static/js/main.55c8060b.js"
]
}

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

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

File diff suppressed because one or more lines are too long

View File

@@ -1,57 +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"/>
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="./manifest.json"/>
<!--
Notice the use of in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>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 type="module" crossorigin src="./assets/index-DokhsDQR.js"></script>
<link rel="modulepreload" crossorigin href="./assets/vendor-DojlIpLz.js">
<link rel="stylesheet" crossorigin href="./assets/vendor-D1GxaB_c.css">
<link rel="stylesheet" crossorigin href="./assets/index-Do1w9kcl.css">
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>
<!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

View File

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

View File

@@ -70,7 +70,7 @@ 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{}
alertsToSend := make([]Alert, 0, len(alerts))
alertsToSend := alerts[:0]
lblss := make([][]prompbmarshal.Label, 0, len(alerts))
for _, a := range alerts {
lbls := a.applyRelabelingIfNeeded(am.relabelConfigs)

View File

@@ -162,7 +162,7 @@ func TestHandler(t *testing.T) {
gotRuleWithUpdates := apiRuleWithUpdates{}
getResp(t, ts.URL+"/"+expRule.APILink(), &gotRuleWithUpdates, 200)
if len(gotRuleWithUpdates.StateUpdates) < 1 {
if gotRuleWithUpdates.StateUpdates == nil || len(gotRuleWithUpdates.StateUpdates) < 1 {
t.Fatalf("expected %+v to have state updates field not empty", gotRuleWithUpdates.StateUpdates)
}
})

View File

@@ -98,7 +98,6 @@ func TestCreateTargetURLSuccess(t *testing.T) {
up, hc := ui.getURLPrefixAndHeaders(u, u.Host, nil)
if up == nil {
t.Fatalf("cannot match available backend: %s", err)
return
}
bu := up.getBackendURL()
target := mergeURLs(bu.url, u, up.dropSrcPathPrefixParts)
@@ -310,7 +309,6 @@ func TestUserInfoGetBackendURL_SRV(t *testing.T) {
up, _ := ui.getURLPrefixAndHeaders(u, u.Host, nil)
if up == nil {
t.Fatalf("cannot match available backend: %s", err)
return
}
bu := up.getBackendURL()
target := mergeURLs(bu.url, u, up.dropSrcPathPrefixParts)

View File

@@ -791,7 +791,7 @@ func LabelNames(qt *querytracer.Tracer, sq *storage.SearchQuery, maxLabelNames i
if err != nil {
return nil, err
}
labels, err := vmstorage.SearchLabelNames(qt, tfss, tr, maxLabelNames, sq.MaxMetrics, deadline.Deadline())
labels, err := vmstorage.SearchLabelNamesWithFiltersOnTimeRange(qt, tfss, tr, maxLabelNames, sq.MaxMetrics, deadline.Deadline())
if err != nil {
return nil, fmt.Errorf("error during labels search on time range: %w", err)
}
@@ -864,7 +864,7 @@ func LabelValues(qt *querytracer.Tracer, labelName string, sq *storage.SearchQue
if err != nil {
return nil, err
}
labelValues, err := vmstorage.SearchLabelValues(qt, labelName, tfss, tr, maxLabelValues, sq.MaxMetrics, deadline.Deadline())
labelValues, err := vmstorage.SearchLabelValuesWithFiltersOnTimeRange(qt, labelName, tfss, tr, maxLabelValues, sq.MaxMetrics, deadline.Deadline())
if err != nil {
return nil, fmt.Errorf("error during label values search on time range for labelName=%q: %w", labelName, err)
}

View File

@@ -39,7 +39,7 @@ var (
"It can be overridden on per-query basis via latency_offset arg. "+
"Too small value can result in incomplete last points for query results")
maxQueryLen = flagutil.NewBytes("search.maxQueryLen", 16*1024, "The maximum search query length in bytes")
maxLookback = flag.Duration("search.maxLookback", 0, "Synonym to -query.lookback-delta from Prometheus. "+
maxLookback = flag.Duration("search.maxLookback", 0, "Synonym to -search.lookback-delta from Prometheus. "+
"The value is dynamically detected from interval between time series datapoints if not set. It can be overridden on per-query basis via max_lookback arg. "+
"See also '-search.maxStalenessInterval' flag, which has the same meaning due to historical reasons")
maxStalenessInterval = flag.Duration("search.maxStalenessInterval", 0, "The maximum interval for staleness calculations. "+

View File

@@ -71,11 +71,6 @@ var (
"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")
disablePerDayIndex = flag.Bool("disablePerDayIndex", false, "Disable per-day index and use global index for all searches. "+
"This may improve performance and decrease disk space usage for the use cases with fixed set of timeseries scattered across a "+
"big time range (for example, when loading years of historical data). "+
"See https://docs.victoriametrics.com/single-server-victoriametrics/#index-tuning")
)
// CheckTimeRange returns true if the given tr is denied for querying.
@@ -115,14 +110,7 @@ func Init(resetCacheIfNeeded func(mrs []storage.MetricRow)) {
logger.Infof("opening storage at %q with -retentionPeriod=%s", *DataPath, retentionPeriod)
startTime := time.Now()
WG = syncwg.WaitGroup{}
opts := storage.OpenOptions{
Retention: retentionPeriod.Duration(),
MaxHourlySeries: *maxHourlySeries,
MaxDailySeries: *maxDailySeries,
DisablePerDayIndex: *disablePerDayIndex,
}
strg := storage.MustOpenStorage(*DataPath, opts)
strg := storage.MustOpenStorage(*DataPath, retentionPeriod.Duration(), *maxHourlySeries, *maxDailySeries)
Storage = strg
initStaleSnapshotsRemover(strg)
@@ -201,19 +189,20 @@ func SearchMetricNames(qt *querytracer.Tracer, tfss []*storage.TagFilters, tr st
return metricNames, err
}
// SearchLabelNames searches for tag keys matching the given tfss on tr.
func SearchLabelNames(qt *querytracer.Tracer, tfss []*storage.TagFilters, tr storage.TimeRange, maxTagKeys, maxMetrics int, deadline uint64) ([]string, error) {
// SearchLabelNamesWithFiltersOnTimeRange searches for tag keys matching the given tfss on tr.
func SearchLabelNamesWithFiltersOnTimeRange(qt *querytracer.Tracer, tfss []*storage.TagFilters, tr storage.TimeRange, maxTagKeys, maxMetrics int, deadline uint64) ([]string, error) {
WG.Add(1)
labelNames, err := Storage.SearchLabelNames(qt, tfss, tr, maxTagKeys, maxMetrics, deadline)
labelNames, err := Storage.SearchLabelNamesWithFiltersOnTimeRange(qt, tfss, tr, maxTagKeys, maxMetrics, deadline)
WG.Done()
return labelNames, err
}
// SearchLabelValues searches for label values for the given labelName, tfss and
// tr.
func SearchLabelValues(qt *querytracer.Tracer, labelName string, tfss []*storage.TagFilters, tr storage.TimeRange, maxLabelValues, maxMetrics int, deadline uint64) ([]string, error) {
// SearchLabelValuesWithFiltersOnTimeRange searches for label values for the given labelName, tfss and tr.
func SearchLabelValuesWithFiltersOnTimeRange(qt *querytracer.Tracer, labelName string, tfss []*storage.TagFilters,
tr storage.TimeRange, maxLabelValues, maxMetrics int, deadline uint64,
) ([]string, error) {
WG.Add(1)
labelValues, err := Storage.SearchLabelValues(qt, labelName, tfss, tr, maxLabelValues, maxMetrics, deadline)
labelValues, err := Storage.SearchLabelValuesWithFiltersOnTimeRange(qt, labelName, tfss, tr, maxLabelValues, maxMetrics, deadline)
WG.Done()
return labelValues, err
}

View File

@@ -1 +1 @@
VITE_APP_TYPE=victoriametrics
FAST_REFRESH=false

View File

@@ -1 +0,0 @@
VITE_APP_TYPE=victorialogs

View File

@@ -1 +0,0 @@
VITE_APP_TYPE=vmanomaly

View File

@@ -0,0 +1,48 @@
// eslint-disable-next-line no-undef
module.exports = {
"env": {
"browser": true,
"es2021": true
},
"extends": [
"eslint:recommended",
"plugin:react/recommended",
"plugin:@typescript-eslint/recommended"
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaFeatures": { "jsx": true },
"ecmaVersion": 12,
"sourceType": "module"
},
"plugins": [
"react",
"@typescript-eslint"
],
"rules": {
"@typescript-eslint/no-unused-vars": ["warn", { "varsIgnorePattern": "^_" }],
"react/jsx-closing-bracket-location": [1, "line-aligned"],
"react/jsx-max-props-per-line":[1, { "maximum": 1 }],
"react/jsx-first-prop-new-line": [1, "multiline"],
"object-curly-spacing": [2, "always"],
"indent": ["error", 2, { "SwitchCase": 1 }],
"linebreak-style": ["error", "unix"],
"quotes": ["error", "double"],
"semi": ["error", "always"],
"react/prop-types": 0
},
"settings": {
"react": {
"pragma": "React", // Pragma to use, default to "React"
"version": "detect"
},
"linkComponents": [
// Components used as alternatives to <a> for linking, eg. <Link to={ url } />
"Hyperlink",
{
"name": "Link", "linkAttribute": "to"
}
]
}
};

View File

@@ -0,0 +1,42 @@
/* eslint-disable */
const { override, addExternalBabelPlugin, addWebpackAlias, addWebpackPlugin } = require("customize-cra");
const webpack = require("webpack");
const fs = require('fs');
const path = require('path');
// This will replace the default check
const pathIndexHTML = (() => {
switch (process.env.REACT_APP_TYPE) {
case 'logs':
return 'src/html/victorialogs.html';
case 'anomaly':
return 'src/html/vmanomaly.html';
default:
return 'src/html/victoriametrics.html';
}
})();
const fileContent = fs.readFileSync(path.resolve(__dirname, pathIndexHTML), 'utf8');
fs.writeFileSync(path.resolve(__dirname, 'public/index.html'), fileContent);
module.exports = override(
addExternalBabelPlugin("@babel/plugin-proposal-nullish-coalescing-operator"),
addWebpackAlias({
"react": "preact/compat",
"react-dom/test-utils": "preact/test-utils",
"react-dom": "preact/compat", // Must be below test-utils
"react/jsx-runtime": "preact/jsx-runtime"
}),
addWebpackPlugin(
new webpack.NormalModuleReplacementPlugin(
/\.\/App/,
function (resource) {
if (process.env.REACT_APP_TYPE === "logs") {
resource.request = "./AppLogs";
}
if (process.env.REACT_APP_TYPE === "anomaly") {
resource.request = "./AppAnomaly";
}
}
)
)
);

View File

@@ -1,23 +0,0 @@
import { readFile } from "fs/promises";
import { IndexHtmlTransform } from "vite";
/**
* Vite plugin to dynamically load index.html based on the current mode.
* If a specific mode-based index file (e.g., index.victorialogs.html) exists, it is used.
* Otherwise, the default index.html is loaded.
*/
export default function dynamicIndexHtmlPlugin({ mode }) {
return {
name: "vm-dynamic-index-html",
transformIndexHtml: {
order: "pre",
handler: async () => {
try {
return await readFile(`./index.${mode}.html`, "utf8");
} catch (error) {
return await readFile("./index.html", "utf8");
}
}
} as IndexHtmlTransform
};
}

View File

@@ -1,90 +0,0 @@
import react from "eslint-plugin-react";
import typescriptEslint from "@typescript-eslint/eslint-plugin";
import globals from "globals";
import tsParser from "@typescript-eslint/parser";
import path from "node:path";
import { fileURLToPath } from "node:url";
import js from "@eslint/js";
import { FlatCompat } from "@eslint/eslintrc";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const compat = new FlatCompat({
baseDirectory: __dirname,
recommendedConfig: js.configs.recommended,
allConfig: js.configs.all
});
export default [...compat.extends(
"eslint:recommended",
"plugin:react/recommended",
"plugin:@typescript-eslint/recommended",
), {
plugins: {
react,
"@typescript-eslint": typescriptEslint,
},
languageOptions: {
globals: {
...globals.browser,
},
parser: tsParser,
ecmaVersion: 12,
sourceType: "module",
parserOptions: {
ecmaFeatures: {
jsx: true,
},
},
},
settings: {
react: {
pragma: "React",
version: "detect",
},
linkComponents: ["Hyperlink", {
name: "Link",
linkAttribute: "to",
}],
},
rules: {
"@typescript-eslint/no-unused-expressions": ["error", {
allowShortCircuit: true,
allowTernary: true
}],
"@typescript-eslint/no-unused-vars": ["warn", {
"argsIgnorePattern": "^_",
"caughtErrors": "none",
"caughtErrorsIgnorePattern": "^_",
"destructuredArrayIgnorePattern": "^_",
"varsIgnorePattern": "^_",
"ignoreRestSiblings": true
}],
"react/jsx-closing-bracket-location": [1, "line-aligned"],
"react/jsx-max-props-per-line": [1, {
maximum: 1,
}],
"react/jsx-first-prop-new-line": [1, "multiline"],
"object-curly-spacing": [2, "always"],
indent: ["error", 2, {
SwitchCase: 1,
}],
"linebreak-style": ["error", "unix"],
quotes: ["error", "double"],
semi: ["error", "always"],
"react/prop-types": 0,
},
}];

File diff suppressed because it is too large Load Diff

View File

@@ -3,40 +3,50 @@
"version": "0.1.0",
"private": true,
"homepage": "./",
"type": "module",
"dependencies": {
"@types/lodash.debounce": "^4.0.9",
"@types/lodash.get": "^4.4.9",
"@types/qs": "^6.9.18",
"@types/react": "^19.0.8",
"@types/react-input-mask": "^3.0.6",
"@types/lodash.throttle": "^4.1.9",
"@types/node": "^22.5.4",
"@types/qs": "^6.9.15",
"@types/react-input-mask": "^3.0.5",
"@types/react-router-dom": "^5.3.3",
"@types/webpack-env": "^1.18.5",
"classnames": "^2.5.1",
"dayjs": "^1.11.13",
"lodash.debounce": "^4.0.8",
"lodash.get": "^4.4.2",
"marked": "^15.0.6",
"marked-emoji": "^1.4.3",
"preact": "^10.25.4",
"qs": "^6.14.0",
"lodash.throttle": "^4.1.1",
"marked": "^14.1.2",
"marked-emoji": "^1.4.2",
"preact": "^10.23.2",
"qs": "^6.13.0",
"react-input-mask": "^2.0.4",
"react-router-dom": "^7.1.5",
"uplot": "^1.6.31",
"vite": "^6.0.11",
"web-vitals": "^4.2.4"
"react-router-dom": "^6.26.2",
"sass": "^1.78.0",
"source-map-explorer": "^2.5.3",
"typescript": "~4.6.2",
"uplot": "^1.6.30",
"web-vitals": "^4.2.3"
},
"scripts": {
"prestart": "npm run copy-metricsql-docs",
"start": "vite",
"start:logs": "vite --mode victorialogs",
"start:anomaly": "vite --mode vmanomaly",
"build": "vite build",
"build:logs": "vite build --mode victorialogs",
"build:anomaly": "vite build --mode vmanomaly",
"lint": "eslint 'src/**/*.{ts,tsx}'",
"lint:fix": "eslint 'src/**/*.{ts,tsx}' --fix",
"copy-metricsql-docs": "cp ../../../../docs/MetricsQL.md src/assets/MetricsQL.md || true",
"preview": "vite preview"
"start": "react-app-rewired start",
"start:logs": "cross-env REACT_APP_TYPE=logs npm run start",
"start:anomaly": "cross-env REACT_APP_TYPE=anomaly npm run start",
"build": "GENERATE_SOURCEMAP=false react-app-rewired build",
"build:logs": "cross-env REACT_APP_TYPE=logs npm run build",
"build:anomaly": "cross-env REACT_APP_TYPE=anomaly npm run build",
"lint": "eslint src --ext tsx,ts",
"lint:fix": "eslint src --ext tsx,ts --fix",
"analyze": "source-map-explorer 'build/static/js/*.js'",
"copy-metricsql-docs": "cp ../../../../docs/MetricsQL.md src/assets/MetricsQL.md || true"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
@@ -51,24 +61,26 @@
]
},
"devDependencies": {
"@babel/plugin-proposal-nullish-coalescing-operator": "^7.18.6",
"@babel/plugin-proposal-nullish-coalescing-operator": "^7.16.7",
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
"@eslint/eslintrc": "^3.2.0",
"@eslint/js": "^9.19.0",
"@preact/preset-vite": "^2.10.1",
"@types/node": "^22.13.0",
"@typescript-eslint/eslint-plugin": "^8.22.0",
"@typescript-eslint/parser": "^8.22.0",
"@types/react-dom": "^18.3.0",
"@typescript-eslint/eslint-plugin": "^5.15.0",
"@typescript-eslint/parser": "^5.15.0",
"cross-env": "^7.0.3",
"eslint": "^9.19.0",
"eslint-plugin-react": "^7.37.4",
"globals": "^15.14.0",
"http-proxy-middleware": "^3.0.3",
"postcss": "^8.5.1",
"rollup-plugin-visualizer": "^5.14.0",
"sass": "^1.83.4",
"sass-embedded": "^1.83.4",
"typescript": "^5.7.3",
"webpack": "^5.97.1"
"customize-cra": "^1.0.0",
"eslint": "^8.44.0",
"eslint-config-react-app": "^7.0.1",
"eslint-plugin-react": "^7.36.1",
"http-proxy-middleware": "^3.0.2",
"react-app-rewired": "^2.2.1",
"webpack": "^5.94.0"
},
"overrides": {
"react-app-rewired": {
"nth-check": "^2.0.1"
},
"css-select": {
"nth-check": "^2.0.1"
}
}
}

View File

@@ -38,4 +38,4 @@ const AppAnomaly: FC = () => {
</>;
};
export default AppAnomaly;
export default AppAnomaly;

View File

@@ -1,6 +1,7 @@
import React, { FC, useCallback, useEffect, useRef, useState, createPortal } from "preact/compat";
import React, { FC, useCallback, useEffect, useRef, useState } from "preact/compat";
import { MouseEvent as ReactMouseEvent } from "react";
import useEventListener from "../../../hooks/useEventListener";
import ReactDOM from "react-dom";
import classNames from "classnames";
import uPlot from "uplot";
import Button from "../../Main/Button/Button";
@@ -48,7 +49,7 @@ const ChartTooltip: FC<ChartTooltipProps> = ({
onClose && onClose(id);
};
const handleMouseDown = (e: ReactMouseEvent<HTMLButtonElement>) => {
const handleMouseDown = (e: ReactMouseEvent) => {
setMoved(true);
setMoving(true);
const { clientX, clientY } = e;
@@ -106,7 +107,7 @@ const ChartTooltip: FC<ChartTooltipProps> = ({
if (!u) return null;
return createPortal((
return ReactDOM.createPortal((
<div
className={classNames({
"vm-chart-tooltip": true,

View File

@@ -1,5 +1,4 @@
@use "src/styles/variables" as *;
@use 'sass:color';
$color-bar: #33BB55;
$color-bar-highest: #F79420;
@@ -8,7 +7,7 @@ $color-bar-highest: #F79420;
display: grid;
grid-template-columns: auto 1fr;
height: 100%;
padding-bottom: calc($font-size-small / 2);
padding-bottom: calc($font-size-small/2);
overflow: hidden;
&-y-axis {
@@ -55,19 +54,19 @@ $color-bar-highest: #F79420;
flex-grow: 1;
width: 100%;
min-width: 1px;
height: calc(100% - ($font-size-small * 4));
height: calc(100% - ($font-size-small*4));
background-color: $color-bar;
transition: background-color 200ms ease-in;
&:hover {
background-color: color.scale($color-bar, $lightness: 40%);
background-color: lighten($color-bar, 10%);
}
&:first-child {
background-color: $color-bar-highest;
&:hover {
background-color: color.scale($color-bar-highest, $lightness: 40%);
background-color: lighten($color-bar-highest, 10%);
}
}
}

View File

@@ -12,11 +12,14 @@ import Timezones from "./Timezones/Timezones";
import ThemeControl from "../ThemeControl/ThemeControl";
import useDeviceDetect from "../../../hooks/useDeviceDetect";
import useBoolean from "../../../hooks/useBoolean";
import { AppType } from "../../../types/appType";
import SwitchMarkdownParsing from "../LogsSettings/MarkdownParsing/SwitchMarkdownParsing";
import { APP_TYPE_LOGS } from "../../../constants/appType";
const title = "Settings";
const { REACT_APP_TYPE } = process.env;
const isLogsApp = REACT_APP_TYPE === AppType.logs;
export interface ChildComponentHandle {
handleApply: () => void;
}
@@ -45,21 +48,21 @@ const GlobalSettings: FC = () => {
const controls = [
{
show: !appModeEnable && !APP_TYPE_LOGS,
show: !appModeEnable && !isLogsApp,
component: <ServerConfigurator
ref={serverSettingRef}
onClose={handleClose}
/>
},
{
show: !APP_TYPE_LOGS,
show: !isLogsApp,
component: <LimitsConfigurator
ref={limitsSettingRef}
onClose={handleClose}
/>
},
{
show: APP_TYPE_LOGS,
show: isLogsApp,
component: <SwitchMarkdownParsing/>
},
{

View File

@@ -9,7 +9,6 @@ import { useCallback } from "react";
import { AUTOCOMPLETE_LIMITS } from "../../../../constants/queryAutocomplete";
import { LogsFiledValues } from "../../../../api/types";
import { useLogsDispatch, useLogsState } from "../../../../state/logsPanel/LogsStateContext";
import { useSearchParams } from "react-router-dom";
type FetchDataArgs = {
urlSuffix: string;
@@ -28,8 +27,6 @@ const icons = {
};
export const useFetchLogsQLOptions = (contextData?: ContextData) => {
const [searchParams] = useSearchParams();
const { serverUrl } = useAppState();
const { period: { start, end } } = useTimeState();
const { autocompleteCache } = useLogsState();
@@ -63,18 +60,11 @@ export const useFetchLogsQLOptions = (contextData?: ContextData) => {
};
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 tenant = {
AccountID: searchParams.get("accountID") || "0",
ProjectID: searchParams.get("projectID") || "0"
};
const tenantString = new URLSearchParams(tenant).toString();
const key = `${urlSuffix}?${params?.toString()}&${tenantString}`;
const key = `${urlSuffix}?${params?.toString()}`;
setLoading(true);
try {
const cachedData = autocompleteCache.get(key);
@@ -83,12 +73,7 @@ export const useFetchLogsQLOptions = (contextData?: ContextData) => {
setLoading(false);
return;
}
const response = await fetch(`${serverUrl}/select/logsql/${urlSuffix}?${params}`, {
signal,
headers: { ...tenant }
});
const response = await fetch(`${serverUrl}/select/logsql/${urlSuffix}?${params}`, { signal });
if (response.ok) {
const data = await response.json();
const value = (data?.values || []) as LogsFiledValues[];

View File

@@ -36,7 +36,7 @@ export class QueryAutocompleteCache {
put(key: QueryAutocompleteCacheItem, value: string[]) {
if (this.map.size >= this.maxSize) {
const firstKey = this.map.keys().next().value;
firstKey && this.map.delete(firstKey);
this.map.delete(firstKey);
}
this.map.set(JSON.stringify(key), value);
}

View File

@@ -1,6 +1,7 @@
import React, { FC, useEffect, useRef, useState } from "preact/compat";
import { KeyboardEvent } from "react";
import { ErrorTypes } from "../../../types";
import TextField, { TextFieldKeyboardEvent } from "../../Main/TextField/TextField";
import TextField from "../../Main/TextField/TextField";
import "./style.scss";
import { QueryStats } from "../../../api/types";
import { partialWarning, seriesFetchedWarning } from "./warningText";
@@ -80,7 +81,7 @@ const QueryEditor: FC<QueryEditorProps> = ({
setCaretPositionInput([caretPosition, caretPosition]);
};
const handleKeyDown = (e: TextFieldKeyboardEvent) => {
const handleKeyDown = (e: KeyboardEvent) => {
const { key, ctrlKey, metaKey, shiftKey } = e;
const value = (e.target as HTMLTextAreaElement).value || "";

View File

@@ -14,8 +14,6 @@ import Popper from "../../Main/Popper/Popper";
import useDeviceDetect from "../../../hooks/useDeviceDetect";
import classNames from "classnames";
import useBoolean from "../../../hooks/useBoolean";
import { useCustomPanelState } from "../../../state/customPanel/CustomPanelStateContext";
import Hyperlink from "../../Main/Hyperlink/Hyperlink";
const StepConfigurator: FC = () => {
const appModeEnable = getAppModeEnable();
@@ -24,20 +22,16 @@ const StepConfigurator: FC = () => {
const { customStep: value, isHistogram } = useGraphState();
const { period: { step, end, start } } = useTimeState();
const graphDispatch = useGraphDispatch();
const { displayType } = useCustomPanelState();
const prevDuration = usePrevious(end - start);
const defaultStep = useMemo(() => {
return getStepFromDuration(end - start, isHistogram, displayType);
}, [end, start, isHistogram, displayType]);
const prevDefaultStep = usePrevious(defaultStep);
return getStepFromDuration(end - start, isHistogram);
}, [step, isHistogram]);
const [customStep, setCustomStep] = useState(value || defaultStep);
const [error, setError] = useState("");
const isAutoStep = value === defaultStep;
const {
value: openOptions,
toggle: toggleOpenOptions,
@@ -107,15 +101,15 @@ const StepConfigurator: FC = () => {
useEffect(() => {
const dur = end - start;
if (dur === prevDuration || !prevDuration || value !== prevDefaultStep) return;
if (dur === prevDuration || !prevDuration) return;
if (defaultStep) {
handleApply(defaultStep);
}
}, [prevDuration, defaultStep]);
}, [end, start, prevDuration, defaultStep]);
useEffect(() => {
if (step === value || step === defaultStep) handleApply(defaultStep);
}, [isHistogram, displayType]);
}, [isHistogram]);
return (
<div
@@ -135,15 +129,22 @@ const StepConfigurator: FC = () => {
<span className="vm-mobile-option__arrow"><ArrowDownIcon/></span>
</div>
) : (
<Button
className={appModeEnable ? "" : "vm-header-button"}
variant="contained"
color="primary"
startIcon={<TimelineIcon/>}
onClick={toggleOpenOptions}
>
Step: {isAutoStep ? `auto (${customStep})` : customStep}
</Button>
<Tooltip title="Query resolution step width">
<Button
className={appModeEnable ? "" : "vm-header-button"}
variant="contained"
color="primary"
startIcon={<TimelineIcon/>}
onClick={toggleOpenOptions}
>
<p>
STEP
<p className="vm-step-control__value">
{customStep}
</p>
</p>
</Button>
</Tooltip>
)}
<Popper
open={openOptions}
@@ -168,7 +169,7 @@ const StepConfigurator: FC = () => {
onFocus={handleFocus}
onBlur={handleApply}
endIcon={(
<Tooltip title={`Reset to auto step (${defaultStep})`}>
<Tooltip title={`Set default step value: ${defaultStep}`}>
<Button
size="small"
variant="text"
@@ -181,23 +182,25 @@ const StepConfigurator: FC = () => {
)}
/>
<div className="vm-step-control-popper-info">
<p>
<code>step</code> - the <Hyperlink
href="https://prometheus.io/docs/prometheus/latest/querying/basics/#time-durations"
text="interval"
/> between datapoints, which must be returned from the range query.
The <code>query</code> is executed
at <code>start</code>, <code>start+step</code>, <code>start+2*step</code>, , <code>end</code> timestamps.
</p>
<p>
Read more about <Hyperlink
href="https://docs.victoriametrics.com/keyConcepts.html#range-query"
text="Range"
/> and <Hyperlink
href="https://docs.victoriametrics.com/keyconcepts/#instant-query"
text="Instant"
/> queries.
</p>
<code>step</code> - the <a
className="vm-link vm-link_colored"
href="https://prometheus.io/docs/prometheus/latest/querying/basics/#time-durations"
target="_blank"
rel="noreferrer"
>
interval
</a>
between datapoints, which must be returned from the range query.
The <code>query</code> is executed at
<code>start</code>, <code>start+step</code>, <code>start+2*step</code>, , <code>end</code> timestamps.
<a
className="vm-link vm-link_colored"
href="https://docs.victoriametrics.com/keyConcepts.html#range-query"
target="_blank"
rel="help noreferrer"
>
Read more about Range query
</a>
</div>
</div>
</Popper>

View File

@@ -8,10 +8,16 @@
text-transform: none;
}
&__value {
display: inline;
margin-left: 3px;
}
&-popper {
display: grid;
gap: $padding-small;
max-width: 300px;
max-height: 208px;
overflow: auto;
padding: $padding-global;
font-size: $font-size;
@@ -30,11 +36,13 @@
font-size: $font-size-small;
line-height: 1.8;
a {
margin: 0 0.4em;
}
code {
padding: .2em .4em;
margin: 0;
font-size: 85%;
white-space: break-spaces;
padding: 0.2em 0.4em;
margin: 0 0.2em;
background-color: $color-hover-black;
border-radius: 6px;
}

View File

@@ -14,8 +14,8 @@ interface ButtonProps {
disabled?: boolean
children?: ReactNode
className?: string
onClick?: (e: ReactMouseEvent<HTMLButtonElement>) => void
onMouseDown?: (e: ReactMouseEvent<HTMLButtonElement>) => void
onClick?: (e: ReactMouseEvent<HTMLButtonElement, MouseEvent>) => void
onMouseDown?: (e: ReactMouseEvent<HTMLButtonElement, MouseEvent>) => void
}
const Button: FC<ButtonProps> = ({

View File

@@ -46,7 +46,7 @@ const DateTimeInput: FC<DateTimeInputProps> = ({
onChange(maskedValue);
};
const handleKeyUp = (e: KeyboardEvent<HTMLInputElement>) => {
const handleKeyUp = (e: KeyboardEvent) => {
if (e.key === "Enter") {
onChange(maskedValue);
setAwaitChangeForEnter(true);

View File

@@ -89,8 +89,7 @@ const TimePicker: FC<CalendarTimepickerProps>= ({ selectDate, onChangeTime, onCl
};
const handleFocusInput = (unit: TimeUnits, e: FocusEvent<HTMLInputElement>) => {
const target = e.target as HTMLInputElement;
target && target.select();
e.target.select();
setActiveField(unit);
};

View File

@@ -1,4 +1,5 @@
import React, { FC, useCallback, useEffect, createPortal } from "preact/compat";
import React, { FC, useCallback, useEffect } from "preact/compat";
import ReactDOM from "react-dom";
import { CloseIcon } from "../Icons";
import Button from "../Button/Button";
import { ReactNode, MouseEvent } from "react";
@@ -57,7 +58,7 @@ const Modal: FC<ModalProps> = ({
useEventListener("popstate", handlePopstate);
useEventListener("keyup", handleKeyUp);
return createPortal((
return ReactDOM.createPortal((
<div
className={classNames({
"vm-modal": true,

View File

@@ -1,15 +1,6 @@
import React, {
FC,
MouseEvent as ReactMouseEvent,
ReactNode,
useEffect,
useMemo,
useRef,
useState,
useCallback,
createPortal
} from "react";
import React, { FC, MouseEvent as ReactMouseEvent, ReactNode, useEffect, useMemo, useRef, useState } from "react";
import classNames from "classnames";
import ReactDOM from "react-dom";
import "./style.scss";
import useClickOutside from "../../../hooks/useClickOutside";
import useDeviceDetect from "../../../hooks/useDeviceDetect";
@@ -17,6 +8,7 @@ import Button from "../Button/Button";
import { CloseIcon } from "../Icons";
import { useLocation, useNavigate } from "react-router-dom";
import useEventListener from "../../../hooks/useEventListener";
import { useCallback } from "preact/compat";
interface PopperProps {
children: ReactNode
@@ -127,7 +119,7 @@ const Popper: FC<PopperProps> = ({
return position;
}, [buttonRef, placement, isOpen, children, fullWidth]);
const handleClickClose = (e: ReactMouseEvent<HTMLButtonElement>) => {
const handleClickClose = (e: ReactMouseEvent<HTMLButtonElement, MouseEvent>) => {
e.stopPropagation();
onClose();
};
@@ -164,7 +156,7 @@ const Popper: FC<PopperProps> = ({
return (
<>
{(isOpen || !popperSize.width) && createPortal((
{(isOpen || !popperSize.width) && ReactDOM.createPortal((
<div
className={classNames({
"vm-popper": true,

View File

@@ -11,7 +11,7 @@ interface MultipleSelectedValueProps {
const MultipleSelectedValue: FC<MultipleSelectedValueProps> = ({ values, onRemoveItem }) => {
const { isMobile } = useDeviceDetect();
const createHandleClick = (value: string) => (e: MouseEvent<HTMLDivElement>) => {
const createHandleClick = (value: string) => (e: MouseEvent) => {
onRemoveItem(value);
e.stopPropagation();
};

View File

@@ -92,7 +92,7 @@ const Select: FC<SelectProps> = ({
setSearch((e.target as HTMLInputElement).value);
};
const createHandleClick = (value: string) => (e: MouseEvent<HTMLDivElement>) => {
const createHandleClick = (value: string) => (e: MouseEvent) => {
handleSelected(value);
e.stopPropagation();
};

View File

@@ -15,8 +15,6 @@ import useDeviceDetect from "../../../hooks/useDeviceDetect";
import TextFieldMessage from "./TextFieldMessage";
import "./style.scss";
export type TextFieldKeyboardEvent = KeyboardEvent<HTMLInputElement | HTMLTextAreaElement>;
interface TextFieldProps {
label?: string,
value?: string | number
@@ -33,7 +31,7 @@ interface TextFieldProps {
caretPosition?: [number, number]
onChange?: (value: string) => void
onEnter?: () => void
onKeyDown?: (e: TextFieldKeyboardEvent) => void
onKeyDown?: (e: KeyboardEvent) => void
onFocus?: () => void
onBlur?: () => void
onChangeCaret?: (position: [number, number]) => void
@@ -86,7 +84,7 @@ const TextField: FC<TextFieldProps> = ({
updateCaretPosition(e.currentTarget);
};
const handleKeyDown = (e: TextFieldKeyboardEvent) => {
const handleKeyDown = (e: KeyboardEvent<HTMLInputElement | HTMLTextAreaElement>) => {
onKeyDown && onKeyDown(e);
const { key, ctrlKey, metaKey } = e;
const isEnter = key === "Enter";
@@ -97,7 +95,7 @@ const TextField: FC<TextFieldProps> = ({
}
};
const handleKeyUp = (e: TextFieldKeyboardEvent) => {
const handleKeyUp = (e: KeyboardEvent<HTMLInputElement | HTMLTextAreaElement>) => {
updateCaretPosition(e.currentTarget);
};

View File

@@ -1,6 +1,8 @@
import React, { FC, useEffect, useMemo, useRef, useState, Fragment, createPortal } from "preact/compat";
import React, { FC, useEffect, useMemo, useRef, useState, Fragment } from "preact/compat";
import ReactDOM from "react-dom";
import "./style.scss";
import { ReactNode } from "react";
import { ExoticComponent } from "react";
import useDeviceDetect from "../../../hooks/useDeviceDetect";
interface TooltipProps {
@@ -23,7 +25,7 @@ const Tooltip: FC<TooltipProps> = ({
const [isOpen, setIsOpen] = useState(false);
const [popperSize, setPopperSize] = useState({ width: 0, height: 0 });
const buttonRef = useRef<ReactNode>(null);
const buttonRef = useRef<ExoticComponent>(null);
const popperRef = useRef<HTMLDivElement>(null);
const onScrollWindow = () => setIsOpen(false);
@@ -118,7 +120,7 @@ const Tooltip: FC<TooltipProps> = ({
{children}
</Fragment>
{!isMobile && isOpen && createPortal((
{!isMobile && isOpen && ReactDOM.createPortal((
<div
className="vm-tooltip"
ref={popperRef}

View File

@@ -8,8 +8,8 @@ import Switch from "../../Main/Switch/Switch";
import { arrayEquals } from "../../../utils/array";
import classNames from "classnames";
import useBoolean from "../../../hooks/useBoolean";
import TextField, { TextFieldKeyboardEvent } from "../../Main/TextField/TextField";
import { useState } from "react";
import TextField from "../../Main/TextField/TextField";
import { KeyboardEvent, useState } from "react";
import Modal from "../../Main/Modal/Modal";
import { useSearchParams } from "react-router-dom";
@@ -96,7 +96,7 @@ const TableSettings: FC<TableSettingsProps> = ({
setIndexFocusItem(-1);
};
const handleKeyDown = (e: TextFieldKeyboardEvent) => {
const handleKeyDown = (e: KeyboardEvent) => {
const arrowUp = e.key === "ArrowUp";
const arrowDown = e.key === "ArrowDown";
const enter = e.key === "Enter";

View File

@@ -6,8 +6,8 @@ const dateColumns = ["date", "timestamp", "time"];
export function descendingComparator<T>(a: T, b: T, orderBy: keyof T) {
const valueA = a[orderBy];
const valueB = b[orderBy];
const parsedValueA = dateColumns.includes(String(orderBy)) ? dayjs(`${valueA}`).unix() : valueA;
const parsedValueB = dateColumns.includes(String(orderBy)) ? dayjs(`${valueB}`).unix() : valueB;
const parsedValueA = dateColumns.includes(`${orderBy}`) ? dayjs(`${valueA}`).unix() : valueA;
const parsedValueB = dateColumns.includes(`${orderBy}`) ? dayjs(`${valueB}`).unix() : valueB;
if (parsedValueB < parsedValueA) {
return -1;
}

View File

@@ -1,12 +0,0 @@
export enum AppType {
victoriametrics = "victoriametrics",
victorialogs = "victorialogs",
vmanomaly = "vmanomaly",
}
export const APP_TYPE = import.meta.env.VITE_APP_TYPE;
export const APP_TYPE_VM = APP_TYPE === AppType.victoriametrics;
export const APP_TYPE_LOGS = APP_TYPE === AppType.victorialogs;
export const APP_TYPE_ANOMALY = APP_TYPE === AppType.vmanomaly;

View File

@@ -1,7 +1,6 @@
import { useAppDispatch } from "../state/common/StateContext";
import { useEffect, useState } from "preact/compat";
import { ErrorTypes } from "../types";
import { APP_TYPE_VM } from "../constants/appType";
const useFetchFlags = () => {
const dispatch = useAppDispatch();
@@ -11,7 +10,7 @@ const useFetchFlags = () => {
useEffect(() => {
const fetchAppConfig = async () => {
if (!APP_TYPE_VM) return;
if (process.env.REACT_APP_TYPE) return;
setError("");
setIsLoading(true);

View File

@@ -5,7 +5,6 @@ import { useTimeDispatch } from "../state/time/TimeStateContext";
import { getFromStorage } from "../utils/storage";
import dayjs from "dayjs";
import { getBrowserTimezone } from "../utils/time";
import { APP_TYPE_VM } from "../constants/appType";
const disabledDefaultTimezone = Boolean(getFromStorage("DISABLED_DEFAULT_TIMEZONE"));
@@ -29,7 +28,7 @@ const useFetchDefaultTimezone = () => {
};
const fetchDefaultTimezone = async () => {
if (!serverUrl || !APP_TYPE_VM) return;
if (!serverUrl || process.env.REACT_APP_TYPE) return;
setError("");
setIsLoading(true);

View File

@@ -1,7 +1,6 @@
import { useAppDispatch, useAppState } from "../state/common/StateContext";
import { useEffect, useState } from "preact/compat";
import { ErrorTypes } from "../types";
import { APP_TYPE_VM } from "../constants/appType";
const useFetchFlags = () => {
const { serverUrl } = useAppState();
@@ -12,7 +11,7 @@ const useFetchFlags = () => {
useEffect(() => {
const fetchFlags = async () => {
if (!serverUrl || !APP_TYPE_VM) return;
if (!serverUrl || process.env.REACT_APP_TYPE) return;
setError("");
setIsLoading(true);

View File

@@ -12,8 +12,8 @@ import { useCustomPanelState } from "../state/customPanel/CustomPanelStateContex
import { isHistogramData } from "../utils/metric";
import { useGraphState } from "../state/graph/GraphStateContext";
import { getStepFromDuration } from "../utils/time";
import { AppType } from "../types/appType";
import { getQueryStringValue } from "../utils/query-string";
import { APP_TYPE_ANOMALY } from "../constants/appType";
interface FetchQueryParams {
predefinedQuery?: string[]
@@ -49,6 +49,8 @@ interface FetchDataParams {
hideQuery?: number[]
}
const isAnomalyUI = AppType.anomaly === process.env.REACT_APP_TYPE;
export const useFetchQuery = ({
predefinedQuery,
visible,
@@ -76,8 +78,8 @@ export const useFetchQuery = ({
const defaultStep = useMemo(() => {
const { end, start } = period;
return getStepFromDuration(end - start, isHistogramState, displayType);
}, [period, isHistogramState, displayType]);
return getStepFromDuration(end - start, isHistogramState);
}, [period, isHistogramState]);
const fetchData = async ({
fetchUrl,
@@ -132,7 +134,7 @@ export const useFetchQuery = ({
}
const preventChangeType = !!getQueryStringValue("display_mode", null);
isHistogramResult = !APP_TYPE_ANOMALY && isDisplayChart && !preventChangeType && isHistogramData(resp.data.result);
isHistogramResult = !isAnomalyUI && isDisplayChart && !preventChangeType && isHistogramData(resp.data.result);
seriesLimit = isHistogramResult ? Infinity : defaultLimit;
const freeTempSize = seriesLimit - tempData.length;
resp.data.result.slice(0, freeTempSize).forEach((d: MetricBase) => {

View File

@@ -2,9 +2,9 @@
<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">
<link rel="icon" href="%PUBLIC_URL%/favicon.svg" />
<link rel="apple-touch-icon" href="%PUBLIC_URL%/favicon.svg" />
<link rel="mask-icon" href="%PUBLIC_URL%/favicon.svg" color="#000000">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=5"/>
<meta name="theme-color" content="#000000"/>
@@ -13,13 +13,13 @@
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="/manifest.json"/>
<link rel="manifest" href="%PUBLIC_URL%/manifest.json"/>
<!--
Notice the use of in the tags above.
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "/favicon.ico" will
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
@@ -29,7 +29,7 @@
<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 name="twitter:image" content="%PUBLIC_URL%/preview.jpg">
<meta property="og:type" content="website">
<meta property="og:title" content="UI for VictoriaLogs">
@@ -49,6 +49,5 @@
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
<script type="module" src="/src/index.tsx"></script>
</body>
</html>

View File

@@ -2,9 +2,9 @@
<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">
<link rel="icon" href="%PUBLIC_URL%/favicon.svg" />
<link rel="apple-touch-icon" href="%PUBLIC_URL%/favicon.svg" />
<link rel="mask-icon" href="%PUBLIC_URL%/favicon.svg" color="#000000">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=5"/>
<meta name="theme-color" content="#000000"/>
@@ -13,24 +13,24 @@
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="/manifest.json"/>
<link rel="manifest" href="%PUBLIC_URL%/manifest.json"/>
<!--
Notice the use of in the tags above.
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "/favicon.ico" will
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>vmui</title>
<script src="/dashboards/index.js" type="module"></script>
<script src="%PUBLIC_URL%/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 name="twitter:image" content="%PUBLIC_URL%/preview.jpg">
<meta property="og:type" content="website">
<meta property="og:title" content="UI for VictoriaMetrics">
@@ -50,6 +50,5 @@
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
<script type="module" src="/src/index.tsx"></script>
</body>
</html>

View File

@@ -2,9 +2,9 @@
<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">
<link rel="icon" href="%PUBLIC_URL%/favicon.svg" />
<link rel="apple-touch-icon" href="%PUBLIC_URL%/favicon.svg" />
<link rel="mask-icon" href="%PUBLIC_URL%/favicon.svg" color="#000000">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=5"/>
<meta name="theme-color" content="#000000"/>
@@ -13,13 +13,13 @@
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="/manifest.json"/>
<link rel="manifest" href="%PUBLIC_URL%/manifest.json"/>
<!--
Notice the use of in the tags above.
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "/favicon.ico" will
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
@@ -29,7 +29,7 @@
<meta name="twitter:title" content="UI for VictoriaMetrics Anomaly Detection">
<meta name="twitter:site" content="@https://victoriametrics.com/products/enterprise/anomaly-detection/">
<meta name="twitter:description" content="Detect anomalies in your metrics with VictoriaMetrics Anomaly Detection UI">
<meta name="twitter:image" content="/preview.jpg">
<meta name="twitter:image" content="%PUBLIC_URL%/preview.jpg">
<meta property="og:type" content="website">
<meta property="og:title" content="UI for VictoriaMetrics Anomaly Detection">
@@ -49,6 +49,5 @@
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
<script type="module" src="/src/index.tsx"></script>
</body>
</html>

View File

@@ -3,23 +3,9 @@ import "./constants/dayjsPlugins";
import App from "./App";
import reportWebVitals from "./reportWebVitals";
import "./styles/style.scss";
import { APP_TYPE, AppType } from "./constants/appType";
import AppLogs from "./AppLogs";
import AppAnomaly from "./AppAnomaly";
const getAppComponent = () => {
switch (APP_TYPE) {
case AppType.victorialogs:
return <AppLogs/>;
case AppType.vmanomaly:
return <AppAnomaly/>;
default:
return <App/>;
}
};
const root = document.getElementById("root");
if (root) render(getAppComponent(), root);
if (root) render(<App />, root);
// If you want to start measuring performance in your app, pass a function

View File

@@ -13,16 +13,19 @@ import HeaderControls, { ControlsProps } from "./HeaderControls/HeaderControls";
import useDeviceDetect from "../../hooks/useDeviceDetect";
import useWindowSize from "../../hooks/useWindowSize";
import { ComponentType } from "react";
import { APP_TYPE, AppType } from "../../constants/appType";
import { AppType } from "../../types/appType";
export interface HeaderProps {
controlsComponent: ComponentType<ControlsProps>
}
const { REACT_APP_TYPE } = process.env;
const isCustomApp = REACT_APP_TYPE === AppType.logs || REACT_APP_TYPE === AppType.anomaly;
const Logo = () => {
switch (APP_TYPE) {
case AppType.victorialogs:
switch (REACT_APP_TYPE) {
case AppType.logs:
return <LogoLogsIcon/>;
case AppType.vmanomaly:
case AppType.anomaly:
return <LogoAnomalyIcon/>;
default:
return <LogoIcon/>;
@@ -78,7 +81,10 @@ const Header: FC<HeaderProps> = ({ controlsComponent }) => {
<>
{!appModeEnable && (
<div
className="vm-header-logo"
className={classNames({
"vm-header-logo": true,
"vm-header-logo_logs": isCustomApp
})}
onClick={onClickLogo}
style={{ color }}
>
@@ -96,6 +102,7 @@ const Header: FC<HeaderProps> = ({ controlsComponent }) => {
className={classNames({
"vm-header-logo": true,
"vm-header-logo_mobile": true,
"vm-header-logo_logs": isCustomApp
})}
onClick={onClickLogo}
style={{ color }}

View File

@@ -8,13 +8,16 @@ import MenuBurger from "../../../components/Main/MenuBurger/MenuBurger";
import useDeviceDetect from "../../../hooks/useDeviceDetect";
import "./style.scss";
import useBoolean from "../../../hooks/useBoolean";
import { APP_TYPE_LOGS } from "../../../constants/appType";
import { AppType } from "../../../types/appType";
interface SidebarHeaderProps {
background: string
color: string
}
const { REACT_APP_TYPE } = process.env;
const isLogsApp = REACT_APP_TYPE === AppType.logs;
const SidebarHeader: FC<SidebarHeaderProps> = ({
background,
color,
@@ -61,7 +64,7 @@ const SidebarHeader: FC<SidebarHeaderProps> = ({
/>
</div>
<div className="vm-header-sidebar-menu-settings">
{!isMobile && !APP_TYPE_LOGS && <ShortcutKeys showTitle={true}/>}
{!isMobile && !isLogsApp && <ShortcutKeys showTitle={true}/>}
</div>
</div>
</div>;

View File

@@ -47,14 +47,14 @@
justify-content: flex-start;
cursor: pointer;
width: 100%;
max-width: 75px;
min-width: 75px;
max-width: 65px;
min-width: 65px;
margin-bottom: 2px;
overflow: hidden;
svg {
max-width: 75px;
min-width: 75px;
max-width: 65px;
min-width: 65px;
}
&_mobile {
@@ -62,5 +62,10 @@
min-width: 65px;
margin: 0 auto;
}
&_logs, &_logs svg {
max-width: 75px;
min-width: 75px;
}
}
}

View File

@@ -15,7 +15,7 @@ const EnhancedTable: FC<TableProps> = ({
const [orderBy, setOrderBy] = useState<keyof Data>(defaultSortColumn);
const handleRequestSort = (
event: MouseEvent<HTMLTableCellElement>,
event: MouseEvent<unknown>,
property: keyof Data,
) => {
const isAsc = orderBy === property && order === "asc";

View File

@@ -7,7 +7,7 @@ import Tooltip from "../../../components/Main/Tooltip/Tooltip";
export function EnhancedTableHead(props: EnhancedHeaderTableProps) {
const { order, orderBy, onRequestSort, headerCells } = props;
const createSortHandler = (property: keyof Data) => (event: MouseEvent<HTMLTableCellElement>) => {
const createSortHandler = (property: keyof Data) => (event: MouseEvent<unknown>) => {
onRequestSort(event, property);
};

View File

@@ -9,7 +9,7 @@ export interface HeadCell {
}
export interface EnhancedHeaderTableProps {
onRequestSort: (event: MouseEvent<HTMLTableCellElement>, property: keyof Data) => void;
onRequestSort: (event: MouseEvent<unknown>, property: keyof Data) => void;
order: Order;
orderBy: string;
rowCount: number;

View File

@@ -3,11 +3,10 @@ import { useCustomPanelDispatch, useCustomPanelState } from "../../state/customP
import { ChartIcon, CodeIcon, TableIcon } from "../../components/Main/Icons";
import Tabs from "../../components/Main/Tabs/Tabs";
import { DisplayType } from "../../types";
import { ReactNode } from "react";
type DisplayTab = {
value: DisplayType
icon: ReactNode
icon: JSX.Element
label: string
prometheusCode: number
}

View File

@@ -114,7 +114,7 @@ const QueryConfigurator: FC<QueryConfiguratorProps> = ({
setStateQuery(prev => prev.filter((q, i) => i !== index));
};
const handleToggleHideQuery = (e: ReactMouseEvent<HTMLButtonElement>, index: number) => {
const handleToggleHideQuery = (e: ReactMouseEvent<HTMLButtonElement, MouseEvent>, index: number) => {
const { ctrlKey, metaKey } = e;
const ctrlMetaKey = ctrlKey || metaKey;
@@ -160,7 +160,7 @@ const QueryConfigurator: FC<QueryConfiguratorProps> = ({
setHideQuery(prev => prev.includes(i) ? prev.filter(n => n !== i) : prev.map(n => n > i ? n - 1 : n));
};
const createHandlerHideQuery = (i: number) => (e: ReactMouseEvent<HTMLButtonElement>) => {
const createHandlerHideQuery = (i: number) => (e: ReactMouseEvent<HTMLButtonElement, MouseEvent>) => {
handleToggleHideQuery(e, i);
};

View File

@@ -1,5 +1,4 @@
@use "src/styles/variables" as *;
@use 'sass:color';
$font-size-logs: var(--font-size-logs, $font-size-small);
@@ -80,8 +79,8 @@ $font-size-logs: var(--font-size-logs, $font-size-small);
&__pair {
order: 0;
padding: calc($padding-global / 2) $padding-global;
background-color: color.scale($color-tropical-blue, $lightness: 60%);
color: color.scale($color-tropical-blue, $lightness: -60%);
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;
@@ -106,7 +105,7 @@ $font-size-logs: var(--font-size-logs, $font-size-small);
}
&_dark {
color: color.scale($color-dodger-blue, $lightness: 40%);
color: lighten($color-dodger-blue, 20%);
background-color: $color-background-body;
opacity: 0.8;

View File

@@ -44,7 +44,7 @@ const PredefinedDashboard: FC<PredefinedDashboardProps> = ({
setPanelsWidth(width);
}, [resize, sizeSection]);
const handleMouseDown = (e: ReactMouseEvent<HTMLButtonElement>, i: number) => {
const handleMouseDown = (e: ReactMouseEvent<HTMLButtonElement, MouseEvent>, i: number) => {
setResize({
start: e.clientX,
target: i,

View File

@@ -3,7 +3,6 @@ import { DashboardSettings, ErrorTypes } from "../../../types";
import { useAppState } from "../../../state/common/StateContext";
import { useDashboardsDispatch } from "../../../state/dashboards/DashboardsStateContext";
import { getAppModeEnable } from "../../../utils/app-mode";
import { APP_TYPE_VM } from "../../../constants/appType";
const importModule = async (filename: string) => {
const data = await fetch(`./dashboards/${filename}`);
@@ -35,7 +34,7 @@ export const useFetchDashboards = (): {
};
const fetchRemoteDashboards = async () => {
if (!serverUrl || !APP_TYPE_VM) return;
if (!serverUrl || process.env.REACT_APP_TYPE) return;
setError("");
setIsLoading(true);

View File

@@ -108,12 +108,10 @@ const QueryAnalyzer: FC = () => {
};
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
if (!e.target) return;
const target = e.target as HTMLInputElement;
setError("");
const files = Array.from(target.files || []);
const files = Array.from(e.target.files || []);
handleReadFiles(files);
target.value = "";
e.target.value = "";
};
const handleCloseError = () => {

View File

@@ -1,4 +1,4 @@
import React, { FC, useEffect, useMemo } from "react";
import React, { FC, useEffect, useMemo, KeyboardEvent } from "react";
import { useFetchTopQueries } from "./hooks/useFetchTopQueries";
import Spinner from "../../components/Main/Spinner/Spinner";
import TopQueryPanel from "./TopQueryPanel/TopQueryPanel";
@@ -8,7 +8,7 @@ import dayjs from "dayjs";
import { TopQueryStats } from "../../types";
import Button from "../../components/Main/Button/Button";
import { PlayIcon } from "../../components/Main/Icons";
import TextField, { TextFieldKeyboardEvent } from "../../components/Main/TextField/TextField";
import TextField from "../../components/Main/TextField/TextField";
import Alert from "../../components/Main/Alert/Alert";
import Tooltip from "../../components/Main/Tooltip/Tooltip";
import "./style.scss";
@@ -55,7 +55,7 @@ const TopQueries: FC = () => {
setMaxLifetime(value);
};
const onKeyDown = (e: TextFieldKeyboardEvent) => {
const onKeyDown = (e: KeyboardEvent) => {
if (e.key === "Enter") fetch();
};

View File

@@ -56,12 +56,10 @@ const TracePage: FC = () => {
};
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
if (!e.target) return;
const target = e.target as HTMLInputElement;
setErrors([]);
const files = Array.from(target.files || []);
const files = Array.from(e.target.files || []);
handleReadFiles(files);
target.value = "";
e.target.value = "";
};
const handleTraceDelete = (trace: Trace) => {

View File

@@ -0,0 +1 @@
/// <reference types="react-scripts" />

View File

@@ -1,4 +1,4 @@
import { APP_TYPE_LOGS } from "../constants/appType";
import { AppType } from "../types/appType";
const router = {
home: "/",
@@ -34,12 +34,15 @@ export interface RouterOptions {
header: RouterOptionsHeader
}
const { REACT_APP_TYPE } = process.env;
const isLogsApp = REACT_APP_TYPE === AppType.logs;
const routerOptionsDefault = {
header: {
tenant: true,
stepControl: !APP_TYPE_LOGS,
timeSelector: !APP_TYPE_LOGS,
executionControls: !APP_TYPE_LOGS,
stepControl: !isLogsApp,
timeSelector: !isLogsApp,
executionControls: !isLogsApp,
}
};

View File

@@ -2,9 +2,11 @@ import { getAppModeEnable } from "../utils/app-mode";
import { useDashboardsState } from "../state/dashboards/DashboardsStateContext";
import { useAppState } from "../state/common/StateContext";
import { useMemo } from "preact/compat";
import { AppType } from "../types/appType";
import { processNavigationItems } from "./utils";
import { getAnomalyNavigation, getDefaultNavigation, getLogsNavigation } from "./navigation";
import { APP_TYPE, AppType } from "../constants/appType";
const appType = process.env.REACT_APP_TYPE;
const useNavigationMenu = () => {
const appModeEnable = getAppModeEnable();
@@ -23,10 +25,10 @@ const useNavigationMenu = () => {
const menu = useMemo(() => {
switch (APP_TYPE) {
case AppType.victorialogs:
switch (appType) {
case AppType.logs:
return getLogsNavigation();
case AppType.vmanomaly:
case AppType.anomaly:
return getAnomalyNavigation();
default:
return getDefaultNavigation(navigationConfig);

View File

@@ -28,7 +28,7 @@ export function reducer(state: LogsState, action: LogsAction): LogsState {
case "SET_AUTOCOMPLETE_CACHE": {
if (state.autocompleteCache.size >= AUTOCOMPLETE_LIMITS.cacheLimit) {
const firstKey = state.autocompleteCache.keys().next().value;
firstKey && state.autocompleteCache.delete(firstKey);
state.autocompleteCache.delete(firstKey);
}
state.autocompleteCache.set(action.payload.key, action.payload.value);

View File

@@ -0,0 +1,4 @@
export enum AppType {
logs = "logs",
anomaly = "anomaly",
}

View File

@@ -4,7 +4,7 @@ export const arrayEquals = (a: (string|number)[], b: (string|number)[]) => {
export function groupByMultipleKeys<T>(items: T[], keys: (keyof T)[]): { keys: string[], values: T[] }[] {
const groups = items.reduce((result, item) => {
const compositeKey = keys.map(key => `${String(key)}: ${item[key] || "-"}`).join("|");
const compositeKey = keys.map(key => `${key}: ${item[key] || "-"}`).join("|");
(result[compositeKey] = result[compositeKey] || []).push(item);

View File

@@ -1,12 +1,12 @@
import React, { ComponentProps, FC, ReactNode } from "react";
import React, { ComponentProps, FC } from "react";
type Props = { children: ReactNode };
type Props = { children: JSX.Element };
export const combineComponents = (...components: FC<Props>[]): FC<Props> => {
return components.reduce(
(AccumulatedComponents, CurrentComponent) => {
// eslint-disable-next-line react/display-name
return ({ children }: ComponentProps<FC<Props>>): ReactNode => (
return ({ children }: ComponentProps<FC<Props>>): JSX.Element => (
<AccumulatedComponents>
<CurrentComponent>{children}</CurrentComponent>
</AccumulatedComponents>

View File

@@ -1,7 +1,8 @@
import { getAppModeParams } from "./app-mode";
import { replaceTenantId } from "./tenants";
import { APP_TYPE, AppType } from "../constants/appType";
import { AppType } from "../types/appType";
import { getFromStorage } from "./storage";
const { REACT_APP_TYPE } = process.env;
export const getDefaultServer = (tenantId?: string): string => {
const { serverURL } = getAppModeParams();
@@ -11,10 +12,10 @@ export const getDefaultServer = (tenantId?: string): string => {
const defaultURL = window.location.href.replace(/\/(?:prometheus\/)?(?:graph|vmui)\/.*/, "/prometheus");
const url = serverURL || storageURL || defaultURL;
switch (APP_TYPE) {
case AppType.victorialogs:
switch (REACT_APP_TYPE) {
case AppType.logs:
return logsURL;
case AppType.vmanomaly:
case AppType.anomaly:
return storageURL || anomalyURL;
default:
return tenantId ? replaceTenantId(url, tenantId) : url;

View File

@@ -1,9 +1,9 @@
import { DisplayType, RelativeTimeOption, TimeParams, TimePeriod, Timezone } from "../types";
import { RelativeTimeOption, TimeParams, TimePeriod, Timezone } from "../types";
import dayjs, { UnitTypeShort } from "dayjs";
import { getQueryStringValue } from "./query-string";
import { DATE_ISO_FORMAT } from "../constants/date";
import timezones from "../constants/timezones";
import { APP_TYPE_LOGS } from "../constants/appType";
import { AppType } from "../types/appType";
const MAX_ITEMS_PER_CHART = window.innerWidth / 4;
const MAX_ITEMS_PER_HISTOGRAM = window.innerWidth / 40;
@@ -88,10 +88,7 @@ export const getSecondsFromDuration = (dur: string) => {
return dayjs.duration(durObject).asSeconds();
};
const instantQueryViews = [DisplayType.table, DisplayType.code];
export const getStepFromDuration = (dur: number, histogram?: boolean, displayType?: DisplayType): string => {
if (displayType && instantQueryViews.includes(displayType)) return roundStep(dur);
export const getStepFromDuration = (dur: number, histogram?: boolean): string => {
const size = histogram ? MAX_ITEMS_PER_HISTOGRAM : MAX_ITEMS_PER_CHART;
return roundStep(dur / size);
};
@@ -163,10 +160,11 @@ export const dateFromSeconds = (epochTimeInSeconds: number): Date => {
const getYesterday = () => dayjs().tz().subtract(1, "day").endOf("day").toDate();
const getToday = () => dayjs().tz().endOf("day").toDate();
const isLogsApp = process.env.REACT_APP_TYPE === AppType.logs;
export const relativeTimeOptions: RelativeTimeOption[] = [
{ title: "Last 5 minutes", duration: "5m", isDefault: APP_TYPE_LOGS },
{ title: "Last 5 minutes", duration: "5m", isDefault: isLogsApp },
{ title: "Last 15 minutes", duration: "15m" },
{ title: "Last 30 minutes", duration: "30m", isDefault: !APP_TYPE_LOGS },
{ title: "Last 30 minutes", duration: "30m", isDefault: !isLogsApp },
{ title: "Last 1 hour", duration: "1h" },
{ title: "Last 3 hours", duration: "3h" },
{ title: "Last 6 hours", duration: "6h" },

View File

@@ -1,2 +0,0 @@
/// <reference types="vite/client" />
/// <reference types="vite/types/importMeta.d.ts" />

View File

@@ -1,7 +1,6 @@
{
"compilerOptions": {
"target": "ESNext",
"types": ["vite/client"],
"target": "es5",
"lib": [
"dom",
"dom.iterable",
@@ -20,13 +19,7 @@
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"downlevelIteration": true,
"paths": {
"react": ["./node_modules/preact/compat/"],
"react/jsx-runtime": ["./node_modules/preact/jsx-runtime"],
"react-dom": ["./node_modules/preact/compat/"],
"react-dom/*": ["./node_modules/preact/compat/*"]
}
"downlevelIteration": true
},
"include": [
"src"

View File

@@ -1,39 +0,0 @@
import * as path from "path";
import { defineConfig } from "vite";
import preact from "@preact/preset-vite";
import dynamicIndexHtmlPlugin from "./config/plugins/dynamicIndexHtml";
export default defineConfig(({ mode }) => {
return {
base: "",
plugins: [
preact(),
dynamicIndexHtmlPlugin({ mode })
],
assetsInclude: ["**/*.md"],
server: {
open: true,
port: 3000,
},
resolve: {
alias: {
"src": path.resolve(__dirname, "src"),
},
},
build: {
outDir: "./build",
rollupOptions: {
output: {
manualChunks(id) {
if (id.includes("node_modules")) {
return "vendor";
}
}
}
}
},
};
});

View File

@@ -44,4 +44,4 @@ only from `cluster` branch. Hence, not all test cases suitable to run in both br
- If test is using binaries from `cluster` branch, then test name should be prefixed
with `TestCluster` word
- If test is using binaries from `master` branch, then test name should be prefixed
with `TestSingle` word.
with `TestVmsingle` word.

View File

@@ -233,23 +233,6 @@ func (tc *TestCase) StopApp(instance string) {
}
}
// StopPrometheusWriteQuerier stop all apps that are a part of the pwq.
func (tc *TestCase) StopPrometheusWriteQuerier(pwq PrometheusWriteQuerier) {
tc.t.Helper()
switch t := pwq.(type) {
case *Vmsingle:
tc.StopApp(t.Name())
case *vmcluster:
tc.StopApp(t.Vminsert.Name())
tc.StopApp(t.Vmselect.Name())
for _, vmstorage := range t.vmstorages {
tc.StopApp(vmstorage.Name())
}
default:
tc.t.Fatalf("Unsupported type: %v", t)
}
}
// ForceFlush flushes zero or more storages.
func (tc *TestCase) ForceFlush(apps ...*Vmstorage) {
tc.t.Helper()

View File

@@ -1,220 +0,0 @@
package tests
import (
"fmt"
"testing"
at "github.com/VictoriaMetrics/VictoriaMetrics/apptest"
)
func TestSingleSearchWithDisabledPerDayIndex(t *testing.T) {
tc := at.NewTestCase(t)
defer tc.Stop()
testSearchWithDisabledPerDayIndex(tc, func(name string, disablePerDayIndex bool) at.PrometheusWriteQuerier {
return tc.MustStartVmsingle("vmsingle-"+name, []string{
"-storageDataPath=" + tc.Dir() + "/vmsingle",
"-retentionPeriod=100y",
"-search.maxStalenessInterval=1m",
fmt.Sprintf("-disablePerDayIndex=%t", disablePerDayIndex),
})
})
}
func TestClusterSearchWithDisabledPerDayIndex(t *testing.T) {
tc := at.NewTestCase(t)
defer tc.Stop()
testSearchWithDisabledPerDayIndex(tc, func(name string, disablePerDayIndex bool) at.PrometheusWriteQuerier {
return tc.MustStartCluster(&at.ClusterOptions{
Vmstorage1Instance: "vmstorage1-" + name,
Vmstorage1Flags: []string{
fmt.Sprintf("-disablePerDayIndex=%t", disablePerDayIndex),
},
Vmstorage2Instance: "vmstorage2-" + name,
Vmstorage2Flags: []string{
fmt.Sprintf("-disablePerDayIndex=%t", disablePerDayIndex),
},
VminsertInstance: "vminsert",
VmselectInstance: "vmselect",
})
})
}
type startSUTFunc func(name string, disablePerDayIndex bool) at.PrometheusWriteQuerier
// testDisablePerDayIndex_Search shows what search results to expect when data
// is first inserted with per-day index enabled and then with per-day index
// disabled.
//
// The data inserted with enabled per-day index must be searchable with disabled
// per-day index.
//
// The data inserted with disabled per-day index is not searcheable with per-day
// index enabled unless the search time range is > 40 days.
func testSearchWithDisabledPerDayIndex(tc *at.TestCase, start startSUTFunc) {
t := tc.T()
type opts struct {
start, end string
wantSeries []map[string]string
wantQueryResults []*at.QueryResult
}
assertSearchResults := func(sut at.PrometheusQuerier, opts *opts) {
t.Helper()
tc.Assert(&at.AssertOptions{
Msg: "unexpected /api/v1/series response",
Got: func() any {
return sut.PrometheusAPIV1Series(t, `{__name__=~".*"}`, at.QueryOpts{
Start: opts.start,
End: opts.end,
}).Sort()
},
Want: &at.PrometheusAPIV1SeriesResponse{
Status: "success",
Data: opts.wantSeries,
},
})
tc.Assert(&at.AssertOptions{
Msg: "unexpected /api/v1/query_range response",
Got: func() any {
return sut.PrometheusAPIV1QueryRange(t, `{__name__=~".*"}`, at.QueryOpts{
Start: opts.start,
End: opts.end,
Step: "1d",
})
},
Want: &at.PrometheusAPIV1QueryResponse{
Status: "success",
Data: &at.QueryData{
ResultType: "matrix",
Result: opts.wantQueryResults,
},
},
})
}
// Start vmsingle with enabled per-day index, insert sample1, and confirm it
// is searcheable.
sut := start("with-per-day-index", false)
sample1 := []string{"metric1 111 1704067200000"} // 2024-01-01T00:00:00Z
sut.PrometheusAPIV1ImportPrometheus(t, sample1, at.QueryOpts{})
sut.ForceFlush(t)
assertSearchResults(sut, &opts{
start: "2024-01-01T00:00:00Z",
end: "2024-01-01T23:59:59Z",
wantSeries: []map[string]string{{"__name__": "metric1"}},
wantQueryResults: []*at.QueryResult{
{
Metric: map[string]string{"__name__": "metric1"},
Samples: []*at.Sample{{Timestamp: 1704067200000, Value: float64(111)}},
},
},
})
// Restart vmsingle with disabled per-day index, insert sample2, and confirm
// that both sample1 and sample2 is searcheable.
tc.StopPrometheusWriteQuerier(sut)
sut = start("without-per-day-index", true)
sample2 := []string{"metric2 222 1704067200000"} // 2024-01-01T00:00:00Z
sut.PrometheusAPIV1ImportPrometheus(t, sample2, at.QueryOpts{})
sut.ForceFlush(t)
assertSearchResults(sut, &opts{
start: "2024-01-01T00:00:00Z",
end: "2024-01-01T23:59:59Z",
wantSeries: []map[string]string{
{"__name__": "metric1"},
{"__name__": "metric2"},
},
wantQueryResults: []*at.QueryResult{
{
Metric: map[string]string{"__name__": "metric1"},
Samples: []*at.Sample{{Timestamp: 1704067200000, Value: float64(111)}},
},
{
Metric: map[string]string{"__name__": "metric2"},
Samples: []*at.Sample{{Timestamp: 1704067200000, Value: float64(222)}},
},
},
})
// Insert sample1 but for a different date, restart vmsingle with enabled
// per-day index and confirm that:
// - sample1 is searcheable within the time range of Jan 1st
// - sample1 is not searcheable within the time range of Jan 20th
// - sample1 is searcheable within the time range of Jan 1st-20th (because
// the metric1 metricID will be found in the per-day index for Jan 1st).
// - sample2 is not searcheable when the time range is <= 40 days
// - sample2 becomes searcheable when the time range is > 40 days
sample3 := []string{"metric1 333 1705708800000"} // 2024-01-20T00:00:00Z
sut.PrometheusAPIV1ImportPrometheus(t, sample3, at.QueryOpts{})
sut.ForceFlush(t)
tc.StopPrometheusWriteQuerier(sut)
sut = start("with-per-day-index2", false)
// Time range is 1 day (Jan 1st) <= 40 days
assertSearchResults(sut, &opts{
start: "2024-01-01T00:00:00Z",
end: "2024-01-01T23:59:59Z",
wantSeries: []map[string]string{
{"__name__": "metric1"},
},
wantQueryResults: []*at.QueryResult{
{
Metric: map[string]string{"__name__": "metric1"},
Samples: []*at.Sample{{Timestamp: 1704067200000, Value: float64(111)}},
},
},
})
// Time range is 1 day (Jan 20th) <= 40 days
assertSearchResults(sut, &opts{
start: "2024-01-20T00:00:00Z",
end: "2024-01-20T23:59:59Z",
wantSeries: []map[string]string{},
wantQueryResults: []*at.QueryResult{},
})
// Time range is 20 days (Jan 1st-20th) <= 40 days
assertSearchResults(sut, &opts{
start: "2024-01-01T00:00:00Z",
end: "2024-01-20T23:59:59Z",
wantSeries: []map[string]string{
{"__name__": "metric1"},
},
wantQueryResults: []*at.QueryResult{
{
Metric: map[string]string{"__name__": "metric1"},
Samples: []*at.Sample{
{Timestamp: 1704067200000, Value: float64(111)},
{Timestamp: 1705708800000, Value: float64(333)},
},
},
},
})
// Time range > 40 days
assertSearchResults(sut, &opts{
start: "2024-01-01T00:00:00Z",
end: "2024-02-29T23:59:59Z",
wantSeries: []map[string]string{
{"__name__": "metric1"},
{"__name__": "metric2"},
},
wantQueryResults: []*at.QueryResult{
{
Metric: map[string]string{"__name__": "metric1"},
Samples: []*at.Sample{
{Timestamp: 1704067200000, Value: float64(111)},
{Timestamp: 1705708800000, Value: float64(333)},
},
},
{
Metric: map[string]string{"__name__": "metric2"},
Samples: []*at.Sample{
{Timestamp: 1704067200000, Value: float64(222)},
},
},
},
})
}

View File

@@ -11,7 +11,7 @@ import (
"github.com/VictoriaMetrics/VictoriaMetrics/apptest"
)
func TestSingleVMAuthRouterWithAuth(t *testing.T) {
func TestVMAuthRouterWithAuth(t *testing.T) {
tc := apptest.NewTestCase(t)
defer tc.Stop()
@@ -117,7 +117,7 @@ users:
}
func TestSingleVMAuthRouterWithInternalAddr(t *testing.T) {
func TestVMAuthRouterWithInternalAddr(t *testing.T) {
tc := apptest.NewTestCase(t)
defer tc.Stop()

View File

@@ -4,7 +4,7 @@ services:
# And forward them to --remoteWrite.url
vmagent:
container_name: vmagent
image: victoriametrics/vmagent:v1.111.0
image: victoriametrics/vmagent:v1.110.0
depends_on:
- "vminsert"
ports:
@@ -39,7 +39,7 @@ services:
# where N is number of vmstorages (2 in this case).
vmstorage-1:
container_name: vmstorage-1
image: victoriametrics/vmstorage:v1.111.0-cluster
image: victoriametrics/vmstorage:v1.110.0-cluster
ports:
- 8482
- 8400
@@ -51,7 +51,7 @@ services:
restart: always
vmstorage-2:
container_name: vmstorage-2
image: victoriametrics/vmstorage:v1.111.0-cluster
image: victoriametrics/vmstorage:v1.110.0-cluster
ports:
- 8482
- 8400
@@ -66,7 +66,7 @@ services:
# pre-process them and distributes across configured vmstorage shards.
vminsert:
container_name: vminsert
image: victoriametrics/vminsert:v1.111.0-cluster
image: victoriametrics/vminsert:v1.110.0-cluster
depends_on:
- "vmstorage-1"
- "vmstorage-2"
@@ -81,7 +81,7 @@ services:
# vmselect collects results from configured `--storageNode` shards.
vmselect-1:
container_name: vmselect-1
image: victoriametrics/vmselect:v1.111.0-cluster
image: victoriametrics/vmselect:v1.110.0-cluster
depends_on:
- "vmstorage-1"
- "vmstorage-2"
@@ -94,7 +94,7 @@ services:
restart: always
vmselect-2:
container_name: vmselect-2
image: victoriametrics/vmselect:v1.111.0-cluster
image: victoriametrics/vmselect:v1.110.0-cluster
depends_on:
- "vmstorage-1"
- "vmstorage-2"
@@ -112,7 +112,7 @@ services:
# It can be used as an authentication proxy.
vmauth:
container_name: vmauth
image: victoriametrics/vmauth:v1.111.0
image: victoriametrics/vmauth:v1.110.0
depends_on:
- "vmselect-1"
- "vmselect-2"
@@ -127,7 +127,7 @@ services:
# vmalert executes alerting and recording rules
vmalert:
container_name: vmalert
image: victoriametrics/vmalert:v1.111.0
image: victoriametrics/vmalert:v1.110.0
depends_on:
- "vmauth"
ports:

View File

@@ -44,7 +44,7 @@ services:
# storing logs and serving read queries.
victorialogs:
container_name: victorialogs
image: victoriametrics/victoria-logs:v1.10.0-victorialogs
image: victoriametrics/victoria-logs:v1.8.0-victorialogs
command:
- "--storageDataPath=/vlogs"
- "--httpListenAddr=:9428"
@@ -59,7 +59,7 @@ services:
# scraping, storing metrics and serve read requests.
victoriametrics:
container_name: victoriametrics
image: victoriametrics/victoria-metrics:v1.111.0
image: victoriametrics/victoria-metrics:v1.110.0
ports:
- 8428:8428
volumes:
@@ -78,7 +78,7 @@ services:
# depending on the requested path.
vmauth:
container_name: vmauth
image: victoriametrics/vmauth:v1.111.0
image: victoriametrics/vmauth:v1.110.0
depends_on:
- "victoriametrics"
- "victorialogs"
@@ -95,7 +95,7 @@ services:
# vmalert executes alerting and recording rules according to given rule type.
vmalert:
container_name: vmalert
image: victoriametrics/vmalert:v1.111.0
image: victoriametrics/vmalert:v1.110.0
depends_on:
- "vmauth"
- "alertmanager"

View File

@@ -4,7 +4,7 @@ services:
# And forward them to --remoteWrite.url
vmagent:
container_name: vmagent
image: victoriametrics/vmagent:v1.111.0
image: victoriametrics/vmagent:v1.110.0
depends_on:
- "victoriametrics"
ports:
@@ -22,7 +22,7 @@ services:
# storing metrics and serve read requests.
victoriametrics:
container_name: victoriametrics
image: victoriametrics/victoria-metrics:v1.111.0
image: victoriametrics/victoria-metrics:v1.110.0
ports:
- 8428:8428
- 8089:8089
@@ -65,7 +65,7 @@ services:
# vmalert executes alerting and recording rules
vmalert:
container_name: vmalert
image: victoriametrics/vmalert:v1.111.0
image: victoriametrics/vmalert:v1.110.0
depends_on:
- "victoriametrics"
- "alertmanager"

View File

@@ -1,7 +1,7 @@
services:
# meta service will be ignored by compose
.victorialogs:
image: docker.io/victoriametrics/victoria-logs:v1.10.0-victorialogs
image: docker.io/victoriametrics/victoria-logs:v1.8.0-victorialogs
command:
- -storageDataPath=/vlogs
- -loggerFormat=json
@@ -19,7 +19,7 @@ services:
retries: 10
dd-proxy:
image: docker.io/victoriametrics/vmauth:v1.111.0
image: docker.io/victoriametrics/vmauth:v1.110.0
restart: on-failure
volumes:
- ./:/etc/vmauth
@@ -45,7 +45,7 @@ services:
replicas: 0
victoriametrics:
image: victoriametrics/victoria-metrics:v1.111.0
image: victoriametrics/victoria-metrics:v1.110.0
ports:
- '8428:8428'
command:

View File

@@ -1,7 +1,7 @@
services:
vmagent:
container_name: vmagent
image: victoriametrics/vmagent:v1.111.0
image: victoriametrics/vmagent:v1.110.0
depends_on:
- "victoriametrics"
ports:
@@ -18,7 +18,7 @@ services:
victoriametrics:
container_name: victoriametrics
image: victoriametrics/victoria-metrics:v1.111.0
image: victoriametrics/victoria-metrics:v1.110.0
ports:
- 8428:8428
volumes:
@@ -50,7 +50,7 @@ services:
vmalert:
container_name: vmalert
image: victoriametrics/vmalert:v1.111.0
image: victoriametrics/vmalert:v1.110.0
depends_on:
- "victoriametrics"
ports:

View File

@@ -3,7 +3,7 @@ version: "3"
services:
# Run `make package-victoria-logs` to build victoria-logs image
vlogs:
image: docker.io/victoriametrics/victoria-logs:v1.10.0-victorialogs
image: docker.io/victoriametrics/victoria-logs:v1.8.0-victorialogs
volumes:
- vlogs:/vlogs
ports:

View File

@@ -1602,7 +1602,7 @@ Below is the output for `/path/to/vmselect -help`:
-search.maxLabelsAPISeries int
The maximum number of time series, which could be scanned when searching for the matching time series at /api/v1/labels and /api/v1/label/.../values. This option allows limiting memory usage and CPU usage. See also -search.maxLabelsAPIDuration, -search.maxTagKeys, -search.maxTagValues and -search.ignoreExtraFiltersAtLabelsAPI (default 1000000)
-search.maxLookback duration
Synonym to -query.lookback-delta from Prometheus. The value is dynamically detected from interval between time series datapoints if not set. It can be overridden on per-query basis via max_lookback arg. See also '-search.maxStalenessInterval' flag, which has the same meaning due to historical reasons
Synonym to -search.lookback-delta from Prometheus. The value is dynamically detected from interval between time series datapoints if not set. It can be overridden on per-query basis via max_lookback arg. See also '-search.maxStalenessInterval' flag, which has the same meaning due to historical reasons
-search.maxMemoryPerQuery size
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
Supports the following optional suffixes for size values: KB, MB, GB, TB, KiB, MiB, GiB, TiB (default 0)

View File

@@ -22,5 +22,5 @@ to [the latest available releases](https://docs.victoriametrics.com/changelog/).
## Currently supported LTS release lines
- v1.110.x - the latest one is [v1.110.1 LTS release](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.110.1)
- v1.102.x - the latest one is [v1.102.13 LTS release](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.102.13)
- v1.102.x - the latest one is [v1.102.12 LTS release](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.102.12)
- v1.97.x - the latest one is [v1.97.17 LTS release](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.97.17)

View File

@@ -55,8 +55,8 @@ under the current directory:
```sh
docker pull victoriametrics/victoria-metrics:v1.111.0
docker run -it --rm -v `pwd`/victoria-metrics-data:/victoria-metrics-data -p 8428:8428 victoriametrics/victoria-metrics:v1.111.0
docker pull victoriametrics/victoria-metrics:v1.110.0
docker run -it --rm -v `pwd`/victoria-metrics-data:/victoria-metrics-data -p 8428:8428 victoriametrics/victoria-metrics:v1.110.0
```

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