mirror of
https://github.com/VictoriaMetrics/VictoriaMetrics.git
synced 2026-06-09 03:43:58 +03:00
Compare commits
82 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fe736c5388 | ||
|
|
607b542222 | ||
|
|
8b9ebf625a | ||
|
|
7b87fac8e7 | ||
|
|
7b1caf1db3 | ||
|
|
9254e494f9 | ||
|
|
68985455f1 | ||
|
|
4cf37c5e70 | ||
|
|
3d331e4c5d | ||
|
|
4ecb61e247 | ||
|
|
442a9f16b4 | ||
|
|
dcc5616126 | ||
|
|
080a3e2396 | ||
|
|
ac8bc77688 | ||
|
|
1cbdcd391c | ||
|
|
0788be35eb | ||
|
|
ab57b92932 | ||
|
|
6a7faf9f22 | ||
|
|
ac14d50c18 | ||
|
|
51ad94677c | ||
|
|
bd716d1b0c | ||
|
|
06f6b76521 | ||
|
|
a0c8b86eab | ||
|
|
ff39a91147 | ||
|
|
372b1688d7 | ||
|
|
1b81d8f542 | ||
|
|
7e355080ce | ||
|
|
2afa4ae00a | ||
|
|
6342eb5523 | ||
|
|
54a0ccbaca | ||
|
|
b28cf0faa8 | ||
|
|
3251d392b5 | ||
|
|
119010f7f2 | ||
|
|
eedb294754 | ||
|
|
4250b67fe8 | ||
|
|
465c889f7f | ||
|
|
834c18a346 | ||
|
|
36941d6d75 | ||
|
|
558165521b | ||
|
|
0890adde67 | ||
|
|
28d92a2f31 | ||
|
|
cb374677a9 | ||
|
|
73256fe438 | ||
|
|
2c1419c687 | ||
|
|
465a285324 | ||
|
|
95ee86b600 | ||
|
|
28f66f0079 | ||
|
|
d655d6b047 | ||
|
|
99d49e3ceb | ||
|
|
20ad848c5d | ||
|
|
1a8875b417 | ||
|
|
c7c4786f3f | ||
|
|
a971bcc3fe | ||
|
|
0fd440cdb4 | ||
|
|
c496f06ca3 | ||
|
|
f7acdb13db | ||
|
|
1cfa183c2b | ||
|
|
3536bef36e | ||
|
|
babecd8363 | ||
|
|
393876e52a | ||
|
|
2c4e384f07 | ||
|
|
ba5a6c851c | ||
|
|
1a3a6ef907 | ||
|
|
7030429958 | ||
|
|
30e968df6d | ||
|
|
f2b40dbe9a | ||
|
|
a11dc6689a | ||
|
|
0a4d8dc777 | ||
|
|
3d1cb011b6 | ||
|
|
a7f8ce5e3d | ||
|
|
7c1daade15 | ||
|
|
f3cb412508 | ||
|
|
edc51b3119 | ||
|
|
cb5d073bc9 | ||
|
|
864c651c03 | ||
|
|
4e25bc2087 | ||
|
|
74df30456b | ||
|
|
df7b81b44d | ||
|
|
d513230d43 | ||
|
|
e8554cd1cb | ||
|
|
59b67f1cfa | ||
|
|
adeec6e369 |
2
.github/workflows/main.yml
vendored
2
.github/workflows/main.yml
vendored
@@ -15,6 +15,8 @@ jobs:
|
||||
steps:
|
||||
- name: Code checkout
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
ref: ${{ github.event.workflow_run.head_branch }}
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v3
|
||||
|
||||
11
Makefile
11
Makefile
@@ -144,6 +144,7 @@ vmutils-windows-amd64: \
|
||||
vmctl-windows-amd64
|
||||
|
||||
victoria-metrics-crossbuild: \
|
||||
victoria-metrics-linux-386 \
|
||||
victoria-metrics-linux-amd64 \
|
||||
victoria-metrics-linux-arm64 \
|
||||
victoria-metrics-linux-arm \
|
||||
@@ -155,6 +156,7 @@ victoria-metrics-crossbuild: \
|
||||
victoria-metrics-openbsd-amd64
|
||||
|
||||
vmutils-crossbuild: \
|
||||
vmutils-linux-386 \
|
||||
vmutils-linux-amd64 \
|
||||
vmutils-linux-arm64 \
|
||||
vmutils-linux-arm \
|
||||
@@ -177,6 +179,7 @@ release: \
|
||||
release-vmutils
|
||||
|
||||
release-victoria-metrics: \
|
||||
release-victoria-metrics-linux-386 \
|
||||
release-victoria-metrics-linux-amd64 \
|
||||
release-victoria-metrics-linux-arm \
|
||||
release-victoria-metrics-linux-arm64 \
|
||||
@@ -185,6 +188,10 @@ release-victoria-metrics: \
|
||||
release-victoria-metrics-freebsd-amd64 \
|
||||
release-victoria-metrics-openbsd-amd64
|
||||
|
||||
# adds i386 arch
|
||||
release-victoria-metrics-linux-386:
|
||||
GOOS=linux GOARCH=386 $(MAKE) release-victoria-metrics-goos-goarch
|
||||
|
||||
release-victoria-metrics-linux-amd64:
|
||||
GOOS=linux GOARCH=amd64 $(MAKE) release-victoria-metrics-goos-goarch
|
||||
|
||||
@@ -216,6 +223,7 @@ release-victoria-metrics-goos-goarch: victoria-metrics-$(GOOS)-$(GOARCH)-prod
|
||||
cd bin && rm -rf victoria-metrics-$(GOOS)-$(GOARCH)-prod
|
||||
|
||||
release-vmutils: \
|
||||
release-vmutils-linux-386 \
|
||||
release-vmutils-linux-amd64 \
|
||||
release-vmutils-linux-arm64 \
|
||||
release-vmutils-linux-arm \
|
||||
@@ -225,6 +233,9 @@ release-vmutils: \
|
||||
release-vmutils-openbsd-amd64 \
|
||||
release-vmutils-windows-amd64
|
||||
|
||||
release-vmutils-linux-386:
|
||||
GOOS=linux GOARCH=386 $(MAKE) release-vmutils-goos-goarch
|
||||
|
||||
release-vmutils-linux-amd64:
|
||||
GOOS=linux GOARCH=amd64 $(MAKE) release-vmutils-goos-goarch
|
||||
|
||||
|
||||
42
README.md
42
README.md
@@ -1085,7 +1085,9 @@ The [deduplication](#deduplication) isn't applied for the data exported in nativ
|
||||
|
||||
## How to import time series data
|
||||
|
||||
Time series data can be imported into VictoriaMetrics via any supported data ingestion protocol:
|
||||
VictoriaMetrics can discover and scrape metrics from Prometheus-compatible targets (aka "pull" protocol) -
|
||||
see [these docs](#how-to-scrape-prometheus-exporters-such-as-node-exporter).
|
||||
Additionally, VictoriaMetrics can accept metrics via the following popular data ingestion protocols (aka "push" protocols):
|
||||
|
||||
* [Prometheus remote_write API](https://prometheus.io/docs/prometheus/latest/configuration/configuration/#remote_write). See [these docs](#prometheus-setup) for details.
|
||||
* DataDog `submit metrics` API. See [these docs](#how-to-send-data-from-datadog-agent) for details.
|
||||
@@ -1614,7 +1616,9 @@ VictoriaMetrics provides the following security-related command-line flags:
|
||||
Explicitly set internal network interface for TCP and UDP ports for data ingestion with Graphite and OpenTSDB formats.
|
||||
For example, substitute `-graphiteListenAddr=:2003` with `-graphiteListenAddr=<internal_iface_ip>:2003`. This protects from unexpected requests from untrusted network interfaces.
|
||||
|
||||
VictoriaMetrics has achieved security certifications for Database Software Development and Software-Based Monitoring Services. We apply strict security measures in everything we do. See our [Security page](https://victoriametrics.com/security/) for more details.
|
||||
See also [security recommendation for VictoriaMetrics cluster](https://docs.victoriametrics.com/Cluster-VictoriaMetrics.html#security)
|
||||
and [the general security page at VictoriaMetrics website](https://victoriametrics.com/security/).
|
||||
|
||||
|
||||
## Tuning
|
||||
|
||||
@@ -2170,7 +2174,9 @@ Pass `-help` to VictoriaMetrics in order to see the list of supported command-li
|
||||
-fs.disableMmap
|
||||
Whether to use pread() instead of mmap() for reading data files. By default mmap() is used for 64-bit arches and pread() is used for 32-bit arches, since they cannot read data files bigger than 2^32 bytes in memory. mmap() is usually faster for reading small data chunks than pread()
|
||||
-graphiteListenAddr string
|
||||
TCP and UDP address to listen for Graphite plaintext data. Usually :2003 must be set. Doesn't work if empty
|
||||
TCP and UDP address to listen for Graphite plaintext data. Usually :2003 must be set. Doesn't work if empty. See also -graphiteListenAddr.useProxyProtocol
|
||||
-graphiteListenAddr.useProxyProtocol
|
||||
Whether to use proxy protocol for connections accepted at -graphiteListenAddr . See https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt
|
||||
-graphiteTrimTimestamp duration
|
||||
Trim timestamps for Graphite data to this duration. Minimum practical duration is 1s. Higher duration (i.e. 1m) may be used for reducing disk space usage for timestamp data (default 1s)
|
||||
-http.connTimeout duration
|
||||
@@ -2190,7 +2196,9 @@ Pass `-help` to VictoriaMetrics in order to see the list of supported command-li
|
||||
-httpAuth.username string
|
||||
Username for HTTP Basic Auth. The authentication is disabled if empty. See also -httpAuth.password
|
||||
-httpListenAddr string
|
||||
TCP address to listen for http connections (default ":8428")
|
||||
TCP address to listen for http connections. See also -httpListenAddr.useProxyProtocol (default ":8428")
|
||||
-httpListenAddr.useProxyProtocol
|
||||
Whether to use proxy protocol for connections accepted at -httpListenAddr . See https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt
|
||||
-import.maxLineLen size
|
||||
The maximum length in bytes of a single line accepted by /api/v1/import; the line length can be limited with 'max_rows_per_line' query arg passed to /api/v1/export
|
||||
Supports the following optional suffixes for size values: KB, MB, GB, TB, KiB, MiB, GiB, TiB (default 104857600)
|
||||
@@ -2203,7 +2211,9 @@ Pass `-help` to VictoriaMetrics in order to see the list of supported command-li
|
||||
-influxDBLabel string
|
||||
Default label for the DB name sent over '?db={db_name}' query parameter (default "db")
|
||||
-influxListenAddr string
|
||||
TCP and UDP address to listen for InfluxDB line protocol data. Usually :8089 must be set. Doesn't work if empty. This flag isn't needed when ingesting data over HTTP - just send it to http://<victoriametrics>:8428/write
|
||||
TCP and UDP address to listen for InfluxDB line protocol data. Usually :8089 must be set. Doesn't work if empty. This flag isn't needed when ingesting data over HTTP - just send it to http://<victoriametrics>:8428/write . See also -influxListenAddr.useProxyProtocol
|
||||
-influxListenAddr.useProxyProtocol
|
||||
Whether to use proxy protocol for connections accepted at -influxListenAddr . See https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt
|
||||
-influxMeasurementFieldSeparator string
|
||||
Separator for '{measurement}{separator}{field_name}' metric name when inserted via InfluxDB line protocol (default "_")
|
||||
-influxSkipMeasurement
|
||||
@@ -2215,7 +2225,9 @@ Pass `-help` to VictoriaMetrics in order to see the list of supported command-li
|
||||
-inmemoryDataFlushInterval duration
|
||||
The interval for guaranteed saving of in-memory data to disk. The saved data survives unclean shutdown such as OOM crash, hardware reset, SIGKILL, etc. Bigger intervals may help increasing lifetime of flash storage with limited write cycles (e.g. Raspberry PI). Smaller intervals increase disk IO load. Minimum supported value is 1s (default 5s)
|
||||
-insert.maxQueueDuration duration
|
||||
The maximum duration for waiting in the queue for insert requests due to -maxConcurrentInserts (default 1m0s)
|
||||
The maximum duration to wait in the queue when -maxConcurrentInserts concurrent insert requests are executed (default 1m0s)
|
||||
-internStringMaxLen int
|
||||
The maximum length for strings to intern. Lower limit may save memory at the cost of higher CPU usage. See https://en.wikipedia.org/wiki/String_interning (default 300)
|
||||
-logNewSeries
|
||||
Whether to log new series. This option is for debug purposes only. It can lead to performance issues when big number of new series are ingested into VictoriaMetrics
|
||||
-loggerDisableTimestamps
|
||||
@@ -2251,9 +2263,13 @@ Pass `-help` to VictoriaMetrics in order to see the list of supported command-li
|
||||
-metricsAuthKey string
|
||||
Auth key for /metrics endpoint. It must be passed via authKey query arg. It overrides httpAuth.* settings
|
||||
-opentsdbHTTPListenAddr string
|
||||
TCP address to listen for OpentTSDB HTTP put requests. Usually :4242 must be set. Doesn't work if empty
|
||||
TCP address to listen for OpentTSDB HTTP put requests. Usually :4242 must be set. Doesn't work if empty. See also -opentsdbHTTPListenAddr.useProxyProtocol
|
||||
-opentsdbHTTPListenAddr.useProxyProtocol
|
||||
Whether to use proxy protocol for connections accepted at -opentsdbHTTPListenAddr . See https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt
|
||||
-opentsdbListenAddr string
|
||||
TCP and UDP address to listen for OpentTSDB metrics. Telnet put messages and HTTP /api/put messages are simultaneously served on TCP port. Usually :4242 must be set. Doesn't work if empty
|
||||
TCP and UDP address to listen for OpentTSDB metrics. Telnet put messages and HTTP /api/put messages are simultaneously served on TCP port. Usually :4242 must be set. Doesn't work if empty. See also -opentsdbListenAddr.useProxyProtocol
|
||||
-opentsdbListenAddr.useProxyProtocol
|
||||
Whether to use proxy protocol for connections accepted at -opentsdbListenAddr . See https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt
|
||||
-opentsdbTrimTimestamp duration
|
||||
Trim timestamps for OpenTSDB 'telnet put' data to this duration. Minimum practical duration is 1s. Higher duration (i.e. 1m) may be used for reducing disk space usage for timestamp data (default 1s)
|
||||
-opentsdbhttp.maxInsertRequestSize size
|
||||
@@ -2332,12 +2348,12 @@ Pass `-help` to VictoriaMetrics in order to see the list of supported command-li
|
||||
-promscrape.minResponseSizeForStreamParse size
|
||||
The minimum target response size for automatic switching to stream parsing mode, which can reduce memory usage. See https://docs.victoriametrics.com/vmagent.html#stream-parsing-mode
|
||||
Supports the following optional suffixes for size values: KB, MB, GB, TB, KiB, MiB, GiB, TiB (default 1000000)
|
||||
-promscrape.noStaleMarkers
|
||||
Whether to disable sending Prometheus stale markers for metrics when scrape target disappears. This option may reduce memory usage if stale markers aren't needed for your setup. This option also disables populating the scrape_series_added metric. See https://prometheus.io/docs/concepts/jobs_instances/#automatically-generated-labels-and-time-series
|
||||
-promscrape.nomad.waitTime duration
|
||||
Wait time used by Nomad service discovery. Default value is used if not set
|
||||
-promscrape.nomadSDCheckInterval duration
|
||||
Interval for checking for changes in Nomad. This works only if nomad_sd_configs is configured in '-promscrape.config' file. See https://docs.victoriametrics.com/sd_configs.html#nomad_sd_configs for details (default 30s)
|
||||
-promscrape.noStaleMarkers
|
||||
Whether to disable sending Prometheus stale markers for metrics when scrape target disappears. This option may reduce memory usage if stale markers aren't needed for your setup. This option also disables populating the scrape_series_added metric. See https://prometheus.io/docs/concepts/jobs_instances/#automatically-generated-labels-and-time-series
|
||||
-promscrape.openstackSDCheckInterval duration
|
||||
Interval for checking for changes in openstack API server. This works only if openstack_sd_configs is configured in '-promscrape.config' file. See https://docs.victoriametrics.com/sd_configs.html#openstack_sd_configs for details (default 30s)
|
||||
-promscrape.seriesLimitPerTarget int
|
||||
@@ -2483,9 +2499,11 @@ Pass `-help` to VictoriaMetrics in order to see the list of supported command-li
|
||||
-storageDataPath string
|
||||
Path to storage data (default "victoria-metrics-data")
|
||||
-streamAggr.config string
|
||||
Optional path to file with stream aggregation config. See https://docs.victoriametrics.com/stream-aggregation.html .See also -remoteWrite.streamAggr.keepInput
|
||||
Optional path to file with stream aggregation config. See https://docs.victoriametrics.com/stream-aggregation.html . See also -remoteWrite.streamAggr.keepInput and -streamAggr.dedupInterval
|
||||
-streamAggr.dedupInterval duration
|
||||
Input samples are de-duplicated with this interval before being aggregated. Only the last sample per each time series per each interval is aggregated if the interval is greater than zero
|
||||
-streamAggr.keepInput
|
||||
Whether to keep input samples after the aggregation with -streamAggr.config .By default the input is dropped after the aggregation, so only the aggregate data is stored. See https://docs.victoriametrics.com/stream-aggregation.html
|
||||
Whether to keep input samples after the aggregation with -streamAggr.config. By default the input is dropped after the aggregation, so only the aggregate data is stored. See https://docs.victoriametrics.com/stream-aggregation.html
|
||||
-tls
|
||||
Whether to enable TLS for incoming HTTP requests at -httpListenAddr (aka https). -tlsCertFile and -tlsKeyFile must be set if -tls is set
|
||||
-tlsCertFile string
|
||||
|
||||
@@ -24,7 +24,9 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
httpListenAddr = flag.String("httpListenAddr", ":8428", "TCP address to listen for http connections")
|
||||
httpListenAddr = flag.String("httpListenAddr", ":8428", "TCP address to listen for http connections. See also -httpListenAddr.useProxyProtocol")
|
||||
useProxyProtocol = flag.Bool("httpListenAddr.useProxyProtocol", false, "Whether to use proxy protocol for connections accepted at -httpListenAddr . "+
|
||||
"See https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt")
|
||||
minScrapeInterval = flag.Duration("dedup.minScrapeInterval", 0, "Leave only the last sample in every time series per each discrete interval "+
|
||||
"equal to -dedup.minScrapeInterval > 0. See https://docs.victoriametrics.com/#deduplication and https://docs.victoriametrics.com/#downsampling")
|
||||
dryRun = flag.Bool("dryRun", false, "Whether to check only -promscrape.config and then exit. "+
|
||||
@@ -64,7 +66,7 @@ func main() {
|
||||
vminsert.Init()
|
||||
startSelfScraper()
|
||||
|
||||
go httpserver.Serve(*httpListenAddr, requestHandler)
|
||||
go httpserver.Serve(*httpListenAddr, *useProxyProtocol, requestHandler)
|
||||
logger.Infof("started VictoriaMetrics in %.3f seconds", time.Since(startTime).Seconds())
|
||||
|
||||
sig := procutil.WaitForSigterm()
|
||||
|
||||
@@ -129,10 +129,10 @@ func setUp() {
|
||||
storagePath = filepath.Join(os.TempDir(), testStorageSuffix)
|
||||
processFlags()
|
||||
logger.Init()
|
||||
vmstorage.InitWithoutMetrics(promql.ResetRollupResultCacheIfNeeded)
|
||||
vmstorage.Init(promql.ResetRollupResultCacheIfNeeded)
|
||||
vmselect.Init()
|
||||
vminsert.Init()
|
||||
go httpserver.Serve(*httpListenAddr, requestHandler)
|
||||
go httpserver.Serve(*httpListenAddr, false, requestHandler)
|
||||
readyStorageCheckFunc := func() bool {
|
||||
resp, err := http.Get(testHealthHTTPPath)
|
||||
if err != nil {
|
||||
@@ -189,10 +189,8 @@ func tearDown() {
|
||||
|
||||
func TestWriteRead(t *testing.T) {
|
||||
t.Run("write", testWrite)
|
||||
vmstorage.Storage.DebugFlush()
|
||||
time.Sleep(1 * time.Second)
|
||||
vmstorage.Stop()
|
||||
// open storage after stop in write
|
||||
vmstorage.InitWithoutMetrics(promql.ResetRollupResultCacheIfNeeded)
|
||||
t.Run("read", testRead)
|
||||
}
|
||||
|
||||
|
||||
@@ -763,7 +763,8 @@ scrape_configs:
|
||||
|
||||
## Cardinality limiter
|
||||
|
||||
By default `vmagent` doesn't limit the number of time series each scrape target can expose. The limit can be enforced in the following places:
|
||||
By default `vmagent` doesn't limit the number of time series each scrape target can expose.
|
||||
The limit can be enforced in the following places:
|
||||
|
||||
* Via `-promscrape.seriesLimitPerTarget` command-line option. This limit is applied individually
|
||||
to all the scrape targets defined in the file pointed by `-promscrape.config`.
|
||||
@@ -774,10 +775,7 @@ By default `vmagent` doesn't limit the number of time series each scrape target
|
||||
via [Kubernetes annotations](https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/) for targets,
|
||||
which may expose too high number of time series.
|
||||
|
||||
See also `sample_limit` option at [scrape_config section](https://docs.victoriametrics.com/sd_configs.html#scrape_configs).
|
||||
|
||||
Scraped metrics are dropped for time series exceeding the given limit.
|
||||
|
||||
Scraped metrics are dropped for time series exceeding the given limit on the time window of 24h.
|
||||
`vmagent` creates the following additional per-target metrics for targets with non-zero series limit:
|
||||
|
||||
- `scrape_series_limit_samples_dropped` - the number of dropped samples during the scrape when the unique series limit is exceeded.
|
||||
@@ -791,6 +789,7 @@ These metrics allow building the following alerting rules:
|
||||
- `scrape_series_current / scrape_series_limit > 0.9` - alerts when the number of series exposed by the target reaches 90% of the limit.
|
||||
- `sum_over_time(scrape_series_limit_samples_dropped[1h]) > 0` - alerts when some samples are dropped because the series limit on a particular target is reached.
|
||||
|
||||
See also `sample_limit` option at [scrape_config section](https://docs.victoriametrics.com/sd_configs.html#scrape_configs).
|
||||
|
||||
By default `vmagent` doesn't limit the number of time series written to remote storage systems specified at `-remoteWrite.url`.
|
||||
The limit can be enforced by setting the following command-line flags:
|
||||
@@ -1162,7 +1161,9 @@ See the docs at https://docs.victoriametrics.com/vmagent.html .
|
||||
-fs.disableMmap
|
||||
Whether to use pread() instead of mmap() for reading data files. By default mmap() is used for 64-bit arches and pread() is used for 32-bit arches, since they cannot read data files bigger than 2^32 bytes in memory. mmap() is usually faster for reading small data chunks than pread()
|
||||
-graphiteListenAddr string
|
||||
TCP and UDP address to listen for Graphite plaintext data. Usually :2003 must be set. Doesn't work if empty
|
||||
TCP and UDP address to listen for Graphite plaintext data. Usually :2003 must be set. Doesn't work if empty. See also -graphiteListenAddr.useProxyProtocol
|
||||
-graphiteListenAddr.useProxyProtocol
|
||||
Whether to use proxy protocol for connections accepted at -graphiteListenAddr . See https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt
|
||||
-graphiteTrimTimestamp duration
|
||||
Trim timestamps for Graphite data to this duration. Minimum practical duration is 1s. Higher duration (i.e. 1m) may be used for reducing disk space usage for timestamp data (default 1s)
|
||||
-http.connTimeout duration
|
||||
@@ -1182,7 +1183,9 @@ See the docs at https://docs.victoriametrics.com/vmagent.html .
|
||||
-httpAuth.username string
|
||||
Username for HTTP Basic Auth. The authentication is disabled if empty. See also -httpAuth.password
|
||||
-httpListenAddr string
|
||||
TCP address to listen for http connections. Set this flag to empty value in order to disable listening on any port. This mode may be useful for running multiple vmagent instances on the same server. Note that /targets and /metrics pages aren't available if -httpListenAddr='' (default ":8429")
|
||||
TCP address to listen for http connections. Set this flag to empty value in order to disable listening on any port. This mode may be useful for running multiple vmagent instances on the same server. Note that /targets and /metrics pages aren't available if -httpListenAddr=''. See also -httpListenAddr.useProxyProtocol (default ":8429")
|
||||
-httpListenAddr.useProxyProtocol
|
||||
Whether to use proxy protocol for connections accepted at -httpListenAddr . See https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt
|
||||
-import.maxLineLen size
|
||||
The maximum length in bytes of a single line accepted by /api/v1/import; the line length can be limited with 'max_rows_per_line' query arg passed to /api/v1/export
|
||||
Supports the following optional suffixes for size values: KB, MB, GB, TB, KiB, MiB, GiB, TiB (default 104857600)
|
||||
@@ -1195,7 +1198,9 @@ See the docs at https://docs.victoriametrics.com/vmagent.html .
|
||||
-influxDBLabel string
|
||||
Default label for the DB name sent over '?db={db_name}' query parameter (default "db")
|
||||
-influxListenAddr string
|
||||
TCP and UDP address to listen for InfluxDB line protocol data. Usually :8089 must be set. Doesn't work if empty. This flag isn't needed when ingesting data over HTTP - just send it to http://<vmagent>:8429/write
|
||||
TCP and UDP address to listen for InfluxDB line protocol data. Usually :8089 must be set. Doesn't work if empty. This flag isn't needed when ingesting data over HTTP - just send it to http://<vmagent>:8429/write . See also -influxListenAddr.useProxyProtocol
|
||||
-influxListenAddr.useProxyProtocol
|
||||
Whether to use proxy protocol for connections accepted at -influxListenAddr . See https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt
|
||||
-influxMeasurementFieldSeparator string
|
||||
Separator for '{measurement}{separator}{field_name}' metric name when inserted via InfluxDB line protocol (default "_")
|
||||
-influxSkipMeasurement
|
||||
@@ -1205,7 +1210,9 @@ See the docs at https://docs.victoriametrics.com/vmagent.html .
|
||||
-influxTrimTimestamp duration
|
||||
Trim timestamps for InfluxDB line protocol data to this duration. Minimum practical duration is 1ms. Higher duration (i.e. 1s) may be used for reducing disk space usage for timestamp data (default 1ms)
|
||||
-insert.maxQueueDuration duration
|
||||
The maximum duration for waiting in the queue for insert requests due to -maxConcurrentInserts (default 1m0s)
|
||||
The maximum duration to wait in the queue when -maxConcurrentInserts concurrent insert requests are executed (default 1m0s)
|
||||
-internStringMaxLen int
|
||||
The maximum length for strings to intern. Lower limit may save memory at the cost of higher CPU usage. See https://en.wikipedia.org/wiki/String_interning (default 300)
|
||||
-kafka.consumer.topic array
|
||||
Kafka topic names for data consumption. This flag is available only in VictoriaMetrics enterprise. See https://docs.victoriametrics.com/enterprise.html
|
||||
Supports an array of values separated by comma or specified via multiple flags.
|
||||
@@ -1261,9 +1268,13 @@ See the docs at https://docs.victoriametrics.com/vmagent.html .
|
||||
-metricsAuthKey string
|
||||
Auth key for /metrics endpoint. It must be passed via authKey query arg. It overrides httpAuth.* settings
|
||||
-opentsdbHTTPListenAddr string
|
||||
TCP address to listen for OpentTSDB HTTP put requests. Usually :4242 must be set. Doesn't work if empty
|
||||
TCP address to listen for OpentTSDB HTTP put requests. Usually :4242 must be set. Doesn't work if empty. See also -opentsdbHTTPListenAddr.useProxyProtocol
|
||||
-opentsdbHTTPListenAddr.useProxyProtocol
|
||||
Whether to use proxy protocol for connections accepted at -opentsdbHTTPListenAddr . See https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt
|
||||
-opentsdbListenAddr string
|
||||
TCP and UDP address to listen for OpentTSDB metrics. Telnet put messages and HTTP /api/put messages are simultaneously served on TCP port. Usually :4242 must be set. Doesn't work if empty
|
||||
TCP and UDP address to listen for OpentTSDB metrics. Telnet put messages and HTTP /api/put messages are simultaneously served on TCP port. Usually :4242 must be set. Doesn't work if empty. See also -opentsdbListenAddr.useProxyProtocol
|
||||
-opentsdbListenAddr.useProxyProtocol
|
||||
Whether to use proxy protocol for connections accepted at -opentsdbListenAddr . See https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt
|
||||
-opentsdbTrimTimestamp duration
|
||||
Trim timestamps for OpenTSDB 'telnet put' data to this duration. Minimum practical duration is 1s. Higher duration (i.e. 1m) may be used for reducing disk space usage for timestamp data (default 1s)
|
||||
-opentsdbhttp.maxInsertRequestSize size
|
||||
@@ -1340,12 +1351,12 @@ See the docs at https://docs.victoriametrics.com/vmagent.html .
|
||||
-promscrape.minResponseSizeForStreamParse size
|
||||
The minimum target response size for automatic switching to stream parsing mode, which can reduce memory usage. See https://docs.victoriametrics.com/vmagent.html#stream-parsing-mode
|
||||
Supports the following optional suffixes for size values: KB, MB, GB, TB, KiB, MiB, GiB, TiB (default 1000000)
|
||||
-promscrape.noStaleMarkers
|
||||
Whether to disable sending Prometheus stale markers for metrics when scrape target disappears. This option may reduce memory usage if stale markers aren't needed for your setup. This option also disables populating the scrape_series_added metric. See https://prometheus.io/docs/concepts/jobs_instances/#automatically-generated-labels-and-time-series
|
||||
-promscrape.nomad.waitTime duration
|
||||
Wait time used by Nomad service discovery. Default value is used if not set
|
||||
-promscrape.nomadSDCheckInterval duration
|
||||
Interval for checking for changes in Nomad. This works only if nomad_sd_configs is configured in '-promscrape.config' file. See https://docs.victoriametrics.com/sd_configs.html#nomad_sd_configs for details (default 30s)
|
||||
-promscrape.noStaleMarkers
|
||||
Whether to disable sending Prometheus stale markers for metrics when scrape target disappears. This option may reduce memory usage if stale markers aren't needed for your setup. This option also disables populating the scrape_series_added metric. See https://prometheus.io/docs/concepts/jobs_instances/#automatically-generated-labels-and-time-series
|
||||
-promscrape.openstackSDCheckInterval duration
|
||||
Interval for checking for changes in openstack API server. This works only if openstack_sd_configs is configured in '-promscrape.config' file. See https://docs.victoriametrics.com/sd_configs.html#openstack_sd_configs for details (default 30s)
|
||||
-promscrape.seriesLimitPerTarget int
|
||||
@@ -1468,8 +1479,11 @@ See the docs at https://docs.victoriametrics.com/vmagent.html .
|
||||
The number of significant figures to leave in metric values before writing them to remote storage. See https://en.wikipedia.org/wiki/Significant_figures . Zero value saves all the significant figures. This option may be used for improving data compression for the stored metrics. See also -remoteWrite.roundDigits
|
||||
Supports array of values separated by comma or specified via multiple flags.
|
||||
-remoteWrite.streamAggr.config array
|
||||
Optional path to file with stream aggregation config. See https://docs.victoriametrics.com/stream-aggregation.html . See also -remoteWrite.streamAggr.keepInput
|
||||
Optional path to file with stream aggregation config. See https://docs.victoriametrics.com/stream-aggregation.html . See also -remoteWrite.streamAggr.keepInput and -remoteWrite.streamAggr.dedupInterval
|
||||
Supports an array of values separated by comma or specified via multiple flags.
|
||||
-remoteWrite.streamAggr.dedupInterval array
|
||||
Input samples are de-duplicated with this interval before being aggregated. Only the last sample per each time series per each interval is aggregated if the interval is greater than zero
|
||||
Supports array of values separated by comma or specified via multiple flags.
|
||||
-remoteWrite.streamAggr.keepInput array
|
||||
Whether to keep input samples after the aggregation with -remoteWrite.streamAggr.config. By default the input is dropped after the aggregation, so only the aggregate data is sent to the -remoteWrite.url. See https://docs.victoriametrics.com/stream-aggregation.html
|
||||
Supports array of values separated by comma or specified via multiple flags.
|
||||
|
||||
@@ -44,16 +44,29 @@ import (
|
||||
var (
|
||||
httpListenAddr = flag.String("httpListenAddr", ":8429", "TCP address to listen for http connections. "+
|
||||
"Set this flag to empty value in order to disable listening on any port. This mode may be useful for running multiple vmagent instances on the same server. "+
|
||||
"Note that /targets and /metrics pages aren't available if -httpListenAddr=''")
|
||||
"Note that /targets and /metrics pages aren't available if -httpListenAddr=''. See also -httpListenAddr.useProxyProtocol")
|
||||
useProxyProtocol = flag.Bool("httpListenAddr.useProxyProtocol", false, "Whether to use proxy protocol for connections accepted at -httpListenAddr . "+
|
||||
"See https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt")
|
||||
influxListenAddr = flag.String("influxListenAddr", "", "TCP and UDP address to listen for InfluxDB line protocol data. Usually :8089 must be set. Doesn't work if empty. "+
|
||||
"This flag isn't needed when ingesting data over HTTP - just send it to http://<vmagent>:8429/write")
|
||||
graphiteListenAddr = flag.String("graphiteListenAddr", "", "TCP and UDP address to listen for Graphite plaintext data. Usually :2003 must be set. Doesn't work if empty")
|
||||
"This flag isn't needed when ingesting data over HTTP - just send it to http://<vmagent>:8429/write . "+
|
||||
"See also -influxListenAddr.useProxyProtocol")
|
||||
influxUseProxyProtocol = flag.Bool("influxListenAddr.useProxyProtocol", false, "Whether to use proxy protocol for connections accepted at -influxListenAddr . "+
|
||||
"See https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt")
|
||||
graphiteListenAddr = flag.String("graphiteListenAddr", "", "TCP and UDP address to listen for Graphite plaintext data. Usually :2003 must be set. Doesn't work if empty. "+
|
||||
"See also -graphiteListenAddr.useProxyProtocol")
|
||||
graphiteUseProxyProtocol = flag.Bool("graphiteListenAddr.useProxyProtocol", false, "Whether to use proxy protocol for connections accepted at -graphiteListenAddr . "+
|
||||
"See https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt")
|
||||
opentsdbListenAddr = flag.String("opentsdbListenAddr", "", "TCP and UDP address to listen for OpentTSDB metrics. "+
|
||||
"Telnet put messages and HTTP /api/put messages are simultaneously served on TCP port. "+
|
||||
"Usually :4242 must be set. Doesn't work if empty")
|
||||
opentsdbHTTPListenAddr = flag.String("opentsdbHTTPListenAddr", "", "TCP address to listen for OpentTSDB HTTP put requests. Usually :4242 must be set. Doesn't work if empty")
|
||||
configAuthKey = flag.String("configAuthKey", "", "Authorization key for accessing /config page. It must be passed via authKey query arg")
|
||||
dryRun = flag.Bool("dryRun", false, "Whether to check only config files without running vmagent. The following files are checked: "+
|
||||
"Usually :4242 must be set. Doesn't work if empty. See also -opentsdbListenAddr.useProxyProtocol")
|
||||
opentsdbUseProxyProtocol = flag.Bool("opentsdbListenAddr.useProxyProtocol", false, "Whether to use proxy protocol for connections accepted at -opentsdbListenAddr . "+
|
||||
"See https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt")
|
||||
opentsdbHTTPListenAddr = flag.String("opentsdbHTTPListenAddr", "", "TCP address to listen for OpentTSDB HTTP put requests. Usually :4242 must be set. Doesn't work if empty. "+
|
||||
"See also -opentsdbHTTPListenAddr.useProxyProtocol")
|
||||
opentsdbHTTPUseProxyProtocol = flag.Bool("opentsdbHTTPListenAddr.useProxyProtocol", false, "Whether to use proxy protocol for connections accepted "+
|
||||
"at -opentsdbHTTPListenAddr . See https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt")
|
||||
configAuthKey = flag.String("configAuthKey", "", "Authorization key for accessing /config page. It must be passed via authKey query arg")
|
||||
dryRun = flag.Bool("dryRun", false, "Whether to check only config files without running vmagent. The following files are checked: "+
|
||||
"-promscrape.config, -remoteWrite.relabelConfig, -remoteWrite.urlRelabelConfig . "+
|
||||
"Unknown config entries aren't allowed in -promscrape.config by default. This can be changed by passing -promscrape.config.strictParse=false command-line flag")
|
||||
)
|
||||
@@ -104,26 +117,26 @@ func main() {
|
||||
remotewrite.Init()
|
||||
common.StartUnmarshalWorkers()
|
||||
if len(*influxListenAddr) > 0 {
|
||||
influxServer = influxserver.MustStart(*influxListenAddr, func(r io.Reader) error {
|
||||
influxServer = influxserver.MustStart(*influxListenAddr, *influxUseProxyProtocol, func(r io.Reader) error {
|
||||
return influx.InsertHandlerForReader(r, false)
|
||||
})
|
||||
}
|
||||
if len(*graphiteListenAddr) > 0 {
|
||||
graphiteServer = graphiteserver.MustStart(*graphiteListenAddr, graphite.InsertHandler)
|
||||
graphiteServer = graphiteserver.MustStart(*graphiteListenAddr, *graphiteUseProxyProtocol, graphite.InsertHandler)
|
||||
}
|
||||
if len(*opentsdbListenAddr) > 0 {
|
||||
httpInsertHandler := getOpenTSDBHTTPInsertHandler()
|
||||
opentsdbServer = opentsdbserver.MustStart(*opentsdbListenAddr, opentsdb.InsertHandler, httpInsertHandler)
|
||||
opentsdbServer = opentsdbserver.MustStart(*opentsdbListenAddr, *opentsdbUseProxyProtocol, opentsdb.InsertHandler, httpInsertHandler)
|
||||
}
|
||||
if len(*opentsdbHTTPListenAddr) > 0 {
|
||||
httpInsertHandler := getOpenTSDBHTTPInsertHandler()
|
||||
opentsdbhttpServer = opentsdbhttpserver.MustStart(*opentsdbHTTPListenAddr, httpInsertHandler)
|
||||
opentsdbhttpServer = opentsdbhttpserver.MustStart(*opentsdbHTTPListenAddr, *opentsdbHTTPUseProxyProtocol, httpInsertHandler)
|
||||
}
|
||||
|
||||
promscrape.Init(remotewrite.Push)
|
||||
|
||||
if len(*httpListenAddr) > 0 {
|
||||
go httpserver.Serve(*httpListenAddr, requestHandler)
|
||||
go httpserver.Serve(*httpListenAddr, *useProxyProtocol, requestHandler)
|
||||
}
|
||||
logger.Infof("started vmagent in %.3f seconds", time.Since(startTime).Seconds())
|
||||
|
||||
@@ -223,7 +236,14 @@ func requestHandler(w http.ResponseWriter, r *http.Request) bool {
|
||||
httpserver.Errorf(w, r, "%s", err)
|
||||
return true
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
statusCode := http.StatusNoContent
|
||||
if strings.HasPrefix(path, "/prometheus/api/v1/import/prometheus/metrics/job/") ||
|
||||
strings.HasPrefix(path, "/api/v1/import/prometheus/metrics/job/") {
|
||||
// Return 200 status code for pushgateway requests.
|
||||
// See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/3636
|
||||
statusCode = http.StatusOK
|
||||
}
|
||||
w.WriteHeader(statusCode)
|
||||
return true
|
||||
}
|
||||
if strings.HasPrefix(path, "datadog/") {
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
package prometheusimport
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmagent/common"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmagent/remotewrite"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/auth"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/httpserver"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/prompbmarshal"
|
||||
parserCommon "github.com/VictoriaMetrics/VictoriaMetrics/lib/protoparser/common"
|
||||
parser "github.com/VictoriaMetrics/VictoriaMetrics/lib/protoparser/prometheus"
|
||||
@@ -33,14 +33,9 @@ func InsertHandler(at *auth.Token, req *http.Request) error {
|
||||
isGzipped := req.Header.Get("Content-Encoding") == "gzip"
|
||||
return parser.ParseStream(req.Body, defaultTimestamp, isGzipped, func(rows []parser.Row) error {
|
||||
return insertRows(at, rows, extraLabels)
|
||||
}, nil)
|
||||
}
|
||||
|
||||
// InsertHandlerForReader processes metrics from given reader with optional gzip format
|
||||
func InsertHandlerForReader(r io.Reader, isGzipped bool) error {
|
||||
return parser.ParseStream(r, 0, isGzipped, func(rows []parser.Row) error {
|
||||
return insertRows(nil, rows, nil)
|
||||
}, nil)
|
||||
}, func(s string) {
|
||||
httpserver.LogError(req, s)
|
||||
})
|
||||
}
|
||||
|
||||
func insertRows(at *auth.Token, rows []parser.Row, extraLabels []prompbmarshal.Label) error {
|
||||
|
||||
60
app/vmagent/prometheusimport/request_handler_test.go
Normal file
60
app/vmagent/prometheusimport/request_handler_test.go
Normal file
@@ -0,0 +1,60 @@
|
||||
package prometheusimport
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"flag"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmagent/remotewrite"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/fs"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/protoparser/common"
|
||||
)
|
||||
|
||||
var (
|
||||
srv *httptest.Server
|
||||
testOutput *bytes.Buffer
|
||||
)
|
||||
|
||||
func TestInsertHandler(t *testing.T) {
|
||||
setUp()
|
||||
defer tearDown()
|
||||
req := httptest.NewRequest("POST", "/insert/0/api/v1/import/prometheus", bytes.NewBufferString(`{"foo":"bar"}
|
||||
go_memstats_alloc_bytes_total 1`))
|
||||
if err := InsertHandler(nil, req); err != nil {
|
||||
t.Errorf("unxepected error %s", err)
|
||||
}
|
||||
expectedMsg := "cannot unmarshal Prometheus line"
|
||||
if !strings.Contains(testOutput.String(), expectedMsg) {
|
||||
t.Errorf("output %q should contain %q", testOutput.String(), expectedMsg)
|
||||
}
|
||||
}
|
||||
|
||||
func setUp() {
|
||||
srv = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.WriteHeader(204)
|
||||
}))
|
||||
flag.Parse()
|
||||
remoteWriteFlag := "remoteWrite.url"
|
||||
if err := flag.Lookup(remoteWriteFlag).Value.Set(srv.URL); err != nil {
|
||||
log.Fatalf("unable to set %q with value %q, err: %v", remoteWriteFlag, srv.URL, err)
|
||||
}
|
||||
logger.Init()
|
||||
common.StartUnmarshalWorkers()
|
||||
remotewrite.Init()
|
||||
testOutput = &bytes.Buffer{}
|
||||
logger.SetOutputForTests(testOutput)
|
||||
}
|
||||
|
||||
func tearDown() {
|
||||
common.StopUnmarshalWorkers()
|
||||
srv.Close()
|
||||
logger.ResetOutputForTest()
|
||||
tmpDataDir := flag.Lookup("remoteWrite.tmpDataPath").Value.String()
|
||||
fs.MustRemoveAll(tmpDataDir)
|
||||
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
package promremotewrite
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmagent/common"
|
||||
@@ -33,13 +32,6 @@ func InsertHandler(at *auth.Token, req *http.Request) error {
|
||||
})
|
||||
}
|
||||
|
||||
// InsertHandlerForReader processes metrics from given reader
|
||||
func InsertHandlerForReader(at *auth.Token, r io.Reader) error {
|
||||
return parser.ParseStream(r, func(tss []prompb.TimeSeries) error {
|
||||
return insertRows(at, tss, nil)
|
||||
})
|
||||
}
|
||||
|
||||
func insertRows(at *auth.Token, timeseries []prompb.TimeSeries, extraLabels []prompbmarshal.Label) error {
|
||||
ctx := common.GetPushCtx()
|
||||
defer common.PutPushCtx(ctx)
|
||||
|
||||
@@ -62,10 +62,12 @@ var (
|
||||
|
||||
streamAggrConfig = flagutil.NewArrayString("remoteWrite.streamAggr.config", "Optional path to file with stream aggregation config. "+
|
||||
"See https://docs.victoriametrics.com/stream-aggregation.html . "+
|
||||
"See also -remoteWrite.streamAggr.keepInput")
|
||||
"See also -remoteWrite.streamAggr.keepInput and -remoteWrite.streamAggr.dedupInterval")
|
||||
streamAggrKeepInput = flagutil.NewArrayBool("remoteWrite.streamAggr.keepInput", "Whether to keep input samples after the aggregation with -remoteWrite.streamAggr.config. "+
|
||||
"By default the input is dropped after the aggregation, so only the aggregate data is sent to the -remoteWrite.url. "+
|
||||
"See https://docs.victoriametrics.com/stream-aggregation.html")
|
||||
streamAggrDedupInterval = flagutil.NewArrayDuration("remoteWrite.streamAggr.dedupInterval", "Input samples are de-duplicated with this interval before being aggregated. "+
|
||||
"Only the last sample per each time series per each interval is aggregated if the interval is greater than zero")
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -509,7 +511,8 @@ func newRemoteWriteCtx(argIdx int, at *auth.Token, remoteWriteURL *url.URL, maxI
|
||||
// Initialize sas
|
||||
sasFile := streamAggrConfig.GetOptionalArg(argIdx)
|
||||
if sasFile != "" {
|
||||
sas, err := streamaggr.LoadFromFile(sasFile, rwctx.pushInternal)
|
||||
dedupInterval := streamAggrDedupInterval.GetOptionalArgOrDefault(argIdx, 0)
|
||||
sas, err := streamaggr.LoadFromFile(sasFile, rwctx.pushInternal, dedupInterval)
|
||||
if err != nil {
|
||||
logger.Fatalf("cannot initialize stream aggregators from -remoteWrite.streamAggrFile=%q: %s", sasFile, err)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package vmimport
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmagent/common"
|
||||
@@ -36,13 +35,6 @@ func InsertHandler(at *auth.Token, req *http.Request) error {
|
||||
})
|
||||
}
|
||||
|
||||
// InsertHandlerForReader processes metrics from given reader
|
||||
func InsertHandlerForReader(r io.Reader, isGzipped bool) error {
|
||||
return parser.ParseStream(r, isGzipped, func(rows []parser.Row) error {
|
||||
return insertRows(nil, rows, nil)
|
||||
})
|
||||
}
|
||||
|
||||
func insertRows(at *auth.Token, rows []parser.Row, extraLabels []prompbmarshal.Label) error {
|
||||
ctx := common.GetPushCtx()
|
||||
defer common.PutPushCtx(ctx)
|
||||
|
||||
@@ -9,6 +9,13 @@ protocol and require `-remoteWrite.url` to be configured.
|
||||
Vmalert is heavily inspired by [Prometheus](https://prometheus.io/docs/alerting/latest/overview/)
|
||||
implementation and aims to be compatible with its syntax.
|
||||
|
||||
A [single-node](https://docs.victoriametrics.com/Single-server-VictoriaMetrics.html#vmalert)
|
||||
or [cluster version](https://docs.victoriametrics.com/Cluster-VictoriaMetrics.html#vmalert)
|
||||
of VictoriaMetrics are capable of proxying requests to vmalert via `-vmalert.proxyURL` command-line flag.
|
||||
Use this feature for the following cases:
|
||||
* for proxying requests from [Grafana Alerting UI](https://grafana.com/docs/grafana/latest/alerting/);
|
||||
* for accessing vmalert's UI through VictoriaMetrics Web interface.
|
||||
|
||||
## Features
|
||||
|
||||
* Integration with [VictoriaMetrics](https://github.com/VictoriaMetrics/VictoriaMetrics) TSDB;
|
||||
@@ -897,7 +904,13 @@ The shortlist of configuration flags is the following:
|
||||
-httpAuth.username string
|
||||
Username for HTTP Basic Auth. The authentication is disabled if empty. See also -httpAuth.password
|
||||
-httpListenAddr string
|
||||
Address to listen for http connections (default ":8880")
|
||||
Address to listen for http connections. See also -httpListenAddr.useProxyProtocol (default ":8880")
|
||||
-httpListenAddr.useProxyProtocol
|
||||
Whether to use proxy protocol for connections accepted at -httpListenAddr . See https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt
|
||||
-insert.maxQueueDuration duration
|
||||
The maximum duration to wait in the queue when -maxConcurrentInserts concurrent insert requests are executed (default 1m0s)
|
||||
-internStringMaxLen int
|
||||
The maximum length for strings to intern. Lower limit may save memory at the cost of higher CPU usage. See https://en.wikipedia.org/wiki/String_interning (default 300)
|
||||
-loggerDisableTimestamps
|
||||
Whether to disable writing timestamps in logs
|
||||
-loggerErrorsPerSecondLimit int
|
||||
@@ -914,6 +927,8 @@ The shortlist of configuration flags is the following:
|
||||
Timezone to use for timestamps in logs. Timezone must be a valid IANA Time Zone. For example: America/New_York, Europe/Berlin, Etc/GMT+3 or Local (default "UTC")
|
||||
-loggerWarnsPerSecondLimit int
|
||||
Per-second limit on the number of WARN messages. If more than the given number of warns are emitted per second, then the remaining warns are suppressed. Zero values disable the rate limit
|
||||
-maxConcurrentInserts int
|
||||
The maximum number of concurrent insert requests. Default value should work for most cases, since it minimizes the memory usage. The default value can be increased when clients send data over slow networks. See also -insert.maxQueueDuration (default 8)
|
||||
-memory.allowedBytes size
|
||||
Allowed size of system memory VictoriaMetrics caches may occupy. This option overrides -memory.allowedPercent if set to a non-zero value. Too low a value may increase the cache miss rate usually resulting in higher CPU and disk IO usage. Too high a value may evict too much data from OS page cache resulting in higher disk IO usage
|
||||
Supports the following optional suffixes for size values: KB, MB, GB, TB, KiB, MiB, GiB, TiB (default 0)
|
||||
|
||||
@@ -49,7 +49,9 @@ absolute path to all .tpl files in root.`)
|
||||
configCheckInterval = flag.Duration("configCheckInterval", 0, "Interval for checking for changes in '-rule' or '-notifier.config' files. "+
|
||||
"By default the checking is disabled. Send SIGHUP signal in order to force config check for changes.")
|
||||
|
||||
httpListenAddr = flag.String("httpListenAddr", ":8880", "Address to listen for http connections")
|
||||
httpListenAddr = flag.String("httpListenAddr", ":8880", "Address to listen for http connections. See also -httpListenAddr.useProxyProtocol")
|
||||
useProxyProtocol = flag.Bool("httpListenAddr.useProxyProtocol", false, "Whether to use proxy protocol for connections accepted at -httpListenAddr . "+
|
||||
"See https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt")
|
||||
evaluationInterval = flag.Duration("evaluationInterval", time.Minute, "How often to evaluate the rules")
|
||||
|
||||
validateTemplates = flag.Bool("rule.validateTemplates", true, "Whether to validate annotation and label templates")
|
||||
@@ -170,7 +172,7 @@ func main() {
|
||||
go configReload(ctx, manager, groupsCfg, sighupCh)
|
||||
|
||||
rh := &requestHandler{m: manager}
|
||||
go httpserver.Serve(*httpListenAddr, rh.handler)
|
||||
go httpserver.Serve(*httpListenAddr, *useProxyProtocol, rh.handler)
|
||||
|
||||
sig := procutil.WaitForSigterm()
|
||||
logger.Infof("service received signal %s", sig)
|
||||
|
||||
@@ -64,17 +64,18 @@ func TestManagerUpdateConcurrent(t *testing.T) {
|
||||
wg := sync.WaitGroup{}
|
||||
wg.Add(workers)
|
||||
for i := 0; i < workers; i++ {
|
||||
go func() {
|
||||
go func(n int) {
|
||||
defer wg.Done()
|
||||
r := rand.New(rand.NewSource(int64(n)))
|
||||
for i := 0; i < iterations; i++ {
|
||||
rnd := rand.Intn(len(paths))
|
||||
rnd := r.Intn(len(paths))
|
||||
cfg, err := config.Parse([]string{paths[rnd]}, notifier.ValidateTemplates, true)
|
||||
if err != nil { // update can fail and this is expected
|
||||
continue
|
||||
}
|
||||
_ = m.update(context.Background(), cfg, false)
|
||||
}
|
||||
}()
|
||||
}(i)
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
@@ -162,14 +162,15 @@ consul_sd_configs:
|
||||
wg := sync.WaitGroup{}
|
||||
wg.Add(workers)
|
||||
for i := 0; i < workers; i++ {
|
||||
go func() {
|
||||
go func(n int) {
|
||||
defer wg.Done()
|
||||
r := rand.New(rand.NewSource(int64(n)))
|
||||
for i := 0; i < iterations; i++ {
|
||||
rnd := rand.Intn(len(paths))
|
||||
rnd := r.Intn(len(paths))
|
||||
_ = cw.reload(paths[rnd]) // update can fail and this is expected
|
||||
_ = cw.notifiers()
|
||||
}
|
||||
}()
|
||||
}(i)
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
@@ -27,12 +27,13 @@ func TestClient_Push(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create client: %s", err)
|
||||
}
|
||||
r := rand.New(rand.NewSource(1))
|
||||
const rowsN = 1e4
|
||||
var sent int
|
||||
for i := 0; i < rowsN; i++ {
|
||||
s := prompbmarshal.TimeSeries{
|
||||
Samples: []prompbmarshal.Sample{{
|
||||
Value: rand.Float64(),
|
||||
Value: r.Float64(),
|
||||
Timestamp: time.Now().Unix(),
|
||||
}},
|
||||
}
|
||||
|
||||
@@ -261,7 +261,11 @@ See the docs at https://docs.victoriametrics.com/vmauth.html .
|
||||
-httpAuth.username string
|
||||
Username for HTTP Basic Auth. The authentication is disabled if empty. See also -httpAuth.password
|
||||
-httpListenAddr string
|
||||
TCP address to listen for http connections (default ":8427")
|
||||
TCP address to listen for http connections. See also -httpListenAddr.useProxyProtocol (default ":8427")
|
||||
-httpListenAddr.useProxyProtocol
|
||||
Whether to use proxy protocol for connections accepted at -httpListenAddr . See https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt
|
||||
-internStringMaxLen int
|
||||
The maximum length for strings to intern. Lower limit may save memory at the cost of higher CPU usage. See https://en.wikipedia.org/wiki/String_interning (default 300)
|
||||
-logInvalidAuthTokens
|
||||
Whether to log requests with invalid auth tokens. Such requests are always counted at vmauth_http_request_errors_total{reason="invalid_auth_token"} metric, which is exposed at /metrics page
|
||||
-loggerDisableTimestamps
|
||||
@@ -280,8 +284,10 @@ See the docs at https://docs.victoriametrics.com/vmauth.html .
|
||||
Timezone to use for timestamps in logs. Timezone must be a valid IANA Time Zone. For example: America/New_York, Europe/Berlin, Etc/GMT+3 or Local (default "UTC")
|
||||
-loggerWarnsPerSecondLimit int
|
||||
Per-second limit on the number of WARN messages. If more than the given number of warns are emitted per second, then the remaining warns are suppressed. Zero values disable the rate limit
|
||||
-maxConcurrentRequests int
|
||||
The maximum number of concurrent requests vmauth can process. Other requests are rejected with '429 Too Many Requests' http status code. See also -maxIdleConnsPerBackend (default 1000)
|
||||
-maxIdleConnsPerBackend int
|
||||
The maximum number of idle connections vmauth can open per each backend host (default 100)
|
||||
The maximum number of idle connections vmauth can open per each backend host. See also -maxConcurrentRequests (default 100)
|
||||
-memory.allowedBytes size
|
||||
Allowed size of system memory VictoriaMetrics caches may occupy. This option overrides -memory.allowedPercent if set to a non-zero value. Too low a value may increase the cache miss rate usually resulting in higher CPU and disk IO usage. Too high a value may evict too much data from OS page cache resulting in higher disk IO usage
|
||||
Supports the following optional suffixes for size values: KB, MB, GB, TB, KiB, MiB, GiB, TiB (default 0)
|
||||
@@ -301,6 +307,8 @@ See the docs at https://docs.victoriametrics.com/vmauth.html .
|
||||
Supports an array of values separated by comma or specified via multiple flags.
|
||||
-reloadAuthKey string
|
||||
Auth key for /-/reload http endpoint. It must be passed as authKey=...
|
||||
-responseTimeout duration
|
||||
The timeout for receiving a response from backend (default 5m0s)
|
||||
-tls
|
||||
Whether to enable TLS for incoming HTTP requests at -httpListenAddr (aka https). -tlsCertFile and -tlsKeyFile must be set if -tls is set
|
||||
-tlsCertFile string
|
||||
|
||||
@@ -37,7 +37,7 @@ type UserInfo struct {
|
||||
Username string `yaml:"username,omitempty"`
|
||||
Password string `yaml:"password,omitempty"`
|
||||
URLPrefix *URLPrefix `yaml:"url_prefix,omitempty"`
|
||||
URLMap []URLMap `yaml:"url_map,omitempty"`
|
||||
URLMaps []URLMap `yaml:"url_map,omitempty"`
|
||||
Headers []Header `yaml:"headers,omitempty"`
|
||||
|
||||
requests *metrics.Counter
|
||||
@@ -284,7 +284,7 @@ func parseAuthConfig(data []byte) (map[string]*UserInfo, error) {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
for _, e := range ui.URLMap {
|
||||
for _, e := range ui.URLMaps {
|
||||
if len(e.SrcPaths) == 0 {
|
||||
return nil, fmt.Errorf("missing `src_paths` in `url_map`")
|
||||
}
|
||||
@@ -295,7 +295,7 @@ func parseAuthConfig(data []byte) (map[string]*UserInfo, error) {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if len(ui.URLMap) == 0 && ui.URLPrefix == nil {
|
||||
if len(ui.URLMaps) == 0 && ui.URLPrefix == nil {
|
||||
return nil, fmt.Errorf("missing `url_prefix`")
|
||||
}
|
||||
if ui.BearerToken != "" {
|
||||
|
||||
@@ -278,7 +278,7 @@ users:
|
||||
`, map[string]*UserInfo{
|
||||
getAuthToken("foo", "", ""): {
|
||||
BearerToken: "foo",
|
||||
URLMap: []URLMap{
|
||||
URLMaps: []URLMap{
|
||||
{
|
||||
SrcPaths: getSrcPaths([]string{"/api/v1/query", "/api/v1/query_range", "/api/v1/label/[^./]+/.+"}),
|
||||
URLPrefix: mustParseURL("http://vmselect/select/0/prometheus"),
|
||||
@@ -304,7 +304,7 @@ users:
|
||||
},
|
||||
getAuthToken("", "foo", ""): {
|
||||
BearerToken: "foo",
|
||||
URLMap: []URLMap{
|
||||
URLMaps: []URLMap{
|
||||
{
|
||||
SrcPaths: getSrcPaths([]string{"/api/v1/query", "/api/v1/query_range", "/api/v1/label/[^./]+/.+"}),
|
||||
URLPrefix: mustParseURL("http://vmselect/select/0/prometheus"),
|
||||
|
||||
@@ -3,8 +3,10 @@ package main
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/textproto"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
@@ -12,20 +14,28 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/buildinfo"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/bytesutil"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/envflag"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/flagutil"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/httpserver"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/netutil"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/procutil"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/pushmetrics"
|
||||
"github.com/VictoriaMetrics/metrics"
|
||||
)
|
||||
|
||||
var (
|
||||
httpListenAddr = flag.String("httpListenAddr", ":8427", "TCP address to listen for http connections")
|
||||
maxIdleConnsPerBackend = flag.Int("maxIdleConnsPerBackend", 100, "The maximum number of idle connections vmauth can open per each backend host")
|
||||
reloadAuthKey = flag.String("reloadAuthKey", "", "Auth key for /-/reload http endpoint. It must be passed as authKey=...")
|
||||
logInvalidAuthTokens = flag.Bool("logInvalidAuthTokens", false, "Whether to log requests with invalid auth tokens. "+
|
||||
httpListenAddr = flag.String("httpListenAddr", ":8427", "TCP address to listen for http connections. See also -httpListenAddr.useProxyProtocol")
|
||||
useProxyProtocol = flag.Bool("httpListenAddr.useProxyProtocol", false, "Whether to use proxy protocol for connections accepted at -httpListenAddr . "+
|
||||
"See https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt")
|
||||
maxIdleConnsPerBackend = flag.Int("maxIdleConnsPerBackend", 100, "The maximum number of idle connections vmauth can open per each backend host. "+
|
||||
"See also -maxConcurrentRequests")
|
||||
responseTimeout = flag.Duration("responseTimeout", 5*time.Minute, "The timeout for receiving a response from backend")
|
||||
maxConcurrentRequests = flag.Int("maxConcurrentRequests", 1000, "The maximum number of concurrent requests vmauth can process. Other requests are rejected with "+
|
||||
"'429 Too Many Requests' http status code. See also -maxIdleConnsPerBackend")
|
||||
reloadAuthKey = flag.String("reloadAuthKey", "", "Auth key for /-/reload http endpoint. It must be passed as authKey=...")
|
||||
logInvalidAuthTokens = flag.Bool("logInvalidAuthTokens", false, "Whether to log requests with invalid auth tokens. "+
|
||||
`Such requests are always counted at vmauth_http_request_errors_total{reason="invalid_auth_token"} metric, which is exposed at /metrics page`)
|
||||
)
|
||||
|
||||
@@ -41,7 +51,7 @@ func main() {
|
||||
logger.Infof("starting vmauth at %q...", *httpListenAddr)
|
||||
startTime := time.Now()
|
||||
initAuthConfig()
|
||||
go httpserver.Serve(*httpListenAddr, requestHandler)
|
||||
go httpserver.Serve(*httpListenAddr, *useProxyProtocol, requestHandler)
|
||||
logger.Infof("started vmauth in %.3f seconds", time.Since(startTime).Seconds())
|
||||
|
||||
sig := procutil.WaitForSigterm()
|
||||
@@ -79,15 +89,20 @@ func requestHandler(w http.ResponseWriter, r *http.Request) bool {
|
||||
// See https://docs.influxdata.com/influxdb/v2.0/api/
|
||||
authToken = strings.Replace(authToken, "Token", "Bearer", 1)
|
||||
}
|
||||
|
||||
ac := authConfig.Load().(map[string]*UserInfo)
|
||||
ui := ac[authToken]
|
||||
if ui == nil {
|
||||
invalidAuthTokenRequests.Inc()
|
||||
err := fmt.Errorf("cannot find the provided auth token %q in config", authToken)
|
||||
if *logInvalidAuthTokens {
|
||||
httpserver.Errorf(w, r, "cannot find the provided auth token %q in config", authToken)
|
||||
err = &httpserver.ErrorWithStatusCode{
|
||||
Err: err,
|
||||
StatusCode: http.StatusUnauthorized,
|
||||
}
|
||||
httpserver.Errorf(w, r, "%s", err)
|
||||
} else {
|
||||
errStr := fmt.Sprintf("cannot find the provided auth token %q in config", authToken)
|
||||
http.Error(w, errStr, http.StatusBadRequest)
|
||||
http.Error(w, err.Error(), http.StatusUnauthorized)
|
||||
}
|
||||
return true
|
||||
}
|
||||
@@ -97,26 +112,121 @@ func requestHandler(w http.ResponseWriter, r *http.Request) bool {
|
||||
httpserver.Errorf(w, r, "cannot determine targetURL: %s", err)
|
||||
return true
|
||||
}
|
||||
r.Header.Set("vm-target-url", targetURL.String())
|
||||
for _, h := range headers {
|
||||
r.Header.Set(h.Name, h.Value)
|
||||
|
||||
// Limit the concurrency of requests to backends
|
||||
concurrencyLimitOnce.Do(concurrencyLimitInit)
|
||||
select {
|
||||
case concurrencyLimitCh <- struct{}{}:
|
||||
default:
|
||||
concurrentRequestsLimitReachedTotal.Inc()
|
||||
w.Header().Add("Retry-After", "10")
|
||||
err := &httpserver.ErrorWithStatusCode{
|
||||
Err: fmt.Errorf("cannot serve more than -maxConcurrentRequests=%d concurrent requests", cap(concurrencyLimitCh)),
|
||||
StatusCode: http.StatusTooManyRequests,
|
||||
}
|
||||
httpserver.Errorf(w, r, "%s", err)
|
||||
return true
|
||||
}
|
||||
proxyRequest(w, r)
|
||||
processRequest(w, r, targetURL, headers)
|
||||
<-concurrencyLimitCh
|
||||
return true
|
||||
}
|
||||
|
||||
func proxyRequest(w http.ResponseWriter, r *http.Request) {
|
||||
defer func() {
|
||||
err := recover()
|
||||
if err == nil || err == http.ErrAbortHandler {
|
||||
// Suppress http.ErrAbortHandler panic.
|
||||
// See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/1353
|
||||
return
|
||||
func processRequest(w http.ResponseWriter, r *http.Request, targetURL *url.URL, headers []Header) {
|
||||
// This code has been copied from net/http/httputil/reverseproxy.go
|
||||
req := sanitizeRequestHeaders(r)
|
||||
req.URL = targetURL
|
||||
for _, h := range headers {
|
||||
req.Header.Set(h.Name, h.Value)
|
||||
}
|
||||
transportOnce.Do(transportInit)
|
||||
res, err := transport.RoundTrip(req)
|
||||
if err != nil {
|
||||
err = &httpserver.ErrorWithStatusCode{
|
||||
Err: fmt.Errorf("error when proxying the request to %q: %s", targetURL, err),
|
||||
StatusCode: http.StatusBadGateway,
|
||||
}
|
||||
// Forward other panics to the caller.
|
||||
panic(err)
|
||||
}()
|
||||
getReverseProxy().ServeHTTP(w, r)
|
||||
httpserver.Errorf(w, r, "%s", err)
|
||||
return
|
||||
}
|
||||
removeHopHeaders(res.Header)
|
||||
copyHeader(w.Header(), res.Header)
|
||||
w.WriteHeader(res.StatusCode)
|
||||
|
||||
copyBuf := copyBufPool.Get()
|
||||
copyBuf.B = bytesutil.ResizeNoCopyNoOverallocate(copyBuf.B, 16*1024)
|
||||
_, err = io.CopyBuffer(w, res.Body, copyBuf.B)
|
||||
copyBufPool.Put(copyBuf)
|
||||
_ = res.Body.Close()
|
||||
if err != nil && !netutil.IsTrivialNetworkError(err) {
|
||||
remoteAddr := httpserver.GetQuotedRemoteAddr(r)
|
||||
requestURI := httpserver.GetRequestURI(r)
|
||||
logger.Warnf("remoteAddr: %s; requestURI: %s; error when proxying response body from %s: %s", remoteAddr, requestURI, targetURL, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
var copyBufPool bytesutil.ByteBufferPool
|
||||
|
||||
func copyHeader(dst, src http.Header) {
|
||||
for k, vv := range src {
|
||||
for _, v := range vv {
|
||||
dst.Add(k, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func sanitizeRequestHeaders(r *http.Request) *http.Request {
|
||||
// This code has been copied from net/http/httputil/reverseproxy.go
|
||||
req := r.Clone(r.Context())
|
||||
removeHopHeaders(req.Header)
|
||||
if clientIP, _, err := net.SplitHostPort(req.RemoteAddr); err == nil {
|
||||
// If we aren't the first proxy retain prior
|
||||
// X-Forwarded-For information as a comma+space
|
||||
// separated list and fold multiple headers into one.
|
||||
prior := req.Header["X-Forwarded-For"]
|
||||
if len(prior) > 0 {
|
||||
clientIP = strings.Join(prior, ", ") + ", " + clientIP
|
||||
}
|
||||
req.Header.Set("X-Forwarded-For", clientIP)
|
||||
}
|
||||
return req
|
||||
}
|
||||
|
||||
func removeHopHeaders(h http.Header) {
|
||||
// remove hop-by-hop headers listed in the "Connection" header of h.
|
||||
// See RFC 7230, section 6.1
|
||||
for _, f := range h["Connection"] {
|
||||
for _, sf := range strings.Split(f, ",") {
|
||||
if sf = textproto.TrimString(sf); sf != "" {
|
||||
h.Del(sf)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remove hop-by-hop headers to the backend. Especially
|
||||
// important is "Connection" because we want a persistent
|
||||
// connection, regardless of what the client sent to us.
|
||||
for _, key := range hopHeaders {
|
||||
h.Del(key)
|
||||
}
|
||||
}
|
||||
|
||||
// Hop-by-hop headers. These are removed when sent to the backend.
|
||||
// As of RFC 7230, hop-by-hop headers are required to appear in the
|
||||
// Connection header field. These are the headers defined by the
|
||||
// obsoleted RFC 2616 (section 13.5.1) and are used for backward
|
||||
// compatibility.
|
||||
var hopHeaders = []string{
|
||||
"Connection",
|
||||
"Proxy-Connection", // non-standard but still sent by libcurl and rejected by e.g. google
|
||||
"Keep-Alive",
|
||||
"Proxy-Authenticate",
|
||||
"Proxy-Authorization",
|
||||
"Te", // canonicalized version of "TE"
|
||||
"Trailer", // not Trailers per URL above; https://www.rfc-editor.org/errata_search.php?eid=4522
|
||||
"Transfer-Encoding",
|
||||
"Upgrade",
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -126,43 +236,41 @@ var (
|
||||
)
|
||||
|
||||
var (
|
||||
reverseProxy *httputil.ReverseProxy
|
||||
reverseProxyOnce sync.Once
|
||||
transport *http.Transport
|
||||
transportOnce sync.Once
|
||||
)
|
||||
|
||||
func getReverseProxy() *httputil.ReverseProxy {
|
||||
reverseProxyOnce.Do(initReverseProxy)
|
||||
return reverseProxy
|
||||
func transportInit() {
|
||||
tr := http.DefaultTransport.(*http.Transport).Clone()
|
||||
tr.ResponseHeaderTimeout = *responseTimeout
|
||||
// Automatic compression must be disabled in order to fix https://github.com/VictoriaMetrics/VictoriaMetrics/issues/535
|
||||
tr.DisableCompression = true
|
||||
// Disable HTTP/2.0, since VictoriaMetrics components don't support HTTP/2.0 (because there is no sense in this).
|
||||
tr.ForceAttemptHTTP2 = false
|
||||
tr.MaxIdleConnsPerHost = *maxIdleConnsPerBackend
|
||||
if tr.MaxIdleConns != 0 && tr.MaxIdleConns < tr.MaxIdleConnsPerHost {
|
||||
tr.MaxIdleConns = tr.MaxIdleConnsPerHost
|
||||
}
|
||||
transport = tr
|
||||
}
|
||||
|
||||
// initReverseProxy must be called after flag.Parse(), since it uses command-line flags.
|
||||
func initReverseProxy() {
|
||||
reverseProxy = &httputil.ReverseProxy{
|
||||
Director: func(r *http.Request) {
|
||||
targetURL := r.Header.Get("vm-target-url")
|
||||
target, err := url.Parse(targetURL)
|
||||
if err != nil {
|
||||
logger.Panicf("BUG: unexpected error when parsing targetURL=%q: %s", targetURL, err)
|
||||
}
|
||||
r.URL = target
|
||||
},
|
||||
Transport: func() *http.Transport {
|
||||
tr := http.DefaultTransport.(*http.Transport).Clone()
|
||||
// Automatic compression must be disabled in order to fix https://github.com/VictoriaMetrics/VictoriaMetrics/issues/535
|
||||
tr.DisableCompression = true
|
||||
// Disable HTTP/2.0, since VictoriaMetrics components don't support HTTP/2.0 (because there is no sense in this).
|
||||
tr.ForceAttemptHTTP2 = false
|
||||
tr.MaxIdleConnsPerHost = *maxIdleConnsPerBackend
|
||||
if tr.MaxIdleConns != 0 && tr.MaxIdleConns < tr.MaxIdleConnsPerHost {
|
||||
tr.MaxIdleConns = tr.MaxIdleConnsPerHost
|
||||
}
|
||||
return tr
|
||||
}(),
|
||||
FlushInterval: time.Second,
|
||||
ErrorLog: logger.StdErrorLogger(),
|
||||
}
|
||||
var (
|
||||
concurrencyLimitCh chan struct{}
|
||||
concurrencyLimitOnce sync.Once
|
||||
)
|
||||
|
||||
func concurrencyLimitInit() {
|
||||
concurrencyLimitCh = make(chan struct{}, *maxConcurrentRequests)
|
||||
_ = metrics.NewGauge("vmauth_concurrent_requests_capacity", func() float64 {
|
||||
return float64(*maxConcurrentRequests)
|
||||
})
|
||||
_ = metrics.NewGauge("vmauth_concurrent_requests_current", func() float64 {
|
||||
return float64(len(concurrencyLimitCh))
|
||||
})
|
||||
}
|
||||
|
||||
var concurrentRequestsLimitReachedTotal = metrics.NewCounter("vmauth_concurrent_requests_limit_reached_total")
|
||||
|
||||
func usage() {
|
||||
const s = `
|
||||
vmauth authenticates and authorizes incoming requests and proxies them to VictoriaMetrics.
|
||||
|
||||
@@ -52,7 +52,7 @@ func createTargetURL(ui *UserInfo, uOrig *url.URL) (*url.URL, []Header, error) {
|
||||
// See https://github.com/VictoriaMetrics/VictoriaMetrics/pull/1554
|
||||
u.Path = ""
|
||||
}
|
||||
for _, e := range ui.URLMap {
|
||||
for _, e := range ui.URLMaps {
|
||||
for _, sp := range e.SrcPaths {
|
||||
if sp.match(u.Path) {
|
||||
return e.URLPrefix.mergeURLs(&u), e.Headers, nil
|
||||
|
||||
@@ -54,7 +54,7 @@ func TestCreateTargetURLSuccess(t *testing.T) {
|
||||
|
||||
// Complex routing with `url_map`
|
||||
ui := &UserInfo{
|
||||
URLMap: []URLMap{
|
||||
URLMaps: []URLMap{
|
||||
{
|
||||
SrcPaths: getSrcPaths([]string{"/api/v1/query"}),
|
||||
URLPrefix: mustParseURL("http://vmselect/0/prometheus"),
|
||||
@@ -86,7 +86,7 @@ func TestCreateTargetURLSuccess(t *testing.T) {
|
||||
|
||||
// Complex routing regexp paths in `url_map`
|
||||
ui = &UserInfo{
|
||||
URLMap: []URLMap{
|
||||
URLMaps: []URLMap{
|
||||
{
|
||||
SrcPaths: getSrcPaths([]string{"/api/v1/query(_range)?", "/api/v1/label/[^/]+/values"}),
|
||||
URLPrefix: mustParseURL("http://vmselect/0/prometheus"),
|
||||
@@ -132,7 +132,7 @@ func TestCreateTargetURLFailure(t *testing.T) {
|
||||
}
|
||||
f(&UserInfo{}, "/foo/bar")
|
||||
f(&UserInfo{
|
||||
URLMap: []URLMap{
|
||||
URLMaps: []URLMap{
|
||||
{
|
||||
SrcPaths: getSrcPaths([]string{"/api/v1/query"}),
|
||||
URLPrefix: mustParseURL("http://foobar/baz"),
|
||||
|
||||
@@ -65,7 +65,7 @@ func main() {
|
||||
logger.Fatalf("Failed to set snapshot.deleteURL flag: %v", err)
|
||||
}
|
||||
}
|
||||
deleteUrl, err := url.Parse(*snapshotCreateURL)
|
||||
deleteUrl, err := url.Parse(*snapshotDeleteURL)
|
||||
if err != nil {
|
||||
logger.Fatalf("cannot parse snapshotDeleteURL: %s", err)
|
||||
}
|
||||
@@ -93,7 +93,7 @@ func main() {
|
||||
logger.Fatalf("invalid -snapshotName=%q: %s", *snapshotName, err)
|
||||
}
|
||||
|
||||
go httpserver.Serve(*httpListenAddr, nil)
|
||||
go httpserver.Serve(*httpListenAddr, false, nil)
|
||||
|
||||
srcFS, err := newSrcFS()
|
||||
if err != nil {
|
||||
|
||||
@@ -102,6 +102,7 @@ func (pp *prometheusProcessor) do(b tsdb.BlockReader) error {
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read block: %s", err)
|
||||
}
|
||||
var it chunkenc.Iterator
|
||||
for ss.Next() {
|
||||
var name string
|
||||
var labels []vm.LabelPair
|
||||
@@ -123,7 +124,7 @@ func (pp *prometheusProcessor) do(b tsdb.BlockReader) error {
|
||||
|
||||
var timestamps []int64
|
||||
var values []float64
|
||||
it := series.Iterator()
|
||||
it = series.Iterator(it)
|
||||
for {
|
||||
typ := it.Next()
|
||||
if typ == chunkenc.ValNone {
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
"github.com/prometheus/prometheus/model/labels"
|
||||
"github.com/prometheus/prometheus/prompb"
|
||||
"github.com/prometheus/prometheus/storage/remote"
|
||||
"github.com/prometheus/prometheus/tsdb/chunks"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -154,7 +155,7 @@ func (rrs *RemoteReadServer) getStreamReadHandler(t *testing.T) http.Handler {
|
||||
t.Fatalf("error unmarshal read request: %s", err)
|
||||
}
|
||||
|
||||
var chunks []prompb.Chunk
|
||||
var chks []prompb.Chunk
|
||||
ctx := context.Background()
|
||||
for idx, r := range req.Queries {
|
||||
startTs := r.StartTimestampMs
|
||||
@@ -171,9 +172,10 @@ func (rrs *RemoteReadServer) getStreamReadHandler(t *testing.T) http.Handler {
|
||||
}
|
||||
|
||||
ss := q.Select(false, nil, matchers...)
|
||||
var iter chunks.Iterator
|
||||
for ss.Next() {
|
||||
series := ss.At()
|
||||
iter := series.Iterator()
|
||||
iter = series.Iterator(iter)
|
||||
labels := remote.MergeLabels(labelsToLabelsProto(series.Labels()), nil)
|
||||
|
||||
frameBytesLeft := maxBytesInFrame
|
||||
@@ -190,14 +192,14 @@ func (rrs *RemoteReadServer) getStreamReadHandler(t *testing.T) http.Handler {
|
||||
t.Fatalf("error found not populated chunk returned by SeriesSet at ref: %v", chunk.Ref)
|
||||
}
|
||||
|
||||
chunks = append(chunks, prompb.Chunk{
|
||||
chks = append(chks, prompb.Chunk{
|
||||
MinTimeMs: chunk.MinTime,
|
||||
MaxTimeMs: chunk.MaxTime,
|
||||
Type: prompb.Chunk_Encoding(chunk.Chunk.Encoding()),
|
||||
Data: chunk.Chunk.Bytes(),
|
||||
})
|
||||
|
||||
frameBytesLeft -= chunks[len(chunks)-1].Size()
|
||||
frameBytesLeft -= chks[len(chks)-1].Size()
|
||||
|
||||
// We are fine with minor inaccuracy of max bytes per frame. The inaccuracy will be max of full chunk size.
|
||||
isNext = iter.Next()
|
||||
@@ -207,7 +209,7 @@ func (rrs *RemoteReadServer) getStreamReadHandler(t *testing.T) http.Handler {
|
||||
|
||||
resp := &prompb.ChunkedReadResponse{
|
||||
ChunkedSeries: []*prompb.ChunkedSeries{
|
||||
{Labels: labels, Chunks: chunks},
|
||||
{Labels: labels, Chunks: chks},
|
||||
},
|
||||
QueryIndex: int64(idx),
|
||||
}
|
||||
@@ -220,7 +222,7 @@ func (rrs *RemoteReadServer) getStreamReadHandler(t *testing.T) http.Handler {
|
||||
if _, err := stream.Write(b); err != nil {
|
||||
t.Fatalf("error write to stream: %s", err)
|
||||
}
|
||||
chunks = chunks[:0]
|
||||
chks = chks[:0]
|
||||
rrs.storage.Reset()
|
||||
}
|
||||
if err := iter.Err(); err != nil {
|
||||
|
||||
@@ -304,7 +304,11 @@ The shortlist of configuration flags include the following:
|
||||
-httpAuth.username string
|
||||
Username for HTTP Basic Auth. The authentication is disabled if empty. See also -httpAuth.password
|
||||
-httpListenAddr string
|
||||
TCP address to listen for http connections (default ":8431")
|
||||
TCP address to listen for http connections. See also -httpListenAddr.useProxyProtocol (default ":8431")
|
||||
-httpListenAddr.useProxyProtocol
|
||||
Whether to use proxy protocol for connections accepted at -httpListenAddr . See https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt
|
||||
-internStringMaxLen int
|
||||
The maximum length for strings to intern. Lower limit may save memory at the cost of higher CPU usage. See https://en.wikipedia.org/wiki/String_interning (default 300)
|
||||
-loggerDisableTimestamps
|
||||
Whether to disable writing timestamps in logs
|
||||
-loggerErrorsPerSecondLimit int
|
||||
|
||||
@@ -16,10 +16,12 @@ import (
|
||||
var (
|
||||
streamAggrConfig = flag.String("streamAggr.config", "", "Optional path to file with stream aggregation config. "+
|
||||
"See https://docs.victoriametrics.com/stream-aggregation.html . "+
|
||||
"See also -remoteWrite.streamAggr.keepInput")
|
||||
"See also -remoteWrite.streamAggr.keepInput and -streamAggr.dedupInterval")
|
||||
streamAggrKeepInput = flag.Bool("streamAggr.keepInput", false, "Whether to keep input samples after the aggregation with -streamAggr.config. "+
|
||||
"By default the input is dropped after the aggregation, so only the aggregate data is stored. "+
|
||||
"See https://docs.victoriametrics.com/stream-aggregation.html")
|
||||
streamAggrDedupInterval = flag.Duration("streamAggr.dedupInterval", 0, "Input samples are de-duplicated with this interval before being aggregated. "+
|
||||
"Only the last sample per each time series per each interval is aggregated if the interval is greater than zero")
|
||||
)
|
||||
|
||||
// InitStreamAggr must be called after flag.Parse and before using the common package.
|
||||
@@ -30,7 +32,7 @@ func InitStreamAggr() {
|
||||
// Nothing to initialize
|
||||
return
|
||||
}
|
||||
a, err := streamaggr.LoadFromFile(*streamAggrConfig, pushAggregateSeries)
|
||||
a, err := streamaggr.LoadFromFile(*streamAggrConfig, pushAggregateSeries, *streamAggrDedupInterval)
|
||||
if err != nil {
|
||||
logger.Fatalf("cannot load -streamAggr.config=%q: %s", *streamAggrConfig, err)
|
||||
}
|
||||
|
||||
@@ -39,13 +39,25 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
graphiteListenAddr = flag.String("graphiteListenAddr", "", "TCP and UDP address to listen for Graphite plaintext data. Usually :2003 must be set. Doesn't work if empty")
|
||||
influxListenAddr = flag.String("influxListenAddr", "", "TCP and UDP address to listen for InfluxDB line protocol data. Usually :8089 must be set. Doesn't work if empty. "+
|
||||
"This flag isn't needed when ingesting data over HTTP - just send it to http://<victoriametrics>:8428/write")
|
||||
graphiteListenAddr = flag.String("graphiteListenAddr", "", "TCP and UDP address to listen for Graphite plaintext data. Usually :2003 must be set. Doesn't work if empty. "+
|
||||
"See also -graphiteListenAddr.useProxyProtocol")
|
||||
graphiteUseProxyProtocol = flag.Bool("graphiteListenAddr.useProxyProtocol", false, "Whether to use proxy protocol for connections accepted at -graphiteListenAddr . "+
|
||||
"See https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt")
|
||||
influxListenAddr = flag.String("influxListenAddr", "", "TCP and UDP address to listen for InfluxDB line protocol data. Usually :8089 must be set. Doesn't work if empty. "+
|
||||
"This flag isn't needed when ingesting data over HTTP - just send it to http://<victoriametrics>:8428/write . "+
|
||||
"See also -influxListenAddr.useProxyProtocol")
|
||||
influxUseProxyProtocol = flag.Bool("influxListenAddr.useProxyProtocol", false, "Whether to use proxy protocol for connections accepted at -influxListenAddr . "+
|
||||
"See https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt")
|
||||
opentsdbListenAddr = flag.String("opentsdbListenAddr", "", "TCP and UDP address to listen for OpentTSDB metrics. "+
|
||||
"Telnet put messages and HTTP /api/put messages are simultaneously served on TCP port. "+
|
||||
"Usually :4242 must be set. Doesn't work if empty")
|
||||
opentsdbHTTPListenAddr = flag.String("opentsdbHTTPListenAddr", "", "TCP address to listen for OpentTSDB HTTP put requests. Usually :4242 must be set. Doesn't work if empty")
|
||||
"Usually :4242 must be set. Doesn't work if empty. "+
|
||||
"See also -opentsdbListenAddr.useProxyProtocol")
|
||||
opentsdbUseProxyProtocol = flag.Bool("opentsdbListenAddr.useProxyProtocol", false, "Whether to use proxy protocol for connections accepted at -opentsdbListenAddr . "+
|
||||
"See https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt")
|
||||
opentsdbHTTPListenAddr = flag.String("opentsdbHTTPListenAddr", "", "TCP address to listen for OpentTSDB HTTP put requests. Usually :4242 must be set. Doesn't work if empty. "+
|
||||
"See also -opentsdbHTTPListenAddr.useProxyProtocol")
|
||||
opentsdbHTTPUseProxyProtocol = flag.Bool("opentsdbHTTPListenAddr.useProxyProtocol", false, "Whether to use proxy protocol for connections accepted "+
|
||||
"at -opentsdbHTTPListenAddr . See https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt")
|
||||
configAuthKey = flag.String("configAuthKey", "", "Authorization key for accessing /config page. It must be passed via authKey query arg")
|
||||
maxLabelsPerTimeseries = flag.Int("maxLabelsPerTimeseries", 30, "The maximum number of labels accepted per time series. Superfluous labels are dropped. In this case the vm_metrics_with_dropped_labels_total metric at /metrics page is incremented")
|
||||
maxLabelValueLen = flag.Int("maxLabelValueLen", 16*1024, "The maximum length of label values in the accepted time series. Longer label values are truncated. In this case the vm_too_long_label_values_total metric at /metrics page is incremented")
|
||||
@@ -71,16 +83,16 @@ func Init() {
|
||||
storage.SetMaxLabelValueLen(*maxLabelValueLen)
|
||||
common.StartUnmarshalWorkers()
|
||||
if len(*graphiteListenAddr) > 0 {
|
||||
graphiteServer = graphiteserver.MustStart(*graphiteListenAddr, graphite.InsertHandler)
|
||||
graphiteServer = graphiteserver.MustStart(*graphiteListenAddr, *graphiteUseProxyProtocol, graphite.InsertHandler)
|
||||
}
|
||||
if len(*influxListenAddr) > 0 {
|
||||
influxServer = influxserver.MustStart(*influxListenAddr, influx.InsertHandlerForReader)
|
||||
influxServer = influxserver.MustStart(*influxListenAddr, *influxUseProxyProtocol, influx.InsertHandlerForReader)
|
||||
}
|
||||
if len(*opentsdbListenAddr) > 0 {
|
||||
opentsdbServer = opentsdbserver.MustStart(*opentsdbListenAddr, opentsdb.InsertHandler, opentsdbhttp.InsertHandler)
|
||||
opentsdbServer = opentsdbserver.MustStart(*opentsdbListenAddr, *opentsdbUseProxyProtocol, opentsdb.InsertHandler, opentsdbhttp.InsertHandler)
|
||||
}
|
||||
if len(*opentsdbHTTPListenAddr) > 0 {
|
||||
opentsdbhttpServer = opentsdbhttpserver.MustStart(*opentsdbHTTPListenAddr, opentsdbhttp.InsertHandler)
|
||||
opentsdbhttpServer = opentsdbhttpserver.MustStart(*opentsdbHTTPListenAddr, *opentsdbHTTPUseProxyProtocol, opentsdbhttp.InsertHandler)
|
||||
}
|
||||
promscrape.Init(func(at *auth.Token, wr *prompbmarshal.WriteRequest) {
|
||||
prompush.Push(wr)
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vminsert/common"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vminsert/relabel"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/httpserver"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/prompbmarshal"
|
||||
parserCommon "github.com/VictoriaMetrics/VictoriaMetrics/lib/protoparser/common"
|
||||
parser "github.com/VictoriaMetrics/VictoriaMetrics/lib/protoparser/prometheus"
|
||||
@@ -29,7 +30,9 @@ func InsertHandler(req *http.Request) error {
|
||||
isGzipped := req.Header.Get("Content-Encoding") == "gzip"
|
||||
return parser.ParseStream(req.Body, defaultTimestamp, isGzipped, func(rows []parser.Row) error {
|
||||
return insertRows(rows, extraLabels)
|
||||
}, nil)
|
||||
}, func(s string) {
|
||||
httpserver.LogError(req, s)
|
||||
})
|
||||
}
|
||||
|
||||
func insertRows(rows []parser.Row, extraLabels []prompbmarshal.Label) error {
|
||||
|
||||
@@ -38,7 +38,7 @@ func main() {
|
||||
logger.Init()
|
||||
pushmetrics.Init()
|
||||
|
||||
go httpserver.Serve(*httpListenAddr, nil)
|
||||
go httpserver.Serve(*httpListenAddr, false, nil)
|
||||
|
||||
srcFS, err := newSrcFS()
|
||||
if err != nil {
|
||||
|
||||
@@ -4,8 +4,9 @@ import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/netutil"
|
||||
)
|
||||
|
||||
// Get returns buffered writer for the given w.
|
||||
@@ -63,7 +64,7 @@ func (bw *Writer) Write(p []byte) (int, error) {
|
||||
return 0, bw.err
|
||||
}
|
||||
n, err := bw.bw.Write(p)
|
||||
if err != nil && !isTrivialNetworkError(err) {
|
||||
if err != nil && !netutil.IsTrivialNetworkError(err) {
|
||||
bw.err = fmt.Errorf("cannot send %d bytes to client: %w", len(p), err)
|
||||
}
|
||||
return n, bw.err
|
||||
@@ -76,7 +77,7 @@ func (bw *Writer) Flush() error {
|
||||
if bw.err != nil {
|
||||
return bw.err
|
||||
}
|
||||
if err := bw.bw.Flush(); err != nil && !isTrivialNetworkError(err) {
|
||||
if err := bw.bw.Flush(); err != nil && !netutil.IsTrivialNetworkError(err) {
|
||||
bw.err = fmt.Errorf("cannot flush data to client: %w", err)
|
||||
}
|
||||
return bw.err
|
||||
@@ -88,12 +89,3 @@ func (bw *Writer) Error() error {
|
||||
defer bw.lock.Unlock()
|
||||
return bw.err
|
||||
}
|
||||
|
||||
func isTrivialNetworkError(err error) bool {
|
||||
// Suppress trivial network errors, which could occur at remote side.
|
||||
s := err.Error()
|
||||
if strings.Contains(s, "broken pipe") || strings.Contains(s, "reset by peer") {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -35,9 +35,10 @@ var (
|
||||
)
|
||||
|
||||
var benchValues = func() []float64 {
|
||||
r := rand.New(rand.NewSource(1))
|
||||
values := make([]float64, 1000)
|
||||
for i := range values {
|
||||
values[i] = rand.Float64() * 100
|
||||
values[i] = r.Float64() * 100
|
||||
}
|
||||
return values
|
||||
}()
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
{
|
||||
"files": {
|
||||
"main.css": "./static/css/main.e8e53cbf.css",
|
||||
"main.js": "./static/js/main.58fe7908.js",
|
||||
"main.css": "./static/css/main.3f9cb68f.css",
|
||||
"main.js": "./static/js/main.b1572032.js",
|
||||
"static/js/27.c1ccfd29.chunk.js": "./static/js/27.c1ccfd29.chunk.js",
|
||||
"static/media/Lato-Regular.ttf": "./static/media/Lato-Regular.d714fec1633b69a9c2e9.ttf",
|
||||
"static/media/Lato-Bold.ttf": "./static/media/Lato-Bold.32360ba4b57802daa4d6.ttf",
|
||||
"index.html": "./index.html"
|
||||
},
|
||||
"entrypoints": [
|
||||
"static/css/main.e8e53cbf.css",
|
||||
"static/js/main.58fe7908.js"
|
||||
"static/css/main.3f9cb68f.css",
|
||||
"static/js/main.b1572032.js"
|
||||
]
|
||||
}
|
||||
@@ -1 +1 @@
|
||||
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="./favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><meta name="description" content="VM-UI is a metric explorer for Victoria Metrics"/><link rel="apple-touch-icon" href="./apple-touch-icon.png"/><link rel="icon" type="image/png" sizes="32x32" href="./favicon-32x32.png"><link rel="manifest" href="./manifest.json"/><title>VM UI</title><link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin><link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono&family=Lato:wght@300;400;700&display=swap" rel="stylesheet"><script src="./dashboards/index.js" type="module"></script><script defer="defer" src="./static/js/main.58fe7908.js"></script><link href="./static/css/main.e8e53cbf.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>
|
||||
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="./favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><meta name="description" content="UI for VictoriaMetrics"/><link rel="apple-touch-icon" href="./apple-touch-icon.png"/><link rel="icon" type="image/png" sizes="32x32" href="./favicon-32x32.png"><link rel="manifest" href="./manifest.json"/><title>VM UI</title><script src="./dashboards/index.js" type="module"></script><meta name="twitter:card" content="summary_large_image"><meta name="twitter:image" content="./preview.jpg"><meta name="twitter:title" content="UI for VictoriaMetrics"><meta name="twitter:description" content="Explore and troubleshoot your VictoriaMetrics data"><meta name="twitter:site" content="@VictoriaMetrics"><meta property="og:title" content="Metric explorer for VictoriaMetrics"><meta property="og:description" content="Explore and troubleshoot your VictoriaMetrics data"><meta property="og:image" content="./preview.jpg"><meta property="og:type" content="website"><script defer="defer" src="./static/js/main.b1572032.js"></script><link href="./static/css/main.3f9cb68f.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>
|
||||
BIN
app/vmselect/vmui/preview.jpg
Normal file
BIN
app/vmselect/vmui/preview.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 67 KiB |
1
app/vmselect/vmui/static/css/main.3f9cb68f.css
Normal file
1
app/vmselect/vmui/static/css/main.3f9cb68f.css
Normal file
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
2
app/vmselect/vmui/static/js/main.b1572032.js
Normal file
2
app/vmselect/vmui/static/js/main.b1572032.js
Normal file
File diff suppressed because one or more lines are too long
@@ -7,7 +7,7 @@
|
||||
/*! regenerator-runtime -- Copyright (c) 2014-present, Facebook, Inc. -- license (MIT): https://github.com/facebook/regenerator/blob/main/LICENSE */
|
||||
|
||||
/**
|
||||
* @remix-run/router v1.0.5
|
||||
* @remix-run/router v1.3.0
|
||||
*
|
||||
* Copyright (c) Remix Software Inc.
|
||||
*
|
||||
@@ -18,7 +18,7 @@
|
||||
*/
|
||||
|
||||
/**
|
||||
* React Router DOM v6.4.5
|
||||
* React Router DOM v6.7.0
|
||||
*
|
||||
* Copyright (c) Remix Software Inc.
|
||||
*
|
||||
@@ -29,7 +29,7 @@
|
||||
*/
|
||||
|
||||
/**
|
||||
* React Router v6.4.5
|
||||
* React Router v6.7.0
|
||||
*
|
||||
* Copyright (c) Remix Software Inc.
|
||||
*
|
||||
Binary file not shown.
Binary file not shown.
@@ -84,14 +84,6 @@ func CheckTimeRange(tr storage.TimeRange) error {
|
||||
|
||||
// Init initializes vmstorage.
|
||||
func Init(resetCacheIfNeeded func(mrs []storage.MetricRow)) {
|
||||
InitWithoutMetrics(resetCacheIfNeeded)
|
||||
registerStorageMetrics(Storage)
|
||||
}
|
||||
|
||||
// InitWithoutMetrics must be called instead of Init inside tests.
|
||||
//
|
||||
// This allows multiple Init / Stop cycles.
|
||||
func InitWithoutMetrics(resetCacheIfNeeded func(mrs []storage.MetricRow)) {
|
||||
if err := encoding.CheckPrecisionBits(uint8(*precisionBits)); err != nil {
|
||||
logger.Fatalf("invalid `-precisionBits`: %s", err)
|
||||
}
|
||||
@@ -130,6 +122,7 @@ func InitWithoutMetrics(resetCacheIfNeeded func(mrs []storage.MetricRow)) {
|
||||
sizeBytes := tm.SmallSizeBytes + tm.BigSizeBytes
|
||||
logger.Infof("successfully opened storage %q in %.3f seconds; partsCount: %d; blocksCount: %d; rowsCount: %d; sizeBytes: %d",
|
||||
*DataPath, time.Since(startTime).Seconds(), partsCount, blocksCount, rowsCount, sizeBytes)
|
||||
registerStorageMetrics(Storage)
|
||||
}
|
||||
|
||||
// Storage is a storage.
|
||||
|
||||
1246
app/vmui/packages/vmui/package-lock.json
generated
1246
app/vmui/packages/vmui/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta
|
||||
name="description"
|
||||
content="VM-UI is a metric explorer for Victoria Metrics"
|
||||
content="UI for VictoriaMetrics"
|
||||
/>
|
||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/apple-touch-icon.png" />
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="%PUBLIC_URL%/favicon-32x32.png">
|
||||
@@ -26,10 +26,18 @@
|
||||
Learn how to configure a non-root public URL by running `npm run build`.
|
||||
-->
|
||||
<title>VM UI</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono&family=Lato:wght@300;400;700&display=swap" rel="stylesheet">
|
||||
<script src="%PUBLIC_URL%/dashboards/index.js" type="module"></script>
|
||||
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
<meta name="twitter:image" content="%PUBLIC_URL%/preview.jpg">
|
||||
<meta name="twitter:title" content="UI for VictoriaMetrics">
|
||||
<meta name="twitter:description" content="Explore and troubleshoot your VictoriaMetrics data">
|
||||
<meta name="twitter:site" content="@VictoriaMetrics">
|
||||
|
||||
<meta property="og:title" content="Metric explorer for VictoriaMetrics">
|
||||
<meta property="og:description" content="Explore and troubleshoot your VictoriaMetrics data">
|
||||
<meta property="og:image" content="%PUBLIC_URL%/preview.jpg">
|
||||
<meta property="og:type" content="website">
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
|
||||
BIN
app/vmui/packages/vmui/public/preview.jpg
Normal file
BIN
app/vmui/packages/vmui/public/preview.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 67 KiB |
@@ -8,60 +8,57 @@ import DashboardsLayout from "./pages/PredefinedPanels";
|
||||
import CardinalityPanel from "./pages/CardinalityPanel";
|
||||
import TopQueries from "./pages/TopQueries";
|
||||
import ThemeProvider from "./components/Main/ThemeProvider/ThemeProvider";
|
||||
import Spinner from "./components/Main/Spinner/Spinner";
|
||||
import TracePage from "./pages/TracePage";
|
||||
import ExploreMetrics from "./pages/ExploreMetrics";
|
||||
import PreviewIcons from "./components/Main/Icons/PreviewIcons";
|
||||
|
||||
const App: FC = () => {
|
||||
|
||||
const [loadingTheme, setLoadingTheme] = useState(true);
|
||||
|
||||
if (loadingTheme) return (
|
||||
<>
|
||||
<Spinner/>
|
||||
<ThemeProvider setLoadingTheme={setLoadingTheme}/>;
|
||||
</>
|
||||
);
|
||||
const [loadedTheme, setLoadedTheme] = useState(false);
|
||||
|
||||
return <>
|
||||
<HashRouter>
|
||||
<AppContextProvider>
|
||||
<Routes>
|
||||
<Route
|
||||
path={"/"}
|
||||
element={<Layout/>}
|
||||
>
|
||||
<Route
|
||||
path={router.home}
|
||||
element={<CustomPanel/>}
|
||||
/>
|
||||
<Route
|
||||
path={router.metrics}
|
||||
element={<ExploreMetrics/>}
|
||||
/>
|
||||
<Route
|
||||
path={router.cardinality}
|
||||
element={<CardinalityPanel/>}
|
||||
/>
|
||||
<Route
|
||||
path={router.topQueries}
|
||||
element={<TopQueries/>}
|
||||
/>
|
||||
<Route
|
||||
path={router.trace}
|
||||
element={<TracePage/>}
|
||||
/>
|
||||
<Route
|
||||
path={router.dashboards}
|
||||
element={<DashboardsLayout/>}
|
||||
/>
|
||||
<Route
|
||||
path={router.icons}
|
||||
element={<PreviewIcons/>}
|
||||
/>
|
||||
</Route>
|
||||
</Routes>
|
||||
<>
|
||||
<ThemeProvider onLoaded={setLoadedTheme}/>
|
||||
{loadedTheme && (
|
||||
<Routes>
|
||||
<Route
|
||||
path={"/"}
|
||||
element={<Layout/>}
|
||||
>
|
||||
<Route
|
||||
path={router.home}
|
||||
element={<CustomPanel/>}
|
||||
/>
|
||||
<Route
|
||||
path={router.metrics}
|
||||
element={<ExploreMetrics/>}
|
||||
/>
|
||||
<Route
|
||||
path={router.cardinality}
|
||||
element={<CardinalityPanel/>}
|
||||
/>
|
||||
<Route
|
||||
path={router.topQueries}
|
||||
element={<TopQueries/>}
|
||||
/>
|
||||
<Route
|
||||
path={router.trace}
|
||||
element={<TracePage/>}
|
||||
/>
|
||||
<Route
|
||||
path={router.dashboards}
|
||||
element={<DashboardsLayout/>}
|
||||
/>
|
||||
<Route
|
||||
path={router.icons}
|
||||
element={<PreviewIcons/>}
|
||||
/>
|
||||
</Route>
|
||||
</Routes>
|
||||
)}
|
||||
</>
|
||||
</AppContextProvider>
|
||||
</HashRouter>
|
||||
</>;
|
||||
|
||||
2
app/vmui/packages/vmui/src/api/accountId.ts
Normal file
2
app/vmui/packages/vmui/src/api/accountId.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export const getAccountIds = (server: string) =>
|
||||
`${server.replace(/^(.+)(\/select.+)/, "$1")}/admin/tenants`;
|
||||
BIN
app/vmui/packages/vmui/src/assets/fonts/Lato/Lato-Bold.ttf
Normal file
BIN
app/vmui/packages/vmui/src/assets/fonts/Lato/Lato-Bold.ttf
Normal file
Binary file not shown.
BIN
app/vmui/packages/vmui/src/assets/fonts/Lato/Lato-Regular.ttf
Normal file
BIN
app/vmui/packages/vmui/src/assets/fonts/Lato/Lato-Regular.ttf
Normal file
Binary file not shown.
@@ -3,11 +3,13 @@ import uPlot, { Options as uPlotOptions } from "uplot";
|
||||
import useResize from "../../../hooks/useResize";
|
||||
import { BarChartProps } from "./types";
|
||||
import "./style.scss";
|
||||
import { useAppState } from "../../../state/common/StateContext";
|
||||
|
||||
const BarChart: FC<BarChartProps> = ({
|
||||
data,
|
||||
container,
|
||||
configs }) => {
|
||||
const { isDarkTheme } = useAppState();
|
||||
|
||||
const uPlotRef = useRef<HTMLDivElement>(null);
|
||||
const [uPlotInst, setUPlotInst] = useState<uPlot>();
|
||||
@@ -28,7 +30,7 @@ const BarChart: FC<BarChartProps> = ({
|
||||
const u = new uPlot(options, data, uPlotRef.current);
|
||||
setUPlotInst(u);
|
||||
return u.destroy;
|
||||
}, [uPlotRef.current, layoutSize]);
|
||||
}, [uPlotRef.current, layoutSize, isDarkTheme]);
|
||||
|
||||
useEffect(() => updateChart(), [data]);
|
||||
|
||||
|
||||
@@ -25,7 +25,6 @@ $chart-tooltip-y: -1 * ($padding-small + $chart-tooltip-half-icon);
|
||||
pointer-events: none;
|
||||
|
||||
&_sticky {
|
||||
background-color: $color-dove-gray;
|
||||
pointer-events: auto;
|
||||
z-index: 99;
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ import "./style.scss";
|
||||
import classNames from "classnames";
|
||||
import ChartTooltip, { ChartTooltipProps } from "../ChartTooltip/ChartTooltip";
|
||||
import dayjs from "dayjs";
|
||||
import { useAppState } from "../../../state/common/StateContext";
|
||||
|
||||
export interface LineChartProps {
|
||||
metrics: MetricResult[];
|
||||
@@ -47,6 +48,8 @@ const LineChart: FC<LineChartProps> = ({
|
||||
container,
|
||||
height
|
||||
}) => {
|
||||
const { isDarkTheme } = useAppState();
|
||||
|
||||
const uPlotRef = useRef<HTMLDivElement>(null);
|
||||
const [isPanning, setPanning] = useState(false);
|
||||
const [xRange, setXRange] = useState({ min: period.start, max: period.end });
|
||||
@@ -222,7 +225,7 @@ const LineChart: FC<LineChartProps> = ({
|
||||
setUPlotInst(u);
|
||||
setXRange({ min: period.start, max: period.end });
|
||||
return u.destroy;
|
||||
}, [uPlotRef.current, series, layoutSize, height]);
|
||||
}, [uPlotRef.current, series, layoutSize, height, isDarkTheme]);
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener("keydown", handleKeyDown);
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import React, { FC } from "preact/compat";
|
||||
import { getAppModeParams } from "../../../utils/app-mode";
|
||||
import TenantsConfiguration from "../TenantsConfiguration/TenantsConfiguration";
|
||||
import { useCustomPanelDispatch, useCustomPanelState } from "../../../state/customPanel/CustomPanelStateContext";
|
||||
import { useQueryDispatch, useQueryState } from "../../../state/query/QueryStateContext";
|
||||
import "./style.scss";
|
||||
@@ -8,8 +6,6 @@ import Switch from "../../Main/Switch/Switch";
|
||||
|
||||
const AdditionalSettings: FC = () => {
|
||||
|
||||
const { inputTenantID } = getAppModeParams();
|
||||
|
||||
const { autocomplete } = useQueryState();
|
||||
const queryDispatch = useQueryDispatch();
|
||||
|
||||
@@ -44,11 +40,6 @@ const AdditionalSettings: FC = () => {
|
||||
value={isTracingEnabled}
|
||||
onChange={onChangeQueryTracing}
|
||||
/>
|
||||
{!!inputTenantID && (
|
||||
<div className="vm-additional-settings__input">
|
||||
<TenantsConfiguration/>
|
||||
</div>
|
||||
)}
|
||||
</div>;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { FC, useState } from "preact/compat";
|
||||
import React, { FC, useEffect, useState } from "preact/compat";
|
||||
import ServerConfigurator from "./ServerConfigurator/ServerConfigurator";
|
||||
import { useAppDispatch, useAppState } from "../../../state/common/StateContext";
|
||||
import { SettingsIcon } from "../../Main/Icons";
|
||||
@@ -13,6 +13,7 @@ import { getAppModeEnable } from "../../../utils/app-mode";
|
||||
import classNames from "classnames";
|
||||
import Timezones from "./Timezones/Timezones";
|
||||
import { useTimeDispatch, useTimeState } from "../../../state/time/TimeStateContext";
|
||||
import ThemeControl from "../ThemeControl/ThemeControl";
|
||||
|
||||
const title = "Settings";
|
||||
|
||||
@@ -42,6 +43,11 @@ const GlobalSettings: FC = () => {
|
||||
handleClose();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (stateServerUrl === serverUrl) return;
|
||||
setServerUrl(stateServerUrl);
|
||||
}, [stateServerUrl]);
|
||||
|
||||
return <>
|
||||
<Tooltip title={title}>
|
||||
<Button
|
||||
@@ -82,19 +88,22 @@ const GlobalSettings: FC = () => {
|
||||
onChange={setTimezone}
|
||||
/>
|
||||
</div>
|
||||
<div className="vm-server-configurator__input">
|
||||
<ThemeControl/>
|
||||
</div>
|
||||
<div className="vm-server-configurator__footer">
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="error"
|
||||
onClick={handleClose}
|
||||
>
|
||||
Cancel
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={handlerApply}
|
||||
>
|
||||
apply
|
||||
apply
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -22,18 +22,14 @@ const ServerConfigurator: FC<ServerConfiguratorProps> = ({ serverUrl, onChange ,
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="vm-server-configurator__title">
|
||||
Server URL
|
||||
</div>
|
||||
<TextField
|
||||
autofocus
|
||||
value={serverUrl}
|
||||
error={error}
|
||||
onChange={onChangeServer}
|
||||
onEnter={onEnter}
|
||||
/>
|
||||
</div>
|
||||
<TextField
|
||||
autofocus
|
||||
label="Server URL"
|
||||
value={serverUrl}
|
||||
error={error}
|
||||
onChange={onChangeServer}
|
||||
onEnter={onEnter}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
import React, { FC, useState, useRef, useEffect, useMemo } from "preact/compat";
|
||||
import { useAppDispatch, useAppState } from "../../../../state/common/StateContext";
|
||||
import { useTimeDispatch } from "../../../../state/time/TimeStateContext";
|
||||
import { ArrowDownIcon, StorageIcons } from "../../../Main/Icons";
|
||||
import Button from "../../../Main/Button/Button";
|
||||
import "./style.scss";
|
||||
import { replaceTenantId } from "../../../../utils/default-server-url";
|
||||
import classNames from "classnames";
|
||||
import Popper from "../../../Main/Popper/Popper";
|
||||
import { getAppModeEnable } from "../../../../utils/app-mode";
|
||||
import Tooltip from "../../../Main/Tooltip/Tooltip";
|
||||
|
||||
const TenantsConfiguration: FC<{accountIds: string[]}> = ({ accountIds }) => {
|
||||
const appModeEnable = getAppModeEnable();
|
||||
|
||||
const { tenantId: tenantIdState, serverUrl } = useAppState();
|
||||
const dispatch = useAppDispatch();
|
||||
const timeDispatch = useTimeDispatch();
|
||||
|
||||
const [openOptions, setOpenOptions] = useState(false);
|
||||
const optionsButtonRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const getTenantIdFromUrl = (url: string) => {
|
||||
const regexp = /(\/select\/)(\d+|\d.+)(\/)(.+)/;
|
||||
return (url.match(regexp) || [])[2];
|
||||
};
|
||||
|
||||
const showTenantSelector = useMemo(() => {
|
||||
const id = getTenantIdFromUrl(serverUrl);
|
||||
return accountIds.length > 1 && id;
|
||||
}, [accountIds, serverUrl]);
|
||||
|
||||
const toggleOpenOptions = () => {
|
||||
setOpenOptions(prev => !prev);
|
||||
};
|
||||
|
||||
const handleCloseOptions = () => {
|
||||
setOpenOptions(false);
|
||||
};
|
||||
|
||||
const createHandlerChange = (value: string) => () => {
|
||||
const tenant = value;
|
||||
dispatch({ type: "SET_TENANT_ID", payload: tenant });
|
||||
if (serverUrl) {
|
||||
const updateServerUrl = replaceTenantId(serverUrl, tenant);
|
||||
if (updateServerUrl === serverUrl) return;
|
||||
dispatch({ type: "SET_SERVER", payload: updateServerUrl });
|
||||
timeDispatch({ type: "RUN_QUERY" });
|
||||
}
|
||||
handleCloseOptions();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const id = getTenantIdFromUrl(serverUrl);
|
||||
|
||||
if (tenantIdState && tenantIdState !== id) {
|
||||
createHandlerChange(tenantIdState)();
|
||||
} else {
|
||||
createHandlerChange(id)();
|
||||
}
|
||||
}, [serverUrl]);
|
||||
|
||||
if (!showTenantSelector) return null;
|
||||
|
||||
return (
|
||||
<div className="vm-tenant-input">
|
||||
<Tooltip title="Define Tenant ID if you need request to another storage">
|
||||
<div ref={optionsButtonRef}>
|
||||
<Button
|
||||
className={appModeEnable ? "" : "vm-header-button"}
|
||||
variant="contained"
|
||||
color="primary"
|
||||
fullWidth
|
||||
startIcon={<StorageIcons/>}
|
||||
endIcon={(
|
||||
<div
|
||||
className={classNames({
|
||||
"vm-execution-controls-buttons__arrow": true,
|
||||
"vm-execution-controls-buttons__arrow_open": openOptions,
|
||||
})}
|
||||
>
|
||||
<ArrowDownIcon/>
|
||||
</div>
|
||||
)}
|
||||
onClick={toggleOpenOptions}
|
||||
>
|
||||
{tenantIdState}
|
||||
</Button>
|
||||
</div>
|
||||
</Tooltip>
|
||||
<Popper
|
||||
open={openOptions}
|
||||
placement="bottom-left"
|
||||
onClose={handleCloseOptions}
|
||||
buttonRef={optionsButtonRef}
|
||||
fullWidth
|
||||
>
|
||||
<div className="vm-list">
|
||||
{accountIds.map(id => (
|
||||
<div
|
||||
className={classNames({
|
||||
"vm-list-item": true,
|
||||
"vm-list-item_active": id === tenantIdState
|
||||
})}
|
||||
key={id}
|
||||
onClick={createHandlerChange(id)}
|
||||
>
|
||||
{id}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Popper>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TenantsConfiguration;
|
||||
@@ -0,0 +1,41 @@
|
||||
import { useAppState } from "../../../../../state/common/StateContext";
|
||||
import { useEffect, useMemo, useState } from "preact/compat";
|
||||
import { ErrorTypes } from "../../../../../types";
|
||||
import { getAccountIds } from "../../../../../api/accountId";
|
||||
|
||||
export const useFetchAccountIds = () => {
|
||||
const { serverUrl } = useAppState();
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<ErrorTypes | string>();
|
||||
const [accountIds, setAccountIds] = useState<string[]>([]);
|
||||
|
||||
const fetchUrl = useMemo(() => getAccountIds(serverUrl), [serverUrl]);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const response = await fetch(fetchUrl);
|
||||
const resp = await response.json();
|
||||
const data = (resp.data || []) as string[];
|
||||
setAccountIds(data);
|
||||
|
||||
if (response.ok) {
|
||||
setError(undefined);
|
||||
} else {
|
||||
setError(`${resp.errorType}\r\n${resp?.error}`);
|
||||
}
|
||||
} catch (e) {
|
||||
if (e instanceof Error) {
|
||||
setError(`${e.name}: ${e.message}`);
|
||||
}
|
||||
}
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
fetchData().catch(console.error);
|
||||
}, [fetchUrl]);
|
||||
|
||||
return { accountIds, isLoading, error };
|
||||
};
|
||||
@@ -0,0 +1,5 @@
|
||||
@use "../../../../styles/variables" as *;
|
||||
|
||||
.vm-tenant-input {
|
||||
position: relative;
|
||||
}
|
||||
@@ -23,7 +23,7 @@
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: rgba($color-black, 0.06);
|
||||
background-color: $color-hover-black;
|
||||
padding: calc($padding-small/2);
|
||||
border-radius: $border-radius-small;
|
||||
}
|
||||
|
||||
@@ -8,9 +8,15 @@
|
||||
|
||||
&__input {
|
||||
|
||||
&_server {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
gap: 0 $padding-small;
|
||||
}
|
||||
}
|
||||
|
||||
&__title {
|
||||
grid-column: auto / span 2;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
|
||||
@@ -57,6 +57,7 @@ const GraphSettings: FC<GraphSettingsProps> = ({ yaxis, setYaxisLimits, toggleEn
|
||||
</h3>
|
||||
<Button
|
||||
size="small"
|
||||
variant="text"
|
||||
startIcon={<CloseIcon/>}
|
||||
onClick={handleClose}
|
||||
/>
|
||||
|
||||
@@ -158,7 +158,7 @@ const StepConfigurator: FC = () => {
|
||||
className="vm-link vm-link_colored"
|
||||
href="https://docs.victoriametrics.com/keyConcepts.html#range-query"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
rel="help noreferrer"
|
||||
>
|
||||
Read more about Range query
|
||||
</a>
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
padding: 0.2em 0.4em;
|
||||
margin: 0 0.2em;
|
||||
font-size: 85%;
|
||||
background-color: rgba($color-black, 0.05);
|
||||
background-color: $color-hover-black;
|
||||
border-radius: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
import React, { FC, useState, useEffect, useCallback } from "preact/compat";
|
||||
import { useAppDispatch, useAppState } from "../../../state/common/StateContext";
|
||||
import debounce from "lodash.debounce";
|
||||
import { getAppModeParams } from "../../../utils/app-mode";
|
||||
import { useTimeDispatch } from "../../../state/time/TimeStateContext";
|
||||
import { InfoIcon } from "../../Main/Icons";
|
||||
import TextField from "../../Main/TextField/TextField";
|
||||
import Button from "../../Main/Button/Button";
|
||||
import Tooltip from "../../Main/Tooltip/Tooltip";
|
||||
|
||||
const TenantsConfiguration: FC = () => {
|
||||
const { serverURL } = getAppModeParams();
|
||||
const { tenantId: tenantIdState } = useAppState();
|
||||
const dispatch = useAppDispatch();
|
||||
const timeDispatch = useTimeDispatch();
|
||||
|
||||
const [tenantId, setTenantId] = useState<string | number>(tenantIdState || 0);
|
||||
|
||||
const handleApply = (value: string | number) => {
|
||||
const tenantId = Number(value);
|
||||
dispatch({ type: "SET_TENANT_ID", payload: tenantId });
|
||||
if (serverURL) {
|
||||
const updateServerUrl = serverURL.replace(/(\/select\/)([\d]+)(\/prometheus)/, `$1${tenantId}$3`);
|
||||
dispatch({ type: "SET_SERVER", payload: updateServerUrl });
|
||||
timeDispatch({ type: "RUN_QUERY" });
|
||||
}
|
||||
};
|
||||
|
||||
const debouncedHandleApply = useCallback(debounce(handleApply, 700), []);
|
||||
|
||||
const handleChange = (value: string) => {
|
||||
setTenantId(value);
|
||||
debouncedHandleApply(value);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (tenantId === tenantIdState) return;
|
||||
setTenantId(tenantIdState);
|
||||
}, [tenantIdState]);
|
||||
|
||||
return <TextField
|
||||
label="Tenant ID"
|
||||
type="number"
|
||||
value={tenantId}
|
||||
onChange={handleChange}
|
||||
endIcon={(
|
||||
<Tooltip title={"Define tenant id if you need request to another storage"}>
|
||||
<Button
|
||||
variant={"text"}
|
||||
size={"small"}
|
||||
startIcon={<InfoIcon/>}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
/>;
|
||||
};
|
||||
|
||||
export default TenantsConfiguration;
|
||||
@@ -0,0 +1,32 @@
|
||||
import React from "react";
|
||||
import "./style.scss";
|
||||
import { useAppDispatch, useAppState } from "../../../state/common/StateContext";
|
||||
import { Theme } from "../../../types";
|
||||
import Toggle from "../../Main/Toggle/Toggle";
|
||||
|
||||
const options = Object.values(Theme).map(value => ({ title: value, value }));
|
||||
const ThemeControl = () => {
|
||||
const { theme } = useAppState();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const handleClickItem = (value: string) => {
|
||||
dispatch({ type: "SET_THEME", payload: value as Theme });
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="vm-theme-control">
|
||||
<div className="vm-server-configurator__title">
|
||||
Theme preferences
|
||||
</div>
|
||||
<div className="vm-theme-control__toggle">
|
||||
<Toggle
|
||||
options={options}
|
||||
value={theme}
|
||||
onChange={handleClickItem}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ThemeControl;
|
||||
@@ -0,0 +1,10 @@
|
||||
@use "src/styles/variables" as *;
|
||||
|
||||
.vm-theme-control {
|
||||
|
||||
&__toggle {
|
||||
display: inline-flex;
|
||||
min-width: 300px;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
}
|
||||
@@ -13,11 +13,14 @@ import useResize from "../../../../hooks/useResize";
|
||||
import DatePicker from "../../../Main/DatePicker/DatePicker";
|
||||
import "./style.scss";
|
||||
import useClickOutside from "../../../../hooks/useClickOutside";
|
||||
import classNames from "classnames";
|
||||
import { useAppState } from "../../../../state/common/StateContext";
|
||||
|
||||
export const TimeSelector: FC = () => {
|
||||
const { isDarkTheme } = useAppState();
|
||||
const wrapperRef = useRef<HTMLDivElement>(null);
|
||||
const documentSize = useResize(document.body);
|
||||
const displayFullDate = useMemo(() => documentSize.width > 1120, [documentSize]);
|
||||
const displayFullDate = useMemo(() => documentSize.width > 1280, [documentSize]);
|
||||
|
||||
const [until, setUntil] = useState<string>();
|
||||
const [from, setFrom] = useState<string>();
|
||||
@@ -120,7 +123,7 @@ export const TimeSelector: FC = () => {
|
||||
|
||||
return <>
|
||||
<div ref={buttonRef}>
|
||||
<Tooltip title="Time range controls">
|
||||
<Tooltip title={displayFullDate ? "Time range controls" : dateTitle}>
|
||||
<Button
|
||||
className={appModeEnable ? "" : "vm-header-button"}
|
||||
variant="contained"
|
||||
@@ -144,7 +147,12 @@ export const TimeSelector: FC = () => {
|
||||
ref={wrapperRef}
|
||||
>
|
||||
<div className="vm-time-selector-left">
|
||||
<div className="vm-time-selector-left-inputs">
|
||||
<div
|
||||
className={classNames({
|
||||
"vm-time-selector-left-inputs": true,
|
||||
"vm-time-selector-left-inputs_dark": isDarkTheme
|
||||
})}
|
||||
>
|
||||
<div
|
||||
className="vm-time-selector-left-inputs__date"
|
||||
ref={fromRef}
|
||||
|
||||
@@ -18,6 +18,10 @@
|
||||
align-items: flex-start;
|
||||
justify-content: stretch;
|
||||
|
||||
&_dark &__date {
|
||||
border-color: $color-text-disabled;
|
||||
}
|
||||
|
||||
&__date {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 14px;
|
||||
@@ -70,7 +74,7 @@
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: rgba($color-black, 0.06);
|
||||
background-color: $color-hover-black;
|
||||
padding: calc($padding-small/2);
|
||||
border-radius: $border-radius-small;
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
code {
|
||||
padding: 0.2em 0.4em;
|
||||
font-size: 85%;
|
||||
background-color: rgba($color-black, 0.05);
|
||||
background-color: $color-hover-black;
|
||||
border-radius: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { FC } from "preact/compat";
|
||||
import dayjs from "dayjs";
|
||||
import "./style.scss";
|
||||
import { LogoIcon } from "../../Main/Icons";
|
||||
import { IssueIcon, LogoIcon, WikiIcon } from "../../Main/Icons";
|
||||
|
||||
const Footer: FC = () => {
|
||||
const copyrightYears = `2019-${dayjs().format("YYYY")}`;
|
||||
@@ -11,18 +11,28 @@ const Footer: FC = () => {
|
||||
className="vm-link vm-footer__website"
|
||||
target="_blank"
|
||||
href="https://victoriametrics.com/"
|
||||
rel="noreferrer"
|
||||
rel="me noreferrer"
|
||||
>
|
||||
<LogoIcon/>
|
||||
victoriametrics.com
|
||||
</a>
|
||||
<a
|
||||
className="vm-link"
|
||||
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"
|
||||
>
|
||||
create an issue
|
||||
<IssueIcon/>
|
||||
Create an issue
|
||||
</a>
|
||||
<div className="vm-footer__copyright">
|
||||
© {copyrightYears} VictoriaMetrics
|
||||
|
||||
@@ -5,10 +5,12 @@
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: $padding-medium;
|
||||
gap: $padding-large;
|
||||
gap: $padding-medium;
|
||||
border-top: $border-divider;
|
||||
color: $color-text-secondary;
|
||||
background: $color-background-body;
|
||||
|
||||
&__link,
|
||||
&__website {
|
||||
display: grid;
|
||||
grid-template-columns: 12px auto;
|
||||
@@ -17,6 +19,14 @@
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
&__website {
|
||||
margin-right: $padding-global;
|
||||
}
|
||||
|
||||
&__link {
|
||||
grid-template-columns: 14px auto;
|
||||
}
|
||||
|
||||
&__copyright {
|
||||
text-align: right;
|
||||
flex-grow: 1;
|
||||
|
||||
@@ -1,86 +1,60 @@
|
||||
import React, { FC, useMemo, useState } from "preact/compat";
|
||||
import React, { FC, useMemo } from "preact/compat";
|
||||
import { ExecutionControls } from "../../Configurators/TimeRangeSettings/ExecutionControls/ExecutionControls";
|
||||
import { setQueryStringWithoutPageReload } from "../../../utils/query-string";
|
||||
import { TimeSelector } from "../../Configurators/TimeRangeSettings/TimeSelector/TimeSelector";
|
||||
import GlobalSettings from "../../Configurators/GlobalSettings/GlobalSettings";
|
||||
import { useLocation, useNavigate } from "react-router-dom";
|
||||
import router, { RouterOptions, routerOptions } from "../../../router";
|
||||
import { useEffect } from "react";
|
||||
import ShortcutKeys from "../../Main/ShortcutKeys/ShortcutKeys";
|
||||
import { getAppModeEnable, getAppModeParams } from "../../../utils/app-mode";
|
||||
import CardinalityDatePicker from "../../Configurators/CardinalityDatePicker/CardinalityDatePicker";
|
||||
import { LogoFullIcon } from "../../Main/Icons";
|
||||
import { getCssVariable } from "../../../utils/theme";
|
||||
import Tabs from "../../Main/Tabs/Tabs";
|
||||
import "./style.scss";
|
||||
import classNames from "classnames";
|
||||
import { useDashboardsState } from "../../../state/dashboards/DashboardsStateContext";
|
||||
import StepConfigurator from "../../Configurators/StepConfigurator/StepConfigurator";
|
||||
import { useAppState } from "../../../state/common/StateContext";
|
||||
import HeaderNav from "./HeaderNav/HeaderNav";
|
||||
import TenantsConfiguration from "../../Configurators/GlobalSettings/TenantsConfiguration/TenantsConfiguration";
|
||||
import { useFetchAccountIds } from "../../Configurators/GlobalSettings/TenantsConfiguration/hooks/useFetchAccountIds";
|
||||
|
||||
const Header: FC = () => {
|
||||
const primaryColor = getCssVariable("color-primary");
|
||||
const { isDarkTheme } = useAppState();
|
||||
const appModeEnable = getAppModeEnable();
|
||||
const { dashboardsSettings } = useDashboardsState();
|
||||
const { accountIds } = useFetchAccountIds();
|
||||
|
||||
const { headerStyles: {
|
||||
background = appModeEnable ? "#FFF" : primaryColor,
|
||||
color = appModeEnable ? primaryColor : "#FFF",
|
||||
} = {} } = getAppModeParams();
|
||||
const primaryColor = useMemo(() => {
|
||||
const variable = isDarkTheme ? "color-background-block" : "color-primary";
|
||||
return getCssVariable(variable);
|
||||
}, [isDarkTheme]);
|
||||
|
||||
const { background, color } = useMemo(() => {
|
||||
const { headerStyles: {
|
||||
background = appModeEnable ? "#FFF" : primaryColor,
|
||||
color = appModeEnable ? primaryColor : "#FFF",
|
||||
} = {} } = getAppModeParams();
|
||||
|
||||
return { background, color };
|
||||
}, [primaryColor]);
|
||||
|
||||
const navigate = useNavigate();
|
||||
const { search, pathname } = useLocation();
|
||||
const routes = useMemo(() => ([
|
||||
{
|
||||
label: routerOptions[router.home].title,
|
||||
value: router.home,
|
||||
},
|
||||
{
|
||||
label: routerOptions[router.metrics].title,
|
||||
value: router.metrics,
|
||||
},
|
||||
{
|
||||
label: routerOptions[router.cardinality].title,
|
||||
value: router.cardinality,
|
||||
},
|
||||
{
|
||||
label: routerOptions[router.topQueries].title,
|
||||
value: router.topQueries,
|
||||
},
|
||||
{
|
||||
label: routerOptions[router.trace].title,
|
||||
value: router.trace,
|
||||
},
|
||||
{
|
||||
label: routerOptions[router.dashboards].title,
|
||||
value: router.dashboards,
|
||||
hide: appModeEnable || !dashboardsSettings.length
|
||||
}
|
||||
]), [appModeEnable, dashboardsSettings]);
|
||||
|
||||
const [activeMenu, setActiveMenu] = useState(pathname);
|
||||
|
||||
const headerSetup = useMemo(() => {
|
||||
return ((routerOptions[pathname] || {}) as RouterOptions).header || {};
|
||||
}, [pathname]);
|
||||
|
||||
const onClickLogo = () => {
|
||||
navigateHandler(router.home);
|
||||
navigate({ pathname: router.home, search: search });
|
||||
setQueryStringWithoutPageReload({});
|
||||
window.location.reload();
|
||||
};
|
||||
|
||||
const navigateHandler = (pathname: string) => {
|
||||
navigate({ pathname, search: search });
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setActiveMenu(pathname);
|
||||
}, [pathname]);
|
||||
|
||||
return <header
|
||||
className={classNames({
|
||||
"vm-header": true,
|
||||
"vm-header_app": appModeEnable
|
||||
"vm-header_app": appModeEnable,
|
||||
"vm-header_dark": isDarkTheme
|
||||
})}
|
||||
style={{ background, color }}
|
||||
>
|
||||
@@ -93,15 +67,12 @@ const Header: FC = () => {
|
||||
<LogoFullIcon/>
|
||||
</div>
|
||||
)}
|
||||
<div className="vm-header-nav">
|
||||
<Tabs
|
||||
isNavLink
|
||||
activeItem={activeMenu}
|
||||
items={routes.filter(r => !r.hide)}
|
||||
color={color}
|
||||
/>
|
||||
</div>
|
||||
<HeaderNav
|
||||
color={color}
|
||||
background={background}
|
||||
/>
|
||||
<div className="vm-header__settings">
|
||||
{headerSetup?.tenant && <TenantsConfiguration accountIds={accountIds}/>}
|
||||
{headerSetup?.stepControl && <StepConfigurator/>}
|
||||
{headerSetup?.timeSelector && <TimeSelector/>}
|
||||
{headerSetup?.cardinalityDatePicker && <CardinalityDatePicker/>}
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
import React, { FC, useMemo, useState } from "preact/compat";
|
||||
import router, { routerOptions } from "../../../../router";
|
||||
import { getAppModeEnable } from "../../../../utils/app-mode";
|
||||
import { useLocation } from "react-router-dom";
|
||||
import { useDashboardsState } from "../../../../state/dashboards/DashboardsStateContext";
|
||||
import { useEffect } from "react";
|
||||
import "./style.scss";
|
||||
import NavItem from "./NavItem";
|
||||
import NavSubItem from "./NavSubItem";
|
||||
|
||||
interface HeaderNavProps {
|
||||
color: string
|
||||
background: string
|
||||
}
|
||||
|
||||
const HeaderNav: FC<HeaderNavProps> = ({ color, background }) => {
|
||||
const appModeEnable = getAppModeEnable();
|
||||
const { dashboardsSettings } = useDashboardsState();
|
||||
const { pathname } = useLocation();
|
||||
|
||||
const [activeMenu, setActiveMenu] = useState(pathname);
|
||||
|
||||
const menu = useMemo(() => ([
|
||||
{
|
||||
label: routerOptions[router.home].title,
|
||||
value: router.home,
|
||||
},
|
||||
{
|
||||
label: "Explore",
|
||||
submenu: [
|
||||
{
|
||||
label: routerOptions[router.metrics].title,
|
||||
value: router.metrics,
|
||||
},
|
||||
{
|
||||
label: routerOptions[router.cardinality].title,
|
||||
value: router.cardinality,
|
||||
},
|
||||
{
|
||||
label: routerOptions[router.topQueries].title,
|
||||
value: router.topQueries,
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
label: routerOptions[router.trace].title,
|
||||
value: router.trace,
|
||||
},
|
||||
{
|
||||
label: routerOptions[router.dashboards].title,
|
||||
value: router.dashboards,
|
||||
hide: appModeEnable || !dashboardsSettings.length,
|
||||
}
|
||||
].filter(r => !r.hide)), [appModeEnable, dashboardsSettings]);
|
||||
|
||||
useEffect(() => {
|
||||
setActiveMenu(pathname);
|
||||
}, [pathname]);
|
||||
|
||||
|
||||
return (
|
||||
<nav className="vm-header-nav">
|
||||
{menu.map(m => (
|
||||
m.submenu
|
||||
? (
|
||||
<NavSubItem
|
||||
key={m.label}
|
||||
activeMenu={activeMenu}
|
||||
label={m.label || ""}
|
||||
submenu={m.submenu}
|
||||
color={color}
|
||||
background={background}
|
||||
/>
|
||||
)
|
||||
: (
|
||||
<NavItem
|
||||
key={m.value}
|
||||
activeMenu={activeMenu}
|
||||
value={m.value}
|
||||
label={m.label || ""}
|
||||
color={color}
|
||||
/>
|
||||
)
|
||||
))}
|
||||
</nav>
|
||||
);
|
||||
};
|
||||
|
||||
export default HeaderNav;
|
||||
@@ -0,0 +1,30 @@
|
||||
import React, { FC } from "preact/compat";
|
||||
import { NavLink } from "react-router-dom";
|
||||
import classNames from "classnames";
|
||||
|
||||
interface NavItemProps {
|
||||
activeMenu: string,
|
||||
label: string,
|
||||
value: string,
|
||||
color?: string
|
||||
}
|
||||
|
||||
const NavItem: FC<NavItemProps> = ({
|
||||
activeMenu,
|
||||
label,
|
||||
value,
|
||||
color
|
||||
}) => (
|
||||
<NavLink
|
||||
className={classNames({
|
||||
"vm-header-nav-item": true,
|
||||
"vm-header-nav-item_active": activeMenu === value // || m.submenu?.find(m => m.value === activeMenu)
|
||||
})}
|
||||
style={{ color }}
|
||||
to={value}
|
||||
>
|
||||
{label}
|
||||
</NavLink>
|
||||
);
|
||||
|
||||
export default NavItem;
|
||||
@@ -0,0 +1,96 @@
|
||||
import React, { FC, useRef, useState } from "preact/compat";
|
||||
import { useLocation } from "react-router-dom";
|
||||
import classNames from "classnames";
|
||||
import { ArrowDropDownIcon } from "../../../Main/Icons";
|
||||
import Popper from "../../../Main/Popper/Popper";
|
||||
import NavItem from "./NavItem";
|
||||
import { useEffect } from "react";
|
||||
|
||||
interface NavItemProps {
|
||||
activeMenu: string,
|
||||
label: string,
|
||||
submenu: {label: string | undefined, value: string}[],
|
||||
color?: string
|
||||
background?: string
|
||||
}
|
||||
|
||||
const NavSubItem: FC<NavItemProps> = ({
|
||||
activeMenu,
|
||||
label,
|
||||
color,
|
||||
background,
|
||||
submenu
|
||||
}) => {
|
||||
const { pathname } = useLocation();
|
||||
|
||||
const [openSubmenu, setOpenSubmenu] = useState(false);
|
||||
const [menuTimeout, setMenuTimeout] = useState<NodeJS.Timeout | null>(null);
|
||||
const buttonRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const handleOpenSubmenu = () => {
|
||||
setOpenSubmenu(true);
|
||||
if (menuTimeout) clearTimeout(menuTimeout);
|
||||
};
|
||||
|
||||
const handleCloseSubmenu = () => {
|
||||
setOpenSubmenu(false);
|
||||
};
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
if (menuTimeout) clearTimeout(menuTimeout);
|
||||
const timeout = setTimeout(handleCloseSubmenu, 300);
|
||||
setMenuTimeout(timeout);
|
||||
};
|
||||
|
||||
const handleMouseEnterPopup = () => {
|
||||
if (menuTimeout) clearTimeout(menuTimeout);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
handleCloseSubmenu();
|
||||
}, [pathname]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames({
|
||||
"vm-header-nav-item": true,
|
||||
"vm-header-nav-item_sub": true,
|
||||
"vm-header-nav-item_open": openSubmenu,
|
||||
"vm-header-nav-item_active": submenu.find(m => m.value === activeMenu)
|
||||
})}
|
||||
style={{ color }}
|
||||
onMouseEnter={handleOpenSubmenu}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
ref={buttonRef}
|
||||
>
|
||||
{label}
|
||||
<ArrowDropDownIcon/>
|
||||
|
||||
<Popper
|
||||
open={openSubmenu}
|
||||
placement="bottom-left"
|
||||
offset={{ top: 12, left: 0 }}
|
||||
onClose={handleCloseSubmenu}
|
||||
buttonRef={buttonRef}
|
||||
>
|
||||
<div
|
||||
className="vm-header-nav-item-submenu"
|
||||
style={{ background }}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
onMouseEnter={handleMouseEnterPopup}
|
||||
>
|
||||
{submenu.map(sm => (
|
||||
<NavItem
|
||||
key={sm.value}
|
||||
activeMenu={activeMenu}
|
||||
value={sm.value}
|
||||
label={sm.label || ""}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</Popper>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default NavSubItem;
|
||||
@@ -0,0 +1,63 @@
|
||||
@use "src/styles/variables" as *;
|
||||
|
||||
.vm-header-nav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: $padding-global;
|
||||
font-size: $font-size-small;
|
||||
font-weight: bold;
|
||||
|
||||
&-item {
|
||||
position: relative;
|
||||
padding: $padding-global $padding-small;
|
||||
opacity: 0.5;
|
||||
cursor: pointer;
|
||||
transition: opacity 200ms ease-in;
|
||||
text-transform: uppercase;
|
||||
|
||||
&_sub {
|
||||
display: grid;
|
||||
grid-template-columns: auto 14px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&_active {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
svg {
|
||||
transform: rotate(0deg);
|
||||
transition: transform 200ms ease-in;
|
||||
}
|
||||
|
||||
&_open {
|
||||
svg {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
|
||||
&-submenu {
|
||||
display: grid;
|
||||
white-space: nowrap;
|
||||
padding: $padding-small;
|
||||
color: $color-white;
|
||||
border-radius: 2px;
|
||||
opacity: 1;
|
||||
transform-origin: top center;
|
||||
font-size: $font-size-small;
|
||||
font-weight: bold;
|
||||
|
||||
&-item {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,17 +6,18 @@
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
padding: $padding-small $padding-medium;
|
||||
gap: $padding-large;
|
||||
gap: 0 $padding-large;
|
||||
z-index: 99;
|
||||
|
||||
&_app {
|
||||
padding: $padding-small 0;
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
gap: $padding-global;
|
||||
|
||||
.vm-tabs {
|
||||
gap: 0;
|
||||
&_dark {
|
||||
.vm-header-button,
|
||||
button:before,
|
||||
button {
|
||||
background-color: $color-background-block;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,7 +46,7 @@
|
||||
|
||||
&-nav {
|
||||
font-size: $font-size-small;
|
||||
font-weight: 600;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
&__settings {
|
||||
|
||||
@@ -14,7 +14,7 @@ const Layout: FC = () => {
|
||||
|
||||
const { pathname } = useLocation();
|
||||
useEffect(() => {
|
||||
const defaultTitle = "VM UI";
|
||||
const defaultTitle = "vmui";
|
||||
const routeTitle = routerOptions[pathname]?.title;
|
||||
document.title = routeTitle ? `${routeTitle} - ${defaultTitle}` : defaultTitle;
|
||||
}, [pathname]);
|
||||
|
||||
@@ -3,6 +3,7 @@ import { ReactNode } from "react";
|
||||
import classNames from "classnames";
|
||||
import { ErrorIcon, InfoIcon, SuccessIcon, WarningIcon } from "../Icons";
|
||||
import "./style.scss";
|
||||
import { useAppState } from "../../../state/common/StateContext";
|
||||
|
||||
interface AlertProps {
|
||||
variant?: "success" | "error" | "info" | "warning"
|
||||
@@ -19,12 +20,14 @@ const icons = {
|
||||
const Alert: FC<AlertProps> = ({
|
||||
variant,
|
||||
children }) => {
|
||||
const { isDarkTheme } = useAppState();
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames({
|
||||
"vm-alert": true,
|
||||
[`vm-alert_${variant}`]: variant
|
||||
[`vm-alert_${variant}`]: variant,
|
||||
"vm-alert_dark": isDarkTheme
|
||||
})}
|
||||
>
|
||||
<div className="vm-alert__icon">{icons[variant || "info"]}</div>
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
border-radius: $border-radius-medium;
|
||||
box-shadow: $box-shadow;
|
||||
font-size: $font-size-medium;
|
||||
font-weight: 500;
|
||||
font-weight: normal;
|
||||
color: $color-text;
|
||||
line-height: 20px;
|
||||
|
||||
@@ -75,4 +75,14 @@
|
||||
background-color: $color-warning;
|
||||
}
|
||||
}
|
||||
|
||||
&_dark {
|
||||
&:after {
|
||||
opacity: 0.1;
|
||||
}
|
||||
}
|
||||
|
||||
&_dark &__content {
|
||||
filter: none;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ $button-radius: 6px;
|
||||
padding: 6px 14px;
|
||||
font-size: $font-size-small;
|
||||
line-height: 15px;
|
||||
font-weight: 500;
|
||||
font-weight: normal;
|
||||
min-height: 31px;
|
||||
border-radius: $button-radius;
|
||||
color: $color-white;
|
||||
@@ -21,7 +21,7 @@ $button-radius: 6px;
|
||||
white-space: nowrap;
|
||||
|
||||
&:hover:after {
|
||||
background-color: rgba($color-black, 0.05);
|
||||
background-color: $color-hover-black;
|
||||
}
|
||||
|
||||
&:before,
|
||||
|
||||
@@ -104,7 +104,7 @@
|
||||
transition: color 200ms ease, background-color 300ms ease-in-out;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba($color-black, 0.05);
|
||||
background-color: $color-hover-black;
|
||||
}
|
||||
|
||||
&_empty {
|
||||
@@ -144,7 +144,7 @@
|
||||
transition: color 200ms ease, background-color 300ms ease-in-out;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba($color-black, 0.05);
|
||||
background-color: $color-hover-black;
|
||||
}
|
||||
|
||||
&_selected {
|
||||
@@ -270,6 +270,10 @@
|
||||
justify-content: space-between;
|
||||
margin-top: $padding-global;
|
||||
|
||||
&_dark &__input {
|
||||
border-color: $color-text-disabled;
|
||||
}
|
||||
|
||||
span {
|
||||
margin: 0 $padding-small;
|
||||
}
|
||||
@@ -277,11 +281,13 @@
|
||||
&__input {
|
||||
width: 64px;
|
||||
height: 32px;
|
||||
border: 1px solid $color-alto;
|
||||
border: $border-divider;
|
||||
border-radius: $border-radius-small;
|
||||
font-size: $font-size-medium;
|
||||
padding: 2px $padding-small;
|
||||
text-align: center;
|
||||
background-color: transparent;
|
||||
color: $color-text;
|
||||
|
||||
&:focus {
|
||||
border-color: $color-primary;
|
||||
|
||||
@@ -2,6 +2,7 @@ import React, { FC, useEffect, useMemo, useRef, useState } from "preact/compat";
|
||||
import { Dayjs } from "dayjs";
|
||||
import { FormEvent, FocusEvent } from "react";
|
||||
import classNames from "classnames";
|
||||
import { useAppState } from "../../../../state/common/StateContext";
|
||||
|
||||
interface CalendarTimepickerProps {
|
||||
selectDate: Dayjs
|
||||
@@ -13,6 +14,7 @@ enum TimeUnits { hour, minutes, seconds }
|
||||
|
||||
|
||||
const TimePicker: FC<CalendarTimepickerProps>= ({ selectDate, onChangeTime, onClose }) => {
|
||||
const { isDarkTheme } = useAppState();
|
||||
|
||||
const [activeField, setActiveField] = useState<TimeUnits>(TimeUnits.hour);
|
||||
const [hours, setHours] = useState(selectDate.format("HH"));
|
||||
@@ -154,7 +156,12 @@ const TimePicker: FC<CalendarTimepickerProps>= ({ selectDate, onChangeTime, onCl
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="vm-calendar-time-picker-fields">
|
||||
<div
|
||||
className={classNames({
|
||||
"vm-calendar-time-picker-fields": true,
|
||||
"vm-calendar-time-picker-fields_dark": isDarkTheme
|
||||
})}
|
||||
>
|
||||
<input
|
||||
className="vm-calendar-time-picker-fields__input"
|
||||
value={hours}
|
||||
|
||||
@@ -344,3 +344,59 @@ export const TimelineIcon = () => (
|
||||
></path>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const WikiIcon = () => (
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M21 5C19.89 4.65 18.67 4.5 17.5 4.5C15.55 4.5 13.45 4.9 12 6C10.55 4.9 8.45 4.5 6.5 4.5C5.33 4.5 4.11 4.65 3 5C2.25 5.25 1.6 5.55 1 6V20.6C1 20.85 1.25 21.1 1.5 21.1C1.6 21.1 1.65 21.1 1.75 21.05C3.15 20.3 4.85 20 6.5 20C8.2 20 10.65 20.65 12 21.5C13.35 20.65 15.8 20 17.5 20C19.15 20 20.85 20.3 22.25 21.05C22.35 21.1 22.4 21.1 22.5 21.1C22.75 21.1 23 20.85 23 20.6V6C22.4 5.55 21.75 5.25 21 5ZM21 18.5C19.9 18.15 18.7 18 17.5 18C15.8 18 13.35 18.65 12 19.5C10.65 18.65 8.2 18 6.5 18C5.3 18 4.1 18.15 3 18.5V7C4.1 6.65 5.3 6.5 6.5 6.5C8.2 6.5 10.65 7.15 12 8C13.35 7.15 15.8 6.5 17.5 6.5C18.7 6.5 19.9 6.65 21 7V18.5Z"
|
||||
/>
|
||||
<path
|
||||
d="M17.5 10.5C18.38 10.5 19.23 10.59 20 10.76V9.24C19.21 9.09 18.36 9 17.5 9C15.8 9 14.26 9.29 13 9.83V11.49C14.13 10.85 15.7 10.5 17.5 10.5ZM13 12.49V14.15C14.13 13.51 15.7 13.16 17.5 13.16C18.38 13.16 19.23 13.25 20 13.42V11.9C19.21 11.75 18.36 11.66 17.5 11.66C15.8 11.66 14.26 11.96 13 12.49ZM17.5 14.33C15.8 14.33 14.26 14.62 13 15.16V16.82C14.13 16.18 15.7 15.83 17.5 15.83C18.38 15.83 19.23 15.92 20 16.09V14.57C19.21 14.41 18.36 14.33 17.5 14.33Z"
|
||||
/>
|
||||
<path
|
||||
d="M6.5 10.5C5.62 10.5 4.77 10.59 4 10.76V9.24C4.79 9.09 5.64 9 6.5 9C8.2 9 9.74 9.29 11 9.83V11.49C9.87 10.85 8.3 10.5 6.5 10.5ZM11 12.49V14.15C9.87 13.51 8.3 13.16 6.5 13.16C5.62 13.16 4.77 13.25 4 13.42V11.9C4.79 11.75 5.64 11.66 6.5 11.66C8.2 11.66 9.74 11.96 11 12.49ZM6.5 14.33C8.2 14.33 9.74 14.62 11 15.16V16.82C9.87 16.18 8.3 15.83 6.5 15.83C5.62 15.83 4.77 15.92 4 16.09V14.57C4.79 14.41 5.64 14.33 6.5 14.33Z"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
);
|
||||
|
||||
export const IssueIcon = () => (
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path
|
||||
d="M12 2C6.49 2 2 6.49 2 12s4.49 10 10 10 10-4.49 10-10S17.51 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm3-8c0 1.66-1.34 3-3 3s-3-1.34-3-3 1.34-3 3-3 3 1.34 3 3z"
|
||||
></path>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const QuestionIcon = () => (
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M12 2C6.48 2 2 6.48 2 12C2 17.52 6.48 22 12 22C17.52 22 22 17.52 22 12C22 6.48 17.52 2 12 2ZM12 6C9.79 6 8 7.79 8 10H10C10 8.9 10.9 8 12 8C13.1 8 14 8.9 14 10C14 10.8792 13.4202 11.3236 12.7704 11.8217C11.9421 12.4566 11 13.1787 11 15H13C13 13.9046 13.711 13.2833 14.4408 12.6455C15.21 11.9733 16 11.2829 16 10C16 7.79 14.21 6 12 6ZM13 16V18H11V16H13Z"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
);
|
||||
|
||||
export const StorageIcons = () => (
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path
|
||||
d="M4 20h16c1.1 0 2-.9 2-2s-.9-2-2-2H4c-1.1 0-2 .9-2 2s.9 2 2 2zm0-3h2v2H4v-2zM2 6c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2s-.9-2-2-2H4c-1.1 0-2 .9-2 2zm4 1H4V5h2v2zm-2 7h16c1.1 0 2-.9 2-2s-.9-2-2-2H4c-1.1 0-2 .9-2 2s.9 2 2 2zm0-3h2v2H4v-2z"
|
||||
></path>
|
||||
</svg>
|
||||
);
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
&-track {
|
||||
width: 100%;
|
||||
height: 20px;
|
||||
background-color: rgba($color-black, 0.05);
|
||||
background-color: $color-hover-black;
|
||||
border-radius: $border-radius-small;
|
||||
|
||||
&__thumb {
|
||||
|
||||
@@ -16,9 +16,11 @@ $padding-modal: 22px;
|
||||
|
||||
&-content {
|
||||
padding: $padding-modal;
|
||||
background: $color-white;
|
||||
background: $color-background-block;
|
||||
box-shadow: 0 0 24px rgba($color-black, 0.07);
|
||||
border-radius: $border-radius-small;
|
||||
max-height: 90vh;
|
||||
overflow: auto;
|
||||
|
||||
&-header {
|
||||
display: grid;
|
||||
|
||||
@@ -3,6 +3,7 @@ import classNames from "classnames";
|
||||
import { ArrowDropDownIcon, CloseIcon } from "../Icons";
|
||||
import { FormEvent, MouseEvent } from "react";
|
||||
import Autocomplete from "../Autocomplete/Autocomplete";
|
||||
import { useAppState } from "../../../state/common/StateContext";
|
||||
import "./style.scss";
|
||||
|
||||
interface SelectProps {
|
||||
@@ -26,6 +27,7 @@ const Select: FC<SelectProps> = ({
|
||||
autofocus,
|
||||
onChange
|
||||
}) => {
|
||||
const { isDarkTheme } = useAppState();
|
||||
|
||||
const [search, setSearch] = useState("");
|
||||
const autocompleteAnchorEl = useRef<HTMLDivElement>(null);
|
||||
@@ -106,7 +108,12 @@ const Select: FC<SelectProps> = ({
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="vm-select">
|
||||
<div
|
||||
className={classNames({
|
||||
"vm-select": true,
|
||||
"vm-select_dark": isDarkTheme
|
||||
})}
|
||||
>
|
||||
<div
|
||||
className="vm-select-input"
|
||||
onClick={handleToggleList}
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: rgba($color-black, 0.06);
|
||||
background-color: $color-hover-black;
|
||||
padding: 2px 2px 2px 6px;
|
||||
border-radius: $border-radius-small;
|
||||
font-size: $font-size;
|
||||
@@ -60,6 +60,8 @@
|
||||
z-index: 2;
|
||||
min-width: 100px;
|
||||
flex-grow: 1;
|
||||
background-color: transparent;
|
||||
color: $color-text;
|
||||
|
||||
&:placeholder-shown {
|
||||
width: auto;
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
line-height: 2;
|
||||
color: $color-text;
|
||||
text-align: center;
|
||||
background-color: $color-white;
|
||||
background-color: $color-background-body;
|
||||
background-repeat: repeat-x;
|
||||
border: $border-divider;
|
||||
border-radius: 4px;
|
||||
|
||||
@@ -1,22 +1,31 @@
|
||||
import React, { CSSProperties, FC } from "preact/compat";
|
||||
import "./style.scss";
|
||||
import classNames from "classnames";
|
||||
import { useAppState } from "../../../state/common/StateContext";
|
||||
|
||||
interface SpinnerProps {
|
||||
containerStyles?: CSSProperties;
|
||||
message?: string
|
||||
}
|
||||
|
||||
const Spinner: FC<SpinnerProps> = ({ containerStyles = {}, message }) => (
|
||||
<div
|
||||
className="vm-spinner"
|
||||
style={containerStyles && {}}
|
||||
>
|
||||
<div className="half-circle-spinner">
|
||||
<div className="circle circle-1"></div>
|
||||
<div className="circle circle-2"></div>
|
||||
const Spinner: FC<SpinnerProps> = ({ containerStyles = {}, message }) => {
|
||||
const { isDarkTheme } = useAppState();
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames({
|
||||
"vm-spinner": true,
|
||||
"vm-spinner_dark": isDarkTheme,
|
||||
})}
|
||||
style={containerStyles && {}}
|
||||
>
|
||||
<div className="half-circle-spinner">
|
||||
<div className="circle circle-1"></div>
|
||||
<div className="circle circle-2"></div>
|
||||
</div>
|
||||
{message && <div className="vm-spinner__message">{message}</div>}
|
||||
</div>
|
||||
{message && <div className="vm-spinner__message">{message}</div>}
|
||||
</div>
|
||||
);
|
||||
);
|
||||
};
|
||||
|
||||
export default Spinner;
|
||||
|
||||
@@ -15,6 +15,10 @@
|
||||
z-index: 99;
|
||||
animation: vm-fade 2s cubic-bezier(0.280, 0.840, 0.420, 1.1);
|
||||
|
||||
&_dark {
|
||||
background-color: rgba($color-black, 0.2);
|
||||
}
|
||||
|
||||
&__message {
|
||||
margin-top: $padding-medium;
|
||||
white-space: pre-line;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { FC, KeyboardEvent, useEffect, useRef, HTMLInputTypeAttribute, ReactNode } from "react";
|
||||
import classNames from "classnames";
|
||||
import { useMemo } from "preact/compat";
|
||||
import { useAppState } from "../../../state/common/StateContext";
|
||||
import "./style.scss";
|
||||
|
||||
interface TextFieldProps {
|
||||
@@ -38,6 +39,7 @@ const TextField: FC<TextFieldProps> = ({
|
||||
onFocus,
|
||||
onBlur
|
||||
}) => {
|
||||
const { isDarkTheme } = useAppState();
|
||||
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
@@ -81,6 +83,7 @@ const TextField: FC<TextFieldProps> = ({
|
||||
className={classNames({
|
||||
"vm-text-field": true,
|
||||
"vm-text-field_textarea": type === "textarea",
|
||||
"vm-text-field_dark": isDarkTheme
|
||||
})}
|
||||
data-replicated-value={value}
|
||||
>
|
||||
|
||||
@@ -67,6 +67,8 @@
|
||||
min-height: 34px;
|
||||
resize: none;
|
||||
overflow: hidden;
|
||||
background-color: transparent;
|
||||
color: $color-text;
|
||||
|
||||
&:focus {
|
||||
border: 1px solid $color-primary;
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
import { FC, useEffect } from "preact/compat";
|
||||
import { FC, useEffect, useState } from "preact/compat";
|
||||
import { getContrastColor } from "../../../utils/color";
|
||||
import { getCssVariable, setCssVariable } from "../../../utils/theme";
|
||||
import { getCssVariable, isSystemDark, setCssVariable } from "../../../utils/theme";
|
||||
import { AppParams, getAppModeParams } from "../../../utils/app-mode";
|
||||
import { getFromStorage } from "../../../utils/storage";
|
||||
import { darkPalette, lightPalette } from "../../../constants/palette";
|
||||
import { Theme } from "../../../types";
|
||||
import { useAppDispatch, useAppState } from "../../../state/common/StateContext";
|
||||
import useSystemTheme from "../../../hooks/useSystemTheme";
|
||||
|
||||
interface StyleVariablesProps {
|
||||
setLoadingTheme: (val: boolean) => void
|
||||
interface ThemeProviderProps {
|
||||
onLoaded: (val: boolean) => void
|
||||
}
|
||||
|
||||
const colorVariables = [
|
||||
@@ -16,9 +21,18 @@ const colorVariables = [
|
||||
"success",
|
||||
];
|
||||
|
||||
export const ThemeProvider: FC<StyleVariablesProps> = ({ setLoadingTheme }) => {
|
||||
export const ThemeProvider: FC<ThemeProviderProps> = ({ onLoaded }) => {
|
||||
|
||||
const { palette = {} } = getAppModeParams();
|
||||
const { palette: paletteAppMode = {} } = getAppModeParams();
|
||||
const { theme } = useAppState();
|
||||
const isDarkTheme = useSystemTheme();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const [palette, setPalette] = useState({
|
||||
[Theme.dark]: darkPalette,
|
||||
[Theme.light]: lightPalette,
|
||||
[Theme.system]: isSystemDark() ? darkPalette : lightPalette
|
||||
});
|
||||
|
||||
const setScrollbarSize = () => {
|
||||
const { innerWidth, innerHeight } = window;
|
||||
@@ -27,27 +41,56 @@ export const ThemeProvider: FC<StyleVariablesProps> = ({ setLoadingTheme }) => {
|
||||
setCssVariable("scrollbar-height", `${innerHeight - clientHeight}px`);
|
||||
};
|
||||
|
||||
const setAppModePalette = () => {
|
||||
colorVariables.forEach(variable => {
|
||||
const colorFromAppMode = palette[variable as keyof AppParams["palette"]];
|
||||
if (colorFromAppMode) setCssVariable(`color-${variable}`, colorFromAppMode);
|
||||
});
|
||||
};
|
||||
|
||||
const setContrastText = () => {
|
||||
colorVariables.forEach(variable => {
|
||||
colorVariables.forEach((variable, i) => {
|
||||
const color = getCssVariable(`color-${variable}`);
|
||||
const text = getContrastColor(color);
|
||||
setCssVariable(`${variable}-text`, text);
|
||||
|
||||
if (i === colorVariables.length - 1) {
|
||||
dispatch({ type: "SET_DARK_THEME" });
|
||||
onLoaded(true);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const setAppModePalette = () => {
|
||||
colorVariables.forEach(variable => {
|
||||
const colorFromAppMode = paletteAppMode[variable as keyof AppParams["palette"]];
|
||||
if (colorFromAppMode) setCssVariable(`color-${variable}`, colorFromAppMode);
|
||||
});
|
||||
|
||||
setContrastText();
|
||||
};
|
||||
|
||||
const setTheme = () => {
|
||||
const theme = (getFromStorage("THEME") || Theme.system) as Theme;
|
||||
const result = palette[theme];
|
||||
Object.entries(result).forEach(([variable, value]) => {
|
||||
setCssVariable(variable, value);
|
||||
});
|
||||
setContrastText();
|
||||
};
|
||||
|
||||
const updatePalette = () => {
|
||||
const newSystemPalette = isSystemDark() ? darkPalette : lightPalette;
|
||||
if (palette[Theme.system] === newSystemPalette) {
|
||||
setTheme();
|
||||
return;
|
||||
}
|
||||
setPalette(prev => ({
|
||||
...prev,
|
||||
[Theme.system]: newSystemPalette
|
||||
}));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setAppModePalette();
|
||||
setScrollbarSize();
|
||||
setContrastText();
|
||||
setLoadingTheme(false);
|
||||
}, []);
|
||||
setTheme();
|
||||
}, [palette]);
|
||||
|
||||
useEffect(updatePalette, [theme, isDarkTheme]);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
94
app/vmui/packages/vmui/src/components/Main/Toggle/Toggle.tsx
Normal file
94
app/vmui/packages/vmui/src/components/Main/Toggle/Toggle.tsx
Normal file
@@ -0,0 +1,94 @@
|
||||
import React, { FC, useEffect, useRef, useState } from "preact/compat";
|
||||
import classNames from "classnames";
|
||||
import { ReactNode } from "react";
|
||||
import "./style.scss";
|
||||
|
||||
interface ToggleProps {
|
||||
options: {value: string, title?: string, icon?: ReactNode}[]
|
||||
value: string
|
||||
onChange: (val: string) => void
|
||||
label?: string
|
||||
}
|
||||
|
||||
const Toggle: FC<ToggleProps> = ({ options, value, label, onChange }) => {
|
||||
|
||||
const activeRef = useRef<HTMLDivElement>(null);
|
||||
const [position, setPosition] = useState({
|
||||
width: "0px",
|
||||
left: "0px",
|
||||
borderRadius: "0px"
|
||||
});
|
||||
|
||||
const createHandlerChange = (value: string) => () => {
|
||||
onChange(value);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!activeRef.current) {
|
||||
setPosition({
|
||||
width: "0px",
|
||||
left: "0px",
|
||||
borderRadius: "0px"
|
||||
});
|
||||
return;
|
||||
}
|
||||
const index = options.findIndex(o => o.value === value);
|
||||
const { width: widthRect } = activeRef.current.getBoundingClientRect();
|
||||
|
||||
let width = widthRect;
|
||||
let left = index * width;
|
||||
let borderRadius = "0";
|
||||
if (index === 0) borderRadius = "16px 0 0 16px";
|
||||
|
||||
if (index === options.length - 1) {
|
||||
borderRadius = "10px";
|
||||
left -= 1;
|
||||
borderRadius = "0 16px 16px 0";
|
||||
}
|
||||
|
||||
if (index !== 0 && (index !== options.length - 1)) {
|
||||
width += 1;
|
||||
left -= 1;
|
||||
}
|
||||
|
||||
|
||||
setPosition({ width: `${width}px`, left: `${left}px`, borderRadius });
|
||||
}, [activeRef, value, options]);
|
||||
|
||||
return (
|
||||
<div className="vm-toggles">
|
||||
{label && (
|
||||
<label className="vm-toggles__label">
|
||||
{label}
|
||||
</label>
|
||||
)}
|
||||
<div
|
||||
className="vm-toggles-group"
|
||||
style={{ gridTemplateColumns: `repeat(${options.length}, 1fr)` }}
|
||||
>
|
||||
{position.borderRadius && <div
|
||||
className="vm-toggles-group__highlight"
|
||||
style={position}
|
||||
/>}
|
||||
{options.map((option, i) => (
|
||||
<div
|
||||
className={classNames({
|
||||
"vm-toggles-group-item": true,
|
||||
"vm-toggles-group-item_first": i === 0,
|
||||
"vm-toggles-group-item_active": option.value === value,
|
||||
"vm-toggles-group-item_icon": option.icon && option.title
|
||||
})}
|
||||
onClick={createHandlerChange(option.value)}
|
||||
key={option.value}
|
||||
ref={option.value === value ? activeRef : null}
|
||||
>
|
||||
{option.icon}
|
||||
{option.title}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Toggle;
|
||||
81
app/vmui/packages/vmui/src/components/Main/Toggle/style.scss
Normal file
81
app/vmui/packages/vmui/src/components/Main/Toggle/style.scss
Normal file
@@ -0,0 +1,81 @@
|
||||
@use "src/styles/variables" as *;
|
||||
|
||||
.vm-toggles {
|
||||
position: relative;
|
||||
display: grid;
|
||||
gap: 3px;
|
||||
width: 100%;
|
||||
|
||||
&__label {
|
||||
padding: 0 $padding-global;
|
||||
color: $color-text-secondary;
|
||||
font-size: $font-size-small;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
&-group {
|
||||
position: relative;
|
||||
display: grid;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
|
||||
&-item {
|
||||
position: relative;
|
||||
display: grid;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: $padding-small;
|
||||
border-right: $border-divider;
|
||||
border-top: $border-divider;
|
||||
border-bottom: $border-divider;
|
||||
font-size: $font-size-small;
|
||||
color: $color-text-secondary;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
transition: color 150ms ease-in;
|
||||
z-index: 2;
|
||||
user-select: none;
|
||||
|
||||
&_first {
|
||||
border-radius: 16px 0 0 16px;
|
||||
border-left: $border-divider
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-radius: 0 16px 16px 0;
|
||||
border-left: none;
|
||||
}
|
||||
|
||||
&_icon {
|
||||
grid-template-columns: 14px auto;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: $color-primary;
|
||||
}
|
||||
|
||||
&_active {
|
||||
color: $color-primary;
|
||||
border-color: transparent;
|
||||
|
||||
&:hover {
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__highlight {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
background-color: rgba($color-primary, 0.08);
|
||||
border: 1px solid $color-primary;
|
||||
transition: left 200ms cubic-bezier(0.280, 0.840, 0.420, 1), border-radius 200ms linear;
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import Trace from "../Trace";
|
||||
import { ArrowDownIcon } from "../../Main/Icons";
|
||||
import "./style.scss";
|
||||
import classNames from "classnames";
|
||||
import { useAppState } from "../../../state/common/StateContext";
|
||||
|
||||
interface RecursiveProps {
|
||||
trace: Trace;
|
||||
@@ -15,6 +16,7 @@ interface OpenLevels {
|
||||
}
|
||||
|
||||
const NestedNav: FC<RecursiveProps> = ({ trace, totalMsec }) => {
|
||||
const { isDarkTheme } = useAppState();
|
||||
const [openLevels, setOpenLevels] = useState({} as OpenLevels);
|
||||
|
||||
const handleListClick = (level: number) => () => {
|
||||
@@ -26,7 +28,12 @@ const NestedNav: FC<RecursiveProps> = ({ trace, totalMsec }) => {
|
||||
const progress = trace.duration / totalMsec * 100;
|
||||
|
||||
return (
|
||||
<div className="vm-nested-nav">
|
||||
<div
|
||||
className={classNames({
|
||||
"vm-nested-nav": true,
|
||||
"vm-nested-nav_dark": isDarkTheme,
|
||||
})}
|
||||
>
|
||||
<div
|
||||
className="vm-nested-nav-header"
|
||||
onClick={handleListClick(trace.idValue)}
|
||||
|
||||
@@ -5,6 +5,10 @@
|
||||
border-radius: $border-radius-small;
|
||||
background-color: rgba($color-tropical-blue, 0.4);
|
||||
|
||||
&_dark {
|
||||
background-color: rgba($color-black, 0.1);
|
||||
}
|
||||
|
||||
&-header {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
@@ -15,7 +19,7 @@
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba($color-black, 0.06);
|
||||
background-color: $color-hover-black;
|
||||
}
|
||||
|
||||
&__icon {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user