From 2af39a96c5d27243d65c5f8492455d862d693dbc Mon Sep 17 00:00:00 2001 From: Denys Holius <5650611+denisgolius@users.noreply.github.com> Date: Tue, 30 Mar 2021 20:43:54 +0300 Subject: [PATCH 01/63] deployment: Grafana version updated to 7.5.1 (#1161) --- deployment/docker/docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deployment/docker/docker-compose.yml b/deployment/docker/docker-compose.yml index 27208bff1..30b92563f 100644 --- a/deployment/docker/docker-compose.yml +++ b/deployment/docker/docker-compose.yml @@ -39,7 +39,7 @@ services: restart: always grafana: container_name: grafana - image: grafana/grafana:7.1.1 + image: grafana/grafana:7.5.1 depends_on: - "victoriametrics" ports: From e7fdea5953e67c8ba48058771ec18ba05a3117bb Mon Sep 17 00:00:00 2001 From: Aliaksandr Valialkin Date: Tue, 30 Mar 2021 21:38:59 +0300 Subject: [PATCH 02/63] app/vmselect: add `-search.maxStatusRequestDuration` command-line flag for limiting the duration of requests to `/api/v1/status/*` and `/api/v1/series/count` --- app/vmselect/prometheus/prometheus.go | 6 +++--- app/vmselect/searchutils/searchutils.go | 11 +++++++++-- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/app/vmselect/prometheus/prometheus.go b/app/vmselect/prometheus/prometheus.go index 8ab587d4f..f89c5fb5b 100644 --- a/app/vmselect/prometheus/prometheus.go +++ b/app/vmselect/prometheus/prometheus.go @@ -610,7 +610,7 @@ var labelValuesDuration = metrics.NewSummary(`vm_request_duration_seconds{path=" // LabelsCountHandler processes /api/v1/labels/count request. func LabelsCountHandler(startTime time.Time, w http.ResponseWriter, r *http.Request) error { - deadline := searchutils.GetDeadlineForQuery(r, startTime) + deadline := searchutils.GetDeadlineForStatusRequest(r, startTime) labelEntries, err := netstorage.GetLabelEntries(deadline) if err != nil { return fmt.Errorf(`cannot obtain label entries: %w`, err) @@ -634,7 +634,7 @@ const secsPerDay = 3600 * 24 // // See https://prometheus.io/docs/prometheus/latest/querying/api/#tsdb-stats func TSDBStatusHandler(startTime time.Time, w http.ResponseWriter, r *http.Request) error { - deadline := searchutils.GetDeadlineForQuery(r, startTime) + deadline := searchutils.GetDeadlineForStatusRequest(r, startTime) if err := r.ParseForm(); err != nil { return fmt.Errorf("cannot parse form values: %w", err) } @@ -810,7 +810,7 @@ var labelsDuration = metrics.NewSummary(`vm_request_duration_seconds{path="/api/ // SeriesCountHandler processes /api/v1/series/count request. func SeriesCountHandler(startTime time.Time, w http.ResponseWriter, r *http.Request) error { - deadline := searchutils.GetDeadlineForQuery(r, startTime) + deadline := searchutils.GetDeadlineForStatusRequest(r, startTime) n, err := netstorage.GetSeriesCount(deadline) if err != nil { return fmt.Errorf("cannot obtain series count: %w", err) diff --git a/app/vmselect/searchutils/searchutils.go b/app/vmselect/searchutils/searchutils.go index 1033f6ba1..93dc1fbcf 100644 --- a/app/vmselect/searchutils/searchutils.go +++ b/app/vmselect/searchutils/searchutils.go @@ -16,8 +16,9 @@ import ( ) var ( - maxExportDuration = flag.Duration("search.maxExportDuration", time.Hour*24*30, "The maximum duration for /api/v1/export call") - maxQueryDuration = flag.Duration("search.maxQueryDuration", time.Second*30, "The maximum duration for query execution") + maxExportDuration = flag.Duration("search.maxExportDuration", time.Hour*24*30, "The maximum duration for /api/v1/export call") + maxQueryDuration = flag.Duration("search.maxQueryDuration", time.Second*30, "The maximum duration for query execution") + maxStatusRequestDuration = flag.Duration("search.maxStatusRequestDuration", time.Minute*5, "The maximum duration for /api/v1/status/* requests") ) func roundToSeconds(ms int64) int64 { @@ -125,6 +126,12 @@ func GetDeadlineForQuery(r *http.Request, startTime time.Time) Deadline { return getDeadlineWithMaxDuration(r, startTime, dMax, "-search.maxQueryDuration") } +// GetDeadlineForStatusRequest returns deadline for the given request to /api/v1/status/*. +func GetDeadlineForStatusRequest(r *http.Request, startTime time.Time) Deadline { + dMax := maxStatusRequestDuration.Milliseconds() + return getDeadlineWithMaxDuration(r, startTime, dMax, "-search.maxStatusRequestDuration") +} + // GetDeadlineForExport returns deadline for the given request to /api/v1/export. func GetDeadlineForExport(r *http.Request, startTime time.Time) Deadline { dMax := maxExportDuration.Milliseconds() From 3be0e6b0878ed74e03c70f5d6ff217d69629c977 Mon Sep 17 00:00:00 2001 From: Aliaksandr Valialkin Date: Tue, 30 Mar 2021 21:44:39 +0300 Subject: [PATCH 03/63] docs/Single-server-VictoriaMetrics.md: update `victoria-metrics -help` output after e7fdea5953e67c8ba48058771ec18ba05a3117bb --- README.md | 2 ++ docs/Single-server-VictoriaMetrics.md | 2 ++ 2 files changed, 4 insertions(+) diff --git a/README.md b/README.md index 90a9355a7..793818290 100644 --- a/README.md +++ b/README.md @@ -1758,6 +1758,8 @@ Pass `-help` to VictoriaMetrics in order to see the list of supported command-li The maximum time the request waits for execution when -search.maxConcurrentRequests limit is reached; see also -search.maxQueryDuration (default 10s) -search.maxStalenessInterval duration The maximum interval for staleness calculations. By default it is automatically calculated from the median interval between samples. This flag could be useful for tuning Prometheus data model closer to Influx-style data model. See https://prometheus.io/docs/prometheus/latest/querying/basics/#staleness for details. See also '-search.maxLookback' flag, which has the same meaning due to historical reasons + -search.maxStatusRequestDuration duration + The maximum duration for /api/v1/status/* requests (default 5m0s) -search.maxStepForPointsAdjustment duration The maximum step when /api/v1/query_range handler adjusts points with timestamps closer than -search.latencyOffset to the current time. The adjustment is needed because such points may contain incomplete data (default 1m0s) -search.maxTagKeys int diff --git a/docs/Single-server-VictoriaMetrics.md b/docs/Single-server-VictoriaMetrics.md index 90a9355a7..793818290 100644 --- a/docs/Single-server-VictoriaMetrics.md +++ b/docs/Single-server-VictoriaMetrics.md @@ -1758,6 +1758,8 @@ Pass `-help` to VictoriaMetrics in order to see the list of supported command-li The maximum time the request waits for execution when -search.maxConcurrentRequests limit is reached; see also -search.maxQueryDuration (default 10s) -search.maxStalenessInterval duration The maximum interval for staleness calculations. By default it is automatically calculated from the median interval between samples. This flag could be useful for tuning Prometheus data model closer to Influx-style data model. See https://prometheus.io/docs/prometheus/latest/querying/basics/#staleness for details. See also '-search.maxLookback' flag, which has the same meaning due to historical reasons + -search.maxStatusRequestDuration duration + The maximum duration for /api/v1/status/* requests (default 5m0s) -search.maxStepForPointsAdjustment duration The maximum step when /api/v1/query_range handler adjusts points with timestamps closer than -search.latencyOffset to the current time. The adjustment is needed because such points may contain incomplete data (default 1m0s) -search.maxTagKeys int From 33622c409c925c11dbf5f69dafb1e40e4cfdc16a Mon Sep 17 00:00:00 2001 From: Aliaksandr Valialkin Date: Wed, 31 Mar 2021 00:44:31 +0300 Subject: [PATCH 04/63] app/vmagent/remotewrite: reduce memory usage when samples with big number of labels are sent to remote storage --- app/vmagent/remotewrite/pendingseries.go | 5 ++++- app/vmagent/remotewrite/remotewrite.go | 4 +++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/app/vmagent/remotewrite/pendingseries.go b/app/vmagent/remotewrite/pendingseries.go index bcab93fb1..23dc72a8a 100644 --- a/app/vmagent/remotewrite/pendingseries.go +++ b/app/vmagent/remotewrite/pendingseries.go @@ -27,6 +27,9 @@ var ( // the maximum number of rows to send per each block. const maxRowsPerBlock = 10000 +// the maximum number of labels to send per each block. +const maxLabelsPerBlock = 40000 + type pendingSeries struct { mu sync.Mutex wr writeRequest @@ -153,7 +156,7 @@ func (wr *writeRequest) push(src []prompbmarshal.TimeSeries) { for i := range src { tssDst = append(tssDst, prompbmarshal.TimeSeries{}) wr.copyTimeSeries(&tssDst[len(tssDst)-1], &src[i]) - if len(wr.samples) >= maxRowsPerBlock { + if len(wr.samples) >= maxRowsPerBlock || len(wr.labels) >= maxLabelsPerBlock { wr.tss = tssDst wr.flush() tssDst = wr.tss diff --git a/app/vmagent/remotewrite/remotewrite.go b/app/vmagent/remotewrite/remotewrite.go index 3a551ed0e..6c1c13b7c 100644 --- a/app/vmagent/remotewrite/remotewrite.go +++ b/app/vmagent/remotewrite/remotewrite.go @@ -151,11 +151,13 @@ func Push(wr *prompbmarshal.WriteRequest) { for len(tss) > 0 { // Process big tss in smaller blocks in order to reduce the maximum memory usage samplesCount := 0 + labelsCount := 0 i := 0 for i < len(tss) { samplesCount += len(tss[i].Samples) + labelsCount += len(tss[i].Labels) i++ - if samplesCount > maxRowsPerBlock { + if samplesCount >= maxRowsPerBlock || labelsCount >= maxLabelsPerBlock { break } } From f888d194dafe5f0924cca5d992c74d56fc0e0a9c Mon Sep 17 00:00:00 2001 From: Aliaksandr Valialkin Date: Wed, 31 Mar 2021 15:14:35 +0300 Subject: [PATCH 05/63] docs/Articles.md: add a link to https://blog.kintone.io/entry/2021/03/31/175256 --- docs/Articles.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/Articles.md b/docs/Articles.md index deab85c77..a17cbc919 100644 --- a/docs/Articles.md +++ b/docs/Articles.md @@ -30,6 +30,7 @@ * [Monitoring with Prometheus, Grafana, AlertManager and VictoriaMetrics](https://www.sensedia.com/post/monitoring-with-prometheus-alertmanager) * [Solving Metrics at scale with VictoriaMetrics](https://www.youtube.com/watch?v=QgLMztnj7-8) * [Monitoring Kubernetes clusters with VictoriaMetrics and Grafana](https://blog.cybozu.io/entry/2021/03/18/115743) +* [Multi-tenancy monitoring system for Kubernetes cluster using VictoriaMetrics and operators](https://blog.kintone.io/entry/2021/03/31/175256) ## Our articles From 48275d8c1287b153f1bd5ddeb6ba0db17417c724 Mon Sep 17 00:00:00 2001 From: Aliaksandr Valialkin Date: Wed, 31 Mar 2021 16:16:26 +0300 Subject: [PATCH 06/63] app/vmagent/remotewrite: reduce memory usage when `-remoteWrite.queues` is set to a big value See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/1167 --- app/vmagent/remotewrite/remotewrite.go | 8 +++++++- docs/CHANGELOG.md | 2 ++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/app/vmagent/remotewrite/remotewrite.go b/app/vmagent/remotewrite/remotewrite.go index 6c1c13b7c..7961cb560 100644 --- a/app/vmagent/remotewrite/remotewrite.go +++ b/app/vmagent/remotewrite/remotewrite.go @@ -210,7 +210,13 @@ func newRemoteWriteCtx(argIdx int, remoteWriteURL string, maxInmemoryBlocks int, c := newClient(argIdx, remoteWriteURL, sanitizedURL, fq, *queues) sf := significantFigures.GetOptionalArgOrDefault(argIdx, 0) rd := roundDigits.GetOptionalArgOrDefault(argIdx, 100) - pss := make([]*pendingSeries, *queues) + pssLen := *queues + if n := cgroup.AvailableCPUs(); pssLen > n { + // There is no sense in running more than availableCPUs concurrent pendingSeries, + // since every pendingSeries can saturate up to a single CPU. + pssLen = n + } + pss := make([]*pendingSeries, pssLen) for i := range pss { pss[i] = newPendingSeries(fq.MustWriteBlock, sf, rd) } diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index d50bf273d..c72b82f73 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -2,6 +2,8 @@ # tip +* FEATURE: vmagent: reduce memory usage when `-remoteWrite.queues` is set to a big value. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/1167). + # [v1.57.1](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.57.1) From db963205cc457cc5ec157ae1eb6dd7182ebac357 Mon Sep 17 00:00:00 2001 From: Lapo Luchini Date: Wed, 31 Mar 2021 16:42:08 +0200 Subject: [PATCH 07/63] Add `vmutils-pure` target (#1163) --- Makefile | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Makefile b/Makefile index a92100797..736896a57 100644 --- a/Makefile +++ b/Makefile @@ -53,6 +53,14 @@ vmutils: \ vmrestore \ vmctl +vmutils-pure: \ + vmagent-pure \ + vmalert-pure \ + vmauth-pure \ + vmbackup-pure \ + vmrestore-pure \ + vmctl-pure + vmutils-arm64: \ vmagent-arm64 \ vmalert-arm64 \ From e1f699bb6ca22f31030ea6802c112a5072cc9b97 Mon Sep 17 00:00:00 2001 From: Aliaksandr Valialkin Date: Wed, 31 Mar 2021 21:22:40 +0300 Subject: [PATCH 08/63] lib/storage: reduce memory usage when ingesting samples for the same time series with distinct order of labels --- app/vmstorage/main.go | 3 ++ docs/CHANGELOG.md | 1 + lib/storage/storage.go | 66 +++++++++++++++++++++++++----------------- 3 files changed, 43 insertions(+), 27 deletions(-) diff --git a/app/vmstorage/main.go b/app/vmstorage/main.go index 731fc0002..b23140a8d 100644 --- a/app/vmstorage/main.go +++ b/app/vmstorage/main.go @@ -557,6 +557,9 @@ func registerStorageMetrics() { return float64(m().SearchDelays) }) + metrics.NewGauge(`vm_sorted_row_labels_inserts_total`, func() float64 { + return float64(m().SortedRowLabelsInserts) + }) metrics.NewGauge(`vm_slow_row_inserts_total`, func() float64 { return float64(m().SlowRowInserts) }) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index c72b82f73..00fea8f42 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -2,6 +2,7 @@ # tip +* FEATURE: reduce the size of `MetricName -> internal_series_id` cache (aka `vm_cache_size_bytes{type="storage/tsid"}`) when ingesting samples for the same time series with distinct order of labels. For example, `foo{k1="v1",k2="v2"}` and `foo{k2="v2",k1="v1"}` represent a single time series. Previously VictoriaMetrics could need additional memory when ingesting such samples. The number of ingested samples with distinct order of labels for the same time series can be monitored with `vm_sorted_row_labels_inserts_total` metric. * FEATURE: vmagent: reduce memory usage when `-remoteWrite.queues` is set to a big value. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/1167). diff --git a/lib/storage/storage.go b/lib/storage/storage.go index 48af275c1..8300400d5 100644 --- a/lib/storage/storage.go +++ b/lib/storage/storage.go @@ -48,6 +48,7 @@ type Storage struct { searchTSIDsConcurrencyLimitReached uint64 searchTSIDsConcurrencyLimitTimeout uint64 + sortedRowLabelsInserts uint64 slowRowInserts uint64 slowPerDayIndexInserts uint64 slowMetricNameLoads uint64 @@ -358,6 +359,7 @@ type Metrics struct { SearchDelays uint64 + SortedRowLabelsInserts uint64 SlowRowInserts uint64 SlowPerDayIndexInserts uint64 SlowMetricNameLoads uint64 @@ -427,6 +429,7 @@ func (s *Storage) UpdateMetrics(m *Metrics) { m.SearchDelays = storagepacelimiter.Search.DelaysTotal() + m.SortedRowLabelsInserts += atomic.LoadUint64(&s.sortedRowLabelsInserts) m.SlowRowInserts += atomic.LoadUint64(&s.slowRowInserts) m.SlowPerDayIndexInserts += atomic.LoadUint64(&s.slowPerDayIndexInserts) m.SlowMetricNameLoads += atomic.LoadUint64(&s.slowMetricNameLoads) @@ -1318,6 +1321,8 @@ func (s *Storage) ForceMergePartitions(partitionNamePrefix string) error { var rowsAddedTotal uint64 // AddRows adds the given mrs to s. +// +// AddRows can modify mrs contents. func (s *Storage) AddRows(mrs []MetricRow, precisionBits uint8) error { if len(mrs) == 0 { return nil @@ -1442,6 +1447,9 @@ func (s *Storage) add(rows []rawRow, mrs []MetricRow, precisionBits uint8) ([]ra prevMetricNameRaw []byte ) var pmrs *pendingMetricRows + var mn MetricName + var metricNameRawSorted []byte + var sortedRowLabelsInserts uint64 minTimestamp, maxTimestamp := s.tb.getMinMaxTimestamps() // Return only the first error, since it has no sense in returning all errors. var firstWarn error @@ -1485,7 +1493,7 @@ func (s *Storage) add(rows []rawRow, mrs []MetricRow, precisionBits uint8) ([]ra continue } if s.getTSIDFromCache(&r.TSID, mr.MetricNameRaw) { - // Fast path - the TSID for the given MetricName has been found in cache and isn't deleted. + // Fast path - the TSID for the given MetricNameRaw has been found in cache and isn't deleted. // There is no need in checking whether r.TSID.MetricID is deleted, since tsidCache doesn't // contain MetricName->TSID entries for deleted time series. // See Storage.DeleteMetrics code for details. @@ -1494,22 +1502,40 @@ func (s *Storage) add(rows []rawRow, mrs []MetricRow, precisionBits uint8) ([]ra continue } + // Slower path - sort labels in MetricNameRaw and check the cache again. + // This should limit the number of cache entries for metrics with distinct order of labels to 1. + if err := mn.unmarshalRaw(mr.MetricNameRaw); err != nil { + if firstWarn == nil { + firstWarn = fmt.Errorf("cannot unmarshal MetricNameRaw %q: %w", mr.MetricNameRaw, err) + } + j-- + continue + } + mn.sortTags() + metricNameRawSorted = mn.marshalRaw(metricNameRawSorted[:0]) + if s.getTSIDFromCache(&r.TSID, metricNameRawSorted) { + // The TSID for the given metricNameRawSorted has been found in cache and isn't deleted. + // There is no need in checking whether r.TSID.MetricID is deleted, since tsidCache doesn't + // contain MetricName->TSID entries for deleted time series. + // See Storage.DeleteMetrics code for details. + sortedRowLabelsInserts++ + prevTSID = r.TSID + prevMetricNameRaw = mr.MetricNameRaw + continue + } + // Slow path - the TSID is missing in the cache. // Postpone its search in the loop below. j-- if pmrs == nil { pmrs = getPendingMetricRows() } - if err := pmrs.addRow(mr); err != nil { - // Do not stop adding rows on error - just skip invalid row. - // This guarantees that invalid rows don't prevent - // from adding valid rows into the storage. - if firstWarn == nil { - firstWarn = err - } - continue + if string(mr.MetricNameRaw) != string(metricNameRawSorted) { + mr.MetricNameRaw = append(mr.MetricNameRaw[:0], metricNameRawSorted...) } + pmrs.addRow(mr, &mn) } + atomic.AddUint64(&s.sortedRowLabelsInserts, sortedRowLabelsInserts) if pmrs != nil { // Sort pendingMetricRows by canonical metric name in order to speed up search via `is` in the loop below. pendingMetricRows := pmrs.pmrs @@ -1533,15 +1559,6 @@ func (s *Storage) add(rows []rawRow, mrs []MetricRow, precisionBits uint8) ([]ra r.TSID = prevTSID continue } - if s.getTSIDFromCache(&r.TSID, mr.MetricNameRaw) { - // Fast path - the TSID for the given MetricName has been found in cache and isn't deleted. - // There is no need in checking whether r.TSID.MetricID is deleted, since tsidCache doesn't - // contain MetricName->TSID entries for deleted time series. - // See Storage.DeleteMetrics code for details. - prevTSID = r.TSID - prevMetricNameRaw = mr.MetricNameRaw - continue - } slowInsertsCount++ if err := is.GetOrCreateTSIDByName(&r.TSID, pmr.MetricName); err != nil { // Do not stop adding rows on error - just skip invalid row. @@ -1554,6 +1571,8 @@ func (s *Storage) add(rows []rawRow, mrs []MetricRow, precisionBits uint8) ([]ra continue } s.putTSIDToCache(&r.TSID, mr.MetricNameRaw) + prevTSID = r.TSID + prevMetricNameRaw = mr.MetricNameRaw } idb.putIndexSearch(is) putPendingMetricRows(pmrs) @@ -1596,7 +1615,6 @@ type pendingMetricRows struct { lastMetricNameRaw []byte lastMetricName []byte - mn MetricName } func (pmrs *pendingMetricRows) reset() { @@ -1608,19 +1626,14 @@ func (pmrs *pendingMetricRows) reset() { pmrs.metricNamesBuf = pmrs.metricNamesBuf[:0] pmrs.lastMetricNameRaw = nil pmrs.lastMetricName = nil - pmrs.mn.Reset() } -func (pmrs *pendingMetricRows) addRow(mr *MetricRow) error { +func (pmrs *pendingMetricRows) addRow(mr *MetricRow, mn *MetricName) { // Do not spend CPU time on re-calculating canonical metricName during bulk import // of many rows for the same metric. if string(mr.MetricNameRaw) != string(pmrs.lastMetricNameRaw) { - if err := pmrs.mn.unmarshalRaw(mr.MetricNameRaw); err != nil { - return fmt.Errorf("cannot unmarshal MetricNameRaw %q: %w", mr.MetricNameRaw, err) - } - pmrs.mn.sortTags() metricNamesBufLen := len(pmrs.metricNamesBuf) - pmrs.metricNamesBuf = pmrs.mn.Marshal(pmrs.metricNamesBuf) + pmrs.metricNamesBuf = mn.Marshal(pmrs.metricNamesBuf) pmrs.lastMetricName = pmrs.metricNamesBuf[metricNamesBufLen:] pmrs.lastMetricNameRaw = mr.MetricNameRaw } @@ -1628,7 +1641,6 @@ func (pmrs *pendingMetricRows) addRow(mr *MetricRow) error { MetricName: pmrs.lastMetricName, mr: *mr, }) - return nil } func getPendingMetricRows() *pendingMetricRows { From dc9eafcd023d569b2283f9c77bfdecbbf79639f8 Mon Sep 17 00:00:00 2001 From: Aliaksandr Valialkin Date: Wed, 31 Mar 2021 23:12:56 +0300 Subject: [PATCH 09/63] app/{vminsert,vmagent}: add `-sortLabels` command-line option for sorting time series labels before ingesting them in the storage This option can be useful when samples for the same time series are ingested with distinct order of labels. For example, metric{k1="v1",k2="v2"} and metric{k2="v2",k1="v1"}. --- README.md | 13 +++-- app/vmagent/README.md | 2 + app/vmagent/remotewrite/pendingseries.go | 1 + app/vmagent/remotewrite/sort_labels.go | 51 ++++++++++++++++++ app/vminsert/common/insert_ctx.go | 2 +- app/vminsert/common/sort_labels.go | 32 +++++++++++ app/vminsert/csvimport/request_handler.go | 1 + app/vminsert/graphite/request_handler.go | 1 + app/vminsert/influx/request_handler.go | 2 + app/vminsert/native/request_handler.go | 1 + app/vminsert/opentsdb/request_handler.go | 1 + app/vminsert/opentsdbhttp/request_handler.go | 1 + .../prometheusimport/request_handler.go | 1 + app/vminsert/prompush/push.go | 1 + .../promremotewrite/request_handler.go | 1 + app/vminsert/vmimport/request_handler.go | 1 + app/vmstorage/main.go | 3 -- docs/CHANGELOG.md | 2 +- docs/Single-server-VictoriaMetrics.md | 13 +++-- docs/vmagent.md | 2 + lib/storage/storage.go | 53 ++++++------------- 21 files changed, 136 insertions(+), 49 deletions(-) create mode 100644 app/vmagent/remotewrite/sort_labels.go create mode 100644 app/vminsert/common/sort_labels.go diff --git a/README.md b/README.md index 793818290..aed76c314 100644 --- a/README.md +++ b/README.md @@ -1324,6 +1324,8 @@ See the example of alerting rules for VM components [here](https://github.com/Vi * It is recommended to use default command-line flag values (i.e. don't set them explicitly) until the need of tweaking these flag values arises. +* It is recommended inspecting logs during troubleshooting, since they may contain useful information. + * It is recommended upgrading to the latest available release from [this page](https://github.com/VictoriaMetrics/VictoriaMetrics/releases), since the encountered issue could be already fixed there. @@ -1338,8 +1340,6 @@ See the example of alerting rules for VM components [here](https://github.com/Vi if background merge cannot be initiated due to free disk space shortage. The value shows the number of per-month partitions, which would start background merge if they had more free disk space. -* It is recommended inspecting logs during troubleshooting, since they may contain useful information. - * VictoriaMetrics buffers incoming data in memory for up to a few seconds before flushing it to persistent storage. This may lead to the following "issues": * Data becomes available for querying in a few seconds after inserting. It is possible to flush in-memory buffers to persistent storage @@ -1349,10 +1349,13 @@ See the example of alerting rules for VM components [here](https://github.com/Vi * If VictoriaMetrics works slowly and eats more than a CPU core per 100K ingested data points per second, then it is likely you have too many active time series for the current amount of RAM. - VictoriaMetrics [exposes](#monitoring) `vm_slow_*` metrics, which could be used as an indicator of low amounts of RAM. - It is recommended increasing the amount of RAM on the node with VictoriaMetrics in order to improve + VictoriaMetrics [exposes](#monitoring) `vm_slow_*` metrics such as `vm_slow_row_inserts_total` and `vm_slow_metric_name_loads_total`, which could be used + as an indicator of low amounts of RAM. It is recommended increasing the amount of RAM on the node with VictoriaMetrics in order to improve ingestion and query performance in this case. +* If the order of labels for the same metrics can change over time (e.g. if `metric{k1="v1",k2="v2"}` may become `metric{k2="v2",k1="v1"}`), + then it is recommended running VictoriaMetrics with `-sortLabels` command-line flag in order to reduce memory usage and CPU usage. + * VictoriaMetrics prioritizes data ingestion over data querying. So if it has no enough resources for data ingestion, then data querying may slow down significantly. @@ -1790,6 +1793,8 @@ Pass `-help` to VictoriaMetrics in order to see the list of supported command-li The maximum number of CPU cores to use for small merges. Default value is used if set to 0 -snapshotAuthKey string authKey, which must be passed in query string to /snapshot* pages + -sortLabels + Whether to sort labels for incoming samples before writing them to storage. This may be needed for reducing memory usage at storage when the order of labels in incoming samples is random. For example, if m{k1="v1",k2="v2"} may be sent as m{k2="v2",k1="v1"}. Enabled sorting for labels can slow down ingestion performance a bit -storageDataPath string Path to storage data (default "victoria-metrics-data") -tls diff --git a/app/vmagent/README.md b/app/vmagent/README.md index cf52fb800..815a0d030 100644 --- a/app/vmagent/README.md +++ b/app/vmagent/README.md @@ -702,6 +702,8 @@ See the docs at https://victoriametrics.github.io/vmagent.html . -remoteWrite.urlRelabelConfig array Optional path to relabel config for the corresponding -remoteWrite.url Supports array of values separated by comma or specified via multiple flags. + -sortLabels + Whether to sort labels for incoming samples before writing them to all the configured remote storage systems. This may be needed for reducing memory usage at remote storage when the order of labels in incoming samples is random. For example, if m{k1="v1",k2="v2"} may be sent as m{k2="v2",k1="v1"}Enabled sorting for labels can slow down ingestion performance a bit -tls Whether to enable TLS (aka HTTPS) for incoming requests. -tlsCertFile and -tlsKeyFile must be set if -tls is set -tlsCertFile string diff --git a/app/vmagent/remotewrite/pendingseries.go b/app/vmagent/remotewrite/pendingseries.go index 23dc72a8a..2d6cf12c0 100644 --- a/app/vmagent/remotewrite/pendingseries.go +++ b/app/vmagent/remotewrite/pendingseries.go @@ -128,6 +128,7 @@ func (wr *writeRequest) reset() { } func (wr *writeRequest) flush() { + sortLabelsIfNeeded(wr.tss) wr.wr.Timeseries = wr.tss wr.adjustSampleValues() atomic.StoreUint64(&wr.lastFlushTime, fasttime.UnixTimestamp()) diff --git a/app/vmagent/remotewrite/sort_labels.go b/app/vmagent/remotewrite/sort_labels.go new file mode 100644 index 000000000..e9ec252bd --- /dev/null +++ b/app/vmagent/remotewrite/sort_labels.go @@ -0,0 +1,51 @@ +package remotewrite + +import ( + "flag" + "sort" + "sync" + + "github.com/VictoriaMetrics/VictoriaMetrics/lib/prompbmarshal" +) + +var sortLabels = flag.Bool("sortLabels", false, `Whether to sort labels for incoming samples before writing them to all the configured remote storage systems. `+ + `This may be needed for reducing memory usage at remote storage when the order of labels in incoming samples is random. `+ + `For example, if m{k1="v1",k2="v2"} may be sent as m{k2="v2",k1="v1"}`+ + `Enabled sorting for labels can slow down ingestion performance a bit`) + +// sortLabelsIfNeeded sorts labels if -sortLabels command-line flag is set. +func sortLabelsIfNeeded(tss []prompbmarshal.TimeSeries) { + if !*sortLabels { + return + } + // The slc is used for avoiding memory allocation when passing labels to sort.Sort. + slc := sortLabelsCtxPool.Get().(*sortLabelsCtx) + for i := range tss { + slc.labels = tss[i].Labels + sort.Sort(&slc.labels) + } + slc.labels = nil + sortLabelsCtxPool.Put(slc) +} + +type sortLabelsCtx struct { + labels sortedLabels +} + +var sortLabelsCtxPool = &sync.Pool{ + New: func() interface{} { + return &sortLabelsCtx{} + }, +} + +type sortedLabels []prompbmarshal.Label + +func (sl *sortedLabels) Len() int { return len(*sl) } +func (sl *sortedLabels) Less(i, j int) bool { + a := *sl + return a[i].Name < a[j].Name +} +func (sl *sortedLabels) Swap(i, j int) { + a := *sl + a[i], a[j] = a[j], a[i] +} diff --git a/app/vminsert/common/insert_ctx.go b/app/vminsert/common/insert_ctx.go index ce6ff849f..e62cf267c 100644 --- a/app/vminsert/common/insert_ctx.go +++ b/app/vminsert/common/insert_ctx.go @@ -14,7 +14,7 @@ import ( // InsertCtx contains common bits for data points insertion. type InsertCtx struct { - Labels []prompb.Label + Labels sortedLabels mrs []storage.MetricRow metricNamesBuf []byte diff --git a/app/vminsert/common/sort_labels.go b/app/vminsert/common/sort_labels.go new file mode 100644 index 000000000..16fa88cc0 --- /dev/null +++ b/app/vminsert/common/sort_labels.go @@ -0,0 +1,32 @@ +package common + +import ( + "flag" + "sort" + + "github.com/VictoriaMetrics/VictoriaMetrics/lib/prompb" +) + +var sortLabels = flag.Bool("sortLabels", false, `Whether to sort labels for incoming samples before writing them to storage. `+ + `This may be needed for reducing memory usage at storage when the order of labels in incoming samples is random. `+ + `For example, if m{k1="v1",k2="v2"} may be sent as m{k2="v2",k1="v1"}. `+ + `Enabled sorting for labels can slow down ingestion performance a bit`) + +// SortLabelsIfNeeded sorts labels if -sortLabels command-line flag is set +func (ctx *InsertCtx) SortLabelsIfNeeded() { + if *sortLabels { + sort.Sort(&ctx.Labels) + } +} + +type sortedLabels []prompb.Label + +func (sl *sortedLabels) Len() int { return len(*sl) } +func (sl *sortedLabels) Less(i, j int) bool { + a := *sl + return string(a[i].Name) < string(a[j].Name) +} +func (sl *sortedLabels) Swap(i, j int) { + a := *sl + a[i], a[j] = a[j], a[i] +} diff --git a/app/vminsert/csvimport/request_handler.go b/app/vminsert/csvimport/request_handler.go index fc858936e..6997dd4cb 100644 --- a/app/vminsert/csvimport/request_handler.go +++ b/app/vminsert/csvimport/request_handler.go @@ -55,6 +55,7 @@ func insertRows(rows []parser.Row, extraLabels []prompbmarshal.Label) error { // Skip metric without labels. continue } + ctx.SortLabelsIfNeeded() if err := ctx.WriteDataPoint(nil, ctx.Labels, r.Timestamp, r.Value); err != nil { return err } diff --git a/app/vminsert/graphite/request_handler.go b/app/vminsert/graphite/request_handler.go index 84532485c..5aa24f929 100644 --- a/app/vminsert/graphite/request_handler.go +++ b/app/vminsert/graphite/request_handler.go @@ -45,6 +45,7 @@ func insertRows(rows []parser.Row) error { // Skip metric without labels. continue } + ctx.SortLabelsIfNeeded() if err := ctx.WriteDataPoint(nil, ctx.Labels, r.Timestamp, r.Value); err != nil { return err } diff --git a/app/vminsert/influx/request_handler.go b/app/vminsert/influx/request_handler.go index 784c2c3ec..f1c81bf80 100644 --- a/app/vminsert/influx/request_handler.go +++ b/app/vminsert/influx/request_handler.go @@ -117,11 +117,13 @@ func insertRows(db string, rows []parser.Row, extraLabels []prompbmarshal.Label) // Skip metric without labels. continue } + ic.SortLabelsIfNeeded() if err := ic.WriteDataPoint(nil, ic.Labels, r.Timestamp, f.Value); err != nil { return err } } } else { + ic.SortLabelsIfNeeded() ctx.metricNameBuf = storage.MarshalMetricNameRaw(ctx.metricNameBuf[:0], ic.Labels) labelsLen := len(ic.Labels) for j := range r.Fields { diff --git a/app/vminsert/native/request_handler.go b/app/vminsert/native/request_handler.go index cb36025dc..78cbe2d8e 100644 --- a/app/vminsert/native/request_handler.go +++ b/app/vminsert/native/request_handler.go @@ -65,6 +65,7 @@ func insertRows(block *parser.Block, extraLabels []prompbmarshal.Label) error { // Skip metric without labels. return nil } + ic.SortLabelsIfNeeded() ctx.metricNameBuf = storage.MarshalMetricNameRaw(ctx.metricNameBuf[:0], ic.Labels) values := block.Values timestamps := block.Timestamps diff --git a/app/vminsert/opentsdb/request_handler.go b/app/vminsert/opentsdb/request_handler.go index 852708862..49a6157a1 100644 --- a/app/vminsert/opentsdb/request_handler.go +++ b/app/vminsert/opentsdb/request_handler.go @@ -45,6 +45,7 @@ func insertRows(rows []parser.Row) error { // Skip metric without labels. continue } + ctx.SortLabelsIfNeeded() if err := ctx.WriteDataPoint(nil, ctx.Labels, r.Timestamp, r.Value); err != nil { return err } diff --git a/app/vminsert/opentsdbhttp/request_handler.go b/app/vminsert/opentsdbhttp/request_handler.go index 83fc33729..b5927ecc7 100644 --- a/app/vminsert/opentsdbhttp/request_handler.go +++ b/app/vminsert/opentsdbhttp/request_handler.go @@ -63,6 +63,7 @@ func insertRows(rows []parser.Row, extraLabels []prompbmarshal.Label) error { // Skip metric without labels. continue } + ctx.SortLabelsIfNeeded() if err := ctx.WriteDataPoint(nil, ctx.Labels, r.Timestamp, r.Value); err != nil { return err } diff --git a/app/vminsert/prometheusimport/request_handler.go b/app/vminsert/prometheusimport/request_handler.go index cc916e8b5..aa65b7da5 100644 --- a/app/vminsert/prometheusimport/request_handler.go +++ b/app/vminsert/prometheusimport/request_handler.go @@ -60,6 +60,7 @@ func insertRows(rows []parser.Row, extraLabels []prompbmarshal.Label) error { // Skip metric without labels. continue } + ctx.SortLabelsIfNeeded() if err := ctx.WriteDataPoint(nil, ctx.Labels, r.Timestamp, r.Value); err != nil { return err } diff --git a/app/vminsert/prompush/push.go b/app/vminsert/prompush/push.go index 1c6ebe0d7..40d495971 100644 --- a/app/vminsert/prompush/push.go +++ b/app/vminsert/prompush/push.go @@ -62,6 +62,7 @@ func push(ctx *common.InsertCtx, tss []prompbmarshal.TimeSeries) { // Skip metric without labels. continue } + ctx.SortLabelsIfNeeded() var metricNameRaw []byte var err error for i := range ts.Samples { diff --git a/app/vminsert/promremotewrite/request_handler.go b/app/vminsert/promremotewrite/request_handler.go index 15f3895ed..e56f12e25 100644 --- a/app/vminsert/promremotewrite/request_handler.go +++ b/app/vminsert/promremotewrite/request_handler.go @@ -61,6 +61,7 @@ func insertRows(timeseries []prompb.TimeSeries, extraLabels []prompbmarshal.Labe // Skip metric without labels. continue } + ctx.SortLabelsIfNeeded() var metricNameRaw []byte var err error samples := ts.Samples diff --git a/app/vminsert/vmimport/request_handler.go b/app/vminsert/vmimport/request_handler.go index 64002b4d4..5b4332ac9 100644 --- a/app/vminsert/vmimport/request_handler.go +++ b/app/vminsert/vmimport/request_handler.go @@ -67,6 +67,7 @@ func insertRows(rows []parser.Row, extraLabels []prompbmarshal.Label) error { // Skip metric without labels. continue } + ic.SortLabelsIfNeeded() ctx.metricNameBuf = storage.MarshalMetricNameRaw(ctx.metricNameBuf[:0], ic.Labels) values := r.Values timestamps := r.Timestamps diff --git a/app/vmstorage/main.go b/app/vmstorage/main.go index b23140a8d..731fc0002 100644 --- a/app/vmstorage/main.go +++ b/app/vmstorage/main.go @@ -557,9 +557,6 @@ func registerStorageMetrics() { return float64(m().SearchDelays) }) - metrics.NewGauge(`vm_sorted_row_labels_inserts_total`, func() float64 { - return float64(m().SortedRowLabelsInserts) - }) metrics.NewGauge(`vm_slow_row_inserts_total`, func() float64 { return float64(m().SlowRowInserts) }) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 00fea8f42..36285e2c8 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -2,7 +2,7 @@ # tip -* FEATURE: reduce the size of `MetricName -> internal_series_id` cache (aka `vm_cache_size_bytes{type="storage/tsid"}`) when ingesting samples for the same time series with distinct order of labels. For example, `foo{k1="v1",k2="v2"}` and `foo{k2="v2",k1="v1"}` represent a single time series. Previously VictoriaMetrics could need additional memory when ingesting such samples. The number of ingested samples with distinct order of labels for the same time series can be monitored with `vm_sorted_row_labels_inserts_total` metric. +* FEATURE: vminsert and vmagent: add `-sortLabels` command-line flag for sorting metric labels before pushing them to `vmstorage`. This should reduce the size of `MetricName -> internal_series_id` cache (aka `vm_cache_size_bytes{type="storage/tsid"}`) when ingesting samples for the same time series with distinct order of labels. For example, `foo{k1="v1",k2="v2"}` and `foo{k2="v2",k1="v1"}` represent a single time series. * FEATURE: vmagent: reduce memory usage when `-remoteWrite.queues` is set to a big value. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/1167). diff --git a/docs/Single-server-VictoriaMetrics.md b/docs/Single-server-VictoriaMetrics.md index 793818290..aed76c314 100644 --- a/docs/Single-server-VictoriaMetrics.md +++ b/docs/Single-server-VictoriaMetrics.md @@ -1324,6 +1324,8 @@ See the example of alerting rules for VM components [here](https://github.com/Vi * It is recommended to use default command-line flag values (i.e. don't set them explicitly) until the need of tweaking these flag values arises. +* It is recommended inspecting logs during troubleshooting, since they may contain useful information. + * It is recommended upgrading to the latest available release from [this page](https://github.com/VictoriaMetrics/VictoriaMetrics/releases), since the encountered issue could be already fixed there. @@ -1338,8 +1340,6 @@ See the example of alerting rules for VM components [here](https://github.com/Vi if background merge cannot be initiated due to free disk space shortage. The value shows the number of per-month partitions, which would start background merge if they had more free disk space. -* It is recommended inspecting logs during troubleshooting, since they may contain useful information. - * VictoriaMetrics buffers incoming data in memory for up to a few seconds before flushing it to persistent storage. This may lead to the following "issues": * Data becomes available for querying in a few seconds after inserting. It is possible to flush in-memory buffers to persistent storage @@ -1349,10 +1349,13 @@ See the example of alerting rules for VM components [here](https://github.com/Vi * If VictoriaMetrics works slowly and eats more than a CPU core per 100K ingested data points per second, then it is likely you have too many active time series for the current amount of RAM. - VictoriaMetrics [exposes](#monitoring) `vm_slow_*` metrics, which could be used as an indicator of low amounts of RAM. - It is recommended increasing the amount of RAM on the node with VictoriaMetrics in order to improve + VictoriaMetrics [exposes](#monitoring) `vm_slow_*` metrics such as `vm_slow_row_inserts_total` and `vm_slow_metric_name_loads_total`, which could be used + as an indicator of low amounts of RAM. It is recommended increasing the amount of RAM on the node with VictoriaMetrics in order to improve ingestion and query performance in this case. +* If the order of labels for the same metrics can change over time (e.g. if `metric{k1="v1",k2="v2"}` may become `metric{k2="v2",k1="v1"}`), + then it is recommended running VictoriaMetrics with `-sortLabels` command-line flag in order to reduce memory usage and CPU usage. + * VictoriaMetrics prioritizes data ingestion over data querying. So if it has no enough resources for data ingestion, then data querying may slow down significantly. @@ -1790,6 +1793,8 @@ Pass `-help` to VictoriaMetrics in order to see the list of supported command-li The maximum number of CPU cores to use for small merges. Default value is used if set to 0 -snapshotAuthKey string authKey, which must be passed in query string to /snapshot* pages + -sortLabels + Whether to sort labels for incoming samples before writing them to storage. This may be needed for reducing memory usage at storage when the order of labels in incoming samples is random. For example, if m{k1="v1",k2="v2"} may be sent as m{k2="v2",k1="v1"}. Enabled sorting for labels can slow down ingestion performance a bit -storageDataPath string Path to storage data (default "victoria-metrics-data") -tls diff --git a/docs/vmagent.md b/docs/vmagent.md index cf52fb800..815a0d030 100644 --- a/docs/vmagent.md +++ b/docs/vmagent.md @@ -702,6 +702,8 @@ See the docs at https://victoriametrics.github.io/vmagent.html . -remoteWrite.urlRelabelConfig array Optional path to relabel config for the corresponding -remoteWrite.url Supports array of values separated by comma or specified via multiple flags. + -sortLabels + Whether to sort labels for incoming samples before writing them to all the configured remote storage systems. This may be needed for reducing memory usage at remote storage when the order of labels in incoming samples is random. For example, if m{k1="v1",k2="v2"} may be sent as m{k2="v2",k1="v1"}Enabled sorting for labels can slow down ingestion performance a bit -tls Whether to enable TLS (aka HTTPS) for incoming requests. -tlsCertFile and -tlsKeyFile must be set if -tls is set -tlsCertFile string diff --git a/lib/storage/storage.go b/lib/storage/storage.go index 8300400d5..7c5c82911 100644 --- a/lib/storage/storage.go +++ b/lib/storage/storage.go @@ -48,7 +48,6 @@ type Storage struct { searchTSIDsConcurrencyLimitReached uint64 searchTSIDsConcurrencyLimitTimeout uint64 - sortedRowLabelsInserts uint64 slowRowInserts uint64 slowPerDayIndexInserts uint64 slowMetricNameLoads uint64 @@ -359,7 +358,6 @@ type Metrics struct { SearchDelays uint64 - SortedRowLabelsInserts uint64 SlowRowInserts uint64 SlowPerDayIndexInserts uint64 SlowMetricNameLoads uint64 @@ -429,7 +427,6 @@ func (s *Storage) UpdateMetrics(m *Metrics) { m.SearchDelays = storagepacelimiter.Search.DelaysTotal() - m.SortedRowLabelsInserts += atomic.LoadUint64(&s.sortedRowLabelsInserts) m.SlowRowInserts += atomic.LoadUint64(&s.slowRowInserts) m.SlowPerDayIndexInserts += atomic.LoadUint64(&s.slowPerDayIndexInserts) m.SlowMetricNameLoads += atomic.LoadUint64(&s.slowMetricNameLoads) @@ -1321,8 +1318,6 @@ func (s *Storage) ForceMergePartitions(partitionNamePrefix string) error { var rowsAddedTotal uint64 // AddRows adds the given mrs to s. -// -// AddRows can modify mrs contents. func (s *Storage) AddRows(mrs []MetricRow, precisionBits uint8) error { if len(mrs) == 0 { return nil @@ -1447,9 +1442,6 @@ func (s *Storage) add(rows []rawRow, mrs []MetricRow, precisionBits uint8) ([]ra prevMetricNameRaw []byte ) var pmrs *pendingMetricRows - var mn MetricName - var metricNameRawSorted []byte - var sortedRowLabelsInserts uint64 minTimestamp, maxTimestamp := s.tb.getMinMaxTimestamps() // Return only the first error, since it has no sense in returning all errors. var firstWarn error @@ -1502,40 +1494,22 @@ func (s *Storage) add(rows []rawRow, mrs []MetricRow, precisionBits uint8) ([]ra continue } - // Slower path - sort labels in MetricNameRaw and check the cache again. - // This should limit the number of cache entries for metrics with distinct order of labels to 1. - if err := mn.unmarshalRaw(mr.MetricNameRaw); err != nil { - if firstWarn == nil { - firstWarn = fmt.Errorf("cannot unmarshal MetricNameRaw %q: %w", mr.MetricNameRaw, err) - } - j-- - continue - } - mn.sortTags() - metricNameRawSorted = mn.marshalRaw(metricNameRawSorted[:0]) - if s.getTSIDFromCache(&r.TSID, metricNameRawSorted) { - // The TSID for the given metricNameRawSorted has been found in cache and isn't deleted. - // There is no need in checking whether r.TSID.MetricID is deleted, since tsidCache doesn't - // contain MetricName->TSID entries for deleted time series. - // See Storage.DeleteMetrics code for details. - sortedRowLabelsInserts++ - prevTSID = r.TSID - prevMetricNameRaw = mr.MetricNameRaw - continue - } - // Slow path - the TSID is missing in the cache. // Postpone its search in the loop below. j-- if pmrs == nil { pmrs = getPendingMetricRows() } - if string(mr.MetricNameRaw) != string(metricNameRawSorted) { - mr.MetricNameRaw = append(mr.MetricNameRaw[:0], metricNameRawSorted...) + if err := pmrs.addRow(mr); err != nil { + // Do not stop adding rows on error - just skip invalid row. + // This guarantees that invalid rows don't prevent + // from adding valid rows into the storage. + if firstWarn == nil { + firstWarn = err + } + continue } - pmrs.addRow(mr, &mn) } - atomic.AddUint64(&s.sortedRowLabelsInserts, sortedRowLabelsInserts) if pmrs != nil { // Sort pendingMetricRows by canonical metric name in order to speed up search via `is` in the loop below. pendingMetricRows := pmrs.pmrs @@ -1615,6 +1589,7 @@ type pendingMetricRows struct { lastMetricNameRaw []byte lastMetricName []byte + mn MetricName } func (pmrs *pendingMetricRows) reset() { @@ -1626,14 +1601,19 @@ func (pmrs *pendingMetricRows) reset() { pmrs.metricNamesBuf = pmrs.metricNamesBuf[:0] pmrs.lastMetricNameRaw = nil pmrs.lastMetricName = nil + pmrs.mn.Reset() } -func (pmrs *pendingMetricRows) addRow(mr *MetricRow, mn *MetricName) { +func (pmrs *pendingMetricRows) addRow(mr *MetricRow) error { // Do not spend CPU time on re-calculating canonical metricName during bulk import // of many rows for the same metric. if string(mr.MetricNameRaw) != string(pmrs.lastMetricNameRaw) { + if err := pmrs.mn.unmarshalRaw(mr.MetricNameRaw); err != nil { + return fmt.Errorf("cannot unmarshal MetricNameRaw %q: %w", mr.MetricNameRaw, err) + } + pmrs.mn.sortTags() metricNamesBufLen := len(pmrs.metricNamesBuf) - pmrs.metricNamesBuf = mn.Marshal(pmrs.metricNamesBuf) + pmrs.metricNamesBuf = pmrs.mn.Marshal(pmrs.metricNamesBuf) pmrs.lastMetricName = pmrs.metricNamesBuf[metricNamesBufLen:] pmrs.lastMetricNameRaw = mr.MetricNameRaw } @@ -1641,6 +1621,7 @@ func (pmrs *pendingMetricRows) addRow(mr *MetricRow, mn *MetricName) { MetricName: pmrs.lastMetricName, mr: *mr, }) + return nil } func getPendingMetricRows() *pendingMetricRows { From 759c9388709fee7092692fce95c7880493ecb6cc Mon Sep 17 00:00:00 2001 From: Aliaksandr Valialkin Date: Fri, 2 Apr 2021 11:49:57 +0300 Subject: [PATCH 10/63] Makefile: properly generate checksums for *.tar.gz files https://github.com/VictoriaMetrics/VictoriaMetrics/issues/1171 --- Makefile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 736896a57..8f7f2ce9e 100644 --- a/Makefile +++ b/Makefile @@ -111,7 +111,7 @@ release-victoria-metrics-generic: victoria-metrics-$(GOARCH)-prod victoria-metrics-$(GOARCH)-prod \ && sha256sum victoria-metrics-$(GOARCH)-$(PKG_TAG).tar.gz \ victoria-metrics-$(GOARCH)-prod \ - | sed s/-$(GOARCH)// > victoria-metrics-$(GOARCH)-$(PKG_TAG)_checksums.txt + | sed s/-$(GOARCH)-prod// > victoria-metrics-$(GOARCH)-$(PKG_TAG)_checksums.txt release-vmutils: \ release-vmutils-amd64 \ @@ -153,7 +153,7 @@ release-vmutils-generic: \ vmbackup-$(GOARCH)-prod \ vmrestore-$(GOARCH)-prod \ vmctl-$(GOARCH)-prod \ - | sed s/-$(GOARCH)// > vmutils-$(GOARCH)-$(PKG_TAG)_checksums.txt + | sed s/-$(GOARCH)-prod// > vmutils-$(GOARCH)-$(PKG_TAG)_checksums.txt release-vmutils-windows-generic: \ vmagent-windows-$(GOARCH)-prod \ From 9d237408c655470cd75e0940dd9a389500279fab Mon Sep 17 00:00:00 2001 From: Aliaksandr Valialkin Date: Fri, 2 Apr 2021 11:55:21 +0300 Subject: [PATCH 11/63] Makefile: add missing `-prod` suffix to binary names in *_checksums.txt file Updates https://github.com/VictoriaMetrics/VictoriaMetrics/issues/1171 --- Makefile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 8f7f2ce9e..237a391a2 100644 --- a/Makefile +++ b/Makefile @@ -111,7 +111,7 @@ release-victoria-metrics-generic: victoria-metrics-$(GOARCH)-prod victoria-metrics-$(GOARCH)-prod \ && sha256sum victoria-metrics-$(GOARCH)-$(PKG_TAG).tar.gz \ victoria-metrics-$(GOARCH)-prod \ - | sed s/-$(GOARCH)-prod// > victoria-metrics-$(GOARCH)-$(PKG_TAG)_checksums.txt + | sed s/-$(GOARCH)-prod/-prod/ > victoria-metrics-$(GOARCH)-$(PKG_TAG)_checksums.txt release-vmutils: \ release-vmutils-amd64 \ @@ -153,7 +153,7 @@ release-vmutils-generic: \ vmbackup-$(GOARCH)-prod \ vmrestore-$(GOARCH)-prod \ vmctl-$(GOARCH)-prod \ - | sed s/-$(GOARCH)-prod// > vmutils-$(GOARCH)-$(PKG_TAG)_checksums.txt + | sed s/-$(GOARCH)-prod/-prod/ > vmutils-$(GOARCH)-$(PKG_TAG)_checksums.txt release-vmutils-windows-generic: \ vmagent-windows-$(GOARCH)-prod \ From fdb8995642016afd8723b59a0a2a35367455395f Mon Sep 17 00:00:00 2001 From: Nikolay Date: Fri, 2 Apr 2021 11:56:40 +0300 Subject: [PATCH 12/63] Adds aws ECS credentials support (#1175) --- lib/promscrape/discovery/ec2/api.go | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/lib/promscrape/discovery/ec2/api.go b/lib/promscrape/discovery/ec2/api.go index e0e6b30a3..9a13dcb9d 100644 --- a/lib/promscrape/discovery/ec2/api.go +++ b/lib/promscrape/discovery/ec2/api.go @@ -172,6 +172,11 @@ func getAPICredentials(cfg *apiConfig) (*apiCredentials, error) { return getRoleWebIdentityCredentials(cfg.stsEndpoint, cfg.roleARN, string(token)) } + if ecsMetaURI := os.Getenv("AWS_CONTAINER_CREDENTIALS_RELATIVE_URI"); len(ecsMetaURI) > 0 { + path := "http://169.254.170.2" + ecsMetaURI + return getECSRoleCredentialsByPath(path) + } + // we need instance credentials if dont have access keys if len(acNew.AccessKeyID) == 0 && len(acNew.SecretAccessKey) == 0 { ac, err := getInstanceRoleCredentials() @@ -200,6 +205,22 @@ func getAPICredentials(cfg *apiConfig) (*apiCredentials, error) { return acNew, nil } +// getECSRoleCredentialsByPath makes request to ecs metadata service +// and retrieves instances credentails +// https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-iam-roles.html +func getECSRoleCredentialsByPath(path string) (*apiCredentials, error) { + client := discoveryutils.GetHTTPClient() + resp, err := client.Get(path) + if err != nil { + return nil, fmt.Errorf("cannot get ECS instance role credentials: %w", err) + } + data, err := readResponseBody(resp, path) + if err != nil { + return nil, err + } + return parseMetadataSecurityCredentials(data) +} + // getInstanceRoleCredentials makes request to local ec2 instance metadata service // and tries to retrieve credentials from assigned iam role. // From 3967bd705adf81bb4daf16ae15688fb282dd73a1 Mon Sep 17 00:00:00 2001 From: Aliaksandr Valialkin Date: Fri, 2 Apr 2021 13:10:01 +0300 Subject: [PATCH 13/63] docs/CHANGELOG.md: mention about AWS IAM roles for tasks support for EC2 service discovery Follow-up for fdb8995642016afd8723b59a0a2a35367455395f --- docs/CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 36285e2c8..ff7ea0086 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -4,6 +4,9 @@ * FEATURE: vminsert and vmagent: add `-sortLabels` command-line flag for sorting metric labels before pushing them to `vmstorage`. This should reduce the size of `MetricName -> internal_series_id` cache (aka `vm_cache_size_bytes{type="storage/tsid"}`) when ingesting samples for the same time series with distinct order of labels. For example, `foo{k1="v1",k2="v2"}` and `foo{k2="v2",k1="v1"}` represent a single time series. * FEATURE: vmagent: reduce memory usage when `-remoteWrite.queues` is set to a big value. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/1167). +* FEATURE: vmagent: add AWS IAM roles for tasks support for EC2 service discovery according to [these docs](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-iam-roles.html). + +* BUGFIX: properly generate filename for `*.tar.gz` archive inside `_checksums.txt` file posted at [releases page](https://github.com/VictoriaMetrics/VictoriaMetrics/releases). See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/1171). # [v1.57.1](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.57.1) From 12e4785fe8dc64d6c4422725e833cefdab49d8f6 Mon Sep 17 00:00:00 2001 From: Aliaksandr Valialkin Date: Fri, 2 Apr 2021 14:17:53 +0300 Subject: [PATCH 14/63] lib/promscrape/discovery/kubernetes: properly discover targets in multiple namespaces Updates https://github.com/VictoriaMetrics/VictoriaMetrics/issues/1170 --- docs/CHANGELOG.md | 1 + .../discovery/kubernetes/api_watcher.go | 192 ++++++++++-------- .../discovery/kubernetes/api_watcher_test.go | 51 ++--- .../discovery/kubernetes/common_types.go | 4 - .../discovery/kubernetes/endpoints.go | 10 +- .../discovery/kubernetes/endpointslices.go | 10 +- .../discovery/kubernetes/ingress.go | 10 +- lib/promscrape/discovery/kubernetes/node.go | 11 +- lib/promscrape/discovery/kubernetes/pod.go | 10 +- .../discovery/kubernetes/service.go | 10 +- 10 files changed, 164 insertions(+), 145 deletions(-) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index ff7ea0086..dafce3a4e 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -6,6 +6,7 @@ * FEATURE: vmagent: reduce memory usage when `-remoteWrite.queues` is set to a big value. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/1167). * FEATURE: vmagent: add AWS IAM roles for tasks support for EC2 service discovery according to [these docs](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-iam-roles.html). +* BUGFIX: vmagent: properly discovery targets if multiple namespace selectors are put inside `kubernetes_sd_config`. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/1170). * BUGFIX: properly generate filename for `*.tar.gz` archive inside `_checksums.txt` file posted at [releases page](https://github.com/VictoriaMetrics/VictoriaMetrics/releases). See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/1171). diff --git a/lib/promscrape/discovery/kubernetes/api_watcher.go b/lib/promscrape/discovery/kubernetes/api_watcher.go index 2e1bfe0cc..4397676bc 100644 --- a/lib/promscrape/discovery/kubernetes/api_watcher.go +++ b/lib/promscrape/discovery/kubernetes/api_watcher.go @@ -32,7 +32,7 @@ type WatchEvent struct { // object is any Kubernetes object. type object interface { - key() string + name() string getTargetLabels(gw *groupWatcher) []map[string]string } @@ -51,9 +51,9 @@ type apiWatcher struct { gw *groupWatcher - // swos contains a map of ScrapeWork objects for the given apiWatcher - swosByKey map[string][]interface{} - swosByKeyLock sync.Mutex + // swos contains per-namepsace maps of ScrapeWork objects for the given apiWatcher + swosByNamespace map[string]map[string][]interface{} + swosByNamespaceLock sync.Mutex swosCount *metrics.Counter } @@ -64,44 +64,54 @@ func newAPIWatcher(apiServer string, ac *promauth.Config, sdc *SDConfig, swcFunc proxyURL := sdc.ProxyURL.URL() gw := getGroupWatcher(apiServer, ac, namespaces, selectors, proxyURL) return &apiWatcher{ - role: sdc.Role, - swcFunc: swcFunc, - gw: gw, - swosByKey: make(map[string][]interface{}), - swosCount: metrics.GetOrCreateCounter(fmt.Sprintf(`vm_promscrape_discovery_kubernetes_scrape_works{role=%q}`, sdc.Role)), + role: sdc.Role, + swcFunc: swcFunc, + gw: gw, + swosByNamespace: make(map[string]map[string][]interface{}), + swosCount: metrics.GetOrCreateCounter(fmt.Sprintf(`vm_promscrape_discovery_kubernetes_scrape_works{role=%q}`, sdc.Role)), } } func (aw *apiWatcher) mustStop() { aw.gw.unsubscribeAPIWatcher(aw) - aw.reloadScrapeWorks(make(map[string][]interface{})) + aw.swosByNamespaceLock.Lock() + aw.swosByNamespace = make(map[string]map[string][]interface{}) + aw.swosByNamespaceLock.Unlock() } -func (aw *apiWatcher) reloadScrapeWorks(swosByKey map[string][]interface{}) { - aw.swosByKeyLock.Lock() - aw.swosCount.Add(len(swosByKey) - len(aw.swosByKey)) - aw.swosByKey = swosByKey - aw.swosByKeyLock.Unlock() +func (aw *apiWatcher) reloadScrapeWorks(namespace string, swosByName map[string][]interface{}) { + aw.swosByNamespaceLock.Lock() + aw.swosCount.Add(len(swosByName) - len(aw.swosByNamespace[namespace])) + aw.swosByNamespace[namespace] = swosByName + aw.swosByNamespaceLock.Unlock() } -func (aw *apiWatcher) setScrapeWorks(key string, labels []map[string]string) { +func (aw *apiWatcher) setScrapeWorks(namespace, name string, labels []map[string]string) { swos := getScrapeWorkObjectsForLabels(aw.swcFunc, labels) - aw.swosByKeyLock.Lock() - if len(swos) > 0 { - aw.swosCount.Add(len(swos) - len(aw.swosByKey[key])) - aw.swosByKey[key] = swos - } else { - aw.swosCount.Add(-len(aw.swosByKey[key])) - delete(aw.swosByKey, key) + aw.swosByNamespaceLock.Lock() + swosByName := aw.swosByNamespace[namespace] + if swosByName == nil { + swosByName = make(map[string][]interface{}) + aw.swosByNamespace[namespace] = swosByName } - aw.swosByKeyLock.Unlock() + if len(swos) > 0 { + aw.swosCount.Add(len(swos) - len(swosByName[name])) + swosByName[name] = swos + } else { + aw.swosCount.Add(-len(swosByName[name])) + delete(swosByName, name) + } + aw.swosByNamespaceLock.Unlock() } -func (aw *apiWatcher) removeScrapeWorks(key string) { - aw.swosByKeyLock.Lock() - aw.swosCount.Add(-len(aw.swosByKey[key])) - delete(aw.swosByKey, key) - aw.swosByKeyLock.Unlock() +func (aw *apiWatcher) removeScrapeWorks(namespace, name string) { + aw.swosByNamespaceLock.Lock() + swosByName := aw.swosByNamespace[namespace] + if len(swosByName) > 0 { + aw.swosCount.Add(-len(swosByName[name])) + delete(swosByName, name) + } + aw.swosByNamespaceLock.Unlock() } func getScrapeWorkObjectsForLabels(swcFunc ScrapeWorkConstructorFunc, labelss []map[string]string) []interface{} { @@ -119,16 +129,20 @@ func getScrapeWorkObjectsForLabels(swcFunc ScrapeWorkConstructorFunc, labelss [] // getScrapeWorkObjects returns all the ScrapeWork objects for the given aw. func (aw *apiWatcher) getScrapeWorkObjects() []interface{} { aw.gw.startWatchersForRole(aw.role, aw) - aw.swosByKeyLock.Lock() - defer aw.swosByKeyLock.Unlock() + aw.swosByNamespaceLock.Lock() + defer aw.swosByNamespaceLock.Unlock() size := 0 - for _, swosLocal := range aw.swosByKey { - size += len(swosLocal) + for _, swosByName := range aw.swosByNamespace { + for _, swosLocal := range swosByName { + size += len(swosLocal) + } } swos := make([]interface{}, 0, size) - for _, swosLocal := range aw.swosByKey { - swos = append(swos, swosLocal...) + for _, swosByName := range aw.swosByNamespace { + for _, swosLocal := range swosByName { + swos = append(swos, swosLocal...) + } } return swos } @@ -209,17 +223,21 @@ func (gw *groupWatcher) getObjectByRole(role, namespace, name string) object { // this is needed for testing return nil } - key := namespace + "/" + name gw.startWatchersForRole(role, nil) gw.mu.Lock() defer gw.mu.Unlock() for _, uw := range gw.m { if uw.role != role { + // Role mismatch + continue + } + if uw.namespace != "" && uw.namespace != namespace { + // Namespace mismatch continue } uw.mu.Lock() - o := uw.objectsByKey[key] + o := uw.objectsByName[name] uw.mu.Unlock() if o != nil { return o @@ -229,13 +247,13 @@ func (gw *groupWatcher) getObjectByRole(role, namespace, name string) object { } func (gw *groupWatcher) startWatchersForRole(role string, aw *apiWatcher) { - paths := getAPIPaths(role, gw.namespaces, gw.selectors) - for _, path := range paths { + paths, namespaces := getAPIPathsWithNamespaces(role, gw.namespaces, gw.selectors) + for i, path := range paths { apiURL := gw.apiServer + path gw.mu.Lock() uw := gw.m[apiURL] if uw == nil { - uw = newURLWatcher(role, apiURL, gw) + uw = newURLWatcher(role, namespaces[i], apiURL, gw) gw.m[apiURL] = uw } gw.mu.Unlock() @@ -243,25 +261,25 @@ func (gw *groupWatcher) startWatchersForRole(role string, aw *apiWatcher) { } } -func (gw *groupWatcher) reloadScrapeWorksForAPIWatchers(aws []*apiWatcher, objectsByKey map[string]object) { +func (gw *groupWatcher) reloadScrapeWorksForAPIWatchers(namespace string, aws []*apiWatcher, objectsByName map[string]object) { if len(aws) == 0 { return } - swosByKey := make([]map[string][]interface{}, len(aws)) + swosByName := make([]map[string][]interface{}, len(aws)) for i := range aws { - swosByKey[i] = make(map[string][]interface{}) + swosByName[i] = make(map[string][]interface{}) } - for key, o := range objectsByKey { + for name, o := range objectsByName { labels := o.getTargetLabels(gw) for i, aw := range aws { swos := getScrapeWorkObjectsForLabels(aw.swcFunc, labels) if len(swos) > 0 { - swosByKey[i][key] = swos + swosByName[i][name] = swos } } } for i, aw := range aws { - aw.reloadScrapeWorks(swosByKey[i]) + aw.reloadScrapeWorks(namespace, swosByName[i]) } } @@ -285,16 +303,17 @@ func (gw *groupWatcher) unsubscribeAPIWatcher(aw *apiWatcher) { gw.mu.Unlock() } -// urlWatcher watches for an apiURL and updates object states in objectsByKey. +// urlWatcher watches for an apiURL and updates object states in objectsByName. type urlWatcher struct { - role string - apiURL string - gw *groupWatcher + role string + namespace string + apiURL string + gw *groupWatcher parseObject parseObjectFunc parseObjectList parseObjectListFunc - // mu protects aws, awsPending, objectsByKey and resourceVersion + // mu protects aws, awsPending, objectsByName and resourceVersion mu sync.Mutex // aws contains registered apiWatcher objects @@ -303,8 +322,8 @@ type urlWatcher struct { // awsPending contains pending apiWatcher objects, which must be moved to aws in a batch awsPending map[*apiWatcher]struct{} - // objectsByKey contains the latest state for objects obtained from apiURL - objectsByKey map[string]object + // objectsByName contains the latest state for objects obtained from apiURL + objectsByName map[string]object resourceVersion string @@ -315,20 +334,21 @@ type urlWatcher struct { staleResourceVersions *metrics.Counter } -func newURLWatcher(role, apiURL string, gw *groupWatcher) *urlWatcher { +func newURLWatcher(role, namespace, apiURL string, gw *groupWatcher) *urlWatcher { parseObject, parseObjectList := getObjectParsersForRole(role) metrics.GetOrCreateCounter(fmt.Sprintf(`vm_promscrape_discovery_kubernetes_url_watchers{role=%q}`, role)).Inc() uw := &urlWatcher{ - role: role, - apiURL: apiURL, - gw: gw, + role: role, + namespace: namespace, + apiURL: apiURL, + gw: gw, parseObject: parseObject, parseObjectList: parseObjectList, - aws: make(map[*apiWatcher]struct{}), - awsPending: make(map[*apiWatcher]struct{}), - objectsByKey: make(map[string]object), + aws: make(map[*apiWatcher]struct{}), + awsPending: make(map[*apiWatcher]struct{}), + objectsByName: make(map[string]object), objectsCount: metrics.GetOrCreateCounter(fmt.Sprintf(`vm_promscrape_discovery_kubernetes_objects{role=%q}`, role)), objectsAdded: metrics.GetOrCreateCounter(fmt.Sprintf(`vm_promscrape_discovery_kubernetes_objects_added_total{role=%q}`, role)), @@ -372,7 +392,7 @@ func (uw *urlWatcher) processPendingSubscribers() { t := time.NewTicker(time.Second) for range t.C { var awsPending []*apiWatcher - var objectsByKey map[string]object + var objectsByName map[string]object uw.mu.Lock() if len(uw.awsPending) > 0 { @@ -384,16 +404,16 @@ func (uw *urlWatcher) processPendingSubscribers() { uw.aws[aw] = struct{}{} delete(uw.awsPending, aw) } - objectsByKey = make(map[string]object, len(uw.objectsByKey)) - for key, o := range uw.objectsByKey { - objectsByKey[key] = o + objectsByName = make(map[string]object, len(uw.objectsByName)) + for name, o := range uw.objectsByName { + objectsByName[name] = o } } metrics.GetOrCreateCounter(fmt.Sprintf(`vm_promscrape_discovery_kubernetes_subscibers{role=%q,type="pending"}`, uw.role)).Add(-len(awsPending)) metrics.GetOrCreateCounter(fmt.Sprintf(`vm_promscrape_discovery_kubernetes_subscibers{role=%q,type="permanent"}`, uw.role)).Add(len(awsPending)) uw.mu.Unlock() - uw.gw.reloadScrapeWorksForAPIWatchers(awsPending, objectsByKey) + uw.gw.reloadScrapeWorksForAPIWatchers(uw.namespace, awsPending, objectsByName) } } @@ -425,7 +445,7 @@ func (uw *urlWatcher) reloadObjects() string { logger.Errorf("unexpected status code for request to %q: %d; want %d; response: %q", requestURL, resp.StatusCode, http.StatusOK, body) return "" } - objectsByKey, metadata, err := uw.parseObjectList(resp.Body) + objectsByName, metadata, err := uw.parseObjectList(resp.Body) _ = resp.Body.Close() if err != nil { logger.Errorf("cannot parse objects from %q: %s", requestURL, err) @@ -434,18 +454,18 @@ func (uw *urlWatcher) reloadObjects() string { uw.mu.Lock() var updated, removed, added int - for key := range uw.objectsByKey { - if o, ok := objectsByKey[key]; ok { - uw.objectsByKey[key] = o + for name := range uw.objectsByName { + if o, ok := objectsByName[name]; ok { + uw.objectsByName[name] = o updated++ } else { - delete(uw.objectsByKey, key) + delete(uw.objectsByName, name) removed++ } } - for key, o := range objectsByKey { - if _, ok := uw.objectsByKey[key]; !ok { - uw.objectsByKey[key] = o + for name, o := range objectsByName { + if _, ok := uw.objectsByName[name]; !ok { + uw.objectsByName[name] = o added++ } } @@ -457,8 +477,8 @@ func (uw *urlWatcher) reloadObjects() string { aws := getAPIWatchers(uw.aws) uw.mu.Unlock() - uw.gw.reloadScrapeWorksForAPIWatchers(aws, objectsByKey) - logger.Infof("reloaded %d objects from %q", len(objectsByKey), requestURL) + uw.gw.reloadScrapeWorksForAPIWatchers(uw.namespace, aws, objectsByName) + logger.Infof("reloaded %d objects from %q", len(objectsByName), requestURL) return metadata.ResourceVersion } @@ -544,37 +564,37 @@ func (uw *urlWatcher) readObjectUpdateStream(r io.Reader) error { if err != nil { return err } - key := o.key() + name := o.name() uw.mu.Lock() - if _, ok := uw.objectsByKey[key]; !ok { + if _, ok := uw.objectsByName[name]; !ok { uw.objectsCount.Inc() uw.objectsAdded.Inc() } else { uw.objectsUpdated.Inc() } - uw.objectsByKey[key] = o + uw.objectsByName[name] = o aws := getAPIWatchers(uw.aws) uw.mu.Unlock() labels := o.getTargetLabels(uw.gw) for _, aw := range aws { - aw.setScrapeWorks(key, labels) + aw.setScrapeWorks(uw.namespace, name, labels) } case "DELETED": o, err := uw.parseObject(we.Object) if err != nil { return err } - key := o.key() + name := o.name() uw.mu.Lock() - if _, ok := uw.objectsByKey[key]; ok { + if _, ok := uw.objectsByName[name]; ok { uw.objectsCount.Dec() uw.objectsRemoved.Inc() - delete(uw.objectsByKey, key) + delete(uw.objectsByName, name) } aws := getAPIWatchers(uw.aws) uw.mu.Unlock() for _, aw := range aws { - aw.removeScrapeWorks(key) + aw.removeScrapeWorks(uw.namespace, name) } case "BOOKMARK": // See https://kubernetes.io/docs/reference/using-api/api-concepts/#watch-bookmarks @@ -630,19 +650,19 @@ func parseError(data []byte) (*Error, error) { return &em, nil } -func getAPIPaths(role string, namespaces []string, selectors []Selector) []string { +func getAPIPathsWithNamespaces(role string, namespaces []string, selectors []Selector) ([]string, []string) { objectName := getObjectNameByRole(role) if objectName == "nodes" || len(namespaces) == 0 { query := joinSelectors(role, selectors) path := getAPIPath(objectName, "", query) - return []string{path} + return []string{path}, []string{""} } query := joinSelectors(role, selectors) paths := make([]string, len(namespaces)) for i, namespace := range namespaces { paths[i] = getAPIPath(objectName, namespace, query) } - return paths + return paths, namespaces } func getAPIPath(objectName, namespace, query string) string { diff --git a/lib/promscrape/discovery/kubernetes/api_watcher_test.go b/lib/promscrape/discovery/kubernetes/api_watcher_test.go index 7d56ada08..f971512cf 100644 --- a/lib/promscrape/discovery/kubernetes/api_watcher_test.go +++ b/lib/promscrape/discovery/kubernetes/api_watcher_test.go @@ -5,52 +5,55 @@ import ( "testing" ) -func TestGetAPIPaths(t *testing.T) { - f := func(role string, namespaces []string, selectors []Selector, expectedPaths []string) { +func TestGetAPIPathsWithNamespaces(t *testing.T) { + f := func(role string, namespaces []string, selectors []Selector, expectedPaths, expectedNamespaces []string) { t.Helper() - paths := getAPIPaths(role, namespaces, selectors) + paths, resultNamespaces := getAPIPathsWithNamespaces(role, namespaces, selectors) if !reflect.DeepEqual(paths, expectedPaths) { t.Fatalf("unexpected paths; got\n%q\nwant\n%q", paths, expectedPaths) } + if !reflect.DeepEqual(resultNamespaces, expectedNamespaces) { + t.Fatalf("unexpected namespaces; got\n%q\nwant\n%q", resultNamespaces, expectedNamespaces) + } } // role=node - f("node", nil, nil, []string{"/api/v1/nodes"}) - f("node", []string{"foo", "bar"}, nil, []string{"/api/v1/nodes"}) + f("node", nil, nil, []string{"/api/v1/nodes"}, []string{""}) + f("node", []string{"foo", "bar"}, nil, []string{"/api/v1/nodes"}, []string{""}) f("node", nil, []Selector{ { Role: "pod", Label: "foo", Field: "bar", }, - }, []string{"/api/v1/nodes"}) + }, []string{"/api/v1/nodes"}, []string{""}) f("node", nil, []Selector{ { Role: "node", Label: "foo", Field: "bar", }, - }, []string{"/api/v1/nodes?labelSelector=foo&fieldSelector=bar"}) + }, []string{"/api/v1/nodes?labelSelector=foo&fieldSelector=bar"}, []string{""}) f("node", []string{"x", "y"}, []Selector{ { Role: "node", Label: "foo", Field: "bar", }, - }, []string{"/api/v1/nodes?labelSelector=foo&fieldSelector=bar"}) + }, []string{"/api/v1/nodes?labelSelector=foo&fieldSelector=bar"}, []string{""}) // role=pod - f("pod", nil, nil, []string{"/api/v1/pods"}) + f("pod", nil, nil, []string{"/api/v1/pods"}, []string{""}) f("pod", []string{"foo", "bar"}, nil, []string{ "/api/v1/namespaces/foo/pods", "/api/v1/namespaces/bar/pods", - }) + }, []string{"foo", "bar"}) f("pod", nil, []Selector{ { Role: "node", Label: "foo", }, - }, []string{"/api/v1/pods"}) + }, []string{"/api/v1/pods"}, []string{""}) f("pod", nil, []Selector{ { Role: "pod", @@ -61,7 +64,7 @@ func TestGetAPIPaths(t *testing.T) { Label: "x", Field: "y", }, - }, []string{"/api/v1/pods?labelSelector=foo%2Cx&fieldSelector=y"}) + }, []string{"/api/v1/pods?labelSelector=foo%2Cx&fieldSelector=y"}, []string{""}) f("pod", []string{"x", "y"}, []Selector{ { Role: "pod", @@ -75,14 +78,14 @@ func TestGetAPIPaths(t *testing.T) { }, []string{ "/api/v1/namespaces/x/pods?labelSelector=foo%2Cx&fieldSelector=y", "/api/v1/namespaces/y/pods?labelSelector=foo%2Cx&fieldSelector=y", - }) + }, []string{"x", "y"}) // role=service - f("service", nil, nil, []string{"/api/v1/services"}) + f("service", nil, nil, []string{"/api/v1/services"}, []string{""}) f("service", []string{"x", "y"}, nil, []string{ "/api/v1/namespaces/x/services", "/api/v1/namespaces/y/services", - }) + }, []string{"x", "y"}) f("service", nil, []Selector{ { Role: "node", @@ -92,7 +95,7 @@ func TestGetAPIPaths(t *testing.T) { Role: "service", Field: "bar", }, - }, []string{"/api/v1/services?fieldSelector=bar"}) + }, []string{"/api/v1/services?fieldSelector=bar"}, []string{""}) f("service", []string{"x", "y"}, []Selector{ { Role: "service", @@ -101,14 +104,14 @@ func TestGetAPIPaths(t *testing.T) { }, []string{ "/api/v1/namespaces/x/services?labelSelector=abc%3Dde", "/api/v1/namespaces/y/services?labelSelector=abc%3Dde", - }) + }, []string{"x", "y"}) // role=endpoints - f("endpoints", nil, nil, []string{"/api/v1/endpoints"}) + f("endpoints", nil, nil, []string{"/api/v1/endpoints"}, []string{""}) f("endpoints", []string{"x", "y"}, nil, []string{ "/api/v1/namespaces/x/endpoints", "/api/v1/namespaces/y/endpoints", - }) + }, []string{"x", "y"}) f("endpoints", []string{"x", "y"}, []Selector{ { Role: "endpoints", @@ -121,10 +124,10 @@ func TestGetAPIPaths(t *testing.T) { }, []string{ "/api/v1/namespaces/x/endpoints?labelSelector=bbb", "/api/v1/namespaces/y/endpoints?labelSelector=bbb", - }) + }, []string{"x", "y"}) // role=endpointslices - f("endpointslices", nil, nil, []string{"/apis/discovery.k8s.io/v1beta1/endpointslices"}) + f("endpointslices", nil, nil, []string{"/apis/discovery.k8s.io/v1beta1/endpointslices"}, []string{""}) f("endpointslices", []string{"x", "y"}, []Selector{ { Role: "endpointslices", @@ -134,10 +137,10 @@ func TestGetAPIPaths(t *testing.T) { }, []string{ "/apis/discovery.k8s.io/v1beta1/namespaces/x/endpointslices?labelSelector=label&fieldSelector=field", "/apis/discovery.k8s.io/v1beta1/namespaces/y/endpointslices?labelSelector=label&fieldSelector=field", - }) + }, []string{"x", "y"}) // role=ingress - f("ingress", nil, nil, []string{"/apis/networking.k8s.io/v1beta1/ingresses"}) + f("ingress", nil, nil, []string{"/apis/networking.k8s.io/v1beta1/ingresses"}, []string{""}) f("ingress", []string{"x", "y"}, []Selector{ { Role: "node", @@ -158,7 +161,7 @@ func TestGetAPIPaths(t *testing.T) { }, []string{ "/apis/networking.k8s.io/v1beta1/namespaces/x/ingresses?labelSelector=cde%2Cbaaa&fieldSelector=abc", "/apis/networking.k8s.io/v1beta1/namespaces/y/ingresses?labelSelector=cde%2Cbaaa&fieldSelector=abc", - }) + }, []string{"x", "y"}) } func TestParseBookmark(t *testing.T) { diff --git a/lib/promscrape/discovery/kubernetes/common_types.go b/lib/promscrape/discovery/kubernetes/common_types.go index be93bbb4a..5eab6e4d1 100644 --- a/lib/promscrape/discovery/kubernetes/common_types.go +++ b/lib/promscrape/discovery/kubernetes/common_types.go @@ -16,10 +16,6 @@ type ObjectMeta struct { OwnerReferences []OwnerReference } -func (om *ObjectMeta) key() string { - return om.Namespace + "/" + om.Name -} - // ListMeta is a Kubernetes list metadata // https://v1-17.docs.kubernetes.io/docs/reference/generated/kubernetes-api/v1.17/#listmeta-v1-meta type ListMeta struct { diff --git a/lib/promscrape/discovery/kubernetes/endpoints.go b/lib/promscrape/discovery/kubernetes/endpoints.go index 805a88b01..032eebb29 100644 --- a/lib/promscrape/discovery/kubernetes/endpoints.go +++ b/lib/promscrape/discovery/kubernetes/endpoints.go @@ -8,8 +8,8 @@ import ( "github.com/VictoriaMetrics/VictoriaMetrics/lib/promscrape/discoveryutils" ) -func (eps *Endpoints) key() string { - return eps.Metadata.key() +func (eps *Endpoints) name() string { + return eps.Metadata.Name } func parseEndpointsList(r io.Reader) (map[string]object, ListMeta, error) { @@ -18,11 +18,11 @@ func parseEndpointsList(r io.Reader) (map[string]object, ListMeta, error) { if err := d.Decode(&epsl); err != nil { return nil, epsl.Metadata, fmt.Errorf("cannot unmarshal EndpointsList: %w", err) } - objectsByKey := make(map[string]object) + objectsByName := make(map[string]object) for _, eps := range epsl.Items { - objectsByKey[eps.key()] = eps + objectsByName[eps.name()] = eps } - return objectsByKey, epsl.Metadata, nil + return objectsByName, epsl.Metadata, nil } func parseEndpoints(data []byte) (object, error) { diff --git a/lib/promscrape/discovery/kubernetes/endpointslices.go b/lib/promscrape/discovery/kubernetes/endpointslices.go index 5e1961e92..bf4f96f3e 100644 --- a/lib/promscrape/discovery/kubernetes/endpointslices.go +++ b/lib/promscrape/discovery/kubernetes/endpointslices.go @@ -9,8 +9,8 @@ import ( "github.com/VictoriaMetrics/VictoriaMetrics/lib/promscrape/discoveryutils" ) -func (eps *EndpointSlice) key() string { - return eps.Metadata.key() +func (eps *EndpointSlice) name() string { + return eps.Metadata.Name } func parseEndpointSliceList(r io.Reader) (map[string]object, ListMeta, error) { @@ -19,11 +19,11 @@ func parseEndpointSliceList(r io.Reader) (map[string]object, ListMeta, error) { if err := d.Decode(&epsl); err != nil { return nil, epsl.Metadata, fmt.Errorf("cannot unmarshal EndpointSliceList: %w", err) } - objectsByKey := make(map[string]object) + objectsByName := make(map[string]object) for _, eps := range epsl.Items { - objectsByKey[eps.key()] = eps + objectsByName[eps.name()] = eps } - return objectsByKey, epsl.Metadata, nil + return objectsByName, epsl.Metadata, nil } func parseEndpointSlice(data []byte) (object, error) { diff --git a/lib/promscrape/discovery/kubernetes/ingress.go b/lib/promscrape/discovery/kubernetes/ingress.go index cca6bdb24..9a99b267f 100644 --- a/lib/promscrape/discovery/kubernetes/ingress.go +++ b/lib/promscrape/discovery/kubernetes/ingress.go @@ -6,8 +6,8 @@ import ( "io" ) -func (ig *Ingress) key() string { - return ig.Metadata.key() +func (ig *Ingress) name() string { + return ig.Metadata.Name } func parseIngressList(r io.Reader) (map[string]object, ListMeta, error) { @@ -16,11 +16,11 @@ func parseIngressList(r io.Reader) (map[string]object, ListMeta, error) { if err := d.Decode(&igl); err != nil { return nil, igl.Metadata, fmt.Errorf("cannot unmarshal IngressList: %w", err) } - objectsByKey := make(map[string]object) + objectsByName := make(map[string]object) for _, ig := range igl.Items { - objectsByKey[ig.key()] = ig + objectsByName[ig.name()] = ig } - return objectsByKey, igl.Metadata, nil + return objectsByName, igl.Metadata, nil } func parseIngress(data []byte) (object, error) { diff --git a/lib/promscrape/discovery/kubernetes/node.go b/lib/promscrape/discovery/kubernetes/node.go index 6c990c846..a1d07f96d 100644 --- a/lib/promscrape/discovery/kubernetes/node.go +++ b/lib/promscrape/discovery/kubernetes/node.go @@ -8,9 +8,8 @@ import ( "github.com/VictoriaMetrics/VictoriaMetrics/lib/promscrape/discoveryutils" ) -// getNodesLabels returns labels for k8s nodes obtained from the given cfg -func (n *Node) key() string { - return n.Metadata.key() +func (n *Node) name() string { + return n.Metadata.Name } func parseNodeList(r io.Reader) (map[string]object, ListMeta, error) { @@ -19,11 +18,11 @@ func parseNodeList(r io.Reader) (map[string]object, ListMeta, error) { if err := d.Decode(&nl); err != nil { return nil, nl.Metadata, fmt.Errorf("cannot unmarshal NodeList: %w", err) } - objectsByKey := make(map[string]object) + objectsByName := make(map[string]object) for _, n := range nl.Items { - objectsByKey[n.key()] = n + objectsByName[n.name()] = n } - return objectsByKey, nl.Metadata, nil + return objectsByName, nl.Metadata, nil } func parseNode(data []byte) (object, error) { diff --git a/lib/promscrape/discovery/kubernetes/pod.go b/lib/promscrape/discovery/kubernetes/pod.go index 8a88ffaca..56280523c 100644 --- a/lib/promscrape/discovery/kubernetes/pod.go +++ b/lib/promscrape/discovery/kubernetes/pod.go @@ -10,8 +10,8 @@ import ( "github.com/VictoriaMetrics/VictoriaMetrics/lib/promscrape/discoveryutils" ) -func (p *Pod) key() string { - return p.Metadata.key() +func (p *Pod) name() string { + return p.Metadata.Name } func parsePodList(r io.Reader) (map[string]object, ListMeta, error) { @@ -20,11 +20,11 @@ func parsePodList(r io.Reader) (map[string]object, ListMeta, error) { if err := d.Decode(&pl); err != nil { return nil, pl.Metadata, fmt.Errorf("cannot unmarshal PodList: %w", err) } - objectsByKey := make(map[string]object) + objectsByName := make(map[string]object) for _, p := range pl.Items { - objectsByKey[p.key()] = p + objectsByName[p.name()] = p } - return objectsByKey, pl.Metadata, nil + return objectsByName, pl.Metadata, nil } func parsePod(data []byte) (object, error) { diff --git a/lib/promscrape/discovery/kubernetes/service.go b/lib/promscrape/discovery/kubernetes/service.go index b74bd2653..d140992b0 100644 --- a/lib/promscrape/discovery/kubernetes/service.go +++ b/lib/promscrape/discovery/kubernetes/service.go @@ -8,8 +8,8 @@ import ( "github.com/VictoriaMetrics/VictoriaMetrics/lib/promscrape/discoveryutils" ) -func (s *Service) key() string { - return s.Metadata.key() +func (s *Service) name() string { + return s.Metadata.Name } func parseServiceList(r io.Reader) (map[string]object, ListMeta, error) { @@ -18,11 +18,11 @@ func parseServiceList(r io.Reader) (map[string]object, ListMeta, error) { if err := d.Decode(&sl); err != nil { return nil, sl.Metadata, fmt.Errorf("cannot unmarshal ServiceList: %w", err) } - objectsByKey := make(map[string]object) + objectsByName := make(map[string]object) for _, s := range sl.Items { - objectsByKey[s.key()] = s + objectsByName[s.name()] = s } - return objectsByKey, sl.Metadata, nil + return objectsByName, sl.Metadata, nil } func parseService(data []byte) (object, error) { From 5b08e6fb1695429c1785aeac67ff54fb34e3b911 Mon Sep 17 00:00:00 2001 From: Aliaksandr Valialkin Date: Fri, 2 Apr 2021 14:45:08 +0300 Subject: [PATCH 15/63] lib/promscrape/discovery/kubernetes: properly track objects with the same names in multiple namespaces This is a follow-up for 12e4785fe8dc64d6c4422725e833cefdab49d8f6 Updates https://github.com/VictoriaMetrics/VictoriaMetrics/issues/1170 --- .../discovery/kubernetes/api_watcher.go | 119 +++++++++--------- .../discovery/kubernetes/common_types.go | 4 + .../discovery/kubernetes/endpoints.go | 10 +- .../discovery/kubernetes/endpointslices.go | 10 +- .../discovery/kubernetes/ingress.go | 10 +- lib/promscrape/discovery/kubernetes/node.go | 11 +- lib/promscrape/discovery/kubernetes/pod.go | 10 +- .../discovery/kubernetes/service.go | 10 +- 8 files changed, 95 insertions(+), 89 deletions(-) diff --git a/lib/promscrape/discovery/kubernetes/api_watcher.go b/lib/promscrape/discovery/kubernetes/api_watcher.go index 4397676bc..e9f096e72 100644 --- a/lib/promscrape/discovery/kubernetes/api_watcher.go +++ b/lib/promscrape/discovery/kubernetes/api_watcher.go @@ -32,7 +32,7 @@ type WatchEvent struct { // object is any Kubernetes object. type object interface { - name() string + key() string getTargetLabels(gw *groupWatcher) []map[string]string } @@ -79,37 +79,37 @@ func (aw *apiWatcher) mustStop() { aw.swosByNamespaceLock.Unlock() } -func (aw *apiWatcher) reloadScrapeWorks(namespace string, swosByName map[string][]interface{}) { +func (aw *apiWatcher) reloadScrapeWorks(namespace string, swosByKey map[string][]interface{}) { aw.swosByNamespaceLock.Lock() - aw.swosCount.Add(len(swosByName) - len(aw.swosByNamespace[namespace])) - aw.swosByNamespace[namespace] = swosByName + aw.swosCount.Add(len(swosByKey) - len(aw.swosByNamespace[namespace])) + aw.swosByNamespace[namespace] = swosByKey aw.swosByNamespaceLock.Unlock() } -func (aw *apiWatcher) setScrapeWorks(namespace, name string, labels []map[string]string) { +func (aw *apiWatcher) setScrapeWorks(namespace, key string, labels []map[string]string) { swos := getScrapeWorkObjectsForLabels(aw.swcFunc, labels) aw.swosByNamespaceLock.Lock() - swosByName := aw.swosByNamespace[namespace] - if swosByName == nil { - swosByName = make(map[string][]interface{}) - aw.swosByNamespace[namespace] = swosByName + swosByKey := aw.swosByNamespace[namespace] + if swosByKey == nil { + swosByKey = make(map[string][]interface{}) + aw.swosByNamespace[namespace] = swosByKey } if len(swos) > 0 { - aw.swosCount.Add(len(swos) - len(swosByName[name])) - swosByName[name] = swos + aw.swosCount.Add(len(swos) - len(swosByKey[key])) + swosByKey[key] = swos } else { - aw.swosCount.Add(-len(swosByName[name])) - delete(swosByName, name) + aw.swosCount.Add(-len(swosByKey[key])) + delete(swosByKey, key) } aw.swosByNamespaceLock.Unlock() } -func (aw *apiWatcher) removeScrapeWorks(namespace, name string) { +func (aw *apiWatcher) removeScrapeWorks(namespace, key string) { aw.swosByNamespaceLock.Lock() - swosByName := aw.swosByNamespace[namespace] - if len(swosByName) > 0 { - aw.swosCount.Add(-len(swosByName[name])) - delete(swosByName, name) + swosByKey := aw.swosByNamespace[namespace] + if len(swosByKey) > 0 { + aw.swosCount.Add(-len(swosByKey[key])) + delete(swosByKey, key) } aw.swosByNamespaceLock.Unlock() } @@ -133,14 +133,14 @@ func (aw *apiWatcher) getScrapeWorkObjects() []interface{} { defer aw.swosByNamespaceLock.Unlock() size := 0 - for _, swosByName := range aw.swosByNamespace { - for _, swosLocal := range swosByName { + for _, swosByKey := range aw.swosByNamespace { + for _, swosLocal := range swosByKey { size += len(swosLocal) } } swos := make([]interface{}, 0, size) - for _, swosByName := range aw.swosByNamespace { - for _, swosLocal := range swosByName { + for _, swosByKey := range aw.swosByNamespace { + for _, swosLocal := range swosByKey { swos = append(swos, swosLocal...) } } @@ -223,6 +223,7 @@ func (gw *groupWatcher) getObjectByRole(role, namespace, name string) object { // this is needed for testing return nil } + key := namespace + "/" + name gw.startWatchersForRole(role, nil) gw.mu.Lock() defer gw.mu.Unlock() @@ -237,7 +238,7 @@ func (gw *groupWatcher) getObjectByRole(role, namespace, name string) object { continue } uw.mu.Lock() - o := uw.objectsByName[name] + o := uw.objectsByKey[key] uw.mu.Unlock() if o != nil { return o @@ -261,25 +262,25 @@ func (gw *groupWatcher) startWatchersForRole(role string, aw *apiWatcher) { } } -func (gw *groupWatcher) reloadScrapeWorksForAPIWatchers(namespace string, aws []*apiWatcher, objectsByName map[string]object) { +func (gw *groupWatcher) reloadScrapeWorksForAPIWatchers(namespace string, aws []*apiWatcher, objectsByKey map[string]object) { if len(aws) == 0 { return } - swosByName := make([]map[string][]interface{}, len(aws)) + swosByKey := make([]map[string][]interface{}, len(aws)) for i := range aws { - swosByName[i] = make(map[string][]interface{}) + swosByKey[i] = make(map[string][]interface{}) } - for name, o := range objectsByName { + for key, o := range objectsByKey { labels := o.getTargetLabels(gw) for i, aw := range aws { swos := getScrapeWorkObjectsForLabels(aw.swcFunc, labels) if len(swos) > 0 { - swosByName[i][name] = swos + swosByKey[i][key] = swos } } } for i, aw := range aws { - aw.reloadScrapeWorks(namespace, swosByName[i]) + aw.reloadScrapeWorks(namespace, swosByKey[i]) } } @@ -303,7 +304,7 @@ func (gw *groupWatcher) unsubscribeAPIWatcher(aw *apiWatcher) { gw.mu.Unlock() } -// urlWatcher watches for an apiURL and updates object states in objectsByName. +// urlWatcher watches for an apiURL and updates object states in objectsByKey. type urlWatcher struct { role string namespace string @@ -313,7 +314,7 @@ type urlWatcher struct { parseObject parseObjectFunc parseObjectList parseObjectListFunc - // mu protects aws, awsPending, objectsByName and resourceVersion + // mu protects aws, awsPending, objectsByKey and resourceVersion mu sync.Mutex // aws contains registered apiWatcher objects @@ -322,8 +323,8 @@ type urlWatcher struct { // awsPending contains pending apiWatcher objects, which must be moved to aws in a batch awsPending map[*apiWatcher]struct{} - // objectsByName contains the latest state for objects obtained from apiURL - objectsByName map[string]object + // objectsByKey contains the latest state for objects obtained from apiURL + objectsByKey map[string]object resourceVersion string @@ -346,9 +347,9 @@ func newURLWatcher(role, namespace, apiURL string, gw *groupWatcher) *urlWatcher parseObject: parseObject, parseObjectList: parseObjectList, - aws: make(map[*apiWatcher]struct{}), - awsPending: make(map[*apiWatcher]struct{}), - objectsByName: make(map[string]object), + aws: make(map[*apiWatcher]struct{}), + awsPending: make(map[*apiWatcher]struct{}), + objectsByKey: make(map[string]object), objectsCount: metrics.GetOrCreateCounter(fmt.Sprintf(`vm_promscrape_discovery_kubernetes_objects{role=%q}`, role)), objectsAdded: metrics.GetOrCreateCounter(fmt.Sprintf(`vm_promscrape_discovery_kubernetes_objects_added_total{role=%q}`, role)), @@ -392,7 +393,7 @@ func (uw *urlWatcher) processPendingSubscribers() { t := time.NewTicker(time.Second) for range t.C { var awsPending []*apiWatcher - var objectsByName map[string]object + var objectsByKey map[string]object uw.mu.Lock() if len(uw.awsPending) > 0 { @@ -404,16 +405,16 @@ func (uw *urlWatcher) processPendingSubscribers() { uw.aws[aw] = struct{}{} delete(uw.awsPending, aw) } - objectsByName = make(map[string]object, len(uw.objectsByName)) - for name, o := range uw.objectsByName { - objectsByName[name] = o + objectsByKey = make(map[string]object, len(uw.objectsByKey)) + for key, o := range uw.objectsByKey { + objectsByKey[key] = o } } metrics.GetOrCreateCounter(fmt.Sprintf(`vm_promscrape_discovery_kubernetes_subscibers{role=%q,type="pending"}`, uw.role)).Add(-len(awsPending)) metrics.GetOrCreateCounter(fmt.Sprintf(`vm_promscrape_discovery_kubernetes_subscibers{role=%q,type="permanent"}`, uw.role)).Add(len(awsPending)) uw.mu.Unlock() - uw.gw.reloadScrapeWorksForAPIWatchers(uw.namespace, awsPending, objectsByName) + uw.gw.reloadScrapeWorksForAPIWatchers(uw.namespace, awsPending, objectsByKey) } } @@ -445,7 +446,7 @@ func (uw *urlWatcher) reloadObjects() string { logger.Errorf("unexpected status code for request to %q: %d; want %d; response: %q", requestURL, resp.StatusCode, http.StatusOK, body) return "" } - objectsByName, metadata, err := uw.parseObjectList(resp.Body) + objectsByKey, metadata, err := uw.parseObjectList(resp.Body) _ = resp.Body.Close() if err != nil { logger.Errorf("cannot parse objects from %q: %s", requestURL, err) @@ -454,18 +455,18 @@ func (uw *urlWatcher) reloadObjects() string { uw.mu.Lock() var updated, removed, added int - for name := range uw.objectsByName { - if o, ok := objectsByName[name]; ok { - uw.objectsByName[name] = o + for key := range uw.objectsByKey { + if o, ok := objectsByKey[key]; ok { + uw.objectsByKey[key] = o updated++ } else { - delete(uw.objectsByName, name) + delete(uw.objectsByKey, key) removed++ } } - for name, o := range objectsByName { - if _, ok := uw.objectsByName[name]; !ok { - uw.objectsByName[name] = o + for key, o := range objectsByKey { + if _, ok := uw.objectsByKey[key]; !ok { + uw.objectsByKey[key] = o added++ } } @@ -477,8 +478,8 @@ func (uw *urlWatcher) reloadObjects() string { aws := getAPIWatchers(uw.aws) uw.mu.Unlock() - uw.gw.reloadScrapeWorksForAPIWatchers(uw.namespace, aws, objectsByName) - logger.Infof("reloaded %d objects from %q", len(objectsByName), requestURL) + uw.gw.reloadScrapeWorksForAPIWatchers(uw.namespace, aws, objectsByKey) + logger.Infof("reloaded %d objects from %q", len(objectsByKey), requestURL) return metadata.ResourceVersion } @@ -564,37 +565,37 @@ func (uw *urlWatcher) readObjectUpdateStream(r io.Reader) error { if err != nil { return err } - name := o.name() + key := o.key() uw.mu.Lock() - if _, ok := uw.objectsByName[name]; !ok { + if _, ok := uw.objectsByKey[key]; !ok { uw.objectsCount.Inc() uw.objectsAdded.Inc() } else { uw.objectsUpdated.Inc() } - uw.objectsByName[name] = o + uw.objectsByKey[key] = o aws := getAPIWatchers(uw.aws) uw.mu.Unlock() labels := o.getTargetLabels(uw.gw) for _, aw := range aws { - aw.setScrapeWorks(uw.namespace, name, labels) + aw.setScrapeWorks(uw.namespace, key, labels) } case "DELETED": o, err := uw.parseObject(we.Object) if err != nil { return err } - name := o.name() + key := o.key() uw.mu.Lock() - if _, ok := uw.objectsByName[name]; ok { + if _, ok := uw.objectsByKey[key]; ok { uw.objectsCount.Dec() uw.objectsRemoved.Inc() - delete(uw.objectsByName, name) + delete(uw.objectsByKey, key) } aws := getAPIWatchers(uw.aws) uw.mu.Unlock() for _, aw := range aws { - aw.removeScrapeWorks(uw.namespace, name) + aw.removeScrapeWorks(uw.namespace, key) } case "BOOKMARK": // See https://kubernetes.io/docs/reference/using-api/api-concepts/#watch-bookmarks diff --git a/lib/promscrape/discovery/kubernetes/common_types.go b/lib/promscrape/discovery/kubernetes/common_types.go index 5eab6e4d1..be93bbb4a 100644 --- a/lib/promscrape/discovery/kubernetes/common_types.go +++ b/lib/promscrape/discovery/kubernetes/common_types.go @@ -16,6 +16,10 @@ type ObjectMeta struct { OwnerReferences []OwnerReference } +func (om *ObjectMeta) key() string { + return om.Namespace + "/" + om.Name +} + // ListMeta is a Kubernetes list metadata // https://v1-17.docs.kubernetes.io/docs/reference/generated/kubernetes-api/v1.17/#listmeta-v1-meta type ListMeta struct { diff --git a/lib/promscrape/discovery/kubernetes/endpoints.go b/lib/promscrape/discovery/kubernetes/endpoints.go index 032eebb29..805a88b01 100644 --- a/lib/promscrape/discovery/kubernetes/endpoints.go +++ b/lib/promscrape/discovery/kubernetes/endpoints.go @@ -8,8 +8,8 @@ import ( "github.com/VictoriaMetrics/VictoriaMetrics/lib/promscrape/discoveryutils" ) -func (eps *Endpoints) name() string { - return eps.Metadata.Name +func (eps *Endpoints) key() string { + return eps.Metadata.key() } func parseEndpointsList(r io.Reader) (map[string]object, ListMeta, error) { @@ -18,11 +18,11 @@ func parseEndpointsList(r io.Reader) (map[string]object, ListMeta, error) { if err := d.Decode(&epsl); err != nil { return nil, epsl.Metadata, fmt.Errorf("cannot unmarshal EndpointsList: %w", err) } - objectsByName := make(map[string]object) + objectsByKey := make(map[string]object) for _, eps := range epsl.Items { - objectsByName[eps.name()] = eps + objectsByKey[eps.key()] = eps } - return objectsByName, epsl.Metadata, nil + return objectsByKey, epsl.Metadata, nil } func parseEndpoints(data []byte) (object, error) { diff --git a/lib/promscrape/discovery/kubernetes/endpointslices.go b/lib/promscrape/discovery/kubernetes/endpointslices.go index bf4f96f3e..5e1961e92 100644 --- a/lib/promscrape/discovery/kubernetes/endpointslices.go +++ b/lib/promscrape/discovery/kubernetes/endpointslices.go @@ -9,8 +9,8 @@ import ( "github.com/VictoriaMetrics/VictoriaMetrics/lib/promscrape/discoveryutils" ) -func (eps *EndpointSlice) name() string { - return eps.Metadata.Name +func (eps *EndpointSlice) key() string { + return eps.Metadata.key() } func parseEndpointSliceList(r io.Reader) (map[string]object, ListMeta, error) { @@ -19,11 +19,11 @@ func parseEndpointSliceList(r io.Reader) (map[string]object, ListMeta, error) { if err := d.Decode(&epsl); err != nil { return nil, epsl.Metadata, fmt.Errorf("cannot unmarshal EndpointSliceList: %w", err) } - objectsByName := make(map[string]object) + objectsByKey := make(map[string]object) for _, eps := range epsl.Items { - objectsByName[eps.name()] = eps + objectsByKey[eps.key()] = eps } - return objectsByName, epsl.Metadata, nil + return objectsByKey, epsl.Metadata, nil } func parseEndpointSlice(data []byte) (object, error) { diff --git a/lib/promscrape/discovery/kubernetes/ingress.go b/lib/promscrape/discovery/kubernetes/ingress.go index 9a99b267f..cca6bdb24 100644 --- a/lib/promscrape/discovery/kubernetes/ingress.go +++ b/lib/promscrape/discovery/kubernetes/ingress.go @@ -6,8 +6,8 @@ import ( "io" ) -func (ig *Ingress) name() string { - return ig.Metadata.Name +func (ig *Ingress) key() string { + return ig.Metadata.key() } func parseIngressList(r io.Reader) (map[string]object, ListMeta, error) { @@ -16,11 +16,11 @@ func parseIngressList(r io.Reader) (map[string]object, ListMeta, error) { if err := d.Decode(&igl); err != nil { return nil, igl.Metadata, fmt.Errorf("cannot unmarshal IngressList: %w", err) } - objectsByName := make(map[string]object) + objectsByKey := make(map[string]object) for _, ig := range igl.Items { - objectsByName[ig.name()] = ig + objectsByKey[ig.key()] = ig } - return objectsByName, igl.Metadata, nil + return objectsByKey, igl.Metadata, nil } func parseIngress(data []byte) (object, error) { diff --git a/lib/promscrape/discovery/kubernetes/node.go b/lib/promscrape/discovery/kubernetes/node.go index a1d07f96d..6c990c846 100644 --- a/lib/promscrape/discovery/kubernetes/node.go +++ b/lib/promscrape/discovery/kubernetes/node.go @@ -8,8 +8,9 @@ import ( "github.com/VictoriaMetrics/VictoriaMetrics/lib/promscrape/discoveryutils" ) -func (n *Node) name() string { - return n.Metadata.Name +// getNodesLabels returns labels for k8s nodes obtained from the given cfg +func (n *Node) key() string { + return n.Metadata.key() } func parseNodeList(r io.Reader) (map[string]object, ListMeta, error) { @@ -18,11 +19,11 @@ func parseNodeList(r io.Reader) (map[string]object, ListMeta, error) { if err := d.Decode(&nl); err != nil { return nil, nl.Metadata, fmt.Errorf("cannot unmarshal NodeList: %w", err) } - objectsByName := make(map[string]object) + objectsByKey := make(map[string]object) for _, n := range nl.Items { - objectsByName[n.name()] = n + objectsByKey[n.key()] = n } - return objectsByName, nl.Metadata, nil + return objectsByKey, nl.Metadata, nil } func parseNode(data []byte) (object, error) { diff --git a/lib/promscrape/discovery/kubernetes/pod.go b/lib/promscrape/discovery/kubernetes/pod.go index 56280523c..8a88ffaca 100644 --- a/lib/promscrape/discovery/kubernetes/pod.go +++ b/lib/promscrape/discovery/kubernetes/pod.go @@ -10,8 +10,8 @@ import ( "github.com/VictoriaMetrics/VictoriaMetrics/lib/promscrape/discoveryutils" ) -func (p *Pod) name() string { - return p.Metadata.Name +func (p *Pod) key() string { + return p.Metadata.key() } func parsePodList(r io.Reader) (map[string]object, ListMeta, error) { @@ -20,11 +20,11 @@ func parsePodList(r io.Reader) (map[string]object, ListMeta, error) { if err := d.Decode(&pl); err != nil { return nil, pl.Metadata, fmt.Errorf("cannot unmarshal PodList: %w", err) } - objectsByName := make(map[string]object) + objectsByKey := make(map[string]object) for _, p := range pl.Items { - objectsByName[p.name()] = p + objectsByKey[p.key()] = p } - return objectsByName, pl.Metadata, nil + return objectsByKey, pl.Metadata, nil } func parsePod(data []byte) (object, error) { diff --git a/lib/promscrape/discovery/kubernetes/service.go b/lib/promscrape/discovery/kubernetes/service.go index d140992b0..b74bd2653 100644 --- a/lib/promscrape/discovery/kubernetes/service.go +++ b/lib/promscrape/discovery/kubernetes/service.go @@ -8,8 +8,8 @@ import ( "github.com/VictoriaMetrics/VictoriaMetrics/lib/promscrape/discoveryutils" ) -func (s *Service) name() string { - return s.Metadata.Name +func (s *Service) key() string { + return s.Metadata.key() } func parseServiceList(r io.Reader) (map[string]object, ListMeta, error) { @@ -18,11 +18,11 @@ func parseServiceList(r io.Reader) (map[string]object, ListMeta, error) { if err := d.Decode(&sl); err != nil { return nil, sl.Metadata, fmt.Errorf("cannot unmarshal ServiceList: %w", err) } - objectsByName := make(map[string]object) + objectsByKey := make(map[string]object) for _, s := range sl.Items { - objectsByName[s.name()] = s + objectsByKey[s.key()] = s } - return objectsByName, sl.Metadata, nil + return objectsByKey, sl.Metadata, nil } func parseService(data []byte) (object, error) { From c79e4a2f909f0bd64d0d7e2ea77f340d68e0b307 Mon Sep 17 00:00:00 2001 From: Aliaksandr Valialkin Date: Fri, 2 Apr 2021 15:02:00 +0300 Subject: [PATCH 16/63] app/vmselect/promql: remove the limit on the number of time series that can be sorted, since it may confuse users Always sort time series returned from `/api/v1/query` and `/api/v1/query_range` unless `sort_*` function is used at top level of the query. --- app/vmselect/promql/exec.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/app/vmselect/promql/exec.go b/app/vmselect/promql/exec.go index 25e9953d9..e46a20647 100644 --- a/app/vmselect/promql/exec.go +++ b/app/vmselect/promql/exec.go @@ -85,10 +85,6 @@ func Exec(ec *EvalConfig, q string, isFirstPointOnly bool) ([]netstorage.Result, } func maySortResults(e metricsql.Expr, tss []*timeseries) bool { - if len(tss) > 100 { - // There is no sense in sorting a lot of results - return false - } fe, ok := e.(*metricsql.FuncExpr) if !ok { return true From d1dcbfd0f9d24c9756cc51282f6ce3a2b11947e5 Mon Sep 17 00:00:00 2001 From: Aliaksandr Valialkin Date: Fri, 2 Apr 2021 19:22:30 +0300 Subject: [PATCH 17/63] deployment/docker: upgrade Go builder from v1.16.2 to v1.16.3 See https://github.com/golang/go/issues?q=milestone%3AGo1.16.3+label%3ACherryPickApproved --- deployment/docker/Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deployment/docker/Makefile b/deployment/docker/Makefile index e8efa986b..7a8059cc8 100644 --- a/deployment/docker/Makefile +++ b/deployment/docker/Makefile @@ -4,7 +4,7 @@ DOCKER_NAMESPACE := victoriametrics ROOT_IMAGE ?= alpine:3.13.2 CERTS_IMAGE := alpine:3.13.2 -GO_BUILDER_IMAGE := golang:1.16.2 +GO_BUILDER_IMAGE := golang:1.16.3 BUILDER_IMAGE := local/builder:2.0.0-$(shell echo $(GO_BUILDER_IMAGE) | tr : _) BASE_IMAGE := local/base:1.1.3-$(shell echo $(ROOT_IMAGE) | tr : _)-$(shell echo $(CERTS_IMAGE) | tr : _) From 7f9c68cdcbfe7ed9ec6544ac77b0bdd2a4b9d3d0 Mon Sep 17 00:00:00 2001 From: Aliaksandr Valialkin Date: Fri, 2 Apr 2021 19:56:38 +0300 Subject: [PATCH 18/63] lib/promscrape: add `follow_redirect` option to `scrape_configs` section like Prometheus does See https://github.com/prometheus/prometheus/pull/8546 --- docs/CHANGELOG.md | 2 ++ lib/promscrape/client.go | 25 ++++++++++++++++++------- lib/promscrape/config.go | 8 ++++++++ lib/promscrape/config_test.go | 3 +++ lib/promscrape/scrapework.go | 7 +++++-- 5 files changed, 36 insertions(+), 9 deletions(-) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index dafce3a4e..31b9900e5 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -3,6 +3,8 @@ # tip * FEATURE: vminsert and vmagent: add `-sortLabels` command-line flag for sorting metric labels before pushing them to `vmstorage`. This should reduce the size of `MetricName -> internal_series_id` cache (aka `vm_cache_size_bytes{type="storage/tsid"}`) when ingesting samples for the same time series with distinct order of labels. For example, `foo{k1="v1",k2="v2"}` and `foo{k2="v2",k1="v1"}` represent a single time series. +* FEATURE: update Go builder from `v1.16.2` to `v1.16.3`. This should fix [these issues](https://github.com/golang/go/issues?q=milestone%3AGo1.16.3+label%3ACherryPickApproved). +* FEATURE: vmagent: add support for `follow_redirects` option to `scrape_configs` section in the same way as [Prometheus does](https://github.com/prometheus/prometheus/pull/8546). * FEATURE: vmagent: reduce memory usage when `-remoteWrite.queues` is set to a big value. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/1167). * FEATURE: vmagent: add AWS IAM roles for tasks support for EC2 service discovery according to [these docs](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-iam-roles.html). diff --git a/lib/promscrape/client.go b/lib/promscrape/client.go index 0f2ddd202..1f6f31421 100644 --- a/lib/promscrape/client.go +++ b/lib/promscrape/client.go @@ -46,6 +46,7 @@ type client struct { host string requestURI string authHeader string + denyRedirects bool disableCompression bool disableKeepAlive bool } @@ -101,6 +102,11 @@ func newClient(sw *ScrapeWork) *client { }, Timeout: sw.ScrapeTimeout, } + if sw.DenyRedirects { + sc.CheckRedirect = func(req *http.Request, via []*http.Request) error { + return http.ErrUseLastResponse + } + } } return &client{ hc: hc, @@ -109,6 +115,7 @@ func newClient(sw *ScrapeWork) *client { host: host, requestURI: requestURI, authHeader: sw.AuthConfig.Authorization, + denyRedirects: sw.DenyRedirects, disableCompression: sw.DisableCompression, disableKeepAlive: sw.DisableKeepAlive, } @@ -181,13 +188,17 @@ func (c *client) ReadData(dst []byte) ([]byte, error) { err := doRequestWithPossibleRetry(c.hc, req, resp, deadline) statusCode := resp.StatusCode() if err == nil && (statusCode == fasthttp.StatusMovedPermanently || statusCode == fasthttp.StatusFound) { - // Allow a single redirect. - // It is expected that the redirect is made on the same host. - // Otherwise it won't work. - if location := resp.Header.Peek("Location"); len(location) > 0 { - req.URI().UpdateBytes(location) - err = c.hc.DoDeadline(req, resp, deadline) - statusCode = resp.StatusCode() + if c.denyRedirects { + err = fmt.Errorf("cannot follow redirects if `follow_redirects: false` is set") + } else { + // Allow a single redirect. + // It is expected that the redirect is made on the same host. + // Otherwise it won't work. + if location := resp.Header.Peek("Location"); len(location) > 0 { + req.URI().UpdateBytes(location) + err = c.hc.DoDeadline(req, resp, deadline) + statusCode = resp.StatusCode() + } } } if swapResponseBodies { diff --git a/lib/promscrape/config.go b/lib/promscrape/config.go index a65f651c3..d6f019e5e 100644 --- a/lib/promscrape/config.go +++ b/lib/promscrape/config.go @@ -88,6 +88,7 @@ type ScrapeConfig struct { MetricsPath string `yaml:"metrics_path,omitempty"` HonorLabels bool `yaml:"honor_labels,omitempty"` HonorTimestamps bool `yaml:"honor_timestamps,omitempty"` + FollowRedirects *bool `yaml:"follow_redirects"` // omitempty isn't set, since the default value for this flag is true. Scheme string `yaml:"scheme,omitempty"` Params map[string][]string `yaml:"params,omitempty"` BasicAuth *promauth.BasicAuthConfig `yaml:"basic_auth,omitempty"` @@ -531,6 +532,10 @@ func getScrapeWorkConfig(sc *ScrapeConfig, baseDir string, globalCfg *GlobalConf } honorLabels := sc.HonorLabels honorTimestamps := sc.HonorTimestamps + denyRedirects := false + if sc.FollowRedirects != nil { + denyRedirects = !*sc.FollowRedirects + } metricsPath := sc.MetricsPath if metricsPath == "" { metricsPath = "/metrics" @@ -571,6 +576,7 @@ func getScrapeWorkConfig(sc *ScrapeConfig, baseDir string, globalCfg *GlobalConf authConfig: ac, honorLabels: honorLabels, honorTimestamps: honorTimestamps, + denyRedirects: denyRedirects, externalLabels: globalCfg.ExternalLabels, relabelConfigs: relabelConfigs, metricRelabelConfigs: metricRelabelConfigs, @@ -596,6 +602,7 @@ type scrapeWorkConfig struct { authConfig *promauth.Config honorLabels bool honorTimestamps bool + denyRedirects bool externalLabels map[string]string relabelConfigs *promrelabel.ParsedConfigs metricRelabelConfigs *promrelabel.ParsedConfigs @@ -856,6 +863,7 @@ func (swc *scrapeWorkConfig) getScrapeWork(target string, extraLabels, metaLabel ScrapeTimeout: swc.scrapeTimeout, HonorLabels: swc.honorLabels, HonorTimestamps: swc.honorTimestamps, + DenyRedirects: swc.denyRedirects, OriginalLabels: originalLabels, Labels: labels, ProxyURL: swc.proxyURL, diff --git a/lib/promscrape/config_test.go b/lib/promscrape/config_test.go index ac7ce98fb..93e5ddcba 100644 --- a/lib/promscrape/config_test.go +++ b/lib/promscrape/config_test.go @@ -751,6 +751,7 @@ scrape_configs: scheme: https honor_labels: true honor_timestamps: true + follow_redirects: false params: p: ["x&y", "="] xaa: @@ -779,6 +780,7 @@ scrape_configs: ScrapeTimeout: 12 * time.Second, HonorLabels: true, HonorTimestamps: true, + DenyRedirects: true, Labels: []prompbmarshal.Label{ { Name: "__address__", @@ -824,6 +826,7 @@ scrape_configs: ScrapeTimeout: 12 * time.Second, HonorLabels: true, HonorTimestamps: true, + DenyRedirects: true, Labels: []prompbmarshal.Label{ { Name: "__address__", diff --git a/lib/promscrape/scrapework.go b/lib/promscrape/scrapework.go index b9da864d5..3a0948298 100644 --- a/lib/promscrape/scrapework.go +++ b/lib/promscrape/scrapework.go @@ -48,6 +48,9 @@ type ScrapeWork struct { // See https://prometheus.io/docs/prometheus/latest/configuration/configuration/#scrape_config HonorTimestamps bool + // Whether to deny redirects during requests to scrape config. + DenyRedirects bool + // OriginalLabels contains original labels before relabeling. // // These labels are needed for relabeling troubleshooting at /targets page. @@ -107,10 +110,10 @@ type ScrapeWork struct { // it can be used for comparing for equality for two ScrapeWork objects. func (sw *ScrapeWork) key() string { // Do not take into account OriginalLabels. - key := fmt.Sprintf("ScrapeURL=%s, ScrapeInterval=%s, ScrapeTimeout=%s, HonorLabels=%v, HonorTimestamps=%v, Labels=%s, "+ + key := fmt.Sprintf("ScrapeURL=%s, ScrapeInterval=%s, ScrapeTimeout=%s, HonorLabels=%v, HonorTimestamps=%v, DenyRedirects=%v, Labels=%s, "+ "ProxyURL=%s, ProxyAuthConfig=%s, AuthConfig=%s, MetricRelabelConfigs=%s, SampleLimit=%d, DisableCompression=%v, DisableKeepAlive=%v, StreamParse=%v, "+ "ScrapeAlignInterval=%s, ScrapeOffset=%s", - sw.ScrapeURL, sw.ScrapeInterval, sw.ScrapeTimeout, sw.HonorLabels, sw.HonorTimestamps, sw.LabelsString(), + sw.ScrapeURL, sw.ScrapeInterval, sw.ScrapeTimeout, sw.HonorLabels, sw.HonorTimestamps, sw.DenyRedirects, sw.LabelsString(), sw.ProxyURL.String(), sw.ProxyAuthConfig.String(), sw.AuthConfig.String(), sw.MetricRelabelConfigs.String(), sw.SampleLimit, sw.DisableCompression, sw.DisableKeepAlive, sw.StreamParse, sw.ScrapeAlignInterval, sw.ScrapeOffset) From df148f48b749da302494f1f546159e237ba75032 Mon Sep 17 00:00:00 2001 From: Aliaksandr Valialkin Date: Fri, 2 Apr 2021 21:17:43 +0300 Subject: [PATCH 19/63] lib/promscrape: add support for `authorization` config in `-promscrape.config` as Prometheus 2.26 does See https://github.com/prometheus/prometheus/pull/8512 --- app/vmagent/remotewrite/client.go | 2 +- docs/CHANGELOG.md | 3 +- lib/promauth/config.go | 54 +++++++++++++++++-- lib/promscrape/config.go | 33 ++++++------ lib/promscrape/discovery/consul/api.go | 2 +- lib/promscrape/discovery/dockerswarm/api.go | 4 +- .../discovery/dockerswarm/dockerswarm.go | 7 +-- lib/promscrape/discovery/eureka/api.go | 21 ++------ lib/promscrape/discovery/eureka/eureka.go | 22 +++----- .../discovery/eureka/eureka_test.go | 9 ++-- lib/promscrape/discovery/kubernetes/api.go | 6 +-- .../discovery/kubernetes/kubernetes.go | 15 +++--- lib/promscrape/discovery/openstack/api.go | 2 +- 13 files changed, 100 insertions(+), 80 deletions(-) diff --git a/app/vmagent/remotewrite/client.go b/app/vmagent/remotewrite/client.go index f56ba3a8a..580d2625e 100644 --- a/app/vmagent/remotewrite/client.go +++ b/app/vmagent/remotewrite/client.go @@ -160,7 +160,7 @@ func getTLSConfig(argIdx int) (*tls.Config, error) { if c.CAFile == "" && c.CertFile == "" && c.KeyFile == "" && c.ServerName == "" && !c.InsecureSkipVerify { return nil, nil } - cfg, err := promauth.NewConfig(".", nil, "", "", c) + cfg, err := promauth.NewConfig(".", nil, nil, "", "", c) if err != nil { return nil, fmt.Errorf("cannot populate TLS config: %w", err) } diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 31b9900e5..86c60f132 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -4,7 +4,8 @@ * FEATURE: vminsert and vmagent: add `-sortLabels` command-line flag for sorting metric labels before pushing them to `vmstorage`. This should reduce the size of `MetricName -> internal_series_id` cache (aka `vm_cache_size_bytes{type="storage/tsid"}`) when ingesting samples for the same time series with distinct order of labels. For example, `foo{k1="v1",k2="v2"}` and `foo{k2="v2",k1="v1"}` represent a single time series. * FEATURE: update Go builder from `v1.16.2` to `v1.16.3`. This should fix [these issues](https://github.com/golang/go/issues?q=milestone%3AGo1.16.3+label%3ACherryPickApproved). -* FEATURE: vmagent: add support for `follow_redirects` option to `scrape_configs` section in the same way as [Prometheus does](https://github.com/prometheus/prometheus/pull/8546). +* FEATURE: vmagent: add support for `follow_redirects` option to `scrape_configs` section in the same way as [Prometheus 2.26 does](https://github.com/prometheus/prometheus/pull/8546). +* FEATURE: vmagent: add support for `authorization` section in `-promscrape.config` in the same way as [Prometheus 2.26 does](https://github.com/prometheus/prometheus/pull/8512). * FEATURE: vmagent: reduce memory usage when `-remoteWrite.queues` is set to a big value. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/1167). * FEATURE: vmagent: add AWS IAM roles for tasks support for EC2 service discovery according to [these docs](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-iam-roles.html). diff --git a/lib/promauth/config.go b/lib/promauth/config.go index f80b15ae2..1797aa365 100644 --- a/lib/promauth/config.go +++ b/lib/promauth/config.go @@ -20,6 +20,15 @@ type TLSConfig struct { InsecureSkipVerify bool `yaml:"insecure_skip_verify,omitempty"` } +// Authorization represents generic authorization config. +// +// See https://prometheus.io/docs/prometheus/latest/configuration/configuration/ +type Authorization struct { + Type string `yaml:"type,omitempty"` + Credentials string `yaml:"credentials,omitempty"` + CredentialsFile string `yaml:"credentials_file,omitempty"` +} + // BasicAuthConfig represents basic auth config. type BasicAuthConfig struct { Username string `yaml:"username"` @@ -27,6 +36,15 @@ type BasicAuthConfig struct { PasswordFile string `yaml:"password_file,omitempty"` } +// HTTPClientConfig represents http client config. +type HTTPClientConfig struct { + Authorization *Authorization `yaml:"authorization,omitempty"` + BasicAuth *BasicAuthConfig `yaml:"basic_auth,omitempty"` + BearerToken string `yaml:"bearer_token,omitempty"` + BearerTokenFile string `yaml:"bearer_token_file,omitempty"` + TLSConfig *TLSConfig `yaml:"tls_config,omitempty"` +} + // Config is auth config. type Config struct { // Optional `Authorization` header. @@ -80,10 +98,37 @@ func (ac *Config) NewTLSConfig() *tls.Config { return tlsCfg } +// NewConfig creates auth config for the given hcc. +func (hcc *HTTPClientConfig) NewConfig(baseDir string) (*Config, error) { + return NewConfig(baseDir, hcc.Authorization, hcc.BasicAuth, hcc.BearerToken, hcc.BearerTokenFile, hcc.TLSConfig) +} + // NewConfig creates auth config from the given args. -func NewConfig(baseDir string, basicAuth *BasicAuthConfig, bearerToken, bearerTokenFile string, tlsConfig *TLSConfig) (*Config, error) { +func NewConfig(baseDir string, az *Authorization, basicAuth *BasicAuthConfig, bearerToken, bearerTokenFile string, tlsConfig *TLSConfig) (*Config, error) { var authorization string + if az != nil { + azType := "Bearer" + if az.Type != "" { + azType = az.Type + } + azToken := az.Credentials + if az.CredentialsFile != "" { + if az.Credentials != "" { + return nil, fmt.Errorf("both `credentials`=%q and `credentials_file`=%q are set", az.Credentials, az.CredentialsFile) + } + path := getFilepath(baseDir, az.CredentialsFile) + token, err := readPasswordFromFile(path) + if err != nil { + return nil, fmt.Errorf("cannot read credentials from `credentials_file`=%q: %w", az.CredentialsFile, err) + } + azToken = token + } + authorization = azType + " " + azToken + } if basicAuth != nil { + if authorization != "" { + return nil, fmt.Errorf("cannot use both `authorization` and `basic_auth`") + } if basicAuth.Username == "" { return nil, fmt.Errorf("missing `username` in `basic_auth` section") } @@ -106,6 +151,9 @@ func NewConfig(baseDir string, basicAuth *BasicAuthConfig, bearerToken, bearerTo authorization = "Basic " + token64 } if bearerTokenFile != "" { + if authorization != "" { + return nil, fmt.Errorf("cannot simultaneously use `authorization`, `basic_auth` and `bearer_token_file`") + } if bearerToken != "" { return nil, fmt.Errorf("both `bearer_token`=%q and `bearer_token_file`=%q are set", bearerToken, bearerTokenFile) } @@ -114,11 +162,11 @@ func NewConfig(baseDir string, basicAuth *BasicAuthConfig, bearerToken, bearerTo if err != nil { return nil, fmt.Errorf("cannot read bearer token from `bearer_token_file`=%q: %w", bearerTokenFile, err) } - bearerToken = token + authorization = "Bearer " + token } if bearerToken != "" { if authorization != "" { - return nil, fmt.Errorf("cannot use both `basic_auth` and `bearer_token`") + return nil, fmt.Errorf("cannot simultaneously use `authorization`, `basic_auth` and `bearer_token`") } authorization = "Bearer " + bearerToken } diff --git a/lib/promscrape/config.go b/lib/promscrape/config.go index d6f019e5e..2710c0680 100644 --- a/lib/promscrape/config.go +++ b/lib/promscrape/config.go @@ -91,35 +91,34 @@ type ScrapeConfig struct { FollowRedirects *bool `yaml:"follow_redirects"` // omitempty isn't set, since the default value for this flag is true. Scheme string `yaml:"scheme,omitempty"` Params map[string][]string `yaml:"params,omitempty"` - BasicAuth *promauth.BasicAuthConfig `yaml:"basic_auth,omitempty"` - BearerToken string `yaml:"bearer_token,omitempty"` - BearerTokenFile string `yaml:"bearer_token_file,omitempty"` + HTTPClientConfig promauth.HTTPClientConfig `yaml:",inline"` ProxyURL proxy.URL `yaml:"proxy_url,omitempty"` - TLSConfig *promauth.TLSConfig `yaml:"tls_config,omitempty"` - StaticConfigs []StaticConfig `yaml:"static_configs,omitempty"` - FileSDConfigs []FileSDConfig `yaml:"file_sd_configs,omitempty"` - KubernetesSDConfigs []kubernetes.SDConfig `yaml:"kubernetes_sd_configs,omitempty"` - OpenStackSDConfigs []openstack.SDConfig `yaml:"openstack_sd_configs,omitempty"` - ConsulSDConfigs []consul.SDConfig `yaml:"consul_sd_configs,omitempty"` - EurekaSDConfigs []eureka.SDConfig `yaml:"eureka_sd_configs,omitempty"` - DockerSwarmSDConfigs []dockerswarm.SDConfig `yaml:"dockerswarm_sd_configs,omitempty"` - DNSSDConfigs []dns.SDConfig `yaml:"dns_sd_configs,omitempty"` - EC2SDConfigs []ec2.SDConfig `yaml:"ec2_sd_configs,omitempty"` - GCESDConfigs []gce.SDConfig `yaml:"gce_sd_configs,omitempty"` RelabelConfigs []promrelabel.RelabelConfig `yaml:"relabel_configs,omitempty"` MetricRelabelConfigs []promrelabel.RelabelConfig `yaml:"metric_relabel_configs,omitempty"` SampleLimit int `yaml:"sample_limit,omitempty"` + StaticConfigs []StaticConfig `yaml:"static_configs,omitempty"` + FileSDConfigs []FileSDConfig `yaml:"file_sd_configs,omitempty"` + KubernetesSDConfigs []kubernetes.SDConfig `yaml:"kubernetes_sd_configs,omitempty"` + OpenStackSDConfigs []openstack.SDConfig `yaml:"openstack_sd_configs,omitempty"` + ConsulSDConfigs []consul.SDConfig `yaml:"consul_sd_configs,omitempty"` + EurekaSDConfigs []eureka.SDConfig `yaml:"eureka_sd_configs,omitempty"` + DockerSwarmSDConfigs []dockerswarm.SDConfig `yaml:"dockerswarm_sd_configs,omitempty"` + DNSSDConfigs []dns.SDConfig `yaml:"dns_sd_configs,omitempty"` + EC2SDConfigs []ec2.SDConfig `yaml:"ec2_sd_configs,omitempty"` + GCESDConfigs []gce.SDConfig `yaml:"gce_sd_configs,omitempty"` + // These options are supported only by lib/promscrape. DisableCompression bool `yaml:"disable_compression,omitempty"` DisableKeepAlive bool `yaml:"disable_keepalive,omitempty"` StreamParse bool `yaml:"stream_parse,omitempty"` ScrapeAlignInterval time.Duration `yaml:"scrape_align_interval,omitempty"` ScrapeOffset time.Duration `yaml:"scrape_offset,omitempty"` - ProxyTLSConfig *promauth.TLSConfig `yaml:"proxy_tls_config,omitempty"` + ProxyAuthorization *promauth.Authorization `yaml:"proxy_authorization,omitempty"` ProxyBasicAuth *promauth.BasicAuthConfig `yaml:"proxy_basic_auth,omitempty"` ProxyBearerToken string `yaml:"proxy_bearer_token,omitempty"` ProxyBearerTokenFile string `yaml:"proxy_bearer_token_file,omitempty"` + ProxyTLSConfig *promauth.TLSConfig `yaml:"proxy_tls_config,omitempty"` // This is set in loadConfig swc *scrapeWorkConfig @@ -548,11 +547,11 @@ func getScrapeWorkConfig(sc *ScrapeConfig, baseDir string, globalCfg *GlobalConf return nil, fmt.Errorf("unexpected `scheme` for `job_name` %q: %q; supported values: http or https", jobName, scheme) } params := sc.Params - ac, err := promauth.NewConfig(baseDir, sc.BasicAuth, sc.BearerToken, sc.BearerTokenFile, sc.TLSConfig) + ac, err := sc.HTTPClientConfig.NewConfig(baseDir) if err != nil { return nil, fmt.Errorf("cannot parse auth config for `job_name` %q: %w", jobName, err) } - proxyAC, err := promauth.NewConfig(baseDir, sc.ProxyBasicAuth, sc.ProxyBearerToken, sc.ProxyBearerTokenFile, sc.ProxyTLSConfig) + proxyAC, err := promauth.NewConfig(baseDir, sc.ProxyAuthorization, sc.ProxyBasicAuth, sc.ProxyBearerToken, sc.ProxyBearerTokenFile, sc.ProxyTLSConfig) if err != nil { return nil, fmt.Errorf("cannot parse proxy auth config for `job_name` %q: %w", jobName, err) } diff --git a/lib/promscrape/discovery/consul/api.go b/lib/promscrape/discovery/consul/api.go index 583af0e75..d87e00330 100644 --- a/lib/promscrape/discovery/consul/api.go +++ b/lib/promscrape/discovery/consul/api.go @@ -50,7 +50,7 @@ func newAPIConfig(sdc *SDConfig, baseDir string) (*apiConfig, error) { } token = "" } - ac, err := promauth.NewConfig(baseDir, ba, token, "", sdc.TLSConfig) + ac, err := promauth.NewConfig(baseDir, nil, ba, token, "", sdc.TLSConfig) if err != nil { return nil, fmt.Errorf("cannot parse auth config: %w", err) } diff --git a/lib/promscrape/discovery/dockerswarm/api.go b/lib/promscrape/discovery/dockerswarm/api.go index c79d12fc7..88e8abe7a 100644 --- a/lib/promscrape/discovery/dockerswarm/api.go +++ b/lib/promscrape/discovery/dockerswarm/api.go @@ -7,7 +7,6 @@ import ( "strings" "github.com/VictoriaMetrics/VictoriaMetrics/lib/logger" - "github.com/VictoriaMetrics/VictoriaMetrics/lib/promauth" "github.com/VictoriaMetrics/VictoriaMetrics/lib/promscrape/discoveryutils" ) @@ -34,8 +33,7 @@ func newAPIConfig(sdc *SDConfig, baseDir string) (*apiConfig, error) { port: sdc.Port, filtersQueryArg: getFiltersQueryArg(sdc.Filters), } - - ac, err := promauth.NewConfig(baseDir, sdc.BasicAuth, sdc.BearerToken, sdc.BearerTokenFile, sdc.TLSConfig) + ac, err := sdc.HTTPClientConfig.NewConfig(baseDir) if err != nil { return nil, err } diff --git a/lib/promscrape/discovery/dockerswarm/dockerswarm.go b/lib/promscrape/discovery/dockerswarm/dockerswarm.go index a3cd256ea..7d1aa6c4a 100644 --- a/lib/promscrape/discovery/dockerswarm/dockerswarm.go +++ b/lib/promscrape/discovery/dockerswarm/dockerswarm.go @@ -16,12 +16,9 @@ type SDConfig struct { Port int `yaml:"port,omitempty"` Filters []Filter `yaml:"filters,omitempty"` - ProxyURL proxy.URL `yaml:"proxy_url,omitempty"` - TLSConfig *promauth.TLSConfig `yaml:"tls_config,omitempty"` + ProxyURL proxy.URL `yaml:"proxy_url,omitempty"` + HTTPClientConfig promauth.HTTPClientConfig `yaml:",inline"` // refresh_interval is obtained from `-promscrape.dockerswarmSDCheckInterval` command-line option - BasicAuth *promauth.BasicAuthConfig `yaml:"basic_auth,omitempty"` - BearerToken string `yaml:"bearer_token,omitempty"` - BearerTokenFile string `yaml:"bearer_token_file,omitempty"` } // Filter is a filter, which can be passed to SDConfig. diff --git a/lib/promscrape/discovery/eureka/api.go b/lib/promscrape/discovery/eureka/api.go index 255c9005d..e7e5b740b 100644 --- a/lib/promscrape/discovery/eureka/api.go +++ b/lib/promscrape/discovery/eureka/api.go @@ -5,7 +5,6 @@ import ( "fmt" "strings" - "github.com/VictoriaMetrics/VictoriaMetrics/lib/promauth" "github.com/VictoriaMetrics/VictoriaMetrics/lib/promscrape/discoveryutils" ) @@ -16,19 +15,7 @@ type apiConfig struct { } func newAPIConfig(sdc *SDConfig, baseDir string) (*apiConfig, error) { - token := "" - if sdc.Token != nil { - token = *sdc.Token - } - var ba *promauth.BasicAuthConfig - if len(sdc.Username) > 0 { - ba = &promauth.BasicAuthConfig{ - Username: sdc.Username, - Password: sdc.Password, - } - token = "" - } - ac, err := promauth.NewConfig(baseDir, ba, token, "", sdc.TLSConfig) + ac, err := sdc.HTTPClientConfig.NewConfig(baseDir) if err != nil { return nil, fmt.Errorf("cannot parse auth config: %w", err) } @@ -37,9 +24,9 @@ func newAPIConfig(sdc *SDConfig, baseDir string) (*apiConfig, error) { apiServer = "localhost:8080/eureka/v2" } if !strings.Contains(apiServer, "://") { - scheme := sdc.Scheme - if scheme == "" { - scheme = "http" + scheme := "http" + if sdc.HTTPClientConfig.TLSConfig != nil { + scheme = "https" } apiServer = scheme + "://" + apiServer } diff --git a/lib/promscrape/discovery/eureka/eureka.go b/lib/promscrape/discovery/eureka/eureka.go index 2c2f19f3b..0ea4fbefb 100644 --- a/lib/promscrape/discovery/eureka/eureka.go +++ b/lib/promscrape/discovery/eureka/eureka.go @@ -16,17 +16,11 @@ const appsAPIPath = "/apps" // // See https://prometheus.io/docs/prometheus/latest/configuration/configuration/#eureka type SDConfig struct { - Server string `yaml:"server,omitempty"` - Token *string `yaml:"token"` - Datacenter string `yaml:"datacenter"` - Scheme string `yaml:"scheme,omitempty"` - Username string `yaml:"username"` - Password string `yaml:"password"` - ProxyURL proxy.URL `yaml:"proxy_url,omitempty"` - TLSConfig *promauth.TLSConfig `yaml:"tls_config,omitempty"` + Server string `yaml:"server,omitempty"` + ProxyURL proxy.URL `yaml:"proxy_url,omitempty"` + HTTPClientConfig promauth.HTTPClientConfig `ymal:",inline"` // RefreshInterval time.Duration `yaml:"refresh_interval"` // refresh_interval is obtained from `-promscrape.ec2SDCheckInterval` command-line option. - Port *int `yaml:"port,omitempty"` } type applications struct { @@ -95,11 +89,7 @@ func (sdc *SDConfig) GetLabels(baseDir string) ([]map[string]string, error) { if err != nil { return nil, err } - port := 80 - if sdc.Port != nil { - port = *sdc.Port - } - return addInstanceLabels(apps, port), nil + return addInstanceLabels(apps), nil } // MustStop stops further usage for sdc. @@ -107,11 +97,11 @@ func (sdc *SDConfig) MustStop() { configMap.Delete(sdc) } -func addInstanceLabels(apps *applications, port int) []map[string]string { +func addInstanceLabels(apps *applications) []map[string]string { var ms []map[string]string for _, app := range apps.Applications { for _, instance := range app.Instances { - instancePort := port + instancePort := 80 if instance.Port.Port != 0 { instancePort = instance.Port.Port } diff --git a/lib/promscrape/discovery/eureka/eureka_test.go b/lib/promscrape/discovery/eureka/eureka_test.go index 8b5091648..26bc0988c 100644 --- a/lib/promscrape/discovery/eureka/eureka_test.go +++ b/lib/promscrape/discovery/eureka/eureka_test.go @@ -11,7 +11,6 @@ import ( func Test_addInstanceLabels(t *testing.T) { type args struct { applications *applications - port int } tests := []struct { name string @@ -21,7 +20,6 @@ func Test_addInstanceLabels(t *testing.T) { { name: "1 application", args: args{ - port: 9100, applications: &applications{ Applications: []Application{ { @@ -43,6 +41,9 @@ func Test_addInstanceLabels(t *testing.T) { XMLName: struct{ Space, Local string }{Local: "key-1"}, }, }}, + Port: Port{ + Port: 9100, + }, }, }, }, @@ -64,6 +65,8 @@ func Test_addInstanceLabels(t *testing.T) { "__meta_eureka_app_instance_statuspage_url": "some-status-url", "__meta_eureka_app_instance_id": "some-id", "__meta_eureka_app_instance_metadata_key_1": "value-1", + "__meta_eureka_app_instance_port": "9100", + "__meta_eureka_app_instance_port_enabled": "false", "__meta_eureka_app_instance_status": "Ok", }), }, @@ -71,7 +74,7 @@ func Test_addInstanceLabels(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got := addInstanceLabels(tt.args.applications, tt.args.port) + got := addInstanceLabels(tt.args.applications) var sortedLabelss [][]prompbmarshal.Label for _, labels := range got { sortedLabelss = append(sortedLabelss, discoveryutils.GetSortedLabels(labels)) diff --git a/lib/promscrape/discovery/kubernetes/api.go b/lib/promscrape/discovery/kubernetes/api.go index 5596c7f5b..a09221087 100644 --- a/lib/promscrape/discovery/kubernetes/api.go +++ b/lib/promscrape/discovery/kubernetes/api.go @@ -35,7 +35,7 @@ func newAPIConfig(sdc *SDConfig, baseDir string, swcFunc ScrapeWorkConstructorFu default: return nil, fmt.Errorf("unexpected `role`: %q; must be one of `node`, `pod`, `service`, `endpoints`, `endpointslices` or `ingress`", sdc.Role) } - ac, err := promauth.NewConfig(baseDir, sdc.BasicAuth, sdc.BearerToken, sdc.BearerTokenFile, sdc.TLSConfig) + ac, err := sdc.HTTPClientConfig.NewConfig(baseDir) if err != nil { return nil, fmt.Errorf("cannot parse auth config: %w", err) } @@ -58,7 +58,7 @@ func newAPIConfig(sdc *SDConfig, baseDir string, swcFunc ScrapeWorkConstructorFu tlsConfig := promauth.TLSConfig{ CAFile: "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt", } - acNew, err := promauth.NewConfig(".", nil, "", "/var/run/secrets/kubernetes.io/serviceaccount/token", &tlsConfig) + acNew, err := promauth.NewConfig(".", nil, nil, "", "/var/run/secrets/kubernetes.io/serviceaccount/token", &tlsConfig) if err != nil { return nil, fmt.Errorf("cannot initialize service account auth: %w; probably, `kubernetes_sd_config->api_server` is missing in Prometheus configs?", err) } @@ -66,7 +66,7 @@ func newAPIConfig(sdc *SDConfig, baseDir string, swcFunc ScrapeWorkConstructorFu } if !strings.Contains(apiServer, "://") { proto := "http" - if sdc.TLSConfig != nil { + if sdc.HTTPClientConfig.TLSConfig != nil { proto = "https" } apiServer = proto + "://" + apiServer diff --git a/lib/promscrape/discovery/kubernetes/kubernetes.go b/lib/promscrape/discovery/kubernetes/kubernetes.go index b7dc29b64..9315730f2 100644 --- a/lib/promscrape/discovery/kubernetes/kubernetes.go +++ b/lib/promscrape/discovery/kubernetes/kubernetes.go @@ -11,15 +11,12 @@ import ( // // See https://prometheus.io/docs/prometheus/latest/configuration/configuration/#kubernetes_sd_config type SDConfig struct { - APIServer string `yaml:"api_server,omitempty"` - Role string `yaml:"role"` - BasicAuth *promauth.BasicAuthConfig `yaml:"basic_auth,omitempty"` - BearerToken string `yaml:"bearer_token,omitempty"` - BearerTokenFile string `yaml:"bearer_token_file,omitempty"` - ProxyURL proxy.URL `yaml:"proxy_url,omitempty"` - TLSConfig *promauth.TLSConfig `yaml:"tls_config,omitempty"` - Namespaces Namespaces `yaml:"namespaces,omitempty"` - Selectors []Selector `yaml:"selectors,omitempty"` + APIServer string `yaml:"api_server,omitempty"` + Role string `yaml:"role"` + HTTPClientConfig promauth.HTTPClientConfig `yaml:",inline"` + ProxyURL proxy.URL `yaml:"proxy_url,omitempty"` + Namespaces Namespaces `yaml:"namespaces,omitempty"` + Selectors []Selector `yaml:"selectors,omitempty"` } // Namespaces represents namespaces for SDConfig diff --git a/lib/promscrape/discovery/openstack/api.go b/lib/promscrape/discovery/openstack/api.go index 615fe8971..fecfd3158 100644 --- a/lib/promscrape/discovery/openstack/api.go +++ b/lib/promscrape/discovery/openstack/api.go @@ -77,7 +77,7 @@ func newAPIConfig(sdc *SDConfig, baseDir string) (*apiConfig, error) { port: sdc.Port, } if sdc.TLSConfig != nil { - ac, err := promauth.NewConfig(baseDir, nil, "", "", sdc.TLSConfig) + ac, err := promauth.NewConfig(baseDir, nil, nil, "", "", sdc.TLSConfig) if err != nil { return nil, err } From b88feb631ea13990276f2d43b3469a68e686219f Mon Sep 17 00:00:00 2001 From: Aliaksandr Valialkin Date: Fri, 2 Apr 2021 21:24:11 +0300 Subject: [PATCH 20/63] docs/vmagent.md: mention about proxy_authorization section --- app/vmagent/README.md | 1 + docs/vmagent.md | 1 + 2 files changed, 2 insertions(+) diff --git a/app/vmagent/README.md b/app/vmagent/README.md index 815a0d030..f6f159177 100644 --- a/app/vmagent/README.md +++ b/app/vmagent/README.md @@ -273,6 +273,7 @@ scrape_configs: Proxy can be configured with the following optional settings: +* `proxy_authorization` for generic token authorization. See [Prometheus docs for details on authorization section](https://prometheus.io/docs/prometheus/latest/configuration/configuration/#scrape_config) * `proxy_bearer_token` and `proxy_bearer_token_file` for Bearer token authorization * `proxy_basic_auth` for Basic authorization. See [these docs](https://prometheus.io/docs/prometheus/latest/configuration/configuration/#scrape_config). * `proxy_tls_config` for TLS config. See [these docs](https://prometheus.io/docs/prometheus/latest/configuration/configuration/#tls_config). diff --git a/docs/vmagent.md b/docs/vmagent.md index 815a0d030..f6f159177 100644 --- a/docs/vmagent.md +++ b/docs/vmagent.md @@ -273,6 +273,7 @@ scrape_configs: Proxy can be configured with the following optional settings: +* `proxy_authorization` for generic token authorization. See [Prometheus docs for details on authorization section](https://prometheus.io/docs/prometheus/latest/configuration/configuration/#scrape_config) * `proxy_bearer_token` and `proxy_bearer_token_file` for Bearer token authorization * `proxy_basic_auth` for Basic authorization. See [these docs](https://prometheus.io/docs/prometheus/latest/configuration/configuration/#scrape_config). * `proxy_tls_config` for TLS config. See [these docs](https://prometheus.io/docs/prometheus/latest/configuration/configuration/#tls_config). From b1d0028e79a20e40f9f93f48dc588a1ee27c30b1 Mon Sep 17 00:00:00 2001 From: Aliaksandr Valialkin Date: Fri, 2 Apr 2021 22:14:53 +0300 Subject: [PATCH 21/63] app/vmauth: add support for authorization via `Authorization: Bearer ` --- app/vmauth/README.md | 22 ++++++++------ app/vmauth/auth_config.go | 54 ++++++++++++++++++++++++++++------ app/vmauth/auth_config_test.go | 39 ++++++++++++++++++++---- app/vmauth/main.go | 12 ++++---- docs/CHANGELOG.md | 1 + 5 files changed, 98 insertions(+), 30 deletions(-) diff --git a/app/vmauth/README.md b/app/vmauth/README.md index 6cb3fc6bf..41953e32c 100644 --- a/app/vmauth/README.md +++ b/app/vmauth/README.md @@ -36,11 +36,15 @@ Auth config is represented in the following simple `yml` format: # Usernames must be unique. users: + # Requests with the 'Authorization: Bearer XXXX' header are proxied to http://localhost:8428 . + # For example, http://vmauth:8427/api/v1/query is proxied to http://localhost:8428/api/v1/query +- bearer_token: "XXXX" + url_prefix: "http://localhost:8428" # The user for querying local single-node VictoriaMetrics. # All the requests to http://vmauth:8427 with the given Basic Auth (username:password) - # will be routed to http://localhost:8428 . - # For example, http://vmauth:8427/api/v1/query is routed to http://localhost:8428/api/v1/query + # will be proxied to http://localhost:8428 . + # For example, http://vmauth:8427/api/v1/query is proxied to http://localhost:8428/api/v1/query - username: "local-single-node" password: "***" url_prefix: "http://localhost:8428" @@ -48,8 +52,8 @@ users: # The user for querying account 123 in VictoriaMetrics cluster # See https://victoriametrics.github.io/Cluster-VictoriaMetrics.html#url-format # All the requests to http://vmauth:8427 with the given Basic Auth (username:password) - # will be routed to http://vmselect:8481/select/123/prometheus . - # For example, http://vmauth:8427/api/v1/query is routed to http://vmselect:8481/select/123/prometheus/api/v1/select + # will be proxied to http://vmselect:8481/select/123/prometheus . + # For example, http://vmauth:8427/api/v1/query is proxied to http://vmselect:8481/select/123/prometheus/api/v1/select - username: "cluster-select-account-123" password: "***" url_prefix: "http://vmselect:8481/select/123/prometheus" @@ -57,8 +61,8 @@ users: # The user for inserting Prometheus data into VictoriaMetrics cluster under account 42 # See https://victoriametrics.github.io/Cluster-VictoriaMetrics.html#url-format # All the requests to http://vmauth:8427 with the given Basic Auth (username:password) - # will be routed to http://vminsert:8480/insert/42/prometheus . - # For example, http://vmauth:8427/api/v1/write is routed to http://vminsert:8480/insert/42/prometheus/api/v1/write + # will be proxied to http://vminsert:8480/insert/42/prometheus . + # For example, http://vmauth:8427/api/v1/write is proxied to http://vminsert:8480/insert/42/prometheus/api/v1/write - username: "cluster-insert-account-42" password: "***" url_prefix: "http://vminsert:8480/insert/42/prometheus" @@ -66,9 +70,9 @@ users: # A single user for querying and inserting data: # - Requests to http://vmauth:8427/api/v1/query, http://vmauth:8427/api/v1/query_range - # and http://vmauth:8427/api/v1/label//values are routed to http://vmselect:8481/select/42/prometheus. - # For example, http://vmauth:8427/api/v1/query is routed to http://vmselect:8480/select/42/prometheus/api/v1/query - # - Requests to http://vmauth:8427/api/v1/write are routed to http://vminsert:8480/insert/42/prometheus/api/v1/write + # and http://vmauth:8427/api/v1/label//values are proxied to http://vmselect:8481/select/42/prometheus. + # For example, http://vmauth:8427/api/v1/query is proxied to http://vmselect:8480/select/42/prometheus/api/v1/query + # - Requests to http://vmauth:8427/api/v1/write are proxied to http://vminsert:8480/insert/42/prometheus/api/v1/write - username: "foobar" url_map: - src_paths: ["/api/v1/query", "/api/v1/query_range", "/api/v1/label/[^/]+/values"] diff --git a/app/vmauth/auth_config.go b/app/vmauth/auth_config.go index dc8c1da2f..139056ffd 100644 --- a/app/vmauth/auth_config.go +++ b/app/vmauth/auth_config.go @@ -1,6 +1,7 @@ package main import ( + "encoding/base64" "flag" "fmt" "io/ioutil" @@ -29,10 +30,11 @@ type AuthConfig struct { // UserInfo is user information read from authConfigPath type UserInfo struct { - Username string `yaml:"username"` - Password string `yaml:"password"` - URLPrefix string `yaml:"url_prefix"` - URLMap []URLMap `yaml:"url_map"` + BearerToken string `yaml:"bearer_token"` + Username string `yaml:"username"` + Password string `yaml:"password"` + URLPrefix string `yaml:"url_prefix"` + URLMap []URLMap `yaml:"url_map"` requests *metrics.Counter } @@ -150,12 +152,27 @@ func parseAuthConfig(data []byte) (map[string]*UserInfo, error) { if len(uis) == 0 { return nil, fmt.Errorf("`users` section cannot be empty in AuthConfig") } - m := make(map[string]*UserInfo, len(uis)) + byAuthToken := make(map[string]*UserInfo, len(uis)) + byUsername := make(map[string]bool, len(uis)) + byBearerToken := make(map[string]bool, len(uis)) for i := range uis { ui := &uis[i] - if m[ui.Username] != nil { + if ui.BearerToken == "" && ui.Username == "" { + return nil, fmt.Errorf("either bearer_token or username must be set") + } + if ui.BearerToken != "" && ui.Username != "" { + return nil, fmt.Errorf("bearer_token=%q and username=%q cannot be set simultaneously", ui.BearerToken, ui.Username) + } + if byBearerToken[ui.BearerToken] { + return nil, fmt.Errorf("duplicate bearer_token found; bearer_token: %q", ui.BearerToken) + } + if byUsername[ui.Username] { return nil, fmt.Errorf("duplicate username found; username: %q", ui.Username) } + authToken := getAuthToken(ui.BearerToken, ui.Username, ui.Password) + if byAuthToken[authToken] != nil { + return nil, fmt.Errorf("duplicate auth token found for bearer_token=%q, username=%q: %q", authToken, ui.BearerToken, ui.Username) + } if len(ui.URLPrefix) > 0 { urlPrefix, err := sanitizeURLPrefix(ui.URLPrefix) if err != nil { @@ -176,10 +193,29 @@ func parseAuthConfig(data []byte) (map[string]*UserInfo, error) { if len(ui.URLMap) == 0 && len(ui.URLPrefix) == 0 { return nil, fmt.Errorf("missing `url_prefix`") } - ui.requests = metrics.GetOrCreateCounter(fmt.Sprintf(`vmauth_user_requests_total{username=%q}`, ui.Username)) - m[ui.Username] = ui + if ui.BearerToken != "" { + if ui.Password != "" { + return nil, fmt.Errorf("password shouldn't be set for bearer_token %q", ui.BearerToken) + } + ui.requests = metrics.GetOrCreateCounter(`vmauth_user_requests_total{username="bearer_token"}`) + byBearerToken[ui.BearerToken] = true + } + if ui.Username != "" { + ui.requests = metrics.GetOrCreateCounter(fmt.Sprintf(`vmauth_user_requests_total{username=%q}`, ui.Username)) + byUsername[ui.Username] = true + } + byAuthToken[authToken] = ui } - return m, nil + return byAuthToken, nil +} + +func getAuthToken(bearerToken, username, password string) string { + if bearerToken != "" { + return "Bearer " + bearerToken + } + token := username + ":" + password + token64 := base64.StdEncoding.EncodeToString([]byte(token)) + return "Basic " + token64 } func sanitizeURLPrefix(urlPrefix string) (string, error) { diff --git a/app/vmauth/auth_config_test.go b/app/vmauth/auth_config_test.go index 0033b2b42..759136e5f 100644 --- a/app/vmauth/auth_config_test.go +++ b/app/vmauth/auth_config_test.go @@ -56,6 +56,22 @@ users: url_prefix: http:///bar `) + // Username and bearer_token in a single config + f(` +users: +- username: foo + bearer_token: bbb + url_prefix: http://foo.bar +`) + + // Bearer_token and password in a single config + f(` +users: +- password: foo + bearer_token: bbb + url_prefix: http://foo.bar +`) + // Duplicate users f(` users: @@ -67,6 +83,17 @@ users: url_prefix: https://sss.sss `) + // Duplicate bearer_tokens + f(` +users: +- bearer_token: foo + url_prefix: http://foo.bar +- username: bar + url_prefix: http://xxx.yyy +- bearer_token: foo + url_prefix: https://sss.sss +`) + // Missing url_prefix in url_map f(` users: @@ -113,7 +140,7 @@ users: password: bar url_prefix: http://aaa:343/bbb `, map[string]*UserInfo{ - "foo": { + getAuthToken("", "foo", "bar"): { Username: "foo", Password: "bar", URLPrefix: "http://aaa:343/bbb", @@ -128,11 +155,11 @@ users: - username: bar url_prefix: https://bar/x/// `, map[string]*UserInfo{ - "foo": { + getAuthToken("", "foo", ""): { Username: "foo", URLPrefix: "http://foo", }, - "bar": { + getAuthToken("", "bar", ""): { Username: "bar", URLPrefix: "https://bar/x", }, @@ -141,15 +168,15 @@ users: // non-empty URLMap f(` users: -- username: foo +- bearer_token: foo url_map: - src_paths: ["/api/v1/query","/api/v1/query_range","/api/v1/label/[^./]+/.+"] url_prefix: http://vmselect/select/0/prometheus - src_paths: ["/api/v1/write"] url_prefix: http://vminsert/insert/0/prometheus `, map[string]*UserInfo{ - "foo": { - Username: "foo", + getAuthToken("foo", "", ""): { + BearerToken: "foo", URLMap: []URLMap{ { SrcPaths: getSrcPaths([]string{"/api/v1/query", "/api/v1/query_range", "/api/v1/label/[^./]+/.+"}), diff --git a/app/vmauth/main.go b/app/vmauth/main.go index 282467347..23c22d282 100644 --- a/app/vmauth/main.go +++ b/app/vmauth/main.go @@ -47,16 +47,16 @@ func main() { } func requestHandler(w http.ResponseWriter, r *http.Request) bool { - username, password, ok := r.BasicAuth() - if !ok { + authToken := r.Header.Get("Authorization") + if authToken == "" { w.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`) - http.Error(w, "missing `Authorization: Basic *` header", http.StatusUnauthorized) + http.Error(w, "missing `Authorization` request header", http.StatusUnauthorized) return true } ac := authConfig.Load().(map[string]*UserInfo) - ui := ac[username] - if ui == nil || ui.Password != password { - httpserver.Errorf(w, r, "cannot find the provided username %q or password in config", username) + ui := ac[authToken] + if ui == nil { + httpserver.Errorf(w, r, "cannot find the provided auth token %q in config", authToken) return true } ui.requests.Inc() diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 86c60f132..9030fef97 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -8,6 +8,7 @@ * FEATURE: vmagent: add support for `authorization` section in `-promscrape.config` in the same way as [Prometheus 2.26 does](https://github.com/prometheus/prometheus/pull/8512). * FEATURE: vmagent: reduce memory usage when `-remoteWrite.queues` is set to a big value. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/1167). * FEATURE: vmagent: add AWS IAM roles for tasks support for EC2 service discovery according to [these docs](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-iam-roles.html). +FEATURE: vmauth: add support for authorization via [bearer token](https://swagger.io/docs/specification/authentication/bearer-authentication/). See [the docs](https://victoriametrics.github.io/vmauth.html#auth-config) for details. * BUGFIX: vmagent: properly discovery targets if multiple namespace selectors are put inside `kubernetes_sd_config`. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/1170). * BUGFIX: properly generate filename for `*.tar.gz` archive inside `_checksums.txt` file posted at [releases page](https://github.com/VictoriaMetrics/VictoriaMetrics/releases). See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/1171). From 569e58dcdfd9ac800881d9e59a4b7efd909a4d31 Mon Sep 17 00:00:00 2001 From: Aliaksandr Valialkin Date: Fri, 2 Apr 2021 22:20:17 +0300 Subject: [PATCH 22/63] vendor: `make vendor-update` --- go.mod | 16 +- go.sum | 45 ++-- vendor/cloud.google.com/go/CHANGES.md | 16 ++ vendor/cloud.google.com/go/go.mod | 6 +- vendor/cloud.google.com/go/go.sum | 14 +- .../go/internal/.repo-metadata-full.json | 34 +-- .../aws/aws-sdk-go/aws/endpoints/defaults.go | 209 ++++++++++++++++-- .../github.com/aws/aws-sdk-go/aws/version.go | 2 +- .../mattn/go-runewidth/runewidth.go | 52 ++++- .../mattn/go-runewidth/runewidth_table.go | 6 +- .../google/internal/externalaccount/aws.go | 3 +- vendor/golang.org/x/sys/unix/mkerrors.sh | 7 +- .../x/sys/unix/zerrors_freebsd_arm.go | 9 + vendor/golang.org/x/sys/unix/zerrors_linux.go | 4 + .../x/sys/unix/zerrors_solaris_amd64.go | 3 + .../x/sys/unix/zerrors_zos_s390x.go | 1 + .../x/text/secure/bidirule/bidirule10.0.0.go | 1 + .../x/text/secure/bidirule/bidirule9.0.0.go | 1 + .../x/text/unicode/bidi/tables10.0.0.go | 1 + .../x/text/unicode/bidi/tables11.0.0.go | 1 + .../x/text/unicode/bidi/tables12.0.0.go | 1 + .../x/text/unicode/bidi/tables13.0.0.go | 1 + .../x/text/unicode/bidi/tables9.0.0.go | 1 + .../x/text/unicode/norm/tables10.0.0.go | 1 + .../x/text/unicode/norm/tables11.0.0.go | 1 + .../x/text/unicode/norm/tables12.0.0.go | 1 + .../x/text/unicode/norm/tables13.0.0.go | 1 + .../x/text/unicode/norm/tables9.0.0.go | 1 + vendor/google.golang.org/grpc/version.go | 2 +- vendor/modules.txt | 26 ++- 30 files changed, 361 insertions(+), 106 deletions(-) diff --git a/go.mod b/go.mod index 55ee81ee5..312b15770 100644 --- a/go.mod +++ b/go.mod @@ -1,7 +1,7 @@ module github.com/VictoriaMetrics/VictoriaMetrics require ( - cloud.google.com/go v0.80.0 // indirect + cloud.google.com/go v0.81.0 // indirect cloud.google.com/go/storage v1.14.0 github.com/VictoriaMetrics/fastcache v1.5.8 @@ -10,15 +10,17 @@ require ( github.com/VictoriaMetrics/fasthttp v1.0.14 github.com/VictoriaMetrics/metrics v1.17.2 github.com/VictoriaMetrics/metricsql v0.14.0 - github.com/aws/aws-sdk-go v1.38.4 + github.com/aws/aws-sdk-go v1.38.12 github.com/cespare/xxhash/v2 v2.1.1 github.com/cheggaaa/pb/v3 v3.0.7 github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect github.com/fatih/color v1.10.0 // indirect + github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect + github.com/golang/protobuf v1.5.2 // indirect github.com/golang/snappy v0.0.3 github.com/influxdata/influxdb v1.8.4 github.com/klauspost/compress v1.11.13 - github.com/mattn/go-runewidth v0.0.10 // indirect + github.com/mattn/go-runewidth v0.0.12 // indirect github.com/prometheus/client_golang v1.10.0 // indirect github.com/prometheus/common v0.20.0 // indirect github.com/prometheus/prometheus v1.8.2-0.20201119142752-3ad25a6dc3d9 @@ -32,11 +34,11 @@ require ( github.com/valyala/histogram v1.1.2 github.com/valyala/quicktemplate v1.6.3 golang.org/x/mod v0.4.2 // indirect - golang.org/x/net v0.0.0-20210324205630-d1beb07c2056 // indirect - golang.org/x/oauth2 v0.0.0-20210323180902-22b0adad7558 - golang.org/x/sys v0.0.0-20210324051608-47abb6519492 + golang.org/x/net v0.0.0-20210331212208-0fccb6fa2b5c // indirect + golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602 + golang.org/x/sys v0.0.0-20210331175145-43e1dd70ce54 + golang.org/x/text v0.3.6 // indirect google.golang.org/api v0.43.0 - google.golang.org/genproto v0.0.0-20210325141258-5636347f2b14 // indirect gopkg.in/yaml.v2 v2.4.0 ) diff --git a/go.sum b/go.sum index 685b72025..7f330d895 100644 --- a/go.sum +++ b/go.sum @@ -20,8 +20,8 @@ cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmW cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg= cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8= -cloud.google.com/go v0.80.0 h1:kAdyAMrj9CjqOSGiluseVjIgAyQ3uxADYtUYR6MwYeY= -cloud.google.com/go v0.80.0/go.mod h1:fqpb6QRi1CFGAMXDoE72G+b+Ybv7dMB/T1tbExDHktI= +cloud.google.com/go v0.81.0 h1:at8Tk2zUz63cLPR0JPWm5vp77pEZmzxEQBEfRKn1VV8= +cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= @@ -125,8 +125,8 @@ github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQ github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= github.com/aws/aws-sdk-go v1.34.28/go.mod h1:H7NKnBqNVzoTJpGfLrQkkD+ytBA93eiDYi/+8rV9s48= github.com/aws/aws-sdk-go v1.35.31/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro= -github.com/aws/aws-sdk-go v1.38.4 h1:ifewiUUfuB6LrOR6PDqjlld3IIoWskrTVEGrzF2Q/v4= -github.com/aws/aws-sdk-go v1.38.4/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro= +github.com/aws/aws-sdk-go v1.38.12 h1:khtODkUna3iF53Cg3dCF4e6oWgrAEbZDU4x1aq+G0WY= +github.com/aws/aws-sdk-go v1.38.12/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro= github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= @@ -342,8 +342,9 @@ github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfU github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e h1:1r7pUrabqp18hOBcwBwiTsbnFeTZHV9eER/QT5JVZxY= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= @@ -367,8 +368,9 @@ github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QD github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.1 h1:jAbXjIeW2ZSW2AwFxlGTDoc2CjI2XujLkV3ArsZFCvc= github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= +github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.2/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= @@ -563,8 +565,8 @@ github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Ky github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= -github.com/mattn/go-runewidth v0.0.10 h1:CoZ3S2P7pvtP45xOtBw+/mDL2z0RKI576gSkzRRpdGg= -github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= +github.com/mattn/go-runewidth v0.0.12 h1:Y41i/hVW3Pgwr8gV+J23B9YEY0zxjptBuCWEaxmAOow= +github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= github.com/mattn/go-sqlite3 v1.11.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/mattn/go-tty v0.0.0-20180907095812-13ff1204f104/go.mod h1:XPvLUNfbS4fJH25nqRHfWLMa1ONC8Amw+mIA639KxkE= github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= @@ -923,8 +925,8 @@ golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= -golang.org/x/net v0.0.0-20210324205630-d1beb07c2056 h1:sANdAef76Ioam9aQUUdcAqricwY/WUaMc4+7LY4eGg8= -golang.org/x/net v0.0.0-20210324205630-d1beb07c2056/go.mod h1:uSPa2vr4CLtc/ILN5odXGNXS6mhrKVzTaCXzk9m6W3k= +golang.org/x/net v0.0.0-20210331212208-0fccb6fa2b5c h1:KHUzaHIpjWVlVVNh65G3hhuj3KB1HnjY6Cq5cTvRQT8= +golang.org/x/net v0.0.0-20210331212208-0fccb6fa2b5c/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -936,8 +938,8 @@ golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210323180902-22b0adad7558 h1:D7nTwh4J0i+5mW4Zjzn5omvlr6YBcWywE6KOcatyNxY= -golang.org/x/oauth2 v0.0.0-20210323180902-22b0adad7558/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602 h1:0Ja1LBD+yisY6RWM/BH7TJVXWsSjs2VwBSmvSX4HdBc= +golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -1019,11 +1021,11 @@ golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210309074719-68d13333faf2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210314195730-07df6a141424/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210324051608-47abb6519492 h1:Paq34FxTluEPvVyayQqMPgHm+vTOrIifmcYxFBx9TLg= -golang.org/x/sys v0.0.0-20210324051608-47abb6519492/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210331175145-43e1dd70ce54 h1:rF3Ohx8DRyl8h2zw9qojyLHLhrJpEMgyPOImREEryf0= +golang.org/x/sys v0.0.0-20210331175145-43e1dd70ce54/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -1031,8 +1033,9 @@ golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3 golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.5 h1:i6eZZ+zk0SOf0xgBpEpPD18qWcJda6q1sxt3S0kzyUQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -1141,7 +1144,6 @@ google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU= -google.golang.org/api v0.42.0/go.mod h1:+Oj4s6ch2SEGtPjGqfUfZonBH0GjQH89gTeKKAEGZKI= google.golang.org/api v0.43.0 h1:4sAyIHT6ZohtAQDoxws+ez7bROYmUlOVvsUscYCDTqA= google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= @@ -1196,11 +1198,9 @@ google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6D google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210312152112-fc591d9ea70f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210323160006-e668133fea6a/go.mod h1:f2Bd7+2PlaVKmvKQ52aspJZXIDaRQBVdOOBfJ5i8OEs= -google.golang.org/genproto v0.0.0-20210325141258-5636347f2b14 h1:0VNRpy5TroA/6mYt3pPEq+E3oomxLJ+FUit3+oIsUy4= -google.golang.org/genproto v0.0.0-20210325141258-5636347f2b14/go.mod h1:f2Bd7+2PlaVKmvKQ52aspJZXIDaRQBVdOOBfJ5i8OEs= +google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1 h1:E7wSQBXkH3T3diucK+9Z1kjn4+/9tNG7lZLr75oOhh8= +google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.0/go.mod h1:chYK+tFQF0nDUGJgXMSgLCQk3phJEuONr2DCgLDdAQM= @@ -1223,8 +1223,9 @@ google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTp google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.36.0 h1:o1bcQ6imQMIOpdrO3SWf2z5RV72WbDwdXuK0MDlc8As= google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.36.1 h1:cmUfbeGKnz9+2DD/UYsMQXeqbHZqZDs4eQwW0sFOpBY= +google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= diff --git a/vendor/cloud.google.com/go/CHANGES.md b/vendor/cloud.google.com/go/CHANGES.md index d38b3a4ea..6fc75f1f1 100644 --- a/vendor/cloud.google.com/go/CHANGES.md +++ b/vendor/cloud.google.com/go/CHANGES.md @@ -1,6 +1,22 @@ # Changes +## [0.81.0](https://www.github.com/googleapis/google-cloud-go/compare/v0.80.0...v0.81.0) (2021-04-02) + + +### Features + +* **datacatalog:** Policy Tag Manager v1 API service feat: new RenameTagTemplateFieldEnumValue API feat: adding fully_qualified_name in lookup and search feat: added DATAPROC_METASTORE integrated system along with new entry types: DATABASE and SERVICE docs: Documentation improvements ([2b02a03](https://www.github.com/googleapis/google-cloud-go/commit/2b02a03ff9f78884da5a8e7b64a336014c61bde7)) +* **dialogflow/cx:** include original user query in WebhookRequest; add GetTextCaseresult API. doc: clarify resource format for session response. ([a0b1f6f](https://www.github.com/googleapis/google-cloud-go/commit/a0b1f6faae77d014fdee166ab018ddcd6f846ab4)) +* **dialogflow/cx:** include original user query in WebhookRequest; add GetTextCaseresult API. doc: clarify resource format for session response. ([b5b4da6](https://www.github.com/googleapis/google-cloud-go/commit/b5b4da6952922440d03051f629f3166f731dfaa3)) +* **dialogflow:** expose MP3_64_KBPS and MULAW for output audio encodings. ([b5b4da6](https://www.github.com/googleapis/google-cloud-go/commit/b5b4da6952922440d03051f629f3166f731dfaa3)) +* **secretmanager:** Rotation for Secrets ([2b02a03](https://www.github.com/googleapis/google-cloud-go/commit/2b02a03ff9f78884da5a8e7b64a336014c61bde7)) + + +### Bug Fixes + +* **internal/godocfx:** filter out non-Cloud ([#3878](https://www.github.com/googleapis/google-cloud-go/issues/3878)) ([625aef9](https://www.github.com/googleapis/google-cloud-go/commit/625aef9b47181cf627587cc9cde9e400713c6678)) + ## [0.80.0](https://www.github.com/googleapis/google-cloud-go/compare/v0.79.0...v0.80.0) (2021-03-23) diff --git a/vendor/cloud.google.com/go/go.mod b/vendor/cloud.google.com/go/go.mod index 24ebd1a85..4fa03cae5 100644 --- a/vendor/cloud.google.com/go/go.mod +++ b/vendor/cloud.google.com/go/go.mod @@ -17,7 +17,7 @@ require ( golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84 golang.org/x/text v0.3.5 golang.org/x/tools v0.1.0 - google.golang.org/api v0.42.0 - google.golang.org/genproto v0.0.0-20210323160006-e668133fea6a - google.golang.org/grpc v1.36.0 + google.golang.org/api v0.43.0 + google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1 + google.golang.org/grpc v1.36.1 ) diff --git a/vendor/cloud.google.com/go/go.sum b/vendor/cloud.google.com/go/go.sum index 6e00313a1..d0209b286 100644 --- a/vendor/cloud.google.com/go/go.sum +++ b/vendor/cloud.google.com/go/go.sum @@ -284,7 +284,6 @@ golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210314195730-07df6a141424/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4 h1:EZ2mChiOa8udjfp6rRmswTbtZN/QzUQp4ptM4rnjHvc= golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -372,8 +371,8 @@ google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU= -google.golang.org/api v0.42.0 h1:uqATLkpxiBrhrvFoebXUjvyzE9nQf+pVyy0Z0IHE+fc= -google.golang.org/api v0.42.0/go.mod h1:+Oj4s6ch2SEGtPjGqfUfZonBH0GjQH89gTeKKAEGZKI= +google.golang.org/api v0.43.0 h1:4sAyIHT6ZohtAQDoxws+ez7bROYmUlOVvsUscYCDTqA= +google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= @@ -419,9 +418,9 @@ google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6D google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210312152112-fc591d9ea70f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210323160006-e668133fea6a h1:XVaQ1+BDKvrRcgppHhtAaniHCKyV5xJAvymwsPHHFaE= -google.golang.org/genproto v0.0.0-20210323160006-e668133fea6a/go.mod h1:f2Bd7+2PlaVKmvKQ52aspJZXIDaRQBVdOOBfJ5i8OEs= +google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1 h1:E7wSQBXkH3T3diucK+9Z1kjn4+/9tNG7lZLr75oOhh8= +google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= @@ -438,8 +437,9 @@ google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.36.0 h1:o1bcQ6imQMIOpdrO3SWf2z5RV72WbDwdXuK0MDlc8As= google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.36.1 h1:cmUfbeGKnz9+2DD/UYsMQXeqbHZqZDs4eQwW0sFOpBY= +google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= diff --git a/vendor/cloud.google.com/go/internal/.repo-metadata-full.json b/vendor/cloud.google.com/go/internal/.repo-metadata-full.json index ffe045e4e..ecb5f8efd 100644 --- a/vendor/cloud.google.com/go/internal/.repo-metadata-full.json +++ b/vendor/cloud.google.com/go/internal/.repo-metadata-full.json @@ -1,7 +1,7 @@ { "cloud.google.com/go/accessapproval/apiv1": { "distribution_name": "cloud.google.com/go/accessapproval/apiv1", - "description": "", + "description": "Access Approval API", "language": "Go", "client_library_type": "generated", "docs_url": "https://pkg.go.dev/cloud.google.com/go/accessapproval/apiv1", @@ -9,7 +9,7 @@ }, "cloud.google.com/go/analytics/admin/apiv1alpha": { "distribution_name": "cloud.google.com/go/analytics/admin/apiv1alpha", - "description": "", + "description": "Google Analytics Admin API", "language": "Go", "client_library_type": "generated", "docs_url": "https://pkg.go.dev/cloud.google.com/go/analytics/admin/apiv1alpha", @@ -17,7 +17,7 @@ }, "cloud.google.com/go/analytics/data/apiv1alpha": { "distribution_name": "cloud.google.com/go/analytics/data/apiv1alpha", - "description": "", + "description": "Google Analytics Data API", "language": "Go", "client_library_type": "generated", "docs_url": "https://pkg.go.dev/cloud.google.com/go/analytics/data/apiv1alpha", @@ -81,7 +81,7 @@ }, "cloud.google.com/go/assuredworkloads/apiv1beta1": { "distribution_name": "cloud.google.com/go/assuredworkloads/apiv1beta1", - "description": "", + "description": "Assured Workloads API", "language": "Go", "client_library_type": "generated", "docs_url": "https://pkg.go.dev/cloud.google.com/go/assuredworkloads/apiv1beta1", @@ -209,7 +209,7 @@ }, "cloud.google.com/go/billing/budgets/apiv1beta1": { "distribution_name": "cloud.google.com/go/billing/budgets/apiv1beta1", - "description": "", + "description": "Cloud Billing Budget API", "language": "Go", "client_library_type": "generated", "docs_url": "https://pkg.go.dev/cloud.google.com/go/billing/budgets/apiv1beta1", @@ -449,7 +449,7 @@ }, "cloud.google.com/go/functions/apiv1": { "distribution_name": "cloud.google.com/go/functions/apiv1", - "description": "", + "description": "Cloud Functions API", "language": "Go", "client_library_type": "generated", "docs_url": "https://pkg.go.dev/cloud.google.com/go/functions/apiv1", @@ -457,7 +457,7 @@ }, "cloud.google.com/go/gaming/apiv1": { "distribution_name": "cloud.google.com/go/gaming/apiv1", - "description": "", + "description": "Game Services API", "language": "Go", "client_library_type": "generated", "docs_url": "https://pkg.go.dev/cloud.google.com/go/gaming/apiv1", @@ -465,7 +465,7 @@ }, "cloud.google.com/go/gaming/apiv1beta": { "distribution_name": "cloud.google.com/go/gaming/apiv1beta", - "description": "", + "description": "Game Services API", "language": "Go", "client_library_type": "generated", "docs_url": "https://pkg.go.dev/cloud.google.com/go/gaming/apiv1beta", @@ -609,7 +609,7 @@ }, "cloud.google.com/go/monitoring/dashboard/apiv1": { "distribution_name": "cloud.google.com/go/monitoring/dashboard/apiv1", - "description": "", + "description": "Cloud Monitoring API", "language": "Go", "client_library_type": "generated", "docs_url": "https://pkg.go.dev/cloud.google.com/go/monitoring/dashboard/apiv1", @@ -729,7 +729,7 @@ }, "cloud.google.com/go/pubsublite/apiv1": { "distribution_name": "cloud.google.com/go/pubsublite/apiv1", - "description": "", + "description": "Pub/Sub Lite API", "language": "Go", "client_library_type": "generated", "docs_url": "https://pkg.go.dev/cloud.google.com/go/pubsublite/apiv1", @@ -857,7 +857,7 @@ }, "cloud.google.com/go/security/privateca/apiv1beta1": { "distribution_name": "cloud.google.com/go/security/privateca/apiv1beta1", - "description": "", + "description": "Certificate Authority API", "language": "Go", "client_library_type": "generated", "docs_url": "https://pkg.go.dev/cloud.google.com/go/security/privateca/apiv1beta1", @@ -897,7 +897,7 @@ }, "cloud.google.com/go/servicecontrol/apiv1": { "distribution_name": "cloud.google.com/go/servicecontrol/apiv1", - "description": "", + "description": "Service Control API", "language": "Go", "client_library_type": "generated", "docs_url": "https://pkg.go.dev/cloud.google.com/go/servicecontrol/apiv1", @@ -921,7 +921,7 @@ }, "cloud.google.com/go/servicemanagement/apiv1": { "distribution_name": "cloud.google.com/go/servicemanagement/apiv1", - "description": "", + "description": "Service Management API", "language": "Go", "client_library_type": "generated", "docs_url": "https://pkg.go.dev/cloud.google.com/go/servicemanagement/apiv1", @@ -985,7 +985,7 @@ }, "cloud.google.com/go/talent/apiv4": { "distribution_name": "cloud.google.com/go/talent/apiv4", - "description": "", + "description": "Cloud Talent Solution API", "language": "Go", "client_library_type": "generated", "docs_url": "https://pkg.go.dev/cloud.google.com/go/talent/apiv4", @@ -1033,7 +1033,7 @@ }, "cloud.google.com/go/video/transcoder/apiv1beta1": { "distribution_name": "cloud.google.com/go/video/transcoder/apiv1beta1", - "description": "", + "description": "Transcoder API", "language": "Go", "client_library_type": "generated", "docs_url": "https://pkg.go.dev/cloud.google.com/go/video/transcoder/apiv1beta1", @@ -1097,7 +1097,7 @@ }, "cloud.google.com/go/workflows/apiv1beta": { "distribution_name": "cloud.google.com/go/workflows/apiv1beta", - "description": "", + "description": "Workflows API", "language": "Go", "client_library_type": "generated", "docs_url": "https://pkg.go.dev/cloud.google.com/go/workflows/apiv1beta", @@ -1105,7 +1105,7 @@ }, "cloud.google.com/go/workflows/executions/apiv1beta": { "distribution_name": "cloud.google.com/go/workflows/executions/apiv1beta", - "description": "", + "description": "Workflow Executions API", "language": "Go", "client_library_type": "generated", "docs_url": "https://pkg.go.dev/cloud.google.com/go/workflows/executions/apiv1beta", diff --git a/vendor/github.com/aws/aws-sdk-go/aws/endpoints/defaults.go b/vendor/github.com/aws/aws-sdk-go/aws/endpoints/defaults.go index 05cbff629..6da1da736 100644 --- a/vendor/github.com/aws/aws-sdk-go/aws/endpoints/defaults.go +++ b/vendor/github.com/aws/aws-sdk-go/aws/endpoints/defaults.go @@ -744,6 +744,7 @@ var awsPartition = partition{ "ap-northeast-1": endpoint{}, "ap-southeast-1": endpoint{}, "ap-southeast-2": endpoint{}, + "ca-central-1": endpoint{}, "eu-central-1": endpoint{}, "eu-west-2": endpoint{}, "us-east-1": endpoint{}, @@ -983,6 +984,7 @@ var awsPartition = partition{ "ap-east-1": endpoint{}, "ap-northeast-1": endpoint{}, "ap-northeast-2": endpoint{}, + "ap-northeast-3": endpoint{}, "ap-south-1": endpoint{}, "ap-southeast-1": endpoint{}, "ap-southeast-2": endpoint{}, @@ -1777,6 +1779,7 @@ var awsPartition = partition{ "ap-northeast-1": endpoint{}, "ap-southeast-1": endpoint{}, "ap-southeast-2": endpoint{}, + "ca-central-1": endpoint{}, "eu-central-1": endpoint{}, "eu-west-2": endpoint{}, "us-east-1": endpoint{}, @@ -3359,8 +3362,15 @@ var awsPartition = partition{ Endpoints: endpoints{ "af-south-1": endpoint{}, "ap-southeast-2": endpoint{}, + "eu-central-1": endpoint{}, "eu-north-1": endpoint{}, "eu-west-1": endpoint{}, + "fips-us-east-1": endpoint{ + Hostname: "groundstation-fips.us-east-1.amazonaws.com", + CredentialScope: credentialScope{ + Region: "us-east-1", + }, + }, "fips-us-east-2": endpoint{ Hostname: "groundstation-fips.us-east-2.amazonaws.com", CredentialScope: credentialScope{ @@ -3374,6 +3384,7 @@ var awsPartition = partition{ }, }, "me-south-1": endpoint{}, + "us-east-1": endpoint{}, "us-east-2": endpoint{}, "us-west-2": endpoint{}, }, @@ -4462,6 +4473,7 @@ var awsPartition = partition{ "ap-east-1": endpoint{}, "ap-northeast-1": endpoint{}, "ap-northeast-2": endpoint{}, + "ap-northeast-3": endpoint{}, "ap-south-1": endpoint{}, "ap-southeast-1": endpoint{}, "ap-southeast-2": endpoint{}, @@ -5424,6 +5436,90 @@ var awsPartition = partition{ DualStackHostname: "{service}.dualstack.{region}.{dnsSuffix}", }, Endpoints: endpoints{ + "accesspoint-af-south-1": endpoint{ + Hostname: "s3-accesspoint.af-south-1.amazonaws.com", + SignatureVersions: []string{"s3v4"}, + }, + "accesspoint-ap-east-1": endpoint{ + Hostname: "s3-accesspoint.ap-east-1.amazonaws.com", + SignatureVersions: []string{"s3v4"}, + }, + "accesspoint-ap-northeast-1": endpoint{ + Hostname: "s3-accesspoint.ap-northeast-1.amazonaws.com", + SignatureVersions: []string{"s3v4"}, + }, + "accesspoint-ap-northeast-2": endpoint{ + Hostname: "s3-accesspoint.ap-northeast-2.amazonaws.com", + SignatureVersions: []string{"s3v4"}, + }, + "accesspoint-ap-northeast-3": endpoint{ + Hostname: "s3-accesspoint.ap-northeast-3.amazonaws.com", + SignatureVersions: []string{"s3v4"}, + }, + "accesspoint-ap-south-1": endpoint{ + Hostname: "s3-accesspoint.ap-south-1.amazonaws.com", + SignatureVersions: []string{"s3v4"}, + }, + "accesspoint-ap-southeast-1": endpoint{ + Hostname: "s3-accesspoint.ap-southeast-1.amazonaws.com", + SignatureVersions: []string{"s3v4"}, + }, + "accesspoint-ap-southeast-2": endpoint{ + Hostname: "s3-accesspoint.ap-southeast-2.amazonaws.com", + SignatureVersions: []string{"s3v4"}, + }, + "accesspoint-ca-central-1": endpoint{ + Hostname: "s3-accesspoint.ca-central-1.amazonaws.com", + SignatureVersions: []string{"s3v4"}, + }, + "accesspoint-eu-central-1": endpoint{ + Hostname: "s3-accesspoint.eu-central-1.amazonaws.com", + SignatureVersions: []string{"s3v4"}, + }, + "accesspoint-eu-north-1": endpoint{ + Hostname: "s3-accesspoint.eu-north-1.amazonaws.com", + SignatureVersions: []string{"s3v4"}, + }, + "accesspoint-eu-south-1": endpoint{ + Hostname: "s3-accesspoint.eu-south-1.amazonaws.com", + SignatureVersions: []string{"s3v4"}, + }, + "accesspoint-eu-west-1": endpoint{ + Hostname: "s3-accesspoint.eu-west-1.amazonaws.com", + SignatureVersions: []string{"s3v4"}, + }, + "accesspoint-eu-west-2": endpoint{ + Hostname: "s3-accesspoint.eu-west-2.amazonaws.com", + SignatureVersions: []string{"s3v4"}, + }, + "accesspoint-eu-west-3": endpoint{ + Hostname: "s3-accesspoint.eu-west-3.amazonaws.com", + SignatureVersions: []string{"s3v4"}, + }, + "accesspoint-me-south-1": endpoint{ + Hostname: "s3-accesspoint.me-south-1.amazonaws.com", + SignatureVersions: []string{"s3v4"}, + }, + "accesspoint-sa-east-1": endpoint{ + Hostname: "s3-accesspoint.sa-east-1.amazonaws.com", + SignatureVersions: []string{"s3v4"}, + }, + "accesspoint-us-east-1": endpoint{ + Hostname: "s3-accesspoint.us-east-1.amazonaws.com", + SignatureVersions: []string{"s3v4"}, + }, + "accesspoint-us-east-2": endpoint{ + Hostname: "s3-accesspoint.us-east-2.amazonaws.com", + SignatureVersions: []string{"s3v4"}, + }, + "accesspoint-us-west-1": endpoint{ + Hostname: "s3-accesspoint.us-west-1.amazonaws.com", + SignatureVersions: []string{"s3v4"}, + }, + "accesspoint-us-west-2": endpoint{ + Hostname: "s3-accesspoint.us-west-2.amazonaws.com", + SignatureVersions: []string{"s3v4"}, + }, "af-south-1": endpoint{}, "ap-east-1": endpoint{}, "ap-northeast-1": endpoint{ @@ -5456,8 +5552,28 @@ var awsPartition = partition{ Hostname: "s3.eu-west-1.amazonaws.com", SignatureVersions: []string{"s3", "s3v4"}, }, - "eu-west-2": endpoint{}, - "eu-west-3": endpoint{}, + "eu-west-2": endpoint{}, + "eu-west-3": endpoint{}, + "fips-accesspoint-ca-central-1": endpoint{ + Hostname: "s3-accesspoint-fips.ca-central-1.amazonaws.com", + SignatureVersions: []string{"s3v4"}, + }, + "fips-accesspoint-us-east-1": endpoint{ + Hostname: "s3-accesspoint-fips.us-east-1.amazonaws.com", + SignatureVersions: []string{"s3v4"}, + }, + "fips-accesspoint-us-east-2": endpoint{ + Hostname: "s3-accesspoint-fips.us-east-2.amazonaws.com", + SignatureVersions: []string{"s3v4"}, + }, + "fips-accesspoint-us-west-1": endpoint{ + Hostname: "s3-accesspoint-fips.us-west-1.amazonaws.com", + SignatureVersions: []string{"s3v4"}, + }, + "fips-accesspoint-us-west-2": endpoint{ + Hostname: "s3-accesspoint-fips.us-west-2.amazonaws.com", + SignatureVersions: []string{"s3v4"}, + }, "me-south-1": endpoint{}, "s3-external-1": endpoint{ Hostname: "s3-external-1.amazonaws.com", @@ -5760,6 +5876,7 @@ var awsPartition = partition{ "ap-east-1": endpoint{}, "ap-northeast-1": endpoint{}, "ap-northeast-2": endpoint{}, + "ap-northeast-3": endpoint{}, "ap-south-1": endpoint{}, "ap-southeast-1": endpoint{}, "ap-southeast-2": endpoint{}, @@ -6276,12 +6393,10 @@ var awsPartition = partition{ }, "me-south-1": endpoint{}, "sa-east-1": endpoint{}, - "us-east-1": endpoint{ - SSLCommonName: "queue.{dnsSuffix}", - }, - "us-east-2": endpoint{}, - "us-west-1": endpoint{}, - "us-west-2": endpoint{}, + "us-east-1": endpoint{}, + "us-east-2": endpoint{}, + "us-west-1": endpoint{}, + "us-west-2": endpoint{}, }, }, "ssm": service{ @@ -7824,6 +7939,14 @@ var awscnPartition = partition{ DualStackHostname: "{service}.dualstack.{region}.{dnsSuffix}", }, Endpoints: endpoints{ + "accesspoint-cn-north-1": endpoint{ + Hostname: "s3-accesspoint.cn-north-1.amazonaws.com.cn", + SignatureVersions: []string{"s3v4"}, + }, + "accesspoint-cn-northwest-1": endpoint{ + Hostname: "s3-accesspoint.cn-northwest-1.amazonaws.com.cn", + SignatureVersions: []string{"s3v4"}, + }, "cn-north-1": endpoint{}, "cn-northwest-1": endpoint{}, }, @@ -8117,6 +8240,27 @@ var awsusgovPartition = partition{ "us-gov-west-1": endpoint{}, }, }, + "api.detective": service{ + Defaults: endpoint{ + Protocols: []string{"https"}, + }, + Endpoints: endpoints{ + "us-gov-east-1": endpoint{}, + "us-gov-east-1-fips": endpoint{ + Hostname: "api.detective-fips.us-gov-east-1.amazonaws.com", + CredentialScope: credentialScope{ + Region: "us-gov-east-1", + }, + }, + "us-gov-west-1": endpoint{}, + "us-gov-west-1-fips": endpoint{ + Hostname: "api.detective-fips.us-gov-west-1.amazonaws.com", + CredentialScope: credentialScope{ + Region: "us-gov-west-1", + }, + }, + }, + }, "api.ecr": service{ Endpoints: endpoints{ @@ -8270,18 +8414,6 @@ var awsusgovPartition = partition{ "batch": service{ Endpoints: endpoints{ - "fips-us-gov-east-1": endpoint{ - Hostname: "batch.us-gov-east-1.amazonaws.com", - CredentialScope: credentialScope{ - Region: "us-gov-east-1", - }, - }, - "fips-us-gov-west-1": endpoint{ - Hostname: "batch.us-gov-west-1.amazonaws.com", - CredentialScope: credentialScope{ - Region: "us-gov-west-1", - }, - }, "us-gov-east-1": endpoint{}, "us-gov-west-1": endpoint{}, }, @@ -9389,6 +9521,22 @@ var awsusgovPartition = partition{ DualStackHostname: "{service}.dualstack.{region}.{dnsSuffix}", }, Endpoints: endpoints{ + "accesspoint-us-gov-east-1": endpoint{ + Hostname: "s3-accesspoint.us-gov-east-1.amazonaws.com", + SignatureVersions: []string{"s3v4"}, + }, + "accesspoint-us-gov-west-1": endpoint{ + Hostname: "s3-accesspoint.us-gov-west-1.amazonaws.com", + SignatureVersions: []string{"s3v4"}, + }, + "fips-accesspoint-us-gov-east-1": endpoint{ + Hostname: "s3-accesspoint-fips.us-gov-east-1.amazonaws.com", + SignatureVersions: []string{"s3v4"}, + }, + "fips-accesspoint-us-gov-west-1": endpoint{ + Hostname: "s3-accesspoint-fips.us-gov-west-1.amazonaws.com", + SignatureVersions: []string{"s3v4"}, + }, "fips-us-gov-west-1": endpoint{ Hostname: "s3-fips.us-gov-west-1.amazonaws.com", CredentialScope: credentialScope{ @@ -9522,6 +9670,27 @@ var awsusgovPartition = partition{ }, }, }, + "servicequotas": service{ + Defaults: endpoint{ + Protocols: []string{"https"}, + }, + Endpoints: endpoints{ + "fips-us-gov-east-1": endpoint{ + Hostname: "servicequotas.us-gov-east-1.amazonaws.com", + CredentialScope: credentialScope{ + Region: "us-gov-east-1", + }, + }, + "fips-us-gov-west-1": endpoint{ + Hostname: "servicequotas.us-gov-west-1.amazonaws.com", + CredentialScope: credentialScope{ + Region: "us-gov-west-1", + }, + }, + "us-gov-east-1": endpoint{}, + "us-gov-west-1": endpoint{}, + }, + }, "sms": service{ Endpoints: endpoints{ diff --git a/vendor/github.com/aws/aws-sdk-go/aws/version.go b/vendor/github.com/aws/aws-sdk-go/aws/version.go index 88f5dc06d..c0aa3c2d2 100644 --- a/vendor/github.com/aws/aws-sdk-go/aws/version.go +++ b/vendor/github.com/aws/aws-sdk-go/aws/version.go @@ -5,4 +5,4 @@ package aws const SDKName = "aws-sdk-go" // SDKVersion is the version of this SDK -const SDKVersion = "1.38.4" +const SDKVersion = "1.38.12" diff --git a/vendor/github.com/mattn/go-runewidth/runewidth.go b/vendor/github.com/mattn/go-runewidth/runewidth.go index f3871a624..3d7fa560b 100644 --- a/vendor/github.com/mattn/go-runewidth/runewidth.go +++ b/vendor/github.com/mattn/go-runewidth/runewidth.go @@ -12,8 +12,14 @@ var ( // EastAsianWidth will be set true if the current locale is CJK EastAsianWidth bool + // StrictEmojiNeutral should be set false if handle broken fonts + StrictEmojiNeutral bool = true + // DefaultCondition is a condition in current locale - DefaultCondition = &Condition{} + DefaultCondition = &Condition{ + EastAsianWidth: false, + StrictEmojiNeutral: true, + } ) func init() { @@ -83,26 +89,52 @@ var nonprint = table{ // Condition have flag EastAsianWidth whether the current locale is CJK or not. type Condition struct { - EastAsianWidth bool + EastAsianWidth bool + StrictEmojiNeutral bool } // NewCondition return new instance of Condition which is current locale. func NewCondition() *Condition { return &Condition{ - EastAsianWidth: EastAsianWidth, + EastAsianWidth: EastAsianWidth, + StrictEmojiNeutral: StrictEmojiNeutral, } } // RuneWidth returns the number of cells in r. // See http://www.unicode.org/reports/tr11/ func (c *Condition) RuneWidth(r rune) int { - switch { - case r < 0 || r > 0x10FFFF || inTables(r, nonprint, combining, notassigned): - return 0 - case (c.EastAsianWidth && IsAmbiguousWidth(r)) || inTables(r, doublewidth): - return 2 - default: - return 1 + // optimized version, verified by TestRuneWidthChecksums() + if !c.EastAsianWidth { + switch { + case r < 0x20 || r > 0x10FFFF: + return 0 + case (r >= 0x7F && r <= 0x9F) || r == 0xAD: // nonprint + return 0 + case r < 0x300: + return 1 + case inTable(r, narrow): + return 1 + case inTables(r, nonprint, combining): + return 0 + case inTable(r, doublewidth): + return 2 + default: + return 1 + } + } else { + switch { + case r < 0 || r > 0x10FFFF || inTables(r, nonprint, combining): + return 0 + case inTable(r, narrow): + return 1 + case inTables(r, ambiguous, doublewidth): + return 2 + case !c.StrictEmojiNeutral && inTables(r, ambiguous, emoji, narrow): + return 2 + default: + return 1 + } } } diff --git a/vendor/github.com/mattn/go-runewidth/runewidth_table.go b/vendor/github.com/mattn/go-runewidth/runewidth_table.go index b27d77d89..e5d890c26 100644 --- a/vendor/github.com/mattn/go-runewidth/runewidth_table.go +++ b/vendor/github.com/mattn/go-runewidth/runewidth_table.go @@ -124,8 +124,10 @@ var ambiguous = table{ {0x1F18F, 0x1F190}, {0x1F19B, 0x1F1AC}, {0xE0100, 0xE01EF}, {0xF0000, 0xFFFFD}, {0x100000, 0x10FFFD}, } -var notassigned = table{ - {0x27E6, 0x27ED}, {0x2985, 0x2986}, +var narrow = table{ + {0x0020, 0x007E}, {0x00A2, 0x00A3}, {0x00A5, 0x00A6}, + {0x00AC, 0x00AC}, {0x00AF, 0x00AF}, {0x27E6, 0x27ED}, + {0x2985, 0x2986}, } var neutral = table{ diff --git a/vendor/golang.org/x/oauth2/google/internal/externalaccount/aws.go b/vendor/golang.org/x/oauth2/google/internal/externalaccount/aws.go index 2f078f73a..fbcefb474 100644 --- a/vendor/golang.org/x/oauth2/google/internal/externalaccount/aws.go +++ b/vendor/golang.org/x/oauth2/google/internal/externalaccount/aws.go @@ -5,6 +5,7 @@ package externalaccount import ( + "bytes" "context" "crypto/hmac" "crypto/sha256" @@ -127,7 +128,7 @@ func canonicalHeaders(req *http.Request) (string, string) { } sort.Strings(headers) - var fullHeaders strings.Builder + var fullHeaders bytes.Buffer for _, header := range headers { headerValue := strings.Join(lowerCaseHeaders[header], ",") fullHeaders.WriteString(header) diff --git a/vendor/golang.org/x/sys/unix/mkerrors.sh b/vendor/golang.org/x/sys/unix/mkerrors.sh index 086d69411..007358af8 100644 --- a/vendor/golang.org/x/sys/unix/mkerrors.sh +++ b/vendor/golang.org/x/sys/unix/mkerrors.sh @@ -405,10 +405,11 @@ includes_SunOS=' #include #include #include +#include #include -#include #include #include +#include ' @@ -499,10 +500,10 @@ ccflags="$@" $2 ~ /^LOCK_(SH|EX|NB|UN)$/ || $2 ~ /^LO_(KEY|NAME)_SIZE$/ || $2 ~ /^LOOP_(CLR|CTL|GET|SET)_/ || - $2 ~ /^(AF|SOCK|SO|SOL|IPPROTO|IP|IPV6|ICMP6|TCP|MCAST|EVFILT|NOTE|SHUT|PROT|MAP|MFD|T?PACKET|MSG|SCM|MCL|DT|MADV|PR|LOCAL)_/ || + $2 ~ /^(AF|SOCK|SO|SOL|IPPROTO|IP|IPV6|TCP|MCAST|EVFILT|NOTE|SHUT|PROT|MAP|MFD|T?PACKET|MSG|SCM|MCL|DT|MADV|PR|LOCAL)_/ || $2 ~ /^TP_STATUS_/ || $2 ~ /^FALLOC_/ || - $2 ~ /^ICMP(V6)?_FILTER$/ || + $2 ~ /^ICMPV?6?_(FILTER|SEC)/ || $2 == "SOMAXCONN" || $2 == "NAME_MAX" || $2 == "IFNAMSIZ" || diff --git a/vendor/golang.org/x/sys/unix/zerrors_freebsd_arm.go b/vendor/golang.org/x/sys/unix/zerrors_freebsd_arm.go index 0326a6b3a..3df99f285 100644 --- a/vendor/golang.org/x/sys/unix/zerrors_freebsd_arm.go +++ b/vendor/golang.org/x/sys/unix/zerrors_freebsd_arm.go @@ -1022,6 +1022,15 @@ const ( MAP_RESERVED0100 = 0x100 MAP_SHARED = 0x1 MAP_STACK = 0x400 + MCAST_BLOCK_SOURCE = 0x54 + MCAST_EXCLUDE = 0x2 + MCAST_INCLUDE = 0x1 + MCAST_JOIN_GROUP = 0x50 + MCAST_JOIN_SOURCE_GROUP = 0x52 + MCAST_LEAVE_GROUP = 0x51 + MCAST_LEAVE_SOURCE_GROUP = 0x53 + MCAST_UNBLOCK_SOURCE = 0x55 + MCAST_UNDEFINED = 0x0 MCL_CURRENT = 0x1 MCL_FUTURE = 0x2 MNT_ACLS = 0x8000000 diff --git a/vendor/golang.org/x/sys/unix/zerrors_linux.go b/vendor/golang.org/x/sys/unix/zerrors_linux.go index 3b1b9287b..35de419c6 100644 --- a/vendor/golang.org/x/sys/unix/zerrors_linux.go +++ b/vendor/golang.org/x/sys/unix/zerrors_linux.go @@ -974,6 +974,10 @@ const ( HUGETLBFS_MAGIC = 0x958458f6 IBSHIFT = 0x10 ICMPV6_FILTER = 0x1 + ICMPV6_FILTER_BLOCK = 0x1 + ICMPV6_FILTER_BLOCKOTHERS = 0x3 + ICMPV6_FILTER_PASS = 0x2 + ICMPV6_FILTER_PASSONLY = 0x4 ICMP_FILTER = 0x1 ICRNL = 0x100 IFA_F_DADFAILED = 0x8 diff --git a/vendor/golang.org/x/sys/unix/zerrors_solaris_amd64.go b/vendor/golang.org/x/sys/unix/zerrors_solaris_amd64.go index 65fb2c5cd..1afee6a08 100644 --- a/vendor/golang.org/x/sys/unix/zerrors_solaris_amd64.go +++ b/vendor/golang.org/x/sys/unix/zerrors_solaris_amd64.go @@ -366,6 +366,7 @@ const ( HUPCL = 0x400 IBSHIFT = 0x10 ICANON = 0x2 + ICMP6_FILTER = 0x1 ICRNL = 0x100 IEXTEN = 0x8000 IFF_ADDRCONF = 0x80000 @@ -612,6 +613,7 @@ const ( IP_RECVPKTINFO = 0x1a IP_RECVRETOPTS = 0x6 IP_RECVSLLA = 0xa + IP_RECVTOS = 0xc IP_RECVTTL = 0xb IP_RETOPTS = 0x8 IP_REUSEADDR = 0x104 @@ -704,6 +706,7 @@ const ( O_APPEND = 0x8 O_CLOEXEC = 0x800000 O_CREAT = 0x100 + O_DIRECT = 0x2000000 O_DIRECTORY = 0x1000000 O_DSYNC = 0x40 O_EXCL = 0x400 diff --git a/vendor/golang.org/x/sys/unix/zerrors_zos_s390x.go b/vendor/golang.org/x/sys/unix/zerrors_zos_s390x.go index 4117ce08a..c8c790903 100644 --- a/vendor/golang.org/x/sys/unix/zerrors_zos_s390x.go +++ b/vendor/golang.org/x/sys/unix/zerrors_zos_s390x.go @@ -137,6 +137,7 @@ const ( IP_TTL = 3 IP_UNBLOCK_SOURCE = 11 ICANON = 0x0010 + ICMP6_FILTER = 0x26 ICRNL = 0x0002 IEXTEN = 0x0020 IGNBRK = 0x0004 diff --git a/vendor/golang.org/x/text/secure/bidirule/bidirule10.0.0.go b/vendor/golang.org/x/text/secure/bidirule/bidirule10.0.0.go index e4c62289f..8a7392c4a 100644 --- a/vendor/golang.org/x/text/secure/bidirule/bidirule10.0.0.go +++ b/vendor/golang.org/x/text/secure/bidirule/bidirule10.0.0.go @@ -2,6 +2,7 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. +//go:build go1.10 // +build go1.10 package bidirule diff --git a/vendor/golang.org/x/text/secure/bidirule/bidirule9.0.0.go b/vendor/golang.org/x/text/secure/bidirule/bidirule9.0.0.go index 02b9e1e9d..bb0a92001 100644 --- a/vendor/golang.org/x/text/secure/bidirule/bidirule9.0.0.go +++ b/vendor/golang.org/x/text/secure/bidirule/bidirule9.0.0.go @@ -2,6 +2,7 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. +//go:build !go1.10 // +build !go1.10 package bidirule diff --git a/vendor/golang.org/x/text/unicode/bidi/tables10.0.0.go b/vendor/golang.org/x/text/unicode/bidi/tables10.0.0.go index d8c94e1bd..42fa8d72c 100644 --- a/vendor/golang.org/x/text/unicode/bidi/tables10.0.0.go +++ b/vendor/golang.org/x/text/unicode/bidi/tables10.0.0.go @@ -1,5 +1,6 @@ // Code generated by running "go generate" in golang.org/x/text. DO NOT EDIT. +//go:build go1.10 && !go1.13 // +build go1.10,!go1.13 package bidi diff --git a/vendor/golang.org/x/text/unicode/bidi/tables11.0.0.go b/vendor/golang.org/x/text/unicode/bidi/tables11.0.0.go index 16b11db53..56a0e1ea2 100644 --- a/vendor/golang.org/x/text/unicode/bidi/tables11.0.0.go +++ b/vendor/golang.org/x/text/unicode/bidi/tables11.0.0.go @@ -1,5 +1,6 @@ // Code generated by running "go generate" in golang.org/x/text. DO NOT EDIT. +//go:build go1.13 && !go1.14 // +build go1.13,!go1.14 package bidi diff --git a/vendor/golang.org/x/text/unicode/bidi/tables12.0.0.go b/vendor/golang.org/x/text/unicode/bidi/tables12.0.0.go index 647f2d427..baacf32b4 100644 --- a/vendor/golang.org/x/text/unicode/bidi/tables12.0.0.go +++ b/vendor/golang.org/x/text/unicode/bidi/tables12.0.0.go @@ -1,5 +1,6 @@ // Code generated by running "go generate" in golang.org/x/text. DO NOT EDIT. +//go:build go1.14 && !go1.16 // +build go1.14,!go1.16 package bidi diff --git a/vendor/golang.org/x/text/unicode/bidi/tables13.0.0.go b/vendor/golang.org/x/text/unicode/bidi/tables13.0.0.go index c937d0976..f248effae 100644 --- a/vendor/golang.org/x/text/unicode/bidi/tables13.0.0.go +++ b/vendor/golang.org/x/text/unicode/bidi/tables13.0.0.go @@ -1,5 +1,6 @@ // Code generated by running "go generate" in golang.org/x/text. DO NOT EDIT. +//go:build go1.16 // +build go1.16 package bidi diff --git a/vendor/golang.org/x/text/unicode/bidi/tables9.0.0.go b/vendor/golang.org/x/text/unicode/bidi/tables9.0.0.go index 0ca0193eb..f517fdb20 100644 --- a/vendor/golang.org/x/text/unicode/bidi/tables9.0.0.go +++ b/vendor/golang.org/x/text/unicode/bidi/tables9.0.0.go @@ -1,5 +1,6 @@ // Code generated by running "go generate" in golang.org/x/text. DO NOT EDIT. +//go:build !go1.10 // +build !go1.10 package bidi diff --git a/vendor/golang.org/x/text/unicode/norm/tables10.0.0.go b/vendor/golang.org/x/text/unicode/norm/tables10.0.0.go index 26fbd55a1..f5a078827 100644 --- a/vendor/golang.org/x/text/unicode/norm/tables10.0.0.go +++ b/vendor/golang.org/x/text/unicode/norm/tables10.0.0.go @@ -1,5 +1,6 @@ // Code generated by running "go generate" in golang.org/x/text. DO NOT EDIT. +//go:build go1.10 && !go1.13 // +build go1.10,!go1.13 package norm diff --git a/vendor/golang.org/x/text/unicode/norm/tables11.0.0.go b/vendor/golang.org/x/text/unicode/norm/tables11.0.0.go index 2c58f09ba..cb7239c43 100644 --- a/vendor/golang.org/x/text/unicode/norm/tables11.0.0.go +++ b/vendor/golang.org/x/text/unicode/norm/tables11.0.0.go @@ -1,5 +1,6 @@ // Code generated by running "go generate" in golang.org/x/text. DO NOT EDIT. +//go:build go1.13 && !go1.14 // +build go1.13,!go1.14 package norm diff --git a/vendor/golang.org/x/text/unicode/norm/tables12.0.0.go b/vendor/golang.org/x/text/unicode/norm/tables12.0.0.go index 7e1ae096e..11b273300 100644 --- a/vendor/golang.org/x/text/unicode/norm/tables12.0.0.go +++ b/vendor/golang.org/x/text/unicode/norm/tables12.0.0.go @@ -1,5 +1,6 @@ // Code generated by running "go generate" in golang.org/x/text. DO NOT EDIT. +//go:build go1.14 && !go1.16 // +build go1.14,!go1.16 package norm diff --git a/vendor/golang.org/x/text/unicode/norm/tables13.0.0.go b/vendor/golang.org/x/text/unicode/norm/tables13.0.0.go index 9ea1b4214..96a130d30 100644 --- a/vendor/golang.org/x/text/unicode/norm/tables13.0.0.go +++ b/vendor/golang.org/x/text/unicode/norm/tables13.0.0.go @@ -1,5 +1,6 @@ // Code generated by running "go generate" in golang.org/x/text. DO NOT EDIT. +//go:build go1.16 // +build go1.16 package norm diff --git a/vendor/golang.org/x/text/unicode/norm/tables9.0.0.go b/vendor/golang.org/x/text/unicode/norm/tables9.0.0.go index 942906929..0175eae50 100644 --- a/vendor/golang.org/x/text/unicode/norm/tables9.0.0.go +++ b/vendor/golang.org/x/text/unicode/norm/tables9.0.0.go @@ -1,5 +1,6 @@ // Code generated by running "go generate" in golang.org/x/text. DO NOT EDIT. +//go:build !go1.10 // +build !go1.10 package norm diff --git a/vendor/google.golang.org/grpc/version.go b/vendor/google.golang.org/grpc/version.go index 51024d6b3..1051b7eff 100644 --- a/vendor/google.golang.org/grpc/version.go +++ b/vendor/google.golang.org/grpc/version.go @@ -19,4 +19,4 @@ package grpc // Version is the current grpc version. -const Version = "1.36.0" +const Version = "1.36.1" diff --git a/vendor/modules.txt b/vendor/modules.txt index 7741e99a2..914727ba1 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -1,4 +1,4 @@ -# cloud.google.com/go v0.80.0 +# cloud.google.com/go v0.81.0 ## explicit cloud.google.com/go cloud.google.com/go/compute/metadata @@ -27,7 +27,7 @@ github.com/VictoriaMetrics/metricsql github.com/VictoriaMetrics/metricsql/binaryop # github.com/VividCortex/ewma v1.1.1 github.com/VividCortex/ewma -# github.com/aws/aws-sdk-go v1.38.4 +# github.com/aws/aws-sdk-go v1.38.12 ## explicit github.com/aws/aws-sdk-go/aws github.com/aws/aws-sdk-go/aws/arn @@ -100,9 +100,11 @@ github.com/go-kit/kit/log github.com/go-kit/kit/log/level # github.com/go-logfmt/logfmt v0.5.0 github.com/go-logfmt/logfmt -# github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e +# github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da +## explicit github.com/golang/groupcache/lru -# github.com/golang/protobuf v1.5.1 +# github.com/golang/protobuf v1.5.2 +## explicit github.com/golang/protobuf/internal/gengogrpc github.com/golang/protobuf/proto github.com/golang/protobuf/protoc-gen-go @@ -140,7 +142,7 @@ github.com/klauspost/compress/zstd/internal/xxhash github.com/mattn/go-colorable # github.com/mattn/go-isatty v0.0.12 github.com/mattn/go-isatty -# github.com/mattn/go-runewidth v0.0.10 +# github.com/mattn/go-runewidth v0.0.12 ## explicit github.com/mattn/go-runewidth # github.com/matttproud/golang_protobuf_extensions v1.0.1 @@ -237,7 +239,7 @@ golang.org/x/lint/golint ## explicit golang.org/x/mod/module golang.org/x/mod/semver -# golang.org/x/net v0.0.0-20210324205630-d1beb07c2056 +# golang.org/x/net v0.0.0-20210331212208-0fccb6fa2b5c ## explicit golang.org/x/net/context golang.org/x/net/context/ctxhttp @@ -247,7 +249,7 @@ golang.org/x/net/http2/hpack golang.org/x/net/idna golang.org/x/net/internal/timeseries golang.org/x/net/trace -# golang.org/x/oauth2 v0.0.0-20210323180902-22b0adad7558 +# golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602 ## explicit golang.org/x/oauth2 golang.org/x/oauth2/google @@ -257,13 +259,14 @@ golang.org/x/oauth2/jws golang.org/x/oauth2/jwt # golang.org/x/sync v0.0.0-20210220032951-036812b2e83c golang.org/x/sync/errgroup -# golang.org/x/sys v0.0.0-20210324051608-47abb6519492 +# golang.org/x/sys v0.0.0-20210331175145-43e1dd70ce54 ## explicit golang.org/x/sys/execabs golang.org/x/sys/internal/unsafeheader golang.org/x/sys/unix golang.org/x/sys/windows -# golang.org/x/text v0.3.5 +# golang.org/x/text v0.3.6 +## explicit golang.org/x/text/secure/bidirule golang.org/x/text/transform golang.org/x/text/unicode/bidi @@ -311,14 +314,13 @@ google.golang.org/appengine/internal/modules google.golang.org/appengine/internal/remote_api google.golang.org/appengine/internal/urlfetch google.golang.org/appengine/urlfetch -# google.golang.org/genproto v0.0.0-20210325141258-5636347f2b14 -## explicit +# google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1 google.golang.org/genproto/googleapis/api/annotations google.golang.org/genproto/googleapis/iam/v1 google.golang.org/genproto/googleapis/rpc/code google.golang.org/genproto/googleapis/rpc/status google.golang.org/genproto/googleapis/type/expr -# google.golang.org/grpc v1.36.0 +# google.golang.org/grpc v1.36.1 google.golang.org/grpc google.golang.org/grpc/attributes google.golang.org/grpc/backoff From 0db901617d4c8a545b5714815add8a26886696c2 Mon Sep 17 00:00:00 2001 From: Aliaksandr Valialkin Date: Fri, 2 Apr 2021 22:54:06 +0300 Subject: [PATCH 23/63] app: do not process non-GET requests on at `/` handler --- app/victoria-metrics/main.go | 3 +++ app/vmagent/main.go | 3 +++ app/vmalert/web.go | 3 +++ 3 files changed, 9 insertions(+) diff --git a/app/victoria-metrics/main.go b/app/victoria-metrics/main.go index 61f3e61c8..77ba52829 100644 --- a/app/victoria-metrics/main.go +++ b/app/victoria-metrics/main.go @@ -86,6 +86,9 @@ func main() { func requestHandler(w http.ResponseWriter, r *http.Request) bool { if r.URL.Path == "/" { + if r.Method != "GET" { + return false + } fmt.Fprintf(w, "

Single-node VictoriaMetrics.


") fmt.Fprintf(w, "See docs at https://victoriametrics.github.io/
") fmt.Fprintf(w, "Useful endpoints:
") diff --git a/app/vmagent/main.go b/app/vmagent/main.go index 7d7635a10..43552cf94 100644 --- a/app/vmagent/main.go +++ b/app/vmagent/main.go @@ -145,6 +145,9 @@ func main() { func requestHandler(w http.ResponseWriter, r *http.Request) bool { if r.URL.Path == "/" { + if r.Method != "GET" { + return false + } fmt.Fprintf(w, "vmagent - see docs at https://victoriametrics.github.io/vmagent.html") return true } diff --git a/app/vmalert/web.go b/app/vmalert/web.go index a095356d9..8fc71359e 100644 --- a/app/vmalert/web.go +++ b/app/vmalert/web.go @@ -29,6 +29,9 @@ var pathList = [][]string{ func (rh *requestHandler) handler(w http.ResponseWriter, r *http.Request) bool { switch r.URL.Path { case "/": + if r.Method != "GET" { + return false + } for _, path := range pathList { p, doc := path[0], path[1] fmt.Fprintf(w, "%q - %s
", p, p, doc) From 3c1b39c9785f6509baebb9a767812f197e202ccc Mon Sep 17 00:00:00 2001 From: Aliaksandr Valialkin Date: Fri, 2 Apr 2021 23:08:41 +0300 Subject: [PATCH 24/63] app/vmgateway: publish docs --- Makefile | 1 + app/vmgateway/README.md | 281 +++++++++++++++++++++ app/vmgateway/vmgateway-access-control.jpg | Bin 0 -> 39434 bytes app/vmgateway/vmgateway-overview.jpeg | Bin 0 -> 49136 bytes app/vmgateway/vmgateway-rate-limiting.jpg | Bin 0 -> 35619 bytes app/vmgateway/vmgateway.png | Bin 0 -> 49100 bytes docs/vmauth.md | 22 +- docs/vmgateway-access-control.jpg | Bin 0 -> 39434 bytes docs/vmgateway-overview.jpeg | Bin 0 -> 49136 bytes docs/vmgateway-rate-limiting.jpg | Bin 0 -> 35619 bytes docs/vmgateway.md | 281 +++++++++++++++++++++ 11 files changed, 576 insertions(+), 9 deletions(-) create mode 100644 app/vmgateway/README.md create mode 100644 app/vmgateway/vmgateway-access-control.jpg create mode 100644 app/vmgateway/vmgateway-overview.jpeg create mode 100644 app/vmgateway/vmgateway-rate-limiting.jpg create mode 100644 app/vmgateway/vmgateway.png create mode 100644 docs/vmgateway-access-control.jpg create mode 100644 docs/vmgateway-overview.jpeg create mode 100644 docs/vmgateway-rate-limiting.jpg create mode 100644 docs/vmgateway.md diff --git a/Makefile b/Makefile index 237a391a2..a147a074b 100644 --- a/Makefile +++ b/Makefile @@ -269,4 +269,5 @@ docs-sync: cp app/vmbackup/README.md docs/vmbackup.md cp app/vmrestore/README.md docs/vmrestore.md cp app/vmctl/README.md docs/vmctl.md + cp app/vmgateway/README.md docs/vmgateway.md cp README.md docs/Single-server-VictoriaMetrics.md diff --git a/app/vmgateway/README.md b/app/vmgateway/README.md new file mode 100644 index 000000000..fbcb6905d --- /dev/null +++ b/app/vmgateway/README.md @@ -0,0 +1,281 @@ +## Victori Metrics Gateway + + +vmgateway + +The service is a proxy for Victoria Metrics TSDB. It provides the next features: + +* Rate Limiter + * Based on cluster tenants' utilization supports multiple time interval limits for ingestion/retrieving metrics +* Token Access Control + * Supports additional per-label access control for Single and Cluster versions of Victoria Metrics TSDB + * Provides access by tenantID at Cluster version + * Allows to separate write/read/admin access to data + + +### Access Control + +vmgateway-ac + +`vmgateway` supports jwt based authentication. With jwt payload can be configured access to specific tenant, labels, read/write. + +jwt token must be in following format: +```json +{ + "exp": 1617304574, + "vm_access": { + "tenant_id": { + "account_id": 1, + "project_id": 5 + }, + "extra_labels": { + "team": "dev", + "project": "mobile" + }, + "mode": 1 + } +} +``` +Where: +- `exp` - required, expire time in unix_timestamp. If token expires, `vmgateway` rejects request. +- `vm_access` - required, dict with claim info, minimum form: `{"vm_access": {"tenand_id": {}}` +- `tenant_id` - optional, make sense only for cluster mode, routes request to corresponding tenant. +- `extra_labels` - optional, key-value pairs for label filters - added to ingested or selected metrics. +- `mode` - optional, access mode for api - read, write, full. supported values: 0 - full (default value), 1 - read, 2 - write. + +#### QuickStart + +Start single version of Victoria Metrics + +```bash +# single +# start node +./bin/victoria-metrics --selfScrapeInterval=10s +``` + +Start vmgateway + +``` +./bin/vmgateway -eula -enable.auth -read.url http://localhost:8428 --write.url http://localhost:8428 +``` + +Retieve data frof database +``` +curl 'http://localhost:8431/api/v1/series/count' -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ2bV9hY2Nlc3MiOnsidGVuYW50X2lkIjp7fSwicm9sZSI6MX0sImV4cCI6MTkzOTM0NjIxMH0.5WUxEfdcV9hKo4CtQdtuZYOGpGXWwaqM9VuVivMMrVg' + +TODO: need to have queries to show the limits +``` + +Expected result +``` +TODO: must be provided +``` + + +### Rate Limiter + +vmgateway-rl + +TODO: no information about source for rate limiting + +Limits incoming requests by given pre-configured limits. It supports read and write limiting with `minute` and `hour` interval. + +List of supported limit types: +- `queries` - count of api requests made at tenant to read api, such as `/api/v1/query`, `/api/v1/series` and others. +- `active_series` - count of current active series at given tenant. +- `new_series` - count of created series aka churn rate +- `rows_inserted` - count of inserted rows per tenant. + +List of supported time windows: +- `minute` +- `hour` + +Limits can be specified per tenant or at global level, if you omit `project_id` and `account_id`. + +Example of configuration file: + +```yaml +limits: + - type: queries + value: 1000 + resolution: minute + - type: queries + value: 10000 + resolution: hour + - type: queries + value: 10 + resolution: minute + project_id: 5 + account_id: 1 +``` + +#### QuickStart + +ClusterMode +```bash +# start datasource for cluster metrics + +cat << EOF > cluster.yaml +scrape_configs: + - job_name: cluster + scrape_interval: 5s + static_configs: + - targets: ['127.0.0.1:8481','127.0.0.1:8482','127.0.0.1:8480'] +EOF + +./bin/victoria-metrics --promscrape.config cluster.yaml + +# start cluster + +# start vmstorage, vmselect and vminsert +./bin/vmstorage -eula +./bin/vmselect -eula -storageNode 127.0.0.1:8401 +./bin/vminsert -eula -storageNode 127.0.0.1:8400 + +# create base rate limitng config: +cat << EOF > limit.yaml +limits: + - type: queries + value: 100 + - type: rows_inserted + value: 100000 + - type: new_series + value: 1000 + - type: active_series + value: 100000 + - type: queries + value: 1 + account_id: 15 +EOF + +# start gateway with clusterMoe +./bin/vmgateway -eula -enable.rateLimit -ratelimit.config limit.yaml -datasource.url http://localhost:8428 -enable.auth -clusterMode -write.url=http://localhost:8480 --read.url=http://localhost:8481 + +# ingest simple metric to tenant 1:5 +curl 'http://localhost:8431/api/v1/import/prometheus' -X POST -d 'foo{bar="baz1"} 123' -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2MjAxNjIwMDAwMDAsInZtX2FjY2VzcyI6eyJ0ZW5hbnRfaWQiOnsiYWNjb3VudF9pZCI6MTV9fX0.PB1_KXDKPUp-40pxOGk6lt_jt9Yq80PIMpWVJqSForQ' +# read metric from tenant 1:5 +curl 'http://localhost:8431/api/v1/labels' -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2MjAxNjIwMDAwMDAsInZtX2FjY2VzcyI6eyJ0ZW5hbnRfaWQiOnsiYWNjb3VudF9pZCI6MTV9fX0.PB1_KXDKPUp-40pxOGk6lt_jt9Yq80PIMpWVJqSForQ' +``` + +### Configuration + +The shortlist of configuration flags is the following: +```bash + -clusterMode + enable it for cluster version + -datasource.appendTypePrefix + Whether to add type prefix to -datasource.url based on the query type. Set to true if sending different query types to VMSelect URL. + -datasource.basicAuth.password string + Optional basic auth password for -datasource.url + -datasource.basicAuth.username string + Optional basic auth username for -datasource.url + -datasource.lookback duration + Lookback defines how far to look into past when evaluating queries. For example, if datasource.lookback=5m then param "time" with value now()-5m will be added to every query. + -datasource.maxIdleConnections int + Defines the number of idle (keep-alive connections) to configured datasource.Consider to set this value equal to the value: groups_total * group.concurrency. Too low value may result into high number of sockets in TIME_WAIT state. (default 100) + -datasource.queryStep duration + queryStep defines how far a value can fallback to when evaluating queries. For example, if datasource.queryStep=15s then param "step" with value "15s" will be added to every query. + -datasource.tlsCAFile string + Optional path to TLS CA file to use for verifying connections to -datasource.url. By default system CA is used + -datasource.tlsCertFile string + Optional path to client-side TLS certificate file to use when connecting to -datasource.url + -datasource.tlsInsecureSkipVerify + Whether to skip tls verification when connecting to -datasource.url + -datasource.tlsKeyFile string + Optional path to client-side TLS certificate key to use when connecting to -datasource.url + -datasource.tlsServerName string + Optional TLS server name to use for connections to -datasource.url. By default the server name from -datasource.url is used + -datasource.url string + Victoria Metrics or VMSelect url. Required parameter. E.g. http://127.0.0.1:8428 + -enable.auth + enables auth with jwt token + -enable.rateLimit + enables rate limiter + -enableTCP6 + Whether to enable IPv6 for listening and dialing. By default only IPv4 TCP and UDP is used + -envflag.enable + Whether to enable reading flags from environment variables additionally to command line. Command line flag values have priority over values from environment vars. Flags are read only from command line if this flag isnt set + -envflag.prefix string + Prefix for environment variables if -envflag.enable is set + -eula + By specifying this flag you confirm that you have an enterprise license and accept the EULA https://victoriametrics.com/assets/VM_EULA.pdf + -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() + -http.connTimeout duration + Incoming http connections are closed after the configured timeout. This may help spreading incoming load among a cluster of services behind load balancer. Note that the real timeout may be bigger by up to 10% as a protection from Thundering herd problem (default 2m0s) + -http.disableResponseCompression + Disable compression of HTTP responses for saving CPU resources. By default compression is enabled to save network bandwidth + -http.idleConnTimeout duration + Timeout for incoming idle http connections (default 1m0s) + -http.maxGracefulShutdownDuration duration + The maximum duration for graceful shutdown of HTTP server. Highly loaded server may require increased value for graceful shutdown (default 7s) + -http.pathPrefix string + An optional prefix to add to all the paths handled by http server. For example, if '-http.pathPrefix=/foo/bar' is set, then all the http requests will be handled on '/foo/bar/*' paths. This may be useful for proxied requests. See https://www.robustperception.io/using-external-urls-and-proxies-with-prometheus + -http.shutdownDelay duration + Optional delay before http server shutdown. During this dealy the servier returns non-OK responses from /health page, so load balancers can route new requests to other servers + -httpAuth.password string + Password for HTTP Basic Auth. The authentication is disabled if -httpAuth.username is empty + -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") + -loggerDisableTimestamps + Whether to disable writing timestamps in logs + -loggerErrorsPerSecondLimit int + Per-second limit on the number of ERROR messages. If more than the given number of errors are emitted per second, then the remaining errors are suppressed. Zero value disables the rate limit + -loggerFormat string + Format for logs. Possible values: default, json (default "default") + -loggerLevel string + Minimum level of errors to log. Possible values: INFO, WARN, ERROR, FATAL, PANIC (default "INFO") + -loggerOutput string + Output for the logs. Supported values: stderr, stdout (default "stderr") + -loggerTimezone string + 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 value disables the rate limit + -memory.allowedBytes size + Allowed size of system memory VictoriaMetrics caches may occupy. This option overrides -memory.allowedPercent if set to non-zero value. Too low value may increase cache miss rate, which usually results in higher CPU and disk IO usage. Too high value may evict too much data from OS page cache, which will result in higher disk IO usage + Supports the following optional suffixes for size values: KB, MB, GB, KiB, MiB, GiB (default 0) + -memory.allowedPercent float + Allowed percent of system memory VictoriaMetrics caches may occupy. See also -memory.allowedBytes. Too low value may increase cache miss rate, which usually results in higher CPU and disk IO usage. Too high value may evict too much data from OS page cache, which will result in higher disk IO usage (default 60) + -metricsAuthKey string + Auth key for /metrics. It overrides httpAuth settings + -pprofAuthKey string + Auth key for /debug/pprof. It overrides httpAuth settings + -ratelimit.config string + path for configuration file + -ratelimit.extraLabels array + additional labels, that will be applied to fetchdata from datasource + Supports array of values separated by comma or specified via multiple flags. + -ratelimit.refreshInterval duration + (default 5s) + -read.url string + read access url address, example: http://vmselect:8481 + -tls + Whether to enable TLS (aka HTTPS) for incoming requests. -tlsCertFile and -tlsKeyFile must be set if -tls is set + -tlsCertFile string + Path to file with TLS certificate. Used only if -tls is set. Prefer ECDSA certs instead of RSA certs, since RSA certs are slow + -tlsKeyFile string + Path to file with TLS key. Used only if -tls is set + -version + Show VictoriaMetrics version + -write.url string + write access url address, example: http://vminsert:8480 + +``` + +### TroubleShooting + +* Access control: + * incorrect `jwt` format, try https://jwt.io/#debugger-io with our tokens + * expired token, check `exp` field. +* Rate Limiting: + * `scrape_interval` at datasource, reduce it to apply limits faster. + + +### Limitations + +* Access Control: + * `jwt` token must be validated by external system, currently `vmauth` can't validate the signature. +* RateLimiting: + * limits applied based on queries to `datasource.url` \ No newline at end of file diff --git a/app/vmgateway/vmgateway-access-control.jpg b/app/vmgateway/vmgateway-access-control.jpg new file mode 100644 index 0000000000000000000000000000000000000000..24380bf4286f277306dac74e569e8f64d9531386 GIT binary patch literal 39434 zcmeFZ1yo!?x-Po$;1JwN(BSS)un^omxHK9ZLK;XQ!7V_7yGww^Ew}}DcbDK0q`94$ zGjk?6XYQGI-(7dz_tt4vb@$#~yLSD%e*ONxc$j%u0dQW(D#!wG@Bjb@`vV^40cqe7 zGBOG>(jycU6janlXc+jI80hF2B#-g1@hM2DC@DzE$*F1C7^t7J(2$ce@;_tY;N;=q zp<)me72pzKE|-C za`FlFi_5F)pEtkg zf&&o#N*3(>uY~KJm_Ldh@zkBkQb{ehZt$#`qFrHQ4v2Mhra(z}i$*yAVls^c$ zr}*^jZudiHLrLq&;i|zTMa1HF+;~UrJSyG}z$_dgxzkE2&j{m?VRK)y=Z;1}vx+V|@t=rubKc}}-IXHB0xe8-X zgx1f^V{en>_ji4dtPI_F04l5Frn^b&9smz?&bt*Juq6l~Kb$uZJQw&qY79<}8Oy@` zlMcguavA&LiNnta;O*4R1Hc%>KKn*K-Lhr(8GOLA+FSed34a?^=&7T6;}hzI5NXG1!bWHmmNjp;Br-8xUd zv^oN!ef%yprVLKB?}B0C)NW8el&-f!j3jhFRY!f|;;B!`6&;QE<&bCr7{~5DI4aK; zRI4(6=zwz15u_m=T@2CEO%B3WlR)|>ji8)opF)1JqYPy9&baoyd=lCI;@h*}}b zuFHgujLLdT$p^qbnezcyudbINME`=$9_sd^-o>&+N<4V=?I8dCrGKW_Jkh=m%bkHK zYo|xjdyQS4Sc*c-y`abZS@NiIVr^9iOp7wE4oI7g^NW(?$MPH}{h9F}GEv^CuZ5~x z4H9%gYQ?ZjAYHVzLh!CGPBcWmywE#)Z$_a{g5NfM97K~U8-mqIzL|bI9noI;y9z` ziKk;5{g&#c&lHrGEfvU4i%xAwL$1rH31c4sQ<42rNSjLHWzhSMDp7uG(E2=&t`}G} zYOSgFvNGZ^YTo2(O5w~jo@@+0N}lAI0o5TIFgelk#3okXZt9XWyTj9xe95w{B0;xw ziA&n?TN`zt$2P4wg^Ysrx@LNhr!i%$AR>pJoIoAl7*nEf*yBzI&ADvEV`W(d;xa#o zgTx!=lhmZ$D=JW9(}XeObGbK}ISx5k`832A`TL~W*{*9!R_(&V)zI1Fxmkn75*E`) zEj&vLmv+C^=`tIH!to~e47luCIm*wnZkUl4a4v)ukUM-5r!XN3<(A%F>S8;-I;s)la z5lyvJs4ocS?xZW~Cf;yTG*?3svF|gj2FY!75A;SVz8wIxx2o{iAED${2~SwB=Ca#v z87|+fd1}N`n9tUg!TqcVSSBIxFd?kNDvD0Em7r`(gNMtqvWGwO8X~9mIHjg=KhYLrzEYeBjg1E_-=5Z=#2OL3+d{w3o zbCcS2oam+TM+fj^Xq5pvJz`{y-aV$~o-iJeB8C3x+K+(`@^J&rtYlVv?O)eP1TpGp z1P3j=kS-FHbZ#YoSXJO<@mc(Ng5FS*fxp3WpXSr@s@EdT=V>Tztn1RsrvvV z)>W}q`6eL148XTAF7AB$-dLK!pQ|H}qykPScODq`%j z5&*OjTUY`&lUUJ~l=NS@wBXe&s@!T&B_12~%=z>1E=n!G9CdVb)cLLyM2I`^@~}NC zax(<*vjZbEDy(2*30GJ9xW}8U(KK zzA^g&7}4%|0O0ZN>^9;$@R7agBAy_)#C%3VnC9(!0NRU>`R~#B#IIoANq5=Ki^&e3 zoi`TSY*u)6Dj{HhD%cT!0Yu$;$~^!XrBP5shU@XQDW1Mf-OLFG+L%a!$L_TE0Phg^ zw{fcS?}U#d>Ig*f-w4Vz<4V56|0YEGNwtUV4bC{PI?rae9YbIFA z^%AB|mps%Ga9@yhMh_%%CiKll%^Ig1xZAN!81zPi<=cx+f5|~P^zPA4$A=|r2-^QU7*K#jf zgKs({A+-%bwQ@|)`qR}0ZT8bhN8c!7aDs%~_$%7Gvh`#=;@Y12(?J(UVIs?H{s07^ zLU~1NGA(_+*VHDQRfc~J)w1_<{!?N~|0*#Fwr+oy7#e2^xD50w(!6xrXQ5lrDH5Ph zfnG~IhL`9c$t*cE`PM=-T|RfKHR|Aa^hN561qvyO?C&TgTdzJ;;u&48q%=4zxw#eKNe^F** zf3Ca}z5plT4zc>YE*5*O(W7;3b)i7imC0L!M$o=xeN|oP8p_bDsq@~9fB;QoR#RBV zF0-8&rP8Su#Q=aQ$W&n;&-!JPhB-HLe)(o8s=>p7e|EU6h`p|p<2&TJJCCi?GG2)5^0ucGpIDoKn9V`s3(5AcOW8{7?H9U$n3_#T1e_Ns?zao; z1fzE_dmC#~3a%%DM!WzH8S{F(%&k;WfoRlk;t}32jm0NcCwM#^zzP%kXR5(td+ptW za{`D_wam__h=nxe#x4?+`IwzV$dVYNy=JcD$;1@lLWMojz3WT4`6WbdC$Z&%6>E0(#m0sL*&-Ft^DN%WP**DV^N!S)38S&vv zD*!-SvHuMpZuhmJBu0e{%VCz#b1W_l(pn`79XIDHfQ@!Ie=}KZg11AI?@&{l|6+Mj z@e}P^l0n^~D?UQccSPFJ%A$1P{AaoSV&-akM$*raRHU%=^|!ELUSwURO^q7o-pKQ( z3@3(a55S|El-9kxiPg&wn)Q-DQ8|A>P;Tcs+^}CRmoN0Es;KT;s$l9|FZ=fKvET6H zeP)=a5Y&3k7{SbWMw6FSUw+{C(r=j%1Uqkkh<)ZidLLDo-n=$w5v9U50%l~aLeI|4 zs!u;q9KX6#DydzE|Al8!9Yi7H&wYUsHWl=$Fd%w;&Kb4$LNf7tu;{i%Aw;g*tfaSP=Y=8 zZ=e>kMC0qA!VVV)#_=B_T=vj0)q}?*GwY)K7{OhD>UHayk%2-51Tu28G_}g>UL#Xr`W4|T4^%*Ma_6MtM-k+`*G>~_A*oH)Z78CfE03!jrCmb!J()% zCD7_nYr$TF(e&kF`>bw{!)8Ks4`NKhE1}ht{s-Wtc3DOCK<@PeU^r2pal(=ylqE@v zXY=}{2gK#TBIc>!Fc2s!C^CYa7$WwB7&45xyrX@U$YDpCB*~u$pQwfS;e9b~jg%}W zbl|l>Dfn8&KDl$u2FF&vx$EGuR3b9jt3F3s8B1%21@^&r%bSiI+=g%FM32%}?g^pT z*n2i+C!}_Xgl|DcM&4i!wvIyA=ojJZ;`)1ctf1E|yeC0-HLh()2Q^Lz10+f-1_c7c znZ05kc;JxZ)|G??+HPcKlQk#lTVY^}xs6s?8Hv#;-#Z8ra4HYL#c9iRDpBh$4!D2{ z=8g3iWE|*ISr88C6~L241+)|kEAJw)dAoOnv`aH>mQbgx$Jq0XSe84yZSiS`Ne7?I zQ%4nck46i2SZFWR`}wXJ5vlr?BQGxO47dW34A@Bf1%0s=%ESc19+9ZqI}$<>*j*eT zooPPo{5?|liw2e`2R|HH#!ml!tuOMuH1G75I3~*Nd6!Y8LCz;~o<-goMdvXhwE|+Z z@Rdy%Nf2Y&#z@pAN%r8bjQRU!GL~YO)}wAi6h=J3;Ww`OZ@DWP8>jE6SgRpbat=j5 z<9KXzWm_~N1|#3mVvHcLwb1TPU6H1!+LBKGxHKFl*q@s#W)YTEe5`9>E`pJUO~G_= z6>&bG%hLl1KGR6n{4flusrV=oMF_{{Nr0A3lRy&}I&i#|?CoT}9-lO3GxtNgAr=^o zwv@&&)$^02_LpmRy}?Y&UkwpqE*Z>OT6}u#%`@L<;B{qaz+vg!%^fHys1f-YA8u;) z&GV6?65g=Bpv6!6f-+4+&IcAABwzp9rk1PqP8i=5oL(k4w0C+ZmlN^9I4i?sMT(}GT_tS|%VyIL?Nf19)xPiY=60n5IbSFmp(jw?dKE+xBYI(brH8g*Bc)bFO^Qhx#p?3g2h?m zotm1mDUU&(n(^mYTG=Mai2SNTkszUc=_%ev?7YEaWtq_N6V`|c4+lz%$VZ{iVr59? zW>{R< zVv{ORyjB$0NYj}Qz)Ck2l&R`Sd?RxYHa^VR9j$eL3yBae{?hN^LG)+GsB7grss}*Y z_`?J6ju?Dh^y1kqJEyP86Y#3=G58+ShYE@TGZ1Sq3;g~^8!RN{-2q>7!a9fMWJOtS zQI7YT{5gX7?^hNwX3u(mfSmpdw`ROFHa!#x`VY6t{&Uu`p`wYO|NlS9k#XKd8iU8F z%vu$#xaQw)S=$w>1iL-r!dIZORYiHQ+#StSEzH&qP~DSxzEG{KS4&-lrQ8@x2L4&z zS_H-)55Qe0QTio8BaAhIo8jwKK0v8jj;B8yu~cKT55?YiPY*bBfLi3%L-%$NK7ME; zT}t;ZRLHq*_ht6opLNoQGsOrnmW~UyPk+(M{=Tf5c5Hpxy^vSH-kSfh?9}e#g}Mho zr-_|dIm0|I&_@Txy)Lis#L+WTnTEBK=E(eHcrk^qUOWKimJaP4s6V60S17-9+0I?7 zGq%=3$31BufO+4yBU(}foy@rNQL6QqgKj*kD5z8&^V1chC60HBEHz2Iop`fd6j`#^ zpJYeru?zwt`7VY~%c8pU;k~ju_#0RfF&%4PWb_viml5+zpO=O*B7Az+A#~f=Rdn6r zdy8ro`>eF31H)i4x_ntJTGq8E*P5YYU}_u^A++CF*=L=jf;i|Uy;PWr)^fkkfuw~XrkzUqW`ur#g0`ON5B%}yr@oUUxoCt3{av>D(``$&XvYIzWJgl+;dy15|RWM!VnBl7D#T z|GBrG70PUm^N&9M|LlA2F~eQlFeZ40Y5D<>VWnda5Z_4i0zuofTVde zDtZ5%-HL?LN*!`~$P+o21YG@D?epqs_MU3f?AgFy5Ddd`HOt%wV7LsX^#Ylfq13OSCCCiwV^E%^9PFr~uETdl(XD~bN!xD+~gg?l;`Z(fZ+HX7pX^Tr)%V}Ty? zuQZ5JYWmB+Kf38OJ+FDi6S*EGR(rE%*~*&6s(9-f3y0N)!@cZxSAj7j{x%3RJFwtk z{SO;gUVMg^c&L6-R=3_F>%t5fAq!LnO8B7({Qne%-=#lzzox4`8f4j92(q={Qw|yJOC$}|0W6yZ((OL%VB|9 zTe$e07S(@u```6IbdXktf8xFo{bfI$?(wU|;!dpF=2}X{zEGBu6LBgZ=ch^K%KSD|FGPiivuX8&?-|BrAu=MM#Iu9I0Rmj5f` zBb)iJuz)t`4W%Q$f2bctFVHW?E>>wu{9rnuUb*ih{h7CkBPOM z*PvT=6>2-7&SF!iIDc4Wg}?@oS$}BIHb?(hC%OguebMq5gjJZc$>NY&{l4q3Mg^LHJ z29)0``FT5ma0ReMiu}_HdA6G&p~HuFlmf)Z!1r>K1= z&o41Bo->|DPc|s9_TRp$kS76icF9>6?aqY>%`DF?5<{;8E&W;zDVe`pRtAuDjo1mb zq*^+$rIRl8cDF>s@?l_9Pu&vrQN=IpP^EwM6f>E^86P`MHfeF_8#=TB)<)#U z3|k`5H^nK-^C)6tFA%#KImr=Ku*PqG1^G^T*dtp)xwy~R->x;I^dz}=m#!FD^^fY6 z(bm%8`cxS5jUKr~btm~C=zpoW4(qx~@|rxjJs|2XnGjf(VgzQX|}FNk?)?L7$=wJWt=Es z*jjsB=0N&AUj<s;M0 z1w=U3Qu;fSS#d{oH@{wnie^0k&R{0n#gO8sFBr25qJQ~vMM>CqQTogG?^wX+4vPlA zA6jdyvZZb7!4wSg>OWB)$n3!){0^%QTn1!%xe0=77Imy_TqN_f&O+LgF5>9b%!?ZB zR~j(%)s<;ahx0>x7*5eO!VZ$h1fV8^|6et@7t?a%EM->pknvLLx-Z{j2k_C#Y4Boo z17a19KvBomcOP3QVS2{078yiDor--`v`v0EtfrUL8p#vE^Z>{Pl(Q_cD$L{EXG-OmDqtU>1KA6s zZiLoKGM3BNanfT)V`DH)T2Wp1h9J~-(jYAbPkoPcDL$9cL0YZ48VTdw&?h!c98{7$ zu5WLjh6RP4Ys*A_t>yPgZazx6<3I$`@zVkkoDSZ?Xb zg#mF^E}OadCMvp9r#q1a^Os5YYu}O|cizOm$5nY9D?{!wkebt2bQUOT>-GTP8!UP~ z6@RRpCD4_bWAM4_d7+2;7DdL9t)3Hkz|dm%VGL zY;k$R`ItgRVv`3FXsk&Doc=;UU6)~&|1;g48>?iiDGwpP((0=1H)_v|O4G0;$f3N1 zsjK&7@<|GAi%k7P3dkfY0je>((Z6nkuWMx}MmBO-jaSf?JtO0zK)=d5BBr8|Vl8%2 zD1-l~H?e(*D(wL{Jl#ohT|8Q_0$H%A6bfwIPVV0e4QN*LH3X#)sp7?vpL{U9S(!{? z{CcDoCNq{fPNo#4RHcY~JJpVKI=@%fR{VI*K!uh|$q7GJVt&n9;-y2lrNWN0{90aC z0LhPxol_MT;azPd)}e9X>V`PBF|;FrZA5tP1+=tVmCA`j{VC`edI9l9mx$>^x*=(m z450eq`B>@s)VS%m{aDvo!|W?HGV3xOVqP!UK806U{_3N@fVh{xd>;5B8}U<75cm?~ zuP~Y8AIGUG?;CtmE%DWK$%6h#O)4e$7z~v`AHGFfg!xh(#sPXaBBV;q$$8;5VfL0D zrgoa;pH&7^Jcsp1{)Mt5*p%wnk-@!Tt3Pg$4Zu6OWtYDnm)bg7YnXF)e;#u>cfx;H z&GYjvtos4Tw{%M+tz}7#S5s)7->Y=`EY74wcx=z_PZ+ zK`n;xjY66-(V|b&X;h)N?THO;Aj|BsQV+x`oC;3PI+pjb@Z8oEC>aF1MEZX5=ggRk zxy_v~UKg^Me!GExW>pEPt2OXGG)+M2o>}7P=Nt!4&ERJw+sfD`mU+_kmj~dfVfw`t2o{5m z)$gu(-6^=Qyo$I49V+lX>7lX^jMciNwD$$CxbIeWk?&UOjy>-cHj*SE4;A?w-tNRI zGSVo0NLDdTa-KIi!(iU+q;0-h33XGQm0Q~nZbjK~%P6Wa6}XinEuHuT|rt zvHfG{7Ur>EyK(TY(F`-cd(%g=@QOdW6RD<04~kMl7m)~_TNA{ZqHGr6ty=T7o$jK1 zQaiRbD|lS?R9**#Ue8Na_~H&Ul0ANq=`gBqknMEFRpIXm_RTl(N$igr;`X-Pil1+# z;uBgI`vl87cFfutW5+2Smk^$GxC_KSF~`hA)-^F*_zs82NV1sTiD#Ij6KzY%npx32 zc2W9N_=3~6*2P=rMBcKS2!*bL{O%jMPFY93?z~~2D^h%u&SSry>U7#pg$VS~wn;uq z$)o1>Y{Pn;imlL_ZktW?go3QlN~yoa>Hk$$((au1Kg&w~Ej#(IelibVfPbuqJ5w1t8IQw+6-i1r)O)5nJ{V6#~>Da``W!4xliEAVc;SV(;DhleFgj zn6?#HnBz{XjOwj;xcCn4uPnfl6I3?)kDkS8Pd8%ThGt%ZiNPZ@zc7WS<+}*E**h4* zFA?zzdN>20@{*rmDG==xQCXjSxQFKvzatxb0D33<(z~h7AB{W!$4>k(+*xfH=f8*g zw`aBY2NJJ#|NFDjD#zN@Z*HkCwDT$9;~+~0`Fg?jCwR9`LcS}olA4Rz5V!5vS<_Ah zNwJD5C&3UVrN49T-#GZ+KC#8G#bF92X!|}0R^okbS}svWv?`N3m>$Ho-cXx>ik0uS zYAKad_!bM>+-UM&+H*kA_B*WMS^dX}zu+9?FeYYN>*#1`{b+x2=s35_xN5HL+nfZaUlKRrfo)5L=?C^duRpJTCCNsQnb0i+H)w05SYQLjDR*Z-P*PE= zNmmX=iXGx{O4SP)+A?&<57r&GoOmO+uILB9uTeJvxtE<&D79#Ok{2p}Qm+W`KSL3j zM$fdHGhD)oQk{7F#I9zpCy|Cne2(n$tJR!HCDv>BlV#o|+k~XD=5^vX(uKB_wlyAn z!$_%3M|nD)G`?TerNRMV$ah+J)w6C)gaVYk?1in-+!SGwsKw1cR3LcvBhF8OttbiMn#Q=BKnRYXx)1yQhnTIS5`f$ zg%G&C!9xu%Duw2eJBs|>xpxSzk83tD>txXhA7T4t1YB!-1jf#J#dQmLbb>;x6lVxc z8lNiQsHMN^)tvModOelAE?J=X>M(Q9<5DO9e(YqDgo=hqmzhvPVkPFMmOzt1{>~K9 zm3mFF#|OjM9#8;HNzVPuH}Spe_Q88Jml+lzizTn^lvN8y2&;~l^As02M9dg8 zfA*4Wd)43R>=QW&Q>q4D_L&owBNrCqY!>o|j{WqriX6bOlPKlL$~t`-N2 zg9G(^!y!X~g0=Rej^gwiTld1netUcRn$wZkd48!@CiY^^9icdVZA#{|3q=QUwK4Yd z!m`PvkwtrEKI9jh#E15-koDZxH&;RKN_uw|U+4Mu!y1LdRENJBdaLDaVqjA|8YULV z-xU%edP_4-UweF13$I_L#M@o!`wXXM42XCcP97`IK-@)SYT+@rRBfAK8|CM{g^y7X z)7h9NXC5R&(Z`#f?^R&9H)^XN9PgWJJWQtOm?%tQrFA>6M;!4D=S7+fcXxy3QD&a* zg6(lwSgH_|Y_oO&|2s}f0fWD@GWUYE7ylCdiATtr2Bo8P^IY$-KDxa}OLA^AxSEfh zxfDd7)f~~rhe3%?!rwA6v!=xr$e4{LD)xsF4Hqnd0m@T`nd3hTI60&op~OE5m2 zzV`r}8a4_3LOIj#AE}~b{0pGPe+6p%X8FTcHb8tKhCoRq1&As{+T zQfhYwv)8cOGa7?uw12=tK+ zg6)DLA-S36_AUbXK7FvPTSgAL*VA_Mb*^8$iTpeOIr^e=N#d<{|`A9)$N3SQ-$XkAr@3982CykBeoa+T@n+19~s z%K25no%GuLtvLR|uC&&;oBUb`<>Uprm3xyJs<_yuCa~fuYO^K1Y3-v64@dpjS#%dP z%&Rk1E@Hb90v$C+jGbTLewz6O!Cw9PRfjXRe@m+O5K}}?r|l7yT4VLJ8#D1CeXIb_ z?bnyJPq0v}miQd0*MZAH^0t1T2PoL?obr<)i#tS3MvcbkvF@B8J{JcQENXf;Gc9brVuaPdHK#jvB!6h~y{0DVNbgC& z2dS7U0z{AVLnAu;5@A@h_!0)=|DJSGTv7SSkVg8;J1mF6TQ7bSh$&tl((yevSSG>3 z?q5zP{3qKA38Gw98->#eX1zi-QjGQHo11J(L@LW(D-aKXNoUHaG+UdZ)+yhaCAyZT zG+1`@IacD5!SZnBQgWq$CqXJzj@+s~@A^&Zr<3cV zAFHR&AAs4Lscg#PI_!7{Z{PRB_I<>FkD>$xG;_Tbeb-+5O0Op9n=IojYp~I`Lj-|t zEML!D74mCiCFObLD8nk>;?$NGImz!5fPw{qK13YuxES3f%9;Qu)Af>E2uV}eqf^

-w`XEEUAEO1jpKFhzLHOO5xmkJ~&l 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 literal 0 HcmV?d00001 diff --git a/app/vmgateway/vmgateway-overview.jpeg b/app/vmgateway/vmgateway-overview.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..adb30aa59cb110d2a2b8dd6ab9dc991cccec5634 GIT binary patch literal 49136 zcmeFZcR*83w=cYD(gc()Ehs2WL5g&UjV@h5K#52PX(AmGI?@FM6qO)FKq(?k1&MSN zkt#?}2#E9qAq3LC?Q`Gqob$fte&^hK?qBB}*kP0GJ$u&7npwZ~TWdo5PD2AnjP;H5 z0XjMWFa&=98V2y!3-xpZ08>*y761SifRXMHzyO}n0rcPt_g~Jz8|b9}{(h1t!1%{C z0H6#01ptO3y1!ppME}=M8Qv8!{PmnJ==X&*4C`@^d-wb`6%>2}<(*voZoA4m`}rt@ zI{7Oo%AZvLw4tH?PR`!0_r!0zx_kQSoY-nXoe=kQ(K%sz&h)IQ|0P!sPs8xLuGZmZ zSDnMXoi$ueKy}5nLp4Kv{C!;SIf;k*-0=<64AnXDN9CH}`R}(CPKf_;$vtnK6W2{G z#4q{Xbrn~YKP!Ls1bFvd7dK6-%ld!07yM7>#9ta25)vXGqAc%s*IhwTLqkL1tdfF~ z(i!lIGl5~g_nbn{_y$V+wT8>CfzEe5{qK4D`HKIp(do8d&^?_KCltUZDE#FSzfb?x zR$cLbm4EfXzk1+bJ@Bs{_*W17s|Wt)^}yfJj;k*SaY8^W1JFJKM_K5C7~V0^odD>M z(lH#Rqjdog5P>n#{ZW47sNaQ-o`I3+5Hkxa8#{PG;}L+Kj)8%mk%5Vc5kzNnQQ+qQ z<54E=HBySVgY`RB?ie*4$X zE@6+jfACu_5IFuXvB1}VD%Vj^E_y~r21e%Ja?#O;{1*HuBhzukL);fEnVkZ7PAJ8& z@LtS%Uf0elp=^cYyM1?h;s&H^$o6Axzd0>8n8CR@Uue5z$)BX1V1|SYoxI1%kzh^TmkdD zMXb^<2PHq7UsTQrr2!(Nk$cqBbVJ$UkltfO?Dwu3Nf@@|Ga{0(m$Ve5nx)c)7#prR zsl;Rb-mMhX$kq_aqCG@I78GS*17o zW1*e+B36%z$CF_?@3h@Ycnr-{Nrgqus|Kf{PY+wh9X|%`2On^SyY9gYP5vq7?iz3+$yL=3xt!zc)h1?(roVMlmdE?L4>TwZAvw#r_ugI>U3x9cYH~dH$aj z+4@-_@+>rft0yto(cIVk7}%DV9iFT`^MPZBqYM|viu&};w|{psI8gL9O1!;hTxK*N z!-Km{BwC}#Kmz((US$QU0S$00xS7Oocs5`(1;|U$(>S)aQ&vQuBK@B@WXgj>d4W}# z#|ZOZ`@{J=GywXHE?~!6iHcnQRidy$MQGAvf4*>~KnIu2j`gYl4VHQU@Bhz%ppE_C zt1U5tK!r4)rUAf6WMdjBAdz3s&dCqU{Q=mP=*;gNDXdpZ0dtPX>GlTCH7yC@+;nRU$^% zP)gd=Bef>Td>m8HoXtjB&z*d>G|)Ewxl`HjqHaL!=ZJ|3NKFNMSrF*gj*7%j z0aer8b~!`iOc zd+(ktk2SC#8eMPu-1R87W9fd7h~igH3*HrFS9Mpp=kw5(MMBs;q z0%E*{cDI||PpO1(ANles+Po3I)o|gycy4d`Q0AH$OUdgZu8)$GXTMc|eyNmFtjqV* z$xK_qpHn#}b1ppGb}he)25?e`!;$NlC=4pT?f6IoDtVwxkE5nO!fn(OzbY=BZPPSU z=3ff_DIh!rhPmMNOgHmOG{)!qY4EuqW|E%Uy%dS0flQ zaYd&GKVGK+?wLgO@8ayF%aO8xKXf4rS+qs1N7+-ZQnDizNY_KPFo%VtHp&;Od3qex zOdiignVf&^quak;Wpw|v{PiTuP7yR#z4qm`+m_K#WBtpwSxyG5>_{B^h^~jSv|8fW zshr#&@Tz4I=)oxlOhRGSxtT3jrrrdpyzAAIpVCFU;+195JR8?NNucVs_b~Mvfi00! z?|2%jj}~#4g0$}J)Xu+_5KUS#dUJ+E%FqnfsbE)LMkG^3DaE_c$qrH{f;I9|nJ0qh ziE)bXO*g{8i&5;@c}vM^pLf@LzKA5ua7h%(ZJ+($X$lE<;=cL=o~+T8m4ng0D*@AE z;yau$I-e}S(@6u4Me4kQ56_F}imp-F5;x51$~_&0vcGQsnD=&ddR%%OqTcSRv$mEw zZSau>G}mU3j~Cm++jTW?6P*-$tsMEvfCZg1yXs}luAfT{pZJpwvds)WM2$J8ZMI8< z+<^Q_vHFv0fHLAZ5bj8#`dEeB@3Fb@E0cIx3SEfW9vyG{PDySP0=vsRl4D`OYniZ1 ze1d_9dRp5Tdj?a5hGAE-<#XfOWQkrmu;NR5n!$?AG^YPTenzQ* z9|3E))#{AT#@bQRbmfV{?QJX(*SY055S`mzYO!c(?)ocfMU@h=TJB=sen}$gd&weq zjxhb6&wx4vZ#Xyv4FOPSjNCkrmu=f&N|COh z-3rgmuLO%5p;lq8DYH)>4bK=}QmUcT{YHA#j1KJL_n`uZ{yiZf)5P2%#dcG6c4_~L{8}MQ5h6W zB_tDVuE8xFI+PbDBOEBfB_%#{%AwD|Jl`ap~CwI|VrD^GKF?YmM`V_@+Jx=7oFR1{GiN{GP)&6Neo z=eXiJI|8TeTEC9t`X%3N-b;IyGBA_ysfcU#tzF^C5JvuE?W@YuxJu8Lzzxo^)&Iaf ze;&pnd6;ujw}In?muy|0MeovJ(eB>ha_>1bQW)wrKUq(>(;T}@h{Mx>q*mT$B!2ze z2Yq&dw-Q9B3{So#-*#skNqpL<(T9YOKevK#t$TOGMq3@VyN*8MPn z)V>d}n!wa_8Qu7IWi3#%{-qR7maVvy`(Hnsp?Nu#dI5^YxqbK3P@FZFK#M4B0Tvx8 z#eR6-9vbhbP=nQtUD&cwRNTt(Dk)5=?Dw18B0QNWb;qMNWU#$bke^eg`APS_3ulC7 z8fIe*D6Ki0njHK>cLpY6XDH6U9@4eOgl+iouN~xV?X`WYFc!wY zyl$lPDesgH*M!FpyCdpb)Cjl7gm@*vZ6)a7I7<8pmGwD^5d>hzp$XyqC56QYJi%OJ zSH5yB$bwD>Be>ee`n{coe)I#bAZPMBo0Oy|+e5h&*|xGD=HvO!gdfB??D z(Abv&X%1p^Is85dPx28sQ)WRLkQhRAX-kMaStbLy-^9H8qL$3VOlAGXy)cT-jP)_! zt8Oqc2_`Qjvy`klI_;Kyr^KaT{X5ZW<%m%gjKpSpucP8v<|yAj7bs=~;Z(h80W^AJ z=gU3KpxN1E*z@KBB`Di#)N8JfT9>DbR@-!j1NIPa+Qf;)?Y309Loc%m!#ZVn3puJE zjmzqoPe=w!EDP#R(f~TMIm6ExQ5oexP*^KInIsC3{nC@nCs5%`R3Pp+HdCbH$T65AjUZH7$#oJ z^M5`dv~U0SE)N+1fqrH*s2l>g*ura6*-&xKoKCI{CM*s3215lPUNP=1|Ef&{)u;$o zXOIZ>^NX`LZ?6^bRtx87%3bh*bY;!h^}K26FZn8E`2DQ7L{_5u;0w7;LpysumnB2S zC~10Q>EdX%KOHir_l8@TZnkTjyx<#(j_K2m8^Xh$s5$VE%D9X+})-jWC1Mvt$9n0T-N(0C` z4g?b#faFSs61`hJX~1V=C}ws34U9~TG@=2WH?ihaE87dLBpNVpN>%x5U69ED>aBlO z@khV@t1teW^};gz2DI@cxxAVB^|uq;K0j4rFupqy#Bd&#G{i|eGI2l^e^!3Jd}4y)94B7V^)`F#=yX7X;!iDWFd1y*VW=X zvC=hth6a3jjLEz)u`228#q>#i_eX1ZmSs-L&+vPTOMFseQM{HG*DdvU0b4ZlMJk zynsx5^QjcxKF0<<;`F^H5>`2IDjx#~`V;oxSDG~+XbTRcx8`kg?T4AoU0WD9jzSmF z0D(wr=B~=f!8kGcZ=2dW&X}1W(_K^JPv@>}Y?dCnk{4|2Q0-Io=>F8gF}H_Cq%46s zPd}eK+Np+Cb1H5p#9!V28cvrc$C>0d=UtN_@Wk#;;8;ywYTyFtH};ArwxPTzFR6-a z`FSJc?Vn<-p4yO7abN$@X)SN=BkB?)^M2=?rI!yxQ zvAew7YSI3XExM0V-=cf4o0Dbo>j{E+vlQ&xfK|?`Bo~q3Zq=0P!<*fQr!R6FSN1W| zfSgRf{*;>=*;h9Pix>{9J)Tb&-K;ZWTm*CyPV!m-#VOK%J?Prs_Asd|^-P>hx)sLo zQ@S|8_jv$j((a}aQz#=hst*n4{UYkCa<9BV&x6vV zGt)CNMz?brdQTL-T)n`l*dr*Wz5)}P1HC*b!fjT{@<|tjsR3MaawO{c8VL9S`gyTx-T6I0tHqgrdFG06O!VKie*OGijn$#YhD@upTc6lZ~ofuI`S zk5F|&gH?g{;AG4^PrTr#j0N?c+z@%?`vsyJnhY{`a&{5xdn6cHf>Pq!$aOgMVnnq< zx4o?XL33i`EBDdHYIUQxa)X|Vhn=!=^JFke)1MmFb3dxT87@)X*1f|w7%4Wnsgud~ zYq@uRvbh}zxwoN9FiFkiB=(*UbH#0?G^9URozm-n$?fyw`=tKSm;kR;!rob=yx}an z%tCd7LucU2!sAgTJMhWH>?7OQfUg&KM0UtfuA&v|iEbKD$F;$Mg=Rw8g4m%c|?&GaZdF*dX+}zSnv3%(^fz*#BT68`Lt6|kEcMB?y zvH-c);|YHP9uV62zXu(!Oyqiu7}R@#yMd#~+Hjy=FVY9+P9RYpqCCtq+n|*Ewj-f$ zbR2q->@TiInwC-u76cmH-SpTc>q?czop7cwb=BA0e4-rJcl1`xBGy`s$_G8UDcy(kD(Lup-KiibGdQwN68{2A zHa#!)OfR&ymn#!>7<8T?32<@ZH2#T|kU~Q188?Mphf&^Uh+Fe)^|}$oscrm zw8+GZm4Zdh#)PIy5B3Woyn*`PH72E*?&{(s#jqlQE9Zq9t}(UTri+vcY=KDcwM$Ib zTG}x5Kjpje>j9TKxq?1i?jzTR0xp_5Vg>KRt_nmKKD`$O<4i_srI`LL_jPALfyHSOH3T&yzHam1`mbE8f z{+M8;V_5FhTn(MN!7Jb#=$BeMFCbmD`{8uCxEvtNoThN&DLQ8CF0(}0IfchhO8~a? zuSS4E-PQw*uVMpBgqU<9Xd)K+fO<17qNMAr+f@A=yXYCfo`Y zHgB#yZzRE>qw%TIxzIMMO5L#(u9M6dtOL~q-su_eDC@K1X7QsstHytGS;-Ed@3R&e zLhK}I!utvZ@$>CmM`nrMwr#{@jLwOOSE_8=*XJGYFUKz)Z5%`I)lRV_D<(=w{@R~F z3y?)3Z%{j^+~V#KKJB9oko?o<7#uVbowq`oa?UyOAs9d z5b6B4Z(AB3yY3op6vM6WR-jh>DF$;Mk2R6h3myhHlGK0J{ z4Zz))&ww-l{aq~X!)2x|Y zU9$D6`jbc}qQ>HpyDXJ|vmt-ACBVz=8gr>ybFVr}^-s%9(jOWb_EDgwz^NnlnVo(j zgP0GEX$%(jQ82=rRpo2Cq#>4er82zl=Oh-gvP2u=%yoK8I-LF4pM8BaokfitCMuLk{6dI9&0<@!WG+@Cd$Ghfd_-}SUCE(`uxvn>z zvXEnIXA-S6I%VG!R;(Uldp!_TU>ghd216I0n~3ZSf^f-D`^md)$M9t{sTKAocKGcV zO1B?q=~Xz~@YV6f3SQegZfyM2bq2rsEmP}e=1A39(o6_h%M!-8g2GuU#LS6$fN`=Z z{od^`ESBB1VWi+9BF-+wAzh|3=&C@X73X2ENj;KxCshhJk$TSo!9z5+Z2YFu z(m2ntRNCPkCZPV=@JVi>!JTj`)Oni-;fGcg^11QJnJcX3(ag(635k{lQgnw*qJ}>W z4n+qZ*bbtEeiKJ1LP$2dc^Xw^R7GwLl{aBlU@?_GDmFgu`ToZeiKTiWTYoL4fWCJ~ z{-T2TVp`1nDzm@VV#+omeO?qU1pf--^lg(S-$0Q#a~OILTlS=%`?fKYbYyy}li$(% z@s0RI=~N@R*Lt6r40^+pHe5;ik+&g}us0K(p7-Qf-+|`;)xwb+qsD6@VObulw$|@@ zShj2UyYDLh;A>Zf3sXNIz*Di`b=AoJ6I)ct$V<%`h8vC5*KFoQRb_f6B}Uery7<{o zIEfiH-{==)KhyP67Hj%x@~CW9Mir=IjDh2Dh0r4E2waM|QLa}Knqxz3B<$b&{>(*H zbppRkZdF`{Z3?p^8qT^s0G&2B3>q zaJ^13`)rg;jyA2v^CJ1qkA*2wyvvH;3ckH)~uuS(ZHca z7ws6>+c_z4!da5J@kt*CGw11cKLpxpx^K`eRFa)u-n=}_{!Gm}_fCgSD$DD(%Xt?4 zM+1)A2}r(Pc%KQH*>)u=d#DV`U7CERa#hu4zW!>6Oi@R*F#BH1>nL421@qke5F?k| zG)Eh^+h(>)MO@3uov7x)jeL?VSpna>;f)pz#I_-^o{c_%R6c4Png%e?0IyI^VN>~4 zhX@msN!?!MdEvb1)KBCcj{^OpFYgm3rcqpxXYg9sS~7nLI;0l~@pe2*jH(GuB1G!S zebYYk#N)(h4Ug3bb+n{kw7cFc?}sP*&CAz~UG%2#6*4fl&=M-k(rGy?G!e!~)gUfo zCOTlrLg|FETB4khsqS^YUDf$0F=SML=eao~({x@~NyZiKfo6}}T*Gr<7&SI9RDX>S zM>HkrksfNMC;Q+^*ZcVmE)twN2Rn*#d#1L~o7~1nltMShlcj<*LoZKGt%mnBFFca>&HEAY zdFCo8x?l=aU!gOI>j-9Ve{a?7)wCfFef6O9 zi&%>wo9!zO#Sw{@Pf2W7%&>k^AcEu`Bj%g713p_}%7EZUe6_$8*QWV?XuVafX6|QR zlfZbv*JroBjQ?2uN}URA8)bfc;?mt{z=T z>nkt4-uv}vA=6dg8zi6O&^%*pGvc5>agXTi>T!=^GgIoYRmAbeE?baWk8N7X?#b6P zAm5cS$3}F*_%X~;Khpr-NIM+1w~)=$^%UMk&hzE$T5p$iYPH^Ogm=HEcuUyk`Ns~o z!_0syvK0_dc+Y%YQX<0T412dN>w>96Pn#T`+M<@MZMvZ2-DmRQl0|_^JVbU%`c|U! zLW(*zLw!uPLS%^f2c%1jL=m8}?_qVkK3nDUBj0?Uk*uqgQ4y19ah)REf9Hv$ld;-Y#z(lHO-) z#mLQ^`xGE^yT#Tk^XEKhzFyOSXzdFm=T#ce^l+-FMC`+>NRT&6xI>V2R<&HA0X})e z8?qbRVN{ndrTMZJbj{kx6XQ_yBM9@ZIZ?_z5rKY!oIZFTYTl6*8ZK;VA^5}!K{Ti> z8WUF_)k^0_+_O{dQL6er<$s>Jmnz&uDMz56BO8-?A8BiW%L{~sd=6*cWf`j6@!mE8 zQwLMCOzD_gUA7!cn62(~ytqA6WjURgp~&XG)r8*m*$5LeKLmvK+AmSMbXQ4@ugs<_tYGw!>Z>yuH0@Vs`~Ua2VhC z;d|ny4Bz<0)|_Y$hEMRr)S-Et^|1!OOcWE!-BfBJB;`yM`EcQJqW1zM6)r3!U*fYM zA=e&I*4Tx!cbJbA(t2W(t3PMi&vfW9=@5KqDk&N2`Ah<>c3)S6d<%z6Az#CMaMbbs z2o-FF^3~J_L@=_gRS@Bn(+dsh)kb*+DbU7CCyqou5D24)4?)p6Z9I6j1YOx)l2xQJ z-n|>jPj(zWj{8x)z~0+MYx}GD{%oPshh)aeQ6d=lU>wG~ zJ;qP8L&a!-=T^F|46!!?_jZm3bR2W9viEE{;`wI2onR+luBw{1?RbOow82|)y*-4V z9kmQV;t+N6wfIf~m`xOWhs-q5CQ8=Bi)>+0Q@8s&CJ4>hQ$~#TE^VjB)@yIy*Q3|0 z-cH{Wo-?0wE!my`;2DuZL*U9jPn5|_ZPSRnIohTjT8lZE3BpQ<_Z&S=H63GE#oaSR z4Crp24w7tXEHGJ-uL%t*o7B*9WI)D@= zyW{+fh}EG7AI+NU} zQ!t#RFb(+L-|^m{OKU6PHQ5Q5)Wv{p$>g|nXY|HQsQv2r=+`>Z4eMTper+oyWA4j_ z?yfg`UE+57sjNsWGDDMVLyu%k1)Y1r$gr1WlhHP=H8=#pxduO;F;BX&LOn5pZS#;n z*_6n#w7qn?V$SMW$tV!Tiu*-nEg|VdiW8G@20bvA?6r%dR61hdbzE;!!=qzDb0$76 z=bO22C_m3LYIb(T}`aWET$R{Xea0U3NrdU~2&@1w7&O_`k(-&Dzr$i}fuOC}gFCY4=W5xV+z9An>&6%8D zSdQJ$Rv?*v_bf<^;jeTJ@#e2R8_Q7X&?h!lDgQ3Z<=BN8!nvK(=kJe+;1ma8JtzQy z#u9tUXXoQ)at6E28ReNpM9(wywJ(Z@h1+f|y-!HE7AlCo2ZY0U>CXL$uJ}y`9`?gg zgNR`y9>gaFd=e!WngTm`ympP0IFBK(;eHWLfsq0jmWw2d;6?2!kG~r3hu)`fL6uN% zY__QGpOH<_9vU!x2s)jL@mo(K$%oeK;ckacbiV)57*^pmrCrb>FyaW*8SsYiPyXV! z-a}Sw%x5)Ho+Esrliq{@q5(;ZI;Ja!s|B-PB3<%r>gwMP?^HJ2bTEBC>NC*yq))B+ z)0z61r8;5bmb>%EnLnfa6hO;~@!J6L7?Tf?30JVERdF)M;qBcsA9h) zY_O^4n-|WZu|M`Bzd$Z3I%?ragh=9VkO0OpV)K7y6I-qbO>_>XO%AupU^(Dnjb{ux z4u0-C7)HEGe4IBoClKG?vEJOutlmTYG z3wfWAr&*Zlf4b2iv1fkl9YRNWKgk*Y%I-UKWaG{<&JWAAVe~4L z1`uvgo<|;+>Ut)C;ja#f!0McAVwz=tYhSC>F z{dQP$8&{+~4${drl~0huH+P-!;!rcfXSN^3%*tQod`xqB~8xZ`I|# zM3-m4n;W*nYMD6&wQU7MrA#W6_*P+jI3k8BNlbV0shRX*Fllk-Yp-i+gL6+sEZv{H zpapp-na{8+Th912YKqmR_exQ_2sVK9eUV{aBs%odD&jt9J9Wi}+IT>4s}*TY5{8eV z#CBoqp)Xj2s=2!6*?l5qUciW{S6jz?U-W*DuS?z%+8f}x5T32n^_8ojg%0w4@8dk@ zsxBw-3`T*)n=9(%W_q#bcz$NZOG=%ipyiF(bvHfaU9~5TlOgWjXWKP9s%A%(a;4?& zA|(;+r=gz6WLOhqT?UPsl3UoDL|~3jERS2Qtp_w%Fx7pN-#Ecmwb^;};^Tg?yHT%7 z#-QF@EMx^@H$KHLO-y&hx&y|s6D4fbGJl$|H@3=+IRJqrmHM%VV5r-ZvFMOqi%Sey5K{Wc{yRCl;)Za`4i%J6!_ zT?wG1wDFvoe|IvCx|AdR^IBt|(*gUIri2 zt1ouf&pCIh7B*L#kIa38Hn~bNERPJH;aiX4zn{tExh+Y}baf^@HBwMX_lD;RO zoOnJ~Ag>q)wFl{>*y7MjU9cu(_u%(6)3K@stwE;HAm4@oV+XD2qN&QJ4+Z<1s$E%C z{KNysBx69(UOB_biCIh{?-F`ZtB zNgMmFX1Iyk{I*29znz2nZD1g5a9yEf<$eB!4vA}Sb-e%F0CIh9&!+aoF;Q37k%@bD zY$59*ON!6F+--kz)!V#i)gsPd1KEk3&LkKUt8r4-C@)%hu(Ptu9}5`F#j0QROE`3w zfAZyR4Kb1}C2U8oy^$$4f=I9_JBHxmsBE4jPI4H2E*Dpe@yo2<(l)J0rgpx#F*$dA z*7$hBvJy*8QF80EtiJx^bS@INx%JI4?i{*3QJ{MAxVX%67G!UbXiZ@ zhhjDx*Rh1 z4s9w=AP146LbnJU4a4$2yP5PvKNGz6)Dy$RxzI)_#g``%W<>ojaT|&Uq>u_x$ic~j zv!FFKpOUVWMPO_~&*RsYjn;O0N1~w!pU;o3zEDNB#AP)%LD>o(3%wNALvsQvG3Ni6 z*IlAxK6W+t*ebmEVUyE0ND`_}jSNQA1rd|hzdh8-!bTYE*4b(C!$q3_;oLUYI*1`9 zQ9TgCRjLeP)xFww;@j@q;13TKzVAOZ{$O;FHE`HgQCrRUq#a*YT-v71sxUMRF?pwQ zlOr;&irS8NvoziaO(6?lRw7wzZmK<2El0PIP={|0NbS9dlJ|BBSeN3K7I6mHNNg#- ztRE~D^X0z~SGi)5jpZ@S`M477*RlxuSm%0tTe8!Fum@@5&==4Aa;azAHw?qpQobcE zB$0XD+fyp5!CcU-3~@*})_i;`QcI0YKkj`-UrLIEx?{m*#d-cS;A!O$)w zK`oG3KJBx4RBz@Bb4bGFpL!*R3zCnwcMwg3-PKHa3dxZAy^K3CYk{RVeuj6y+q;%@ z)gcdJ5;chxYX%>fN{D#vERf!iDH`fJx|MG5k$QZ4l(JGXIt3_xjqK1_Te-Qe=7Vmw zp8vIGsxB>|A*(6lCLTp6$u_uJx9Ppx6!}X4OvR?MZk7o+55#?VA;T-C$SaPI z&PKhTDEELgGp}1NjJ(3Y(!^NC(vbJ;LsL4?$ReZU8Z^3VsQF8JyUoclDo56V?Xxu$ z3!H~oCIsanIvb7UQnzRAF3BR>A7qZ?bUI*oy5<8XRR+^wEW0n*OG3+yTF(q!3ggMy zSFGuozByc5x?NR+&PBb?PV(yugt4w7>de=@t12^Xh(_p^T;JlG^zH35psna!?+l_H z^fS>&HP5A+WL9FB5)BA8XJ1K!O89!|)+$DLAYUj$>U&(D$iCW3OL4X&zHYC%z12hI zc%8Z-@V@b(A_8s_Z`tc;M2`aN!AyZMYKw)-L^|B;v2b2o&7*k4ylO}D`tL`8UT#o) zTU2H{l&({1FQ>C0yili$KJ(QV--d9=EpJGJ!8!%=J-zonm@L1>G68bKWeq0#^)Uw% zscZ8%OBhjVE1~o(F%^sKBpLkVHs`+1DtN%8OpicoREa_e=9Iiv9cd)!iEu>9-Nu$U z{2UKBO9NsgFQDuRqFfzq0=SNK+}sn-ThNtw>7-u_=S(=~yV#H9^`plnYI?Q& zvxbPaq42aco}GFpV!&H5L*KyN0dcG#nzJ+J;9AtR5XkuC_9Chgx)HUYG7Iw71O(FT z%pL-xj6au{{dQN%*z``O>#fpEv$#{BSzr>zcVaE@Qk`3LrKs)xE5n;)y-;TJ*}2&p zE4V|VsgrMMfJ@53{O#oc2nA7Fi!F#yy!ZXP#@ff_RZU3mIg8G2tt@R5_Z|UM-CQj; zs-JW|@;aC`n4U`m#_d3t5k#-&+~(ICgm~?}jUQosI=`^&9{Pa5Ft3l2bjUz#D4dNb zcNy0|O9Ohp5ctn_2lz+1KT>$2xR?8AqUmL^{P%BHIX}~9_T936<69vj8ZADmx(Vq9>8V%rwmyZn-C4T;9U(^3B`}(I%f3nrxy${OX z0D}E$7r=yA@rZ9PV>T8-l+NzopU!G$7&Y%tj23q?c%@(Cw2zjVs4LPUmMQ zq(;7WadM_u6eO7Ni{sxR?HQU8wm&cF*QU=bJXIHKpxXzNkU3iL0F6#ShA4j#HSNKukq6DgWbe2u+UdB9YKPUuf@DO32%SV zXMp%1_F@%rNPT6UTVcB8#^q^&kFb;rVJl_VIZ=B@YKJ7a^Ja_&#}@K$?U4tGL(I!7 zl$M}{5JX1;u?z`Yf5k<;ZUvccu}rYtn5U=8d`#cF?lJy`5}LR>y;C~`JrhL(l&LC( z_JpnRXebngX>Iozz`+)t>m1TnKQFROjbK=r82!b(vBX`|j=aILOY_Ny1g449-OX31-ubs)L^0=A@hoXT=U9>-(Zt?2lWWWN|nt4 zRfxrBYuCbi=WwU6KUU&(uW?8oYSDsud7iAz-uW~Rm%mWST!#xAq*@|$OW^h>P+ znG!eKDjJC}jdw#`&~BswCt`%+vXFxZ;BCWT^+}Oh(@V3FYQ&24r*FWa;;4d-{>Yeo zNaa|Mk})Uq8em?@j}wFNS2IaRc2QKl+M$~z^S??w_Q*8gV25s>3Pl&sbM%r&Yg3TK z3*HEF>Jk#1L1!Gd5~qpeC!*+uko}!P|qQ^U18SB`;91Sb6$x z7=uV4?ArP3U240Z_OQ_9mGeBmGoJo@fTRJ}GKjVR5f;7mp+df*xS+TF_D@nlWAO&e zQ=|dbI$j?{>fpNvBF9oGBfGk-xe|rPnL3hB?AHq~WyM7c@<|DbAI>21E-LB2ylTa3eR=zn@n0(Vt%XECmwh~ZUPD3>ogH5A zxH#o};q6$<=D4BFJ`F5GjrnuQoPsD`l}9zC+`_NlYH}=&>NhJcyL?WOw3-i1>}7iU zY1NSu`GvT}!u7fykl(eSc=)@Q&a* z^qFz(QvY&~>ZhUCl8Peur^_ORnsB$rdN+$j)^8UXwmvW$DN>VKRtM@4VJYdQPsybj zkLca9=7lGp2){jT*bgYDVbcP7m`5J#@n8Jm;>~-}Qp2)Eqi^3_V^A(G=j~E`3-hsF zUX}+EdX2$tCcsNIe!<)RbQ;I66F$dTSsM41oqAt*jATd_8b^4T$A{;4nlf}BY{l%F zBv;o|HEyu26o04(Swamg{O+5}_9waZKfZb{8;Xu>!Q~K65^J6CTLzRD2++Arc|$hG zL*lFa8c-a1ekBG)E8#E#GMCVTJf)gj_Wtp4!^wU|ma`R_6F*-8;Th9m%b8{#?1CCs zuc`N3^?@b}O5q1CzNigWv)qB4x{_uW%xxqA1Z~BvjXyTMc2(H6OtWebjHv!?_k6%C zSBW~5C?!SCg-pd!KbPfGpBD<&j!BXl_ai`FXA$J8z{nwD@+!Ctj45~W9mFGp+ssgS z_thvJNUUyLMR7{s(C-T6e%WFM*RL%g=s~y70xm_JQ(kKdu{Y0Xl_(DN5|y#c`!RL% z% zl3~3-PRN(HmNCtl);$2!79;O0=FxeV>kbwL=km&XaPOb&1pUu=3$}gGf1E5Z+i?pj zuMXFfE(ACHxgM`gx%!`H+5hGo`yQh2kNBsCej7y0|2zRA@)=@baO@x)${59LsmGU9 z^rr{V(f%8@z>`6n8kftJp`@Ycy7@N- zJO-@x#5dnv&*Rw(5FWc;h1@gK z#jl$)rAx%0=1j)a3R&MR9M@2M=xW2JxEaN+7bZnkatHk!YN~CgA6fMPG%Bi~**Jx0 zH$?qxj8p~fieDuS_~qnSOUM=5+SDFgU!60k9(FXM0f!%hu=E5CDE@|<2F*(w7z`Xp zfZ#qMaaIZkXJ8yC-1I7f{N0jLX3#>4BWX5gt=+TD{%4gN{If=QT5eL&)UVZx;j?^; z$bYQ%KWf0#)d8$a5{N_+FmOEZ38+T4A4Z*jeKio$P-Dg|sR6DMM9jIAH$4HSM;Zxk#a<{sAL zWBMZ<5&@mB-=F-w84W&6mlzT$hl>H+)qgD>Tgg~P?biN0)OHnzT2IG0x1$=HQxSJ& zh1lPc{jj1!otMT+y<^9>+{?L#)>EM}pE|{oj`iF%WFEuwOS9vm-e1ji{3fL3u@LFX zE7N-=L$>=L_P+D|CS6J^2Ca8_%j!8R$U6T%9qr(t?ZQ8K?@b(K>`x16z6O})^Wrbj z8B2eVp340QT8>-)WUIaUX43Fe2$?5`YMi+N;$2s(ei~pgc-VH8J8Fvj;Xm-Qq+rDQ zUERXWcF2V>OFb8lKUa*P{WpBI26-HOatVA|SBer#1w7^jBjt+iLu0Wvc;5s}d2>rv z$=cS1ERh6BchX?c)?tj2JmSg z5jDaj5n@5FX|;C#E$Ulr#IJqu>k$6k*ALicPidGa_PGi6?TG5WU5+bkAYw99SYDph ziGJCvX65j%hizkURx&LJ+{Bla&=WqfsFnovtDSaUa9zH?3yX&`YjcHRHiWyyByiuV zzt&>=*Vy~xFatJgMtPoR(711l0j#bKA(sWXosQiNd?3%eq~`G|N25n}I~eNJq)=3H z`+$vH2=3YeeTCR8#4s<>8{D8LVNC;m=w}kWVMG}L%2D$~6!GpkDAmQ=@1+gZ8d^0% zb%DQLhh5O`-r?KV0hd(prau-~8QcWuq3VMR?3Z*1ZXES{fjxobK;Ra=5ahn$UE}%B z5W6yRAI0f=TWosjPr2;~s?Y?LO^*g#w#L*_Mbg0#%3~oYrW%1Rh3)9+g4=d6ec(RS z{j8GJT+~C|dXxREgU-4rCSidg+r>TdIq(YvQmOaLJ7HuEA(%O?wJ;O)Who?NqB>Gb(T~ zV<1QJ?cYM!BSWHsK<=(f(~Ykv$!5&<>xld}f5G}2$73(TJZ#M97Ae=Hyw2|E`o0G# zbYunF)tJGr0-FRkx|SV_UO_3Xh-C?8(Tu3xaPlbS6;(0P0uz9l*NLZy26Om%Y=ug6EQ7+V?**OqpAox!L>Uq|hud$t;hNV2@Q@xDedM_X1P9p+ zr(5~_=lizIYEMP^lIJh#gXj_$?kTGr5`GjvJk`(pCR0~$MimENAZCIKnHVtwt#E`G zg87Dwj`UeT+}AxZ0&RYQ3r=`(Nxru1d;T_gdP{Xd*pR_kNaYYZEUi>u5N|%MPY^1R z*g2II<}b{~%3k<(b~v+0WHCtG_6OoV0$qiCW1a|g^H!L8a_x!zO*Yt3g?NSap2xDQ z1#T^akKIFDmQH1O#Af&LHM<7$b7bYj63>Uwr zg(q%bC}^q5JS-^LCD#R5;D~>9d8y8Psteu`l{prxNZ#KmJ^a%A@ka)YugZmI!8N)J zZt)X|vmJ~Me-rB0olTVLE%58y;$IDZZ7L!lF)jm4cF{)Yt$K0YL<%MFpivlNJOLib#cQDqL|h zcswia%!pT>((Y*A&pC&3eZjZ+92>)O;1K9w;~}jNEgz@itbX*jS5RbG+bUwj&k+$( zI4COx(>ey_%3NdcV6@E&LY`k8D;JhfxiK|;#I{G0B&XUqQ&@eCm(Nx+W6yM6iN^Qb zqH-9BRx8EAYWkmP8$ZB$M$9T^On#mJc|&%}dk zbl=yp5mIbn^`oc@Ns)!LZ9L04LxMeP)|dy(KH!WV?6w#Bxhk5?#U-e4xjV?)mu1*c z+w`2m$77n4Jzi$u{+b9_ZVMYJ@mb4Bin<{sV!k&iDBrEl%T+Vvv*ksdvY{jQUdVwu zo+#HH-k2*hcUimq+$I|!;aq?5<`3c5=^kI*IN-KV%upOlG!X3zDifRE(nB-p_kUQmKe`=6sDAtwbCfD4k zjq{C?RBe#=5+N}i>OMX13LS^zkm#yK&@8OC?i2kB?p0mRFl>ImV{jh2&e z)2lfvec{ci>X63ffmp=g_mRklCFZ;_tpse#BfwCWT0UT=W=R3qu zjwIz4cIGk=jEzh$bWyWvoT)L39B~->@sPh?E=4H1yZ{Lf?4TtvRQ~v@N`%t1Zj$|ff%0DBDi~ske|#^I;tw?T3v^k!3r5ot2Uf|8 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 Y8~*D{>A#-Azn;Or*1+Fi1HTvk2Nf&-?*IS* literal 0 HcmV?d00001 diff --git a/app/vmgateway/vmgateway.png b/app/vmgateway/vmgateway.png new file mode 100644 index 0000000000000000000000000000000000000000..79b8579b7226d72c9a7719a6dd3613b3ae325efa GIT binary patch literal 49100 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/values are routed to http://vmselect:8481/select/42/prometheus. - # For example, http://vmauth:8427/api/v1/query is routed to http://vmselect:8480/select/42/prometheus/api/v1/query - # - Requests to http://vmauth:8427/api/v1/write are routed to http://vminsert:8480/insert/42/prometheus/api/v1/write + # and http://vmauth:8427/api/v1/label//values are proxied to http://vmselect:8481/select/42/prometheus. + # For example, http://vmauth:8427/api/v1/query is proxied to http://vmselect:8480/select/42/prometheus/api/v1/query + # - Requests to http://vmauth:8427/api/v1/write are proxied to http://vminsert:8480/insert/42/prometheus/api/v1/write - username: "foobar" url_map: - src_paths: ["/api/v1/query", "/api/v1/query_range", "/api/v1/label/[^/]+/values"] diff --git a/docs/vmgateway-access-control.jpg b/docs/vmgateway-access-control.jpg new file mode 100644 index 0000000000000000000000000000000000000000..24380bf4286f277306dac74e569e8f64d9531386 GIT binary patch literal 39434 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

-w`XEEUAEO1jpKFhzLHOO5xmkJ~&l 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 literal 0 HcmV?d00001 diff --git a/docs/vmgateway-overview.jpeg b/docs/vmgateway-overview.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..adb30aa59cb110d2a2b8dd6ab9dc991cccec5634 GIT binary patch literal 49136 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%0DBDi~ske|#^I;tw?T3v^k!3r5ot2Uf|8 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 Y8~*D{>A#-Azn;Or*1+Fi1HTvk2Nf&-?*IS* literal 0 HcmV?d00001 diff --git a/docs/vmgateway.md b/docs/vmgateway.md new file mode 100644 index 000000000..fbcb6905d --- /dev/null +++ b/docs/vmgateway.md @@ -0,0 +1,281 @@ +## Victori Metrics Gateway + + +vmgateway + +The service is a proxy for Victoria Metrics TSDB. It provides the next features: + +* Rate Limiter + * Based on cluster tenants' utilization supports multiple time interval limits for ingestion/retrieving metrics +* Token Access Control + * Supports additional per-label access control for Single and Cluster versions of Victoria Metrics TSDB + * Provides access by tenantID at Cluster version + * Allows to separate write/read/admin access to data + + +### Access Control + +vmgateway-ac + +`vmgateway` supports jwt based authentication. With jwt payload can be configured access to specific tenant, labels, read/write. + +jwt token must be in following format: +```json +{ + "exp": 1617304574, + "vm_access": { + "tenant_id": { + "account_id": 1, + "project_id": 5 + }, + "extra_labels": { + "team": "dev", + "project": "mobile" + }, + "mode": 1 + } +} +``` +Where: +- `exp` - required, expire time in unix_timestamp. If token expires, `vmgateway` rejects request. +- `vm_access` - required, dict with claim info, minimum form: `{"vm_access": {"tenand_id": {}}` +- `tenant_id` - optional, make sense only for cluster mode, routes request to corresponding tenant. +- `extra_labels` - optional, key-value pairs for label filters - added to ingested or selected metrics. +- `mode` - optional, access mode for api - read, write, full. supported values: 0 - full (default value), 1 - read, 2 - write. + +#### QuickStart + +Start single version of Victoria Metrics + +```bash +# single +# start node +./bin/victoria-metrics --selfScrapeInterval=10s +``` + +Start vmgateway + +``` +./bin/vmgateway -eula -enable.auth -read.url http://localhost:8428 --write.url http://localhost:8428 +``` + +Retieve data frof database +``` +curl 'http://localhost:8431/api/v1/series/count' -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ2bV9hY2Nlc3MiOnsidGVuYW50X2lkIjp7fSwicm9sZSI6MX0sImV4cCI6MTkzOTM0NjIxMH0.5WUxEfdcV9hKo4CtQdtuZYOGpGXWwaqM9VuVivMMrVg' + +TODO: need to have queries to show the limits +``` + +Expected result +``` +TODO: must be provided +``` + + +### Rate Limiter + +vmgateway-rl + +TODO: no information about source for rate limiting + +Limits incoming requests by given pre-configured limits. It supports read and write limiting with `minute` and `hour` interval. + +List of supported limit types: +- `queries` - count of api requests made at tenant to read api, such as `/api/v1/query`, `/api/v1/series` and others. +- `active_series` - count of current active series at given tenant. +- `new_series` - count of created series aka churn rate +- `rows_inserted` - count of inserted rows per tenant. + +List of supported time windows: +- `minute` +- `hour` + +Limits can be specified per tenant or at global level, if you omit `project_id` and `account_id`. + +Example of configuration file: + +```yaml +limits: + - type: queries + value: 1000 + resolution: minute + - type: queries + value: 10000 + resolution: hour + - type: queries + value: 10 + resolution: minute + project_id: 5 + account_id: 1 +``` + +#### QuickStart + +ClusterMode +```bash +# start datasource for cluster metrics + +cat << EOF > cluster.yaml +scrape_configs: + - job_name: cluster + scrape_interval: 5s + static_configs: + - targets: ['127.0.0.1:8481','127.0.0.1:8482','127.0.0.1:8480'] +EOF + +./bin/victoria-metrics --promscrape.config cluster.yaml + +# start cluster + +# start vmstorage, vmselect and vminsert +./bin/vmstorage -eula +./bin/vmselect -eula -storageNode 127.0.0.1:8401 +./bin/vminsert -eula -storageNode 127.0.0.1:8400 + +# create base rate limitng config: +cat << EOF > limit.yaml +limits: + - type: queries + value: 100 + - type: rows_inserted + value: 100000 + - type: new_series + value: 1000 + - type: active_series + value: 100000 + - type: queries + value: 1 + account_id: 15 +EOF + +# start gateway with clusterMoe +./bin/vmgateway -eula -enable.rateLimit -ratelimit.config limit.yaml -datasource.url http://localhost:8428 -enable.auth -clusterMode -write.url=http://localhost:8480 --read.url=http://localhost:8481 + +# ingest simple metric to tenant 1:5 +curl 'http://localhost:8431/api/v1/import/prometheus' -X POST -d 'foo{bar="baz1"} 123' -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2MjAxNjIwMDAwMDAsInZtX2FjY2VzcyI6eyJ0ZW5hbnRfaWQiOnsiYWNjb3VudF9pZCI6MTV9fX0.PB1_KXDKPUp-40pxOGk6lt_jt9Yq80PIMpWVJqSForQ' +# read metric from tenant 1:5 +curl 'http://localhost:8431/api/v1/labels' -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2MjAxNjIwMDAwMDAsInZtX2FjY2VzcyI6eyJ0ZW5hbnRfaWQiOnsiYWNjb3VudF9pZCI6MTV9fX0.PB1_KXDKPUp-40pxOGk6lt_jt9Yq80PIMpWVJqSForQ' +``` + +### Configuration + +The shortlist of configuration flags is the following: +```bash + -clusterMode + enable it for cluster version + -datasource.appendTypePrefix + Whether to add type prefix to -datasource.url based on the query type. Set to true if sending different query types to VMSelect URL. + -datasource.basicAuth.password string + Optional basic auth password for -datasource.url + -datasource.basicAuth.username string + Optional basic auth username for -datasource.url + -datasource.lookback duration + Lookback defines how far to look into past when evaluating queries. For example, if datasource.lookback=5m then param "time" with value now()-5m will be added to every query. + -datasource.maxIdleConnections int + Defines the number of idle (keep-alive connections) to configured datasource.Consider to set this value equal to the value: groups_total * group.concurrency. Too low value may result into high number of sockets in TIME_WAIT state. (default 100) + -datasource.queryStep duration + queryStep defines how far a value can fallback to when evaluating queries. For example, if datasource.queryStep=15s then param "step" with value "15s" will be added to every query. + -datasource.tlsCAFile string + Optional path to TLS CA file to use for verifying connections to -datasource.url. By default system CA is used + -datasource.tlsCertFile string + Optional path to client-side TLS certificate file to use when connecting to -datasource.url + -datasource.tlsInsecureSkipVerify + Whether to skip tls verification when connecting to -datasource.url + -datasource.tlsKeyFile string + Optional path to client-side TLS certificate key to use when connecting to -datasource.url + -datasource.tlsServerName string + Optional TLS server name to use for connections to -datasource.url. By default the server name from -datasource.url is used + -datasource.url string + Victoria Metrics or VMSelect url. Required parameter. E.g. http://127.0.0.1:8428 + -enable.auth + enables auth with jwt token + -enable.rateLimit + enables rate limiter + -enableTCP6 + Whether to enable IPv6 for listening and dialing. By default only IPv4 TCP and UDP is used + -envflag.enable + Whether to enable reading flags from environment variables additionally to command line. Command line flag values have priority over values from environment vars. Flags are read only from command line if this flag isnt set + -envflag.prefix string + Prefix for environment variables if -envflag.enable is set + -eula + By specifying this flag you confirm that you have an enterprise license and accept the EULA https://victoriametrics.com/assets/VM_EULA.pdf + -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() + -http.connTimeout duration + Incoming http connections are closed after the configured timeout. This may help spreading incoming load among a cluster of services behind load balancer. Note that the real timeout may be bigger by up to 10% as a protection from Thundering herd problem (default 2m0s) + -http.disableResponseCompression + Disable compression of HTTP responses for saving CPU resources. By default compression is enabled to save network bandwidth + -http.idleConnTimeout duration + Timeout for incoming idle http connections (default 1m0s) + -http.maxGracefulShutdownDuration duration + The maximum duration for graceful shutdown of HTTP server. Highly loaded server may require increased value for graceful shutdown (default 7s) + -http.pathPrefix string + An optional prefix to add to all the paths handled by http server. For example, if '-http.pathPrefix=/foo/bar' is set, then all the http requests will be handled on '/foo/bar/*' paths. This may be useful for proxied requests. See https://www.robustperception.io/using-external-urls-and-proxies-with-prometheus + -http.shutdownDelay duration + Optional delay before http server shutdown. During this dealy the servier returns non-OK responses from /health page, so load balancers can route new requests to other servers + -httpAuth.password string + Password for HTTP Basic Auth. The authentication is disabled if -httpAuth.username is empty + -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") + -loggerDisableTimestamps + Whether to disable writing timestamps in logs + -loggerErrorsPerSecondLimit int + Per-second limit on the number of ERROR messages. If more than the given number of errors are emitted per second, then the remaining errors are suppressed. Zero value disables the rate limit + -loggerFormat string + Format for logs. Possible values: default, json (default "default") + -loggerLevel string + Minimum level of errors to log. Possible values: INFO, WARN, ERROR, FATAL, PANIC (default "INFO") + -loggerOutput string + Output for the logs. Supported values: stderr, stdout (default "stderr") + -loggerTimezone string + 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 value disables the rate limit + -memory.allowedBytes size + Allowed size of system memory VictoriaMetrics caches may occupy. This option overrides -memory.allowedPercent if set to non-zero value. Too low value may increase cache miss rate, which usually results in higher CPU and disk IO usage. Too high value may evict too much data from OS page cache, which will result in higher disk IO usage + Supports the following optional suffixes for size values: KB, MB, GB, KiB, MiB, GiB (default 0) + -memory.allowedPercent float + Allowed percent of system memory VictoriaMetrics caches may occupy. See also -memory.allowedBytes. Too low value may increase cache miss rate, which usually results in higher CPU and disk IO usage. Too high value may evict too much data from OS page cache, which will result in higher disk IO usage (default 60) + -metricsAuthKey string + Auth key for /metrics. It overrides httpAuth settings + -pprofAuthKey string + Auth key for /debug/pprof. It overrides httpAuth settings + -ratelimit.config string + path for configuration file + -ratelimit.extraLabels array + additional labels, that will be applied to fetchdata from datasource + Supports array of values separated by comma or specified via multiple flags. + -ratelimit.refreshInterval duration + (default 5s) + -read.url string + read access url address, example: http://vmselect:8481 + -tls + Whether to enable TLS (aka HTTPS) for incoming requests. -tlsCertFile and -tlsKeyFile must be set if -tls is set + -tlsCertFile string + Path to file with TLS certificate. Used only if -tls is set. Prefer ECDSA certs instead of RSA certs, since RSA certs are slow + -tlsKeyFile string + Path to file with TLS key. Used only if -tls is set + -version + Show VictoriaMetrics version + -write.url string + write access url address, example: http://vminsert:8480 + +``` + +### TroubleShooting + +* Access control: + * incorrect `jwt` format, try https://jwt.io/#debugger-io with our tokens + * expired token, check `exp` field. +* Rate Limiting: + * `scrape_interval` at datasource, reduce it to apply limits faster. + + +### Limitations + +* Access Control: + * `jwt` token must be validated by external system, currently `vmauth` can't validate the signature. +* RateLimiting: + * limits applied based on queries to `datasource.url` \ No newline at end of file From 25e19c75c7922b4d20165d8daac98e308c77fd38 Mon Sep 17 00:00:00 2001 From: Aliaksandr Valialkin Date: Fri, 2 Apr 2021 23:14:47 +0300 Subject: [PATCH 25/63] docs/{vmauth,vmgateway}.md: small fixes --- app/vmauth/README.md | 3 ++- app/vmgateway/README.md | 8 +++++--- docs/vmauth.md | 3 ++- docs/vmgateway.md | 8 +++++--- 4 files changed, 14 insertions(+), 8 deletions(-) diff --git a/app/vmauth/README.md b/app/vmauth/README.md index 41953e32c..ce4ba1b7a 100644 --- a/app/vmauth/README.md +++ b/app/vmauth/README.md @@ -23,7 +23,8 @@ Docker images for `vmauth` are available [here](https://hub.docker.com/r/victori Pass `-help` to `vmauth` in order to see all the supported command-line flags with their descriptions. -Feel free [contacting us](mailto:info@victoriametrics.com) if you need customized auth proxy for VictoriaMetrics with the support of LDAP, SSO, RBAC, SAML, accounting, limits, etc. +Feel free [contacting us](mailto:info@victoriametrics.com) if you need customized auth proxy for VictoriaMetrics with the support of LDAP, SSO, RBAC, SAML, +accounting and rate limiting such as [vmgateway](https://victoriametrics.github.io/vmgateway.html). ## Auth config diff --git a/app/vmgateway/README.md b/app/vmgateway/README.md index fbcb6905d..46a7465b3 100644 --- a/app/vmgateway/README.md +++ b/app/vmgateway/README.md @@ -1,9 +1,9 @@ -## Victori Metrics Gateway +## vmgateway vmgateway -The service is a proxy for Victoria Metrics TSDB. It provides the next features: +`vmgateway` is a proxy for Victoria Metrics TSDB. It provides the following features: * Rate Limiter * Based on cluster tenants' utilization supports multiple time interval limits for ingestion/retrieving metrics @@ -12,6 +12,8 @@ The service is a proxy for Victoria Metrics TSDB. It provides the next features: * Provides access by tenantID at Cluster version * Allows to separate write/read/admin access to data +`vmgateway` is included in an [enterprise package](https://victoriametrics.com/enterprise.html). + ### Access Control @@ -278,4 +280,4 @@ The shortlist of configuration flags is the following: * Access Control: * `jwt` token must be validated by external system, currently `vmauth` can't validate the signature. * RateLimiting: - * limits applied based on queries to `datasource.url` \ No newline at end of file + * limits applied based on queries to `datasource.url` diff --git a/docs/vmauth.md b/docs/vmauth.md index 41953e32c..ce4ba1b7a 100644 --- a/docs/vmauth.md +++ b/docs/vmauth.md @@ -23,7 +23,8 @@ Docker images for `vmauth` are available [here](https://hub.docker.com/r/victori Pass `-help` to `vmauth` in order to see all the supported command-line flags with their descriptions. -Feel free [contacting us](mailto:info@victoriametrics.com) if you need customized auth proxy for VictoriaMetrics with the support of LDAP, SSO, RBAC, SAML, accounting, limits, etc. +Feel free [contacting us](mailto:info@victoriametrics.com) if you need customized auth proxy for VictoriaMetrics with the support of LDAP, SSO, RBAC, SAML, +accounting and rate limiting such as [vmgateway](https://victoriametrics.github.io/vmgateway.html). ## Auth config diff --git a/docs/vmgateway.md b/docs/vmgateway.md index fbcb6905d..46a7465b3 100644 --- a/docs/vmgateway.md +++ b/docs/vmgateway.md @@ -1,9 +1,9 @@ -## Victori Metrics Gateway +## vmgateway vmgateway -The service is a proxy for Victoria Metrics TSDB. It provides the next features: +`vmgateway` is a proxy for Victoria Metrics TSDB. It provides the following features: * Rate Limiter * Based on cluster tenants' utilization supports multiple time interval limits for ingestion/retrieving metrics @@ -12,6 +12,8 @@ The service is a proxy for Victoria Metrics TSDB. It provides the next features: * Provides access by tenantID at Cluster version * Allows to separate write/read/admin access to data +`vmgateway` is included in an [enterprise package](https://victoriametrics.com/enterprise.html). + ### Access Control @@ -278,4 +280,4 @@ The shortlist of configuration flags is the following: * Access Control: * `jwt` token must be validated by external system, currently `vmauth` can't validate the signature. * RateLimiting: - * limits applied based on queries to `datasource.url` \ No newline at end of file + * limits applied based on queries to `datasource.url` From 3055ab0115ee21f9b14f450ca6f255cf0adfef94 Mon Sep 17 00:00:00 2001 From: Aliaksandr Valialkin Date: Fri, 2 Apr 2021 23:55:54 +0300 Subject: [PATCH 26/63] app/vmselect/promql: add ability to set label value additionally to label name for the remaining sum of time series returned from `topk_*` and `bottomk_*` functions in the form: `topk_min(N, m, "label=value")` --- app/vmselect/promql/aggr.go | 8 +++++++- app/vmselect/promql/exec_test.go | 4 ++-- docs/MetricsQL.md | 2 +- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/app/vmselect/promql/aggr.go b/app/vmselect/promql/aggr.go index 5d6a71bf9..46ebc4323 100644 --- a/app/vmselect/promql/aggr.go +++ b/app/vmselect/promql/aggr.go @@ -693,8 +693,14 @@ func getRemainingSumTimeseries(tss []*timeseries, modifier *metricsql.ModifierEx var dst timeseries dst.CopyFromShallowTimestamps(tss[0]) removeGroupTags(&dst.MetricName, modifier) + tagValue := remainingSumTagName + n := strings.IndexByte(remainingSumTagName, '=') + if n >= 0 { + tagValue = remainingSumTagName[n+1:] + remainingSumTagName = remainingSumTagName[:n] + } dst.MetricName.RemoveTag(remainingSumTagName) - dst.MetricName.AddTag(remainingSumTagName, remainingSumTagName) + dst.MetricName.AddTag(remainingSumTagName, tagValue) for i, k := range ks { kn := getIntK(k, len(tss)) var sum float64 diff --git a/app/vmselect/promql/exec_test.go b/app/vmselect/promql/exec_test.go index db3389416..d7a5c1959 100644 --- a/app/vmselect/promql/exec_test.go +++ b/app/vmselect/promql/exec_test.go @@ -4813,7 +4813,7 @@ func TestExecSuccess(t *testing.T) { }) t.Run(`topk_max(1, remaining_sum)`, func(t *testing.T) { t.Parallel() - q := `sort_desc(topk_max(1, label_set(10, "foo", "bar") or label_set(time()/150, "baz", "sss"), "remaining_sum"))` + q := `sort_desc(topk_max(1, label_set(10, "foo", "bar") or label_set(time()/150, "baz", "sss"), "remaining_sum=foo"))` r1 := netstorage.Result{ MetricName: metricNameExpected, Values: []float64{nan, nan, nan, 10.666666666666666, 12, 13.333333333333334}, @@ -4831,7 +4831,7 @@ func TestExecSuccess(t *testing.T) { r2.MetricName.Tags = []storage.Tag{ { Key: []byte("remaining_sum"), - Value: []byte("remaining_sum"), + Value: []byte("foo"), }, } resultExpected := []netstorage.Result{r1, r2} diff --git a/docs/MetricsQL.md b/docs/MetricsQL.md index b67cb4d3c..53be6f107 100644 --- a/docs/MetricsQL.md +++ b/docs/MetricsQL.md @@ -121,7 +121,7 @@ This functionality can be tried at [an editable Grafana dashboard](http://play-g - `bottomk_avg(k, q)` - returns bottom K time series with the min averages on the given time range - `bottomk_median(k, q)` - returns bottom K time series with the min medians on the given time range. - All the `topk_*` and `bottomk_*` functions accept optional third argument - label name for the sum of the remaining time series outside top K or bottom K time series. For example, `topk_max(3, process_resident_memory_bytes, "remaining_sum")` would return up to 3 time series with the maximum value for `process_resident_memory_bytes` plus fourth time series with the sum of the remaining time series if any. The fourth time series will contain `remaining_sum="remaining_sum"` additional label. + All the `topk_*` and `bottomk_*` functions accept optional third argument - label to add to the sum of the remaining time series outside top K or bottom K time series. For example, `topk_max(3, sum(process_resident_memory_bytes) by (job), "job=other")` would return up to 3 time series with the maximum value for `sum(process_resident_memory_bytes) by (job)` plus fourth time series with the sum of the remaining time series if any. The fourth time series will contain `job="other"` label. - `share_le_over_time(m[d], le)` - returns share (in the range 0..1) of values in `m` over `d`, which are smaller or equal to `le`. Useful for calculating SLI and SLO. Example: `share_le_over_time(memory_usage_bytes[24h], 100*1024*1024)` returns the share of time series values for the last 24 hours when memory usage was below or equal to 100MB. - `share_gt_over_time(m[d], gt)` - returns share (in the range 0..1) of values in `m` over `d`, which are bigger than `gt`. Useful for calculating SLI and SLO. From 22949911e9f4892c5f7bd3ceb8a210ce83a48481 Mon Sep 17 00:00:00 2001 From: Aliaksandr Valialkin Date: Sat, 3 Apr 2021 00:23:43 +0300 Subject: [PATCH 27/63] docs/CHANGELOG.md: typo fix --- docs/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 9030fef97..3b6d1503e 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -8,7 +8,7 @@ * FEATURE: vmagent: add support for `authorization` section in `-promscrape.config` in the same way as [Prometheus 2.26 does](https://github.com/prometheus/prometheus/pull/8512). * FEATURE: vmagent: reduce memory usage when `-remoteWrite.queues` is set to a big value. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/1167). * FEATURE: vmagent: add AWS IAM roles for tasks support for EC2 service discovery according to [these docs](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-iam-roles.html). -FEATURE: vmauth: add support for authorization via [bearer token](https://swagger.io/docs/specification/authentication/bearer-authentication/). See [the docs](https://victoriametrics.github.io/vmauth.html#auth-config) for details. +* FEATURE: vmauth: add support for authorization via [bearer token](https://swagger.io/docs/specification/authentication/bearer-authentication/). See [the docs](https://victoriametrics.github.io/vmauth.html#auth-config) for details. * BUGFIX: vmagent: properly discovery targets if multiple namespace selectors are put inside `kubernetes_sd_config`. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/1170). * BUGFIX: properly generate filename for `*.tar.gz` archive inside `_checksums.txt` file posted at [releases page](https://github.com/VictoriaMetrics/VictoriaMetrics/releases). See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/1171). From b8d9d6c326e75d0d98894390ae07db7337f62b2d Mon Sep 17 00:00:00 2001 From: Aliaksandr Valialkin Date: Sat, 3 Apr 2021 00:25:33 +0300 Subject: [PATCH 28/63] docs/CHANGELOG.md: yet another typo fix --- docs/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 3b6d1503e..31b96ad99 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -10,7 +10,7 @@ * FEATURE: vmagent: add AWS IAM roles for tasks support for EC2 service discovery according to [these docs](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-iam-roles.html). * FEATURE: vmauth: add support for authorization via [bearer token](https://swagger.io/docs/specification/authentication/bearer-authentication/). See [the docs](https://victoriametrics.github.io/vmauth.html#auth-config) for details. -* BUGFIX: vmagent: properly discovery targets if multiple namespace selectors are put inside `kubernetes_sd_config`. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/1170). +* BUGFIX: vmagent: properly discover targets if multiple namespace selectors are put inside `kubernetes_sd_config`. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/1170). * BUGFIX: properly generate filename for `*.tar.gz` archive inside `_checksums.txt` file posted at [releases page](https://github.com/VictoriaMetrics/VictoriaMetrics/releases). See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/1171). From 2256b79a8952f22604c030e4c10246a29e6f9001 Mon Sep 17 00:00:00 2001 From: Aliaksandr Valialkin Date: Sat, 3 Apr 2021 00:29:19 +0300 Subject: [PATCH 29/63] docs/vmgateway.md: update docs --- app/vmgateway/README.md | 28 ++++++++++++--------- app/vmgateway/vmgateway-access-control.jpg | Bin 39434 -> 40967 bytes docs/vmgateway-access-control.jpg | Bin 39434 -> 40967 bytes docs/vmgateway.md | 28 ++++++++++++--------- 4 files changed, 32 insertions(+), 24 deletions(-) diff --git a/app/vmgateway/README.md b/app/vmgateway/README.md index 46a7465b3..e9e5ef571 100644 --- a/app/vmgateway/README.md +++ b/app/vmgateway/README.md @@ -57,20 +57,20 @@ Start single version of Victoria Metrics Start vmgateway -``` +```bash ./bin/vmgateway -eula -enable.auth -read.url http://localhost:8428 --write.url http://localhost:8428 ``` -Retieve data frof database -``` +Retrieve data from database +```bash curl 'http://localhost:8431/api/v1/series/count' -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ2bV9hY2Nlc3MiOnsidGVuYW50X2lkIjp7fSwicm9sZSI6MX0sImV4cCI6MTkzOTM0NjIxMH0.5WUxEfdcV9hKo4CtQdtuZYOGpGXWwaqM9VuVivMMrVg' - -TODO: need to have queries to show the limits ``` -Expected result -``` -TODO: must be provided + Request with incorrect token or with out token will be rejected: +```bash +curl 'http://localhost:8431/api/v1/series/count' + +curl 'http://localhost:8431/api/v1/series/count' -H 'Authorization: Bearer incorrect-token' ``` @@ -78,9 +78,10 @@ TODO: must be provided vmgateway-rl -TODO: no information about source for rate limiting + Limits incoming requests by given pre-configured limits. It supports read and write limiting by a tenant. -Limits incoming requests by given pre-configured limits. It supports read and write limiting with `minute` and `hour` interval. + `vmgateway` needs datasource for rate limits queries. It can be single-node or cluster version of `victoria-metrics`. +It must have metrics scrapped from cluster, that you want to rate limit. List of supported limit types: - `queries` - count of api requests made at tenant to read api, such as `/api/v1/query`, `/api/v1/series` and others. @@ -113,7 +114,7 @@ limits: #### QuickStart -ClusterMode + cluster version required for rate limiting. ```bash # start datasource for cluster metrics @@ -157,6 +158,8 @@ EOF curl 'http://localhost:8431/api/v1/import/prometheus' -X POST -d 'foo{bar="baz1"} 123' -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2MjAxNjIwMDAwMDAsInZtX2FjY2VzcyI6eyJ0ZW5hbnRfaWQiOnsiYWNjb3VudF9pZCI6MTV9fX0.PB1_KXDKPUp-40pxOGk6lt_jt9Yq80PIMpWVJqSForQ' # read metric from tenant 1:5 curl 'http://localhost:8431/api/v1/labels' -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2MjAxNjIwMDAwMDAsInZtX2FjY2VzcyI6eyJ0ZW5hbnRfaWQiOnsiYWNjb3VudF9pZCI6MTV9fX0.PB1_KXDKPUp-40pxOGk6lt_jt9Yq80PIMpWVJqSForQ' + +# check rate limit ``` ### Configuration @@ -278,6 +281,7 @@ The shortlist of configuration flags is the following: ### Limitations * Access Control: - * `jwt` token must be validated by external system, currently `vmauth` can't validate the signature. + * `jwt` token must be validated by external system, currently `vmgateway` can't validate the signature. * RateLimiting: * limits applied based on queries to `datasource.url` + * only cluster version can be rate-limited. diff --git a/app/vmgateway/vmgateway-access-control.jpg b/app/vmgateway/vmgateway-access-control.jpg index 24380bf4286f277306dac74e569e8f64d9531386..91988329a6d29a1e84f1affd942b77ad84b1cf64 100644 GIT binary patch literal 40967 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 b{M85k>H~lEfxr5|Uwz>JyFP&WefEC=ihSuZ literal 39434 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

-w`XEEUAEO1jpKFhzLHOO5xmkJ~&l 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 diff --git a/docs/vmgateway-access-control.jpg b/docs/vmgateway-access-control.jpg index 24380bf4286f277306dac74e569e8f64d9531386..91988329a6d29a1e84f1affd942b77ad84b1cf64 100644 GIT binary patch literal 40967 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 b{M85k>H~lEfxr5|Uwz>JyFP&WefEC=ihSuZ literal 39434 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

-w`XEEUAEO1jpKFhzLHOO5xmkJ~&l zo@{T&UB~PE;6$YKKQHH~t^?hILrWTBCXJ2gkri`laamSmI9Ni33Pdcd1@`PFq1oh1 zPmUAnoz|EggkJ~=>vt3?zz<^RP?w#Xbt0N&lhFE&F(OQ`-f3kT%^DaR>?NEUEE=Ng zrW*Ws=_Bh$7WskTwWxl(k+BgS+ZyQF3{Sxk`+I>h0`9J{5?1_uZKl`U1E4gyFN9oPlIKKi@V1KRD9jxbTx7ru^6xl z(?P)JemMM2&^mv9dAQi1w0S^C;#&e&JT2cO0&5_a^Rvq())97|Yxctn18-LAdV}ZD z{FE*`A4Z!ag`)IVs!?f<6RKB#U`u=F*rZ#Iek5JW#=5_LMap>EpBX|nq~g-0cDw`& zrctdTP7st8FB|I8PUveJZJyKR;xfxIsv^T#B2uoWq-^<%Iy%S>VM@)l8@xAYjQ0qp zxk{nu$35T@7pgXPKfZ)I5rc@**c*bfP7lq-U&$IeI3p~OgQum4#1C|`M&SOG(tqI? zg=VD{&2T4;sCmr&ne2YpKuE3r%N;5CSz?#!fS&wqy#|!_2w~|-cg3pWeSXemZ%fGu zhT!MSt-@&xCfq+T9LZnZB+VYPhp;tiWUBai<8vxd99*uNB4QTIpu~EYrTN1Q)U`BZ z^va31ORRN1@x#x48(2}4SO*k*F?>h92rJCpfo1H#P@G%rpAW#a__3?|1JJ_=>mxf2 z4NTgGVp_v$wu&>d{xZ^Y2k_1_SZTr(7|IlN{d|SWQ)ik8x~87KrR0XMiUNx$9;KL^ zlfZ^~bq;QSM!Z`J@_ztkB!|z+0z-lA2Vj-p0VuevhZSL`-`k1*cJ$YI;CiDho)Ffd z(h06f`H+z{}N`Ly_t8}JFih?)c}BEF-+i)vgma;7p7;=Wtw0k?C*W-KiW8`BR~ zRo4BtsmeOG?%2RCtP{VUq&ki?@i=j;6Gu0JavLGx!wthMk02O!nPdG=Si$~DsQ4y5A-j495D`lp`#-Mvaoa=vOcEzNYx(arOj&Gs^puYNHB!geCYLxQQ_)+uN;Tw(y?MwufEL63K ziI>`zpWa-fz0UEVs5{Tk4{nml>G3now5w6wopx2=ad1K4sR@=&Kn+B|h?VuRZuW3f z>kmBGK71p}+ZxKsYt>{shRHI+nfQg)Y;V4A_fEW2Gl8b~PQE5yuy*9oVre};T?{ZO zkm-Zfg!@eEC~0agR!c0rcIJ%RS8j8eP?O<=VWQ3%}R0>DbWC&sk{@?gqI)AuuIvF~m<@+u8$CsHP?=qv^sl+Z4*3A_5B)sdu z6i4*^Ie#J2mzeWkxyN~X!R-^UU40l+;#aWx9JHB{Cap}b-nvp&r_4D z!tXuO`~7*_{S^R<3o%GCk&RKd%?@XmYeRn2-Lot^_1(FIdRqr?ERkLd^ZM5a7$-ZD zcXsJ_tSSiLkwIAHi67N*2C6$Y#{TIZQI%~POTyUH!I2FEga{LE9`ch#*B*tW4H03` z-j)%Z3IyjBAjHP%ZHF6%4kV&5Xb`f_DY!^Whj!7@x3rP`^P6$pX&uX zleCmen4SkpNYshl9th?WP6jFtcTn#b5i$}U<*Qabj|E;M6bOhlO)43WL4PPGu`;VF z37wqOY?gM;dv9;jT8Ce)6@yxsMw+-czz%lKdVblS@msO{hutpc3*thsq)1R9Y)2_c z3HV1g6d$f}_3$w^){b_AR%25LeCk;1QcNGqkl(;#FzI-SA9|NQRY=nVaA2g2zFc*9 z_9jbtaJKtxqspe{g$mWRhRlTJ_yw!S*xV7hcXV&h>kVX*^^te5Ob+y8WKjf8GTnA3*5^Sz+kiA9wTtTSmyfT+2&Ah zwt4+mRMBQQUMHgy%<^H=FdAh~A_M)C4y>*iT?4i|l^cd_F3a699mTKzK-*6W*MIw^ zOy9_8AT>r-dFvbPgf3~PGSRJg38&dTb+NI-&Fl6m<+ukHJ+B$f`; z05;Zdi?YkJsNJLcOWyWBAF*sA=JdYR2BN{LHt?JOp|9GB%D3+llynue$71F%7ES#l z^`pB)a=cg`1evwB-%QfosEb{JbmG+y$**8BTltIR-ReLR*RVqls>{h2cbxSV>{+QJ zFBvt11N1&(<%@IPgQVM+SZ@iULdw~zy)R6#Z7aK3ss(9~wijA?UKPMgkq04SDQedw zulhkbP8^Pk^9=?q3z}$@fy@4Q{G%?R7(lmk2K5j*=2cT#nI)F9u-uXWnjtc2zR1)Zi|Zg zV;87pKYnah`AbybaUVq&sFr>she|$22$DePwYsQ(q|JqQbeT`4g)Km~FTadgr8_{~spD**o zFSit0;adc@)h$g+MH=i%?UjlSwgu)Pv4-(&+qZQ;H1Z?YYR@98vi6dmgKX&zoL+xy zm(M0~y0D)&gvXj{D`|b~C9E;cD~?b6+(pjM)G8si5wnzsOC(trOJZn5S@^0Hm95FV zq~t3MmYLoCimsq7e?g+Y%4TKR&WKx!d(kIAOZf@TSN6oPA@_iAniMNPKdj*8<+w79 z*M?+dDH&gP8wzIVNaNJuLG`_`e1hQfhQJpi{lM(!Z*(exLs)VY5zbJ8$LW* z0qOeVqg3+OU>p6KEQY|<(ADQ^!47qfu0bp2%DWzt5>bt=t4o}2 zx6c>MYfuJ0yq_(~n#d$U{2cAIG7NcG@@rvKfQfAj*BBv^^y)1LWLt){n$Q@?5F)VO zYiYk;gr-=I0UFu$5Ox#5-aCMrN(oVvN^F znT+h?2D8)V+7t~uClxnlk8tE<-#v57c=_Rl5!bXT#P4!iHD{EQxc@7n%Q^4&T_L_) zg6VLEy6V%`YC1jEPrW%`Z-t(qpoDYKBuyN9q`T%^vp*j%H^*JnpOwKVAm*FW1i~Y+ zdEjadiQb8C$Q)nYzhiAZSD*I%{dfve%UxJe4=XO}e)2CwQGbcN{?A|{##`+}S59+> z)ai~MKP!TJ#(B*`fnjJn%njW6RW0nl3neg{Y9AT^&$3e2E=MeBn(~|Hxm^6}mM<2U zxA1-35cWS5`j@>og7l&s6$q#<38P>svtp_{6qVyrG)8y<>fkwo;78e^bhm}1L2AqX zOzD4gK@h)1mxHD4U?r3Y`PW)~E5TcY!E210AuXr!nV+t9>|QYb-bFLoR>%P|z}u2; zi(hblyNIA(HFg@D;4mK6`APrj_4al1ke-v@ba{BkO5m%N{7 zo6pC}SYC@q(4YA68cAO?z-sRFGuQmgb>4O9i$&zw5o)7)JGt=0EgF#2hxhm+pYJza zWxzbC16C?%fVTKGR{E+Abs(9IHu2L)#>6byOij-n=BUv(3I!c&1B2)~6FdXgrsL4X z%YFIqr#Mu$3iyoyJ>Rilg@z3};SeLUQ!)cRM?%H-|D(OHj*GKfuN?vb0t5^02@nVl z!Ciud;O+#sfe_pY1b5fq?(Xgcw*dxs*I>bN-}F>Y@_n~G_t&0Z+uQyHGq3Hv-`Td- zde+0DiGOJ1Yo%n*T$PUs+74SdPe%qeq#6SEkBU7{hy^;s|LTeA4of^^R>$CywbX5+ zsa#~&xawH&R)oo>mv`fHnHXtibd`k%73h zX*NB!Nxdy=0w~aVb6kSU;KJ@JrcQ{wIxEXp;?9SIz+}4DDF*4RF!Ms$yR}txkQ?`6 zg);FlY!%|hPYT-q_$Hqnj4ZP=-P+FJ->C5CtxG?XY)zfmvhZ^?Qf+jiH;Sc?3GtyV zj7YH^09=it6;2&Oa~5)%Cx_`m5$oSuSW~QVhD#Eq^Qe|ljorvS9{sDvQK7fAJg9Sz znn2`OcZ|i8p50AxH}^|kb0=4g`^rO`MXLj?Ss-nC-%4quFh?Gy_Q828BdwQ!z9q8J zB9Yi*hB8VV3-wHrn_1>jjFa5?C)72Mc_WIL4+mak{o0)b zokm5t70pRa6J9JJt9Z>zZU=@-+?6z4IrehfP9}YYc)U=X+ZkV06S3v>k@xF$A{lv4 zK{@AWCEfH1I;X?0FNi-v4O{Mq2+mN`3*yYw2?)X#U-`9H6^-{(22#kiv-rWp6yRSV zM7W1?d%Xtm)#~t@pM3fXx2LaG>JnkT24R3f+{?WvY9oP91=Q&Pe)U&Pr11NjAq~L( z7z8S_e{bSicjyoyTgBrx+XWugGXAqpMhl&to3py5EJnBPJ*Hu5s+i==-=46WPfRj9 zzm+tyZaxz(?%R_nijjl&{Ob{+D5B$OGc5{+IF{5nhH(DfV@Xkv$?u(5puGCi4-nHo z4>Lbq2w_vE+?-cSXcg-xt=Z_~<8Iw%cbRHw1E)X}j5AkAT9$iI0Gnj#_$j2JM7M6*a7TFEPhQi6Is84HOjh)(q|+ zW8YEEGz&TAOCv34kp;^Jdf@~_eLho4m+`fySU;8&!A)181#wiT$j#PK6b%lNX9qgz7)rshInfJWd^v3@LQmQ{-<_C6eB7en`R0Bj@XVDqXyHvlL%q&cWTAQ)*T&?wl;OuW8pX8MT1% ziH3UV2|ipDlPsJZLb1F=F}}zvIE)B!7ma^rp#Dad2_ULwyZ<$|>aUOe4gyL3Hr@A@ ziD=rJ0R53r)d|4Af7GFanGF8RB(Qdhn2b`U3gLUD4~3$pO-+>P<=Rwg7F@(R93h*HJLA;xM4drRVzy?4JZ zlWeRH8=0_33`Wcli@Yo_@M8IZK(#{?l1euDJV>3Q5qV?ozV!`_V=>X)rcK%^ul>px zLSk3Ob?WT{##YN@O#x%^1+DWjbnS?uMzHLgGc);nuMsg-FFvOtN2quJxS25DNf-@n z)RBCtd0h92K6J{UG57Rv6X38>6kN6?zMq$<^1|3z}F#Ozl=yoC9PkBvmV# zV4S_KZhnk$x~!=Ktta5;q2!wRn*Q=Bk8P$k!xDH2c3DYrc6BK9S(j^B0=hG{DwD8` z_>ijd5z5TR+NSAQd#g1Y-V!$X*7|Q!*@ZlT4NG~oaVkgx&cC)*s-AWe34N{A8m^qO zph-w9s`oiQRH=_Up74X}F=$T9MZd=G@U`-ZLlF;jC)!+PCdoag5!o!_J7&8tJ9A`A z44F?;^bC2@ITAJOfEy`JjzRV{hi=V;6&s<^_*PXCtyl=Jx(aBam`H=hbYiGqh!{zTbx#KMLZgf7p3_n1POtR+U6TI>qe#ab| zdyXH33e~k|HTEz&XF{*VCn zw0~V$5A#*`8ou{Rt93eSx~7RRdBV@ez-=rmMliUY#5+e9POT<}-LcWHBww@7uFjRQ zbSPuaW3N`61`)yf-Hfy-$hIyOY5dr)I;wadwY=!EJ}cTBS0{@40e8hPtR8<@W8#j( zENAbjM?9`0uC)bm*03$ZrJZm$rp>@$ss*q_Ufa5UpD~&%og-;qNc?Ipi3h{s4O5h( zk+{y2ez*uU^S!bI{i*7vFZ?SFep|TS!l-yfuM|g1pK1W~1U*MPJ5;GQ)Vl0CpvPQs zur6A`A=rE!Ni{=(BTD0vMTTkteT%?NgL#9YSP`fngywRb$#{j zBcSQ`vigYj!8WzUA`7@lJ}+`smm&Q?W9;yq%jY`GAszMH&`$~nF5xjKC@6M4LXIa_ z5>2TmHN9EHUk-X*ix5kL=7%;mgBf)b!UV|(_Nmk1K-7T3cm)OuSPcHNUoa4q>ra9- zuFuPhV>931GQBPrf>v(?fVr0qe6qr=lZ;1f(x}Y5s!zAHcYC~#Ja_CK|>R$@Tq6{yGn|S1!98f}U-0-DS1PPxwa8(HBqbKM&vA8Mi(CSOvb@6_-Wp(<hZ~p^~k{@T9uCBC->@>Lmq~Vy@{KntE#y@Up3!GG&bg2^g z-QUtNqdnl$Y74u}0MyN^P9NFrA2QcSb=j_dp_S8|>mDL7t2}KVvK^QWrQ+-7Kc7!#OICm|Zq_%u z`k%a0WWOyys$v<+qiuq-NiiS{w8~Dp5X#)msDZ~}g3S-nK~LN7U2NkhBT{;HZStlu z=}F$iVX7KE6v9ExQeGMRD(z?`GTc^>NG8~X-M7FAKYi)~-><;xR7c##?o8~umgSWa zmAPJi?-*z4d^eyWd9^Qau`cis%w}=R(B~d~AIAsb!pS4ut87ADmW_!#FWB~@O?D{y2vj!Efc?%@O*OV?4u;T<%oeZswCo&n2Zc(nSArfWq7_>gjt=7eKW*i^$jzN z4bHgl{GtSrp+d*%{R_whWa23|l-5=9&52l~7kPAAg6o;{RXQ4LvielcNmxMb74-AW zO>W7hw&qad8HJx-sm>zW3ycQg(E9!>%Sn5}FRs#KcjEf-bvU$Gjm~wlhp+8zO%3an z^|bEh)%3i87C_&z(MhyfT_iYUj9Qnjjb)tE*wtZrQ=Em=?Gl3OazevnjclrtWS8XL z8Ve>auB59Jdv@Fy@pZ^03dtvWhZ+_^qBi=?budFTd`7;Ox@?YnK~{64^C@+_)h2@^ zK0*ZNdX7Vl?g4un6!329m#qnCPU9hEAKJEZe?6@Bw<-Dm#uW_8p{nr2MYZUZW#*Tx zEiNu~a{l~vl)MEDwJ0Y8mC9|&83MIFDR&tGR(H7In!f3}8Ff)s=&UqVLl56llR?i9 zkiSW!xO(+&+^WEF+$<1ek`(-a*@8hNZtp^8#EW>mZecu{OM#qpR8SiH0?QsHTdxSk zN2QR3nAJ97^l7)g1f`?_A3h$FKtKVFRQTg-<1G=|9hFzF_UV`cQUEw19^s;Jc*UL1 za~(j()N0P>@(0OCKl}RcDj&+VB+_GRH&f*UusX>D&6LD>eO+CI=XyoKy_tX4!%Nu< z53)%u7wMcPWIttxEUZ+b_vR7CB+h0p0UgC%XZO>{+@VxqZr?rl%2MJ zr|i<&W?b`0;GES(XB?r*C<*X>_1!R1CRPsv`T$R0eUq|)d&Scv-^cNMG)IgK!zZ&160^!E;QXRI5`erEVn1tC&UX^ zv+J)D7#k!L$b5BG5=z^pArnd~Yi9a%GK6nx$SAQdBQns^73j7lq9WiUfKdQuf#-jW zR8dWH3h&bh^F0-i7`sb$7%$ZeEHyIXHw06J6QJ9AHF?8EE_w062dz9%T`vKmaHxI! zr4p00GjCJ&vjQUttimcijFIgSv=Y0dnV@P$)B!A)+5&3}iu6jc!#FZ~PNMbY9FV2F zR(Rl@XO5H0{F{2sBK7oXh6&2};qPy#;$tvUKD7hl0i=nvULB&^sf@{72tDSdnF93+ z?+qq`oEmAuxbf8Zg#8>stF zW$fiAZynznKVPH=>VzeAw%qs%Uv-mK# zhr=P?Aa#pK!uWj3#IjJ`kFWwEzelI>6tJbPu|8<}Vl_fL)3*yR=f=PfUe z{89%#huzi}TBdXr$eh*r$0B;iOB0PmDeHLh(45ITv(*w+#fOm!NbzNXu$Qkz` zA0f$H`co(^n(}G9KP%6wQNccY+ziDd1IJC69nIVHn-pcVYnkffYc;9W_iU}}##ETj ziS)Eg^LV63ZgZo;)UZXF4Nat2a~e)!gK9JOp|35w;J4-i{8R?oLpX+9>^4r!H|v6` z35N;Xb(WQDqGjtwzi@iI=#yKVx+MV3W|kiSTB>116?d9G_3HXot%psD0nYupDua}D zKH87J8N+bjvX*uOGF`sr1iU&T3m36mIsL1RWDk!470Vc`7|<2E+kp6p%c~&I z9hfZ7l{MW5u_8P9gEf-zge3drAiLXPmGxUfvFO7q{cY>Z>a4+T=Ds=sMvgG|zzN}yCsN6_> zX@h3^HSM={3;)|n7G#ZWaVcwg1;t52^HBVBrG;V?E!G? z2}fqUMcQ4yQGZGW7cn(&Vhb$(UUvMgEWqFUJRRM&7TgC+#sFw*OrB-cwURS^3OGiq zirfPQ3-A`iS`6HBPFD3`ZX#Nr@ZzNfRw<{RUc4e}g9%A2lAs=}5pzc!TI;HA2%%{Mo(*z zqvZ_ycu&4MzWpth`g2_5FJY2A>WW@{c;Ee3_g*4~ynKd<7tkS{Wp+m`@jMI$=}j;? z-fZEc^+_z4?Np@cBda?bcO$-|@G`8)@sYYbVT{%X9Q?OY4P0~}zEE>F!6`dewdz}e zXJ#0SP9A4sQppW4M%58$i&3Edr%@b_n6GDaA2;KSfBzstB$;v;DjahEm@iiQZKa%6NqlJ=_VoN5f=?2TemtY_B>Ky=?o zRWk-_KT{>vWB;*>bhH=KbZk^pHSYYwM=z&5R}uUu8JdhDuli91sZgBPsiGnILVT?S z*-pB$;k^#=bag7dGcpwu(TD6upGgy(8$uWmjmdLeX6g>KZ!bfydk<{G-|B13_wkHG zyr~j+N+yWkn%4qy&W$YIAt6cbg``r~*Lv@{zjac245Jmu-BjUK4kWp3L@LUUn{f)Nl5H67k+s>J$5NjYsdG9g)*VojPvK5CiPWqAK zORUBsF?8v%{7W$~Ya|%;wG#*YM0<@**&6R0R6pc&H;X>*68Nf?ic5p{$bC;2dqLB@ z=oDFb{0rqovbf)q()U}hj+7HN)jssdc}bL+F$s?)ypvA|=n&4b;*X(2FMjkmh#4IZ z!6x+&l61rQGppX^F;`C?-cuSEXe}e*QOh@H~KlTl8UkYJMIO)*aN@} zUUxkR+|dJgH(*6F!IPAle(Hbcd9C@-S@r{z?a0PwEo(sVGZ%~o{vVe!f1*D#xQ>3{ zrE5E&&GXzfWUKgXNP1Um5Vmt{5=;)Y-S%(QfZ!vUKDZujAF4+ zcsiY!`Ndx}j*b9tcSCS4aEI6R1JolAh+vm}0HoUDXYic?+7A%+KZzm#_r(7h&9iY6 z-+hd9?cc4zo@SO$BWqOrmT2%3VSZhG2)D!&h2o+5ZK@AzO(uE)Ba2vu5R*W=P?j$B zlZ)xpFJ{&@Hbwp4q)=$WX$hsGq|jL;dB16VHq<*W**!2&v>iUDXOuZr^&bu9RsVQK zIqqx_rdb<=%qfDc3}V7>$2S;dv+7c-Jn8ed-)|An^3j<*u;FWI)Olj~39&;gn$9M| zVY{vbhubrtnbM3Q*ep}1bSGo6#O7n9B<5?JvqfD$P_#Fi%(SpI!oFi8W6+UdMP*ep zgg(=6S++I;1Ml7acDsViQ5TUMd8RDpeyCnFDbnb+)=+^N=7Qg2MEX`08Hpy1ZYr|LLA0U&JUCePq zLCab$`jJ9X`4f%uWr+J^ggADr^XeBvQ~qbwaUB=PJjg=W56RfG0nN!+v?gRs3TXJp zONHVqEy{Fv+QhZNW_dk@v*Oq})tCiHgU#sM;R#0rd|Z1rQc47`v+GX%(i`LANwU&w zzJwJ%XqWUsV;7hezETMCYk9dm3p+RWpoJ8#O<9BQSV!~JUI$V(0H8{&oZBz~7MHHD z#He|i=6IR4=PK?QxytS1J6kOyFQ_+%f_NE)U8&e{$4$$IMmO#fiI2k6kzsQGbm=kd zn}E5wS{6x#bT5#Wp?&TVQeW<@j%fZaq;Bl!tW;kz`a=h_9V4xMI%w!*v-px9f1>y( z>+FdBU@b_gpVLSXjohcdggX-$Z2!f|Bl90Y(PB?b&pzMydtbZbdpf^bTGH;c(^Vs2 zCeY^j0g`%N5I1DANIZ-suhl2<@6ieWCtv$Ne1`iQp|9U(O;p|Ac8f*%yW9>HmbK8_ zGz<4fiD6ajW>b^>?OS|(qM-((COQDU{7csmI^ z_(}tZ9|P0oDG+e*%_F}59lW~M>RI9 z^hmPL1sri_pTmpu(%diWw0b>*R@W8-W}U`#7!FS&5f}~ zec38>a5rf8cWIvAR(6(uczaWE4=5)!IYX-KO9bN?sV~upDrUQjLr{g_7=#d7P;;h> z*i@3))20C`_$dD@z*gu72nZ^LstRe{Z{Nl65k7PPYwH{K39p|P zt-pXcKRE~D9hb2fY2;Uin0M9R(dpcVY={S3JualjVf-h|A@GEPrNPgY&~ z?!a$}nSen?1`7pMk}Z6_4Txg|(4BJSnTH_roqr*_{%MT=@A0?$op|TbB6eE#zKY9O z3%lj^f#XN@!3LYaXM`1dT>Bb79nb)qfQ;IygJKdcsAk1;F^K`uSs0$=%!_qCKve#d z$HG=vSF8e3TU4alOC?XNEwP{)&Nd~J3hhhg?aA#4?{pFEURwCqd9t%48ttj_sg7L% zYWjbS^~4@s0$fhVfw*ZP5Go5yn_lYi4e)IQ;4nQvo^(%W2Dsz?#+Cj24De-P034U< z6c79?bZ=f2rI&j4c><{#fKf!RsO%nVdQ1*58CfILw(gR*h&z23cSc?>30n50Ao(1= z%D3l^tLmdQR%~yIz3zR6mBbbgBX>6c2~Lj)@X)GG09P*~X_zaTW}eLpi-I}2sZ|#47l)$k)Wtq7Y&8)6tY?Y65r#(wFwGmjj4|~il)amIb&7*n;(=`eIT5Sq!b#aBU=)?B2YCR^P36CJ>QKq6 zcezibf-{^;m^)ud?>}T}`vXMJaY6gdec2pzY{zH5tTRSe6KwBPaZ4drz6wE-Nl|C9DrEJ2 z$0f9KyvkFh@&(Om0efw=6BYr#6#m|0n?vNx?k+)kHCz-V{ z;AuEUK9th=ofpTT#gpLTschF}QfpC;?jmnb^?-({z6yyZmv%;i0YyF)QOGmNH32kr zrz~-I_V2k8LxD6>u#clS#88T}eS9K!7slYx{Ml#+1DnmM8e@BMb0|JoTYi^#F8t^Q zCCZavosKL`tL0kV-L+s3ers!DW#EMt6GgFRox98jS({JVi`!nTw=D{n7xwfj(`--M ztZHiOpFABvuA^h$z=TJgXvpydnYGO&VW;3Wh8CG7;>0t(E24hvP;OA|jkt0uNFk=tat8? z2foDi68{~hmh^Mv(EWaS?+?ra*ia28{oX-Y2wkT0kVZ-O74G8%L)NUPbzum(wn|hd zFH<0m!Wv6^IJg?hg>&;d(62?zpF!;WN=2~MMA-ZNJ)jRakc!e>0`Z`8TCnk+q+Qb? zr~2~S`CO%XJ4)iVV4^T*vTxe3bBoumy&hQOYmf_zCW{VF$}SykeEj|(zAWAks1n41 zY^A&~*`E4ZX5sW3etUa7P00XkQtV<9!!w2f<8Y=M`Q%%m4|s*zk=Tg0_0O z)_pTR)PIYiCGn2{k&oSsAs{wtkCd%rO|p`r?=QcfGqMh*gtirnlB9|7suRbsI9Ful0W0YMJ9(G#FW+2vn%y^cWnV5Y8dC@70ur2&pBcv{rVuuxP&fSUJW5#^#=&AF3{fjg39$Z-AAowaFGKqGDJv4&gzzK=eF5)2t2(H zijNr>OUo{ZWgaYSEiQ|8l~Xyxl?4k68+fBblA>hccPp>OM!TdbM`yvjY4kqfOyA!X zH*oeT+s8mMi=E|5q&7g-SsOd1D&YLnkQm@+$^l~dA+$Oj)%#C&9H_s~ zaH;~ou3Q#U5lg`v-gfFQVv)Ex!~necjv6N1$c*yq!hi+xNScF62##*XzLbrHwZkU{ zy&xmS2nxY=QXPxImnY&rhaMD5n#1V;pKH>QUNR~C;=9O`6r8@&@u0cNQ%Ph)fNehrCKa?v#Eun!&_n1LYKIBA(8ny`_1rnNz0?6Y8jwC+$ZEpWzLa z_dO3e68?%hxyMKAJJqbZruXnFO{Q+bH`QySydD#`ULNVIlAf18+}(mtWP7iyjnT)t zTAVBM92KWH!MuDFn7(~MxHyN(`Cw=I3@3PILNXcw)L5!$90Pt0V}XXV@c0m$-5&lR zP-w}DeWV87oFUf-CG=%o9q;Ia+AWUo{$PXqi&>_{2I3o}xFArsPgjqd40L+>JRY7? z1DV#ApimUq%5m>q283yVbL@Uw$N^!rf3j+Z zO*>r%GB5zl=Pb=WU^>nQTy}j780Q3f9$*6j^qEP3Lbcx-t%EA)C1fKqF=w*JM^F$d z$HVV=x<-pL+3q2122tEsb>wDGW|212-|;aMNtj-EI#SS!Kn!mibYPl}D)06bjO8%; z_+VT|V^7f>UCWm+e)!{rn7Ex@kHjTHel8lEG(xhO(HiotG>Jv@M~IxSio$41Bqb?J zhGi52Zv^77{gfO6{{!}?LtZKC%S7hVc$P0&SDG1Wt-3n6ypZA8!Z|ffC@qfK3$-qi zY#_`fNA|y7KJVAdSdWx%m%De?pQ zD6;2R4ZwKA>E20`p7=RwKKPaZ96aa+U@KvI`HnA>2TJP=*iF3dTt24<2y__SpIFE5 z)#8gr1F8|U()J%%zQsTEi1s#bw^tOF{#_b^D2P0kSqLX&hCjiF|CG8xJStYdz{&*6 zHdc(C&uhj^+ucVCnGtYr9g%$y?C+=WVEw`#+o2?#nhY@H#-IM)%=h2%{s@fgXx9MD zRd?^J`vW9#WbWeg8+zBm7h-OOr6M{SXxAi*0o{2mqbk=BJ`~EHVz9O6^2nhU!A+$_ zJpq7fe@a;Y5{XbutlD#99G_3jOSyS?lkZqtUTe0*#)k~X)9s#ad8RwrEM)#|_uFe{ z8H1!(=-H8+d&PNh1gatxd7@Z~=<>>Gay8Oo^nAJUj&#y2q6?W3Pi_o@ssP6dW%}1D z)zj+@uHwd;u7UK~3)%W_INyH^%J z^?{it0bVOi9UWvVYjt@Qq0*y7jZ@jvK){S{-6Wec+pDtm=1D8^ipWe1%~QMucqY}M z(c25C&c1?En%v2BSehqatY4l5{lSE0>zuk01NqZCAerBG*z~N~C6ieaxW>2gu&y>x zKUDTgNzapJJG!0QkifnsCujPiYHpO`(h)-JcBj}#RU{P>`g0po_{MCcLd#{Szqr(GG`<*oRe22pese+$* zC3Gy3ZOtcixxbXB=4W6Fj>4swgjK5t?QB|FvCZ>|NXCsU_bB#XJs}1{km9=FBI{7U z!V-VBC$wqr$Y-+{R+XM&!@p=+M&I4Enme>76xyEcLp_e+ee1ODQxBMx6<43I72~qY z+86G{pQv$0O+b!}OxN_7N;4cxaoqt^J9X(G`to_VWhi}EC=pgZH04tB){<~uob?(qql zBU6yy(}{0>&;FoRc9P-~ISX-L_5p898MjVe@QJ^TbDH3S42;obz)w@K)FKiY8U)Qw z*fmK%ds{bYX}~;7U{3X%jlDOqfh$RB67zk6+;!dv?r9R=m}1;q(^pVuRM>bYvuV`-HS7uip+W?Oc6WT$&%w>7-w0 zmdF|o0Dq_Wn&MM!f>%)D#+a+=WGstT$$R6LYHFWYsU~r-9T|22tg&$n8o7qwLT)>8 zHHbI3w{qMVY7i~0R-2QDB;R}v{VL$)GdlPw4Cw7%0_%g?!1#7}{1C#XKiJ^_xvF*K zNXpGCKqhaTpdC<-!s&&5uY{w=0;6xE(I^%Q&g`)#u9D~kQf}0JL*#TOw1x5F9DQqM z{_>m^7_xsdeCdxW?j!;AFOKdd_GH_6CBf{k3}3D$bDy0ftGuW!@$!;&m!xLzYyy_V|P;PU`OFP2-psceV8Y_i_siO!AW5dLq@`9kv`t6 znQPukTn%}dN{e@dF`F|~n1h5_%3>3NxPM+s+fG6RVZ3{sBS{*}hAbtiZ>c%og+2>d z2s9`xhw4t;#E<5s&a5yjIq*zKzZ<8=6%B>H8YjOy83qzAmkeS}==P>EkdSGLa2&a{)q?xxcq^_P zJy8$m`aG&jak$Y?uS40ak;O}N6X?d8ziwuCQcJ4CP~04neleThYqVdC)|uCZ4x4(# zrEZb&5s1`r>2n!G$;cLze9e{umI(=eKr8-db|H@%gb;ys zV5Po%iS2n$-cYdb;)*x55CnxTjV#h1>IEU9zb6lTRCo_sk-Hp|;#C}gvu5Qw%Qk!* zO^#r;_j(zd9y=pZTyEU1MRV|BV$$-Mx2@qa07ts*olj6Tc$IzGn5l0AYi<)v=&+90 z_s*b3Qk0CgHqG#oRv!|55f|0Lo;X5GcC%+vT oEQglu(F85pGX2>*gvc_3XpsNA?1%ohoPSgY{8w%hfgf}K2i^f`1ONa4 diff --git a/docs/vmgateway.md b/docs/vmgateway.md index 46a7465b3..e9e5ef571 100644 --- a/docs/vmgateway.md +++ b/docs/vmgateway.md @@ -57,20 +57,20 @@ Start single version of Victoria Metrics Start vmgateway -``` +```bash ./bin/vmgateway -eula -enable.auth -read.url http://localhost:8428 --write.url http://localhost:8428 ``` -Retieve data frof database -``` +Retrieve data from database +```bash curl 'http://localhost:8431/api/v1/series/count' -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ2bV9hY2Nlc3MiOnsidGVuYW50X2lkIjp7fSwicm9sZSI6MX0sImV4cCI6MTkzOTM0NjIxMH0.5WUxEfdcV9hKo4CtQdtuZYOGpGXWwaqM9VuVivMMrVg' - -TODO: need to have queries to show the limits ``` -Expected result -``` -TODO: must be provided + Request with incorrect token or with out token will be rejected: +```bash +curl 'http://localhost:8431/api/v1/series/count' + +curl 'http://localhost:8431/api/v1/series/count' -H 'Authorization: Bearer incorrect-token' ``` @@ -78,9 +78,10 @@ TODO: must be provided vmgateway-rl -TODO: no information about source for rate limiting + Limits incoming requests by given pre-configured limits. It supports read and write limiting by a tenant. -Limits incoming requests by given pre-configured limits. It supports read and write limiting with `minute` and `hour` interval. + `vmgateway` needs datasource for rate limits queries. It can be single-node or cluster version of `victoria-metrics`. +It must have metrics scrapped from cluster, that you want to rate limit. List of supported limit types: - `queries` - count of api requests made at tenant to read api, such as `/api/v1/query`, `/api/v1/series` and others. @@ -113,7 +114,7 @@ limits: #### QuickStart -ClusterMode + cluster version required for rate limiting. ```bash # start datasource for cluster metrics @@ -157,6 +158,8 @@ EOF curl 'http://localhost:8431/api/v1/import/prometheus' -X POST -d 'foo{bar="baz1"} 123' -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2MjAxNjIwMDAwMDAsInZtX2FjY2VzcyI6eyJ0ZW5hbnRfaWQiOnsiYWNjb3VudF9pZCI6MTV9fX0.PB1_KXDKPUp-40pxOGk6lt_jt9Yq80PIMpWVJqSForQ' # read metric from tenant 1:5 curl 'http://localhost:8431/api/v1/labels' -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2MjAxNjIwMDAwMDAsInZtX2FjY2VzcyI6eyJ0ZW5hbnRfaWQiOnsiYWNjb3VudF9pZCI6MTV9fX0.PB1_KXDKPUp-40pxOGk6lt_jt9Yq80PIMpWVJqSForQ' + +# check rate limit ``` ### Configuration @@ -278,6 +281,7 @@ The shortlist of configuration flags is the following: ### Limitations * Access Control: - * `jwt` token must be validated by external system, currently `vmauth` can't validate the signature. + * `jwt` token must be validated by external system, currently `vmgateway` can't validate the signature. * RateLimiting: * limits applied based on queries to `datasource.url` + * only cluster version can be rate-limited. From 43f9842b6f7703e044bd667c7ea7cd8193be170b Mon Sep 17 00:00:00 2001 From: Aliaksandr Valialkin Date: Sat, 3 Apr 2021 03:02:52 +0300 Subject: [PATCH 30/63] lib/proxy: log response body on non-200 response code This should improve debuggability for https://github.com/VictoriaMetrics/VictoriaMetrics/issues/1179 --- lib/proxy/proxy.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/proxy/proxy.go b/lib/proxy/proxy.go index 207276cac..1bbd85d5b 100644 --- a/lib/proxy/proxy.go +++ b/lib/proxy/proxy.go @@ -159,7 +159,7 @@ func sendConnectRequest(proxyConn net.Conn, proxyAddr, dstAddr, authHeader strin return nil, fmt.Errorf("cannot read CONNECT response for dstAddr=%q: %w", dstAddr, err) } if statusCode := res.Header.StatusCode(); statusCode != 200 { - return nil, fmt.Errorf("unexpected status code received: %d; want: 200", statusCode) + return nil, fmt.Errorf("unexpected status code received: %d; want: 200; response body: %q", statusCode, res.Body()) } return conn, nil } From e1d1708fa2933cfa9eb15815edf24f445ca391bb Mon Sep 17 00:00:00 2001 From: Aliaksandr Valialkin Date: Sat, 3 Apr 2021 21:34:13 +0300 Subject: [PATCH 31/63] docs/Single-server-VictoriaMetrics.md: add missing link in heading to `Graphite Render API usage` chapter --- README.md | 1 + docs/Single-server-VictoriaMetrics.md | 1 + 2 files changed, 2 insertions(+) diff --git a/README.md b/README.md index aed76c314..756b116eb 100644 --- a/README.md +++ b/README.md @@ -117,6 +117,7 @@ Alphabetically sorted links to case studies: * [Prometheus querying API usage](#prometheus-querying-api-usage) * [Prometheus querying API enhancements](#prometheus-querying-api-enhancements) * [Graphite API usage](#graphite-api-usage) + * [Graphite Render API usage](#graphite-render-api-usage) * [Graphite Metrics API usage](#graphite-metrics-api-usage) * [Graphite Tags API usage](#graphite-tags-api-usage) * [How to build from sources](#how-to-build-from-sources) diff --git a/docs/Single-server-VictoriaMetrics.md b/docs/Single-server-VictoriaMetrics.md index aed76c314..756b116eb 100644 --- a/docs/Single-server-VictoriaMetrics.md +++ b/docs/Single-server-VictoriaMetrics.md @@ -117,6 +117,7 @@ Alphabetically sorted links to case studies: * [Prometheus querying API usage](#prometheus-querying-api-usage) * [Prometheus querying API enhancements](#prometheus-querying-api-enhancements) * [Graphite API usage](#graphite-api-usage) + * [Graphite Render API usage](#graphite-render-api-usage) * [Graphite Metrics API usage](#graphite-metrics-api-usage) * [Graphite Tags API usage](#graphite-tags-api-usage) * [How to build from sources](#how-to-build-from-sources) From 6f3080f9fb78b3a2986f2cab25a39be2970b38f1 Mon Sep 17 00:00:00 2001 From: Aliaksandr Valialkin Date: Sat, 3 Apr 2021 21:41:34 +0300 Subject: [PATCH 32/63] docs/CHANGELOG.md: document the change from 3055ab0115ee21f9b14f450ca6f255cf0adfef94 (add ability to pass "label=value" to the third argument to `topk_*` and `bottomk_*` functions --- docs/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 31b96ad99..3c29577ee 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -3,6 +3,7 @@ # tip * FEATURE: vminsert and vmagent: add `-sortLabels` command-line flag for sorting metric labels before pushing them to `vmstorage`. This should reduce the size of `MetricName -> internal_series_id` cache (aka `vm_cache_size_bytes{type="storage/tsid"}`) when ingesting samples for the same time series with distinct order of labels. For example, `foo{k1="v1",k2="v2"}` and `foo{k2="v2",k1="v1"}` represent a single time series. +* FEATURE: allow specifying label value alongside label name for the `others sum` time series returned from `topk_*` and `bottomk_*` functions from [MetricsQL](https://victoriametrics.github.io/MetricsQL.html). For example, `topk_avg(3, max(process_resident_memory_bytes) by (instance), "instance=other_sum")` would return top 3 series from `max(process_resident_memory_bytes) by (instance)` plus a series containing of the sum of other series. The `others sum` series will have `{instance="other_sum"}` label. * FEATURE: update Go builder from `v1.16.2` to `v1.16.3`. This should fix [these issues](https://github.com/golang/go/issues?q=milestone%3AGo1.16.3+label%3ACherryPickApproved). * FEATURE: vmagent: add support for `follow_redirects` option to `scrape_configs` section in the same way as [Prometheus 2.26 does](https://github.com/prometheus/prometheus/pull/8546). * FEATURE: vmagent: add support for `authorization` section in `-promscrape.config` in the same way as [Prometheus 2.26 does](https://github.com/prometheus/prometheus/pull/8512). From 7a0b964e8dcc71e5342a075bf6e9bb4fdb5f02ba Mon Sep 17 00:00:00 2001 From: Aliaksandr Valialkin Date: Sat, 3 Apr 2021 22:05:06 +0300 Subject: [PATCH 33/63] app/vmselect/promql: do not delete `dst_label` if `src_label` is empty in `label_copy(q, src_label, dst_label)` and `label_move(q, src_label, dst_label)` --- app/vmselect/promql/exec_test.go | 12 ++++++++++++ app/vmselect/promql/transform.go | 7 ++++--- docs/CHANGELOG.md | 1 + docs/MetricsQL.md | 24 ++++++++++++------------ 4 files changed, 29 insertions(+), 15 deletions(-) diff --git a/app/vmselect/promql/exec_test.go b/app/vmselect/promql/exec_test.go index d7a5c1959..f6bfcc1ea 100644 --- a/app/vmselect/promql/exec_test.go +++ b/app/vmselect/promql/exec_test.go @@ -1264,6 +1264,12 @@ func TestExecSuccess(t *testing.T) { Values: []float64{1000, 1200, 1400, 1600, 1800, 2000}, Timestamps: timestampsExpected, } + r.MetricName.Tags = []storage.Tag{ + { + Key: []byte("tagname"), + Value: []byte("foobar"), + }, + } resultExpected := []netstorage.Result{r} f(q, resultExpected) }) @@ -1278,6 +1284,12 @@ func TestExecSuccess(t *testing.T) { Values: []float64{1000, 1200, 1400, 1600, 1800, 2000}, Timestamps: timestampsExpected, } + r.MetricName.Tags = []storage.Tag{ + { + Key: []byte("tagname"), + Value: []byte("foobar"), + }, + } resultExpected := []netstorage.Result{r} f(q, resultExpected) }) diff --git a/app/vmselect/promql/transform.go b/app/vmselect/promql/transform.go index 8165802c3..4402772b7 100644 --- a/app/vmselect/promql/transform.go +++ b/app/vmselect/promql/transform.go @@ -1446,11 +1446,12 @@ func transformLabelCopyExt(tfa *transformFuncArg, removeSrcLabels bool) ([]*time for i, srcLabel := range srcLabels { dstLabel := dstLabels[i] value := mn.GetTagValue(srcLabel) + if len(value) == 0 { + // Do not remove destination label if the source label doesn't exist. + continue + } dstValue := getDstValue(mn, dstLabel) *dstValue = append((*dstValue)[:0], value...) - if len(value) == 0 { - mn.RemoveTag(dstLabel) - } if removeSrcLabels && srcLabel != dstLabel { mn.RemoveTag(srcLabel) } diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 3c29577ee..0da0a1e4d 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -4,6 +4,7 @@ * FEATURE: vminsert and vmagent: add `-sortLabels` command-line flag for sorting metric labels before pushing them to `vmstorage`. This should reduce the size of `MetricName -> internal_series_id` cache (aka `vm_cache_size_bytes{type="storage/tsid"}`) when ingesting samples for the same time series with distinct order of labels. For example, `foo{k1="v1",k2="v2"}` and `foo{k2="v2",k1="v1"}` represent a single time series. * FEATURE: allow specifying label value alongside label name for the `others sum` time series returned from `topk_*` and `bottomk_*` functions from [MetricsQL](https://victoriametrics.github.io/MetricsQL.html). For example, `topk_avg(3, max(process_resident_memory_bytes) by (instance), "instance=other_sum")` would return top 3 series from `max(process_resident_memory_bytes) by (instance)` plus a series containing of the sum of other series. The `others sum` series will have `{instance="other_sum"}` label. +* FEATURE: do not delete `dst_label` when applying `label_copy(q, "src_label", "dst_label")` and `label_move(q, "src_label", "dst_label")` to series without `src_label` and with non-empty `dst_label`. See more details at [MetricsQL docs](https://victoriametrics.github.io/MetricsQL.html). * FEATURE: update Go builder from `v1.16.2` to `v1.16.3`. This should fix [these issues](https://github.com/golang/go/issues?q=milestone%3AGo1.16.3+label%3ACherryPickApproved). * FEATURE: vmagent: add support for `follow_redirects` option to `scrape_configs` section in the same way as [Prometheus 2.26 does](https://github.com/prometheus/prometheus/pull/8546). * FEATURE: vmagent: add support for `authorization` section in `-promscrape.config` in the same way as [Prometheus 2.26 does](https://github.com/prometheus/prometheus/pull/8512). diff --git a/docs/MetricsQL.md b/docs/MetricsQL.md index 53be6f107..514816a36 100644 --- a/docs/MetricsQL.md +++ b/docs/MetricsQL.md @@ -55,19 +55,19 @@ This functionality can be tried at [an editable Grafana dashboard](http://play-g - `ru(freeResources, maxResources)` function for returning resource utilization percentage in the range `0% - 100%`. For instance, `ru(node_memory_MemFree_bytes, node_memory_MemTotal_bytes)` returns memory utilization over [node_exporter](https://github.com/prometheus/node_exporter) metrics. - `ttf(slowlyChangingFreeResources)` function for returning the time in seconds when the given `slowlyChangingFreeResources` expression reaches zero. For instance, `ttf(node_filesystem_avail_byte)` returns the time to storage space exhaustion. This function may be useful for capacity planning. - Functions for label manipulation: - - `alias(q, name)` for setting metric name across all the time series `q`. - - `label_set(q, label1, value1, ... labelN, valueN)` for setting the given values for the given labels on `q`. - - `label_map(q, label, srcValue1, dstValue1, ... srcValueN, dstValueN)` for mapping `label` values from `src*` to `dst*`. - - `label_uppercase(q, label1, ... labelN)` for uppercasing values for the given labels. - - `label_lowercase(q, label2, ... labelN)` for lowercasing value for the given labels. - - `label_del(q, label1, ... labelN)` for deleting the given labels from `q`. - - `label_keep(q, label1, ... labelN)` for deleting all the labels except the given labels from `q`. - - `label_copy(q, src_label1, dst_label1, ... src_labelN, dst_labelN)` for copying label values from `src_*` to `dst_*`. - - `label_move(q, src_label1, dst_label1, ... src_labelN, dst_labelN)` for moving label values from `src_*` to `dst_*`. - - `label_transform(q, label, regexp, replacement)` for replacing all the `regexp` occurences with `replacement` in the `label` values from `q`. - - `label_value(q, label)` - returns numeric values for the given `label` from `q`. + - `alias(q, name)` for setting metric name across all the time series `q`. For example, `alias(foo, "bar")` would give `bar` name to all the `foo` series. + - `label_set(q, label1, value1, ... labelN, valueN)` for setting the given values for the given labels on `q`. For example, `label_set(foo, "bar", "baz")` would add `{bar="baz"}` label to all the `foo` series. + - `label_map(q, label, srcValue1, dstValue1, ... srcValueN, dstValueN)` for mapping `label` values from `src*` to `dst*`. For example, `label_map(foo, "instance", "127.0.0.1", "locahost")` would rename `foo{instance="127.0.0.1"}` to `foo{instance="localhost"}`. + - `label_uppercase(q, label1, ... labelN)` for uppercasing values for the given labels. For example, `label_uppercase(foo, "instance")` would transform `foo{instance="bar"}` to `foo{instance="BAR"}`. + - `label_lowercase(q, label2, ... labelN)` for lowercasing value for the given labels. For example, `label_lowercase(foo, "instance")` would transform `foo{instance="BAR"}` to `foo{instance="bar"}`. + - `label_del(q, label1, ... labelN)` for deleting the given labels from `q`. For example, `label_del(foo, "bar")` would delete `bar` label from all the `foo` series. + - `label_keep(q, label1, ... labelN)` for deleting all the labels except the given labels from `q`. For example, `label_keep(foo, "bar")` would delete all the labels except `bar` from `foo` series. + - `label_copy(q, src_label1, dst_label1, ... src_labelN, dst_labelN)` for copying label values from `src_*` to `dst_*`. If `src_label` is empty, then `dst_label` is left untouched. For example, `label_copy(foo, "bar", baz")` would transform `foo{bar="x"}` to `foo{bar="x",baz="x"}`. + - `label_move(q, src_label1, dst_label1, ... src_labelN, dst_labelN)` for moving label values from `src_*` to `dst_*`. If `src_label` is empty, then `dst_label` is left untouched. For example, `label_move(foo, 'bar", "baz")` would transform `foo{bar="x"}` to `foo{bax="x"}`. + - `label_transform(q, label, regexp, replacement)` for replacing all the `regexp` occurences with `replacement` in the `label` values from `q`. For example, `label_transform(foo, "bar", "-", "_")` would transform `foo{bar="a-b-c"}` to `foo{bar="a_b_c"}`. + - `label_value(q, label)` - returns numeric values for the given `label` from `q`. For example, if `label_value(foo, "bar")` is applied to `foo{bar="1.234"}`, then it will return a time series `foo{bar="1.234"}` with `1.234` value. - `label_match(q, label, regexp)` and `label_mismatch(q, label, regexp)` for filtering time series with labels matching (or not matching) the given regexps. -- `sort_by_label(q, label1, ... labelN)` and `sort_by_label_desc(q, label1, ... labelN)` for sorting time series by the given set of labels. +- `sort_by_label(q, label1, ... labelN)` and `sort_by_label_desc(q, label1, ... labelN)` for sorting time series by the given set of labels. For example, `sort_by_label(foo, "bar")` would sort `foo` series by values of the label `bar` in these series. - `step()` function for returning the step in seconds used in the query. - `start()` and `end()` functions for returning the start and end timestamps of the `[start ... end]` range used in the query. - `integrate(m[d])` for returning integral over the given duration `d` for the given metric `m`. From 4c56b1a6dde0f0e6a8e1eee578cb2a7eb96fac96 Mon Sep 17 00:00:00 2001 From: Aliaksandr Valialkin Date: Sat, 3 Apr 2021 22:13:22 +0300 Subject: [PATCH 34/63] lib/promscrape: add tests for `authorization` config, which has been added in df148f48b749da302494f1f546159e237ba75032 --- lib/promscrape/config_test.go | 69 ++++++++++++++++++++++++++++++++--- 1 file changed, 64 insertions(+), 5 deletions(-) diff --git a/lib/promscrape/config_test.go b/lib/promscrape/config_test.go index 93e5ddcba..8f3cf6a97 100644 --- a/lib/promscrape/config_test.go +++ b/lib/promscrape/config_test.go @@ -328,6 +328,29 @@ scrape_configs: - targets: ["a"] `) + // Both `authorization` and `basic_auth` are set + f(` +scrape_configs: +- job_name: x + authorization: + credentials: foobar + basic_auth: + username: foobar + static_configs: + - targets: ["a"] +`) + + // Both `authorization` and `bearer_token` are set + f(` +scrape_configs: +- job_name: x + authorization: + credentials: foobar + bearer_token: foo + static_configs: + - targets: ["a"] +`) + // Invalid `bearer_token_file` f(` scrape_configs: @@ -773,6 +796,12 @@ scrape_configs: insecure_skip_verify: true static_configs: - targets: [1.2.3.4] +- job_name: asdf + authorization: + type: xyz + credentials: abc + static_configs: + - targets: [foobar] `, []*ScrapeWork{ { ScrapeURL: "https://foo.bar:443/foo/bar?p=x%26y&p=%3D", @@ -867,11 +896,9 @@ scrape_configs: jobNameOriginal: "foo", }, { - ScrapeURL: "http://1.2.3.4:80/metrics", - ScrapeInterval: 8 * time.Second, - ScrapeTimeout: 34 * time.Second, - HonorLabels: false, - HonorTimestamps: false, + ScrapeURL: "http://1.2.3.4:80/metrics", + ScrapeInterval: 8 * time.Second, + ScrapeTimeout: 34 * time.Second, Labels: []prompbmarshal.Label{ { Name: "__address__", @@ -902,6 +929,38 @@ scrape_configs: ProxyAuthConfig: &promauth.Config{}, jobNameOriginal: "qwer", }, + { + ScrapeURL: "http://foobar:80/metrics", + ScrapeInterval: 8 * time.Second, + ScrapeTimeout: 34 * time.Second, + Labels: []prompbmarshal.Label{ + { + Name: "__address__", + Value: "foobar", + }, + { + Name: "__metrics_path__", + Value: "/metrics", + }, + { + Name: "__scheme__", + Value: "http", + }, + { + Name: "instance", + Value: "foobar:80", + }, + { + Name: "job", + Value: "asdf", + }, + }, + AuthConfig: &promauth.Config{ + Authorization: "xyz abc", + }, + ProxyAuthConfig: &promauth.Config{}, + jobNameOriginal: "asdf", + }, }) f(` scrape_configs: From 5153410ced0b5709c6cbd99c79a2b8f365fa9806 Mon Sep 17 00:00:00 2001 From: Aliaksandr Valialkin Date: Sun, 4 Apr 2021 00:40:08 +0300 Subject: [PATCH 35/63] lib/promscrape: support for simple HTTP proxies without `CONNECT` method support such as https://github.com/prometheus-community/PushProx See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/1179 --- app/vmagent/README.md | 2 +- docs/CHANGELOG.md | 2 + docs/vmagent.md | 2 +- lib/promauth/config.go | 14 ++++ lib/promscrape/client.go | 39 +++++++++-- lib/promscrape/config.go | 18 ++--- lib/promscrape/discovery/consul/api.go | 6 +- lib/promscrape/discovery/consul/consul.go | 27 ++++---- lib/promscrape/discovery/dockerswarm/api.go | 8 ++- .../discovery/dockerswarm/dockerswarm.go | 5 +- lib/promscrape/discovery/eureka/api.go | 6 +- lib/promscrape/discovery/eureka/eureka.go | 7 +- lib/promscrape/discoveryutils/client.go | 67 +++++++++++++------ lib/proxy/proxy.go | 36 +++++++--- 14 files changed, 169 insertions(+), 70 deletions(-) diff --git a/app/vmagent/README.md b/app/vmagent/README.md index f6f159177..0b8789652 100644 --- a/app/vmagent/README.md +++ b/app/vmagent/README.md @@ -178,7 +178,7 @@ The following scrape types in [scrape_config](https://prometheus.io/docs/prometh Please file feature requests to [our issue tracker](https://github.com/VictoriaMetrics/VictoriaMetrics/issues) if you need other service discovery mechanisms to be supported by `vmagent`. -`vmagent` also support the following additional options in `scrape_config` section: +`vmagent` also support the following additional options in `scrape_configs` section: * `disable_compression: true` - to disable response compression on a per-job basis. By default `vmagent` requests compressed responses from scrape targets to save network bandwidth. diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 0da0a1e4d..09a0d8bc0 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -10,8 +10,10 @@ * FEATURE: vmagent: add support for `authorization` section in `-promscrape.config` in the same way as [Prometheus 2.26 does](https://github.com/prometheus/prometheus/pull/8512). * FEATURE: vmagent: reduce memory usage when `-remoteWrite.queues` is set to a big value. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/1167). * FEATURE: vmagent: add AWS IAM roles for tasks support for EC2 service discovery according to [these docs](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-iam-roles.html). +* FEATURE: vmagent: add support for `proxy_tls_config`, `proxy_authorization`, `proxy_basic_auth`, `proxy_bearer_token` and `proxy_bearer_token_file` options to `consul_sd_config`, `dockerswarm_sd_config` and `eureka_sd_config` sections. * FEATURE: vmauth: add support for authorization via [bearer token](https://swagger.io/docs/specification/authentication/bearer-authentication/). See [the docs](https://victoriametrics.github.io/vmauth.html#auth-config) for details. +* BUGFIX: vmagent: properly work with simple HTTP proxies which don't support `CONNECT` method. For example, [PushProx](https://github.com/prometheus-community/PushProx). See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/1179). * BUGFIX: vmagent: properly discover targets if multiple namespace selectors are put inside `kubernetes_sd_config`. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/1170). * BUGFIX: properly generate filename for `*.tar.gz` archive inside `_checksums.txt` file posted at [releases page](https://github.com/VictoriaMetrics/VictoriaMetrics/releases). See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/1171). diff --git a/docs/vmagent.md b/docs/vmagent.md index f6f159177..0b8789652 100644 --- a/docs/vmagent.md +++ b/docs/vmagent.md @@ -178,7 +178,7 @@ The following scrape types in [scrape_config](https://prometheus.io/docs/prometh Please file feature requests to [our issue tracker](https://github.com/VictoriaMetrics/VictoriaMetrics/issues) if you need other service discovery mechanisms to be supported by `vmagent`. -`vmagent` also support the following additional options in `scrape_config` section: +`vmagent` also support the following additional options in `scrape_configs` section: * `disable_compression: true` - to disable response compression on a per-job basis. By default `vmagent` requests compressed responses from scrape targets to save network bandwidth. diff --git a/lib/promauth/config.go b/lib/promauth/config.go index 1797aa365..268589f06 100644 --- a/lib/promauth/config.go +++ b/lib/promauth/config.go @@ -45,6 +45,15 @@ type HTTPClientConfig struct { TLSConfig *TLSConfig `yaml:"tls_config,omitempty"` } +// ProxyClientConfig represents proxy client config. +type ProxyClientConfig struct { + Authorization *Authorization `yaml:"proxy_authorization,omitempty"` + BasicAuth *BasicAuthConfig `yaml:"proxy_basic_auth,omitempty"` + BearerToken string `yaml:"proxy_bearer_token,omitempty"` + BearerTokenFile string `yaml:"proxy_bearer_token_file,omitempty"` + TLSConfig *TLSConfig `yaml:"proxy_tls_config,omitempty"` +} + // Config is auth config. type Config struct { // Optional `Authorization` header. @@ -103,6 +112,11 @@ func (hcc *HTTPClientConfig) NewConfig(baseDir string) (*Config, error) { return NewConfig(baseDir, hcc.Authorization, hcc.BasicAuth, hcc.BearerToken, hcc.BearerTokenFile, hcc.TLSConfig) } +// NewConfig creates auth config for the given pcc. +func (pcc *ProxyClientConfig) NewConfig(baseDir string) (*Config, error) { + return NewConfig(baseDir, pcc.Authorization, pcc.BasicAuth, pcc.BearerToken, pcc.BearerTokenFile, pcc.TLSConfig) +} + // NewConfig creates auth config from the given args. func NewConfig(baseDir string, az *Authorization, basicAuth *BasicAuthConfig, bearerToken, bearerTokenFile string, tlsConfig *TLSConfig) (*Config, error) { var authorization string diff --git a/lib/promscrape/client.go b/lib/promscrape/client.go index 1f6f31421..f0d329c9f 100644 --- a/lib/promscrape/client.go +++ b/lib/promscrape/client.go @@ -15,6 +15,7 @@ import ( "github.com/VictoriaMetrics/VictoriaMetrics/lib/bytesutil" "github.com/VictoriaMetrics/VictoriaMetrics/lib/flagutil" "github.com/VictoriaMetrics/VictoriaMetrics/lib/logger" + "github.com/VictoriaMetrics/VictoriaMetrics/lib/proxy" "github.com/VictoriaMetrics/fasthttp" "github.com/VictoriaMetrics/metrics" ) @@ -46,6 +47,7 @@ type client struct { host string requestURI string authHeader string + proxyAuthHeader string denyRedirects bool disableCompression bool disableKeepAlive bool @@ -61,6 +63,22 @@ func newClient(sw *ScrapeWork) *client { if isTLS { tlsCfg = sw.AuthConfig.NewTLSConfig() } + proxyAuthHeader := "" + proxyURL := sw.ProxyURL + if !isTLS && proxyURL.IsHTTPOrHTTPS() { + // Send full sw.ScrapeURL in requests to a proxy host for non-TLS scrape targets + // like net/http package from Go does. + // See https://en.wikipedia.org/wiki/Proxy_server#Web_proxy_servers + pu := proxyURL.URL() + host = pu.Host + requestURI = sw.ScrapeURL + isTLS = pu.Scheme == "https" + if isTLS { + tlsCfg = sw.ProxyAuthConfig.NewTLSConfig() + } + proxyAuthHeader = proxyURL.GetAuthHeader(sw.ProxyAuthConfig) + proxyURL = proxy.URL{} + } if !strings.Contains(host, ":") { if !isTLS { host += ":80" @@ -68,7 +86,7 @@ func newClient(sw *ScrapeWork) *client { host += ":443" } } - dialFunc, err := newStatDialFunc(sw.ProxyURL, sw.ProxyAuthConfig) + dialFunc, err := newStatDialFunc(proxyURL, sw.ProxyAuthConfig) if err != nil { logger.Fatalf("cannot create dial func: %s", err) } @@ -86,14 +104,14 @@ func newClient(sw *ScrapeWork) *client { } var sc *http.Client if *streamParse || sw.StreamParse { - var proxy func(*http.Request) (*url.URL, error) + var proxyURLFunc func(*http.Request) (*url.URL, error) if proxyURL := sw.ProxyURL.URL(); proxyURL != nil { - proxy = http.ProxyURL(proxyURL) + proxyURLFunc = http.ProxyURL(proxyURL) } sc = &http.Client{ Transport: &http.Transport{ TLSClientConfig: tlsCfg, - Proxy: proxy, + Proxy: proxyURLFunc, TLSHandshakeTimeout: 10 * time.Second, IdleConnTimeout: 2 * sw.ScrapeInterval, DisableCompression: *disableCompression || sw.DisableCompression, @@ -115,6 +133,7 @@ func newClient(sw *ScrapeWork) *client { host: host, requestURI: requestURI, authHeader: sw.AuthConfig.Authorization, + proxyAuthHeader: proxyAuthHeader, denyRedirects: sw.DenyRedirects, disableCompression: sw.DisableCompression, disableKeepAlive: sw.DisableKeepAlive, @@ -138,6 +157,9 @@ func (c *client) GetStreamReader() (*streamReader, error) { if c.authHeader != "" { req.Header.Set("Authorization", c.authHeader) } + if c.proxyAuthHeader != "" { + req.Header.Set("Proxy-Authorization", c.proxyAuthHeader) + } resp, err := c.sc.Do(req) if err != nil { cancel() @@ -169,15 +191,18 @@ func (c *client) ReadData(dst []byte) ([]byte, error) { // See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/608 for details. // Do not bloat the `Accept` header with OpenMetrics shit, since it looks like dead standard now. req.Header.Set("Accept", "text/plain;version=0.0.4;q=1,*/*;q=0.1") + if c.authHeader != "" { + req.Header.Set("Authorization", c.authHeader) + } + if c.proxyAuthHeader != "" { + req.Header.Set("Proxy-Authorization", c.proxyAuthHeader) + } if !*disableCompression && !c.disableCompression { req.Header.Set("Accept-Encoding", "gzip") } if *disableKeepAlive || c.disableKeepAlive { req.SetConnectionClose() } - if c.authHeader != "" { - req.Header.Set("Authorization", c.authHeader) - } resp := fasthttp.AcquireResponse() swapResponseBodies := len(dst) == 0 if swapResponseBodies { diff --git a/lib/promscrape/config.go b/lib/promscrape/config.go index 2710c0680..b4a8d5d2c 100644 --- a/lib/promscrape/config.go +++ b/lib/promscrape/config.go @@ -109,16 +109,12 @@ type ScrapeConfig struct { GCESDConfigs []gce.SDConfig `yaml:"gce_sd_configs,omitempty"` // These options are supported only by lib/promscrape. - DisableCompression bool `yaml:"disable_compression,omitempty"` - DisableKeepAlive bool `yaml:"disable_keepalive,omitempty"` - StreamParse bool `yaml:"stream_parse,omitempty"` - ScrapeAlignInterval time.Duration `yaml:"scrape_align_interval,omitempty"` - ScrapeOffset time.Duration `yaml:"scrape_offset,omitempty"` - ProxyAuthorization *promauth.Authorization `yaml:"proxy_authorization,omitempty"` - ProxyBasicAuth *promauth.BasicAuthConfig `yaml:"proxy_basic_auth,omitempty"` - ProxyBearerToken string `yaml:"proxy_bearer_token,omitempty"` - ProxyBearerTokenFile string `yaml:"proxy_bearer_token_file,omitempty"` - ProxyTLSConfig *promauth.TLSConfig `yaml:"proxy_tls_config,omitempty"` + DisableCompression bool `yaml:"disable_compression,omitempty"` + DisableKeepAlive bool `yaml:"disable_keepalive,omitempty"` + StreamParse bool `yaml:"stream_parse,omitempty"` + ScrapeAlignInterval time.Duration `yaml:"scrape_align_interval,omitempty"` + ScrapeOffset time.Duration `yaml:"scrape_offset,omitempty"` + ProxyClientConfig promauth.ProxyClientConfig `yaml:",inline"` // This is set in loadConfig swc *scrapeWorkConfig @@ -551,7 +547,7 @@ func getScrapeWorkConfig(sc *ScrapeConfig, baseDir string, globalCfg *GlobalConf if err != nil { return nil, fmt.Errorf("cannot parse auth config for `job_name` %q: %w", jobName, err) } - proxyAC, err := promauth.NewConfig(baseDir, sc.ProxyAuthorization, sc.ProxyBasicAuth, sc.ProxyBearerToken, sc.ProxyBearerTokenFile, sc.ProxyTLSConfig) + proxyAC, err := sc.ProxyClientConfig.NewConfig(baseDir) if err != nil { return nil, fmt.Errorf("cannot parse proxy auth config for `job_name` %q: %w", jobName, err) } diff --git a/lib/promscrape/discovery/consul/api.go b/lib/promscrape/discovery/consul/api.go index d87e00330..9ffac6945 100644 --- a/lib/promscrape/discovery/consul/api.go +++ b/lib/promscrape/discovery/consul/api.go @@ -65,7 +65,11 @@ func newAPIConfig(sdc *SDConfig, baseDir string) (*apiConfig, error) { } apiServer = scheme + "://" + apiServer } - client, err := discoveryutils.NewClient(apiServer, ac, sdc.ProxyURL) + proxyAC, err := sdc.ProxyClientConfig.NewConfig(baseDir) + if err != nil { + return nil, fmt.Errorf("cannot parse proxy auth config: %w", err) + } + client, err := discoveryutils.NewClient(apiServer, ac, sdc.ProxyURL, proxyAC) if err != nil { return nil, fmt.Errorf("cannot create HTTP client for %q: %w", apiServer, err) } diff --git a/lib/promscrape/discovery/consul/consul.go b/lib/promscrape/discovery/consul/consul.go index bc949641a..5d4e84656 100644 --- a/lib/promscrape/discovery/consul/consul.go +++ b/lib/promscrape/discovery/consul/consul.go @@ -11,19 +11,20 @@ import ( // // See https://prometheus.io/docs/prometheus/latest/configuration/configuration/#consul_sd_config type SDConfig struct { - Server string `yaml:"server,omitempty"` - Token *string `yaml:"token"` - Datacenter string `yaml:"datacenter"` - Scheme string `yaml:"scheme,omitempty"` - Username string `yaml:"username"` - Password string `yaml:"password"` - ProxyURL proxy.URL `yaml:"proxy_url,omitempty"` - TLSConfig *promauth.TLSConfig `yaml:"tls_config,omitempty"` - Services []string `yaml:"services,omitempty"` - Tags []string `yaml:"tags,omitempty"` - NodeMeta map[string]string `yaml:"node_meta,omitempty"` - TagSeparator *string `yaml:"tag_separator,omitempty"` - AllowStale bool `yaml:"allow_stale,omitempty"` + Server string `yaml:"server,omitempty"` + Token *string `yaml:"token"` + Datacenter string `yaml:"datacenter"` + Scheme string `yaml:"scheme,omitempty"` + Username string `yaml:"username"` + Password string `yaml:"password"` + ProxyURL proxy.URL `yaml:"proxy_url,omitempty"` + ProxyClientConfig promauth.ProxyClientConfig `yaml:",inline"` + TLSConfig *promauth.TLSConfig `yaml:"tls_config,omitempty"` + Services []string `yaml:"services,omitempty"` + Tags []string `yaml:"tags,omitempty"` + NodeMeta map[string]string `yaml:"node_meta,omitempty"` + TagSeparator *string `yaml:"tag_separator,omitempty"` + AllowStale bool `yaml:"allow_stale,omitempty"` // RefreshInterval time.Duration `yaml:"refresh_interval"` // refresh_interval is obtained from `-promscrape.consulSDCheckInterval` command-line option. } diff --git a/lib/promscrape/discovery/dockerswarm/api.go b/lib/promscrape/discovery/dockerswarm/api.go index 88e8abe7a..472a07736 100644 --- a/lib/promscrape/discovery/dockerswarm/api.go +++ b/lib/promscrape/discovery/dockerswarm/api.go @@ -35,9 +35,13 @@ func newAPIConfig(sdc *SDConfig, baseDir string) (*apiConfig, error) { } ac, err := sdc.HTTPClientConfig.NewConfig(baseDir) if err != nil { - return nil, err + return nil, fmt.Errorf("cannot parse auth config: %w", err) } - client, err := discoveryutils.NewClient(sdc.Host, ac, sdc.ProxyURL) + proxyAC, err := sdc.ProxyClientConfig.NewConfig(baseDir) + if err != nil { + return nil, fmt.Errorf("cannot parse proxy auth config: %w", err) + } + client, err := discoveryutils.NewClient(sdc.Host, ac, sdc.ProxyURL, proxyAC) if err != nil { return nil, fmt.Errorf("cannot create HTTP client for %q: %w", sdc.Host, err) } diff --git a/lib/promscrape/discovery/dockerswarm/dockerswarm.go b/lib/promscrape/discovery/dockerswarm/dockerswarm.go index 7d1aa6c4a..5d9bd73d1 100644 --- a/lib/promscrape/discovery/dockerswarm/dockerswarm.go +++ b/lib/promscrape/discovery/dockerswarm/dockerswarm.go @@ -16,8 +16,9 @@ type SDConfig struct { Port int `yaml:"port,omitempty"` Filters []Filter `yaml:"filters,omitempty"` - ProxyURL proxy.URL `yaml:"proxy_url,omitempty"` - HTTPClientConfig promauth.HTTPClientConfig `yaml:",inline"` + HTTPClientConfig promauth.HTTPClientConfig `yaml:",inline"` + ProxyURL proxy.URL `yaml:"proxy_url,omitempty"` + ProxyClientConfig promauth.ProxyClientConfig `yaml:",inline"` // refresh_interval is obtained from `-promscrape.dockerswarmSDCheckInterval` command-line option } diff --git a/lib/promscrape/discovery/eureka/api.go b/lib/promscrape/discovery/eureka/api.go index e7e5b740b..93e5e2eeb 100644 --- a/lib/promscrape/discovery/eureka/api.go +++ b/lib/promscrape/discovery/eureka/api.go @@ -30,7 +30,11 @@ func newAPIConfig(sdc *SDConfig, baseDir string) (*apiConfig, error) { } apiServer = scheme + "://" + apiServer } - client, err := discoveryutils.NewClient(apiServer, ac, sdc.ProxyURL) + proxyAC, err := sdc.ProxyClientConfig.NewConfig(baseDir) + if err != nil { + return nil, fmt.Errorf("cannot parse proxy auth config: %w", err) + } + client, err := discoveryutils.NewClient(apiServer, ac, sdc.ProxyURL, proxyAC) if err != nil { return nil, fmt.Errorf("cannot create HTTP client for %q: %w", apiServer, err) } diff --git a/lib/promscrape/discovery/eureka/eureka.go b/lib/promscrape/discovery/eureka/eureka.go index 0ea4fbefb..54b7c03f2 100644 --- a/lib/promscrape/discovery/eureka/eureka.go +++ b/lib/promscrape/discovery/eureka/eureka.go @@ -16,9 +16,10 @@ const appsAPIPath = "/apps" // // See https://prometheus.io/docs/prometheus/latest/configuration/configuration/#eureka type SDConfig struct { - Server string `yaml:"server,omitempty"` - ProxyURL proxy.URL `yaml:"proxy_url,omitempty"` - HTTPClientConfig promauth.HTTPClientConfig `ymal:",inline"` + Server string `yaml:"server,omitempty"` + HTTPClientConfig promauth.HTTPClientConfig `ymal:",inline"` + ProxyURL proxy.URL `yaml:"proxy_url,omitempty"` + ProxyClientConfig promauth.ProxyClientConfig `yaml:",inline"` // RefreshInterval time.Duration `yaml:"refresh_interval"` // refresh_interval is obtained from `-promscrape.ec2SDCheckInterval` command-line option. } diff --git a/lib/promscrape/discoveryutils/client.go b/lib/promscrape/discoveryutils/client.go index ecddf92cc..293e39831 100644 --- a/lib/promscrape/discoveryutils/client.go +++ b/lib/promscrape/discoveryutils/client.go @@ -40,22 +40,20 @@ type Client struct { // blockingClient is used for long-polling requests. blockingClient *fasthttp.HostClient - ac *promauth.Config apiServer string - hostPort string + + authHeader string + proxyAuthHeader string + sendFullURL bool } -// NewClient returns new Client for the given apiServer and the given ac. -func NewClient(apiServer string, ac *promauth.Config, proxyURL proxy.URL) (*Client, error) { - var ( - dialFunc fasthttp.DialFunc - tlsCfg *tls.Config - u fasthttp.URI - err error - ) +// NewClient returns new Client for the given args. +func NewClient(apiServer string, ac *promauth.Config, proxyURL proxy.URL, proxyAC *promauth.Config) (*Client, error) { + var u fasthttp.URI u.Update(apiServer) // special case for unix socket connection + var dialFunc fasthttp.DialFunc if string(u.Scheme()) == "unix" { dialAddr := string(u.Path()) apiServer = "http://" @@ -66,9 +64,25 @@ func NewClient(apiServer string, ac *promauth.Config, proxyURL proxy.URL) (*Clie hostPort := string(u.Host()) isTLS := string(u.Scheme()) == "https" + var tlsCfg *tls.Config if isTLS { tlsCfg = ac.NewTLSConfig() } + sendFullURL := !isTLS && proxyURL.IsHTTPOrHTTPS() + proxyAuthHeader := "" + if sendFullURL { + // Send full urls in requests to a proxy host for non-TLS apiServer + // like net/http package from Go does. + // See https://en.wikipedia.org/wiki/Proxy_server#Web_proxy_servers + pu := proxyURL.URL() + hostPort = pu.Host + isTLS = pu.Scheme == "https" + if isTLS { + tlsCfg = proxyAC.NewTLSConfig() + } + proxyAuthHeader = proxyURL.GetAuthHeader(proxyAC) + proxyURL = proxy.URL{} + } if !strings.Contains(hostPort, ":") { port := "80" if isTLS { @@ -77,7 +91,8 @@ func NewClient(apiServer string, ac *promauth.Config, proxyURL proxy.URL) (*Clie hostPort = net.JoinHostPort(hostPort, port) } if dialFunc == nil { - dialFunc, err = proxyURL.NewDialFunc(ac) + var err error + dialFunc, err = proxyURL.NewDialFunc(proxyAC) if err != nil { return nil, err } @@ -104,12 +119,17 @@ func NewClient(apiServer string, ac *promauth.Config, proxyURL proxy.URL) (*Clie MaxConns: 64 * 1024, Dial: dialFunc, } + authHeader := "" + if ac != nil { + authHeader = ac.Authorization + } return &Client{ - hc: hc, - blockingClient: blockingClient, - ac: ac, - apiServer: apiServer, - hostPort: hostPort, + hc: hc, + blockingClient: blockingClient, + apiServer: apiServer, + authHeader: authHeader, + proxyAuthHeader: proxyAuthHeader, + sendFullURL: sendFullURL, }, nil } @@ -159,11 +179,18 @@ func (c *Client) getAPIResponseWithParamsAndClient(client *fasthttp.HostClient, var u fasthttp.URI u.Update(requestURL) var req fasthttp.Request - req.SetRequestURIBytes(u.RequestURI()) - req.SetHost(c.hostPort) + if c.sendFullURL { + req.SetRequestURIBytes(u.FullURI()) + } else { + req.SetRequestURIBytes(u.RequestURI()) + req.SetHostBytes(u.Host()) + } req.Header.Set("Accept-Encoding", "gzip") - if c.ac != nil && c.ac.Authorization != "" { - req.Header.Set("Authorization", c.ac.Authorization) + if c.authHeader != "" { + req.Header.Set("Authorization", c.authHeader) + } + if c.proxyAuthHeader != "" { + req.Header.Set("Proxy-Authorization", c.proxyAuthHeader) } var resp fasthttp.Response diff --git a/lib/proxy/proxy.go b/lib/proxy/proxy.go index 1bbd85d5b..d2afbc9e8 100644 --- a/lib/proxy/proxy.go +++ b/lib/proxy/proxy.go @@ -40,6 +40,16 @@ func (u *URL) URL() *url.URL { return u.url } +// IsHTTPOrHTTPS returns true if u is http or https +func (u *URL) IsHTTPOrHTTPS() bool { + pu := u.URL() + if pu == nil { + return false + } + scheme := u.url.Scheme + return scheme == "http" || scheme == "https" +} + // String returns string representation of u. func (u *URL) String() string { pu := u.URL() @@ -49,6 +59,23 @@ func (u *URL) String() string { return pu.String() } +// GetAuthHeader returns Proxy-Authorization auth header for the given u and ac. +func (u *URL) GetAuthHeader(ac *promauth.Config) string { + authHeader := "" + if ac != nil { + authHeader = ac.Authorization + } + if u == nil || u.url == nil { + return authHeader + } + pu := u.url + if pu.User != nil && len(pu.User.Username()) > 0 { + userPasswordEncoded := base64.StdEncoding.EncodeToString([]byte(pu.User.String())) + authHeader = "Basic " + userPasswordEncoded + } + return authHeader +} + // MarshalYAML implements yaml.Marshaler interface. func (u *URL) MarshalYAML() (interface{}, error) { if u.url == nil { @@ -82,14 +109,7 @@ func (u *URL) NewDialFunc(ac *promauth.Config) (fasthttp.DialFunc, error) { } isTLS := pu.Scheme == "https" proxyAddr := addMissingPort(pu.Host, isTLS) - var authHeader string - if ac != nil { - authHeader = ac.Authorization - } - if pu.User != nil && len(pu.User.Username()) > 0 { - userPasswordEncoded := base64.StdEncoding.EncodeToString([]byte(pu.User.String())) - authHeader = "Basic " + userPasswordEncoded - } + authHeader := u.GetAuthHeader(ac) if authHeader != "" { authHeader = "Proxy-Authorization: " + authHeader + "\r\n" } From 500e625e8c8896ba43eae0ab2c6e53bccdbf7a5a Mon Sep 17 00:00:00 2001 From: Aliaksandr Valialkin Date: Sun, 4 Apr 2021 01:18:24 +0300 Subject: [PATCH 36/63] lib/promscrape: properly send full url in `GET` request via simple HTTP proxy This is a follow-up for a0ae0f86666a75ec57b45eab2429da7ab4a7b250 Updates https://github.com/VictoriaMetrics/VictoriaMetrics/issues/1179 --- lib/promscrape/client.go | 2 +- lib/promscrape/discoveryutils/client.go | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/promscrape/client.go b/lib/promscrape/client.go index f0d329c9f..565e6915c 100644 --- a/lib/promscrape/client.go +++ b/lib/promscrape/client.go @@ -184,7 +184,7 @@ func (c *client) ReadData(dst []byte) ([]byte, error) { deadline := time.Now().Add(c.hc.ReadTimeout) req := fasthttp.AcquireRequest() req.SetRequestURI(c.requestURI) - req.SetHost(c.host) + req.Header.SetHost(c.host) // The following `Accept` header has been copied from Prometheus sources. // See https://github.com/prometheus/prometheus/blob/f9d21f10ecd2a343a381044f131ea4e46381ce09/scrape/scrape.go#L532 . // This is needed as a workaround for scraping stupid Java-based servers such as Spring Boot. diff --git a/lib/promscrape/discoveryutils/client.go b/lib/promscrape/discoveryutils/client.go index 293e39831..a26190ee6 100644 --- a/lib/promscrape/discoveryutils/client.go +++ b/lib/promscrape/discoveryutils/client.go @@ -42,6 +42,7 @@ type Client struct { apiServer string + hostPort string authHeader string proxyAuthHeader string sendFullURL bool @@ -127,6 +128,7 @@ func NewClient(apiServer string, ac *promauth.Config, proxyURL proxy.URL, proxyA hc: hc, blockingClient: blockingClient, apiServer: apiServer, + hostPort: hostPort, authHeader: authHeader, proxyAuthHeader: proxyAuthHeader, sendFullURL: sendFullURL, @@ -183,8 +185,8 @@ func (c *Client) getAPIResponseWithParamsAndClient(client *fasthttp.HostClient, req.SetRequestURIBytes(u.FullURI()) } else { req.SetRequestURIBytes(u.RequestURI()) - req.SetHostBytes(u.Host()) } + req.Header.SetHost(c.hostPort) req.Header.Set("Accept-Encoding", "gzip") if c.authHeader != "" { req.Header.Set("Authorization", c.authHeader) From a4c6a3b3e191e70e7e8d8d43cc98e0fcfa2b32a5 Mon Sep 17 00:00:00 2001 From: Nikolay Date: Sun, 4 Apr 2021 01:29:54 +0300 Subject: [PATCH 37/63] adds socks5 support for fasthttp client (#1178) https://github.com/VictoriaMetrics/VictoriaMetrics/issues/1177 Co-authored-by: Aliaksandr Valialkin --- go.mod | 2 +- go.sum | 7 + lib/proxy/proxy.go | 29 +- .../golang.org/x/net/internal/socks/client.go | 168 ++++++++++ .../golang.org/x/net/internal/socks/socks.go | 317 ++++++++++++++++++ vendor/golang.org/x/net/proxy/dial.go | 54 +++ vendor/golang.org/x/net/proxy/direct.go | 31 ++ vendor/golang.org/x/net/proxy/per_host.go | 155 +++++++++ vendor/golang.org/x/net/proxy/proxy.go | 149 ++++++++ vendor/golang.org/x/net/proxy/socks5.go | 42 +++ vendor/modules.txt | 2 + 11 files changed, 953 insertions(+), 3 deletions(-) create mode 100644 vendor/golang.org/x/net/internal/socks/client.go create mode 100644 vendor/golang.org/x/net/internal/socks/socks.go create mode 100644 vendor/golang.org/x/net/proxy/dial.go create mode 100644 vendor/golang.org/x/net/proxy/direct.go create mode 100644 vendor/golang.org/x/net/proxy/per_host.go create mode 100644 vendor/golang.org/x/net/proxy/proxy.go create mode 100644 vendor/golang.org/x/net/proxy/socks5.go diff --git a/go.mod b/go.mod index 312b15770..8bc04c7bc 100644 --- a/go.mod +++ b/go.mod @@ -34,7 +34,7 @@ require ( github.com/valyala/histogram v1.1.2 github.com/valyala/quicktemplate v1.6.3 golang.org/x/mod v0.4.2 // indirect - golang.org/x/net v0.0.0-20210331212208-0fccb6fa2b5c // indirect + golang.org/x/net v0.0.0-20210331212208-0fccb6fa2b5c golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602 golang.org/x/sys v0.0.0-20210331175145-43e1dd70ce54 golang.org/x/text v0.3.6 // indirect diff --git a/go.sum b/go.sum index 7f330d895..339216818 100644 --- a/go.sum +++ b/go.sum @@ -925,6 +925,8 @@ golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= +golang.org/x/net v0.0.0-20210324205630-d1beb07c2056 h1:sANdAef76Ioam9aQUUdcAqricwY/WUaMc4+7LY4eGg8= +golang.org/x/net v0.0.0-20210324205630-d1beb07c2056/go.mod h1:uSPa2vr4CLtc/ILN5odXGNXS6mhrKVzTaCXzk9m6W3k= golang.org/x/net v0.0.0-20210331212208-0fccb6fa2b5c h1:KHUzaHIpjWVlVVNh65G3hhuj3KB1HnjY6Cq5cTvRQT8= golang.org/x/net v0.0.0-20210331212208-0fccb6fa2b5c/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -938,6 +940,7 @@ golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210323180902-22b0adad7558/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602 h1:0Ja1LBD+yisY6RWM/BH7TJVXWsSjs2VwBSmvSX4HdBc= golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -1023,6 +1026,10 @@ golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210309074719-68d13333faf2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210324051608-47abb6519492 h1:Paq34FxTluEPvVyayQqMPgHm+vTOrIifmcYxFBx9TLg= +golang.org/x/sys v0.0.0-20210324051608-47abb6519492/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44 h1:Bli41pIlzTzf3KEY06n+xnzK/BESIg2ze4Pgfh/aI8c= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210331175145-43e1dd70ce54 h1:rF3Ohx8DRyl8h2zw9qojyLHLhrJpEMgyPOImREEryf0= golang.org/x/sys v0.0.0-20210331175145-43e1dd70ce54/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= diff --git a/lib/proxy/proxy.go b/lib/proxy/proxy.go index d2afbc9e8..087d35fb0 100644 --- a/lib/proxy/proxy.go +++ b/lib/proxy/proxy.go @@ -14,6 +14,7 @@ import ( "github.com/VictoriaMetrics/VictoriaMetrics/lib/netutil" "github.com/VictoriaMetrics/VictoriaMetrics/lib/promauth" "github.com/VictoriaMetrics/fasthttp" + "golang.org/x/net/proxy" ) // URL implements YAML.Marshaler and yaml.Unmarshaler interfaces for url.URL. @@ -104,11 +105,14 @@ func (u *URL) NewDialFunc(ac *promauth.Config) (fasthttp.DialFunc, error) { return defaultDialFunc, nil } pu := u.url - if pu.Scheme != "http" && pu.Scheme != "https" { - return nil, fmt.Errorf("unknown scheme=%q for proxy_url=%q, must be http or https", pu.Scheme, pu.Redacted()) + if pu.Scheme != "http" && pu.Scheme != "https" && pu.Scheme != "socks5" { + return nil, fmt.Errorf("unknown scheme=%q for proxy_url=%q, must be http, https or socks5", pu.Scheme, pu.Redacted()) } isTLS := pu.Scheme == "https" proxyAddr := addMissingPort(pu.Host, isTLS) + if pu.Scheme == "socks5" { + return socks5DialFunc(proxyAddr, pu) + } authHeader := u.GetAuthHeader(ac) if authHeader != "" { authHeader = "Proxy-Authorization: " + authHeader + "\r\n" @@ -138,6 +142,27 @@ func (u *URL) NewDialFunc(ac *promauth.Config) (fasthttp.DialFunc, error) { return dialFunc, nil } +func socks5DialFunc(proxyAddr string, pu *url.URL) (fasthttp.DialFunc, error) { + var sac *proxy.Auth + if pu.User != nil { + username := pu.User.Username() + password, _ := pu.User.Password() + sac = &proxy.Auth{ + User: username, + Password: password, + } + } + network := netutil.GetTCPNetwork() + d, err := proxy.SOCKS5(network, proxyAddr, sac, proxy.Direct) + if err != nil { + return nil, fmt.Errorf("cannot create socks5 proxy for url: %s, err: %w", pu.Redacted(), err) + } + dialFunc := func(addr string) (net.Conn, error) { + return d.Dial(network, addr) + } + return dialFunc, nil +} + func addMissingPort(addr string, isTLS bool) string { if strings.IndexByte(addr, ':') >= 0 { return addr diff --git a/vendor/golang.org/x/net/internal/socks/client.go b/vendor/golang.org/x/net/internal/socks/client.go new file mode 100644 index 000000000..3d6f516a5 --- /dev/null +++ b/vendor/golang.org/x/net/internal/socks/client.go @@ -0,0 +1,168 @@ +// Copyright 2018 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package socks + +import ( + "context" + "errors" + "io" + "net" + "strconv" + "time" +) + +var ( + noDeadline = time.Time{} + aLongTimeAgo = time.Unix(1, 0) +) + +func (d *Dialer) connect(ctx context.Context, c net.Conn, address string) (_ net.Addr, ctxErr error) { + host, port, err := splitHostPort(address) + if err != nil { + return nil, err + } + if deadline, ok := ctx.Deadline(); ok && !deadline.IsZero() { + c.SetDeadline(deadline) + defer c.SetDeadline(noDeadline) + } + if ctx != context.Background() { + errCh := make(chan error, 1) + done := make(chan struct{}) + defer func() { + close(done) + if ctxErr == nil { + ctxErr = <-errCh + } + }() + go func() { + select { + case <-ctx.Done(): + c.SetDeadline(aLongTimeAgo) + errCh <- ctx.Err() + case <-done: + errCh <- nil + } + }() + } + + b := make([]byte, 0, 6+len(host)) // the size here is just an estimate + b = append(b, Version5) + if len(d.AuthMethods) == 0 || d.Authenticate == nil { + b = append(b, 1, byte(AuthMethodNotRequired)) + } else { + ams := d.AuthMethods + if len(ams) > 255 { + return nil, errors.New("too many authentication methods") + } + b = append(b, byte(len(ams))) + for _, am := range ams { + b = append(b, byte(am)) + } + } + if _, ctxErr = c.Write(b); ctxErr != nil { + return + } + + if _, ctxErr = io.ReadFull(c, b[:2]); ctxErr != nil { + return + } + if b[0] != Version5 { + return nil, errors.New("unexpected protocol version " + strconv.Itoa(int(b[0]))) + } + am := AuthMethod(b[1]) + if am == AuthMethodNoAcceptableMethods { + return nil, errors.New("no acceptable authentication methods") + } + if d.Authenticate != nil { + if ctxErr = d.Authenticate(ctx, c, am); ctxErr != nil { + return + } + } + + b = b[:0] + b = append(b, Version5, byte(d.cmd), 0) + if ip := net.ParseIP(host); ip != nil { + if ip4 := ip.To4(); ip4 != nil { + b = append(b, AddrTypeIPv4) + b = append(b, ip4...) + } else if ip6 := ip.To16(); ip6 != nil { + b = append(b, AddrTypeIPv6) + b = append(b, ip6...) + } else { + return nil, errors.New("unknown address type") + } + } else { + if len(host) > 255 { + return nil, errors.New("FQDN too long") + } + b = append(b, AddrTypeFQDN) + b = append(b, byte(len(host))) + b = append(b, host...) + } + b = append(b, byte(port>>8), byte(port)) + if _, ctxErr = c.Write(b); ctxErr != nil { + return + } + + if _, ctxErr = io.ReadFull(c, b[:4]); ctxErr != nil { + return + } + if b[0] != Version5 { + return nil, errors.New("unexpected protocol version " + strconv.Itoa(int(b[0]))) + } + if cmdErr := Reply(b[1]); cmdErr != StatusSucceeded { + return nil, errors.New("unknown error " + cmdErr.String()) + } + if b[2] != 0 { + return nil, errors.New("non-zero reserved field") + } + l := 2 + var a Addr + switch b[3] { + case AddrTypeIPv4: + l += net.IPv4len + a.IP = make(net.IP, net.IPv4len) + case AddrTypeIPv6: + l += net.IPv6len + a.IP = make(net.IP, net.IPv6len) + case AddrTypeFQDN: + if _, err := io.ReadFull(c, b[:1]); err != nil { + return nil, err + } + l += int(b[0]) + default: + return nil, errors.New("unknown address type " + strconv.Itoa(int(b[3]))) + } + if cap(b) < l { + b = make([]byte, l) + } else { + b = b[:l] + } + if _, ctxErr = io.ReadFull(c, b); ctxErr != nil { + return + } + if a.IP != nil { + copy(a.IP, b) + } else { + a.Name = string(b[:len(b)-2]) + } + a.Port = int(b[len(b)-2])<<8 | int(b[len(b)-1]) + return &a, nil +} + +func splitHostPort(address string) (string, int, error) { + host, port, err := net.SplitHostPort(address) + if err != nil { + return "", 0, err + } + portnum, err := strconv.Atoi(port) + if err != nil { + return "", 0, err + } + if 1 > portnum || portnum > 0xffff { + return "", 0, errors.New("port number out of range " + port) + } + return host, portnum, nil +} diff --git a/vendor/golang.org/x/net/internal/socks/socks.go b/vendor/golang.org/x/net/internal/socks/socks.go new file mode 100644 index 000000000..97db2340e --- /dev/null +++ b/vendor/golang.org/x/net/internal/socks/socks.go @@ -0,0 +1,317 @@ +// Copyright 2018 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package socks provides a SOCKS version 5 client implementation. +// +// SOCKS protocol version 5 is defined in RFC 1928. +// Username/Password authentication for SOCKS version 5 is defined in +// RFC 1929. +package socks + +import ( + "context" + "errors" + "io" + "net" + "strconv" +) + +// A Command represents a SOCKS command. +type Command int + +func (cmd Command) String() string { + switch cmd { + case CmdConnect: + return "socks connect" + case cmdBind: + return "socks bind" + default: + return "socks " + strconv.Itoa(int(cmd)) + } +} + +// An AuthMethod represents a SOCKS authentication method. +type AuthMethod int + +// A Reply represents a SOCKS command reply code. +type Reply int + +func (code Reply) String() string { + switch code { + case StatusSucceeded: + return "succeeded" + case 0x01: + return "general SOCKS server failure" + case 0x02: + return "connection not allowed by ruleset" + case 0x03: + return "network unreachable" + case 0x04: + return "host unreachable" + case 0x05: + return "connection refused" + case 0x06: + return "TTL expired" + case 0x07: + return "command not supported" + case 0x08: + return "address type not supported" + default: + return "unknown code: " + strconv.Itoa(int(code)) + } +} + +// Wire protocol constants. +const ( + Version5 = 0x05 + + AddrTypeIPv4 = 0x01 + AddrTypeFQDN = 0x03 + AddrTypeIPv6 = 0x04 + + CmdConnect Command = 0x01 // establishes an active-open forward proxy connection + cmdBind Command = 0x02 // establishes a passive-open forward proxy connection + + AuthMethodNotRequired AuthMethod = 0x00 // no authentication required + AuthMethodUsernamePassword AuthMethod = 0x02 // use username/password + AuthMethodNoAcceptableMethods AuthMethod = 0xff // no acceptable authentication methods + + StatusSucceeded Reply = 0x00 +) + +// An Addr represents a SOCKS-specific address. +// Either Name or IP is used exclusively. +type Addr struct { + Name string // fully-qualified domain name + IP net.IP + Port int +} + +func (a *Addr) Network() string { return "socks" } + +func (a *Addr) String() string { + if a == nil { + return "" + } + port := strconv.Itoa(a.Port) + if a.IP == nil { + return net.JoinHostPort(a.Name, port) + } + return net.JoinHostPort(a.IP.String(), port) +} + +// A Conn represents a forward proxy connection. +type Conn struct { + net.Conn + + boundAddr net.Addr +} + +// BoundAddr returns the address assigned by the proxy server for +// connecting to the command target address from the proxy server. +func (c *Conn) BoundAddr() net.Addr { + if c == nil { + return nil + } + return c.boundAddr +} + +// A Dialer holds SOCKS-specific options. +type Dialer struct { + cmd Command // either CmdConnect or cmdBind + proxyNetwork string // network between a proxy server and a client + proxyAddress string // proxy server address + + // ProxyDial specifies the optional dial function for + // establishing the transport connection. + ProxyDial func(context.Context, string, string) (net.Conn, error) + + // AuthMethods specifies the list of request authentication + // methods. + // If empty, SOCKS client requests only AuthMethodNotRequired. + AuthMethods []AuthMethod + + // Authenticate specifies the optional authentication + // function. It must be non-nil when AuthMethods is not empty. + // It must return an error when the authentication is failed. + Authenticate func(context.Context, io.ReadWriter, AuthMethod) error +} + +// DialContext connects to the provided address on the provided +// network. +// +// The returned error value may be a net.OpError. When the Op field of +// net.OpError contains "socks", the Source field contains a proxy +// server address and the Addr field contains a command target +// address. +// +// See func Dial of the net package of standard library for a +// description of the network and address parameters. +func (d *Dialer) DialContext(ctx context.Context, network, address string) (net.Conn, error) { + if err := d.validateTarget(network, address); err != nil { + proxy, dst, _ := d.pathAddrs(address) + return nil, &net.OpError{Op: d.cmd.String(), Net: network, Source: proxy, Addr: dst, Err: err} + } + if ctx == nil { + proxy, dst, _ := d.pathAddrs(address) + return nil, &net.OpError{Op: d.cmd.String(), Net: network, Source: proxy, Addr: dst, Err: errors.New("nil context")} + } + var err error + var c net.Conn + if d.ProxyDial != nil { + c, err = d.ProxyDial(ctx, d.proxyNetwork, d.proxyAddress) + } else { + var dd net.Dialer + c, err = dd.DialContext(ctx, d.proxyNetwork, d.proxyAddress) + } + if err != nil { + proxy, dst, _ := d.pathAddrs(address) + return nil, &net.OpError{Op: d.cmd.String(), Net: network, Source: proxy, Addr: dst, Err: err} + } + a, err := d.connect(ctx, c, address) + if err != nil { + c.Close() + proxy, dst, _ := d.pathAddrs(address) + return nil, &net.OpError{Op: d.cmd.String(), Net: network, Source: proxy, Addr: dst, Err: err} + } + return &Conn{Conn: c, boundAddr: a}, nil +} + +// DialWithConn initiates a connection from SOCKS server to the target +// network and address using the connection c that is already +// connected to the SOCKS server. +// +// It returns the connection's local address assigned by the SOCKS +// server. +func (d *Dialer) DialWithConn(ctx context.Context, c net.Conn, network, address string) (net.Addr, error) { + if err := d.validateTarget(network, address); err != nil { + proxy, dst, _ := d.pathAddrs(address) + return nil, &net.OpError{Op: d.cmd.String(), Net: network, Source: proxy, Addr: dst, Err: err} + } + if ctx == nil { + proxy, dst, _ := d.pathAddrs(address) + return nil, &net.OpError{Op: d.cmd.String(), Net: network, Source: proxy, Addr: dst, Err: errors.New("nil context")} + } + a, err := d.connect(ctx, c, address) + if err != nil { + proxy, dst, _ := d.pathAddrs(address) + return nil, &net.OpError{Op: d.cmd.String(), Net: network, Source: proxy, Addr: dst, Err: err} + } + return a, nil +} + +// Dial connects to the provided address on the provided network. +// +// Unlike DialContext, it returns a raw transport connection instead +// of a forward proxy connection. +// +// Deprecated: Use DialContext or DialWithConn instead. +func (d *Dialer) Dial(network, address string) (net.Conn, error) { + if err := d.validateTarget(network, address); err != nil { + proxy, dst, _ := d.pathAddrs(address) + return nil, &net.OpError{Op: d.cmd.String(), Net: network, Source: proxy, Addr: dst, Err: err} + } + var err error + var c net.Conn + if d.ProxyDial != nil { + c, err = d.ProxyDial(context.Background(), d.proxyNetwork, d.proxyAddress) + } else { + c, err = net.Dial(d.proxyNetwork, d.proxyAddress) + } + if err != nil { + proxy, dst, _ := d.pathAddrs(address) + return nil, &net.OpError{Op: d.cmd.String(), Net: network, Source: proxy, Addr: dst, Err: err} + } + if _, err := d.DialWithConn(context.Background(), c, network, address); err != nil { + c.Close() + return nil, err + } + return c, nil +} + +func (d *Dialer) validateTarget(network, address string) error { + switch network { + case "tcp", "tcp6", "tcp4": + default: + return errors.New("network not implemented") + } + switch d.cmd { + case CmdConnect, cmdBind: + default: + return errors.New("command not implemented") + } + return nil +} + +func (d *Dialer) pathAddrs(address string) (proxy, dst net.Addr, err error) { + for i, s := range []string{d.proxyAddress, address} { + host, port, err := splitHostPort(s) + if err != nil { + return nil, nil, err + } + a := &Addr{Port: port} + a.IP = net.ParseIP(host) + if a.IP == nil { + a.Name = host + } + if i == 0 { + proxy = a + } else { + dst = a + } + } + return +} + +// NewDialer returns a new Dialer that dials through the provided +// proxy server's network and address. +func NewDialer(network, address string) *Dialer { + return &Dialer{proxyNetwork: network, proxyAddress: address, cmd: CmdConnect} +} + +const ( + authUsernamePasswordVersion = 0x01 + authStatusSucceeded = 0x00 +) + +// UsernamePassword are the credentials for the username/password +// authentication method. +type UsernamePassword struct { + Username string + Password string +} + +// Authenticate authenticates a pair of username and password with the +// proxy server. +func (up *UsernamePassword) Authenticate(ctx context.Context, rw io.ReadWriter, auth AuthMethod) error { + switch auth { + case AuthMethodNotRequired: + return nil + case AuthMethodUsernamePassword: + if len(up.Username) == 0 || len(up.Username) > 255 || len(up.Password) == 0 || len(up.Password) > 255 { + return errors.New("invalid username/password") + } + b := []byte{authUsernamePasswordVersion} + b = append(b, byte(len(up.Username))) + b = append(b, up.Username...) + b = append(b, byte(len(up.Password))) + b = append(b, up.Password...) + // TODO(mikio): handle IO deadlines and cancelation if + // necessary + if _, err := rw.Write(b); err != nil { + return err + } + if _, err := io.ReadFull(rw, b[:2]); err != nil { + return err + } + if b[0] != authUsernamePasswordVersion { + return errors.New("invalid username/password version") + } + if b[1] != authStatusSucceeded { + return errors.New("username/password authentication failed") + } + return nil + } + return errors.New("unsupported authentication method " + strconv.Itoa(int(auth))) +} diff --git a/vendor/golang.org/x/net/proxy/dial.go b/vendor/golang.org/x/net/proxy/dial.go new file mode 100644 index 000000000..811c2e4e9 --- /dev/null +++ b/vendor/golang.org/x/net/proxy/dial.go @@ -0,0 +1,54 @@ +// Copyright 2019 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package proxy + +import ( + "context" + "net" +) + +// A ContextDialer dials using a context. +type ContextDialer interface { + DialContext(ctx context.Context, network, address string) (net.Conn, error) +} + +// Dial works like DialContext on net.Dialer but using a dialer returned by FromEnvironment. +// +// The passed ctx is only used for returning the Conn, not the lifetime of the Conn. +// +// Custom dialers (registered via RegisterDialerType) that do not implement ContextDialer +// can leak a goroutine for as long as it takes the underlying Dialer implementation to timeout. +// +// A Conn returned from a successful Dial after the context has been cancelled will be immediately closed. +func Dial(ctx context.Context, network, address string) (net.Conn, error) { + d := FromEnvironment() + if xd, ok := d.(ContextDialer); ok { + return xd.DialContext(ctx, network, address) + } + return dialContext(ctx, d, network, address) +} + +// WARNING: this can leak a goroutine for as long as the underlying Dialer implementation takes to timeout +// A Conn returned from a successful Dial after the context has been cancelled will be immediately closed. +func dialContext(ctx context.Context, d Dialer, network, address string) (net.Conn, error) { + var ( + conn net.Conn + done = make(chan struct{}, 1) + err error + ) + go func() { + conn, err = d.Dial(network, address) + close(done) + if conn != nil && ctx.Err() != nil { + conn.Close() + } + }() + select { + case <-ctx.Done(): + err = ctx.Err() + case <-done: + } + return conn, err +} diff --git a/vendor/golang.org/x/net/proxy/direct.go b/vendor/golang.org/x/net/proxy/direct.go new file mode 100644 index 000000000..3d66bdef9 --- /dev/null +++ b/vendor/golang.org/x/net/proxy/direct.go @@ -0,0 +1,31 @@ +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package proxy + +import ( + "context" + "net" +) + +type direct struct{} + +// Direct implements Dialer by making network connections directly using net.Dial or net.DialContext. +var Direct = direct{} + +var ( + _ Dialer = Direct + _ ContextDialer = Direct +) + +// Dial directly invokes net.Dial with the supplied parameters. +func (direct) Dial(network, addr string) (net.Conn, error) { + return net.Dial(network, addr) +} + +// DialContext instantiates a net.Dialer and invokes its DialContext receiver with the supplied parameters. +func (direct) DialContext(ctx context.Context, network, addr string) (net.Conn, error) { + var d net.Dialer + return d.DialContext(ctx, network, addr) +} diff --git a/vendor/golang.org/x/net/proxy/per_host.go b/vendor/golang.org/x/net/proxy/per_host.go new file mode 100644 index 000000000..573fe79e8 --- /dev/null +++ b/vendor/golang.org/x/net/proxy/per_host.go @@ -0,0 +1,155 @@ +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package proxy + +import ( + "context" + "net" + "strings" +) + +// A PerHost directs connections to a default Dialer unless the host name +// requested matches one of a number of exceptions. +type PerHost struct { + def, bypass Dialer + + bypassNetworks []*net.IPNet + bypassIPs []net.IP + bypassZones []string + bypassHosts []string +} + +// NewPerHost returns a PerHost Dialer that directs connections to either +// defaultDialer or bypass, depending on whether the connection matches one of +// the configured rules. +func NewPerHost(defaultDialer, bypass Dialer) *PerHost { + return &PerHost{ + def: defaultDialer, + bypass: bypass, + } +} + +// Dial connects to the address addr on the given network through either +// defaultDialer or bypass. +func (p *PerHost) Dial(network, addr string) (c net.Conn, err error) { + host, _, err := net.SplitHostPort(addr) + if err != nil { + return nil, err + } + + return p.dialerForRequest(host).Dial(network, addr) +} + +// DialContext connects to the address addr on the given network through either +// defaultDialer or bypass. +func (p *PerHost) DialContext(ctx context.Context, network, addr string) (c net.Conn, err error) { + host, _, err := net.SplitHostPort(addr) + if err != nil { + return nil, err + } + d := p.dialerForRequest(host) + if x, ok := d.(ContextDialer); ok { + return x.DialContext(ctx, network, addr) + } + return dialContext(ctx, d, network, addr) +} + +func (p *PerHost) dialerForRequest(host string) Dialer { + if ip := net.ParseIP(host); ip != nil { + for _, net := range p.bypassNetworks { + if net.Contains(ip) { + return p.bypass + } + } + for _, bypassIP := range p.bypassIPs { + if bypassIP.Equal(ip) { + return p.bypass + } + } + return p.def + } + + for _, zone := range p.bypassZones { + if strings.HasSuffix(host, zone) { + return p.bypass + } + if host == zone[1:] { + // For a zone ".example.com", we match "example.com" + // too. + return p.bypass + } + } + for _, bypassHost := range p.bypassHosts { + if bypassHost == host { + return p.bypass + } + } + return p.def +} + +// AddFromString parses a string that contains comma-separated values +// specifying hosts that should use the bypass proxy. Each value is either an +// IP address, a CIDR range, a zone (*.example.com) or a host name +// (localhost). A best effort is made to parse the string and errors are +// ignored. +func (p *PerHost) AddFromString(s string) { + hosts := strings.Split(s, ",") + for _, host := range hosts { + host = strings.TrimSpace(host) + if len(host) == 0 { + continue + } + if strings.Contains(host, "/") { + // We assume that it's a CIDR address like 127.0.0.0/8 + if _, net, err := net.ParseCIDR(host); err == nil { + p.AddNetwork(net) + } + continue + } + if ip := net.ParseIP(host); ip != nil { + p.AddIP(ip) + continue + } + if strings.HasPrefix(host, "*.") { + p.AddZone(host[1:]) + continue + } + p.AddHost(host) + } +} + +// AddIP specifies an IP address that will use the bypass proxy. Note that +// this will only take effect if a literal IP address is dialed. A connection +// to a named host will never match an IP. +func (p *PerHost) AddIP(ip net.IP) { + p.bypassIPs = append(p.bypassIPs, ip) +} + +// AddNetwork specifies an IP range that will use the bypass proxy. Note that +// this will only take effect if a literal IP address is dialed. A connection +// to a named host will never match. +func (p *PerHost) AddNetwork(net *net.IPNet) { + p.bypassNetworks = append(p.bypassNetworks, net) +} + +// AddZone specifies a DNS suffix that will use the bypass proxy. A zone of +// "example.com" matches "example.com" and all of its subdomains. +func (p *PerHost) AddZone(zone string) { + if strings.HasSuffix(zone, ".") { + zone = zone[:len(zone)-1] + } + if !strings.HasPrefix(zone, ".") { + zone = "." + zone + } + p.bypassZones = append(p.bypassZones, zone) +} + +// AddHost specifies a host name that will use the bypass proxy. +func (p *PerHost) AddHost(host string) { + if strings.HasSuffix(host, ".") { + host = host[:len(host)-1] + } + p.bypassHosts = append(p.bypassHosts, host) +} diff --git a/vendor/golang.org/x/net/proxy/proxy.go b/vendor/golang.org/x/net/proxy/proxy.go new file mode 100644 index 000000000..9ff4b9a77 --- /dev/null +++ b/vendor/golang.org/x/net/proxy/proxy.go @@ -0,0 +1,149 @@ +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package proxy provides support for a variety of protocols to proxy network +// data. +package proxy // import "golang.org/x/net/proxy" + +import ( + "errors" + "net" + "net/url" + "os" + "sync" +) + +// A Dialer is a means to establish a connection. +// Custom dialers should also implement ContextDialer. +type Dialer interface { + // Dial connects to the given address via the proxy. + Dial(network, addr string) (c net.Conn, err error) +} + +// Auth contains authentication parameters that specific Dialers may require. +type Auth struct { + User, Password string +} + +// FromEnvironment returns the dialer specified by the proxy-related +// variables in the environment and makes underlying connections +// directly. +func FromEnvironment() Dialer { + return FromEnvironmentUsing(Direct) +} + +// FromEnvironmentUsing returns the dialer specify by the proxy-related +// variables in the environment and makes underlying connections +// using the provided forwarding Dialer (for instance, a *net.Dialer +// with desired configuration). +func FromEnvironmentUsing(forward Dialer) Dialer { + allProxy := allProxyEnv.Get() + if len(allProxy) == 0 { + return forward + } + + proxyURL, err := url.Parse(allProxy) + if err != nil { + return forward + } + proxy, err := FromURL(proxyURL, forward) + if err != nil { + return forward + } + + noProxy := noProxyEnv.Get() + if len(noProxy) == 0 { + return proxy + } + + perHost := NewPerHost(proxy, forward) + perHost.AddFromString(noProxy) + return perHost +} + +// proxySchemes is a map from URL schemes to a function that creates a Dialer +// from a URL with such a scheme. +var proxySchemes map[string]func(*url.URL, Dialer) (Dialer, error) + +// RegisterDialerType takes a URL scheme and a function to generate Dialers from +// a URL with that scheme and a forwarding Dialer. Registered schemes are used +// by FromURL. +func RegisterDialerType(scheme string, f func(*url.URL, Dialer) (Dialer, error)) { + if proxySchemes == nil { + proxySchemes = make(map[string]func(*url.URL, Dialer) (Dialer, error)) + } + proxySchemes[scheme] = f +} + +// FromURL returns a Dialer given a URL specification and an underlying +// Dialer for it to make network requests. +func FromURL(u *url.URL, forward Dialer) (Dialer, error) { + var auth *Auth + if u.User != nil { + auth = new(Auth) + auth.User = u.User.Username() + if p, ok := u.User.Password(); ok { + auth.Password = p + } + } + + switch u.Scheme { + case "socks5", "socks5h": + addr := u.Hostname() + port := u.Port() + if port == "" { + port = "1080" + } + return SOCKS5("tcp", net.JoinHostPort(addr, port), auth, forward) + } + + // If the scheme doesn't match any of the built-in schemes, see if it + // was registered by another package. + if proxySchemes != nil { + if f, ok := proxySchemes[u.Scheme]; ok { + return f(u, forward) + } + } + + return nil, errors.New("proxy: unknown scheme: " + u.Scheme) +} + +var ( + allProxyEnv = &envOnce{ + names: []string{"ALL_PROXY", "all_proxy"}, + } + noProxyEnv = &envOnce{ + names: []string{"NO_PROXY", "no_proxy"}, + } +) + +// envOnce looks up an environment variable (optionally by multiple +// names) once. It mitigates expensive lookups on some platforms +// (e.g. Windows). +// (Borrowed from net/http/transport.go) +type envOnce struct { + names []string + once sync.Once + val string +} + +func (e *envOnce) Get() string { + e.once.Do(e.init) + return e.val +} + +func (e *envOnce) init() { + for _, n := range e.names { + e.val = os.Getenv(n) + if e.val != "" { + return + } + } +} + +// reset is used by tests +func (e *envOnce) reset() { + e.once = sync.Once{} + e.val = "" +} diff --git a/vendor/golang.org/x/net/proxy/socks5.go b/vendor/golang.org/x/net/proxy/socks5.go new file mode 100644 index 000000000..c91651f96 --- /dev/null +++ b/vendor/golang.org/x/net/proxy/socks5.go @@ -0,0 +1,42 @@ +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package proxy + +import ( + "context" + "net" + + "golang.org/x/net/internal/socks" +) + +// SOCKS5 returns a Dialer that makes SOCKSv5 connections to the given +// address with an optional username and password. +// See RFC 1928 and RFC 1929. +func SOCKS5(network, address string, auth *Auth, forward Dialer) (Dialer, error) { + d := socks.NewDialer(network, address) + if forward != nil { + if f, ok := forward.(ContextDialer); ok { + d.ProxyDial = func(ctx context.Context, network string, address string) (net.Conn, error) { + return f.DialContext(ctx, network, address) + } + } else { + d.ProxyDial = func(ctx context.Context, network string, address string) (net.Conn, error) { + return dialContext(ctx, forward, network, address) + } + } + } + if auth != nil { + up := socks.UsernamePassword{ + Username: auth.User, + Password: auth.Password, + } + d.AuthMethods = []socks.AuthMethod{ + socks.AuthMethodNotRequired, + socks.AuthMethodUsernamePassword, + } + d.Authenticate = up.Authenticate + } + return d, nil +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 914727ba1..a4bbf6173 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -247,7 +247,9 @@ golang.org/x/net/http/httpguts golang.org/x/net/http2 golang.org/x/net/http2/hpack golang.org/x/net/idna +golang.org/x/net/internal/socks golang.org/x/net/internal/timeseries +golang.org/x/net/proxy golang.org/x/net/trace # golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602 ## explicit From fc2240fb22ebbd055ac307bd0742211ec9252a0a Mon Sep 17 00:00:00 2001 From: Aliaksandr Valialkin Date: Sun, 4 Apr 2021 01:41:52 +0300 Subject: [PATCH 38/63] docs/CHANGELOG.md: document the ability to use socks5 proxy Follow-up for a4c6a3b3e191e70e7e8d8d43cc98e0fcfa2b32a5 Updates https://github.com/VictoriaMetrics/VictoriaMetrics/issues/1177 --- docs/CHANGELOG.md | 1 + go.sum | 6 ------ 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 09a0d8bc0..8589eba8a 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -8,6 +8,7 @@ * FEATURE: update Go builder from `v1.16.2` to `v1.16.3`. This should fix [these issues](https://github.com/golang/go/issues?q=milestone%3AGo1.16.3+label%3ACherryPickApproved). * FEATURE: vmagent: add support for `follow_redirects` option to `scrape_configs` section in the same way as [Prometheus 2.26 does](https://github.com/prometheus/prometheus/pull/8546). * FEATURE: vmagent: add support for `authorization` section in `-promscrape.config` in the same way as [Prometheus 2.26 does](https://github.com/prometheus/prometheus/pull/8512). +* FEATURE: vmagent: add support for socks5 proxy in `proxy_url` config option. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/1177). * FEATURE: vmagent: reduce memory usage when `-remoteWrite.queues` is set to a big value. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/1167). * FEATURE: vmagent: add AWS IAM roles for tasks support for EC2 service discovery according to [these docs](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-iam-roles.html). * FEATURE: vmagent: add support for `proxy_tls_config`, `proxy_authorization`, `proxy_basic_auth`, `proxy_bearer_token` and `proxy_bearer_token_file` options to `consul_sd_config`, `dockerswarm_sd_config` and `eureka_sd_config` sections. diff --git a/go.sum b/go.sum index 339216818..e59909cf6 100644 --- a/go.sum +++ b/go.sum @@ -925,8 +925,6 @@ golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= -golang.org/x/net v0.0.0-20210324205630-d1beb07c2056 h1:sANdAef76Ioam9aQUUdcAqricwY/WUaMc4+7LY4eGg8= -golang.org/x/net v0.0.0-20210324205630-d1beb07c2056/go.mod h1:uSPa2vr4CLtc/ILN5odXGNXS6mhrKVzTaCXzk9m6W3k= golang.org/x/net v0.0.0-20210331212208-0fccb6fa2b5c h1:KHUzaHIpjWVlVVNh65G3hhuj3KB1HnjY6Cq5cTvRQT8= golang.org/x/net v0.0.0-20210331212208-0fccb6fa2b5c/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -940,7 +938,6 @@ golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210323180902-22b0adad7558/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602 h1:0Ja1LBD+yisY6RWM/BH7TJVXWsSjs2VwBSmvSX4HdBc= golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -1026,9 +1023,6 @@ golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210309074719-68d13333faf2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210324051608-47abb6519492 h1:Paq34FxTluEPvVyayQqMPgHm+vTOrIifmcYxFBx9TLg= -golang.org/x/sys v0.0.0-20210324051608-47abb6519492/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44 h1:Bli41pIlzTzf3KEY06n+xnzK/BESIg2ze4Pgfh/aI8c= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210331175145-43e1dd70ce54 h1:rF3Ohx8DRyl8h2zw9qojyLHLhrJpEMgyPOImREEryf0= From 9a97941e2ae95f54d5fc93e6867294e1ed38a30c Mon Sep 17 00:00:00 2001 From: Aliaksandr Valialkin Date: Sun, 4 Apr 2021 01:45:34 +0300 Subject: [PATCH 39/63] docs/vmagent.md: mention that vmagent supports scraping via socks5 proxy --- app/vmagent/README.md | 2 +- docs/vmagent.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/vmagent/README.md b/app/vmagent/README.md index 0b8789652..7d1f41fe1 100644 --- a/app/vmagent/README.md +++ b/app/vmagent/README.md @@ -262,7 +262,7 @@ See [these docs](https://victoriametrics.github.io/#deduplication) for details. ## Scraping targets via a proxy -`vmagent` supports scraping targets via http and https proxies. Proxy address must be specified in `proxy_url` option. For example, the following scrape config instructs +`vmagent` supports scraping targets via http, https and socks5 proxies. Proxy address must be specified in `proxy_url` option. For example, the following scrape config instructs target scraping via https proxy at `https://proxy-addr:1234`: ```yml diff --git a/docs/vmagent.md b/docs/vmagent.md index 0b8789652..7d1f41fe1 100644 --- a/docs/vmagent.md +++ b/docs/vmagent.md @@ -262,7 +262,7 @@ See [these docs](https://victoriametrics.github.io/#deduplication) for details. ## Scraping targets via a proxy -`vmagent` supports scraping targets via http and https proxies. Proxy address must be specified in `proxy_url` option. For example, the following scrape config instructs +`vmagent` supports scraping targets via http, https and socks5 proxies. Proxy address must be specified in `proxy_url` option. For example, the following scrape config instructs target scraping via https proxy at `https://proxy-addr:1234`: ```yml From 9ff3ecb991311cd5c9fa3f6bb698ff23ea50610f Mon Sep 17 00:00:00 2001 From: Aliaksandr Valialkin Date: Sun, 4 Apr 2021 01:59:07 +0300 Subject: [PATCH 40/63] docs/CHANGELOG.md: explain why `-sortLabels` is set to false by default --- docs/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 8589eba8a..4a0dd2950 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -2,7 +2,7 @@ # tip -* FEATURE: vminsert and vmagent: add `-sortLabels` command-line flag for sorting metric labels before pushing them to `vmstorage`. This should reduce the size of `MetricName -> internal_series_id` cache (aka `vm_cache_size_bytes{type="storage/tsid"}`) when ingesting samples for the same time series with distinct order of labels. For example, `foo{k1="v1",k2="v2"}` and `foo{k2="v2",k1="v1"}` represent a single time series. +* FEATURE: vminsert and vmagent: add `-sortLabels` command-line flag for sorting metric labels before pushing them to `vmstorage`. This should reduce the size of `MetricName -> internal_series_id` cache (aka `vm_cache_size_bytes{type="storage/tsid"}`) when ingesting samples for the same time series with distinct order of labels. For example, `foo{k1="v1",k2="v2"}` and `foo{k2="v2",k1="v1"}` represent a single time series. Labels sorting is disabled by default, since the majority of established exporters preserve the order of labels for the exported metrics. * FEATURE: allow specifying label value alongside label name for the `others sum` time series returned from `topk_*` and `bottomk_*` functions from [MetricsQL](https://victoriametrics.github.io/MetricsQL.html). For example, `topk_avg(3, max(process_resident_memory_bytes) by (instance), "instance=other_sum")` would return top 3 series from `max(process_resident_memory_bytes) by (instance)` plus a series containing of the sum of other series. The `others sum` series will have `{instance="other_sum"}` label. * FEATURE: do not delete `dst_label` when applying `label_copy(q, "src_label", "dst_label")` and `label_move(q, "src_label", "dst_label")` to series without `src_label` and with non-empty `dst_label`. See more details at [MetricsQL docs](https://victoriametrics.github.io/MetricsQL.html). * FEATURE: update Go builder from `v1.16.2` to `v1.16.3`. This should fix [these issues](https://github.com/golang/go/issues?q=milestone%3AGo1.16.3+label%3ACherryPickApproved). From 6742839fd636b416f89a72a9e1f91107be3f6bb3 Mon Sep 17 00:00:00 2001 From: Aliaksandr Valialkin Date: Mon, 5 Apr 2021 12:15:07 +0300 Subject: [PATCH 41/63] lib/promscrape: pass `X-Prometheus-Scrape-Timeout-Seconds` header to scrape targets as Prometheus does --- docs/CHANGELOG.md | 1 + lib/promscrape/client.go | 44 ++++++++++++++++++++++++---------------- 2 files changed, 27 insertions(+), 18 deletions(-) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 4a0dd2950..fff696ae7 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -12,6 +12,7 @@ * FEATURE: vmagent: reduce memory usage when `-remoteWrite.queues` is set to a big value. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/1167). * FEATURE: vmagent: add AWS IAM roles for tasks support for EC2 service discovery according to [these docs](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-iam-roles.html). * FEATURE: vmagent: add support for `proxy_tls_config`, `proxy_authorization`, `proxy_basic_auth`, `proxy_bearer_token` and `proxy_bearer_token_file` options to `consul_sd_config`, `dockerswarm_sd_config` and `eureka_sd_config` sections. +* FEATURE: vmagent: pass `X-Prometheus-Scrape-Timeout-Seconds` header to scrape targets as Prometheus does. In this case scrape targets can limit the time needed for performing the scrape. See [this comment](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/1179#issuecomment-813118733) for details. * FEATURE: vmauth: add support for authorization via [bearer token](https://swagger.io/docs/specification/authentication/bearer-authentication/). See [the docs](https://victoriametrics.github.io/vmauth.html#auth-config) for details. * BUGFIX: vmagent: properly work with simple HTTP proxies which don't support `CONNECT` method. For example, [PushProx](https://github.com/prometheus-community/PushProx). See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/1179). diff --git a/lib/promscrape/client.go b/lib/promscrape/client.go index 565e6915c..e023a95d3 100644 --- a/lib/promscrape/client.go +++ b/lib/promscrape/client.go @@ -43,14 +43,15 @@ type client struct { // It may be useful for scraping targets with millions of metrics per target. sc *http.Client - scrapeURL string - host string - requestURI string - authHeader string - proxyAuthHeader string - denyRedirects bool - disableCompression bool - disableKeepAlive bool + scrapeURL string + scrapeTimeoutSecondsStr string + host string + requestURI string + authHeader string + proxyAuthHeader string + denyRedirects bool + disableCompression bool + disableKeepAlive bool } func newClient(sw *ScrapeWork) *client { @@ -127,16 +128,17 @@ func newClient(sw *ScrapeWork) *client { } } return &client{ - hc: hc, - sc: sc, - scrapeURL: sw.ScrapeURL, - host: host, - requestURI: requestURI, - authHeader: sw.AuthConfig.Authorization, - proxyAuthHeader: proxyAuthHeader, - denyRedirects: sw.DenyRedirects, - disableCompression: sw.DisableCompression, - disableKeepAlive: sw.DisableKeepAlive, + hc: hc, + sc: sc, + scrapeURL: sw.ScrapeURL, + scrapeTimeoutSecondsStr: fmt.Sprintf("%.3f", sw.ScrapeTimeout.Seconds()), + host: host, + requestURI: requestURI, + authHeader: sw.AuthConfig.Authorization, + proxyAuthHeader: proxyAuthHeader, + denyRedirects: sw.DenyRedirects, + disableCompression: sw.DisableCompression, + disableKeepAlive: sw.DisableKeepAlive, } } @@ -154,6 +156,9 @@ func (c *client) GetStreamReader() (*streamReader, error) { // See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/608 for details. // Do not bloat the `Accept` header with OpenMetrics shit, since it looks like dead standard now. req.Header.Set("Accept", "text/plain;version=0.0.4;q=1,*/*;q=0.1") + // Set X-Prometheus-Scrape-Timeout-Seconds like Prometheus does, since it is used by some exporters such as PushProx. + // See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/1179#issuecomment-813117162 + req.Header.Set("X-Prometheus-Scrape-Timeout-Seconds", c.scrapeTimeoutSecondsStr) if c.authHeader != "" { req.Header.Set("Authorization", c.authHeader) } @@ -191,6 +196,9 @@ func (c *client) ReadData(dst []byte) ([]byte, error) { // See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/608 for details. // Do not bloat the `Accept` header with OpenMetrics shit, since it looks like dead standard now. req.Header.Set("Accept", "text/plain;version=0.0.4;q=1,*/*;q=0.1") + // Set X-Prometheus-Scrape-Timeout-Seconds like Prometheus does, since it is used by some exporters such as PushProx. + // See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/1179#issuecomment-813117162 + req.Header.Set("X-Prometheus-Scrape-Timeout-Seconds", c.scrapeTimeoutSecondsStr) if c.authHeader != "" { req.Header.Set("Authorization", c.authHeader) } From a5c5b54c228f3f5652483b43459ecc57d572ab88 Mon Sep 17 00:00:00 2001 From: Aliaksandr Valialkin Date: Mon, 5 Apr 2021 12:56:51 +0300 Subject: [PATCH 42/63] lib/proxy: add support for `socks5 over tls` proxy --- docs/CHANGELOG.md | 1 + lib/proxy/proxy.go | 33 +++++++++++++++++++++------------ 2 files changed, 22 insertions(+), 12 deletions(-) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index fff696ae7..289dda00c 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -9,6 +9,7 @@ * FEATURE: vmagent: add support for `follow_redirects` option to `scrape_configs` section in the same way as [Prometheus 2.26 does](https://github.com/prometheus/prometheus/pull/8546). * FEATURE: vmagent: add support for `authorization` section in `-promscrape.config` in the same way as [Prometheus 2.26 does](https://github.com/prometheus/prometheus/pull/8512). * FEATURE: vmagent: add support for socks5 proxy in `proxy_url` config option. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/1177). +* FEATURE: vmagent: add support for `socks5 over tls` proxy in `proxy_url` config option. It can be set up with the following config: `proxy_url: tls+socks5://proxy-addr:port`. * FEATURE: vmagent: reduce memory usage when `-remoteWrite.queues` is set to a big value. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/1167). * FEATURE: vmagent: add AWS IAM roles for tasks support for EC2 service discovery according to [these docs](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-iam-roles.html). * FEATURE: vmagent: add support for `proxy_tls_config`, `proxy_authorization`, `proxy_basic_auth`, `proxy_bearer_token` and `proxy_bearer_token_file` options to `consul_sd_config`, `dockerswarm_sd_config` and `eureka_sd_config` sections. diff --git a/lib/proxy/proxy.go b/lib/proxy/proxy.go index 087d35fb0..ebf476b60 100644 --- a/lib/proxy/proxy.go +++ b/lib/proxy/proxy.go @@ -105,18 +105,13 @@ func (u *URL) NewDialFunc(ac *promauth.Config) (fasthttp.DialFunc, error) { return defaultDialFunc, nil } pu := u.url - if pu.Scheme != "http" && pu.Scheme != "https" && pu.Scheme != "socks5" { - return nil, fmt.Errorf("unknown scheme=%q for proxy_url=%q, must be http, https or socks5", pu.Scheme, pu.Redacted()) + switch pu.Scheme { + case "http", "https", "socks5", "tls+socks5": + default: + return nil, fmt.Errorf("unknown scheme=%q for proxy_url=%q, must be http, https, socks5 or tls+socks5", pu.Scheme, pu.Redacted()) } - isTLS := pu.Scheme == "https" + isTLS := (pu.Scheme == "https" || pu.Scheme == "tls+socks5") proxyAddr := addMissingPort(pu.Host, isTLS) - if pu.Scheme == "socks5" { - return socks5DialFunc(proxyAddr, pu) - } - authHeader := u.GetAuthHeader(ac) - if authHeader != "" { - authHeader = "Proxy-Authorization: " + authHeader + "\r\n" - } var tlsCfg *tls.Config if isTLS { tlsCfg = ac.NewTLSConfig() @@ -124,6 +119,13 @@ func (u *URL) NewDialFunc(ac *promauth.Config) (fasthttp.DialFunc, error) { tlsCfg.ServerName = tlsServerName(proxyAddr) } } + if pu.Scheme == "socks5" || pu.Scheme == "tls+socks5" { + return socks5DialFunc(proxyAddr, pu, tlsCfg) + } + authHeader := u.GetAuthHeader(ac) + if authHeader != "" { + authHeader = "Proxy-Authorization: " + authHeader + "\r\n" + } dialFunc := func(addr string) (net.Conn, error) { proxyConn, err := defaultDialFunc(proxyAddr) if err != nil { @@ -142,7 +144,7 @@ func (u *URL) NewDialFunc(ac *promauth.Config) (fasthttp.DialFunc, error) { return dialFunc, nil } -func socks5DialFunc(proxyAddr string, pu *url.URL) (fasthttp.DialFunc, error) { +func socks5DialFunc(proxyAddr string, pu *url.URL, tlsCfg *tls.Config) (fasthttp.DialFunc, error) { var sac *proxy.Auth if pu.User != nil { username := pu.User.Username() @@ -153,7 +155,14 @@ func socks5DialFunc(proxyAddr string, pu *url.URL) (fasthttp.DialFunc, error) { } } network := netutil.GetTCPNetwork() - d, err := proxy.SOCKS5(network, proxyAddr, sac, proxy.Direct) + var dialer proxy.Dialer = proxy.Direct + if tlsCfg != nil { + dialer = &tls.Dialer{ + NetDialer: proxy.Direct, + Config: tls.Config, + } + } + d, err := proxy.SOCKS5(network, proxyAddr, sac, dialer) if err != nil { return nil, fmt.Errorf("cannot create socks5 proxy for url: %s, err: %w", pu.Redacted(), err) } From 2c25b0322b8272d7bfe17c1254e215a67c92ca7b Mon Sep 17 00:00:00 2001 From: Aliaksandr Valialkin Date: Mon, 5 Apr 2021 13:45:24 +0300 Subject: [PATCH 43/63] lib/proxy: typo fix after a5c5b54c228f3f5652483b43459ecc57d572ab88 --- lib/proxy/proxy.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/proxy/proxy.go b/lib/proxy/proxy.go index ebf476b60..c792b6ae2 100644 --- a/lib/proxy/proxy.go +++ b/lib/proxy/proxy.go @@ -158,8 +158,7 @@ func socks5DialFunc(proxyAddr string, pu *url.URL, tlsCfg *tls.Config) (fasthttp var dialer proxy.Dialer = proxy.Direct if tlsCfg != nil { dialer = &tls.Dialer{ - NetDialer: proxy.Direct, - Config: tls.Config, + Config: tlsCfg, } } d, err := proxy.SOCKS5(network, proxyAddr, sac, dialer) From f010d773d68250450db1a3f61398cc54e76f70c4 Mon Sep 17 00:00:00 2001 From: Aliaksandr Valialkin Date: Mon, 5 Apr 2021 13:53:26 +0300 Subject: [PATCH 44/63] lib/promscrape/discovery/kubernetes: synchronously load Kubernetes objects on first access Remove async registration of apiWatchers, since it breaks discovering `role: endpoints` and `role: endpointslices` targets, which depend on pod and service objects. There is no need in reloading `endpoints` and `endpointslices` targets if the referenced `pod` or `service` objects change, since in this case the corresponding `endpoints` and `endpointslices` objects should also change because they contain ResourceVersion of the referenced `pod` or `service` objects, which is modified on object update. Updates https://github.com/VictoriaMetrics/VictoriaMetrics/issues/1182 --- docs/CHANGELOG.md | 3 +- .../discovery/kubernetes/api_watcher.go | 115 +++++++----------- lib/promscrape/scraper.go | 19 +-- 3 files changed, 52 insertions(+), 85 deletions(-) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 289dda00c..75f1096bd 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -12,12 +12,13 @@ * FEATURE: vmagent: add support for `socks5 over tls` proxy in `proxy_url` config option. It can be set up with the following config: `proxy_url: tls+socks5://proxy-addr:port`. * FEATURE: vmagent: reduce memory usage when `-remoteWrite.queues` is set to a big value. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/1167). * FEATURE: vmagent: add AWS IAM roles for tasks support for EC2 service discovery according to [these docs](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-iam-roles.html). -* FEATURE: vmagent: add support for `proxy_tls_config`, `proxy_authorization`, `proxy_basic_auth`, `proxy_bearer_token` and `proxy_bearer_token_file` options to `consul_sd_config`, `dockerswarm_sd_config` and `eureka_sd_config` sections. +* FEATURE: vmagent: add support for `proxy_tls_config`, `proxy_authorization`, `proxy_basic_auth`, `proxy_bearer_token` and `proxy_bearer_token_file` options in `consul_sd_config`, `dockerswarm_sd_config` and `eureka_sd_config` sections. * FEATURE: vmagent: pass `X-Prometheus-Scrape-Timeout-Seconds` header to scrape targets as Prometheus does. In this case scrape targets can limit the time needed for performing the scrape. See [this comment](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/1179#issuecomment-813118733) for details. * FEATURE: vmauth: add support for authorization via [bearer token](https://swagger.io/docs/specification/authentication/bearer-authentication/). See [the docs](https://victoriametrics.github.io/vmauth.html#auth-config) for details. * BUGFIX: vmagent: properly work with simple HTTP proxies which don't support `CONNECT` method. For example, [PushProx](https://github.com/prometheus-community/PushProx). See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/1179). * BUGFIX: vmagent: properly discover targets if multiple namespace selectors are put inside `kubernetes_sd_config`. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/1170). +* BUGFIX: vmagent: properly discover `role: endpoints` and `role: endpointslices` targets in `kubernetes_sd_config`. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/1182). * BUGFIX: properly generate filename for `*.tar.gz` archive inside `_checksums.txt` file posted at [releases page](https://github.com/VictoriaMetrics/VictoriaMetrics/releases). See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/1171). diff --git a/lib/promscrape/discovery/kubernetes/api_watcher.go b/lib/promscrape/discovery/kubernetes/api_watcher.go index e9f096e72..eb27c1040 100644 --- a/lib/promscrape/discovery/kubernetes/api_watcher.go +++ b/lib/promscrape/discovery/kubernetes/api_watcher.go @@ -87,7 +87,7 @@ func (aw *apiWatcher) reloadScrapeWorks(namespace string, swosByKey map[string][ } func (aw *apiWatcher) setScrapeWorks(namespace, key string, labels []map[string]string) { - swos := getScrapeWorkObjectsForLabels(aw.swcFunc, labels) + swos := aw.getScrapeWorkObjectsForLabels(labels) aw.swosByNamespaceLock.Lock() swosByKey := aw.swosByNamespace[namespace] if swosByKey == nil { @@ -114,10 +114,10 @@ func (aw *apiWatcher) removeScrapeWorks(namespace, key string) { aw.swosByNamespaceLock.Unlock() } -func getScrapeWorkObjectsForLabels(swcFunc ScrapeWorkConstructorFunc, labelss []map[string]string) []interface{} { +func (aw *apiWatcher) getScrapeWorkObjectsForLabels(labelss []map[string]string) []interface{} { swos := make([]interface{}, 0, len(labelss)) for _, labels := range labelss { - swo := swcFunc(labels) + swo := aw.swcFunc(labels) // The reflect check is needed because of https://mangatmodi.medium.com/go-check-nil-interface-the-right-way-d142776edef1 if swo != nil && !reflect.ValueOf(swo).IsNil() { swos = append(swos, swo) @@ -253,34 +253,17 @@ func (gw *groupWatcher) startWatchersForRole(role string, aw *apiWatcher) { apiURL := gw.apiServer + path gw.mu.Lock() uw := gw.m[apiURL] - if uw == nil { + needStart := uw == nil + if needStart { uw = newURLWatcher(role, namespaces[i], apiURL, gw) gw.m[apiURL] = uw } gw.mu.Unlock() - uw.subscribeAPIWatcher(aw) - } -} - -func (gw *groupWatcher) reloadScrapeWorksForAPIWatchers(namespace string, aws []*apiWatcher, objectsByKey map[string]object) { - if len(aws) == 0 { - return - } - swosByKey := make([]map[string][]interface{}, len(aws)) - for i := range aws { - swosByKey[i] = make(map[string][]interface{}) - } - for key, o := range objectsByKey { - labels := o.getTargetLabels(gw) - for i, aw := range aws { - swos := getScrapeWorkObjectsForLabels(aw.swcFunc, labels) - if len(swos) > 0 { - swosByKey[i][key] = swos - } + if needStart { + uw.reloadObjects() + go uw.watchForUpdates() } - } - for i, aw := range aws { - aw.reloadScrapeWorks(namespace, swosByKey[i]) + uw.subscribeAPIWatcher(aw) } } @@ -314,15 +297,12 @@ type urlWatcher struct { parseObject parseObjectFunc parseObjectList parseObjectListFunc - // mu protects aws, awsPending, objectsByKey and resourceVersion + // mu protects aws, objectsByKey and resourceVersion mu sync.Mutex // aws contains registered apiWatcher objects aws map[*apiWatcher]struct{} - // awsPending contains pending apiWatcher objects, which must be moved to aws in a batch - awsPending map[*apiWatcher]struct{} - // objectsByKey contains the latest state for objects obtained from apiURL objectsByKey map[string]object @@ -348,7 +328,6 @@ func newURLWatcher(role, namespace, apiURL string, gw *groupWatcher) *urlWatcher parseObjectList: parseObjectList, aws: make(map[*apiWatcher]struct{}), - awsPending: make(map[*apiWatcher]struct{}), objectsByKey: make(map[string]object), objectsCount: metrics.GetOrCreateCounter(fmt.Sprintf(`vm_promscrape_discovery_kubernetes_objects{role=%q}`, role)), @@ -358,8 +337,6 @@ func newURLWatcher(role, namespace, apiURL string, gw *groupWatcher) *urlWatcher staleResourceVersions: metrics.GetOrCreateCounter(fmt.Sprintf(`vm_promscrape_discovery_kubernetes_stale_resource_versions_total{role=%q}`, role)), } logger.Infof("started %s watcher for %q", uw.role, uw.apiURL) - go uw.watchForUpdates() - go uw.processPendingSubscribers() return uw } @@ -369,10 +346,16 @@ func (uw *urlWatcher) subscribeAPIWatcher(aw *apiWatcher) { } uw.mu.Lock() if _, ok := uw.aws[aw]; !ok { - if _, ok := uw.awsPending[aw]; !ok { - uw.awsPending[aw] = struct{}{} - metrics.GetOrCreateCounter(fmt.Sprintf(`vm_promscrape_discovery_kubernetes_subscibers{role=%q,type="pending"}`, uw.role)).Inc() + swosByKey := make(map[string][]interface{}) + for key, o := range uw.objectsByKey { + labels := o.getTargetLabels(uw.gw) + swos := aw.getScrapeWorkObjectsForLabels(labels) + if len(swos) > 0 { + swosByKey[key] = swos + } } + aw.reloadScrapeWorks(uw.namespace, swosByKey) + metrics.GetOrCreateCounter(fmt.Sprintf(`vm_promscrape_discovery_kubernetes_subscibers{role=%q}`, uw.role)).Inc() } uw.mu.Unlock() } @@ -381,43 +364,11 @@ func (uw *urlWatcher) unsubscribeAPIWatcher(aw *apiWatcher) { uw.mu.Lock() if _, ok := uw.aws[aw]; ok { delete(uw.aws, aw) - metrics.GetOrCreateCounter(fmt.Sprintf(`vm_promscrape_discovery_kubernetes_subscibers{role=%q,type="permanent"}`, uw.role)).Dec() - } else if _, ok := uw.awsPending[aw]; ok { - delete(uw.awsPending, aw) - metrics.GetOrCreateCounter(fmt.Sprintf(`vm_promscrape_discovery_kubernetes_subscibers{role=%q,type="pending"}`, uw.role)).Dec() + metrics.GetOrCreateCounter(fmt.Sprintf(`vm_promscrape_discovery_kubernetes_subscibers{role=%q}`, uw.role)).Dec() } uw.mu.Unlock() } -func (uw *urlWatcher) processPendingSubscribers() { - t := time.NewTicker(time.Second) - for range t.C { - var awsPending []*apiWatcher - var objectsByKey map[string]object - - uw.mu.Lock() - if len(uw.awsPending) > 0 { - awsPending = getAPIWatchers(uw.awsPending) - for _, aw := range awsPending { - if _, ok := uw.aws[aw]; ok { - logger.Panicf("BUG: aw=%p already exists in uw.aws", aw) - } - uw.aws[aw] = struct{}{} - delete(uw.awsPending, aw) - } - objectsByKey = make(map[string]object, len(uw.objectsByKey)) - for key, o := range uw.objectsByKey { - objectsByKey[key] = o - } - } - metrics.GetOrCreateCounter(fmt.Sprintf(`vm_promscrape_discovery_kubernetes_subscibers{role=%q,type="pending"}`, uw.role)).Add(-len(awsPending)) - metrics.GetOrCreateCounter(fmt.Sprintf(`vm_promscrape_discovery_kubernetes_subscibers{role=%q,type="permanent"}`, uw.role)).Add(len(awsPending)) - uw.mu.Unlock() - - uw.gw.reloadScrapeWorksForAPIWatchers(uw.namespace, awsPending, objectsByKey) - } -} - func (uw *urlWatcher) setResourceVersion(resourceVersion string) { uw.mu.Lock() uw.resourceVersion = resourceVersion @@ -478,9 +429,31 @@ func (uw *urlWatcher) reloadObjects() string { aws := getAPIWatchers(uw.aws) uw.mu.Unlock() - uw.gw.reloadScrapeWorksForAPIWatchers(uw.namespace, aws, objectsByKey) + uw.reloadScrapeWorksForAPIWatchers(aws, objectsByKey) logger.Infof("reloaded %d objects from %q", len(objectsByKey), requestURL) - return metadata.ResourceVersion + return uw.resourceVersion +} + +func (uw *urlWatcher) reloadScrapeWorksForAPIWatchers(aws []*apiWatcher, objectsByKey map[string]object) { + if len(aws) == 0 { + return + } + swosByKey := make([]map[string][]interface{}, len(aws)) + for i := range aws { + swosByKey[i] = make(map[string][]interface{}) + } + for key, o := range objectsByKey { + labels := o.getTargetLabels(uw.gw) + for i, aw := range aws { + swos := aw.getScrapeWorkObjectsForLabels(labels) + if len(swos) > 0 { + swosByKey[i][key] = swos + } + } + } + for i, aw := range aws { + aw.reloadScrapeWorks(uw.namespace, swosByKey[i]) + } } func getAPIWatchers(awsMap map[*apiWatcher]struct{}) []*apiWatcher { diff --git a/lib/promscrape/scraper.go b/lib/promscrape/scraper.go index d02f1732f..861c35c4d 100644 --- a/lib/promscrape/scraper.go +++ b/lib/promscrape/scraper.go @@ -231,17 +231,11 @@ func (scfg *scrapeConfig) run() { cfg := <-scfg.cfgCh var swsPrev []*ScrapeWork updateScrapeWork := func(cfg *Config) { - for { - startTime := time.Now() - sws := scfg.getScrapeWork(cfg, swsPrev) - retry := sg.update(sws) - swsPrev = sws - scfg.discoveryDuration.UpdateDuration(startTime) - if !retry { - return - } - time.Sleep(2 * time.Second) - } + startTime := time.Now() + sws := scfg.getScrapeWork(cfg, swsPrev) + sg.update(sws) + swsPrev = sws + scfg.discoveryDuration.UpdateDuration(startTime) } updateScrapeWork(cfg) atomic.AddInt32(&PendingScrapeConfigs, -1) @@ -301,7 +295,7 @@ func (sg *scraperGroup) stop() { sg.wg.Wait() } -func (sg *scraperGroup) update(sws []*ScrapeWork) (retry bool) { +func (sg *scraperGroup) update(sws []*ScrapeWork) { sg.mLock.Lock() defer sg.mLock.Unlock() @@ -358,7 +352,6 @@ func (sg *scraperGroup) update(sws []*ScrapeWork) (retry bool) { sg.changesCount.Add(additionsCount + deletionsCount) logger.Infof("%s: added targets: %d, removed targets: %d; total targets: %d", sg.name, additionsCount, deletionsCount, len(sg.m)) } - return deletionsCount > 0 && len(sg.m) == 0 } type scraper struct { From 95dbebf51214ae459537e8b11e14720a3a587784 Mon Sep 17 00:00:00 2001 From: Aliaksandr Valialkin Date: Mon, 5 Apr 2021 19:19:58 +0300 Subject: [PATCH 45/63] lib/persistentqueue: delete corrupted persistent queue instead of throwing a fatal error Updates https://github.com/VictoriaMetrics/VictoriaMetrics/issues/1030 --- docs/CHANGELOG.md | 1 + lib/persistentqueue/fastqueue.go | 13 +- lib/persistentqueue/persistentqueue.go | 264 ++++++++++-------- lib/persistentqueue/persistentqueue_test.go | 193 +++---------- .../persistentqueue_timing_test.go | 16 +- 5 files changed, 195 insertions(+), 292 deletions(-) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 75f1096bd..f1294214e 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -14,6 +14,7 @@ * FEATURE: vmagent: add AWS IAM roles for tasks support for EC2 service discovery according to [these docs](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-iam-roles.html). * FEATURE: vmagent: add support for `proxy_tls_config`, `proxy_authorization`, `proxy_basic_auth`, `proxy_bearer_token` and `proxy_bearer_token_file` options in `consul_sd_config`, `dockerswarm_sd_config` and `eureka_sd_config` sections. * FEATURE: vmagent: pass `X-Prometheus-Scrape-Timeout-Seconds` header to scrape targets as Prometheus does. In this case scrape targets can limit the time needed for performing the scrape. See [this comment](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/1179#issuecomment-813118733) for details. +* FEATURE: vmagent: drop corrupted persistent queue files at `-remoteWrite.tmpDataPath` instead of throwing a fatal error. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/1030). * FEATURE: vmauth: add support for authorization via [bearer token](https://swagger.io/docs/specification/authentication/bearer-authentication/). See [the docs](https://victoriametrics.github.io/vmauth.html#auth-config) for details. * BUGFIX: vmagent: properly work with simple HTTP proxies which don't support `CONNECT` method. For example, [PushProx](https://github.com/prometheus-community/PushProx). See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/1179). diff --git a/lib/persistentqueue/fastqueue.go b/lib/persistentqueue/fastqueue.go index 5c25933f6..7dda73a86 100644 --- a/lib/persistentqueue/fastqueue.go +++ b/lib/persistentqueue/fastqueue.go @@ -8,7 +8,7 @@ import ( "github.com/VictoriaMetrics/VictoriaMetrics/lib/logger" ) -// FastQueue is a wrapper around Queue, which prefers sending data via memory. +// FastQueue is fast persistent queue, which prefers sending data via memory. // // It falls back to sending data via file when readers don't catch up with writers. type FastQueue struct { @@ -20,7 +20,7 @@ type FastQueue struct { cond sync.Cond // pq is file-based queue - pq *Queue + pq *queue // ch is in-memory queue ch chan *bytesutil.ByteBuffer @@ -40,7 +40,7 @@ type FastQueue struct { // Otherwise its size is limited by maxPendingBytes. The oldest data is dropped when the queue // reaches maxPendingSize. func MustOpenFastQueue(path, name string, maxInmemoryBlocks, maxPendingBytes int) *FastQueue { - pq := MustOpen(path, name, maxPendingBytes) + pq := mustOpen(path, name, maxPendingBytes) fq := &FastQueue{ pq: pq, ch: make(chan *bytesutil.ByteBuffer, maxInmemoryBlocks), @@ -174,7 +174,12 @@ func (fq *FastQueue) MustReadBlock(dst []byte) ([]byte, bool) { return dst, true } if n := fq.pq.GetPendingBytes(); n > 0 { - return fq.pq.MustReadBlock(dst) + data, ok := fq.pq.MustReadBlockNonblocking(dst) + if ok { + return data, true + } + dst = data + continue } // There are no blocks. Wait for new block. diff --git a/lib/persistentqueue/persistentqueue.go b/lib/persistentqueue/persistentqueue.go index 916276c3f..4c082a9f8 100644 --- a/lib/persistentqueue/persistentqueue.go +++ b/lib/persistentqueue/persistentqueue.go @@ -8,7 +8,6 @@ import ( "os" "regexp" "strconv" - "sync" "github.com/VictoriaMetrics/VictoriaMetrics/lib/bytesutil" "github.com/VictoriaMetrics/VictoriaMetrics/lib/encoding" @@ -26,8 +25,10 @@ const defaultChunkFileSize = (MaxBlockSize + 8) * 16 var chunkFileNameRegex = regexp.MustCompile("^[0-9A-F]{16}$") -// Queue represents persistent queue. -type Queue struct { +// queue represents persistent queue. +// +// It is unsafe to call queue methods from concurrent goroutines. +type queue struct { chunkFileSize uint64 maxBlockSize uint64 maxPendingBytes uint64 @@ -37,13 +38,6 @@ type Queue struct { flockF *os.File - // mu protects all the fields below. - mu sync.Mutex - - // cond is used for notifying blocked readers when new data has been added - // or when MustClose is called. - cond sync.Cond - reader *filestream.Reader readerPath string readerOffset uint64 @@ -74,10 +68,7 @@ type Queue struct { // ResetIfEmpty resets q if it is empty. // // This is needed in order to remove chunk file associated with empty q. -func (q *Queue) ResetIfEmpty() { - q.mu.Lock() - defer q.mu.Unlock() - +func (q *queue) ResetIfEmpty() { if q.readerOffset != q.writerOffset { // The queue isn't empty. return @@ -86,10 +77,13 @@ func (q *Queue) ResetIfEmpty() { // The file is too small to drop. Leave it as is in order to reduce filesystem load. return } + q.mustResetFiles() +} + +func (q *queue) mustResetFiles() { if q.readerPath != q.writerPath { logger.Panicf("BUG: readerPath=%q doesn't match writerPath=%q", q.readerPath, q.writerPath) } - q.reader.MustClose() q.writer.MustClose() fs.MustRemoveAll(q.readerPath) @@ -115,31 +109,29 @@ func (q *Queue) ResetIfEmpty() { } q.reader = r - if err := q.flushMetainfoLocked(); err != nil { + if err := q.flushMetainfo(); err != nil { logger.Panicf("FATAL: cannot flush metainfo: %s", err) } } // GetPendingBytes returns the number of pending bytes in the queue. -func (q *Queue) GetPendingBytes() uint64 { - q.mu.Lock() +func (q *queue) GetPendingBytes() uint64 { n := q.writerOffset - q.readerOffset - q.mu.Unlock() return n } -// MustOpen opens persistent queue from the given path. +// mustOpen opens persistent queue from the given path. // // If maxPendingBytes is greater than 0, then the max queue size is limited by this value. // The oldest data is deleted when queue size exceeds maxPendingBytes. -func MustOpen(path, name string, maxPendingBytes int) *Queue { +func mustOpen(path, name string, maxPendingBytes int) *queue { if maxPendingBytes < 0 { maxPendingBytes = 0 } - return mustOpen(path, name, defaultChunkFileSize, MaxBlockSize, uint64(maxPendingBytes)) + return mustOpenInternal(path, name, defaultChunkFileSize, MaxBlockSize, uint64(maxPendingBytes)) } -func mustOpen(path, name string, chunkFileSize, maxBlockSize, maxPendingBytes uint64) *Queue { +func mustOpenInternal(path, name string, chunkFileSize, maxBlockSize, maxPendingBytes uint64) *queue { if chunkFileSize < 8 || chunkFileSize-8 < maxBlockSize { logger.Panicf("BUG: too small chunkFileSize=%d for maxBlockSize=%d; chunkFileSize must fit at least one block", chunkFileSize, maxBlockSize) } @@ -166,15 +158,14 @@ func mustCreateFlockFile(path string) *os.File { return f } -func tryOpeningQueue(path, name string, chunkFileSize, maxBlockSize, maxPendingBytes uint64) (*Queue, error) { +func tryOpeningQueue(path, name string, chunkFileSize, maxBlockSize, maxPendingBytes uint64) (*queue, error) { // Protect from concurrent opens. - var q Queue + var q queue q.chunkFileSize = chunkFileSize q.maxBlockSize = maxBlockSize q.maxPendingBytes = maxPendingBytes q.dir = path q.name = name - q.cond.L = &q.mu q.blocksDropped = metrics.GetOrCreateCounter(fmt.Sprintf(`vm_persistentqueue_blocks_dropped_total{path=%q}`, path)) q.bytesDropped = metrics.GetOrCreateCounter(fmt.Sprintf(`vm_persistentqueue_bytes_dropped_total{path=%q}`, path)) @@ -346,17 +337,8 @@ func tryOpeningQueue(path, name string, chunkFileSize, maxBlockSize, maxPendingB // MustClose closes q. // -// It unblocks all the MustReadBlock calls. -// // MustWriteBlock mustn't be called during and after the call to MustClose. -func (q *Queue) MustClose() { - q.mu.Lock() - defer q.mu.Unlock() - - // Unblock goroutines blocked on cond in MustReadBlock. - q.mustStop = true - q.cond.Broadcast() - +func (q *queue) MustClose() { // Close writer. q.writer.MustClose() q.writer = nil @@ -366,7 +348,7 @@ func (q *Queue) MustClose() { q.reader = nil // Store metainfo - if err := q.flushMetainfoLocked(); err != nil { + if err := q.flushMetainfo(); err != nil { logger.Panicf("FATAL: cannot flush chunked queue metainfo: %s", err) } @@ -377,11 +359,11 @@ func (q *Queue) MustClose() { q.flockF = nil } -func (q *Queue) chunkFilePath(offset uint64) string { +func (q *queue) chunkFilePath(offset uint64) string { return fmt.Sprintf("%s/%016X", q.dir, offset) } -func (q *Queue) metainfoPath() string { +func (q *queue) metainfoPath() string { return q.dir + "/metainfo.json" } @@ -390,14 +372,10 @@ func (q *Queue) metainfoPath() string { // The block size cannot exceed MaxBlockSize. // // It is safe calling this function from concurrent goroutines. -func (q *Queue) MustWriteBlock(block []byte) { +func (q *queue) MustWriteBlock(block []byte) { if uint64(len(block)) > q.maxBlockSize { logger.Panicf("BUG: too big block to send: %d bytes; it mustn't exceed %d bytes", len(block), q.maxBlockSize) } - - q.mu.Lock() - defer q.mu.Unlock() - if q.mustStop { logger.Panicf("BUG: MustWriteBlock cannot be called after MustClose") } @@ -416,7 +394,10 @@ func (q *Queue) MustWriteBlock(block []byte) { bb := blockBufPool.Get() for q.writerOffset-q.readerOffset > maxPendingBytes { var err error - bb.B, err = q.readBlockLocked(bb.B[:0]) + bb.B, err = q.readBlock(bb.B[:0]) + if err == errEmptyQueue { + break + } if err != nil { logger.Panicf("FATAL: cannot read the oldest block %s", err) } @@ -429,38 +410,18 @@ func (q *Queue) MustWriteBlock(block []byte) { return } } - if err := q.writeBlockLocked(block); err != nil { + if err := q.writeBlock(block); err != nil { logger.Panicf("FATAL: %s", err) } - - // Notify blocked reader if any. - // See https://github.com/VictoriaMetrics/VictoriaMetrics/pull/484 for details. - q.cond.Signal() } var blockBufPool bytesutil.ByteBufferPool -func (q *Queue) writeBlockLocked(block []byte) error { +func (q *queue) writeBlock(block []byte) error { if q.writerLocalOffset+q.maxBlockSize+8 > q.chunkFileSize { - // Finalize the current chunk and start new one. - q.writer.MustClose() - // There is no need to do fs.MustSyncPath(q.writerPath) here, - // since MustClose already does this. - if n := q.writerOffset % q.chunkFileSize; n > 0 { - q.writerOffset += (q.chunkFileSize - n) + if err := q.nextChunkFileForWrite(); err != nil { + return fmt.Errorf("cannot create next chunk file: %w", err) } - q.writerFlushedOffset = q.writerOffset - q.writerLocalOffset = 0 - q.writerPath = q.chunkFilePath(q.writerOffset) - w, err := filestream.Create(q.writerPath, false) - if err != nil { - return fmt.Errorf("cannot create chunk file %q: %w", q.writerPath, err) - } - q.writer = w - if err := q.flushMetainfoLocked(); err != nil { - return fmt.Errorf("cannot flush metainfo: %w", err) - } - fs.MustSyncPath(q.dir) } // Write block len. @@ -479,62 +440,61 @@ func (q *Queue) writeBlockLocked(block []byte) error { } q.blocksWritten.Inc() q.bytesWritten.Add(len(block)) - return q.flushMetainfoIfNeededLocked(true) + return q.flushWriterMetainfoIfNeeded() } -// MustReadBlock appends the next block from q to dst and returns the result. -// -// false is returned after MustClose call. -// -// It is safe calling this function from concurrent goroutines. -func (q *Queue) MustReadBlock(dst []byte) ([]byte, bool) { - q.mu.Lock() - defer q.mu.Unlock() +func (q *queue) nextChunkFileForWrite() error { + // Finalize the current chunk and start new one. + q.writer.MustClose() + // There is no need to do fs.MustSyncPath(q.writerPath) here, + // since MustClose already does this. + if n := q.writerOffset % q.chunkFileSize; n > 0 { + q.writerOffset += q.chunkFileSize - n + } + q.writerFlushedOffset = q.writerOffset + q.writerLocalOffset = 0 + q.writerPath = q.chunkFilePath(q.writerOffset) + w, err := filestream.Create(q.writerPath, false) + if err != nil { + return fmt.Errorf("cannot create chunk file %q: %w", q.writerPath, err) + } + q.writer = w + if err := q.flushMetainfo(); err != nil { + return fmt.Errorf("cannot flush metainfo: %w", err) + } + fs.MustSyncPath(q.dir) + return nil +} - for { - if q.mustStop { +// MustReadBlockNonblocking appends the next block from q to dst and returns the result. +// +// false is returned if q is empty. +func (q *queue) MustReadBlockNonblocking(dst []byte) ([]byte, bool) { + if q.readerOffset > q.writerOffset { + logger.Panicf("BUG: readerOffset=%d cannot exceed writerOffset=%d", q.readerOffset, q.writerOffset) + } + if q.readerOffset == q.writerOffset { + return dst, false + } + var err error + dst, err = q.readBlock(dst) + if err != nil { + if err == errEmptyQueue { return dst, false } - if q.readerOffset > q.writerOffset { - logger.Panicf("BUG: readerOffset=%d cannot exceed writerOffset=%d", q.readerOffset, q.writerOffset) - } - if q.readerOffset < q.writerOffset { - break - } - q.cond.Wait() - } - - data, err := q.readBlockLocked(dst) - if err != nil { - // Skip the current chunk, since it may be broken. - q.readerOffset += q.chunkFileSize - q.readerOffset%q.chunkFileSize - _ = q.flushMetainfoLocked() logger.Panicf("FATAL: %s", err) } - return data, true + return dst, true } -func (q *Queue) readBlockLocked(dst []byte) ([]byte, error) { +func (q *queue) readBlock(dst []byte) ([]byte, error) { if q.readerLocalOffset+q.maxBlockSize+8 > q.chunkFileSize { - // Remove the current chunk and go to the next chunk. - q.reader.MustClose() - fs.MustRemoveAll(q.readerPath) - if n := q.readerOffset % q.chunkFileSize; n > 0 { - q.readerOffset += (q.chunkFileSize - n) + if err := q.nextChunkFileForRead(); err != nil { + return dst, fmt.Errorf("cannot open next chunk file: %w", err) } - q.readerLocalOffset = 0 - q.readerPath = q.chunkFilePath(q.readerOffset) - r, err := filestream.Open(q.readerPath, true) - if err != nil { - return dst, fmt.Errorf("cannot open chunk file %q: %w", q.readerPath, err) - } - q.reader = r - if err := q.flushMetainfoLocked(); err != nil { - return dst, fmt.Errorf("cannot flush metainfo: %w", err) - } - fs.MustSyncPath(q.dir) } +again: // Read block len. header := headerBufPool.Get() header.B = bytesutil.Resize(header.B, 8) @@ -542,27 +502,73 @@ func (q *Queue) readBlockLocked(dst []byte) ([]byte, error) { blockLen := encoding.UnmarshalUint64(header.B) headerBufPool.Put(header) if err != nil { - return dst, fmt.Errorf("cannot read header with size 8 bytes from %q: %w", q.readerPath, err) + logger.Errorf("skipping corrupted %q, since header with size 8 bytes cannot be read from it: %s", q.readerPath, err) + if err := q.skipBrokenChunkFile(); err != nil { + return dst, err + } + goto again } if blockLen > q.maxBlockSize { - return dst, fmt.Errorf("too big block size read from %q: %d bytes; cannot exceed %d bytes", q.readerPath, blockLen, q.maxBlockSize) + logger.Errorf("skipping corrupted %q, since too big block size is read from it: %d bytes; cannot exceed %d bytes", q.readerPath, blockLen, q.maxBlockSize) + if err := q.skipBrokenChunkFile(); err != nil { + return dst, err + } + goto again } // Read block contents. dstLen := len(dst) dst = bytesutil.Resize(dst, dstLen+int(blockLen)) if err := q.readFull(dst[dstLen:]); err != nil { - return dst, fmt.Errorf("cannot read block contents with size %d bytes from %q: %w", blockLen, q.readerPath, err) + logger.Errorf("skipping corrupted %q, since contents with size %d bytes cannot be read from it: %s", q.readerPath, blockLen, err) + if err := q.skipBrokenChunkFile(); err != nil { + return dst[:dstLen], err + } + goto again } q.blocksRead.Inc() q.bytesRead.Add(int(blockLen)) - if err := q.flushMetainfoIfNeededLocked(false); err != nil { + if err := q.flushReaderMetainfoIfNeeded(); err != nil { return dst, err } return dst, nil } -func (q *Queue) write(buf []byte) error { +func (q *queue) skipBrokenChunkFile() error { + // Try to recover from broken chunk file by skipping it. + // See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/1030 + q.readerOffset += q.chunkFileSize - q.readerOffset%q.chunkFileSize + if q.readerOffset >= q.writerOffset { + q.mustResetFiles() + return errEmptyQueue + } + return q.nextChunkFileForRead() +} + +var errEmptyQueue = fmt.Errorf("the queue is empty") + +func (q *queue) nextChunkFileForRead() error { + // Remove the current chunk and go to the next chunk. + q.reader.MustClose() + fs.MustRemoveAll(q.readerPath) + if n := q.readerOffset % q.chunkFileSize; n > 0 { + q.readerOffset += q.chunkFileSize - n + } + q.readerLocalOffset = 0 + q.readerPath = q.chunkFilePath(q.readerOffset) + r, err := filestream.Open(q.readerPath, true) + if err != nil { + return fmt.Errorf("cannot open chunk file %q: %w", q.readerPath, err) + } + q.reader = r + if err := q.flushMetainfo(); err != nil { + return fmt.Errorf("cannot flush metainfo: %w", err) + } + fs.MustSyncPath(q.dir) + return nil +} + +func (q *queue) write(buf []byte) error { bufLen := uint64(len(buf)) n, err := q.writer.Write(buf) if err != nil { @@ -576,7 +582,7 @@ func (q *Queue) write(buf []byte) error { return nil } -func (q *Queue) readFull(buf []byte) error { +func (q *queue) readFull(buf []byte) error { bufLen := uint64(len(buf)) if q.readerOffset+bufLen > q.writerFlushedOffset { q.writer.MustFlush(false) @@ -594,22 +600,32 @@ func (q *Queue) readFull(buf []byte) error { return nil } -func (q *Queue) flushMetainfoIfNeededLocked(flushData bool) error { +func (q *queue) flushReaderMetainfoIfNeeded() error { t := fasttime.UnixTimestamp() if t == q.lastMetainfoFlushTime { return nil } - if flushData { - q.writer.MustFlush(true) - } - if err := q.flushMetainfoLocked(); err != nil { + if err := q.flushMetainfo(); err != nil { return fmt.Errorf("cannot flush metainfo: %w", err) } q.lastMetainfoFlushTime = t return nil } -func (q *Queue) flushMetainfoLocked() error { +func (q *queue) flushWriterMetainfoIfNeeded() error { + t := fasttime.UnixTimestamp() + if t == q.lastMetainfoFlushTime { + return nil + } + q.writer.MustFlush(true) + if err := q.flushMetainfo(); err != nil { + return fmt.Errorf("cannot flush metainfo: %w", err) + } + q.lastMetainfoFlushTime = t + return nil +} + +func (q *queue) flushMetainfo() error { mi := &metainfo{ Name: q.name, ReaderOffset: q.readerOffset, diff --git a/lib/persistentqueue/persistentqueue_test.go b/lib/persistentqueue/persistentqueue_test.go index e4b83d540..e135eb415 100644 --- a/lib/persistentqueue/persistentqueue_test.go +++ b/lib/persistentqueue/persistentqueue_test.go @@ -5,16 +5,14 @@ import ( "io/ioutil" "os" "strconv" - "sync" "testing" - "time" ) func TestQueueOpenClose(t *testing.T) { path := "queue-open-close" mustDeleteDir(path) for i := 0; i < 3; i++ { - q := MustOpen(path, "foobar", 0) + q := mustOpen(path, "foobar", 0) if n := q.GetPendingBytes(); n > 0 { t.Fatalf("pending bytes must be 0; got %d", n) } @@ -28,7 +26,7 @@ func TestQueueOpen(t *testing.T) { path := "queue-open-invalid-metainfo" mustCreateDir(path) mustCreateFile(path+"/metainfo.json", "foobarbaz") - q := MustOpen(path, "foobar", 0) + q := mustOpen(path, "foobar", 0) q.MustClose() mustDeleteDir(path) }) @@ -38,7 +36,7 @@ func TestQueueOpen(t *testing.T) { mustCreateEmptyMetainfo(path, "foobar") mustCreateFile(path+"/junk-file", "foobar") mustCreateDir(path + "/junk-dir") - q := MustOpen(path, "foobar", 0) + q := mustOpen(path, "foobar", 0) q.MustClose() mustDeleteDir(path) }) @@ -47,7 +45,7 @@ func TestQueueOpen(t *testing.T) { mustCreateDir(path) mustCreateEmptyMetainfo(path, "foobar") mustCreateFile(fmt.Sprintf("%s/%016X", path, 1234), "qwere") - q := MustOpen(path, "foobar", 0) + q := mustOpen(path, "foobar", 0) q.MustClose() mustDeleteDir(path) }) @@ -56,7 +54,7 @@ func TestQueueOpen(t *testing.T) { mustCreateDir(path) mustCreateEmptyMetainfo(path, "foobar") mustCreateFile(fmt.Sprintf("%s/%016X", path, 100*uint64(defaultChunkFileSize)), "asdf") - q := MustOpen(path, "foobar", 0) + q := mustOpen(path, "foobar", 0) q.MustClose() mustDeleteDir(path) }) @@ -72,7 +70,7 @@ func TestQueueOpen(t *testing.T) { t.Fatalf("unexpected error: %s", err) } mustCreateFile(fmt.Sprintf("%s/%016X", path, 0), "adfsfd") - q := MustOpen(path, mi.Name, 0) + q := mustOpen(path, mi.Name, 0) q.MustClose() mustDeleteDir(path) }) @@ -86,7 +84,7 @@ func TestQueueOpen(t *testing.T) { if err := mi.WriteToFile(path + "/metainfo.json"); err != nil { t.Fatalf("unexpected error: %s", err) } - q := MustOpen(path, mi.Name, 0) + q := mustOpen(path, mi.Name, 0) q.MustClose() mustDeleteDir(path) }) @@ -94,7 +92,7 @@ func TestQueueOpen(t *testing.T) { path := "queue-open-metainfo-dir" mustCreateDir(path) mustCreateDir(path + "/metainfo.json") - q := MustOpen(path, "foobar", 0) + q := mustOpen(path, "foobar", 0) q.MustClose() mustDeleteDir(path) }) @@ -110,7 +108,7 @@ func TestQueueOpen(t *testing.T) { t.Fatalf("unexpected error: %s", err) } mustCreateFile(fmt.Sprintf("%s/%016X", path, 0), "sdf") - q := MustOpen(path, mi.Name, 0) + q := mustOpen(path, mi.Name, 0) q.MustClose() mustDeleteDir(path) }) @@ -119,7 +117,7 @@ func TestQueueOpen(t *testing.T) { mustCreateDir(path) mustCreateEmptyMetainfo(path, "foobar") mustCreateFile(fmt.Sprintf("%s/%016X", path, 0), "sdfdsf") - q := MustOpen(path, "foobar", 0) + q := mustOpen(path, "foobar", 0) q.MustClose() mustDeleteDir(path) }) @@ -133,7 +131,7 @@ func TestQueueOpen(t *testing.T) { t.Fatalf("unexpected error: %s", err) } mustCreateFile(fmt.Sprintf("%s/%016X", path, 0), "sdf") - q := MustOpen(path, "baz", 0) + q := mustOpen(path, "baz", 0) q.MustClose() mustDeleteDir(path) }) @@ -142,7 +140,7 @@ func TestQueueOpen(t *testing.T) { func TestQueueResetIfEmpty(t *testing.T) { path := "queue-reset-if-empty" mustDeleteDir(path) - q := MustOpen(path, "foobar", 0) + q := mustOpen(path, "foobar", 0) defer func() { q.MustClose() mustDeleteDir(path) @@ -154,14 +152,18 @@ func TestQueueResetIfEmpty(t *testing.T) { for i := 0; i < 10; i++ { q.MustWriteBlock(block) var ok bool - buf, ok = q.MustReadBlock(buf[:0]) + buf, ok = q.MustReadBlockNonblocking(buf[:0]) if !ok { - t.Fatalf("unexpected ok=false returned from MustReadBlock") + t.Fatalf("unexpected ok=false returned from MustReadBlockNonblocking") } } q.ResetIfEmpty() if n := q.GetPendingBytes(); n > 0 { - t.Fatalf("unexpected non-zer pending bytes after queue reset: %d", n) + t.Fatalf("unexpected non-zero pending bytes after queue reset: %d", n) + } + q.ResetIfEmpty() + if n := q.GetPendingBytes(); n > 0 { + t.Fatalf("unexpected non-zero pending bytes after queue reset: %d", n) } } } @@ -169,7 +171,7 @@ func TestQueueResetIfEmpty(t *testing.T) { func TestQueueWriteRead(t *testing.T) { path := "queue-write-read" mustDeleteDir(path) - q := MustOpen(path, "foobar", 0) + q := mustOpen(path, "foobar", 0) defer func() { q.MustClose() mustDeleteDir(path) @@ -188,9 +190,9 @@ func TestQueueWriteRead(t *testing.T) { var buf []byte var ok bool for _, block := range blocks { - buf, ok = q.MustReadBlock(buf[:0]) + buf, ok = q.MustReadBlockNonblocking(buf[:0]) if !ok { - t.Fatalf("unexpected ok=%v returned from MustReadBlock; want true", ok) + t.Fatalf("unexpected ok=%v returned from MustReadBlockNonblocking; want true", ok) } if string(buf) != string(block) { t.Fatalf("unexpected block read; got %q; want %q", buf, block) @@ -205,7 +207,7 @@ func TestQueueWriteRead(t *testing.T) { func TestQueueWriteCloseRead(t *testing.T) { path := "queue-write-close-read" mustDeleteDir(path) - q := MustOpen(path, "foobar", 0) + q := mustOpen(path, "foobar", 0) defer func() { q.MustClose() mustDeleteDir(path) @@ -222,16 +224,16 @@ func TestQueueWriteCloseRead(t *testing.T) { t.Fatalf("pending bytes must be greater than 0; got %d", n) } q.MustClose() - q = MustOpen(path, "foobar", 0) + q = mustOpen(path, "foobar", 0) if n := q.GetPendingBytes(); n <= 0 { t.Fatalf("pending bytes must be greater than 0; got %d", n) } var buf []byte var ok bool for _, block := range blocks { - buf, ok = q.MustReadBlock(buf[:0]) + buf, ok = q.MustReadBlockNonblocking(buf[:0]) if !ok { - t.Fatalf("unexpected ok=%v returned from MustReadBlock; want true", ok) + t.Fatalf("unexpected ok=%v returned from MustReadBlockNonblocking; want true", ok) } if string(buf) != string(block) { t.Fatalf("unexpected block read; got %q; want %q", buf, block) @@ -243,137 +245,12 @@ func TestQueueWriteCloseRead(t *testing.T) { } } -func TestQueueReadEmpty(t *testing.T) { - path := "queue-read-empty" - mustDeleteDir(path) - q := MustOpen(path, "foobar", 0) - defer mustDeleteDir(path) - - resultCh := make(chan error) - go func() { - data, ok := q.MustReadBlock(nil) - var err error - if ok { - err = fmt.Errorf("unexpected ok=%v returned from MustReadBlock; want false", ok) - } else if len(data) > 0 { - err = fmt.Errorf("unexpected non-empty data returned from MustReadBlock: %q", data) - } - resultCh <- err - }() - if n := q.GetPendingBytes(); n > 0 { - t.Fatalf("pending bytes must be 0; got %d", n) - } - q.MustClose() - select { - case err := <-resultCh: - if err != nil { - t.Fatalf("unexpected error: %s", err) - } - case <-time.After(time.Second): - t.Fatalf("timeout") - } -} - -func TestQueueReadWriteConcurrent(t *testing.T) { - path := "queue-read-write-concurrent" - mustDeleteDir(path) - q := MustOpen(path, "foobar", 0) - defer mustDeleteDir(path) - - blocksMap := make(map[string]bool, 1000) - var blocksMapLock sync.Mutex - blocks := make([]string, 1000) - for i := 0; i < 1000; i++ { - block := fmt.Sprintf("block #%d", i) - blocksMap[block] = true - blocks[i] = block - } - - // Start block readers - var readersWG sync.WaitGroup - for workerID := 0; workerID < 10; workerID++ { - readersWG.Add(1) - go func() { - defer readersWG.Done() - for { - block, ok := q.MustReadBlock(nil) - if !ok { - return - } - blocksMapLock.Lock() - if !blocksMap[string(block)] { - panic(fmt.Errorf("unexpected block read: %q", block)) - } - delete(blocksMap, string(block)) - blocksMapLock.Unlock() - } - }() - } - - // Start block writers - blocksCh := make(chan string) - var writersWG sync.WaitGroup - for workerID := 0; workerID < 10; workerID++ { - writersWG.Add(1) - go func(workerID int) { - defer writersWG.Done() - for block := range blocksCh { - q.MustWriteBlock([]byte(block)) - } - }(workerID) - } - for _, block := range blocks { - blocksCh <- block - } - close(blocksCh) - - // Wait for block writers to finish - writersWG.Wait() - - // Notify readers that the queue is closed - q.MustClose() - - // Wait for block readers to finish - readersWG.Wait() - - // Read the remaining blocks in q. - q = MustOpen(path, "foobar", 0) - defer q.MustClose() - resultCh := make(chan error) - go func() { - for len(blocksMap) > 0 { - block, ok := q.MustReadBlock(nil) - if !ok { - resultCh <- fmt.Errorf("unexpected ok=false returned from MustReadBlock") - return - } - if !blocksMap[string(block)] { - resultCh <- fmt.Errorf("unexpected block read from the queue: %q", block) - return - } - delete(blocksMap, string(block)) - } - resultCh <- nil - }() - select { - case err := <-resultCh: - if err != nil { - t.Fatalf("unexpected error: %s", err) - } - case <-time.After(5 * time.Second): - t.Fatalf("timeout") - } - if n := q.GetPendingBytes(); n > 0 { - t.Fatalf("pending bytes must be 0; got %d", n) - } -} - func TestQueueChunkManagementSimple(t *testing.T) { path := "queue-chunk-management-simple" mustDeleteDir(path) const chunkFileSize = 100 const maxBlockSize = 20 - q := mustOpen(path, "foobar", chunkFileSize, maxBlockSize, 0) + q := mustOpenInternal(path, "foobar", chunkFileSize, maxBlockSize, 0) defer mustDeleteDir(path) defer q.MustClose() var blocks []string @@ -386,7 +263,7 @@ func TestQueueChunkManagementSimple(t *testing.T) { t.Fatalf("unexpected zero number of bytes pending") } for _, block := range blocks { - data, ok := q.MustReadBlock(nil) + data, ok := q.MustReadBlockNonblocking(nil) if !ok { t.Fatalf("unexpected ok=false") } @@ -404,7 +281,7 @@ func TestQueueChunkManagementPeriodicClose(t *testing.T) { mustDeleteDir(path) const chunkFileSize = 100 const maxBlockSize = 20 - q := mustOpen(path, "foobar", chunkFileSize, maxBlockSize, 0) + q := mustOpenInternal(path, "foobar", chunkFileSize, maxBlockSize, 0) defer func() { q.MustClose() mustDeleteDir(path) @@ -415,13 +292,13 @@ func TestQueueChunkManagementPeriodicClose(t *testing.T) { q.MustWriteBlock([]byte(block)) blocks = append(blocks, block) q.MustClose() - q = mustOpen(path, "foobar", chunkFileSize, maxBlockSize, 0) + q = mustOpenInternal(path, "foobar", chunkFileSize, maxBlockSize, 0) } if n := q.GetPendingBytes(); n == 0 { t.Fatalf("unexpected zero number of bytes pending") } for _, block := range blocks { - data, ok := q.MustReadBlock(nil) + data, ok := q.MustReadBlockNonblocking(nil) if !ok { t.Fatalf("unexpected ok=false") } @@ -429,7 +306,7 @@ func TestQueueChunkManagementPeriodicClose(t *testing.T) { t.Fatalf("unexpected block read; got %q; want %q", data, block) } q.MustClose() - q = mustOpen(path, "foobar", chunkFileSize, maxBlockSize, 0) + q = mustOpenInternal(path, "foobar", chunkFileSize, maxBlockSize, 0) } if n := q.GetPendingBytes(); n != 0 { t.Fatalf("unexpected non-zero number of pending bytes: %d", n) @@ -440,7 +317,7 @@ func TestQueueLimitedSize(t *testing.T) { const maxPendingBytes = 1000 path := "queue-limited-size" mustDeleteDir(path) - q := MustOpen(path, "foobar", maxPendingBytes) + q := mustOpen(path, "foobar", maxPendingBytes) defer func() { q.MustClose() mustDeleteDir(path) @@ -456,7 +333,7 @@ func TestQueueLimitedSize(t *testing.T) { var buf []byte var ok bool for _, block := range blocks { - buf, ok = q.MustReadBlock(buf[:0]) + buf, ok = q.MustReadBlockNonblocking(buf[:0]) if !ok { t.Fatalf("unexpected ok=false") } @@ -473,7 +350,7 @@ func TestQueueLimitedSize(t *testing.T) { if n := q.GetPendingBytes(); n > maxPendingBytes { t.Fatalf("too many pending bytes; got %d; mustn't exceed %d", n, maxPendingBytes) } - buf, ok = q.MustReadBlock(buf[:0]) + buf, ok = q.MustReadBlockNonblocking(buf[:0]) if !ok { t.Fatalf("unexpected ok=false") } diff --git a/lib/persistentqueue/persistentqueue_timing_test.go b/lib/persistentqueue/persistentqueue_timing_test.go index 02e87513f..e7a3b7874 100644 --- a/lib/persistentqueue/persistentqueue_timing_test.go +++ b/lib/persistentqueue/persistentqueue_timing_test.go @@ -2,13 +2,14 @@ package persistentqueue import ( "fmt" + "sync" "testing" "github.com/VictoriaMetrics/VictoriaMetrics/lib/bytesutil" ) func BenchmarkQueueThroughputSerial(b *testing.B) { - const iterationsCount = 10 + const iterationsCount = 100 for _, blockSize := range []int{1e0, 1e1, 1e2, 1e3, 1e4, 1e5, 1e6} { block := make([]byte, blockSize) b.Run(fmt.Sprintf("block-size-%d", blockSize), func(b *testing.B) { @@ -16,7 +17,7 @@ func BenchmarkQueueThroughputSerial(b *testing.B) { b.SetBytes(int64(blockSize) * iterationsCount) path := fmt.Sprintf("bench-queue-throughput-serial-%d", blockSize) mustDeleteDir(path) - q := MustOpen(path, "foobar", 0) + q := mustOpen(path, "foobar", 0) defer func() { q.MustClose() mustDeleteDir(path) @@ -29,7 +30,7 @@ func BenchmarkQueueThroughputSerial(b *testing.B) { } func BenchmarkQueueThroughputConcurrent(b *testing.B) { - const iterationsCount = 10 + const iterationsCount = 100 for _, blockSize := range []int{1e0, 1e1, 1e2, 1e3, 1e4, 1e5, 1e6} { block := make([]byte, blockSize) b.Run(fmt.Sprintf("block-size-%d", blockSize), func(b *testing.B) { @@ -37,28 +38,31 @@ func BenchmarkQueueThroughputConcurrent(b *testing.B) { b.SetBytes(int64(blockSize) * iterationsCount) path := fmt.Sprintf("bench-queue-throughput-concurrent-%d", blockSize) mustDeleteDir(path) - q := MustOpen(path, "foobar", 0) + q := mustOpen(path, "foobar", 0) + var qLock sync.Mutex defer func() { q.MustClose() mustDeleteDir(path) }() b.RunParallel(func(pb *testing.PB) { for pb.Next() { + qLock.Lock() writeReadIteration(q, block, iterationsCount) + qLock.Unlock() } }) }) } } -func writeReadIteration(q *Queue, block []byte, iterationsCount int) { +func writeReadIteration(q *queue, block []byte, iterationsCount int) { for i := 0; i < iterationsCount; i++ { q.MustWriteBlock(block) } var ok bool bb := bbPool.Get() for i := 0; i < iterationsCount; i++ { - bb.B, ok = q.MustReadBlock(bb.B[:0]) + bb.B, ok = q.MustReadBlockNonblocking(bb.B[:0]) if !ok { panic(fmt.Errorf("unexpected ok=false")) } From a51d0ec6ec1d5c01ed322122cf7eab421d3a2b8e Mon Sep 17 00:00:00 2001 From: Aliaksandr Valialkin Date: Mon, 5 Apr 2021 20:27:23 +0300 Subject: [PATCH 46/63] lib/promscrape/discovery/kubernetes: load objects missing in local cache from api seriver in getObjectByRole() This should fix possible race for `role: endpoints` and `role: endpointslices` service discovery, when the referred `pod` and `service` objects aren't propagated to urlWatcher cache yet. See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/1182#issuecomment-813353359 for details. --- .../discovery/kubernetes/api_watcher.go | 67 ++++++++++++++++--- .../discovery/kubernetes/endpoints.go | 6 +- 2 files changed, 61 insertions(+), 12 deletions(-) diff --git a/lib/promscrape/discovery/kubernetes/api_watcher.go b/lib/promscrape/discovery/kubernetes/api_watcher.go index eb27c1040..aa12daad5 100644 --- a/lib/promscrape/discovery/kubernetes/api_watcher.go +++ b/lib/promscrape/discovery/kubernetes/api_watcher.go @@ -223,6 +223,53 @@ func (gw *groupWatcher) getObjectByRole(role, namespace, name string) object { // this is needed for testing return nil } + o := gw.getCachedObjectByRole(role, namespace, name) + if o != nil { + // Fast path: the object has been found in the cache. + return o + } + + // The object wasn't found in the cache. Try querying it directly from API server. + // See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/1182#issuecomment-813353359 for details. + metrics.GetOrCreateCounter(fmt.Sprintf(`vm_promscrape_discovery_kubernetes_direct_object_loads_total{role=%q}`, role)).Inc() + objectType := getObjectTypeByRole(role) + path := getAPIPath(objectType, namespace, "") + path += "/" + name + requestURL := gw.apiServer + path + resp, err := gw.doRequest(requestURL) + if err != nil { + metrics.GetOrCreateCounter(fmt.Sprintf(`vm_promscrape_discovery_kubernetes_direct_object_load_errors_total{role=%q}`, role)).Inc() + logger.Errorf("cannot obtain data for object %s (namespace=%q, name=%q): %s", role, namespace, name, err) + return nil + } + data, err := ioutil.ReadAll(resp.Body) + _ = resp.Body.Close() + if err != nil { + metrics.GetOrCreateCounter(fmt.Sprintf(`vm_promscrape_discovery_kubernetes_direct_object_load_errors_total{role=%q}`, role)).Inc() + logger.Errorf("cannot read response from %q: %s", requestURL, err) + return nil + } + if resp.StatusCode != http.StatusOK { + if resp.StatusCode == http.StatusNotFound { + metrics.GetOrCreateCounter(fmt.Sprintf(`vm_promscrape_discovery_kubernetes_direct_object_load_misses_total{role=%q}`, role)).Inc() + return nil + } + metrics.GetOrCreateCounter(fmt.Sprintf(`vm_promscrape_discovery_kubernetes_direct_object_load_errors_total{role=%q}`, role)).Inc() + logger.Errorf("unexpected status code when reading response from %q; got %d; want %d; response body: %q", requestURL, resp.StatusCode, http.StatusOK, data) + return nil + } + parseObject, _ := getObjectParsersForRole(role) + o, err = parseObject(data) + if err != nil { + metrics.GetOrCreateCounter(fmt.Sprintf(`vm_promscrape_discovery_kubernetes_direct_object_load_errors_total{role=%q}`, role)).Inc() + logger.Errorf("cannot parse object obtained from %q: %s; response body: %q", requestURL, err, data) + return nil + } + // There is no need in storing the object in urlWatcher cache, since it should be eventually populated there by urlWatcher itself. + return o +} + +func (gw *groupWatcher) getCachedObjectByRole(role, namespace, name string) object { key := namespace + "/" + name gw.startWatchersForRole(role, nil) gw.mu.Lock() @@ -625,32 +672,32 @@ func parseError(data []byte) (*Error, error) { } func getAPIPathsWithNamespaces(role string, namespaces []string, selectors []Selector) ([]string, []string) { - objectName := getObjectNameByRole(role) - if objectName == "nodes" || len(namespaces) == 0 { + objectType := getObjectTypeByRole(role) + if objectType == "nodes" || len(namespaces) == 0 { query := joinSelectors(role, selectors) - path := getAPIPath(objectName, "", query) + path := getAPIPath(objectType, "", query) return []string{path}, []string{""} } query := joinSelectors(role, selectors) paths := make([]string, len(namespaces)) for i, namespace := range namespaces { - paths[i] = getAPIPath(objectName, namespace, query) + paths[i] = getAPIPath(objectType, namespace, query) } return paths, namespaces } -func getAPIPath(objectName, namespace, query string) string { - suffix := objectName +func getAPIPath(objectType, namespace, query string) string { + suffix := objectType if namespace != "" { - suffix = "namespaces/" + namespace + "/" + objectName + suffix = "namespaces/" + namespace + "/" + objectType } if len(query) > 0 { suffix += "?" + query } - if objectName == "ingresses" { + if objectType == "ingresses" { return "/apis/networking.k8s.io/v1beta1/" + suffix } - if objectName == "endpointslices" { + if objectType == "endpointslices" { return "/apis/discovery.k8s.io/v1beta1/" + suffix } return "/api/v1/" + suffix @@ -679,7 +726,7 @@ func joinSelectors(role string, selectors []Selector) string { return strings.Join(args, "&") } -func getObjectNameByRole(role string) string { +func getObjectTypeByRole(role string) string { switch role { case "node": return "nodes" diff --git a/lib/promscrape/discovery/kubernetes/endpoints.go b/lib/promscrape/discovery/kubernetes/endpoints.go index 805a88b01..af4b16b5e 100644 --- a/lib/promscrape/discovery/kubernetes/endpoints.go +++ b/lib/promscrape/discovery/kubernetes/endpoints.go @@ -139,8 +139,10 @@ func appendEndpointLabelsForAddresses(ms []map[string]string, gw *groupWatcher, eas []EndpointAddress, epp EndpointPort, svc *Service, ready string) []map[string]string { for _, ea := range eas { var p *Pod - if o := gw.getObjectByRole("pod", ea.TargetRef.Namespace, ea.TargetRef.Name); o != nil { - p = o.(*Pod) + if ea.TargetRef.Name != "" { + if o := gw.getObjectByRole("pod", ea.TargetRef.Namespace, ea.TargetRef.Name); o != nil { + p = o.(*Pod) + } } m := getEndpointLabelsForAddressAndPort(podPortsSeen, eps, ea, epp, p, svc, ready) ms = append(ms, m) From b46194472f12640e2ae95af1df625f1fcf153fd5 Mon Sep 17 00:00:00 2001 From: Aliaksandr Valialkin Date: Mon, 5 Apr 2021 22:02:09 +0300 Subject: [PATCH 47/63] lib/promscrape/discovery/kubernetes: reduce CPU time spent on registering big number of Kubernetes objects shared among big number of scrape jobs Updates https://github.com/VictoriaMetrics/VictoriaMetrics/issues/1182 --- lib/promscrape/config.go | 34 +++++--- lib/promscrape/discovery/kubernetes/api.go | 15 +--- .../discovery/kubernetes/api_watcher.go | 81 ++++++++++++++----- .../discovery/kubernetes/kubernetes.go | 33 +++++--- lib/promscrape/scraper.go | 3 + 5 files changed, 114 insertions(+), 52 deletions(-) diff --git a/lib/promscrape/config.go b/lib/promscrape/config.go index b4a8d5d2c..1f93c4be5 100644 --- a/lib/promscrape/config.go +++ b/lib/promscrape/config.go @@ -60,6 +60,15 @@ type Config struct { baseDir string } +func (cfg *Config) mustStart() { + startTime := time.Now() + logger.Infof("starting service discovery routines...") + for i := range cfg.ScrapeConfigs { + cfg.ScrapeConfigs[i].mustStart(cfg.baseDir) + } + logger.Infof("started service discovery routines in %.3f seconds", time.Since(startTime).Seconds()) +} + func (cfg *Config) mustStop() { startTime := time.Now() logger.Infof("stopping service discovery routines...") @@ -120,6 +129,21 @@ type ScrapeConfig struct { swc *scrapeWorkConfig } +func (sc *ScrapeConfig) mustStart(baseDir string) { + for i := range sc.KubernetesSDConfigs { + swosFunc := func(metaLabels map[string]string) interface{} { + target := metaLabels["__address__"] + sw, err := sc.swc.getScrapeWork(target, nil, metaLabels) + if err != nil { + logger.Errorf("cannot create kubernetes_sd_config target %q for job_name %q: %s", target, sc.swc.jobName, err) + return nil + } + return sw + } + sc.KubernetesSDConfigs[i].MustStart(baseDir, swosFunc) + } +} + func (sc *ScrapeConfig) mustStop() { for i := range sc.KubernetesSDConfigs { sc.KubernetesSDConfigs[i].MustStop() @@ -243,15 +267,7 @@ func (cfg *Config) getKubernetesSDScrapeWork(prev []*ScrapeWork) []*ScrapeWork { ok := true for j := range sc.KubernetesSDConfigs { sdc := &sc.KubernetesSDConfigs[j] - swos, err := sdc.GetScrapeWorkObjects(cfg.baseDir, func(metaLabels map[string]string) interface{} { - target := metaLabels["__address__"] - sw, err := sc.swc.getScrapeWork(target, nil, metaLabels) - if err != nil { - logger.Errorf("cannot create kubernetes_sd_config target %q for job_name %q: %s", target, sc.swc.jobName, err) - return nil - } - return sw - }) + swos, err := sdc.GetScrapeWorkObjects() if err != nil { logger.Errorf("skipping kubernetes_sd_config targets for job_name %q because of error: %s", sc.swc.jobName, err) ok = false diff --git a/lib/promscrape/discovery/kubernetes/api.go b/lib/promscrape/discovery/kubernetes/api.go index a09221087..8afa6b613 100644 --- a/lib/promscrape/discovery/kubernetes/api.go +++ b/lib/promscrape/discovery/kubernetes/api.go @@ -7,7 +7,6 @@ import ( "strings" "github.com/VictoriaMetrics/VictoriaMetrics/lib/promauth" - "github.com/VictoriaMetrics/VictoriaMetrics/lib/promscrape/discoveryutils" ) // apiConfig contains config for API server @@ -15,18 +14,12 @@ type apiConfig struct { aw *apiWatcher } -func (ac *apiConfig) mustStop() { - ac.aw.mustStop() +func (ac *apiConfig) mustStart() { + ac.aw.mustStart() } -var configMap = discoveryutils.NewConfigMap() - -func getAPIConfig(sdc *SDConfig, baseDir string, swcFunc ScrapeWorkConstructorFunc) (*apiConfig, error) { - v, err := configMap.Get(sdc, func() (interface{}, error) { return newAPIConfig(sdc, baseDir, swcFunc) }) - if err != nil { - return nil, err - } - return v.(*apiConfig), nil +func (ac *apiConfig) mustStop() { + ac.aw.mustStop() } func newAPIConfig(sdc *SDConfig, baseDir string, swcFunc ScrapeWorkConstructorFunc) (*apiConfig, error) { diff --git a/lib/promscrape/discovery/kubernetes/api_watcher.go b/lib/promscrape/discovery/kubernetes/api_watcher.go index aa12daad5..c78b334f8 100644 --- a/lib/promscrape/discovery/kubernetes/api_watcher.go +++ b/lib/promscrape/discovery/kubernetes/api_watcher.go @@ -72,6 +72,10 @@ func newAPIWatcher(apiServer string, ac *promauth.Config, sdc *SDConfig, swcFunc } } +func (aw *apiWatcher) mustStart() { + aw.gw.startWatchersForRole(aw.role, aw) +} + func (aw *apiWatcher) mustStop() { aw.gw.unsubscribeAPIWatcher(aw) aw.swosByNamespaceLock.Lock() @@ -128,7 +132,8 @@ func (aw *apiWatcher) getScrapeWorkObjectsForLabels(labelss []map[string]string) // getScrapeWorkObjects returns all the ScrapeWork objects for the given aw. func (aw *apiWatcher) getScrapeWorkObjects() []interface{} { - aw.gw.startWatchersForRole(aw.role, aw) + aw.gw.registerPendingAPIWatchers() + aw.swosByNamespaceLock.Lock() defer aw.swosByNamespaceLock.Unlock() @@ -272,10 +277,8 @@ func (gw *groupWatcher) getObjectByRole(role, namespace, name string) object { func (gw *groupWatcher) getCachedObjectByRole(role, namespace, name string) object { key := namespace + "/" + name gw.startWatchersForRole(role, nil) - gw.mu.Lock() - defer gw.mu.Unlock() - - for _, uw := range gw.m { + uws := gw.getURLWatchers() + for _, uw := range uws { if uw.role != role { // Role mismatch continue @@ -310,7 +313,9 @@ func (gw *groupWatcher) startWatchersForRole(role string, aw *apiWatcher) { uw.reloadObjects() go uw.watchForUpdates() } - uw.subscribeAPIWatcher(aw) + if aw != nil { + uw.subscribeAPIWatcher(aw) + } } } @@ -326,12 +331,28 @@ func (gw *groupWatcher) doRequest(requestURL string) (*http.Response, error) { return gw.client.Do(req) } -func (gw *groupWatcher) unsubscribeAPIWatcher(aw *apiWatcher) { +func (gw *groupWatcher) registerPendingAPIWatchers() { + uws := gw.getURLWatchers() + for _, uw := range uws { + uw.registerPendingAPIWatchers() + } +} + +func (gw *groupWatcher) getURLWatchers() []*urlWatcher { gw.mu.Lock() + uws := make([]*urlWatcher, 0, len(gw.m)) for _, uw := range gw.m { - uw.unsubscribeAPIWatcher(aw) + uws = append(uws, uw) } gw.mu.Unlock() + return uws +} + +func (gw *groupWatcher) unsubscribeAPIWatcher(aw *apiWatcher) { + uws := gw.getURLWatchers() + for _, uw := range uws { + uw.unsubscribeAPIWatcher(aw) + } } // urlWatcher watches for an apiURL and updates object states in objectsByKey. @@ -344,9 +365,16 @@ type urlWatcher struct { parseObject parseObjectFunc parseObjectList parseObjectListFunc - // mu protects aws, objectsByKey and resourceVersion + // mu protects aws, awsPending, objectsByKey and resourceVersion mu sync.Mutex + // awsPending contains pending apiWatcher objects, which are registered in a batch. + // Batch registering saves CPU time needed for registering big number of Kubernetes objects + // shared among big number of scrape jobs, since per-object labels are generated only once + // for all the scrape jobs (each scrape job is associated with a single apiWatcher). + // See reloadScrapeWorksForAPIWatchers for details. + awsPending map[*apiWatcher]struct{} + // aws contains registered apiWatcher objects aws map[*apiWatcher]struct{} @@ -374,6 +402,7 @@ func newURLWatcher(role, namespace, apiURL string, gw *groupWatcher) *urlWatcher parseObject: parseObject, parseObjectList: parseObjectList, + awsPending: make(map[*apiWatcher]struct{}), aws: make(map[*apiWatcher]struct{}), objectsByKey: make(map[string]object), @@ -388,30 +417,38 @@ func newURLWatcher(role, namespace, apiURL string, gw *groupWatcher) *urlWatcher } func (uw *urlWatcher) subscribeAPIWatcher(aw *apiWatcher) { - if aw == nil { - return - } uw.mu.Lock() if _, ok := uw.aws[aw]; !ok { - swosByKey := make(map[string][]interface{}) - for key, o := range uw.objectsByKey { - labels := o.getTargetLabels(uw.gw) - swos := aw.getScrapeWorkObjectsForLabels(labels) - if len(swos) > 0 { - swosByKey[key] = swos - } + if _, ok := uw.awsPending[aw]; !ok { + uw.awsPending[aw] = struct{}{} + metrics.GetOrCreateCounter(fmt.Sprintf(`vm_promscrape_discovery_kubernetes_subscribers{role=%q,status="pending"}`, uw.role)).Inc() } - aw.reloadScrapeWorks(uw.namespace, swosByKey) - metrics.GetOrCreateCounter(fmt.Sprintf(`vm_promscrape_discovery_kubernetes_subscibers{role=%q}`, uw.role)).Inc() } uw.mu.Unlock() } +func (uw *urlWatcher) registerPendingAPIWatchers() { + uw.mu.Lock() + awsPending := make([]*apiWatcher, 0, len(uw.awsPending)) + for aw := range uw.awsPending { + awsPending = append(awsPending, aw) + delete(uw.awsPending, aw) + } + uw.reloadScrapeWorksForAPIWatchers(awsPending, uw.objectsByKey) + uw.mu.Unlock() + metrics.GetOrCreateCounter(fmt.Sprintf(`vm_promscrape_discovery_kubernetes_subscribers{role=%q,status="working"}`, uw.role)).Add(len(awsPending)) + metrics.GetOrCreateCounter(fmt.Sprintf(`vm_promscrape_discovery_kubernetes_subscribers{role=%q,status="pending"}`, uw.role)).Add(-len(awsPending)) +} + func (uw *urlWatcher) unsubscribeAPIWatcher(aw *apiWatcher) { uw.mu.Lock() + if _, ok := uw.awsPending[aw]; ok { + delete(uw.awsPending, aw) + metrics.GetOrCreateCounter(fmt.Sprintf(`vm_promscrape_discovery_kubernetes_subscribers{role=%q,status="pending"}`, uw.role)).Dec() + } if _, ok := uw.aws[aw]; ok { delete(uw.aws, aw) - metrics.GetOrCreateCounter(fmt.Sprintf(`vm_promscrape_discovery_kubernetes_subscibers{role=%q}`, uw.role)).Dec() + metrics.GetOrCreateCounter(fmt.Sprintf(`vm_promscrape_discovery_kubernetes_subscribers{role=%q,status="working"}`, uw.role)).Dec() } uw.mu.Unlock() } diff --git a/lib/promscrape/discovery/kubernetes/kubernetes.go b/lib/promscrape/discovery/kubernetes/kubernetes.go index 9315730f2..28160743b 100644 --- a/lib/promscrape/discovery/kubernetes/kubernetes.go +++ b/lib/promscrape/discovery/kubernetes/kubernetes.go @@ -17,6 +17,9 @@ type SDConfig struct { ProxyURL proxy.URL `yaml:"proxy_url,omitempty"` Namespaces Namespaces `yaml:"namespaces,omitempty"` Selectors []Selector `yaml:"selectors,omitempty"` + + cfg *apiConfig + startErr error } // Namespaces represents namespaces for SDConfig @@ -37,23 +40,33 @@ type Selector struct { // ScrapeWorkConstructorFunc must construct ScrapeWork object for the given metaLabels. type ScrapeWorkConstructorFunc func(metaLabels map[string]string) interface{} -// GetScrapeWorkObjects returns ScrapeWork objects for the given sdc and baseDir. +// GetScrapeWorkObjects returns ScrapeWork objects for the given sdc. +// +// This function must be called after MustStart call. +func (sdc *SDConfig) GetScrapeWorkObjects() ([]interface{}, error) { + if sdc.cfg == nil { + return nil, sdc.startErr + } + return sdc.cfg.aw.getScrapeWorkObjects(), nil +} + +// MustStart initializes sdc before its usage. // // swcFunc is used for constructing such objects. -func (sdc *SDConfig) GetScrapeWorkObjects(baseDir string, swcFunc ScrapeWorkConstructorFunc) ([]interface{}, error) { - cfg, err := getAPIConfig(sdc, baseDir, swcFunc) +func (sdc *SDConfig) MustStart(baseDir string, swcFunc ScrapeWorkConstructorFunc) { + cfg, err := newAPIConfig(sdc, baseDir, swcFunc) if err != nil { - return nil, fmt.Errorf("cannot create API config: %w", err) + sdc.startErr = fmt.Errorf("cannot create API config for kubernetes: %w", err) + return } - return cfg.aw.getScrapeWorkObjects(), nil + cfg.aw.mustStart() + sdc.cfg = cfg } // MustStop stops further usage for sdc. func (sdc *SDConfig) MustStop() { - v := configMap.Delete(sdc) - if v != nil { - // v can be nil if GetLabels wasn't called yet. - cfg := v.(*apiConfig) - cfg.mustStop() + if sdc.cfg != nil { + // sdc.cfg can be nil on MustStart error. + sdc.cfg.mustStop() } } diff --git a/lib/promscrape/scraper.go b/lib/promscrape/scraper.go index 861c35c4d..7dd454891 100644 --- a/lib/promscrape/scraper.go +++ b/lib/promscrape/scraper.go @@ -93,6 +93,7 @@ func runScraper(configFile string, pushData func(wr *prompbmarshal.WriteRequest) if err != nil { logger.Fatalf("cannot read %q: %s", configFile, err) } + cfg.mustStart() scs := newScrapeConfigs(pushData) scs.add("static_configs", 0, func(cfg *Config, swsPrev []*ScrapeWork) []*ScrapeWork { return cfg.getStaticScrapeWork() }) @@ -130,6 +131,7 @@ func runScraper(configFile string, pushData func(wr *prompbmarshal.WriteRequest) goto waitForChans } cfg.mustStop() + cfgNew.mustStart() cfg = cfgNew data = dataNew case <-tickerCh: @@ -143,6 +145,7 @@ func runScraper(configFile string, pushData func(wr *prompbmarshal.WriteRequest) goto waitForChans } cfg.mustStop() + cfgNew.mustStart() cfg = cfgNew data = dataNew case <-globalStopCh: From b59164cf33594da0085e1b291a1a8e02a01ada36 Mon Sep 17 00:00:00 2001 From: Lu Jiajing Date: Tue, 6 Apr 2021 03:25:31 +0800 Subject: [PATCH 48/63] fix access to nil *url.URL (#1180) * fix access to nil *url.URL Signed-off-by: Megrez Lu * Update lib/promscrape/discovery/kubernetes/api_watcher.go Co-authored-by: Aliaksandr Valialkin --- lib/promscrape/discovery/kubernetes/api_watcher.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/promscrape/discovery/kubernetes/api_watcher.go b/lib/promscrape/discovery/kubernetes/api_watcher.go index c78b334f8..d5def2f29 100644 --- a/lib/promscrape/discovery/kubernetes/api_watcher.go +++ b/lib/promscrape/discovery/kubernetes/api_watcher.go @@ -190,8 +190,12 @@ func newGroupWatcher(apiServer string, ac *promauth.Config, namespaces []string, } func getGroupWatcher(apiServer string, ac *promauth.Config, namespaces []string, selectors []Selector, proxyURL *url.URL) *groupWatcher { - key := fmt.Sprintf("apiServer=%s, namespaces=%s, selectors=%s, proxyURL=%v, authConfig=%s", - apiServer, namespaces, selectorsKey(selectors), proxyURL, ac.String()) + proxyURLStr := "" + if proxyURL != nil { + proxyURLStr = proxyURL.String() + } + key := fmt.Sprintf("apiServer=%s, namespaces=%s, selectors=%s, proxyURL=%s, authConfig=%s", + apiServer, namespaces, selectorsKey(selectors), proxyURLStr, ac.String()) groupWatchersLock.Lock() gw := groupWatchers[key] if gw == nil { From 4a0d06d1db83316103ca3f39b8b6949452b20ec0 Mon Sep 17 00:00:00 2001 From: Aliaksandr Valialkin Date: Mon, 5 Apr 2021 22:30:39 +0300 Subject: [PATCH 49/63] deployment/docker/docker-compose.yml: update Grafana from v7.5.1 to v7.5.2 --- deployment/docker/docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deployment/docker/docker-compose.yml b/deployment/docker/docker-compose.yml index 30b92563f..d20f1e08a 100644 --- a/deployment/docker/docker-compose.yml +++ b/deployment/docker/docker-compose.yml @@ -39,7 +39,7 @@ services: restart: always grafana: container_name: grafana - image: grafana/grafana:7.5.1 + image: grafana/grafana:7.5.2 depends_on: - "victoriametrics" ports: From 5f593b0ed36c40df7d41009629d6be516a683a1f Mon Sep 17 00:00:00 2001 From: Aliaksandr Valialkin Date: Mon, 5 Apr 2021 22:41:48 +0300 Subject: [PATCH 50/63] lib/promscrape/discovery/kubernetes: remove superflouos mustStart and mustStop functions --- lib/promscrape/discovery/kubernetes/api.go | 8 -------- lib/promscrape/discovery/kubernetes/kubernetes.go | 2 +- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/lib/promscrape/discovery/kubernetes/api.go b/lib/promscrape/discovery/kubernetes/api.go index 8afa6b613..fa39666a8 100644 --- a/lib/promscrape/discovery/kubernetes/api.go +++ b/lib/promscrape/discovery/kubernetes/api.go @@ -14,14 +14,6 @@ type apiConfig struct { aw *apiWatcher } -func (ac *apiConfig) mustStart() { - ac.aw.mustStart() -} - -func (ac *apiConfig) mustStop() { - ac.aw.mustStop() -} - func newAPIConfig(sdc *SDConfig, baseDir string, swcFunc ScrapeWorkConstructorFunc) (*apiConfig, error) { switch sdc.Role { case "node", "pod", "service", "endpoints", "endpointslices", "ingress": diff --git a/lib/promscrape/discovery/kubernetes/kubernetes.go b/lib/promscrape/discovery/kubernetes/kubernetes.go index 28160743b..ea827b47a 100644 --- a/lib/promscrape/discovery/kubernetes/kubernetes.go +++ b/lib/promscrape/discovery/kubernetes/kubernetes.go @@ -67,6 +67,6 @@ func (sdc *SDConfig) MustStart(baseDir string, swcFunc ScrapeWorkConstructorFunc func (sdc *SDConfig) MustStop() { if sdc.cfg != nil { // sdc.cfg can be nil on MustStart error. - sdc.cfg.mustStop() + sdc.cfg.aw.mustStop() } } From 78d35d4f4638258a897a0ced7b802a3f67e744d5 Mon Sep 17 00:00:00 2001 From: Aliaksandr Valialkin Date: Mon, 5 Apr 2021 23:01:45 +0300 Subject: [PATCH 51/63] Makefile: prepare `arm64` and `amd64` release archives for cluster version on `make release` command --- docs/CHANGELOG.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index f1294214e..ef059a234 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -3,19 +3,20 @@ # tip * FEATURE: vminsert and vmagent: add `-sortLabels` command-line flag for sorting metric labels before pushing them to `vmstorage`. This should reduce the size of `MetricName -> internal_series_id` cache (aka `vm_cache_size_bytes{type="storage/tsid"}`) when ingesting samples for the same time series with distinct order of labels. For example, `foo{k1="v1",k2="v2"}` and `foo{k2="v2",k1="v1"}` represent a single time series. Labels sorting is disabled by default, since the majority of established exporters preserve the order of labels for the exported metrics. -* FEATURE: allow specifying label value alongside label name for the `others sum` time series returned from `topk_*` and `bottomk_*` functions from [MetricsQL](https://victoriametrics.github.io/MetricsQL.html). For example, `topk_avg(3, max(process_resident_memory_bytes) by (instance), "instance=other_sum")` would return top 3 series from `max(process_resident_memory_bytes) by (instance)` plus a series containing of the sum of other series. The `others sum` series will have `{instance="other_sum"}` label. +* FEATURE: allow specifying label value alongside label name for the `others sum` time series returned from `topk_*` and `bottomk_*` functions from [MetricsQL](https://victoriametrics.github.io/MetricsQL.html). For example, `topk_avg(3, max(process_resident_memory_bytes) by (instance), "instance=other_sum")` would return top 3 series from `max(process_resident_memory_bytes) by (instance)` plus a series containing the sum of other series. The `others sum` series will have `{instance="other_sum"}` label. * FEATURE: do not delete `dst_label` when applying `label_copy(q, "src_label", "dst_label")` and `label_move(q, "src_label", "dst_label")` to series without `src_label` and with non-empty `dst_label`. See more details at [MetricsQL docs](https://victoriametrics.github.io/MetricsQL.html). * FEATURE: update Go builder from `v1.16.2` to `v1.16.3`. This should fix [these issues](https://github.com/golang/go/issues?q=milestone%3AGo1.16.3+label%3ACherryPickApproved). * FEATURE: vmagent: add support for `follow_redirects` option to `scrape_configs` section in the same way as [Prometheus 2.26 does](https://github.com/prometheus/prometheus/pull/8546). * FEATURE: vmagent: add support for `authorization` section in `-promscrape.config` in the same way as [Prometheus 2.26 does](https://github.com/prometheus/prometheus/pull/8512). * FEATURE: vmagent: add support for socks5 proxy in `proxy_url` config option. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/1177). -* FEATURE: vmagent: add support for `socks5 over tls` proxy in `proxy_url` config option. It can be set up with the following config: `proxy_url: tls+socks5://proxy-addr:port`. +* FEATURE: vmagent: add support for `socks5 over tls` proxy in `proxy_url` config option. It can be set up with the following config: `proxy_url: "tls+socks5://proxy-addr:port"`. * FEATURE: vmagent: reduce memory usage when `-remoteWrite.queues` is set to a big value. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/1167). * FEATURE: vmagent: add AWS IAM roles for tasks support for EC2 service discovery according to [these docs](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-iam-roles.html). * FEATURE: vmagent: add support for `proxy_tls_config`, `proxy_authorization`, `proxy_basic_auth`, `proxy_bearer_token` and `proxy_bearer_token_file` options in `consul_sd_config`, `dockerswarm_sd_config` and `eureka_sd_config` sections. * FEATURE: vmagent: pass `X-Prometheus-Scrape-Timeout-Seconds` header to scrape targets as Prometheus does. In this case scrape targets can limit the time needed for performing the scrape. See [this comment](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/1179#issuecomment-813118733) for details. -* FEATURE: vmagent: drop corrupted persistent queue files at `-remoteWrite.tmpDataPath` instead of throwing a fatal error. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/1030). +* FEATURE: vmagent: drop corrupted persistent queue files at `-remoteWrite.tmpDataPath` instead of throwing a fatal error. Corrupted files can appear after unclean shutdown of `vmagent` such as OOM kill or hardware reset. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/1030). * FEATURE: vmauth: add support for authorization via [bearer token](https://swagger.io/docs/specification/authentication/bearer-authentication/). See [the docs](https://victoriametrics.github.io/vmauth.html#auth-config) for details. +* FEATURE: publish `arm64` and `amd64` binaries for cluster version of VictoriaMetrics at [releases page](https://github.com/VictoriaMetrics/VictoriaMetrics/releases). * BUGFIX: vmagent: properly work with simple HTTP proxies which don't support `CONNECT` method. For example, [PushProx](https://github.com/prometheus-community/PushProx). See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/1179). * BUGFIX: vmagent: properly discover targets if multiple namespace selectors are put inside `kubernetes_sd_config`. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/1170). From 7d23598b33dfd127ab06b21bc03c89943aeaa3e9 Mon Sep 17 00:00:00 2001 From: Aliaksandr Valialkin Date: Mon, 5 Apr 2021 23:25:05 +0300 Subject: [PATCH 52/63] app/vmselect: return dumb response on `/api/v1/query_exemplars` request Updates https://github.com/VictoriaMetrics/VictoriaMetrics/issues/1186 --- app/vmselect/main.go | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/app/vmselect/main.go b/app/vmselect/main.go index b8c26c872..f6e86facc 100644 --- a/app/vmselect/main.go +++ b/app/vmselect/main.go @@ -351,23 +351,29 @@ func RequestHandler(w http.ResponseWriter, r *http.Request) bool { } return true case "/api/v1/rules": - // Return dumb placeholder + // Return dumb placeholder for https://prometheus.io/docs/prometheus/latest/querying/api/#rules rulesRequests.Inc() w.Header().Set("Content-Type", "application/json; charset=utf-8") fmt.Fprintf(w, "%s", `{"status":"success","data":{"groups":[]}}`) return true case "/api/v1/alerts": - // Return dumb placehloder + // Return dumb placehloder for https://prometheus.io/docs/prometheus/latest/querying/api/#alerts alertsRequests.Inc() w.Header().Set("Content-Type", "application/json; charset=utf-8") fmt.Fprintf(w, "%s", `{"status":"success","data":{"alerts":[]}}`) return true case "/api/v1/metadata": - // Return dumb placeholder + // Return dumb placeholder for https://prometheus.io/docs/prometheus/latest/querying/api/#querying-metric-metadata metadataRequests.Inc() w.Header().Set("Content-Type", "application/json; charset=utf-8") fmt.Fprintf(w, "%s", `{"status":"success","data":{}}`) return true + case "/api/v1/query_exemplars": + // Return dumb placeholder for https://prometheus.io/docs/prometheus/latest/querying/api/#querying-exemplars + queryExemplarsRequests.Inc() + w.Header().Set("Content-Type", "application/json; charset=utf-8") + fmt.Fprintf(w, "%s", `{"status":"success","data":[]}`) + return true case "/api/v1/admin/tsdb/delete_series": deleteRequests.Inc() authKey := r.FormValue("authKey") @@ -490,7 +496,8 @@ var ( graphiteTagsDelSeriesRequests = metrics.NewCounter(`vm_http_requests_total{path="/tags/delSeries"}`) graphiteTagsDelSeriesErrors = metrics.NewCounter(`vm_http_request_errors_total{path="/tags/delSeries"}`) - rulesRequests = metrics.NewCounter(`vm_http_requests_total{path="/api/v1/rules"}`) - alertsRequests = metrics.NewCounter(`vm_http_requests_total{path="/api/v1/alerts"}`) - metadataRequests = metrics.NewCounter(`vm_http_requests_total{path="/api/v1/metadata"}`) + rulesRequests = metrics.NewCounter(`vm_http_requests_total{path="/api/v1/rules"}`) + alertsRequests = metrics.NewCounter(`vm_http_requests_total{path="/api/v1/alerts"}`) + metadataRequests = metrics.NewCounter(`vm_http_requests_total{path="/api/v1/metadata"}`) + queryExemplarsRequests = metrics.NewCounter(`vm_http_requests_total{path="/api/v1/query_exemplars"}`) ) From 3ec6639bbb4ca52864f2114442db99c724374534 Mon Sep 17 00:00:00 2001 From: Aliaksandr Valialkin Date: Tue, 6 Apr 2021 11:11:40 +0300 Subject: [PATCH 53/63] lib/promscrape/discovery/kubernetes: register pending apiWatchers in uw.aws --- lib/promscrape/discovery/kubernetes/api_watcher.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/promscrape/discovery/kubernetes/api_watcher.go b/lib/promscrape/discovery/kubernetes/api_watcher.go index d5def2f29..e6c56212b 100644 --- a/lib/promscrape/discovery/kubernetes/api_watcher.go +++ b/lib/promscrape/discovery/kubernetes/api_watcher.go @@ -437,6 +437,9 @@ func (uw *urlWatcher) registerPendingAPIWatchers() { for aw := range uw.awsPending { awsPending = append(awsPending, aw) delete(uw.awsPending, aw) + if _, ok := uw.aws[aw]; !ok { + uw.aws[aw] = struct{}{} + } } uw.reloadScrapeWorksForAPIWatchers(awsPending, uw.objectsByKey) uw.mu.Unlock() From 59f9960992abbbd670acb6a53d10ec9b0378c9c4 Mon Sep 17 00:00:00 2001 From: Aliaksandr Valialkin Date: Wed, 7 Apr 2021 13:07:36 +0300 Subject: [PATCH 54/63] lib/promscrape/discovery: remove superflouos check in registerPendingAPIWatchers The check `_, ok := uw.aws[aw]; !ok` isn't needed, since aw cannot exist in uw.aws because of the check inside subscribeAPIWatcher --- lib/promscrape/discovery/kubernetes/api_watcher.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/promscrape/discovery/kubernetes/api_watcher.go b/lib/promscrape/discovery/kubernetes/api_watcher.go index e6c56212b..9e1c80830 100644 --- a/lib/promscrape/discovery/kubernetes/api_watcher.go +++ b/lib/promscrape/discovery/kubernetes/api_watcher.go @@ -437,9 +437,7 @@ func (uw *urlWatcher) registerPendingAPIWatchers() { for aw := range uw.awsPending { awsPending = append(awsPending, aw) delete(uw.awsPending, aw) - if _, ok := uw.aws[aw]; !ok { - uw.aws[aw] = struct{}{} - } + uw.aws[aw] = struct{}{} } uw.reloadScrapeWorksForAPIWatchers(awsPending, uw.objectsByKey) uw.mu.Unlock() From df32d2836c0710132174adf368c72023b1f71c29 Mon Sep 17 00:00:00 2001 From: Aliaksandr Valialkin Date: Wed, 7 Apr 2021 13:31:57 +0300 Subject: [PATCH 55/63] lib/storage: properly handle big time ranges passed to `/api/v1/labels` and `/api/v1/label//values` It should be faster querying all the labels and/or all the values instead of querying per-day labels/values on time ranges exceeding maxDaysForPerDaySearch --- docs/CHANGELOG.md | 1 + lib/storage/index_db.go | 18 ++++++++++-------- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index ef059a234..f1890a501 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -18,6 +18,7 @@ * FEATURE: vmauth: add support for authorization via [bearer token](https://swagger.io/docs/specification/authentication/bearer-authentication/). See [the docs](https://victoriametrics.github.io/vmauth.html#auth-config) for details. * FEATURE: publish `arm64` and `amd64` binaries for cluster version of VictoriaMetrics at [releases page](https://github.com/VictoriaMetrics/VictoriaMetrics/releases). +* BUGFIX: properly handle `/api/v1/labels` and `/api/v1/label//values` queries on big `start ... end` time range. This should fix big resource usage when VictoriaMetrics is queried with [Promxy](https://github.com/jacksontj/promxy) v0.0.62 or newer versions. * BUGFIX: vmagent: properly work with simple HTTP proxies which don't support `CONNECT` method. For example, [PushProx](https://github.com/prometheus-community/PushProx). See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/1179). * BUGFIX: vmagent: properly discover targets if multiple namespace selectors are put inside `kubernetes_sd_config`. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/1170). * BUGFIX: vmagent: properly discover `role: endpoints` and `role: endpointslices` targets in `kubernetes_sd_config`. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/1182). diff --git a/lib/storage/index_db.go b/lib/storage/index_db.go index eca59a20a..3210b4155 100644 --- a/lib/storage/index_db.go +++ b/lib/storage/index_db.go @@ -719,6 +719,9 @@ func (db *indexDB) SearchTagKeysOnTimeRange(tr TimeRange, maxTagKeys int, deadli func (is *indexSearch) searchTagKeysOnTimeRange(tks map[string]struct{}, tr TimeRange, maxTagKeys int) error { minDate := uint64(tr.MinTimestamp) / msecPerDay maxDate := uint64(tr.MaxTimestamp) / msecPerDay + if minDate > maxDate || maxDate-minDate > maxDaysForPerDaySearch { + return is.searchTagKeys(tks, maxTagKeys) + } var mu sync.Mutex var wg sync.WaitGroup var errGlobal error @@ -914,6 +917,9 @@ func (db *indexDB) SearchTagValuesOnTimeRange(tagKey []byte, tr TimeRange, maxTa func (is *indexSearch) searchTagValuesOnTimeRange(tvs map[string]struct{}, tagKey []byte, tr TimeRange, maxTagValues int) error { minDate := uint64(tr.MinTimestamp) / msecPerDay maxDate := uint64(tr.MaxTimestamp) / msecPerDay + if minDate > maxDate || maxDate-minDate > maxDaysForPerDaySearch { + return is.searchTagValues(tvs, tagKey, maxTagValues) + } var mu sync.Mutex var wg sync.WaitGroup var errGlobal error @@ -1126,7 +1132,7 @@ func (db *indexDB) SearchTagValueSuffixes(tr TimeRange, tagKey, tagValuePrefix [ func (is *indexSearch) searchTagValueSuffixesForTimeRange(tvss map[string]struct{}, tr TimeRange, tagKey, tagValuePrefix []byte, delimiter byte, maxTagValueSuffixes int) error { minDate := uint64(tr.MinTimestamp) / msecPerDay maxDate := uint64(tr.MaxTimestamp) / msecPerDay - if maxDate-minDate > maxDaysForDateMetricIDs { + if minDate > maxDate || maxDate-minDate > maxDaysForPerDaySearch { return is.searchTagValueSuffixesAll(tvss, tagKey, tagValuePrefix, delimiter, maxTagValueSuffixes) } // Query over multiple days in parallel. @@ -2673,7 +2679,7 @@ func (is *indexSearch) getMetricIDsForTimeRange(tr TimeRange, maxMetrics int) (* atomic.AddUint64(&is.db.dateMetricIDsSearchCalls, 1) minDate := uint64(tr.MinTimestamp) / msecPerDay maxDate := uint64(tr.MaxTimestamp) / msecPerDay - if maxDate-minDate > maxDaysForDateMetricIDs { + if minDate > maxDate || maxDate-minDate > maxDaysForPerDaySearch { // Too much dates must be covered. Give up. return nil, errMissingMetricIDsForDate } @@ -2722,17 +2728,13 @@ func (is *indexSearch) getMetricIDsForTimeRange(tr TimeRange, maxMetrics int) (* return metricIDs, nil } -const maxDaysForDateMetricIDs = 40 +const maxDaysForPerDaySearch = 40 func (is *indexSearch) tryUpdatingMetricIDsForDateRange(metricIDs *uint64set.Set, tfs *TagFilters, tr TimeRange, maxMetrics int) error { atomic.AddUint64(&is.db.dateRangeSearchCalls, 1) minDate := uint64(tr.MinTimestamp) / msecPerDay maxDate := uint64(tr.MaxTimestamp) / msecPerDay - if maxDate < minDate { - // Per-day inverted index doesn't cover the selected date range. - return fmt.Errorf("maxDate=%d cannot be smaller than minDate=%d", maxDate, minDate) - } - if maxDate-minDate > maxDaysForDateMetricIDs { + if minDate > maxDate || maxDate-minDate > maxDaysForPerDaySearch { // Too much dates must be covered. Give up, since it may be slow. return errFallbackToGlobalSearch } From c6a8ebb11f85d344f551ed5c87256fb3d6a05cb5 Mon Sep 17 00:00:00 2001 From: Roman Khavronenko Date: Wed, 7 Apr 2021 11:39:16 +0100 Subject: [PATCH 56/63] docs: update docs ordering and formatting (#1192) The major change is adding `sort` directive to docs. For those docs which are copied from internal packages `sort` is added via makefile command. For the rest it is added manually since they're updated manually as well. The rest of changes is connected with markdown formatting. For example, changing headers in some files (`##` => `#`) makes navigation on .github.io to look better. This especially useful for `changelog` docs. Table of contents for `vmctl` is dropped, since we already have it autogenerated on .github.io. No link changes expected. The corresponding PR to `cluster` branch will be made in follow-up PR. --- Makefile | 25 ++++++++---- README.md | 6 +-- app/vmagent/README.md | 2 +- app/vmalert/README.md | 38 +++++++++--------- app/vmauth/README.md | 2 +- app/vmbackup/README.md | 2 +- app/vmctl/README.md | 27 ------------- app/vmgateway/README.md | 16 ++++---- app/vmrestore/README.md | 2 +- docs/Articles.md | 4 ++ docs/BestPractices.md | 4 ++ docs/CHANGELOG.md | 56 ++++++++++++++------------- docs/CaseStudies.md | 4 ++ docs/Cluster-VictoriaMetrics.md | 6 ++- docs/FAQ.md | 4 ++ docs/Home.md | 4 ++ docs/MetricsQL.md | 4 ++ docs/Quick-Start.md | 4 ++ docs/Release-Guide.md | 6 ++- docs/SampleSizeCalculations.md | 17 +++++--- docs/Single-server-VictoriaMetrics.md | 10 +++-- docs/vmagent.md | 6 ++- docs/vmalert.md | 42 +++++++++++--------- docs/vmauth.md | 6 ++- docs/vmbackup.md | 6 ++- docs/vmctl.md | 31 ++------------- docs/vmgateway.md | 20 ++++++---- docs/vmrestore.md | 6 ++- 28 files changed, 196 insertions(+), 164 deletions(-) diff --git a/Makefile b/Makefile index a147a074b..6daefb453 100644 --- a/Makefile +++ b/Makefile @@ -262,12 +262,21 @@ golangci-lint: install-golangci-lint install-golangci-lint: which golangci-lint || curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(shell go env GOPATH)/bin v1.29.0 +copy-docs: + echo "---\nsort: ${ORDER}\n---\n" > ${DST} + cat ${SRC} >> ${DST} + +# Copies docs for all components and adds the order tag. +# Cluster docs are supposed to be ordered as 9th. +# For The rest of docs is ordered manually.t docs-sync: - cp app/vmagent/README.md docs/vmagent.md - cp app/vmalert/README.md docs/vmalert.md - cp app/vmauth/README.md docs/vmauth.md - cp app/vmbackup/README.md docs/vmbackup.md - cp app/vmrestore/README.md docs/vmrestore.md - cp app/vmctl/README.md docs/vmctl.md - cp app/vmgateway/README.md docs/vmgateway.md - cp README.md docs/Single-server-VictoriaMetrics.md + SRC=README.md DST=docs/Single-server-VictoriaMetrics.md ORDER=1 $(MAKE) copy-docs + SRC=app/vmagent/README.md DST=docs/vmagent.md ORDER=2 $(MAKE) copy-docs + SRC=app/vmalert/README.md DST=docs/vmalert.md ORDER=3 $(MAKE) copy-docs + SRC=app/vmauth/README.md DST=docs/vmauth.md ORDER=4 $(MAKE) copy-docs + SRC=app/vmbackup/README.md DST=docs/vmbackup.md ORDER=5 $(MAKE) copy-docs + SRC=app/vmrestore/README.md DST=docs/vmrestore.md ORDER=6 $(MAKE) copy-docs + SRC=app/vmctl/README.md DST=docs/vmctl.md ORDER=7 $(MAKE) copy-docs + SRC=app/vmgateway/README.md DST=docs/vmgateway.md ORDER=8 $(MAKE) copy-docs + + diff --git a/README.md b/README.md index 756b116eb..07f12a508 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ +# VictoriaMetrics + [![Latest Release](https://img.shields.io/github/release/VictoriaMetrics/VictoriaMetrics.svg?style=flat-square)](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/latest) [![Docker Pulls](https://img.shields.io/docker/pulls/victoriametrics/victoria-metrics.svg?maxAge=604800)](https://hub.docker.com/r/victoriametrics/victoria-metrics) [![Slack](https://img.shields.io/badge/join%20slack-%23victoriametrics-brightgreen.svg)](http://slack.victoriametrics.com/) @@ -6,9 +8,7 @@ [![Build Status](https://github.com/VictoriaMetrics/VictoriaMetrics/workflows/main/badge.svg)](https://github.com/VictoriaMetrics/VictoriaMetrics/actions) [![codecov](https://codecov.io/gh/VictoriaMetrics/VictoriaMetrics/branch/master/graph/badge.svg)](https://codecov.io/gh/VictoriaMetrics/VictoriaMetrics) -![Victoria Metrics logo](logo.png "Victoria Metrics") - -## VictoriaMetrics +Victoria Metrics logo VictoriaMetrics is a fast, cost-effective and scalable monitoring solution and time series database. diff --git a/app/vmagent/README.md b/app/vmagent/README.md index 7d1f41fe1..cfc77eda2 100644 --- a/app/vmagent/README.md +++ b/app/vmagent/README.md @@ -1,4 +1,4 @@ -## vmagent +# vmagent `vmagent` is a tiny but mighty agent which helps you collect metrics from various sources and store them in [VictoriaMetrics](https://github.com/VictoriaMetrics/VictoriaMetrics) diff --git a/app/vmalert/README.md b/app/vmalert/README.md index e075f27db..2f3daa046 100644 --- a/app/vmalert/README.md +++ b/app/vmalert/README.md @@ -1,10 +1,10 @@ -## vmalert +# vmalert `vmalert` executes a list of given [alerting](https://prometheus.io/docs/prometheus/latest/configuration/alerting_rules/) or [recording](https://prometheus.io/docs/prometheus/latest/configuration/recording_rules/) rules against configured address. -### Features: +## Features * Integration with [VictoriaMetrics](https://github.com/VictoriaMetrics/VictoriaMetrics) TSDB; * VictoriaMetrics [MetricsQL](https://victoriametrics.github.io/MetricsQL.html) support and expressions validation; @@ -15,7 +15,7 @@ rules against configured address. * Graphite datasource can be used for alerting and recording rules. See [these docs](#graphite) for details. * Lightweight without extra dependencies. -### Limitations: +## Limitations * `vmalert` execute queries against remote datasource which has reliability risks because of network. It is recommended to configure alerts thresholds and rules expressions with understanding that network request may fail; @@ -24,7 +24,7 @@ storage is asynchronous. Hence, user shouldn't rely on recording rules chaining recording rule is reused in next one; * `vmalert` has no UI, just an API for getting groups and rules statuses. -### QuickStart +## QuickStart To build `vmalert` from sources: ``` @@ -67,7 +67,7 @@ groups: [ - ] ``` -#### Groups +### Groups Each group has following attributes: ```yaml @@ -89,7 +89,7 @@ rules: [ - ... ] ``` -#### Rules +### Rules There are two types of Rules: * [alerting](https://prometheus.io/docs/prometheus/latest/configuration/alerting_rules/) - @@ -102,7 +102,7 @@ and save their result as a new set of time series. `vmalert` forbids to define duplicates - rules with the same combination of name, expression and labels within one group. -##### Alerting rules +#### Alerting rules The syntax for alerting rule is following: ```yaml @@ -131,7 +131,7 @@ annotations: [ : ] ``` -##### Recording rules +#### Recording rules The syntax for recording rules is following: ```yaml @@ -155,7 +155,7 @@ labels: For recording rules to work `-remoteWrite.url` must specified. -#### Alerts state on restarts +### Alerts state on restarts `vmalert` has no local storage, so alerts state is stored in the process memory. Hence, after reloading of `vmalert` the process alerts state will be lost. To avoid this situation, `vmalert` should be configured via the following flags: @@ -171,7 +171,7 @@ in configured `-remoteRead.url`, weren't updated in the last `1h` or received st rules configuration. -#### WEB +### WEB `vmalert` runs a web-server (`-httpListenAddr`) for serving metrics and alerts endpoints: * `http:///api/v1/groups` - list of all loaded groups and rules; @@ -182,7 +182,7 @@ Used as alert source in AlertManager. * `http:///-/reload` - hot configuration reload. -### Graphite +## Graphite vmalert sends requests to `<-datasource.url>/render?format=json` during evaluation of alerting and recording rules if the corresponding group or rule contains `type: "graphite"` config option. It is expected that the `<-datasource.url>/render` @@ -191,7 +191,7 @@ When using vmalert with both `graphite` and `prometheus` rules configured agains to set `-datasource.appendTypePrefix` flag to `true`, so vmalert can adjust URL prefix automatically based on query type. -### Configuration +## Configuration The shortlist of configuration flags is the following: ``` @@ -375,43 +375,43 @@ command-line flags with their descriptions. To reload configuration without `vmalert` restart send SIGHUP signal or send GET request to `/-/reload` endpoint. -### Contributing +## Contributing `vmalert` is mostly designed and built by VictoriaMetrics community. Feel free to share your experience and ideas for improving this software. Please keep simplicity as the main priority. -### How to build from sources +## How to build from sources It is recommended using [binary releases](https://github.com/VictoriaMetrics/VictoriaMetrics/releases) - `vmalert` is located in `vmutils-*` archives there. -#### Development build +### Development build 1. [Install Go](https://golang.org/doc/install). The minimum supported version is Go 1.15. 2. Run `make vmalert` from the root folder of [the repository](https://github.com/VictoriaMetrics/VictoriaMetrics). It builds `vmalert` binary and puts it into the `bin` folder. -#### Production build +### Production build 1. [Install docker](https://docs.docker.com/install/). 2. Run `make vmalert-prod` from the root folder of [the repository](https://github.com/VictoriaMetrics/VictoriaMetrics). It builds `vmalert-prod` binary and puts it into the `bin` folder. -#### ARM build +### ARM build ARM build may run on Raspberry Pi or on [energy-efficient ARM servers](https://blog.cloudflare.com/arm-takes-wing/). -#### Development ARM build +### Development ARM build 1. [Install Go](https://golang.org/doc/install). The minimum supported version is Go 1.15. 2. Run `make vmalert-arm` or `make vmalert-arm64` from the root folder of [the repository](https://github.com/VictoriaMetrics/VictoriaMetrics). It builds `vmalert-arm` or `vmalert-arm64` binary respectively and puts it into the `bin` folder. -#### Production ARM build +### Production ARM build 1. [Install docker](https://docs.docker.com/install/). 2. Run `make vmalert-arm-prod` or `make vmalert-arm64-prod` from the root folder of [the repository](https://github.com/VictoriaMetrics/VictoriaMetrics). diff --git a/app/vmauth/README.md b/app/vmauth/README.md index ce4ba1b7a..a241af857 100644 --- a/app/vmauth/README.md +++ b/app/vmauth/README.md @@ -1,4 +1,4 @@ -## vmauth +# vmauth `vmauth` is a simple auth proxy and router for [VictoriaMetrics](https://github.com/VictoriaMetrics/VictoriaMetrics). It reads username and password from [Basic Auth headers](https://en.wikipedia.org/wiki/Basic_access_authentication), diff --git a/app/vmbackup/README.md b/app/vmbackup/README.md index 811d71fae..6f9e4a95f 100644 --- a/app/vmbackup/README.md +++ b/app/vmbackup/README.md @@ -1,4 +1,4 @@ -## vmbackup +# vmbackup `vmbackup` creates VictoriaMetrics data backups from [instant snapshots](https://victoriametrics.github.io/Single-server-VictoriaMetrics.html#how-to-work-with-snapshots). diff --git a/app/vmctl/README.md b/app/vmctl/README.md index 61813ab52..f26acba85 100644 --- a/app/vmctl/README.md +++ b/app/vmctl/README.md @@ -9,33 +9,6 @@ Features: - [x] InfluxDB: migrate data from InfluxDB to VictoriaMetrics - [ ] Storage Management: data re-balancing between nodes -# Table of contents - -* [Articles](#articles) -* [How to build](#how-to-build) -* [Migrating data from InfluxDB 1.x](#migrating-data-from-influxdb-1x) - * [Data mapping](#data-mapping) - * [Configuration](#configuration) - * [Filtering](#filtering) -* [Migrating data from InfluxDB 2.x](#migrating-data-from-influxdb-2x) -* [Migrating data from Prometheus](#migrating-data-from-prometheus) - * [Data mapping](#data-mapping-1) - * [Configuration](#configuration-1) - * [Filtering](#filtering-1) -* [Migrating data from Thanos](#migrating-data-from-thanos) - * [Current data](#current-data) - * [Historical data](#historical-data) -* [Migrating data from VictoriaMetrics](#migrating-data-from-victoriametrics) - * [Native protocol](#native-protocol) -* [Tuning](#tuning) - * [Influx mode](#influx-mode) - * [Prometheus mode](#prometheus-mode) - * [VictoriaMetrics importer](#victoriametrics-importer) - * [Importer stats](#importer-stats) -* [Significant figures](#significant-figures) -* [Adding extra labels](#adding-extra-labels) - - ## Articles * [How to migrate data from Prometheus](https://medium.com/@romanhavronenko/victoriametrics-how-to-migrate-data-from-prometheus-d44a6728f043) diff --git a/app/vmgateway/README.md b/app/vmgateway/README.md index e9e5ef571..1a599230a 100644 --- a/app/vmgateway/README.md +++ b/app/vmgateway/README.md @@ -1,4 +1,4 @@ -## vmgateway +# vmgateway vmgateway @@ -15,7 +15,7 @@ `vmgateway` is included in an [enterprise package](https://victoriametrics.com/enterprise.html). -### Access Control +## Access Control vmgateway-ac @@ -45,7 +45,7 @@ Where: - `extra_labels` - optional, key-value pairs for label filters - added to ingested or selected metrics. - `mode` - optional, access mode for api - read, write, full. supported values: 0 - full (default value), 1 - read, 2 - write. -#### QuickStart +## QuickStart Start single version of Victoria Metrics @@ -74,7 +74,7 @@ curl 'http://localhost:8431/api/v1/series/count' -H 'Authorization: Bearer incor ``` -### Rate Limiter +## Rate Limiter vmgateway-rl @@ -112,7 +112,7 @@ limits: account_id: 1 ``` -#### QuickStart +## QuickStart cluster version required for rate limiting. ```bash @@ -162,7 +162,7 @@ curl 'http://localhost:8431/api/v1/labels' -H 'Authorization: Bearer eyJhbGciOiJ # check rate limit ``` -### Configuration +## Configuration The shortlist of configuration flags is the following: ```bash @@ -269,7 +269,7 @@ The shortlist of configuration flags is the following: ``` -### TroubleShooting +## TroubleShooting * Access control: * incorrect `jwt` format, try https://jwt.io/#debugger-io with our tokens @@ -278,7 +278,7 @@ The shortlist of configuration flags is the following: * `scrape_interval` at datasource, reduce it to apply limits faster. -### Limitations +## Limitations * Access Control: * `jwt` token must be validated by external system, currently `vmgateway` can't validate the signature. diff --git a/app/vmrestore/README.md b/app/vmrestore/README.md index 4abd1bc9c..2f8f713cb 100644 --- a/app/vmrestore/README.md +++ b/app/vmrestore/README.md @@ -1,4 +1,4 @@ -## vmrestore +# vmrestore `vmrestore` restores data from backups created by [vmbackup](https://victoriametrics.github.io/vbackup.html). VictoriaMetrics `v1.29.0` and newer versions must be used for working with the restored data. diff --git a/docs/Articles.md b/docs/Articles.md index a17cbc919..bd69f2e87 100644 --- a/docs/Articles.md +++ b/docs/Articles.md @@ -1,3 +1,7 @@ +--- +sort: 16 +--- + # Articles ## Third-party articles and slides about VictoriaMetrics diff --git a/docs/BestPractices.md b/docs/BestPractices.md index 04cc8bb7b..480ee6c38 100644 --- a/docs/BestPractices.md +++ b/docs/BestPractices.md @@ -1,3 +1,7 @@ +--- +sort: 12 +--- + # VM best practices VictoriaMetrics is a fast, cost-effective and scalable monitoring solution and time series database. It can be used as a long-term, remote storage for Prometheus which allows it to gather metrics from different systems and store them in a single location or separate them for different purposes (short-, long-term, responsibility zones etc). diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index f1890a501..26aac290b 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -1,6 +1,10 @@ +--- +sort: 13 +--- + # CHANGELOG -# tip +## tip * FEATURE: vminsert and vmagent: add `-sortLabels` command-line flag for sorting metric labels before pushing them to `vmstorage`. This should reduce the size of `MetricName -> internal_series_id` cache (aka `vm_cache_size_bytes{type="storage/tsid"}`) when ingesting samples for the same time series with distinct order of labels. For example, `foo{k1="v1",k2="v2"}` and `foo{k2="v2",k1="v1"}` represent a single time series. Labels sorting is disabled by default, since the majority of established exporters preserve the order of labels for the exported metrics. * FEATURE: allow specifying label value alongside label name for the `others sum` time series returned from `topk_*` and `bottomk_*` functions from [MetricsQL](https://victoriametrics.github.io/MetricsQL.html). For example, `topk_avg(3, max(process_resident_memory_bytes) by (instance), "instance=other_sum")` would return top 3 series from `max(process_resident_memory_bytes) by (instance)` plus a series containing the sum of other series. The `others sum` series will have `{instance="other_sum"}` label. @@ -25,7 +29,7 @@ * BUGFIX: properly generate filename for `*.tar.gz` archive inside `_checksums.txt` file posted at [releases page](https://github.com/VictoriaMetrics/VictoriaMetrics/releases). See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/1171). -# [v1.57.1](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.57.1) +## [v1.57.1](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.57.1) * FEATURE: publish vmutils for `GOOS=arm` on [releases page](https://github.com/VictoriaMetrics/VictoriaMetrics/releases). @@ -34,7 +38,7 @@ * BUGFIX: vminsert: return back `type` label to per-tenant metric `vm_tenant_inserted_rows_total`. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/932). -# [v1.57.0](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.57.0) +## [v1.57.0](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.57.0) * FEATURE: optimize query performance by up to 10x on systems with many CPU cores. See [this tweet](https://twitter.com/MetricsVictoria/status/1375064484860067840). * FEATURE: add the following metrics at `/metrics` page for every VictoraMetrics app: @@ -57,7 +61,7 @@ * BUGFIX: properly calculate `summarize` and `*Series` functions in [Graphite Render API](https://victoriametrics.github.io/#graphite-render-api-usage). -# [v1.56.0](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.56.0) +## [v1.56.0](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.56.0) * FEATURE: add the following functions to [MetricsQL](https://victoriametrics.github.io/MetricsQL.html): - `histogram_avg(buckets)` - returns the average value for the given buckets. @@ -86,13 +90,13 @@ * BUGFIX: do not crash if a query contains `histogram_over_time()` function name with uppercase chars. For example, `Histogram_Over_Time(m[5m])`. -# [v1.55.1](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.55.1) +## [v1.55.1](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.55.1) * BUGFIX: vmagent: fix a panic in Kubernetes service discovery when a target is filtered out with relabeling. See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/1107 * BUGFIX: vmagent: fix Kubernetes service discovery for `role: ingress`. See https://github.com/VictoriaMetrics/VictoriaMetrics/pull/1110 -# [v1.55.0](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.55.0) +## [v1.55.0](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.55.0) * FEATURE: add `sign(q)` and `clamp(q, min, max)` functions, which are planned to be added in [the upcoming Prometheus release](https://twitter.com/roidelapluie/status/1363428376162295811) . The `last_over_time(m[d])` function is already supported in [MetricsQL](https://victoriametrics.github.io/MetricsQL.html). @@ -128,12 +132,12 @@ * BUGFIX: unescape only `\\`, `\n` and `\"` in label names when parsing Prometheus text exposition format as Prometheus does. Previously other escape sequences could be improperly unescaped. -# [v1.54.1](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.54.1) +## [v1.54.1](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.54.1) * BUGFIX: properly handle queries containing a filter on metric name plus any number of negative filters and zero non-negative filters. For example, `node_cpu_seconds_total{mode!="idle"}`. The bug was introduced in [v1.54.0](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.54.0). -# [v1.54.0](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.54.0) +## [v1.54.0](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.54.0) * FEATURE: optimize searching for matching metrics for `metric{}` queries if `` contains at least a single filter. For example, the query `up{job="foobar"}` should find the matching time series much faster than previously. * FEATURE: reduce execution times for `q1 q2` queries by executing `q1` and `q2` in parallel. @@ -154,12 +158,12 @@ * BUGFIX: vmagent: return back unsent block to the queue during graceful shutdown. Previously this block could be dropped if remote storage is unavailable during vmagent shutdown. See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/1065 . -# [v1.53.1](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.53.1) +## [v1.53.1](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.53.1) * BUGFIX: vmselect: fix the bug peventing from proper searching by Graphite filter with wildcards such as `{__graphite__="foo.*.bar"}`. -# [v1.53.0](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.53.0) +## [v1.53.0](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.53.0) * FEATURE: added [vmctl tool](https://victoriametrics.github.io/vmctl.html) to VictoriaMetrics release process. Now it is packaged in `vmutils-*.tar.gz` archive on [the releases page](https://github.com/VictoriaMetrics/VictoriaMetrics/releases). Source code for `vmctl` tool has been moved from [github.com/VictoriaMetrics/vmctl](https://github.com/VictoriaMetrics/vmctl) to [github.com/VictoriaMetrics/VictoriaMetrics/app/vmctl](https://github.com/VictoriaMetrics/VictoriaMetrics/tree/master/app/vmctl). * FEATURE: added `-loggerTimezone` command-line flag for adjusting time zone for timestamps in log messages. By default UTC is used. @@ -184,7 +188,7 @@ in front of VictoriaMetrics. [Contact us](mailto:sales@victoriametrics.com) if y * BUGFIX: vmagent: retry scrape and service discovery requests when the remote server closes HTTP keep-alive connection. Previously `disable_keepalive: true` option could be used under `scrape_configs` section when working with such servers. -# [v1.52.0](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.52.0) +## [v1.52.0](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.52.0) * FEATURE: provide a sample list of alerting rules for VictoriaMetrics components. It is available [here](https://github.com/VictoriaMetrics/VictoriaMetrics/blob/master/deployment/docker/alerts.yml). * FEATURE: disable final merge for data for the previous month at the beginning of new month, since it may result in high disk IO and CPU usage. Final merge can be enabled by setting `-finalMergeDelay` command-line flag to positive duration. @@ -204,7 +208,7 @@ in front of VictoriaMetrics. [Contact us](mailto:sales@victoriametrics.com) if y * BUGFIX: upgrade base image for Docker packages from Alpine 3.12.1 to Alpine 3.12.3 in order to fix potential security issues. See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/1010 -# [v1.51.0](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.51.0) +## [v1.51.0](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.51.0) * FEATURE: add `/api/v1/status/top_queries` handler, which returns the most frequently executed queries and queries that took the most time for execution. See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/907 * FEATURE: vmagent: add support for `proxy_url` config option in Prometheus scrape configs. See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/503 @@ -216,23 +220,23 @@ in front of VictoriaMetrics. [Contact us](mailto:sales@victoriametrics.com) if y * BUGFIX: do not adjust `offset` value provided in MetricsQL query. Previously it could be modified in order to improve response cache hit ratio. This is unneeded, since cache hit ratio should remain good because the query time range should be already aligned to multiple of `step` values. See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/976 -# [v1.50.2](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.50.2) +## [v1.50.2](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.50.2) * FEATURE: do not publish duplicate Docker images with `-cluster` tag suffix for [vmagent](https://victoriametrics.github.io/vmagent.html), [vmalert](https://victoriametrics.github.io/vmalert.html), [vmauth](https://victoriametrics.github.io/vmauth.html), [vmbackup](https://victoriametrics.github.io/vmbackup.html) and [vmrestore](https://victoriametrics.github.io/vmrestore.html), since they are identical to images without `-cluster` tag suffix. * BUGFIX: vmalert: properly populate template variables. This has been broken in v1.50.0. See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/974 * BUGFIX: properly parse negative combined duration in MetricsQL such as `-1h3m4s`. It must be parsed as `-(1h + 3m + 4s)`. Prevsiously it was parsed as `-1h + 3m + 4s`. -* BUGFIX: properly parse lines in [Prometheus exposition format](https://github.com/prometheus/docs/blob/master/content/docs/instrumenting/exposition_formats.md) and in [OpenMetrics format](https://github.com/OpenObservability/OpenMetrics/blob/master/specification/OpenMetrics.md) with whitespace after the timestamp. For example, `foo 123 456 # some comment here`. See https://github.com/VictoriaMetrics/VictoriaMetrics/pull/970 +* BUGFIX: properly parse lines in [Prometheus exposition format](https://github.com/prometheus/docs/blob/master/content/docs/instrumenting/exposition_formats.md) and in [OpenMetrics format](https://github.com/OpenObservability/OpenMetrics/blob/master/specification/OpenMetrics.md) with whitespace after the timestamp. For example, `foo 123 456 ## some comment here`. See https://github.com/VictoriaMetrics/VictoriaMetrics/pull/970 -# [v1.50.1](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.50.1) +## [v1.50.1](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.50.1) * FEATURE: vmagent: export `vmagent_remotewrite_blocks_sent_total` and `vmagent_remotewrite_blocks_sent_total` metrics for each `-remoteWrite.url`. * BUGFIX: vmagent: properly delete unregistered scrape targets from `/targets` and `/api/v1/targets` pages. They weren't deleted due to the bug in `v1.50.0`. -# [v1.50.0](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.50.0) +## [v1.50.0](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.50.0) * FEATURE: automatically reset response cache when samples with timestamps older than `now - search.cacheTimestampOffset` are ingested to VictoriaMetrics. This makes unnecessary disabling response cache during data backfilling or resetting it after backfilling is complete as described [in these docs](https://victoriametrics.github.io/#backfilling). This feature applies only to single-node VictoriaMetrics. It doesn't apply to cluster version of VictoriaMetrics because `vminsert` nodes don't know about `vmselect` nodes where the response cache must be reset. * FEATURE: vmalert: add `query`, `first` and `value` functions to alert templates. See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/539 @@ -257,7 +261,7 @@ in front of VictoriaMetrics. [Contact us](mailto:sales@victoriametrics.com) if y * BUGFIX: assume the previous value is 0 when calculating `increase()` for the first point on the graph if its value doesn't exceed 100 and the delta between two first points equals to 0. See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/962 -# [v1.49.0](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.49.0) +## [v1.49.0](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.49.0) * FEATURE: optimize Consul service discovery speed when discovering big number of services. See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/574 * FEATURE: add `label_uppercase(q, label1, ... labelN)` and `label_lowercase(q, label1, ... labelN)` function to [MetricsQL](https://victoriametrics.github.io/MetricsQL.html) @@ -277,7 +281,7 @@ in front of VictoriaMetrics. [Contact us](mailto:sales@victoriametrics.com) if y `days_in_month`, `hour`, `month` and `year`. -# [v1.48.0](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.48.0) +## [v1.48.0](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.48.0) * FEATURE: added [Snap package for single-node VictoriaMetrics](https://snapcraft.io/victoriametrics). This simplifies installation under Ubuntu to a single command: ```bash @@ -298,12 +302,12 @@ in front of VictoriaMetrics. [Contact us](mailto:sales@victoriametrics.com) if y * FEATURE: log metric name plus all its labels when the metric timestamp is out of the configured retention. This should simplify detecting the source of metrics with unexpected timestamps. * FEATURE: add `-dryRun` command-line flag to single-node VictoriaMetrics in order to check config file pointed by `-promscrape.config`. -* BUGFIX: properly parse Prometheus metrics with [exemplars](https://github.com/OpenObservability/OpenMetrics/blob/master/OpenMetrics.md#exemplars-1) such as `foo 123 # {bar="baz"} 1`. +* BUGFIX: properly parse Prometheus metrics with [exemplars](https://github.com/OpenObservability/OpenMetrics/blob/master/OpenMetrics.md#exemplars-1) such as `foo 123 ## {bar="baz"} 1`. * BUGFIX: properly parse "infinity" values in [OpenMetrics format](https://github.com/OpenObservability/OpenMetrics/blob/master/OpenMetrics.md#abnf). See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/924 -# [v1.47.0](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.47.0) +## [v1.47.0](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.47.0) * FEATURE: vmselect: return the original error from `vmstorage` node in query response if `-search.denyPartialResponse` is set. See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/891 @@ -333,7 +337,7 @@ in front of VictoriaMetrics. [Contact us](mailto:sales@victoriametrics.com) if y * BUGFIX: vminsert: properly return HTTP 503 status code when all the vmstorage nodes are unavailable. See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/896 -# [v1.46.0](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.46.0) +## [v1.46.0](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.46.0) * FEATURE: optimize requests to `/api/v1/labels` and `/api/v1/label//values` when `start` and `end` args are set. * FEATURE: reduce memory usage when query touches big number of time series. @@ -351,7 +355,7 @@ in front of VictoriaMetrics. [Contact us](mailto:sales@victoriametrics.com) if y See https://github.com/VictoriaMetrics/VictoriaMetrics/pull/883 -# [v1.45.0](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.45.0) +## [v1.45.0](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.45.0) * FEATURE: allow setting `-retentionPeriod` smaller than one month. I.e. `-retentionPeriod=3d`, `-retentionPeriod=2w`, etc. is supported now. See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/173 @@ -385,7 +389,7 @@ in front of VictoriaMetrics. [Contact us](mailto:sales@victoriametrics.com) if y * BUGFIX: vmagent: properly handle 301 redirects. See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/869 -# [v1.44.0](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.44.0) +## [v1.44.0](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.44.0) * FEATURE: automatically add missing label filters to binary operands as described at https://utcc.utoronto.ca/~cks/space/blog/sysadmin/PrometheusLabelNonOptimization . This should improve performance for queries with missing label filters in binary operands. For example, the following query should work faster now, because it shouldn't @@ -445,7 +449,7 @@ in front of VictoriaMetrics. [Contact us](mailto:sales@victoriametrics.com) if y * BUGFIX: fix `mode_over_time(m[d])` calculations. Previously the function could return incorrect results. -# [v1.43.0](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.43.0) +## [v1.43.0](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.43.0) * FEATURE: reduce CPU usage for repeated queries over sliding time window when no new time series are added to the database. Typical use cases: repeated evaluation of alerting rules in [vmalert](https://victoriametrics.github.io/vmalert.html) or dashboard auto-refresh in Grafana. @@ -464,7 +468,7 @@ in front of VictoriaMetrics. [Contact us](mailto:sales@victoriametrics.com) if y * BUGFIX: support parsing floating-point timestamp like Graphite Carbon does. Such timestmaps are truncated to seconds. -# [v1.42.0](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.42.0) +## [v1.42.0](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.42.0) * FEATURE: use all the available CPU cores when accepting data via a single TCP connection for [all the supported protocols](https://victoriametrics.github.io/#how-to-import-time-series-data). @@ -494,6 +498,6 @@ in front of VictoriaMetrics. [Contact us](mailto:sales@victoriametrics.com) if y In this case only the node must be returned with stripped dot in the end of id as carbonapi does. -# Previous releases +## Previous releases See [releases page](https://github.com/VictoriaMetrics/VictoriaMetrics/releases). diff --git a/docs/CaseStudies.md b/docs/CaseStudies.md index 6b54ef38a..baccc44a3 100644 --- a/docs/CaseStudies.md +++ b/docs/CaseStudies.md @@ -1,3 +1,7 @@ +--- +sort: 17 +--- + # Case studies and talks Below please find public case studies and talks from VictoriaMetrics users. You can also join our [community Slack channel](http://slack.victoriametrics.com/) diff --git a/docs/Cluster-VictoriaMetrics.md b/docs/Cluster-VictoriaMetrics.md index 88ed91695..03013a3d0 100644 --- a/docs/Cluster-VictoriaMetrics.md +++ b/docs/Cluster-VictoriaMetrics.md @@ -1,6 +1,10 @@ +--- +sort: 9 +--- + # Cluster version -Victoria Metrics +Victoria Metrics logo VictoriaMetrics is a fast, cost-effective and scalable time series database. It can be used as a long-term remote storage for Prometheus. diff --git a/docs/FAQ.md b/docs/FAQ.md index 882ee80b8..aa8295c88 100644 --- a/docs/FAQ.md +++ b/docs/FAQ.md @@ -1,3 +1,7 @@ +--- +sort: 18 +--- + # FAQ ### What is the main purpose of VictoriaMetrics? diff --git a/docs/Home.md b/docs/Home.md index 4cf04ca97..944eea014 100644 --- a/docs/Home.md +++ b/docs/Home.md @@ -1,3 +1,7 @@ +--- +sort: 19 +--- + # Docs * [Quick start](Quick-Start) diff --git a/docs/MetricsQL.md b/docs/MetricsQL.md index 514816a36..cd86c984e 100644 --- a/docs/MetricsQL.md +++ b/docs/MetricsQL.md @@ -1,3 +1,7 @@ +--- +sort: 11 +--- + # MetricsQL VictoriaMetrics implements MetricsQL - query language inspired by [PromQL](https://prometheus.io/docs/prometheus/latest/querying/basics/). diff --git a/docs/Quick-Start.md b/docs/Quick-Start.md index e2f0f085c..33a93eb6f 100644 --- a/docs/Quick-Start.md +++ b/docs/Quick-Start.md @@ -1,3 +1,7 @@ +--- +sort: 10 +--- + # Quick Start 1. If you run Ubuntu please run the `snap install victoriametrics` command to install and start VictoriaMetrics. Then read [these docs](https://snapcraft.io/victoriametrics). diff --git a/docs/Release-Guide.md b/docs/Release-Guide.md index 29e607d5c..1000f0ac2 100644 --- a/docs/Release-Guide.md +++ b/docs/Release-Guide.md @@ -1,4 +1,8 @@ -Release process guidance +--- +sort: 14 +--- + +# Release process guidance ## Release version and Docker images diff --git a/docs/SampleSizeCalculations.md b/docs/SampleSizeCalculations.md index 9881f52d4..75ebb3599 100644 --- a/docs/SampleSizeCalculations.md +++ b/docs/SampleSizeCalculations.md @@ -1,3 +1,7 @@ +--- +sort: 15 +--- + # Sample size calculations These calculations are for the “Lowest sample size” graph at https://victoriametrics.com/ . @@ -14,7 +18,7 @@ That means each metric will contain 6307200 points. 2tb disk contains 2 (tb) * 1024 (gb) * 1024 (mb) * 1024 (kb) * 1024 (b) = 2199023255552 bytes -# VictoriaMetrics +## VictoriaMetrics Based on production data from our customers, sample size is 0.4 byte That means one metric with 10 seconds resolution will need 6307200 points * 0.4 bytes/point = 2522880 bytes or 2.4 megabytes. @@ -22,13 +26,13 @@ Calculation for number of metrics can be stored in 2 tb disk: 2199023255552 (disk size) / 2522880 (one metric for 2 year) = 871632 metrics So in 2tb we can store 871 632 metrics -# Graphite +## Graphite Based on https://m30m.github.io/whisper-calculator/ sample size of graphite metrics is 12b + 28b for each metric That means, one metric with 10 second resolution will need 75686428 bytes or 72.18 megabytes Calculation for number of metrics can be stored in 2 tb disk: 2199023255552 / 75686428 = 29 054 metrics -# OpenTSDB +## OpenTSDB Let's check official openTSDB site http://opentsdb.net/faq.html 16 bytes of HBase overhead, 3 bytes for the metric, 4 bytes for the timestamp, 6 bytes per tag, 2 bytes of OpenTSDB overhead, up to 8 bytes for the value. Integers are stored with variable length encoding and can consume 1, 2, 4 or 8 bytes. @@ -46,14 +50,15 @@ Also, openTSDB allows to use compression So, let's multiply numbers on 4.2 69 730 * 4,2 = 292 866 metrics for best scenario 29 054 * 4,2 = 122 026 metrics for worst scenario -# m3db + +## M3DB Let's look at official m3db site https://m3db.github.io/m3/m3db/architecture/engine/ They can achieve a sample size of 1.45 bytes/datapoint That means, one metric with 10 second resolution will need 9145440 bytes or 8,72177124 megabytes Calculation for number of metrics can be stored in 2 tb disk: 2199023255552 / 9145440 = 240 450 metrics -# InfluxDB +## InfluxDB Based on official influxDB site https://docs.influxdata.com/influxdb/v1.8/guides/hardware_sizing/#bytes-and-compression "Non-string values require approximately three bytes". That means, one metric with 10 second resolution will need 6307200 * 3 = 18921600 bytes or 18 megabytes @@ -61,7 +66,7 @@ Calculation for number of metrics can be stored in 2 tb disk: 2199023255552 / 18921600 = 116 217 metrics -# Prometheus +## Prometheus Let's check official site: https://prometheus.io/docs/prometheus/latest/storage/ "On average, Prometheus uses only around 1-2 bytes per sample." That means, one metric with 10 second resolution will need diff --git a/docs/Single-server-VictoriaMetrics.md b/docs/Single-server-VictoriaMetrics.md index 756b116eb..9560e2dc2 100644 --- a/docs/Single-server-VictoriaMetrics.md +++ b/docs/Single-server-VictoriaMetrics.md @@ -1,3 +1,9 @@ +--- +sort: 1 +--- + +# VictoriaMetrics + [![Latest Release](https://img.shields.io/github/release/VictoriaMetrics/VictoriaMetrics.svg?style=flat-square)](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/latest) [![Docker Pulls](https://img.shields.io/docker/pulls/victoriametrics/victoria-metrics.svg?maxAge=604800)](https://hub.docker.com/r/victoriametrics/victoria-metrics) [![Slack](https://img.shields.io/badge/join%20slack-%23victoriametrics-brightgreen.svg)](http://slack.victoriametrics.com/) @@ -6,9 +12,7 @@ [![Build Status](https://github.com/VictoriaMetrics/VictoriaMetrics/workflows/main/badge.svg)](https://github.com/VictoriaMetrics/VictoriaMetrics/actions) [![codecov](https://codecov.io/gh/VictoriaMetrics/VictoriaMetrics/branch/master/graph/badge.svg)](https://codecov.io/gh/VictoriaMetrics/VictoriaMetrics) -![Victoria Metrics logo](logo.png "Victoria Metrics") - -## VictoriaMetrics +Victoria Metrics logo VictoriaMetrics is a fast, cost-effective and scalable monitoring solution and time series database. diff --git a/docs/vmagent.md b/docs/vmagent.md index 7d1f41fe1..e610ed05e 100644 --- a/docs/vmagent.md +++ b/docs/vmagent.md @@ -1,4 +1,8 @@ -## vmagent +--- +sort: 2 +--- + +# vmagent `vmagent` is a tiny but mighty agent which helps you collect metrics from various sources and store them in [VictoriaMetrics](https://github.com/VictoriaMetrics/VictoriaMetrics) diff --git a/docs/vmalert.md b/docs/vmalert.md index e075f27db..c1e912aa7 100644 --- a/docs/vmalert.md +++ b/docs/vmalert.md @@ -1,10 +1,14 @@ -## vmalert +--- +sort: 3 +--- + +# vmalert `vmalert` executes a list of given [alerting](https://prometheus.io/docs/prometheus/latest/configuration/alerting_rules/) or [recording](https://prometheus.io/docs/prometheus/latest/configuration/recording_rules/) rules against configured address. -### Features: +## Features * Integration with [VictoriaMetrics](https://github.com/VictoriaMetrics/VictoriaMetrics) TSDB; * VictoriaMetrics [MetricsQL](https://victoriametrics.github.io/MetricsQL.html) support and expressions validation; @@ -15,7 +19,7 @@ rules against configured address. * Graphite datasource can be used for alerting and recording rules. See [these docs](#graphite) for details. * Lightweight without extra dependencies. -### Limitations: +## Limitations * `vmalert` execute queries against remote datasource which has reliability risks because of network. It is recommended to configure alerts thresholds and rules expressions with understanding that network request may fail; @@ -24,7 +28,7 @@ storage is asynchronous. Hence, user shouldn't rely on recording rules chaining recording rule is reused in next one; * `vmalert` has no UI, just an API for getting groups and rules statuses. -### QuickStart +## QuickStart To build `vmalert` from sources: ``` @@ -67,7 +71,7 @@ groups: [ - ] ``` -#### Groups +### Groups Each group has following attributes: ```yaml @@ -89,7 +93,7 @@ rules: [ - ... ] ``` -#### Rules +### Rules There are two types of Rules: * [alerting](https://prometheus.io/docs/prometheus/latest/configuration/alerting_rules/) - @@ -102,7 +106,7 @@ and save their result as a new set of time series. `vmalert` forbids to define duplicates - rules with the same combination of name, expression and labels within one group. -##### Alerting rules +#### Alerting rules The syntax for alerting rule is following: ```yaml @@ -131,7 +135,7 @@ annotations: [ : ] ``` -##### Recording rules +#### Recording rules The syntax for recording rules is following: ```yaml @@ -155,7 +159,7 @@ labels: For recording rules to work `-remoteWrite.url` must specified. -#### Alerts state on restarts +### Alerts state on restarts `vmalert` has no local storage, so alerts state is stored in the process memory. Hence, after reloading of `vmalert` the process alerts state will be lost. To avoid this situation, `vmalert` should be configured via the following flags: @@ -171,7 +175,7 @@ in configured `-remoteRead.url`, weren't updated in the last `1h` or received st rules configuration. -#### WEB +### WEB `vmalert` runs a web-server (`-httpListenAddr`) for serving metrics and alerts endpoints: * `http:///api/v1/groups` - list of all loaded groups and rules; @@ -182,7 +186,7 @@ Used as alert source in AlertManager. * `http:///-/reload` - hot configuration reload. -### Graphite +## Graphite vmalert sends requests to `<-datasource.url>/render?format=json` during evaluation of alerting and recording rules if the corresponding group or rule contains `type: "graphite"` config option. It is expected that the `<-datasource.url>/render` @@ -191,7 +195,7 @@ When using vmalert with both `graphite` and `prometheus` rules configured agains to set `-datasource.appendTypePrefix` flag to `true`, so vmalert can adjust URL prefix automatically based on query type. -### Configuration +## Configuration The shortlist of configuration flags is the following: ``` @@ -375,43 +379,43 @@ command-line flags with their descriptions. To reload configuration without `vmalert` restart send SIGHUP signal or send GET request to `/-/reload` endpoint. -### Contributing +## Contributing `vmalert` is mostly designed and built by VictoriaMetrics community. Feel free to share your experience and ideas for improving this software. Please keep simplicity as the main priority. -### How to build from sources +## How to build from sources It is recommended using [binary releases](https://github.com/VictoriaMetrics/VictoriaMetrics/releases) - `vmalert` is located in `vmutils-*` archives there. -#### Development build +### Development build 1. [Install Go](https://golang.org/doc/install). The minimum supported version is Go 1.15. 2. Run `make vmalert` from the root folder of [the repository](https://github.com/VictoriaMetrics/VictoriaMetrics). It builds `vmalert` binary and puts it into the `bin` folder. -#### Production build +### Production build 1. [Install docker](https://docs.docker.com/install/). 2. Run `make vmalert-prod` from the root folder of [the repository](https://github.com/VictoriaMetrics/VictoriaMetrics). It builds `vmalert-prod` binary and puts it into the `bin` folder. -#### ARM build +### ARM build ARM build may run on Raspberry Pi or on [energy-efficient ARM servers](https://blog.cloudflare.com/arm-takes-wing/). -#### Development ARM build +### Development ARM build 1. [Install Go](https://golang.org/doc/install). The minimum supported version is Go 1.15. 2. Run `make vmalert-arm` or `make vmalert-arm64` from the root folder of [the repository](https://github.com/VictoriaMetrics/VictoriaMetrics). It builds `vmalert-arm` or `vmalert-arm64` binary respectively and puts it into the `bin` folder. -#### Production ARM build +### Production ARM build 1. [Install docker](https://docs.docker.com/install/). 2. Run `make vmalert-arm-prod` or `make vmalert-arm64-prod` from the root folder of [the repository](https://github.com/VictoriaMetrics/VictoriaMetrics). diff --git a/docs/vmauth.md b/docs/vmauth.md index ce4ba1b7a..3b9d64f43 100644 --- a/docs/vmauth.md +++ b/docs/vmauth.md @@ -1,4 +1,8 @@ -## vmauth +--- +sort: 4 +--- + +# vmauth `vmauth` is a simple auth proxy and router for [VictoriaMetrics](https://github.com/VictoriaMetrics/VictoriaMetrics). It reads username and password from [Basic Auth headers](https://en.wikipedia.org/wiki/Basic_access_authentication), diff --git a/docs/vmbackup.md b/docs/vmbackup.md index 811d71fae..9affa6dfd 100644 --- a/docs/vmbackup.md +++ b/docs/vmbackup.md @@ -1,4 +1,8 @@ -## vmbackup +--- +sort: 5 +--- + +# vmbackup `vmbackup` creates VictoriaMetrics data backups from [instant snapshots](https://victoriametrics.github.io/Single-server-VictoriaMetrics.html#how-to-work-with-snapshots). diff --git a/docs/vmctl.md b/docs/vmctl.md index 61813ab52..5fed39e36 100644 --- a/docs/vmctl.md +++ b/docs/vmctl.md @@ -1,3 +1,7 @@ +--- +sort: 7 +--- + # vmctl Victoria metrics command-line tool @@ -9,33 +13,6 @@ Features: - [x] InfluxDB: migrate data from InfluxDB to VictoriaMetrics - [ ] Storage Management: data re-balancing between nodes -# Table of contents - -* [Articles](#articles) -* [How to build](#how-to-build) -* [Migrating data from InfluxDB 1.x](#migrating-data-from-influxdb-1x) - * [Data mapping](#data-mapping) - * [Configuration](#configuration) - * [Filtering](#filtering) -* [Migrating data from InfluxDB 2.x](#migrating-data-from-influxdb-2x) -* [Migrating data from Prometheus](#migrating-data-from-prometheus) - * [Data mapping](#data-mapping-1) - * [Configuration](#configuration-1) - * [Filtering](#filtering-1) -* [Migrating data from Thanos](#migrating-data-from-thanos) - * [Current data](#current-data) - * [Historical data](#historical-data) -* [Migrating data from VictoriaMetrics](#migrating-data-from-victoriametrics) - * [Native protocol](#native-protocol) -* [Tuning](#tuning) - * [Influx mode](#influx-mode) - * [Prometheus mode](#prometheus-mode) - * [VictoriaMetrics importer](#victoriametrics-importer) - * [Importer stats](#importer-stats) -* [Significant figures](#significant-figures) -* [Adding extra labels](#adding-extra-labels) - - ## Articles * [How to migrate data from Prometheus](https://medium.com/@romanhavronenko/victoriametrics-how-to-migrate-data-from-prometheus-d44a6728f043) diff --git a/docs/vmgateway.md b/docs/vmgateway.md index e9e5ef571..f2da4e476 100644 --- a/docs/vmgateway.md +++ b/docs/vmgateway.md @@ -1,4 +1,8 @@ -## vmgateway +--- +sort: 8 +--- + +# vmgateway vmgateway @@ -15,7 +19,7 @@ `vmgateway` is included in an [enterprise package](https://victoriametrics.com/enterprise.html). -### Access Control +## Access Control vmgateway-ac @@ -45,7 +49,7 @@ Where: - `extra_labels` - optional, key-value pairs for label filters - added to ingested or selected metrics. - `mode` - optional, access mode for api - read, write, full. supported values: 0 - full (default value), 1 - read, 2 - write. -#### QuickStart +## QuickStart Start single version of Victoria Metrics @@ -74,7 +78,7 @@ curl 'http://localhost:8431/api/v1/series/count' -H 'Authorization: Bearer incor ``` -### Rate Limiter +## Rate Limiter vmgateway-rl @@ -112,7 +116,7 @@ limits: account_id: 1 ``` -#### QuickStart +## QuickStart cluster version required for rate limiting. ```bash @@ -162,7 +166,7 @@ curl 'http://localhost:8431/api/v1/labels' -H 'Authorization: Bearer eyJhbGciOiJ # check rate limit ``` -### Configuration +## Configuration The shortlist of configuration flags is the following: ```bash @@ -269,7 +273,7 @@ The shortlist of configuration flags is the following: ``` -### TroubleShooting +## TroubleShooting * Access control: * incorrect `jwt` format, try https://jwt.io/#debugger-io with our tokens @@ -278,7 +282,7 @@ The shortlist of configuration flags is the following: * `scrape_interval` at datasource, reduce it to apply limits faster. -### Limitations +## Limitations * Access Control: * `jwt` token must be validated by external system, currently `vmgateway` can't validate the signature. diff --git a/docs/vmrestore.md b/docs/vmrestore.md index 4abd1bc9c..4d7a66894 100644 --- a/docs/vmrestore.md +++ b/docs/vmrestore.md @@ -1,4 +1,8 @@ -## vmrestore +--- +sort: 6 +--- + +# vmrestore `vmrestore` restores data from backups created by [vmbackup](https://victoriametrics.github.io/vbackup.html). VictoriaMetrics `v1.29.0` and newer versions must be used for working with the restored data. From 75f309fbbfd762f882efe0f93fe02879d8595051 Mon Sep 17 00:00:00 2001 From: Aliaksandr Valialkin Date: Wed, 7 Apr 2021 13:45:55 +0300 Subject: [PATCH 57/63] docs/Cluster-VictoriaMetrics.md: follow-up after c6a8ebb11f85d344f551ed5c87256fb3d6a05cb5 --- docs/Cluster-VictoriaMetrics.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/Cluster-VictoriaMetrics.md b/docs/Cluster-VictoriaMetrics.md index 03013a3d0..013a4c7b5 100644 --- a/docs/Cluster-VictoriaMetrics.md +++ b/docs/Cluster-VictoriaMetrics.md @@ -4,7 +4,7 @@ sort: 9 # Cluster version -Victoria Metrics logo +Victoria Metrics VictoriaMetrics is a fast, cost-effective and scalable time series database. It can be used as a long-term remote storage for Prometheus. From 1177dca3da7122935d5f60685907d203e0096cbf Mon Sep 17 00:00:00 2001 From: Aliaksandr Valialkin Date: Wed, 7 Apr 2021 14:14:22 +0300 Subject: [PATCH 58/63] app/vmselect: do not sort series returned from `topk*` and `bottomk*` functions, since these series are already sorted in user-expected order Updates https://github.com/VictoriaMetrics/VictoriaMetrics/issues/1189 --- app/vmselect/promql/exec.go | 6 +++++- docs/CHANGELOG.md | 1 + 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/app/vmselect/promql/exec.go b/app/vmselect/promql/exec.go index e46a20647..4a3d376c7 100644 --- a/app/vmselect/promql/exec.go +++ b/app/vmselect/promql/exec.go @@ -91,7 +91,11 @@ func maySortResults(e metricsql.Expr, tss []*timeseries) bool { } switch fe.Name { case "sort", "sort_desc", - "sort_by_label", "sort_by_label_desc": + "sort_by_label", "sort_by_label_desc", + "topk", "bottomk", + "topk_max", "topk_min", "topk_avg", "topk_median", + "bottomk_max", "bottomk_min", "bottomk_avg", "bottomk_median", + "outliersk": return false default: return true diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 26aac290b..1d866f014 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -23,6 +23,7 @@ sort: 13 * FEATURE: publish `arm64` and `amd64` binaries for cluster version of VictoriaMetrics at [releases page](https://github.com/VictoriaMetrics/VictoriaMetrics/releases). * BUGFIX: properly handle `/api/v1/labels` and `/api/v1/label//values` queries on big `start ... end` time range. This should fix big resource usage when VictoriaMetrics is queried with [Promxy](https://github.com/jacksontj/promxy) v0.0.62 or newer versions. +* BUGFIX: do not break sort order for series returned from `topk*`, `bottomk*` and `outliersk` [MetricsQL](https://victoriametrics.github.io/MetricsQL.html) functions. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/1189). * BUGFIX: vmagent: properly work with simple HTTP proxies which don't support `CONNECT` method. For example, [PushProx](https://github.com/prometheus-community/PushProx). See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/1179). * BUGFIX: vmagent: properly discover targets if multiple namespace selectors are put inside `kubernetes_sd_config`. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/1170). * BUGFIX: vmagent: properly discover `role: endpoints` and `role: endpointslices` targets in `kubernetes_sd_config`. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/1182). From 5a0938d807dc9fb25bf9317e2b57384d4102976c Mon Sep 17 00:00:00 2001 From: Aliaksandr Valialkin Date: Wed, 7 Apr 2021 21:54:06 +0300 Subject: [PATCH 59/63] lib/promscrape: do not spend CPU time on constructing scrapeWork key if clustering is disabled --- lib/promscrape/config.go | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/lib/promscrape/config.go b/lib/promscrape/config.go index 1f93c4be5..059291277 100644 --- a/lib/promscrape/config.go +++ b/lib/promscrape/config.go @@ -795,12 +795,15 @@ func appendSortedKeyValuePairs(dst []byte, m map[string]string) []byte { var scrapeWorkKeyBufPool bytesutil.ByteBufferPool func (swc *scrapeWorkConfig) getScrapeWork(target string, extraLabels, metaLabels map[string]string) (*ScrapeWork, error) { - // Verify whether the scrape work must be skipped. - bb := scrapeWorkKeyBufPool.Get() - defer scrapeWorkKeyBufPool.Put(bb) - bb.B = appendScrapeWorkKey(bb.B[:0], target, extraLabels, metaLabels) - if needSkipScrapeWork(bytesutil.ToUnsafeString(bb.B), *clusterMembersCount, *clusterReplicationFactor, *clusterMemberNum) { - return nil, nil + // Verify whether the scrape work must be skipped because of `-promscrape.cluster.*` configs. + if *clusterMembersCount > 1 { + bb := scrapeWorkKeyBufPool.Get() + bb.B = appendScrapeWorkKey(bb.B[:0], target, extraLabels, metaLabels) + needSkip := needSkipScrapeWork(bytesutil.ToUnsafeString(bb.B), *clusterMembersCount, *clusterReplicationFactor, *clusterMemberNum) + scrapeWorkKeyBufPool.Put(bb) + if needSkip { + return nil, nil + } } labels := mergeLabels(swc.jobName, swc.scheme, target, swc.metricsPath, extraLabels, swc.externalLabels, metaLabels, swc.params) From cb12a8f0a8ac9979d9e0382707a3c8e2e65b4ddd Mon Sep 17 00:00:00 2001 From: Aliaksandr Valialkin Date: Wed, 7 Apr 2021 23:33:40 +0300 Subject: [PATCH 60/63] app/vmselect: return `data:null` instead of `data:[]` from `/api/v1/query_exemplars`, since Grafana throws an error otherwise Updates https://github.com/VictoriaMetrics/VictoriaMetrics/issues/1186 --- app/vmselect/main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/vmselect/main.go b/app/vmselect/main.go index f6e86facc..cdf21b952 100644 --- a/app/vmselect/main.go +++ b/app/vmselect/main.go @@ -372,7 +372,7 @@ func RequestHandler(w http.ResponseWriter, r *http.Request) bool { // Return dumb placeholder for https://prometheus.io/docs/prometheus/latest/querying/api/#querying-exemplars queryExemplarsRequests.Inc() w.Header().Set("Content-Type", "application/json; charset=utf-8") - fmt.Fprintf(w, "%s", `{"status":"success","data":[]}`) + fmt.Fprintf(w, "%s", `{"status":"success","data":null}`) return true case "/api/v1/admin/tsdb/delete_series": deleteRequests.Inc() From d3fa0ccabdd56bf9997b3253e711ca120008ec70 Mon Sep 17 00:00:00 2001 From: Aliaksandr Valialkin Date: Thu, 8 Apr 2021 00:09:34 +0300 Subject: [PATCH 61/63] app/vmselect/promql: properly detect aggregate `topk*` and `bottomk*` aggregate functions in order to disable duplicate sorting Updates https://github.com/VictoriaMetrics/VictoriaMetrics/issues/1189 --- app/vmselect/promql/aggr.go | 16 ++++++++++++++-- app/vmselect/promql/exec.go | 29 +++++++++++++++-------------- 2 files changed, 29 insertions(+), 16 deletions(-) diff --git a/app/vmselect/promql/aggr.go b/app/vmselect/promql/aggr.go index 46ebc4323..2555ad75f 100644 --- a/app/vmselect/promql/aggr.go +++ b/app/vmselect/promql/aggr.go @@ -618,7 +618,9 @@ func newAggrFuncTopK(isReverse bool) aggrFunc { }) fillNaNsAtIdx(n, ks[n], tss) } - return removeNaNs(tss) + tss = removeNaNs(tss) + reverseSeries(tss) + return tss } return aggrFuncExt(afe, args[1], &afa.ae.Modifier, afa.ae.Limit, true) } @@ -683,7 +685,17 @@ func getRangeTopKTimeseries(tss []*timeseries, modifier *metricsql.ModifierExpr, if remainingSumTS != nil { tss = append(tss, remainingSumTS) } - return removeNaNs(tss) + tss = removeNaNs(tss) + reverseSeries(tss) + return tss +} + +func reverseSeries(tss []*timeseries) { + j := len(tss) + for i := 0; i < len(tss)/2; i++ { + j-- + tss[i], tss[j] = tss[j], tss[i] + } } func getRemainingSumTimeseries(tss []*timeseries, modifier *metricsql.ModifierExpr, ks []float64, remainingSumTagName string) *timeseries { diff --git a/app/vmselect/promql/exec.go b/app/vmselect/promql/exec.go index 4a3d376c7..5d594ab90 100644 --- a/app/vmselect/promql/exec.go +++ b/app/vmselect/promql/exec.go @@ -85,21 +85,22 @@ func Exec(ec *EvalConfig, q string, isFirstPointOnly bool) ([]netstorage.Result, } func maySortResults(e metricsql.Expr, tss []*timeseries) bool { - fe, ok := e.(*metricsql.FuncExpr) - if !ok { - return true - } - switch fe.Name { - case "sort", "sort_desc", - "sort_by_label", "sort_by_label_desc", - "topk", "bottomk", - "topk_max", "topk_min", "topk_avg", "topk_median", - "bottomk_max", "bottomk_min", "bottomk_avg", "bottomk_median", - "outliersk": - return false - default: - return true + switch v := e.(type) { + case *metricsql.FuncExpr: + switch strings.ToLower(v.Name) { + case "sort", "sort_desc", + "sort_by_label", "sort_by_label_desc": + return false + } + case *metricsql.AggrFuncExpr: + switch strings.ToLower(v.Name) { + case "topk", "bottomk", "outliersk", + "topk_max", "topk_min", "topk_avg", "topk_median", + "bottomk_max", "bottomk_min", "bottomk_avg", "bottomk_median": + return false + } } + return true } func timeseriesToResult(tss []*timeseries, maySort bool) ([]netstorage.Result, error) { From 544821b7193a1b523160294712602cdfc6654bc0 Mon Sep 17 00:00:00 2001 From: Aliaksandr Valialkin Date: Thu, 8 Apr 2021 00:17:11 +0300 Subject: [PATCH 62/63] app/vmselect/promql: fix tests after d3fa0ccabdd56bf9997b3253e711ca120008ec70 --- app/vmselect/promql/exec_test.go | 44 ++++++++++++++++---------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/app/vmselect/promql/exec_test.go b/app/vmselect/promql/exec_test.go index f6bfcc1ea..61d9318ff 100644 --- a/app/vmselect/promql/exec_test.go +++ b/app/vmselect/promql/exec_test.go @@ -4756,25 +4756,25 @@ func TestExecSuccess(t *testing.T) { }) t.Run(`topk(1)`, func(t *testing.T) { t.Parallel() - q := `sort(topk(1, label_set(10, "foo", "bar") or label_set(time()/150, "baz", "sss")))` + q := `topk(1, label_set(10, "foo", "bar") or label_set(time()/150, "baz", "sss"))` r1 := netstorage.Result{ - MetricName: metricNameExpected, - Values: []float64{10, 10, 10, nan, nan, nan}, - Timestamps: timestampsExpected, - } - r1.MetricName.Tags = []storage.Tag{{ - Key: []byte("foo"), - Value: []byte("bar"), - }} - r2 := netstorage.Result{ MetricName: metricNameExpected, Values: []float64{nan, nan, nan, 10.666666666666666, 12, 13.333333333333334}, Timestamps: timestampsExpected, } - r2.MetricName.Tags = []storage.Tag{{ + r1.MetricName.Tags = []storage.Tag{{ Key: []byte("baz"), Value: []byte("sss"), }} + r2 := netstorage.Result{ + MetricName: metricNameExpected, + Values: []float64{10, 10, 10, nan, nan, nan}, + Timestamps: timestampsExpected, + } + r2.MetricName.Tags = []storage.Tag{{ + Key: []byte("foo"), + Value: []byte("bar"), + }} resultExpected := []netstorage.Result{r1, r2} f(q, resultExpected) }) @@ -5047,25 +5047,25 @@ func TestExecSuccess(t *testing.T) { }) t.Run(`bottomk(1)`, func(t *testing.T) { t.Parallel() - q := `sort(bottomk(1, label_set(10, "foo", "bar") or label_set(time()/150, "baz", "sss")))` + q := `bottomk(1, label_set(10, "foo", "bar") or label_set(time()/150, "baz", "sss"))` r1 := netstorage.Result{ - MetricName: metricNameExpected, - Values: []float64{6.666666666666667, 8, 9.333333333333334, nan, nan, nan}, - Timestamps: timestampsExpected, - } - r1.MetricName.Tags = []storage.Tag{{ - Key: []byte("baz"), - Value: []byte("sss"), - }} - r2 := netstorage.Result{ MetricName: metricNameExpected, Values: []float64{nan, nan, nan, 10, 10, 10}, Timestamps: timestampsExpected, } - r2.MetricName.Tags = []storage.Tag{{ + r1.MetricName.Tags = []storage.Tag{{ Key: []byte("foo"), Value: []byte("bar"), }} + r2 := netstorage.Result{ + MetricName: metricNameExpected, + Values: []float64{6.666666666666667, 8, 9.333333333333334, nan, nan, nan}, + Timestamps: timestampsExpected, + } + r2.MetricName.Tags = []storage.Tag{{ + Key: []byte("baz"), + Value: []byte("sss"), + }} resultExpected := []netstorage.Result{r1, r2} f(q, resultExpected) }) From f1a22b097a0f620a07b08ea7af6e01db9bfc1f2f Mon Sep 17 00:00:00 2001 From: Aliaksandr Valialkin Date: Thu, 8 Apr 2021 00:48:16 +0300 Subject: [PATCH 63/63] docs/CHANGELOG.md: cut v1.58.0 --- docs/CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 1d866f014..60c2371b7 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -6,6 +6,9 @@ sort: 13 ## tip + +## [v1.58.0](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.58.0) + * FEATURE: vminsert and vmagent: add `-sortLabels` command-line flag for sorting metric labels before pushing them to `vmstorage`. This should reduce the size of `MetricName -> internal_series_id` cache (aka `vm_cache_size_bytes{type="storage/tsid"}`) when ingesting samples for the same time series with distinct order of labels. For example, `foo{k1="v1",k2="v2"}` and `foo{k2="v2",k1="v1"}` represent a single time series. Labels sorting is disabled by default, since the majority of established exporters preserve the order of labels for the exported metrics. * FEATURE: allow specifying label value alongside label name for the `others sum` time series returned from `topk_*` and `bottomk_*` functions from [MetricsQL](https://victoriametrics.github.io/MetricsQL.html). For example, `topk_avg(3, max(process_resident_memory_bytes) by (instance), "instance=other_sum")` would return top 3 series from `max(process_resident_memory_bytes) by (instance)` plus a series containing the sum of other series. The `others sum` series will have `{instance="other_sum"}` label. * FEATURE: do not delete `dst_label` when applying `label_copy(q, "src_label", "dst_label")` and `label_move(q, "src_label", "dst_label")` to series without `src_label` and with non-empty `dst_label`. See more details at [MetricsQL docs](https://victoriametrics.github.io/MetricsQL.html).