Repository: VictoriaMetrics/VictoriaMetrics
Pull request #6423 (2024-06-05 15:28:45)

victoria-metrics: constant-time comparison of credentials like authkeys and basic auth credentials


### Describe Your Changes
Changes for constant-time comparison of credentials like authkeys and basic auth credentials.

See: https://github.com/VictoriaMetrics/VictoriaMetrics/issues/6392

### Checklist

The following checks are **mandatory**:

- [ ] My change adheres [VictoriaMetrics contributing guidelines](https://docs.victoriametrics.com/contributing/).


codecov[bot]: ## [Codecov](https://app.codecov.io/gh/VictoriaMetrics/VictoriaMetrics/pull/6423?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=VictoriaMetrics) Report
Attention: Patch coverage is `20.00000%` with `4 lines` in your changes missing coverage. Please review.
> Project coverage is 58.29%. Comparing base [(`8aaa828`)](https://app.codecov.io/gh/VictoriaMetrics/VictoriaMetrics/commit/8aaa828ba3f49a43cb8de89a0bcae160f7988c14?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=VictoriaMetrics) to head [(`e69da0e`)](https://app.codecov.io/gh/VictoriaMetrics/VictoriaMetrics/commit/e69da0e5125a88a9c6b41251d7f80da1d9aee050?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=VictoriaMetrics).
> Report is 827 commits behind head on master.

| [Files](https://app.codecov.io/gh/VictoriaMetrics/VictoriaMetrics/pull/6423?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=VictoriaMetrics) | Patch % | Lines |
|---|---|---|
| [lib/httpserver/httpserver.go](https://app.codecov.io/gh/VictoriaMetrics/VictoriaMetrics/pull/6423?src=pr&el=tree&filepath=lib%2Fhttpserver%2Fhttpserver.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=VictoriaMetrics#diff-bGliL2h0dHBzZXJ2ZXIvaHR0cHNlcnZlci5nbw==) | 20.00% | [4 Missing :warning: ](https://app.codecov.io/gh/VictoriaMetrics/VictoriaMetrics/pull/6423?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=VictoriaMetrics) |

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##           master    #6423      +/-   ##
==========================================
- Coverage   60.37%   58.29%   -2.08%     
==========================================
  Files         411      616     +205     
  Lines       76609    84124    +7515     
==========================================
+ Hits        46253    49041    +2788     
- Misses      27794    31827    +4033     
- Partials     2562     3256     +694     
```



</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/VictoriaMetrics/VictoriaMetrics/pull/6423?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=VictoriaMetrics).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=VictoriaMetrics).

wasim-nihal: > LGTM
> 
> @wasim-nihal Could you please rebase your changes on the recent master and add the following line to a changelog?
> 
> ```
> * FEATURE: all VictoriaMetrics components: use constant-time comparison for comparing HTTP basic auth credentials and auth keys. This should prevent timing attacks when comparing these credentials. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/6392) for details.
> ```

Done. Can you please check
zekker6: Looks good to me, thank you for the PR @wasim-nihal!
hagen1778: Hey @wasim-nihal! Thanks for the PR! Unfortunately I can't update it on my own. So I suggest applying the following changes before it can be merged https://gist.github.com/hagen1778/bc7bf45ccab501a7e80fb554e91d9ea1
wasim-nihal: > Hey @wasim-nihal! Thanks for the PR! Unfortunately I can't update it on my own. So I suggest applying the following changes before it can be merged https://gist.github.com/hagen1778/bc7bf45ccab501a7e80fb554e91d9ea1

Sure, will update my branch. Thanks!

Patch:
diff --git a/README.md b/README.md
index d26c5ede4..e7df2d59b 100644
--- a/README.md
+++ b/README.md
@@ -3093,6 +3093,8 @@ Pass `-help` to VictoriaMetrics in order to see the list of supported command-li
      Whether to disable automatic response cache reset if a sample with timestamp outside -search.cacheTimestampOffset is inserted into VictoriaMetrics
   -search.disableCache
      Whether to disable response caching. This may be useful when ingesting historical data. See https://docs.victoriametrics.com/#backfilling . See also -search.resetRollupResultCacheOnStartup
+  -search.disableImplicitConversion
+     Whether to return an error for queries that rely on implicit subquery conversions, see https://docs.victoriametrics.com/metricsql/#subqueries for details. See also -search.logImplicitConversion
   -search.graphiteMaxPointsPerSeries int
      The maximum number of points per series Graphite render API can return (default 1000000)
   -search.graphiteStorageStep duration
@@ -3101,6 +3103,8 @@ Pass `-help` to VictoriaMetrics in order to see the list of supported command-li
      Whether to ignore match[], extra_filters[] and extra_label query args at /api/v1/labels and /api/v1/label/.../values . This may be useful for decreasing load on VictoriaMetrics when extra filters match too many time series. The downside is that superfluous labels or series could be returned, which do not match the extra filters. See also -search.maxLabelsAPISeries and -search.maxLabelsAPIDuration
   -search.latencyOffset duration
      The time when data points become visible in query results after the collection. It can be overridden on per-query basis via latency_offset arg. Too small value can result in incomplete last points for query results (default 30s)
+  -search.logImplicitConversion
+     Whether to log queries with implicit subquery conversions, see https://docs.victoriametrics.com/metricsql/#subqueries for details. Such conversion can be disabled using -search.disableImplicitConversion
   -search.logQueryMemoryUsage size
      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. See also -search.logSlowQueryDuration and -search.maxMemoryPerQuery
      Supports the following optional suffixes for size values: KB, MB, GB, TB, KiB, MiB, GiB, TiB (default 0)
diff --git a/app/vmalert-tool/main.go b/app/vmalert-tool/main.go
index 790cebd8b..46c90101d 100644
--- a/app/vmalert-tool/main.go
+++ b/app/vmalert-tool/main.go
@@ -26,8 +26,14 @@ func main() {
 				UsageText: "More info in https://docs.victoriametrics.com/vmalert-tool.html#Unit-testing-for-rules",
 				Flags: []cli.Flag{
 					&cli.StringSliceFlag{
-						Name:     "files",
-						Usage:    "files to run unittest with. Supports an array of values separated by comma or specified via multiple flags.",
+						Name: "files",
+						Usage: `File path or http url with test files. Supports an array of values separated by comma or specified via multiple flags.
+						Supports hierarchical patterns and regexpes.
+Examples:
+ -files="/path/to/file". Path to a single test file.
+ -files="http://<some-server-addr>/path/to/test.yaml". HTTP URL to a test file.
+ -files="dir/**/*.yaml". Includes all the .yaml files in "dir" subfolders recursively.
+ `,
 						Required: true,
 					},
 					&cli.BoolFlag{
diff --git a/app/vmalert/config/config.go b/app/vmalert/config/config.go
index 4d8232d06..d2c07469c 100644
--- a/app/vmalert/config/config.go
+++ b/app/vmalert/config/config.go
@@ -234,7 +234,7 @@ func ParseSilent(pathPatterns []string, validateTplFn ValidateTplFn, validateExp
 	cLogger.Suppress(true)
 	defer cLogger.Suppress(false)
 
-	files, err := readFromFS(pathPatterns)
+	files, err := ReadFromFS(pathPatterns)
 	if err != nil {
 		return nil, fmt.Errorf("failed to read from the config: %w", err)
 	}
@@ -243,7 +243,7 @@ func ParseSilent(pathPatterns []string, validateTplFn ValidateTplFn, validateExp
 
 // Parse parses rule configs from given file patterns
 func Parse(pathPatterns []string, validateTplFn ValidateTplFn, validateExpressions bool) ([]Group, error) {
-	files, err := readFromFS(pathPatterns)
+	files, err := ReadFromFS(pathPatterns)
 	if err != nil {
 		return nil, fmt.Errorf("failed to read from the config: %w", err)
 	}
diff --git a/app/vmalert/config/fs.go b/app/vmalert/config/fs.go
index 2fc068370..5f9854c75 100644
--- a/app/vmalert/config/fs.go
+++ b/app/vmalert/config/fs.go
@@ -32,14 +32,14 @@ var (
 	fsRegistry   = make(map[string]FS)
 )
 
-// readFromFS parses the given path list and inits FS for each item.
-// Once initialed, readFromFS will try to read and return files from each FS.
-// readFromFS returns an error if at least one FS failed to init.
+// ReadFromFS parses the given path list and inits FS for each item.
+// Once initialed, ReadFromFS will try to read and return files from each FS.
+// ReadFromFS returns an error if at least one FS failed to init.
 // The function can be called multiple times but each unique path
 // will be initialed only once.
 //
 // It is allowed to mix different FS types in path list.
-func readFromFS(paths []string) (map[string][]byte, error) {
+func ReadFromFS(paths []string) (map[string][]byte, error) {
 	var err error
 	result := make(map[string][]byte)
 	for _, path := range paths {
diff --git a/app/vmui/packages/vmui/src/api/logs.ts b/app/vmui/packages/vmui/src/api/logs.ts
index a5d62126f..35b268b77 100644
--- a/app/vmui/packages/vmui/src/api/logs.ts
+++ b/app/vmui/packages/vmui/src/api/logs.ts
@@ -1,2 +1,5 @@
 export const getLogsUrl = (server: string): string =>
   `${server}/select/logsql/query`;
+
+export const getLogHitsUrl = (server: string): string =>
+  `${server}/select/logsql/hits`;
diff --git a/app/vmui/packages/vmui/src/api/types.ts b/app/vmui/packages/vmui/src/api/types.ts
index eee701f93..e9765ee10 100644
--- a/app/vmui/packages/vmui/src/api/types.ts
+++ b/app/vmui/packages/vmui/src/api/types.ts
@@ -34,3 +34,11 @@ export interface Logs {
   _time: string;
   [key: string]: string;
 }
+
+export interface LogHits {
+  timestamps: string[];
+  values: number[];
+  fields: {
+    [key: string]: string;
+  };
+}
diff --git a/app/vmui/packages/vmui/src/components/Chart/BarHitsChart/BarHitsChart.tsx b/app/vmui/packages/vmui/src/components/Chart/BarHitsChart/BarHitsChart.tsx
new file mode 100644
index 000000000..748bf46f2
--- /dev/null
+++ b/app/vmui/packages/vmui/src/components/Chart/BarHitsChart/BarHitsChart.tsx
@@ -0,0 +1,76 @@
+import React, { FC, 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 TooltipBarHitsChart from "./TooltipBarHitsChart";
+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";
+
+interface Props {
+  data: AlignedData;
+  period: TimeParams;
+  setPeriod: ({ from, to }: { from: Date, to: Date }) => void;
+}
+
+const BarHitsChart: FC<Props> = ({ data, period, setPeriod }) => {
+  const [containerRef, containerSize] = useElementSize();
+  const uPlotRef = useRef<HTMLDivElement>(null);
+  const [uPlotInst, setUPlotInst] = useState<uPlot>();
+
+  const { xRange, setPlotScale } = usePlotScale({ period, setPeriod });
+  const { onReadyChart, isPanning } = useReadyChart(setPlotScale);
+  useZoomChart({ uPlotInst, xRange, setPlotScale });
+  const { options, focusDataIdx } = useBarHitsOptions({ xRange, containerSize, onReadyChart, setPlotScale });
+
+  useEffect(() => {
+    if (!uPlotRef.current) return;
+    const uplot = new uPlot(options, data, uPlotRef.current);
+    setUPlotInst(uplot);
+    return () => uplot.destroy();
+  }, [uPlotRef.current, options]);
+
+  useEffect(() => {
+    if (!uPlotInst) return;
+    uPlotInst.scales.x.range = () => [xRange.min, xRange.max];
+    uPlotInst.redraw();
+  }, [xRange]);
+
+  useEffect(() => {
+    if (!uPlotInst) return;
+    uPlotInst.setSize(containerSize);
+    uPlotInst.redraw();
+  }, [containerSize]);
+
+  useEffect(() => {
+    if (!uPlotInst) return;
+    uPlotInst.setData(data);
+    uPlotInst.redraw();
+  }, [data]);
+
+  return (
+    <div
+      className={classNames({
+        "vm-bar-hits-chart": true,
+        "vm-bar-hits-chart_panning": isPanning
+      })}
+      ref={containerRef}
+    >
+      <div
+        className="vm-line-chart__u-plot"
+        ref={uPlotRef}
+      />
+      <TooltipBarHitsChart
+        uPlotInst={uPlotInst}
+        focusDataIdx={focusDataIdx}
+      />
+    </div>
+  );
+};
+
+export default BarHitsChart;
diff --git a/app/vmui/packages/vmui/src/components/Chart/BarHitsChart/TooltipBarHitsChart.tsx b/app/vmui/packages/vmui/src/components/Chart/BarHitsChart/TooltipBarHitsChart.tsx
new file mode 100644
index 000000000..2d96c32d0
--- /dev/null
+++ b/app/vmui/packages/vmui/src/components/Chart/BarHitsChart/TooltipBarHitsChart.tsx
@@ -0,0 +1,89 @@
+import React, { FC, useMemo, useRef } from "preact/compat";
+import uPlot from "uplot";
+import dayjs from "dayjs";
+import { DATE_TIME_FORMAT } from "../../../constants/date";
+import classNames from "classnames";
+import "./style.scss";
+import "../../../components/Chart/ChartTooltip/style.scss";
+
+interface Props {
+  uPlotInst?: uPlot;
+  focusDataIdx: number
+}
+
+const TooltipBarHitsChart: FC<Props> = ({ focusDataIdx, uPlotInst }) => {
+  const tooltipRef = useRef<HTMLDivElement>(null);
+
+  const tooltipData = useMemo(() => {
+    const value = uPlotInst?.data?.[1]?.[focusDataIdx];
+    const timestamp = uPlotInst?.data?.[0]?.[focusDataIdx] || 0;
+    const top = uPlotInst?.valToPos?.((value || 0), "y") || 0;
+    const left = uPlotInst?.valToPos?.(timestamp, "x") || 0;
+
+    return {
+      point: { top, left },
+      value,
+      timestamp: dayjs(timestamp * 1000).tz().format(DATE_TIME_FORMAT),
+    };
+  }, [focusDataIdx, uPlotInst]);
+
+  const tooltipPosition = useMemo(() => {
+    if (!uPlotInst || !tooltipData.value || !tooltipRef.current) return;
+
+    const { top, left } = tooltipData.point;
+    const uPlotPosition = {
+      left: parseFloat(uPlotInst.over.style.left),
+      top: parseFloat(uPlotInst.over.style.top)
+    };
+
+    const {
+      width: uPlotWidth,
+      height: uPlotHeight
+    } = uPlotInst.over.getBoundingClientRect();
+
+    const {
+      width: tooltipWidth,
+      height: tooltipHeight
+    } = tooltipRef.current.getBoundingClientRect();
+
+    const margin = 10;
+    const overflowX = left + tooltipWidth >= uPlotWidth ? tooltipWidth + (2 * margin) : 0;
+    const overflowY = top + tooltipHeight >= uPlotHeight ? tooltipHeight + (2 * margin) : 0;
+
+    const position = {
+      top: top + uPlotPosition.top + margin - overflowY,
+      left: left + uPlotPosition.left + margin - overflowX
+    };
+
+    if (position.left < 0) position.left = 20;
+    if (position.top < 0) position.top = 20;
+
+    return position;
+  }, [tooltipData, uPlotInst, tooltipRef.current]);
+
+  return (
+    <div
+      className={classNames({
+        "vm-chart-tooltip": true,
+        "vm-bar-hits-chart-tooltip": true,
+        "vm-bar-hits-chart-tooltip_visible": focusDataIdx !== -1
+      })}
+      ref={tooltipRef}
+      style={tooltipPosition}
+    >
+      <div className="vm-chart-tooltip-data">
+        Count of records:
+        <p className="vm-chart-tooltip-data__value">
+          <b>{tooltipData.value}</b>
+        </p>
+      </div>
+      <div className="vm-chart-tooltip-header">
+        <div className="vm-chart-tooltip-header__title">
+          {tooltipData.timestamp}
+        </div>
+      </div>
+    </div>
+  );
+};
+
+export default TooltipBarHitsChart;
diff --git a/app/vmui/packages/vmui/src/components/Chart/BarHitsChart/hooks/useBarHitsOptions.ts b/app/vmui/packages/vmui/src/components/Chart/BarHitsChart/hooks/useBarHitsOptions.ts
new file mode 100644
index 000000000..f72b6cacd
--- /dev/null
+++ b/app/vmui/packages/vmui/src/components/Chart/BarHitsChart/hooks/useBarHitsOptions.ts
@@ -0,0 +1,74 @@
+import { useMemo, useState } from "preact/compat";
+import { getAxes, handleDestroy, setSelect } from "../../../../utils/uplot";
+import dayjs from "dayjs";
+import { dateFromSeconds, formatDateForNativeInput } from "../../../../utils/time";
+import uPlot, { Options } from "uplot";
+import { getCssVariable } from "../../../../utils/theme";
+import { barPaths } from "../../../../utils/uplot/bars";
+import { useAppState } from "../../../../state/common/StateContext";
+import { MinMax, SetMinMax } from "../../../../types";
+
+interface UseGetBarHitsOptionsArgs {
+  xRange: MinMax;
+  containerSize: { width: number, height: number };
+  setPlotScale: SetMinMax;
+  onReadyChart: (u: uPlot) => void;
+}
+
+const useBarHitsOptions = ({ xRange, containerSize, onReadyChart, setPlotScale }: UseGetBarHitsOptionsArgs) => {
+  const { isDarkTheme } = useAppState();
+
+  const [focusDataIdx, setFocusDataIdx] = useState(-1);
+
+  const series = useMemo(() => [
+    {},
+    {
+      label: "y",
+      width: 1,
+      stroke: getCssVariable("color-log-hits-bar"),
+      fill: getCssVariable("color-log-hits-bar"),
+      paths: barPaths,
+    }
+  ], [isDarkTheme]);
+
+  const setCursor = (u: uPlot) => {
+    const dataIdx = u.cursor.idx ?? -1;
+    setFocusDataIdx(dataIdx);
+  };
+
+  const options: Options = useMemo(() => ({
+    series,
+    width: containerSize.width || (window.innerWidth / 2),
+    height: containerSize.height || 200,
+    cursor: {
+      points: {
+        width: (u, seriesIdx, size) => size / 4,
+        size: (u, seriesIdx) => (u.series?.[seriesIdx]?.points?.size || 1) * 2.5,
+        stroke: (u, seriesIdx) => `${series?.[seriesIdx]?.stroke || "#ffffff"}`,
+        fill: () => "#ffffff",
+      },
+    },
+    scales: {
+      x: {
+        time: true,
+        range: () => [xRange.min, xRange.max]
+      }
+    },
+    hooks: {
+      ready: [onReadyChart],
+      setCursor: [setCursor],
+      setSelect: [setSelect(setPlotScale)],
+      destroy: [handleDestroy],
+    },
+    legend: { show: false },
+    axes: getAxes([{}, { scale: "y" }]),
+    tzDate: ts => dayjs(formatDateForNativeInput(dateFromSeconds(ts))).local().toDate(),
+  }), [isDarkTheme]);
+
+  return {
+    options,
+    focusDataIdx,
+  };
+};
+
+export default useBarHitsOptions;
diff --git a/app/vmui/packages/vmui/src/components/Chart/BarHitsChart/style.scss b/app/vmui/packages/vmui/src/components/Chart/BarHitsChart/style.scss
new file mode 100644
index 000000000..509a5517e
--- /dev/null
+++ b/app/vmui/packages/vmui/src/components/Chart/BarHitsChart/style.scss
@@ -0,0 +1,22 @@
+@use "src/styles/variables" as *;
+
+.vm-bar-hits-chart {
+  height: 100%;
+  width: 100%;
+
+  &_panning {
+    pointer-events: none;
+  }
+
+  &-tooltip {
+    opacity: 0;
+    pointer-events: none;
+    width: 240px;
+    gap: $padding-small;
+
+    &_visible {
+      opacity: 1;
+      pointer-events: auto;
+    }
+  }
+}
diff --git a/app/vmui/packages/vmui/src/constants/footerLinks.ts b/app/vmui/packages/vmui/src/constants/footerLinks.ts
new file mode 100644
index 000000000..71b847915
--- /dev/null
+++ b/app/vmui/packages/vmui/src/constants/footerLinks.ts
@@ -0,0 +1,35 @@
+import { CodeIcon, IssueIcon, WikiIcon } from "../components/Main/Icons";
+
+const issueLink = {
+  href: "https://github.com/VictoriaMetrics/VictoriaMetrics/issues/new/choose",
+  Icon: IssueIcon,
+  title: "Create an issue",
+};
+
+export const footerLinksByDefault = [
+  {
+    href: "https://docs.victoriametrics.com/MetricsQL.html",
+    Icon: CodeIcon,
+    title: "MetricsQL",
+  },
+  {
+    href: "https://docs.victoriametrics.com/#vmui",
+    Icon: WikiIcon,
+    title: "Documentation",
+  },
+  issueLink
+];
+
+export const footerLinksToLogs = [
+  {
+    href: "https://docs.victoriametrics.com/victorialogs/logsql/",
+    Icon: CodeIcon,
+    title: "LogsQL",
+  },
+  {
+    href: "https://docs.victoriametrics.com/victorialogs/",
+    Icon: WikiIcon,
+    title: "Documentation",
+  },
+  issueLink
+];
diff --git a/app/vmui/packages/vmui/src/constants/logs.ts b/app/vmui/packages/vmui/src/constants/logs.ts
new file mode 100644
index 000000000..4ad19c3e1
--- /dev/null
+++ b/app/vmui/packages/vmui/src/constants/logs.ts
@@ -0,0 +1,2 @@
+export const LOGS_ENTRIES_LIMIT = 50;
+export const LOGS_BARS_VIEW = 20;
diff --git a/app/vmui/packages/vmui/src/constants/palette.ts b/app/vmui/packages/vmui/src/constants/palette.ts
index abb11bfed..11f78d637 100644
--- a/app/vmui/packages/vmui/src/constants/palette.ts
+++ b/app/vmui/packages/vmui/src/constants/palette.ts
@@ -14,7 +14,8 @@ export const darkPalette = {
   "box-shadow": "rgba(0, 0, 0, 0.16) 1px 2px 6px",
   "box-shadow-popper": "rgba(0, 0, 0, 0.2) 0px 2px 8px 0px",
   "border-divider": "1px solid rgba(99, 110, 123, 0.5)",
-  "color-hover-black": "rgba(0, 0, 0, 0.12)"
+  "color-hover-black": "rgba(0, 0, 0, 0.12)",
+  "color-log-hits-bar": "rgba(255, 255, 255, 0.18)"
 };
 
 export const lightPalette = {
@@ -33,5 +34,6 @@ export const lightPalette = {
   "box-shadow": "rgba(0, 0, 0, 0.08) 1px 2px 6px",
   "box-shadow-popper": "rgba(0, 0, 0, 0.1) 0px 2px 8px 0px",
   "border-divider": "1px solid rgba(0, 0, 0, 0.15)",
-  "color-hover-black": "rgba(0, 0, 0, 0.06)"
+  "color-hover-black": "rgba(0, 0, 0, 0.06)",
+  "color-log-hits-bar": "rgba(0, 0, 0, 0.18)"
 };
diff --git a/app/vmui/packages/vmui/src/layouts/Footer/Footer.tsx b/app/vmui/packages/vmui/src/layouts/Footer/Footer.tsx
index 58058926a..81a091a22 100644
--- a/app/vmui/packages/vmui/src/layouts/Footer/Footer.tsx
+++ b/app/vmui/packages/vmui/src/layouts/Footer/Footer.tsx
@@ -1,8 +1,17 @@
 import React, { FC, memo } from "preact/compat";
-import { CodeIcon, IssueIcon, LogoShortIcon, WikiIcon } from "../../components/Main/Icons";
+import { LogoShortIcon } from "../../components/Main/Icons";
 import "./style.scss";
+import { footerLinksByDefault } from "../../constants/footerLinks";
 
-const Footer: FC = memo(() => {
+interface Props {
+  links?: {
+    href: string;
+    Icon: FC;
+    title: string;
+  }[]
+}
+
+const Footer: FC<Props> = memo(({ links = footerLinksByDefault }) => {
   const copyrightYears = `2019-${new Date().getFullYear()}`;
 
   return <footer className="vm-footer">
@@ -15,33 +24,18 @@ const Footer: FC = memo(() => {
       <LogoShortIcon/>
       victoriametrics.com
     </a>
-    <a
-      className="vm-link vm-footer__link"
-      target="_blank"
-      href="https://docs.victoriametrics.com/MetricsQL.html"
-      rel="help noreferrer"
-    >
-      <CodeIcon/>
-      MetricsQL
-    </a>
-    <a
-      className="vm-link vm-footer__link"
-      target="_blank"
-      href="https://docs.victoriametrics.com/#vmui"
-      rel="help noreferrer"
-    >
-      <WikiIcon/>
-      Documentation
-    </a>
-    <a
-      className="vm-link vm-footer__link"
-      target="_blank"
-      href="https://github.com/VictoriaMetrics/VictoriaMetrics/issues/new/choose"
-      rel="noreferrer"
-    >
-      <IssueIcon/>
-      Create an issue
-    </a>
+    {links.map(({ href, Icon, title }) => (
+      <a
+        className="vm-link vm-footer__link"
+        target="_blank"
+        href={href}
+        rel="help noreferrer"
+        key={`${href}-${title}`}
+      >
+        <Icon/>
+        {title}
+      </a>
+    ))}
     <div className="vm-footer__copyright">
       &copy; {copyrightYears} VictoriaMetrics
     </div>
diff --git a/app/vmui/packages/vmui/src/layouts/LogsLayout/LogsLayout.tsx b/app/vmui/packages/vmui/src/layouts/LogsLayout/LogsLayout.tsx
index 3d6284993..757fb8c08 100644
--- a/app/vmui/packages/vmui/src/layouts/LogsLayout/LogsLayout.tsx
+++ b/app/vmui/packages/vmui/src/layouts/LogsLayout/LogsLayout.tsx
@@ -9,6 +9,7 @@ import router, { routerOptions } from "../../router";
 import useDeviceDetect from "../../hooks/useDeviceDetect";
 import ControlsLogsLayout from "./ControlsLogsLayout";
 import useFetchDefaultTimezone from "../../hooks/useFetchDefaultTimezone";
+import { footerLinksToLogs } from "../../constants/footerLinks";
 
 const LogsLayout: FC = () => {
   const appModeEnable = getAppModeEnable();
@@ -36,7 +37,7 @@ const LogsLayout: FC = () => {
     >
       <Outlet/>
     </div>
-    {!appModeEnable && <Footer/>}
+    {!appModeEnable && <Footer links={footerLinksToLogs}/>}
   </section>;
 };
 
diff --git a/app/vmui/packages/vmui/src/pages/ExploreLogs/ExploreLogs.tsx b/app/vmui/packages/vmui/src/pages/ExploreLogs/ExploreLogs.tsx
index 9dbebedb6..8b289f4f5 100644
--- a/app/vmui/packages/vmui/src/pages/ExploreLogs/ExploreLogs.tsx
+++ b/app/vmui/packages/vmui/src/pages/ExploreLogs/ExploreLogs.tsx
@@ -12,9 +12,12 @@ import { ErrorTypes } from "../../types";
 import { useState } from "react";
 import { useTimeState } from "../../state/time/TimeStateContext";
 import { getFromStorage, saveToStorage } from "../../utils/storage";
+import ExploreLogsBarChart from "./ExploreLogsBarChart/ExploreLogsBarChart";
+import { useFetchLogHits } from "./hooks/useFetchLogHits";
+import { LOGS_ENTRIES_LIMIT } from "../../constants/logs";
 
 const storageLimit = Number(getFromStorage("LOGS_LIMIT"));
-const defaultLimit = isNaN(storageLimit) ? 50 : storageLimit;
+const defaultLimit = isNaN(storageLimit) ? LOGS_ENTRIES_LIMIT : storageLimit;
 
 const ExploreLogs: FC = () => {
   const { serverUrl } = useAppState();
@@ -24,6 +27,7 @@ const ExploreLogs: FC = () => {
   const [limit, setLimit] = useStateSearchParams(defaultLimit, "limit");
   const [query, setQuery] = useStateSearchParams("*", "query");
   const { logs, isLoading, error, fetchLogs } = useFetchLogs(serverUrl, query, limit);
+  const { fetchLogHits, ...dataLogHits } = useFetchLogHits(serverUrl, query);
   const [queryError, setQueryError] = useState<ErrorTypes | string>("");
   const [loaded, isLoaded] = useState(false);
   const [markdownParsing, setMarkdownParsing] = useState(getFromStorage("LOGS_MARKDOWN") === "true");
@@ -37,6 +41,7 @@ const ExploreLogs: FC = () => {
     fetchLogs().then(() => {
       isLoaded(true);
     });
+    fetchLogHits();
 
     setSearchParamsFromKeys( {
       query,
@@ -79,6 +84,11 @@ const ExploreLogs: FC = () => {
       />
       {isLoading && <Spinner />}
       {error && <Alert variant="error">{error}</Alert>}
+      <ExploreLogsBarChart
+        query={query}
+        loaded={loaded}
+        {...dataLogHits}
+      />
       <ExploreLogsBody
         data={logs}
         loaded={loaded}
diff --git a/app/vmui/packages/vmui/src/pages/ExploreLogs/ExploreLogsBarChart/ExploreLogsBarChart.tsx b/app/vmui/packages/vmui/src/pages/ExploreLogs/ExploreLogsBarChart/ExploreLogsBarChart.tsx
new file mode 100644
index 000000000..1b367d9ca
--- /dev/null
+++ b/app/vmui/packages/vmui/src/pages/ExploreLogs/ExploreLogsBarChart/ExploreLogsBarChart.tsx
@@ -0,0 +1,82 @@
+import React, { FC, useMemo } from "preact/compat";
+import "./style.scss";
+import useDeviceDetect from "../../../hooks/useDeviceDetect";
+import classNames from "classnames";
+import { LogHits } from "../../../api/types";
+import dayjs from "dayjs";
+import { useTimeDispatch, useTimeState } from "../../../state/time/TimeStateContext";
+import { AlignedData } from "uplot";
+import BarHitsChart from "../../../components/Chart/BarHitsChart/BarHitsChart";
+import Alert from "../../../components/Main/Alert/Alert";
+
+interface Props {
+  query: string;
+  logHits: LogHits[];
+  error?: string;
+  isLoading: boolean;
+  loaded: boolean;
+}
+
+const ExploreLogsBarChart: FC<Props> = ({ logHits, error, loaded }) => {
+  const { isMobile } = useDeviceDetect();
+  const { period } = useTimeState();
+  const timeDispatch = useTimeDispatch();
+
+  const data = useMemo(() => {
+    const hits = logHits[0];
+    if (!hits) return [[], []] as AlignedData;
+    const { values, timestamps } = hits;
+    const xAxis = timestamps.map(t => t ? dayjs(t).unix() : null).filter(Boolean);
+    const yAxis = values.map(v => v || null);
+    return [xAxis, yAxis] as AlignedData;
+  }, [logHits]);
+
+  const noDataMessage: string = useMemo(() => {
+    const noData = data.every(d => d.length === 0);
+    const noTimestamps = data[0].length === 0;
+    const noValues = data[1].length === 0;
+    if (noData) {
+      return "No logs volume available\nNo volume information available for the current queries and time range.";
+    } else if (noTimestamps) {
+      return "No timestamp information available for the current queries and time range.";
+    } else if (noValues) {
+      return "No value information available for the current queries and time range.";
+    } return "";
+  }, [data]);
+
+  const setPeriod = ({ from, to }: {from: Date, to: Date}) => {
+    timeDispatch({ type: "SET_PERIOD", payload: { from, to } });
+  };
+
+  return (
+    <section
+      className={classNames({
+        "vm-explore-logs-chart": true,
+        "vm-block": true,
+        "vm-block_mobile": isMobile,
+      })}
+    >
+      {!error && loaded && noDataMessage && (
+        <div className="vm-explore-logs-chart__empty">
+          <Alert variant="info">{noDataMessage}</Alert>
+        </div>
+      )}
+
+      {error && loaded && noDataMessage && (
+        <div className="vm-explore-logs-chart__empty">
+          <Alert variant="error">{error}</Alert>
+        </div>
+      )}
+
+      {data && (
+        <BarHitsChart
+          data={data}
+          period={period}
+          setPeriod={setPeriod}
+        />
+      )}
+    </section>
+  );
+};
+
+export default ExploreLogsBarChart;
diff --git a/app/vmui/packages/vmui/src/pages/ExploreLogs/ExploreLogsBarChart/style.scss b/app/vmui/packages/vmui/src/pages/ExploreLogs/ExploreLogsBarChart/style.scss
new file mode 100644
index 000000000..d1e839bea
--- /dev/null
+++ b/app/vmui/packages/vmui/src/pages/ExploreLogs/ExploreLogsBarChart/style.scss
@@ -0,0 +1,21 @@
+@use "src/styles/variables" as *;
+
+.vm-explore-logs-chart {
+  position: relative;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  height: 200px;
+  padding: 0 0 0 $padding-small !important;
+  width: calc(100vw - ($padding-medium * 2));
+
+  @media (max-width: 768px) {
+    width: calc(100vw);
+  }
+
+  &__empty {
+    position: absolute;
+    transform: translateY(-25px);
+    z-index: 2;
+  }
+}
diff --git a/app/vmui/packages/vmui/src/pages/ExploreLogs/ExploreLogsBody/ExploreLogsBody.tsx b/app/vmui/packages/vmui/src/pages/ExploreLogs/ExploreLogsBody/ExploreLogsBody.tsx
index 3a66a3503..f213b9a02 100644
--- a/app/vmui/packages/vmui/src/pages/ExploreLogs/ExploreLogsBody/ExploreLogsBody.tsx
+++ b/app/vmui/packages/vmui/src/pages/ExploreLogs/ExploreLogsBody/ExploreLogsBody.tsx
@@ -48,7 +48,7 @@ const ExploreLogsBody: FC<ExploreLogBodyProps> = ({ data, loaded, markdownParsin
     ...item,
     _vmui_time: item._time ? dayjs(item._time).tz().format(`${DATE_TIME_FORMAT}.SSS`) : "",
     _vmui_data: JSON.stringify(item, null, 2),
-    _vmui_markdown: marked(item._msg.replace(/```/g, "\n```\n")) as string,
+    _vmui_markdown: item._msg ? marked(item._msg.replace(/```/g, "\n```\n")) as string : ""
   })) as Logs[], [data, timezone]);
 
   const columns = useMemo(() => {
diff --git a/app/vmui/packages/vmui/src/pages/ExploreLogs/GroupLogs/GroupLogs.tsx b/app/vmui/packages/vmui/src/pages/ExploreLogs/GroupLogs/GroupLogs.tsx
index 82983a14f..ec0d21da2 100644
--- a/app/vmui/packages/vmui/src/pages/ExploreLogs/GroupLogs/GroupLogs.tsx
+++ b/app/vmui/packages/vmui/src/pages/ExploreLogs/GroupLogs/GroupLogs.tsx
@@ -7,6 +7,8 @@ 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";
 
 interface TableLogsProps {
   logs: Logs[];
@@ -15,6 +17,7 @@ interface TableLogsProps {
 }
 
 const GroupLogs: FC<TableLogsProps> = ({ logs, markdownParsing }) => {
+  const { isDarkTheme } = useAppState();
   const copyToClipboard = useCopyToClipboard();
 
   const [copied, setCopied] = useState<string | null>(null);
@@ -32,7 +35,7 @@ const GroupLogs: FC<TableLogsProps> = ({ logs, markdownParsing }) => {
 
   const handleClickByPair = (pair: string) => async (e: MouseEvent<HTMLDivElement>) => {
     e.stopPropagation();
-    const isCopied = await copyToClipboard(`${pair}`);
+    const isCopied = await copyToClipboard(`${pair.replace(/=/, ": ")}`);
     if (isCopied) {
       setCopied(pair);
     }
@@ -63,7 +66,10 @@ const GroupLogs: FC<TableLogsProps> = ({ logs, markdownParsing }) => {
                     placement={"top-center"}
                   >
                     <div
-                      className="vm-group-logs-section-keys__pair"
+                      className={classNames({
+                        "vm-group-logs-section-keys__pair": true,
+                        "vm-group-logs-section-keys__pair_dark": isDarkTheme
+                      })}
                       onClick={handleClickByPair(pair)}
                     >
                       {pair}
diff --git a/app/vmui/packages/vmui/src/pages/ExploreLogs/GroupLogs/GroupLogsItem.tsx b/app/vmui/packages/vmui/src/pages/ExploreLogs/GroupLogs/GroupLogsItem.tsx
index 64539db0e..0cfa42bbc 100644
--- a/app/vmui/packages/vmui/src/pages/ExploreLogs/GroupLogs/GroupLogsItem.tsx
+++ b/app/vmui/packages/vmui/src/pages/ExploreLogs/GroupLogs/GroupLogsItem.tsx
@@ -1,10 +1,10 @@
-import React, { FC, useEffect, useState } from "preact/compat";
+import React, { FC, useEffect, useMemo, useState } from "preact/compat";
 import { Logs } from "../../../api/types";
 import "./style.scss";
 import useBoolean from "../../../hooks/useBoolean";
 import Button from "../../../components/Main/Button/Button";
 import Tooltip from "../../../components/Main/Tooltip/Tooltip";
-import { ArrowDropDownIcon, CopyIcon } from "../../../components/Main/Icons";
+import { ArrowDownIcon, CopyIcon } from "../../../components/Main/Icons";
 import useCopyToClipboard from "../../../hooks/useCopyToClipboard";
 import classNames from "classnames";
 
@@ -23,6 +23,16 @@ const GroupLogsItem: FC<Props> = ({ log, markdownParsing }) => {
   const fields = Object.entries(log).filter(([key]) => !excludeKeys.includes(key));
   const hasFields = fields.length > 0;
 
+  const displayMessage = useMemo(() => {
+    if (log._msg) return log._msg;
+    if (!hasFields) return;
+    const dataObject = fields.reduce<{[key: string]: string}>((obj, [key, value]) => {
+      obj[key] = value;
+      return obj;
+    }, {});
+    return JSON.stringify(dataObject);
+  }, [log, fields, hasFields]);
+
   const copyToClipboard = useCopyToClipboard();
   const [copied, setCopied] = useState<number | null>(null);
 
@@ -56,7 +66,7 @@ const GroupLogsItem: FC<Props> = ({ log, markdownParsing }) => {
               "vm-group-logs-row-content__arrow_open": isOpenFields,
             })}
           >
-            <ArrowDropDownIcon/>
+            <ArrowDownIcon/>
           </div>
         )}
         <div
@@ -70,11 +80,12 @@ const GroupLogsItem: FC<Props> = ({ log, markdownParsing }) => {
         <div
           className={classNames({
             "vm-group-logs-row-content__msg": true,
-            "vm-group-logs-row-content__msg_missing": !log._msg
+            "vm-group-logs-row-content__msg_empty-msg": !log._msg,
+            "vm-group-logs-row-content__msg_missing": !displayMessage
           })}
           dangerouslySetInnerHTML={markdownParsing && log._vmui_markdown ? { __html: log._vmui_markdown } : undefined}
         >
-          {log._msg || "message missing"}
+          {displayMessage || "-"}
         </div>
       </div>
       {hasFields && isOpenFields && (
@@ -94,7 +105,7 @@ const GroupLogsItem: FC<Props> = ({ log, markdownParsing }) => {
                           color="gray"
                           size="small"
                           startIcon={<CopyIcon/>}
-                          onClick={createCopyHandler(`${key}: ${value}`, i)}
+                          onClick={createCopyHandler(`${key}: "${value}"`, i)}
                           ariaLabel="copy to clipboard"
                         />
                       </Tooltip>
diff --git a/app/vmui/packages/vmui/src/pages/ExploreLogs/GroupLogs/style.scss b/app/vmui/packages/vmui/src/pages/ExploreLogs/GroupLogs/style.scss
index 71ee06179..7868be35b 100644
--- a/app/vmui/packages/vmui/src/pages/ExploreLogs/GroupLogs/style.scss
+++ b/app/vmui/packages/vmui/src/pages/ExploreLogs/GroupLogs/style.scss
@@ -21,7 +21,7 @@
         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;;
+        transition: background-color 0.3s ease-in, transform 0.1s ease-in, opacity 0.3s ease-in;
 
         &:hover {
           background-color: $color-tropical-blue;
@@ -30,6 +30,17 @@
         &:active {
           transform: translate(0, 3px);
         }
+
+        &_dark {
+          color: lighten($color-dodger-blue, 20%);
+          background-color: $color-background-body;
+          opacity: 0.8;
+
+          &:hover {
+            background-color: $color-background-body;
+            opacity: 1;
+          }
+        }
       }
     }
 
@@ -45,9 +56,8 @@
     &-content {
       position: relative;
       display: grid;
-      grid-template-columns: minmax(180px, max-content) 1fr;
-      gap: $padding-small;
-      padding: $padding-global;
+      grid-template-columns: auto minmax(180px, max-content) 1fr;
+      padding: $padding-global 0;
       cursor: pointer;
       transition: background-color 0.2s ease-in;
 
@@ -56,14 +66,11 @@
       }
 
       &__arrow {
-        position: absolute;
-        top: $padding-global;
-        left: 0;
         display: flex;
         align-items: center;
         justify-content: center;
-        width: 14px;
-        height: 14px;
+        width: 16px;
+        height: $font-size;
         transform: rotate(-90deg);
         transition: transform 0.2s ease-out;
 
@@ -76,6 +83,7 @@
         display: flex;
         align-items: flex-start;
         justify-content: flex-end;
+        margin-right: $padding-small;
         line-height: 1;
         white-space: nowrap;
 
@@ -91,6 +99,12 @@
         overflow-wrap: anywhere;
         line-height: 1.1;
 
+        &_empty-msg {
+          overflow: hidden;
+          text-overflow: ellipsis;
+          white-space: nowrap;
+        }
+
         &_missing {
           color: $color-text-disabled;
           font-style: italic;
diff --git a/app/vmui/packages/vmui/src/pages/ExploreLogs/hooks/useFetchLogHits.ts b/app/vmui/packages/vmui/src/pages/ExploreLogs/hooks/useFetchLogHits.ts
new file mode 100644
index 000000000..b6a709c7e
--- /dev/null
+++ b/app/vmui/packages/vmui/src/pages/ExploreLogs/hooks/useFetchLogHits.ts
@@ -0,0 +1,76 @@
+import { useCallback, useMemo, useRef, useState } from "preact/compat";
+import { getLogHitsUrl } from "../../../api/logs";
+import { ErrorTypes } from "../../../types";
+import { LogHits } from "../../../api/types";
+import { useTimeState } from "../../../state/time/TimeStateContext";
+import dayjs from "dayjs";
+import { LOGS_BARS_VIEW } from "../../../constants/logs";
+
+export const useFetchLogHits = (server: string, query: string) => {
+  const { period } = useTimeState();
+  const [logHits, setLogHits] = useState<LogHits[]>([]);
+  const [isLoading, setIsLoading] = useState(false);
+  const [error, setError] = useState<ErrorTypes | string>();
+  const abortControllerRef = useRef(new AbortController());
+
+  const url = useMemo(() => getLogHitsUrl(server), [server]);
+
+  const options = useMemo(() => {
+    const start = dayjs(period.start * 1000);
+    const end = dayjs(period.end * 1000);
+    const totalSeconds = end.diff(start, "milliseconds");
+    const step = Math.ceil(totalSeconds / LOGS_BARS_VIEW) || 1;
+
+    return {
+      method: "POST",
+      body: new URLSearchParams({
+        query: query.trim(),
+        step: `${step}ms`,
+        start: start.toISOString(),
+        end: end.toISOString(),
+      })
+    };
+  }, [query, period]);
+
+  const fetchLogHits = useCallback(async () => {
+    abortControllerRef.current.abort();
+    abortControllerRef.current = new AbortController();
+    const { signal } = abortControllerRef.current;
+    setIsLoading(true);
+    setError(undefined);
+    try {
+      const response = await fetch(url, { ...options, signal });
+
+      if (!response.ok || !response.body) {
+        const text = await response.text();
+        setError(text);
+        setLogHits([]);
+        setIsLoading(false);
+        return;
+      }
+
+      const data = await response.json();
+      const hits = data?.hits as LogHits[];
+      if (!hits) {
+        setError("Error: No 'hits' field in response");
+        return;
+      }
+
+      setLogHits(hits);
+    } catch (e) {
+      if (e instanceof Error && e.name !== "AbortError") {
+        setError(String(e));
+        console.error(e);
+        setLogHits([]);
+      }
+    }
+    // setIsLoading(false);
+  }, [url, options]);
+
+  return {
+    logHits,
+    isLoading,
+    error,
+    fetchLogHits,
+  };
+};
diff --git a/app/vmui/packages/vmui/src/pages/ExploreLogs/hooks/useFetchLogs.ts b/app/vmui/packages/vmui/src/pages/ExploreLogs/hooks/useFetchLogs.ts
index 21f63449c..00801cc2f 100644
--- a/app/vmui/packages/vmui/src/pages/ExploreLogs/hooks/useFetchLogs.ts
+++ b/app/vmui/packages/vmui/src/pages/ExploreLogs/hooks/useFetchLogs.ts
@@ -1,4 +1,4 @@
-import { useCallback, useMemo, useState } from "preact/compat";
+import { useCallback, useMemo, useRef, useState } from "preact/compat";
 import { getLogsUrl } from "../../../api/logs";
 import { ErrorTypes } from "../../../types";
 import { Logs } from "../../../api/types";
@@ -10,6 +10,7 @@ export const useFetchLogs = (server: string, query: string, limit: number) => {
   const [logs, setLogs] = useState<Logs[]>([]);
   const [isLoading, setIsLoading] = useState(false);
   const [error, setError] = useState<ErrorTypes | string>();
+  const abortControllerRef = useRef(new AbortController());
 
   const url = useMemo(() => getLogsUrl(server), [server]);
 
@@ -35,11 +36,14 @@ export const useFetchLogs = (server: string, query: string, limit: number) => {
   };
 
   const fetchLogs = useCallback(async () => {
+    abortControllerRef.current.abort();
+    abortControllerRef.current = new AbortController();
+    const { signal } = abortControllerRef.current;
     const limit = Number(options.body.get("limit"));
     setIsLoading(true);
     setError(undefined);
     try {
-      const response = await fetch(url, options);
+      const response = await fetch(url, { ...options, signal });
       const text = await response.text();
 
       if (!response.ok || !response.body) {
@@ -53,10 +57,10 @@ export const useFetchLogs = (server: string, query: string, limit: number) => {
       const data = lines.map(parseLineToJSON).filter(line => line) as Logs[];
       setLogs(data);
     } catch (e) {
-      console.error(e);
-      setLogs([]);
-      if (e instanceof Error) {
-        setError(`${e.name}: ${e.message}`);
+      if (e instanceof Error && e.name !== "AbortError") {
+        setError(String(e));
+        console.error(e);
+        setLogs([]);
       }
     }
     setIsLoading(false);
diff --git a/app/vmui/packages/vmui/src/utils/uplot/axes.ts b/app/vmui/packages/vmui/src/utils/uplot/axes.ts
index c4c8f9b82..631ba133f 100644
--- a/app/vmui/packages/vmui/src/utils/uplot/axes.ts
+++ b/app/vmui/packages/vmui/src/utils/uplot/axes.ts
@@ -31,7 +31,7 @@ export const getAxes = (series: Series[], unit?: string): Axis[] => Array.from(n
     values: (u: uPlot, ticks: number[]) => formatTicks(u, ticks, unit)
   };
   if (!a) return { space: 80, values: timeValues, stroke, font };
-  if (!(Number(a) % 2)) return { ...axis, side: 1 };
+  if (!(Number(a) % 2) && a !== "y") return { ...axis, side: 1 };
   return axis;
 });
 
diff --git a/app/vmui/packages/vmui/src/utils/uplot/bars.ts b/app/vmui/packages/vmui/src/utils/uplot/bars.ts
new file mode 100644
index 000000000..be7fc1444
--- /dev/null
+++ b/app/vmui/packages/vmui/src/utils/uplot/bars.ts
@@ -0,0 +1,14 @@
+import uPlot from "uplot";
+import { LOGS_BARS_VIEW } from "../../constants/logs";
+
+export const barPaths = (
+  u: uPlot,
+  seriesIdx: number,
+  idx0: number,
+  idx1: number,
+): uPlot.Series.Paths | null => {
+  const barSize = (u.under.clientWidth/LOGS_BARS_VIEW ) - 1;
+  const barsPathBuilderFactory = uPlot?.paths?.bars?.({ size: [0.96, barSize] });
+  return barsPathBuilderFactory ? barsPathBuilderFactory(u, seriesIdx, idx0, idx1) : null;
+};
+
diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md
index e80d199fc..1d5408e12 100644
--- a/docs/CHANGELOG.md
+++ b/docs/CHANGELOG.md
@@ -32,6 +32,7 @@ See also [LTS releases](https://docs.victoriametrics.com/lts-releases/).
 
 **Update note 1: the `--vm-disable-progress-bar` command-line flag at `vmctl` was deprecated. Use `--disable-progress-bar` instead.**
 
+* FEATURE: all VictoriaMetrics components: use constant-time comparison for comparing HTTP basic auth credentials and auth keys. This should prevent timing attacks when comparing these credentials. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/6392) for details. Thanks to @wasim-nihal for [the pull request](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/6423).
 * FEATURE: [alerts-vmagent](https://github.com/VictoriaMetrics/VictoriaMetrics/blob/master/deployment/docker/alerts-vmagent.yml): add new alerting rules `StreamAggrFlushTimeout` and `StreamAggrDedupFlushTimeout` to notify about issues during stream aggregation.
 * FEATURE: [dashboards/vmagent](https://grafana.com/grafana/dashboards/12683): add row `Streaming aggregation` with panels related to [streaming aggregation](https://docs.victoriametrics.com/stream-aggregation/) process.
 * FEATURE: [vmauth](https://docs.victoriametrics.com/vmauth/): add `idleConnTimeout` flag set to 50s by default. It should reduce the probability of `broken pipe` or `connection reset by peer` errors in vmauth logs.
@@ -39,6 +40,7 @@ See also [LTS releases](https://docs.victoriametrics.com/lts-releases/).
 * FEATURE: [vmagent](https://docs.victoriametrics.com/vmagent/): increase default value of `-promscrape.maxDroppedTargets` command-line flag to 10_000 from 1000. This makes it easier to track down large number of dropped targets.
 * FEATURE: [vmsingle](https://docs.victoriametrics.com/single-server-victoriametrics/): check for ranged vector arguments in non-rollup expressions when `-search.disableImplicitConversion` or `-search.logImplicitConversion` are enabled. For example, `sum(up[5m])` or `absent(up[5m])` will fail to execute if these flags are set.
 * FEATURE: [vmsingle](https://docs.victoriametrics.com/single-server-victoriametrics/): validate that rollup expressions has ranged vector arguments passed when `-search.disableImplicitConversion` or `-search.logImplicitConversion` are enabled. For example, `rate(metric)` or `count_over_time(metric)` will fail to execute if these flags are set.
+* FEATURE: [vmalert-tool](https://docs.victoriametrics.com/vmalert-tool/): support file path with hierarchical patterns and regexpes, and http url in unittest cmd-line flag `-files`, e.g. `-files="http://<some-server-addr>/path/to/rules"` or `-files="dir/**/*.yaml"`.
 
 * BUGFIX: all VictoriaMetrics components: prioritize `-configAuthKey` and `-reloadAuthKey` over `-httpAuth.*` settings. This change aligns behavior of mentioned flags with other auth flags like `-metricsAuthKey`, `-flagsAuthKey`, `-pprofAuthKey`. Check [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/6329).
 * BUGFIX: [vmctl](https://docs.victoriametrics.com/vmctl/): add `--disable-progress-bar` global command-line flag. It can be used for disabling dynamic progress bar for all migration modes. `--vm-disable-progress-bar`  command-line flag is deprecated and will be removed in the future releases. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/6367).
diff --git a/docs/Cluster-VictoriaMetrics.md b/docs/Cluster-VictoriaMetrics.md
index 84b3c9289..fbbdfeaf7 100644
--- a/docs/Cluster-VictoriaMetrics.md
+++ b/docs/Cluster-VictoriaMetrics.md
@@ -1487,6 +1487,8 @@ Below is the output for `/path/to/vmselect -help`:
      Whether to deny partial responses if a part of -storageNode instances fail to perform queries; this trades availability over consistency; see also -search.maxQueryDuration
   -search.disableCache
      Whether to disable response caching. This may be useful when ingesting historical data. See https://docs.victoriametrics.com/#backfilling . See also -search.resetRollupResultCacheOnStartup
+  -search.disableImplicitConversion
+     Whether to return an error for queries that rely on implicit subquery conversions, see https://docs.victoriametrics.com/metricsql/#subqueries for details. See also -search.logImplicitConversion
   -search.graphiteMaxPointsPerSeries int
      The maximum number of points per series Graphite render API can return (default 1000000)
   -search.graphiteStorageStep duration
@@ -1495,6 +1497,8 @@ Below is the output for `/path/to/vmselect -help`:
      Whether to ignore match[], extra_filters[] and extra_label query args at /api/v1/labels and /api/v1/label/.../values . This may be useful for decreasing load on VictoriaMetrics when extra filters match too many time series. The downside is that superfluous labels or series could be returned, which do not match the extra filters. See also -search.maxLabelsAPISeries and -search.maxLabelsAPIDuration
   -search.latencyOffset duration
      The time when data points become visible in query results after the collection. It can be overridden on per-query basis via latency_offset arg. Too small value can result in incomplete last points for query results (default 30s)
+  -search.logImplicitConversion
+     Whether to log queries with implicit subquery conversions, see https://docs.victoriametrics.com/metricsql/#subqueries for details. Such conversion can be disabled using -search.disableImplicitConversion
   -search.logQueryMemoryUsage size
      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. See also -search.logSlowQueryDuration and -search.maxMemoryPerQuery
      Supports the following optional suffixes for size values: KB, MB, GB, TB, KiB, MiB, GiB, TiB (default 0)
diff --git a/docs/README.md b/docs/README.md
index cc743edbd..1c14dbd15 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -3096,6 +3096,8 @@ Pass `-help` to VictoriaMetrics in order to see the list of supported command-li
      Whether to disable automatic response cache reset if a sample with timestamp outside -search.cacheTimestampOffset is inserted into VictoriaMetrics
   -search.disableCache
      Whether to disable response caching. This may be useful when ingesting historical data. See https://docs.victoriametrics.com/#backfilling . See also -search.resetRollupResultCacheOnStartup
+  -search.disableImplicitConversion
+     Whether to return an error for queries that rely on implicit subquery conversions, see https://docs.victoriametrics.com/metricsql/#subqueries for details. See also -search.logImplicitConversion
   -search.graphiteMaxPointsPerSeries int
      The maximum number of points per series Graphite render API can return (default 1000000)
   -search.graphiteStorageStep duration
@@ -3104,6 +3106,8 @@ Pass `-help` to VictoriaMetrics in order to see the list of supported command-li
      Whether to ignore match[], extra_filters[] and extra_label query args at /api/v1/labels and /api/v1/label/.../values . This may be useful for decreasing load on VictoriaMetrics when extra filters match too many time series. The downside is that superfluous labels or series could be returned, which do not match the extra filters. See also -search.maxLabelsAPISeries and -search.maxLabelsAPIDuration
   -search.latencyOffset duration
      The time when data points become visible in query results after the collection. It can be overridden on per-query basis via latency_offset arg. Too small value can result in incomplete last points for query results (default 30s)
+  -search.logImplicitConversion
+     Whether to log queries with implicit subquery conversions, see https://docs.victoriametrics.com/metricsql/#subqueries for details. Such conversion can be disabled using -search.disableImplicitConversion
   -search.logQueryMemoryUsage size
      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. See also -search.logSlowQueryDuration and -search.maxMemoryPerQuery
      Supports the following optional suffixes for size values: KB, MB, GB, TB, KiB, MiB, GiB, TiB (default 0)
diff --git a/docs/Single-server-VictoriaMetrics.md b/docs/Single-server-VictoriaMetrics.md
index 0f6e198b8..2432465a3 100644
--- a/docs/Single-server-VictoriaMetrics.md
+++ b/docs/Single-server-VictoriaMetrics.md
@@ -3104,6 +3104,8 @@ Pass `-help` to VictoriaMetrics in order to see the list of supported command-li
      Whether to disable automatic response cache reset if a sample with timestamp outside -search.cacheTimestampOffset is inserted into VictoriaMetrics
   -search.disableCache
      Whether to disable response caching. This may be useful when ingesting historical data. See https://docs.victoriametrics.com/#backfilling . See also -search.resetRollupResultCacheOnStartup
+  -search.disableImplicitConversion
+     Whether to return an error for queries that rely on implicit subquery conversions, see https://docs.victoriametrics.com/metricsql/#subqueries for details. See also -search.logImplicitConversion
   -search.graphiteMaxPointsPerSeries int
      The maximum number of points per series Graphite render API can return (default 1000000)
   -search.graphiteStorageStep duration
@@ -3112,6 +3114,8 @@ Pass `-help` to VictoriaMetrics in order to see the list of supported command-li
      Whether to ignore match[], extra_filters[] and extra_label query args at /api/v1/labels and /api/v1/label/.../values . This may be useful for decreasing load on VictoriaMetrics when extra filters match too many time series. The downside is that superfluous labels or series could be returned, which do not match the extra filters. See also -search.maxLabelsAPISeries and -search.maxLabelsAPIDuration
   -search.latencyOffset duration
      The time when data points become visible in query results after the collection. It can be overridden on per-query basis via latency_offset arg. Too small value can result in incomplete last points for query results (default 30s)
+  -search.logImplicitConversion
+     Whether to log queries with implicit subquery conversions, see https://docs.victoriametrics.com/metricsql/#subqueries for details. Such conversion can be disabled using -search.disableImplicitConversion
   -search.logQueryMemoryUsage size
      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. See also -search.logSlowQueryDuration and -search.maxMemoryPerQuery
      Supports the following optional suffixes for size values: KB, MB, GB, TB, KiB, MiB, GiB, TiB (default 0)
diff --git a/docs/VictoriaLogs/CHANGELOG.md b/docs/VictoriaLogs/CHANGELOG.md
index 7bef94515..d197c44f5 100644
--- a/docs/VictoriaLogs/CHANGELOG.md
+++ b/docs/VictoriaLogs/CHANGELOG.md
@@ -19,6 +19,8 @@ according to [these docs](https://docs.victoriametrics.com/victorialogs/quicksta
 
 ## tip
 
+* FEATURE: [web UI](https://docs.victoriametrics.com/victorialogs/querying/#web-ui): add a bar chart displaying the number of log entries over a time range. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/6404).
+
 ## [v0.20.2](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v0.20.2-victorialogs)
 
 Released at 2024-06-18
diff --git a/docs/vmalert-tool.md b/docs/vmalert-tool.md
index 07551e61a..529084494 100644
--- a/docs/vmalert-tool.md
+++ b/docs/vmalert-tool.md
@@ -26,8 +26,9 @@ It will perform the following actions:
 See how to run vmalert-tool for unit test below:
 
 ```
-# Run vmalert-tool with one or multiple test files via --files cmd-line flag
-./vmalert-tool unittest --files test1.yaml --files test2.yaml
+# Run vmalert-tool with one or multiple test files via `--files` cmd-line flag
+# Supports file path with hierarchical patterns and regexpes, and http url.
+./vmalert-tool unittest --files /path/to/file --files http://<some-server-addr>/path/to/test.yaml
 ```
 
 vmalert-tool unittest is compatible with [Prometheus config format for tests](https://prometheus.io/docs/prometheus/latest/configuration/unit_testing_rules/#test-file-format)
diff --git a/lib/httpserver/httpserver.go b/lib/httpserver/httpserver.go
index 4c9c768e2..40573003a 100644
--- a/lib/httpserver/httpserver.go
+++ b/lib/httpserver/httpserver.go
@@ -2,6 +2,7 @@ package httpserver
 
 import (
 	"context"
+	"crypto/subtle"
 	"crypto/tls"
 	_ "embed"
 	"errors"
@@ -442,7 +443,7 @@ func CheckAuthFlag(w http.ResponseWriter, r *http.Request, flagValue string, fla
 	if flagValue == "" {
 		return CheckBasicAuth(w, r)
 	}
-	if r.FormValue("authKey") != flagValue {
+	if !constantTimeEqual(r.FormValue("authKey"), flagValue) {
 		authKeyRequestErrors.Inc()
 		http.Error(w, fmt.Sprintf("The provided authKey doesn't match -%s", flagName), http.StatusUnauthorized)
 		return false
@@ -459,7 +460,7 @@ func CheckBasicAuth(w http.ResponseWriter, r *http.Request) bool {
 	}
 	username, password, ok := r.BasicAuth()
 	if ok {
-		if username == *httpAuthUsername && password == httpAuthPassword.Get() {
+		if constantTimeEqual(username, *httpAuthUsername) && constantTimeEqual(password, httpAuthPassword.Get()) {
 			return true
 		}
 		authBasicRequestErrors.Inc()
@@ -712,3 +713,16 @@ func LogError(req *http.Request, errStr string) {
 	remoteAddr := GetQuotedRemoteAddr(req)
 	logger.Errorf("uri: %s, remote address: %q: %s", uri, remoteAddr, errStr)
 }
+
+// constantTimeEqual compares two strings in constant-time.
+//
+// It returns true if they are equal, else it returns false.
+func constantTimeEqual(s1, s2 string) bool {
+	a := []byte(s1)
+	b := []byte(s2)
+	// check length explicitly because ConstantTimeCompare doesn't spend time on comparing length
+	if subtle.ConstantTimeEq(int32(len(a)), int32(len(b))) == 0 {
+		return false
+	}
+	return subtle.ConstantTimeCompare(a, b) == 1
+}


Test patch:
diff --git a/app/vmalert-tool/unittest/unittest.go b/app/vmalert-tool/unittest/unittest.go
index cb7cd7996..534153524 100644
--- a/app/vmalert-tool/unittest/unittest.go
+++ b/app/vmalert-tool/unittest/unittest.go
@@ -9,10 +9,12 @@ import (
 	"path/filepath"
 	"reflect"
 	"sort"
+	"strings"
 	"time"
 
 	"gopkg.in/yaml.v2"
 
+	"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/config"
 	vmalertconfig "github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/config"
 	"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/datasource"
 	"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/notifier"
@@ -66,36 +68,38 @@ func UnitTest(files []string, disableGroupLabel bool) bool {
 	defer vminsert.Stop()
 	defer vmselect.Stop()
 	disableAlertgroupLabel = disableGroupLabel
-	return rulesUnitTest(files)
-}
 
-func rulesUnitTest(files []string) bool {
+	testfiles, err := config.ReadFromFS(files)
+	if err != nil {
+		fmt.Println("  FAILED")
+		fmt.Printf("\nfailed to read test files: \n%v", err)
+	}
 	var failed bool
-	for _, f := range files {
-		if err := ruleUnitTest(f); err != nil {
+	for fileName, file := range testfiles {
+		if err := ruleUnitTest(fileName, file); err != nil {
 			fmt.Println("  FAILED")
-			fmt.Printf("\nfailed to run unit test for file %q: \n%v", f, err)
+			fmt.Printf("\nfailed to run unit test for file %q: \n%v", file, err)
 			failed = true
 		} else {
 			fmt.Println("  SUCCESS")
 		}
 	}
+
 	return failed
 }
 
-func ruleUnitTest(filename string) []error {
+func ruleUnitTest(filename string, content []byte) []error {
 	fmt.Println("\nUnit Testing: ", filename)
-	b, err := os.ReadFile(filename)
-	if err != nil {
-		return []error{fmt.Errorf("failed to read file: %w", err)}
-	}
-
 	var unitTestInp unitTestFile
-	if err := yaml.UnmarshalStrict(b, &unitTestInp); err != nil {
+	if err := yaml.UnmarshalStrict(content, &unitTestInp); err != nil {
 		return []error{fmt.Errorf("failed to unmarshal file: %w", err)}
 	}
-	if err := resolveAndGlobFilepaths(filepath.Dir(filename), &unitTestInp); err != nil {
-		return []error{fmt.Errorf("failed to resolve path for `rule_files`: %w", err)}
+
+	// add file directory for rule files if needed
+	for i, rf := range unitTestInp.RuleFiles {
+		if rf != "" && !filepath.IsAbs(rf) && !strings.HasPrefix(rf, "http") {
+			unitTestInp.RuleFiles[i] = filepath.Join(filepath.Dir(filename), rf)
+		}
 	}
 
 	if unitTestInp.EvaluationInterval.Duration() == 0 {
@@ -235,30 +239,6 @@ func tearDown() {
 	fs.MustRemoveAll(storagePath)
 }
 
-// resolveAndGlobFilepaths joins all relative paths in a configuration
-// with a given base directory and replaces all globs with matching files.
-func resolveAndGlobFilepaths(baseDir string, utf *unitTestFile) error {
-	for i, rf := range utf.RuleFiles {
-		if rf != "" && !filepath.IsAbs(rf) {
-			utf.RuleFiles[i] = filepath.Join(baseDir, rf)
-		}
-	}
-
-	var globbedFiles []string
-	for _, rf := range utf.RuleFiles {
-		m, err := filepath.Glob(rf)
-		if err != nil {
-			return err
-		}
-		if len(m) == 0 {
-			fmt.Fprintln(os.Stderr, "  WARNING: no file match pattern", rf)
-		}
-		globbedFiles = append(globbedFiles, m...)
-	}
-	utf.RuleFiles = globbedFiles
-	return nil
-}
-
 func (tg *testGroup) test(evalInterval time.Duration, groupOrderMap map[string]int, testGroups []vmalertconfig.Group) (checkErrs []error) {
 	// set up vmstorage and http server for ingest and read queries
 	setUp()