From b5389192583bac5171716e6467a370f29bda3c43 Mon Sep 17 00:00:00 2001
From: tyltr
Date: Wed, 31 Jan 2024 20:27:23 +0800
Subject: [PATCH 01/44] remove redundant code
Signed-off-by: tyltr
---
discovery/legacymanager/manager_test.go | 1 -
1 file changed, 1 deletion(-)
diff --git a/discovery/legacymanager/manager_test.go b/discovery/legacymanager/manager_test.go
index 6fbecabc2..767532168 100644
--- a/discovery/legacymanager/manager_test.go
+++ b/discovery/legacymanager/manager_test.go
@@ -1091,7 +1091,6 @@ func TestCoordinationWithReceiver(t *testing.T) {
}
for _, tc := range testCases {
- tc := tc
t.Run(tc.title, func(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
From d8a9d69f81d4bfd2183289263e74333ac4d628c1 Mon Sep 17 00:00:00 2001
From: Jan Fajerski
Date: Tue, 25 Jun 2024 09:15:09 +0200
Subject: [PATCH 02/44] ci: Add job to report build_all status
This should enable proper status reporting of matrix jobs for release
branches. See also https://github.com/orgs/community/discussions/4324.
The new job will succeed if all build_all jobs succeeded and fail if
there is a single failed or cancelled build job.
This only runs for PRs, not for release tags or the main branch, unlike
the build_all step.
Signed-off-by: Jan Fajerski
---
.github/workflows/ci.yml | 12 ++++++++++++
1 file changed, 12 insertions(+)
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 978218dba..995aef675 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -143,6 +143,18 @@ jobs:
with:
parallelism: 12
thread: ${{ matrix.thread }}
+ build_all_status:
+ name: Report status of build Prometheus for all architectures
+ runs-on: ubuntu-latest
+ needs: [build_all]
+ if: github.event_name == 'pull_request' && startsWith(github.event.pull_request.base.ref, 'release-')
+ steps:
+ - name: Successful build
+ if: ${{ !(contains(needs.*.result, 'failure')) && !(contains(needs.*.result, 'cancelled')) }}
+ run: exit 0
+ - name: Failing or cancelled build
+ if: ${{ contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') }}
+ run: exit 1
check_generated_parser:
name: Check generated parser
runs-on: ubuntu-latest
From d84282b105e40a6f5f1e3263ae6a7d735155df76 Mon Sep 17 00:00:00 2001
From: Bryan Boreham
Date: Mon, 15 Jul 2024 09:47:16 +0100
Subject: [PATCH 03/44] Labels: use single byte as separator - small speedup
Since `seps` is a variable, `seps[0]` has to be bounds-checked every
time. Replacing with a constant everywhere it is used skips this
overhead.
Signed-off-by: Bryan Boreham
---
model/labels/labels.go | 24 ++++++++++++------------
model/labels/labels_common.go | 5 +++--
model/labels/labels_dedupelabels.go | 24 ++++++++++++------------
model/labels/labels_stringlabels.go | 8 ++++----
model/labels/sharding.go | 4 ++--
model/labels/sharding_dedupelabels.go | 4 ++--
model/labels/sharding_stringlabels.go | 4 ++--
7 files changed, 37 insertions(+), 36 deletions(-)
diff --git a/model/labels/labels.go b/model/labels/labels.go
index 01514abf3..cd30f4f8f 100644
--- a/model/labels/labels.go
+++ b/model/labels/labels.go
@@ -38,10 +38,10 @@ func (ls Labels) Bytes(buf []byte) []byte {
b.WriteByte(labelSep)
for i, l := range ls {
if i > 0 {
- b.WriteByte(seps[0])
+ b.WriteByte(sep)
}
b.WriteString(l.Name)
- b.WriteByte(seps[0])
+ b.WriteByte(sep)
b.WriteString(l.Value)
}
return b.Bytes()
@@ -86,9 +86,9 @@ func (ls Labels) Hash() uint64 {
}
b = append(b, v.Name...)
- b = append(b, seps[0])
+ b = append(b, sep)
b = append(b, v.Value...)
- b = append(b, seps[0])
+ b = append(b, sep)
}
return xxhash.Sum64(b)
}
@@ -106,9 +106,9 @@ func (ls Labels) HashForLabels(b []byte, names ...string) (uint64, []byte) {
i++
default:
b = append(b, ls[i].Name...)
- b = append(b, seps[0])
+ b = append(b, sep)
b = append(b, ls[i].Value...)
- b = append(b, seps[0])
+ b = append(b, sep)
i++
j++
}
@@ -130,9 +130,9 @@ func (ls Labels) HashWithoutLabels(b []byte, names ...string) (uint64, []byte) {
continue
}
b = append(b, ls[i].Name...)
- b = append(b, seps[0])
+ b = append(b, sep)
b = append(b, ls[i].Value...)
- b = append(b, seps[0])
+ b = append(b, sep)
}
return xxhash.Sum64(b), b
}
@@ -151,10 +151,10 @@ func (ls Labels) BytesWithLabels(buf []byte, names ...string) []byte {
i++
default:
if b.Len() > 1 {
- b.WriteByte(seps[0])
+ b.WriteByte(sep)
}
b.WriteString(ls[i].Name)
- b.WriteByte(seps[0])
+ b.WriteByte(sep)
b.WriteString(ls[i].Value)
i++
j++
@@ -177,10 +177,10 @@ func (ls Labels) BytesWithoutLabels(buf []byte, names ...string) []byte {
continue
}
if b.Len() > 1 {
- b.WriteByte(seps[0])
+ b.WriteByte(sep)
}
b.WriteString(ls[i].Name)
- b.WriteByte(seps[0])
+ b.WriteByte(sep)
b.WriteString(ls[i].Value)
}
return b.Bytes()
diff --git a/model/labels/labels_common.go b/model/labels/labels_common.go
index 4bc94f84f..6db86b03c 100644
--- a/model/labels/labels_common.go
+++ b/model/labels/labels_common.go
@@ -29,10 +29,11 @@ const (
BucketLabel = "le"
InstanceName = "instance"
- labelSep = '\xfe'
+ labelSep = '\xfe' // Used at beginning of `Bytes` return.
+ sep = '\xff' // Used between labels in `Bytes` and `Hash`.
)
-var seps = []byte{'\xff'}
+var seps = []byte{sep} // Used with Hash, which has no WriteByte method.
// Label is a key/value pair of strings.
type Label struct {
diff --git a/model/labels/labels_dedupelabels.go b/model/labels/labels_dedupelabels.go
index 0e5bb048b..da8a88cc1 100644
--- a/model/labels/labels_dedupelabels.go
+++ b/model/labels/labels_dedupelabels.go
@@ -146,13 +146,13 @@ func (ls Labels) Bytes(buf []byte) []byte {
b := bytes.NewBuffer(buf[:0])
for i := 0; i < len(ls.data); {
if i > 0 {
- b.WriteByte(seps[0])
+ b.WriteByte(sep)
}
var name, value string
name, i = decodeString(ls.syms, ls.data, i)
value, i = decodeString(ls.syms, ls.data, i)
b.WriteString(name)
- b.WriteByte(seps[0])
+ b.WriteByte(sep)
b.WriteString(value)
}
return b.Bytes()
@@ -201,9 +201,9 @@ func (ls Labels) Hash() uint64 {
}
b = append(b, name...)
- b = append(b, seps[0])
+ b = append(b, sep)
b = append(b, value...)
- b = append(b, seps[0])
+ b = append(b, sep)
pos = newPos
}
return xxhash.Sum64(b)
@@ -226,9 +226,9 @@ func (ls Labels) HashForLabels(b []byte, names ...string) (uint64, []byte) {
}
if name == names[j] {
b = append(b, name...)
- b = append(b, seps[0])
+ b = append(b, sep)
b = append(b, value...)
- b = append(b, seps[0])
+ b = append(b, sep)
}
}
@@ -252,9 +252,9 @@ func (ls Labels) HashWithoutLabels(b []byte, names ...string) (uint64, []byte) {
continue
}
b = append(b, name...)
- b = append(b, seps[0])
+ b = append(b, sep)
b = append(b, value...)
- b = append(b, seps[0])
+ b = append(b, sep)
}
return xxhash.Sum64(b), b
}
@@ -275,10 +275,10 @@ func (ls Labels) BytesWithLabels(buf []byte, names ...string) []byte {
}
if lName == names[j] {
if b.Len() > 1 {
- b.WriteByte(seps[0])
+ b.WriteByte(sep)
}
b.WriteString(lName)
- b.WriteByte(seps[0])
+ b.WriteByte(sep)
b.WriteString(lValue)
}
pos = newPos
@@ -299,10 +299,10 @@ func (ls Labels) BytesWithoutLabels(buf []byte, names ...string) []byte {
}
if j == len(names) || lName != names[j] {
if b.Len() > 1 {
- b.WriteByte(seps[0])
+ b.WriteByte(sep)
}
b.WriteString(lName)
- b.WriteByte(seps[0])
+ b.WriteByte(sep)
b.WriteString(lValue)
}
pos = newPos
diff --git a/model/labels/labels_stringlabels.go b/model/labels/labels_stringlabels.go
index bccceb61f..c8bce5123 100644
--- a/model/labels/labels_stringlabels.go
+++ b/model/labels/labels_stringlabels.go
@@ -112,9 +112,9 @@ func (ls Labels) HashForLabels(b []byte, names ...string) (uint64, []byte) {
}
if name == names[j] {
b = append(b, name...)
- b = append(b, seps[0])
+ b = append(b, sep)
b = append(b, value...)
- b = append(b, seps[0])
+ b = append(b, sep)
}
}
@@ -138,9 +138,9 @@ func (ls Labels) HashWithoutLabels(b []byte, names ...string) (uint64, []byte) {
continue
}
b = append(b, name...)
- b = append(b, seps[0])
+ b = append(b, sep)
b = append(b, value...)
- b = append(b, seps[0])
+ b = append(b, sep)
}
return xxhash.Sum64(b), b
}
diff --git a/model/labels/sharding.go b/model/labels/sharding.go
index 5e3e89fbb..8b3a36939 100644
--- a/model/labels/sharding.go
+++ b/model/labels/sharding.go
@@ -39,9 +39,9 @@ func StableHash(ls Labels) uint64 {
}
b = append(b, v.Name...)
- b = append(b, seps[0])
+ b = append(b, sep)
b = append(b, v.Value...)
- b = append(b, seps[0])
+ b = append(b, sep)
}
return xxhash.Sum64(b)
}
diff --git a/model/labels/sharding_dedupelabels.go b/model/labels/sharding_dedupelabels.go
index 5912724f9..5bf41b05d 100644
--- a/model/labels/sharding_dedupelabels.go
+++ b/model/labels/sharding_dedupelabels.go
@@ -43,9 +43,9 @@ func StableHash(ls Labels) uint64 {
}
b = append(b, name...)
- b = append(b, seps[0])
+ b = append(b, sep)
b = append(b, value...)
- b = append(b, seps[0])
+ b = append(b, sep)
pos = newPos
}
return xxhash.Sum64(b)
diff --git a/model/labels/sharding_stringlabels.go b/model/labels/sharding_stringlabels.go
index 3ad2027d8..798f268eb 100644
--- a/model/labels/sharding_stringlabels.go
+++ b/model/labels/sharding_stringlabels.go
@@ -43,9 +43,9 @@ func StableHash(ls Labels) uint64 {
}
b = append(b, v.Name...)
- b = append(b, seps[0])
+ b = append(b, sep)
b = append(b, v.Value...)
- b = append(b, seps[0])
+ b = append(b, sep)
}
if h != nil {
return h.Sum64()
From d878146c70f5b21d3f6d161d8586828099597ac4 Mon Sep 17 00:00:00 2001
From: Bryan Boreham
Date: Mon, 15 Jul 2024 16:07:42 +0100
Subject: [PATCH 04/44] TSDB: shrink memSeries by moving bools together
In each case the following member requires 8-byte alignment, so moving
one beside the other shrinks memSeries from 176 to 168 bytes, when
compiled with `-tags stringlabels`.
Signed-off-by: Bryan Boreham
---
tsdb/head.go | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/tsdb/head.go b/tsdb/head.go
index 30ad8139a..b7bfaa0fd 100644
--- a/tsdb/head.go
+++ b/tsdb/head.go
@@ -2099,6 +2099,7 @@ type memSeries struct {
nextAt int64 // Timestamp at which to cut the next chunk.
histogramChunkHasComputedEndTime bool // True if nextAt has been predicted for the current histograms chunk; false otherwise.
+ pendingCommit bool // Whether there are samples waiting to be committed to this series.
// We keep the last value here (in addition to appending it to the chunk) so we can check for duplicates.
lastValue float64
@@ -2114,8 +2115,6 @@ type memSeries struct {
// txs is nil if isolation is disabled.
txs *txRing
-
- pendingCommit bool // Whether there are samples waiting to be committed to this series.
}
// memSeriesOOOFields contains the fields required by memSeries
From a25b626792d36e82fe95b05cb3e41f896c4ad2ac Mon Sep 17 00:00:00 2001
From: Arve Knudsen
Date: Mon, 3 Jun 2024 18:02:26 +0200
Subject: [PATCH 05/44] prometheusremotewrite: Support resource attribute
promotion
Signed-off-by: Arve Knudsen
---
config/config.go | 16 ++++++++
docs/configuration/configuration.md | 4 ++
.../prometheusremotewrite/helper.go | 38 +++++++++++++------
.../prometheusremotewrite/histograms.go | 2 +-
.../prometheusremotewrite/metrics_to_prw.go | 13 ++++---
.../number_data_points.go | 4 +-
storage/remote/write_handler.go | 17 ++++++---
storage/remote/write_test.go | 6 ++-
web/api/v1/api.go | 2 +-
web/api/v1/api_test.go | 1 +
10 files changed, 75 insertions(+), 28 deletions(-)
diff --git a/config/config.go b/config/config.go
index c924e3098..0880d518d 100644
--- a/config/config.go
+++ b/config/config.go
@@ -227,6 +227,9 @@ var (
DefaultExemplarsConfig = ExemplarsConfig{
MaxExemplars: 100000,
}
+
+ // DefaultOTLPConfig is the default OTLP configuration.
+ DefaultOTLPConfig = OTLPConfig{}
)
// Config is the top-level configuration for Prometheus's config files.
@@ -242,6 +245,7 @@ type Config struct {
RemoteWriteConfigs []*RemoteWriteConfig `yaml:"remote_write,omitempty"`
RemoteReadConfigs []*RemoteReadConfig `yaml:"remote_read,omitempty"`
+ OTLPConfig OTLPConfig `yaml:"otlp,omitempty"`
}
// SetDirectory joins any relative file paths with dir.
@@ -1304,3 +1308,15 @@ func getGoGCEnv() int {
}
return DefaultRuntimeConfig.GoGC
}
+
+// OTLPConfig is the configuration for writing to the OTLP endpoint.
+type OTLPConfig struct {
+ PromoteResourceAttributes []string `yaml:"promote_resource_attributes,omitempty"`
+}
+
+// UnmarshalYAML implements the yaml.Unmarshaler interface.
+func (c *OTLPConfig) UnmarshalYAML(unmarshal func(interface{}) error) error {
+ *c = DefaultOTLPConfig
+ type plain OTLPConfig
+ return unmarshal((*plain)(c))
+}
diff --git a/docs/configuration/configuration.md b/docs/configuration/configuration.md
index 35976871b..5675210b7 100644
--- a/docs/configuration/configuration.md
+++ b/docs/configuration/configuration.md
@@ -152,6 +152,10 @@ alerting:
remote_write:
[ - ... ]
+# Settings related to the OTLP receiver feature.
+otlp:
+ [ promote_resource_attributes: [, ...] | default = [ ] ]
+
# Settings related to the remote read feature.
remote_read:
[ - ... ]
diff --git a/storage/remote/otlptranslator/prometheusremotewrite/helper.go b/storage/remote/otlptranslator/prometheusremotewrite/helper.go
index 257133853..f2d7ecd4e 100644
--- a/storage/remote/otlptranslator/prometheusremotewrite/helper.go
+++ b/storage/remote/otlptranslator/prometheusremotewrite/helper.go
@@ -65,14 +65,14 @@ type bucketBoundsData struct {
bound float64
}
-// byBucketBoundsData enables the usage of sort.Sort() with a slice of bucket bounds
+// byBucketBoundsData enables the usage of sort.Sort() with a slice of bucket bounds.
type byBucketBoundsData []bucketBoundsData
func (m byBucketBoundsData) Len() int { return len(m) }
func (m byBucketBoundsData) Less(i, j int) bool { return m[i].bound < m[j].bound }
func (m byBucketBoundsData) Swap(i, j int) { m[i], m[j] = m[j], m[i] }
-// ByLabelName enables the usage of sort.Sort() with a slice of labels
+// ByLabelName enables the usage of sort.Sort() with a slice of labels.
type ByLabelName []prompb.Label
func (a ByLabelName) Len() int { return len(a) }
@@ -115,14 +115,23 @@ var seps = []byte{'\xff'}
// createAttributes creates a slice of Prometheus Labels with OTLP attributes and pairs of string values.
// Unpaired string values are ignored. String pairs overwrite OTLP labels if collisions happen and
// if logOnOverwrite is true, the overwrite is logged. Resulting label names are sanitized.
-func createAttributes(resource pcommon.Resource, attributes pcommon.Map, externalLabels map[string]string,
+// If settings.PromoteResourceAttributes is not empty, it's a set of resource attributes that should be promoted to labels.
+func createAttributes(resource pcommon.Resource, attributes pcommon.Map, settings Settings,
ignoreAttrs []string, logOnOverwrite bool, extras ...string) []prompb.Label {
resourceAttrs := resource.Attributes()
serviceName, haveServiceName := resourceAttrs.Get(conventions.AttributeServiceName)
instance, haveInstanceID := resourceAttrs.Get(conventions.AttributeServiceInstanceID)
+ promotedAttrs := make([]prompb.Label, 0, len(settings.PromoteResourceAttributes))
+ for _, name := range settings.PromoteResourceAttributes {
+ if value, exists := resourceAttrs.Get(name); exists {
+ promotedAttrs = append(promotedAttrs, prompb.Label{Name: name, Value: value.AsString()})
+ }
+ }
+ sort.Stable(ByLabelName(promotedAttrs))
+
// Calculate the maximum possible number of labels we could return so we can preallocate l
- maxLabelCount := attributes.Len() + len(externalLabels) + len(extras)/2
+ maxLabelCount := attributes.Len() + len(settings.ExternalLabels) + len(promotedAttrs) + len(extras)/2
if haveServiceName {
maxLabelCount++
@@ -132,9 +141,6 @@ func createAttributes(resource pcommon.Resource, attributes pcommon.Map, externa
maxLabelCount++
}
- // map ensures no duplicate label name
- l := make(map[string]string, maxLabelCount)
-
// Ensure attributes are sorted by key for consistent merging of keys which
// collide when sanitized.
labels := make([]prompb.Label, 0, maxLabelCount)
@@ -148,6 +154,8 @@ func createAttributes(resource pcommon.Resource, attributes pcommon.Map, externa
})
sort.Stable(ByLabelName(labels))
+ // map ensures no duplicate label names.
+ l := make(map[string]string, maxLabelCount)
for _, label := range labels {
var finalKey = prometheustranslator.NormalizeLabel(label.Name)
if existingValue, alreadyExists := l[finalKey]; alreadyExists {
@@ -157,6 +165,13 @@ func createAttributes(resource pcommon.Resource, attributes pcommon.Map, externa
}
}
+ for _, lbl := range promotedAttrs {
+ normalized := prometheustranslator.NormalizeLabel(lbl.Name)
+ if _, exists := l[normalized]; !exists {
+ l[normalized] = lbl.Value
+ }
+ }
+
// Map service.name + service.namespace to job
if haveServiceName {
val := serviceName.AsString()
@@ -169,7 +184,7 @@ func createAttributes(resource pcommon.Resource, attributes pcommon.Map, externa
if haveInstanceID {
l[model.InstanceLabel] = instance.AsString()
}
- for key, value := range externalLabels {
+ for key, value := range settings.ExternalLabels {
// External labels have already been sanitized
if _, alreadyExists := l[key]; alreadyExists {
// Skip external labels if they are overridden by metric attributes
@@ -232,7 +247,7 @@ func (c *PrometheusConverter) addHistogramDataPoints(dataPoints pmetric.Histogra
for x := 0; x < dataPoints.Len(); x++ {
pt := dataPoints.At(x)
timestamp := convertTimeStamp(pt.Timestamp())
- baseLabels := createAttributes(resource, pt.Attributes(), settings.ExternalLabels, nil, false)
+ baseLabels := createAttributes(resource, pt.Attributes(), settings, nil, false)
// If the sum is unset, it indicates the _sum metric point should be
// omitted
@@ -408,7 +423,7 @@ func (c *PrometheusConverter) addSummaryDataPoints(dataPoints pmetric.SummaryDat
for x := 0; x < dataPoints.Len(); x++ {
pt := dataPoints.At(x)
timestamp := convertTimeStamp(pt.Timestamp())
- baseLabels := createAttributes(resource, pt.Attributes(), settings.ExternalLabels, nil, false)
+ baseLabels := createAttributes(resource, pt.Attributes(), settings, nil, false)
// treat sum as a sample in an individual TimeSeries
sum := &prompb.Sample{
@@ -554,7 +569,8 @@ func addResourceTargetInfo(resource pcommon.Resource, settings Settings, timesta
name = settings.Namespace + "_" + name
}
- labels := createAttributes(resource, attributes, settings.ExternalLabels, identifyingAttrs, false, model.MetricNameLabel, name)
+ settings.PromoteResourceAttributes = nil
+ labels := createAttributes(resource, attributes, settings, identifyingAttrs, false, model.MetricNameLabel, name)
haveIdentifier := false
for _, l := range labels {
if l.Name == model.JobLabel || l.Name == model.InstanceLabel {
diff --git a/storage/remote/otlptranslator/prometheusremotewrite/histograms.go b/storage/remote/otlptranslator/prometheusremotewrite/histograms.go
index 21b3f5dd9..73528019d 100644
--- a/storage/remote/otlptranslator/prometheusremotewrite/histograms.go
+++ b/storage/remote/otlptranslator/prometheusremotewrite/histograms.go
@@ -45,7 +45,7 @@ func (c *PrometheusConverter) addExponentialHistogramDataPoints(dataPoints pmetr
lbls := createAttributes(
resource,
pt.Attributes(),
- settings.ExternalLabels,
+ settings,
nil,
true,
model.MetricNameLabel,
diff --git a/storage/remote/otlptranslator/prometheusremotewrite/metrics_to_prw.go b/storage/remote/otlptranslator/prometheusremotewrite/metrics_to_prw.go
index 65dac99c5..a3a789723 100644
--- a/storage/remote/otlptranslator/prometheusremotewrite/metrics_to_prw.go
+++ b/storage/remote/otlptranslator/prometheusremotewrite/metrics_to_prw.go
@@ -30,12 +30,13 @@ import (
)
type Settings struct {
- Namespace string
- ExternalLabels map[string]string
- DisableTargetInfo bool
- ExportCreatedMetric bool
- AddMetricSuffixes bool
- SendMetadata bool
+ Namespace string
+ ExternalLabels map[string]string
+ DisableTargetInfo bool
+ ExportCreatedMetric bool
+ AddMetricSuffixes bool
+ SendMetadata bool
+ PromoteResourceAttributes []string
}
// PrometheusConverter converts from OTel write format to Prometheus remote write format.
diff --git a/storage/remote/otlptranslator/prometheusremotewrite/number_data_points.go b/storage/remote/otlptranslator/prometheusremotewrite/number_data_points.go
index aafebc6c4..80ccb46c7 100644
--- a/storage/remote/otlptranslator/prometheusremotewrite/number_data_points.go
+++ b/storage/remote/otlptranslator/prometheusremotewrite/number_data_points.go
@@ -34,7 +34,7 @@ func (c *PrometheusConverter) addGaugeNumberDataPoints(dataPoints pmetric.Number
labels := createAttributes(
resource,
pt.Attributes(),
- settings.ExternalLabels,
+ settings,
nil,
true,
model.MetricNameLabel,
@@ -64,7 +64,7 @@ func (c *PrometheusConverter) addSumNumberDataPoints(dataPoints pmetric.NumberDa
lbls := createAttributes(
resource,
pt.Attributes(),
- settings.ExternalLabels,
+ settings,
nil,
true,
model.MetricNameLabel,
diff --git a/storage/remote/write_handler.go b/storage/remote/write_handler.go
index d82237371..b69504949 100644
--- a/storage/remote/write_handler.go
+++ b/storage/remote/write_handler.go
@@ -490,21 +490,23 @@ func (h *writeHandler) appendV2(app storage.Appender, req *writev2.Request, rs *
// NewOTLPWriteHandler creates a http.Handler that accepts OTLP write requests and
// writes them to the provided appendable.
-func NewOTLPWriteHandler(logger log.Logger, appendable storage.Appendable) http.Handler {
+func NewOTLPWriteHandler(logger log.Logger, appendable storage.Appendable, configFunc func() config.Config) http.Handler {
rwHandler := &writeHandler{
logger: logger,
appendable: appendable,
}
return &otlpWriteHandler{
- logger: logger,
- rwHandler: rwHandler,
+ logger: logger,
+ rwHandler: rwHandler,
+ configFunc: configFunc,
}
}
type otlpWriteHandler struct {
- logger log.Logger
- rwHandler *writeHandler
+ logger log.Logger
+ rwHandler *writeHandler
+ configFunc func() config.Config
}
func (h *otlpWriteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
@@ -515,9 +517,12 @@ func (h *otlpWriteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
return
}
+ otlpCfg := h.configFunc().OTLPConfig
+
converter := otlptranslator.NewPrometheusConverter()
if err := converter.FromMetrics(req.Metrics(), otlptranslator.Settings{
- AddMetricSuffixes: true,
+ AddMetricSuffixes: true,
+ PromoteResourceAttributes: otlpCfg.PromoteResourceAttributes,
}); err != nil {
level.Warn(h.logger).Log("msg", "Error translating OTLP metrics to Prometheus write request", "err", err)
}
diff --git a/storage/remote/write_test.go b/storage/remote/write_test.go
index 6e7422a58..83dfffbae 100644
--- a/storage/remote/write_test.go
+++ b/storage/remote/write_test.go
@@ -379,7 +379,11 @@ func TestOTLPWriteHandler(t *testing.T) {
req.Header.Set("Content-Type", "application/x-protobuf")
appendable := &mockAppendable{}
- handler := NewOTLPWriteHandler(nil, appendable)
+ handler := NewOTLPWriteHandler(nil, appendable, func() config.Config {
+ return config.Config{
+ OTLPConfig: config.DefaultOTLPConfig,
+ }
+ })
recorder := httptest.NewRecorder()
handler.ServeHTTP(recorder, req)
diff --git a/web/api/v1/api.go b/web/api/v1/api.go
index 03854787f..d58be211f 100644
--- a/web/api/v1/api.go
+++ b/web/api/v1/api.go
@@ -295,7 +295,7 @@ func NewAPI(
a.remoteWriteHandler = remote.NewWriteHandler(logger, registerer, ap, acceptRemoteWriteProtoMsgs)
}
if otlpEnabled {
- a.otlpWriteHandler = remote.NewOTLPWriteHandler(logger, ap)
+ a.otlpWriteHandler = remote.NewOTLPWriteHandler(logger, ap, configFunc)
}
return a
diff --git a/web/api/v1/api_test.go b/web/api/v1/api_test.go
index 86a57ca08..ba38ddc97 100644
--- a/web/api/v1/api_test.go
+++ b/web/api/v1/api_test.go
@@ -359,6 +359,7 @@ var samplePrometheusCfg = config.Config{
ScrapeConfigs: []*config.ScrapeConfig{},
RemoteWriteConfigs: []*config.RemoteWriteConfig{},
RemoteReadConfigs: []*config.RemoteReadConfig{},
+ OTLPConfig: config.OTLPConfig{},
}
var sampleFlagMap = map[string]string{
From ae82a0a9413eb0a27205428b3348ffe0743e5351 Mon Sep 17 00:00:00 2001
From: Arve Knudsen
Date: Tue, 16 Jul 2024 14:32:24 +0200
Subject: [PATCH 06/44] Sanitize configured OTel resource attributes
Signed-off-by: Arve Knudsen
---
config/config.go | 20 +++++++++++++++++++-
1 file changed, 19 insertions(+), 1 deletion(-)
diff --git a/config/config.go b/config/config.go
index 0880d518d..fd2e6e06c 100644
--- a/config/config.go
+++ b/config/config.go
@@ -19,6 +19,7 @@ import (
"net/url"
"os"
"path/filepath"
+ "slices"
"sort"
"strconv"
"strings"
@@ -1318,5 +1319,22 @@ type OTLPConfig struct {
func (c *OTLPConfig) UnmarshalYAML(unmarshal func(interface{}) error) error {
*c = DefaultOTLPConfig
type plain OTLPConfig
- return unmarshal((*plain)(c))
+ if err := unmarshal((*plain)(c)); err != nil {
+ return err
+ }
+
+ seen := map[string]struct{}{}
+ i := 0
+ for i < len(c.PromoteResourceAttributes) {
+ s := strings.TrimSpace(c.PromoteResourceAttributes[i])
+ if _, exists := seen[s]; exists {
+ c.PromoteResourceAttributes = slices.Delete(c.PromoteResourceAttributes, i, i+1)
+ continue
+ }
+
+ seen[s] = struct{}{}
+ c.PromoteResourceAttributes[i] = s
+ i++
+ }
+ return nil
}
From ec818332dcc7a381add078d091bc3968c0d91d3f Mon Sep 17 00:00:00 2001
From: Arve Knudsen
Date: Tue, 16 Jul 2024 14:49:04 +0200
Subject: [PATCH 07/44] Add config tests
Signed-off-by: Arve Knudsen
---
config/config_test.go | 18 ++++++++++++++++++
config/testdata/conf.good.yml | 3 +++
.../otlp_sanitize_resource_attributes.good.yml | 2 ++
3 files changed, 23 insertions(+)
create mode 100644 config/testdata/otlp_sanitize_resource_attributes.good.yml
diff --git a/config/config_test.go b/config/config_test.go
index 3c4907a46..5822d2ceb 100644
--- a/config/config_test.go
+++ b/config/config_test.go
@@ -156,6 +156,12 @@ var expectedConf = &Config{
},
},
+ OTLPConfig: OTLPConfig{
+ PromoteResourceAttributes: []string{
+ "k8s.cluster.name", "k8s.job.name", "k8s.namespace.name",
+ },
+ },
+
RemoteReadConfigs: []*RemoteReadConfig{
{
URL: mustParseURL("http://remote1/read"),
@@ -1471,6 +1477,18 @@ func TestRemoteWriteRetryOnRateLimit(t *testing.T) {
require.False(t, got.RemoteWriteConfigs[1].QueueConfig.RetryOnRateLimit)
}
+func TestOTLPSanitizeResourceAttributes(t *testing.T) {
+ want, err := LoadFile(filepath.Join("testdata", "otlp_sanitize_resource_attributes.good.yml"), false, false, log.NewNopLogger())
+ require.NoError(t, err)
+
+ out, err := yaml.Marshal(want)
+ require.NoError(t, err)
+ var got Config
+ require.NoError(t, yaml.UnmarshalStrict(out, &got))
+
+ require.Equal(t, []string{"k8s.cluster.name", "k8s.job.name", "k8s.namespace.name"}, got.OTLPConfig.PromoteResourceAttributes)
+}
+
func TestLoadConfig(t *testing.T) {
// Parse a valid file that sets a global scrape timeout. This tests whether parsing
// an overwritten default field in the global config permanently changes the default.
diff --git a/config/testdata/conf.good.yml b/config/testdata/conf.good.yml
index 0e0aa2bd5..56741822c 100644
--- a/config/testdata/conf.good.yml
+++ b/config/testdata/conf.good.yml
@@ -45,6 +45,9 @@ remote_write:
headers:
name: value
+otlp:
+ promote_resource_attributes: ["k8s.cluster.name", "k8s.job.name", "k8s.namespace.name"]
+
remote_read:
- url: http://remote1/read
read_recent: true
diff --git a/config/testdata/otlp_sanitize_resource_attributes.good.yml b/config/testdata/otlp_sanitize_resource_attributes.good.yml
new file mode 100644
index 000000000..ce91302fe
--- /dev/null
+++ b/config/testdata/otlp_sanitize_resource_attributes.good.yml
@@ -0,0 +1,2 @@
+otlp:
+ promote_resource_attributes: ["k8s.cluster.name", "k8s.job.name", "k8s.namespace.name", " k8s.job.name "]
From 6a9df95620bf882d206be7c771ad65042400f6ca Mon Sep 17 00:00:00 2001
From: Arve Knudsen
Date: Tue, 16 Jul 2024 15:13:22 +0200
Subject: [PATCH 08/44] Add to changelog
Signed-off-by: Arve Knudsen
---
CHANGELOG.md | 2 ++
1 file changed, 2 insertions(+)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d5a91e900..8488af6dc 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,8 @@
## unreleased
+* [FEATURE] OTLP receiver: Add new option `otlp.promote_resource_attributes`, for any OTel resource attributes that should be promoted to metric labels. #14200
+
## 2.53.1 / 2024-07-10
Fix a bug which would drop samples in remote-write if the sending flow stalled
From 1d21867d8b3d23863f78799cf60b6e3598ab414c Mon Sep 17 00:00:00 2001
From: Arve Knudsen
Date: Tue, 16 Jul 2024 15:13:40 +0200
Subject: [PATCH 09/44] Add otlptranslator tests
Signed-off-by: Arve Knudsen
---
.../prometheusremotewrite/helper_test.go | 161 ++++++++++++++++++
1 file changed, 161 insertions(+)
create mode 100644 storage/remote/otlptranslator/prometheusremotewrite/helper_test.go
diff --git a/storage/remote/otlptranslator/prometheusremotewrite/helper_test.go b/storage/remote/otlptranslator/prometheusremotewrite/helper_test.go
new file mode 100644
index 000000000..c4dd781ae
--- /dev/null
+++ b/storage/remote/otlptranslator/prometheusremotewrite/helper_test.go
@@ -0,0 +1,161 @@
+// Copyright 2024 The Prometheus Authors
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package prometheusremotewrite
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "go.opentelemetry.io/collector/pdata/pcommon"
+
+ "github.com/prometheus/prometheus/prompb"
+)
+
+func TestCreateAttributes(t *testing.T) {
+ resourceAttrs := map[string]string{
+ "service.name": "service name",
+ "service.instance.id": "service ID",
+ "existent-attr": "resource value",
+ // This one is for testing conflict with metric attribute.
+ "metric-attr": "resource value",
+ // This one is for testing conflict with auto-generated job attribute.
+ "job": "resource value",
+ // This one is for testing conflict with auto-generated instance attribute.
+ "instance": "resource value",
+ }
+
+ resource := pcommon.NewResource()
+ for k, v := range resourceAttrs {
+ resource.Attributes().PutStr(k, v)
+ }
+ attrs := pcommon.NewMap()
+ attrs.PutStr("__name__", "test_metric")
+ attrs.PutStr("metric-attr", "metric value")
+
+ testCases := []struct {
+ name string
+ promoteResourceAttributes []string
+ expectedLabels []prompb.Label
+ }{
+ {
+ name: "Successful conversion without resource attribute promotion",
+ promoteResourceAttributes: nil,
+ expectedLabels: []prompb.Label{
+ {
+ Name: "__name__",
+ Value: "test_metric",
+ },
+ {
+ Name: "instance",
+ Value: "service ID",
+ },
+ {
+ Name: "job",
+ Value: "service name",
+ },
+ {
+ Name: "metric_attr",
+ Value: "metric value",
+ },
+ },
+ },
+ {
+ name: "Successful conversion with resource attribute promotion",
+ promoteResourceAttributes: []string{"non-existent-attr", "existent-attr"},
+ expectedLabels: []prompb.Label{
+ {
+ Name: "__name__",
+ Value: "test_metric",
+ },
+ {
+ Name: "instance",
+ Value: "service ID",
+ },
+ {
+ Name: "job",
+ Value: "service name",
+ },
+ {
+ Name: "metric_attr",
+ Value: "metric value",
+ },
+ {
+ Name: "existent_attr",
+ Value: "resource value",
+ },
+ },
+ },
+ {
+ name: "Successful conversion with resource attribute promotion, conflicting resource attributes are ignored",
+ promoteResourceAttributes: []string{"non-existent-attr", "existent-attr", "metric-attr", "job", "instance"},
+ expectedLabels: []prompb.Label{
+ {
+ Name: "__name__",
+ Value: "test_metric",
+ },
+ {
+ Name: "instance",
+ Value: "service ID",
+ },
+ {
+ Name: "job",
+ Value: "service name",
+ },
+ {
+ Name: "existent_attr",
+ Value: "resource value",
+ },
+ {
+ Name: "metric_attr",
+ Value: "metric value",
+ },
+ },
+ },
+ {
+ name: "Successful conversion with resource attribute promotion, attributes are only promoted once",
+ promoteResourceAttributes: []string{"existent-attr", "existent-attr"},
+ expectedLabels: []prompb.Label{
+ {
+ Name: "__name__",
+ Value: "test_metric",
+ },
+ {
+ Name: "instance",
+ Value: "service ID",
+ },
+ {
+ Name: "job",
+ Value: "service name",
+ },
+ {
+ Name: "existent_attr",
+ Value: "resource value",
+ },
+ {
+ Name: "metric_attr",
+ Value: "metric value",
+ },
+ },
+ },
+ }
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ settings := Settings{
+ PromoteResourceAttributes: tc.promoteResourceAttributes,
+ }
+ lbls := createAttributes(resource, attrs, settings, nil, false)
+
+ assert.ElementsMatch(t, lbls, tc.expectedLabels)
+ })
+ }
+}
From 6b4ff79ce04254ae452b5a89bfde5d5dbb2431fe Mon Sep 17 00:00:00 2001
From: Bryan Boreham
Date: Wed, 17 Jul 2024 16:30:36 +0100
Subject: [PATCH 10/44] Update Go dependencies
Prior to preparing v2.54 release.
Note not updating to k8s.io 1.30, which would force Go 1.22.
Signed-off-by: Bryan Boreham
---
documentation/examples/remote_storage/go.mod | 2 +-
go.mod | 94 ++++-----
go.sum | 189 ++++++++++---------
3 files changed, 143 insertions(+), 142 deletions(-)
diff --git a/documentation/examples/remote_storage/go.mod b/documentation/examples/remote_storage/go.mod
index 348b27dfc..4c41a6606 100644
--- a/documentation/examples/remote_storage/go.mod
+++ b/documentation/examples/remote_storage/go.mod
@@ -10,7 +10,7 @@ require (
github.com/influxdata/influxdb v1.11.5
github.com/prometheus/client_golang v1.19.1
github.com/prometheus/common v0.55.0
- github.com/prometheus/prometheus v0.52.1
+ github.com/prometheus/prometheus v0.53.1
github.com/stretchr/testify v1.9.0
)
diff --git a/go.mod b/go.mod
index 4107f3a09..44c0aca7a 100644
--- a/go.mod
+++ b/go.mod
@@ -5,20 +5,20 @@ go 1.21.0
toolchain go1.22.5
require (
- github.com/Azure/azure-sdk-for-go/sdk/azcore v1.11.1
- github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.5.2
+ github.com/Azure/azure-sdk-for-go/sdk/azcore v1.13.0
+ github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.7.0
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v5 v5.7.0
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v4 v4.3.0
github.com/Code-Hex/go-generics-cache v1.5.1
github.com/KimMachineGun/automemlimit v0.6.1
github.com/alecthomas/kingpin/v2 v2.4.0
- github.com/alecthomas/units v0.0.0-20231202071711-9a357b53e9c9
- github.com/aws/aws-sdk-go v1.53.16
+ github.com/alecthomas/units v0.0.0-20240626203959-61d1e3462e30
+ github.com/aws/aws-sdk-go v1.54.19
github.com/bboreham/go-loser v0.0.0-20230920113527-fcc2c21820a3
github.com/cespare/xxhash/v2 v2.3.0
github.com/dennwc/varint v1.0.0
- github.com/digitalocean/godo v1.117.0
- github.com/docker/docker v26.1.3+incompatible
+ github.com/digitalocean/godo v1.118.0
+ github.com/docker/docker v27.0.3+incompatible
github.com/edsrzf/mmap-go v1.1.0
github.com/envoyproxy/go-control-plane v0.12.0
github.com/envoyproxy/protoc-gen-validate v1.0.4
@@ -31,60 +31,60 @@ require (
github.com/gogo/protobuf v1.3.2
github.com/golang/snappy v0.0.4
github.com/google/go-cmp v0.6.0
- github.com/google/pprof v0.0.0-20240528025155-186aa0362fba
+ github.com/google/pprof v0.0.0-20240711041743-f6c9dda6c6da
github.com/google/uuid v1.6.0
- github.com/gophercloud/gophercloud v1.12.0
+ github.com/gophercloud/gophercloud v1.13.0
github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc
github.com/grpc-ecosystem/grpc-gateway v1.16.0
- github.com/hashicorp/consul/api v1.29.1
- github.com/hashicorp/nomad/api v0.0.0-20240604134157-e73d8bb1140d
- github.com/hetznercloud/hcloud-go/v2 v2.9.0
+ github.com/hashicorp/consul/api v1.29.2
+ github.com/hashicorp/nomad/api v0.0.0-20240717122358-3d93bd3778f3
+ github.com/hetznercloud/hcloud-go/v2 v2.10.2
github.com/ionos-cloud/sdk-go/v6 v6.1.11
github.com/json-iterator/go v1.1.12
- github.com/klauspost/compress v1.17.8
+ github.com/klauspost/compress v1.17.9
github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b
- github.com/linode/linodego v1.35.0
- github.com/miekg/dns v1.1.59
+ github.com/linode/linodego v1.37.0
+ github.com/miekg/dns v1.1.61
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f
github.com/nsf/jsondiff v0.0.0-20230430225905-43f6cf3098c1
github.com/oklog/run v1.1.0
github.com/oklog/ulid v1.3.1
- github.com/ovh/go-ovh v1.5.1
+ github.com/ovh/go-ovh v1.6.0
github.com/prometheus/alertmanager v0.27.0
github.com/prometheus/client_golang v1.19.1
github.com/prometheus/client_model v0.6.1
- github.com/prometheus/common v0.54.0
+ github.com/prometheus/common v0.55.0
github.com/prometheus/common/assets v0.2.0
github.com/prometheus/common/sigv4 v0.1.0
github.com/prometheus/exporter-toolkit v0.11.0
- github.com/scaleway/scaleway-sdk-go v1.0.0-beta.27
+ github.com/scaleway/scaleway-sdk-go v1.0.0-beta.29
github.com/shurcooL/httpfs v0.0.0-20230704072500-f1e31cf0ba5c
github.com/stretchr/testify v1.9.0
github.com/vultr/govultr/v2 v2.17.2
- go.opentelemetry.io/collector/pdata v1.11.0
- go.opentelemetry.io/collector/semconv v0.104.0
- go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.52.0
- go.opentelemetry.io/otel v1.27.0
- go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.27.0
- go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0
- go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.27.0
- go.opentelemetry.io/otel/sdk v1.27.0
- go.opentelemetry.io/otel/trace v1.27.0
+ go.opentelemetry.io/collector/pdata v1.12.0
+ go.opentelemetry.io/collector/semconv v0.105.0
+ go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0
+ go.opentelemetry.io/otel v1.28.0
+ go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0
+ go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.28.0
+ go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.28.0
+ go.opentelemetry.io/otel/sdk v1.28.0
+ go.opentelemetry.io/otel/trace v1.28.0
go.uber.org/atomic v1.11.0
go.uber.org/automaxprocs v1.5.3
go.uber.org/goleak v1.3.0
go.uber.org/multierr v1.11.0
- golang.org/x/net v0.26.0
+ golang.org/x/net v0.27.0
golang.org/x/oauth2 v0.21.0
golang.org/x/sync v0.7.0
- golang.org/x/sys v0.21.0
+ golang.org/x/sys v0.22.0
golang.org/x/text v0.16.0
golang.org/x/time v0.5.0
- golang.org/x/tools v0.22.0
- google.golang.org/api v0.183.0
- google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157
- google.golang.org/grpc v1.64.0
+ golang.org/x/tools v0.23.0
+ google.golang.org/api v0.188.0
+ google.golang.org/genproto/googleapis/api v0.0.0-20240711142825-46eb208f015d
+ google.golang.org/grpc v1.65.0
google.golang.org/protobuf v1.34.2
gopkg.in/yaml.v2 v2.4.0
gopkg.in/yaml.v3 v3.0.1
@@ -92,14 +92,14 @@ require (
k8s.io/apimachinery v0.29.3
k8s.io/client-go v0.29.3
k8s.io/klog v1.0.0
- k8s.io/klog/v2 v2.120.1
+ k8s.io/klog/v2 v2.130.1
)
require (
- cloud.google.com/go/auth v0.5.1 // indirect
+ cloud.google.com/go/auth v0.7.0 // indirect
cloud.google.com/go/auth/oauth2adapt v0.2.2 // indirect
- cloud.google.com/go/compute/metadata v0.3.0 // indirect
- github.com/Azure/azure-sdk-for-go/sdk/internal v1.6.0 // indirect
+ cloud.google.com/go/compute/metadata v0.4.0 // indirect
+ github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 // indirect
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 // indirect
github.com/Microsoft/go-winio v0.6.1 // indirect
github.com/armon/go-metrics v0.4.1 // indirect
@@ -107,7 +107,7 @@ require (
github.com/beorn7/perks v1.0.1 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/cilium/ebpf v0.11.0 // indirect
- github.com/cncf/xds/go v0.0.0-20240318125728-8a4994d93e50 // indirect
+ github.com/cncf/xds/go v0.0.0-20240423153145-555b57ec207b // indirect
github.com/containerd/cgroups/v3 v3.0.3 // indirect
github.com/containerd/log v0.1.0 // indirect
github.com/coreos/go-systemd/v22 v22.5.0 // indirect
@@ -121,7 +121,7 @@ require (
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/ghodss/yaml v1.0.0 // indirect
github.com/go-kit/kit v0.12.0 // indirect
- github.com/go-logr/logr v1.4.1 // indirect
+ github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-openapi/analysis v0.22.2 // indirect
github.com/go-openapi/errors v0.22.0 // indirect
@@ -134,7 +134,7 @@ require (
github.com/go-resty/resty/v2 v2.13.1 // indirect
github.com/godbus/dbus/v5 v5.0.4 // indirect
github.com/golang-jwt/jwt/v5 v5.2.1 // indirect
- github.com/golang/glog v1.2.0 // indirect
+ github.com/golang/glog v1.2.1 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/gnostic-models v0.6.8 // indirect
@@ -142,7 +142,7 @@ require (
github.com/google/gofuzz v1.2.0 // indirect
github.com/google/s2a-go v0.1.7 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect
- github.com/googleapis/gax-go/v2 v2.12.4 // indirect
+ github.com/googleapis/gax-go/v2 v2.12.5 // indirect
github.com/gorilla/websocket v1.5.0 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect
github.com/hashicorp/cronexpr v1.1.2 // indirect
@@ -178,20 +178,20 @@ require (
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
- github.com/prometheus/procfs v0.12.0 // indirect
+ github.com/prometheus/procfs v0.15.1 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/xhit/go-str2duration/v2 v2.1.0 // indirect
go.mongodb.org/mongo-driver v1.14.0 // indirect
go.opencensus.io v0.24.0 // indirect
- go.opentelemetry.io/otel/metric v1.27.0 // indirect
- go.opentelemetry.io/proto/otlp v1.2.0 // indirect
- golang.org/x/crypto v0.24.0 // indirect
+ go.opentelemetry.io/otel/metric v1.28.0 // indirect
+ go.opentelemetry.io/proto/otlp v1.3.1 // indirect
+ golang.org/x/crypto v0.25.0 // indirect
golang.org/x/exp v0.0.0-20240119083558-1b970713d09a // indirect
- golang.org/x/mod v0.18.0 // indirect
- golang.org/x/term v0.21.0 // indirect
- google.golang.org/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157 // indirect
+ golang.org/x/mod v0.19.0 // indirect
+ golang.org/x/term v0.22.0 // indirect
+ google.golang.org/genproto/googleapis/rpc v0.0.0-20240708141625-4ad9e859172b // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gotest.tools/v3 v3.0.3 // indirect
diff --git a/go.sum b/go.sum
index dc4a7ecfd..bb515753d 100644
--- a/go.sum
+++ b/go.sum
@@ -12,8 +12,8 @@ cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bP
cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
-cloud.google.com/go/auth v0.5.1 h1:0QNO7VThG54LUzKiQxv8C6x1YX7lUrzlAa1nVLF8CIw=
-cloud.google.com/go/auth v0.5.1/go.mod h1:vbZT8GjzDf3AVqCcQmqeeM32U9HBFc32vVVAbwDsa6s=
+cloud.google.com/go/auth v0.7.0 h1:kf/x9B3WTbBUHkC+1VS8wwwli9TzhSt0vSTVBmMR8Ts=
+cloud.google.com/go/auth v0.7.0/go.mod h1:D+WqdrpcjmiCgWrXmLLxOVq1GACoE36chW6KXoEvuIw=
cloud.google.com/go/auth/oauth2adapt v0.2.2 h1:+TTV8aXpjeChS9M+aTtN/TjdQnzJvmzKFt//oWu7HX4=
cloud.google.com/go/auth/oauth2adapt v0.2.2/go.mod h1:wcYjgpZI9+Yu7LyYBg4pqSiaRkfEK3GQcpb7C/uyF1Q=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
@@ -22,8 +22,8 @@ cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvf
cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
-cloud.google.com/go/compute/metadata v0.3.0 h1:Tz+eQXMEqDIKRsmY3cHTL6FVaynIjX2QxYC4trgAKZc=
-cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k=
+cloud.google.com/go/compute/metadata v0.4.0 h1:vHzJCWaM4g8XIcm8kopr3XmDA4Gy/lblD3EhhSux05c=
+cloud.google.com/go/compute/metadata v0.4.0/go.mod h1:SIQh1Kkb4ZJ8zJ874fqVkslA29PRXuleyj6vOzlbK7M=
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
@@ -36,12 +36,12 @@ cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohl
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
-github.com/Azure/azure-sdk-for-go/sdk/azcore v1.11.1 h1:E+OJmp2tPvt1W+amx48v1eqbjDYsgN+RzP4q16yV5eM=
-github.com/Azure/azure-sdk-for-go/sdk/azcore v1.11.1/go.mod h1:a6xsAQUZg+VsS3TJ05SRp524Hs4pZ/AeFSr5ENf0Yjo=
-github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.5.2 h1:FDif4R1+UUR+00q6wquyX90K7A8dN+R5E8GEadoP7sU=
-github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.5.2/go.mod h1:aiYBYui4BJ/BJCAIKs92XiPyQfTaBWqvHujDwKb6CBU=
-github.com/Azure/azure-sdk-for-go/sdk/internal v1.6.0 h1:sUFnFjzDUie80h24I7mrKtwCKgLY9L8h5Tp2x9+TWqk=
-github.com/Azure/azure-sdk-for-go/sdk/internal v1.6.0/go.mod h1:52JbnQTp15qg5mRkMBHwp0j0ZFwHJ42Sx3zVV5RE9p0=
+github.com/Azure/azure-sdk-for-go/sdk/azcore v1.13.0 h1:GJHeeA2N7xrG3q30L2UXDyuWRzDM900/65j70wcM4Ww=
+github.com/Azure/azure-sdk-for-go/sdk/azcore v1.13.0/go.mod h1:l38EPgmsp71HHLq9j7De57JcKOWPyhrsW1Awm1JS6K0=
+github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.7.0 h1:tfLQ34V6F7tVSwoTf/4lH5sE0o6eCJuNDTmH09nDpbc=
+github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.7.0/go.mod h1:9kIvujWAA58nmPmWB1m23fyWic1kYZMxD9CxaWn4Qpg=
+github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 h1:ywEEhmNahHBihViHepv3xPBn1663uRv2t2q/ESv9seY=
+github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0/go.mod h1:iZDifYGJTIgIIkYRNWPENUnqx6bJ2xnSDFI2tjwZNuY=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v5 v5.7.0 h1:LkHbJbgF3YyvC53aqYGR+wWQDn2Rdp9AQdGndf9QvY4=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v5 v5.7.0/go.mod h1:QyiQdW4f4/BIfB8ZutZ2s+28RAgfa/pT+zS++ZHyM1I=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal/v2 v2.0.0 h1:PTFGRSlMKCQelWwxUyYVEUqseBJVemLyqWJjvMyt0do=
@@ -75,8 +75,8 @@ github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuy
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
-github.com/alecthomas/units v0.0.0-20231202071711-9a357b53e9c9 h1:ez/4by2iGztzR4L0zgAOR8lTQK9VlyBVVd7G4omaOQs=
-github.com/alecthomas/units v0.0.0-20231202071711-9a357b53e9c9/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE=
+github.com/alecthomas/units v0.0.0-20240626203959-61d1e3462e30 h1:t3eaIm0rUkzbrIewtiFmMK5RXHej2XnoXNhxVsAYUfg=
+github.com/alecthomas/units v0.0.0-20240626203959-61d1e3462e30/go.mod h1:fvzegU4vN3H1qMT+8wDmzjAcDONcgo2/SZ/TyfdUOFs=
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
@@ -92,8 +92,8 @@ github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:W
github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU=
github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
github.com/aws/aws-sdk-go v1.38.35/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro=
-github.com/aws/aws-sdk-go v1.53.16 h1:8oZjKQO/ml1WLUZw5hvF7pvYjPf8o9f57Wldoy/q9Qc=
-github.com/aws/aws-sdk-go v1.53.16/go.mod h1:LF8svs817+Nz+DmiMQKTO3ubZ/6IaTpq3TjupRn3Eqk=
+github.com/aws/aws-sdk-go v1.54.19 h1:tyWV+07jagrNiCcGRzRhdtVjQs7Vy41NwsuOcl0IbVI=
+github.com/aws/aws-sdk-go v1.54.19/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU=
github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g=
github.com/bboreham/go-loser v0.0.0-20230920113527-fcc2c21820a3 h1:6df1vn4bBlDDo4tARvBm7l6KA9iVMnE3NWizDeWSrps=
github.com/bboreham/go-loser v0.0.0-20230920113527-fcc2c21820a3/go.mod h1:CIWtjkly68+yqLPbvwwR/fjNJA/idrtULjZWh2v1ys0=
@@ -120,8 +120,8 @@ github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp
github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4dUb/I5gc9Hdhagfvm9+RyrPryS/auMzxE=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
-github.com/cncf/xds/go v0.0.0-20240318125728-8a4994d93e50 h1:DBmgJDC9dTfkVyGgipamEh2BpGYxScCH1TOF1LL1cXc=
-github.com/cncf/xds/go v0.0.0-20240318125728-8a4994d93e50/go.mod h1:5e1+Vvlzido69INQaVO6d87Qn543Xr6nooe9Kz7oBFM=
+github.com/cncf/xds/go v0.0.0-20240423153145-555b57ec207b h1:ga8SEFjZ60pxLcmhnThWgvH2wg8376yUJmPhEH4H3kw=
+github.com/cncf/xds/go v0.0.0-20240423153145-555b57ec207b/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8=
github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8=
github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI=
github.com/containerd/cgroups/v3 v3.0.3 h1:S5ByHZ/h9PMe5IOQoN7E+nMc2UcLEM/V48DGDJ9kip0=
@@ -143,14 +143,14 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8Yc
github.com/dennwc/varint v1.0.0 h1:kGNFFSSw8ToIy3obO/kKr8U9GZYUAxQEVuix4zfDWzE=
github.com/dennwc/varint v1.0.0/go.mod h1:hnItb35rvZvJrbTALZtY/iQfDs48JKRG1RPpgziApxA=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
-github.com/digitalocean/godo v1.117.0 h1:WVlTe09melDYTd7VCVyvHcNWbgB+uI1O115+5LOtdSw=
-github.com/digitalocean/godo v1.117.0/go.mod h1:Vk0vpCot2HOAJwc5WE8wljZGtJ3ZtWIc8MQ8rF38sdo=
+github.com/digitalocean/godo v1.118.0 h1:lkzGFQmACrVCp7UqH1sAi4JK/PWwlc5aaxubgorKmC4=
+github.com/digitalocean/godo v1.118.0/go.mod h1:Vk0vpCot2HOAJwc5WE8wljZGtJ3ZtWIc8MQ8rF38sdo=
github.com/distribution/reference v0.5.0 h1:/FUIFXtfc/x2gpa5/VGfiGLuOIdYa1t65IKK2OFGvA0=
github.com/distribution/reference v0.5.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/dnaeon/go-vcr v1.2.0 h1:zHCHvJYTMh1N7xnV7zf1m1GPBF9Ad0Jk/whtQ1663qI=
github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ=
-github.com/docker/docker v26.1.3+incompatible h1:lLCzRbrVZrljpVNobJu1J2FHk8V0s4BawoZippkc+xo=
-github.com/docker/docker v26.1.3+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
+github.com/docker/docker v27.0.3+incompatible h1:aBGI9TeQ4MPlhquTQKq9XbK79rKFVwXNUAYz9aXyEBE=
+github.com/docker/docker v27.0.3+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ=
github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
@@ -210,8 +210,8 @@ github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG
github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4=
github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
-github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
-github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
+github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
+github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-openapi/analysis v0.22.2 h1:ZBmNoP2h5omLKr/srIC9bfqrUGzT6g6gNv03HE9Vpj0=
@@ -251,8 +251,8 @@ github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69
github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
-github.com/golang/glog v1.2.0 h1:uCdmnmatrKCgMBlM4rMuJZWOkPDqdbZPnrMXDY4gI68=
-github.com/golang/glog v1.2.0/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w=
+github.com/golang/glog v1.2.1 h1:OptwRhECazUx5ix5TTWC3EZhsZEHWcYWY4FQHTIubm4=
+github.com/golang/glog v1.2.1/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w=
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=
@@ -319,8 +319,8 @@ github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hf
github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
-github.com/google/pprof v0.0.0-20240528025155-186aa0362fba h1:ql1qNgCyOB7iAEk8JTNM+zJrgIbnyCKX/wdlyPufP5g=
-github.com/google/pprof v0.0.0-20240528025155-186aa0362fba/go.mod h1:K1liHPHnj73Fdn/EKuT8nrFqBihUSKXoLYU0BuatOYo=
+github.com/google/pprof v0.0.0-20240711041743-f6c9dda6c6da h1:xRmpO92tb8y+Z85iUOMOicpCfaYcv7o3Cg3wKrIpg8g=
+github.com/google/pprof v0.0.0-20240711041743-f6c9dda6c6da/go.mod h1:K1liHPHnj73Fdn/EKuT8nrFqBihUSKXoLYU0BuatOYo=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o=
github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw=
@@ -332,10 +332,10 @@ github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfF
github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
-github.com/googleapis/gax-go/v2 v2.12.4 h1:9gWcmF85Wvq4ryPFvGFaOgPIs1AQX0d0bcbGw4Z96qg=
-github.com/googleapis/gax-go/v2 v2.12.4/go.mod h1:KYEYLorsnIGDi/rPC8b5TdlB9kbKoFubselGIoBMCwI=
-github.com/gophercloud/gophercloud v1.12.0 h1:Jrz16vPAL93l80q16fp8NplrTCp93y7rZh2P3Q4Yq7g=
-github.com/gophercloud/gophercloud v1.12.0/go.mod h1:aAVqcocTSXh2vYFZ1JTvx4EQmfgzxRcNupUfxZbBNDM=
+github.com/googleapis/gax-go/v2 v2.12.5 h1:8gw9KZK8TiVKB6q3zHY3SBzLnrGp6HQjyfYBYGmXdxA=
+github.com/googleapis/gax-go/v2 v2.12.5/go.mod h1:BUDKcWo+RaKq5SC9vVYL0wLADa3VcfswbOMMRmB9H3E=
+github.com/gophercloud/gophercloud v1.13.0 h1:8iY9d1DAbzMW6Vok1AxbbK5ZaUjzMp0tdyt4fX9IeJ0=
+github.com/gophercloud/gophercloud v1.13.0/go.mod h1:aAVqcocTSXh2vYFZ1JTvx4EQmfgzxRcNupUfxZbBNDM=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
@@ -353,10 +353,10 @@ github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFb
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1YCS1PXdKYWi8FsN0=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k=
github.com/hashicorp/consul/api v1.3.0/go.mod h1:MmDNSzIMUjNpY/mQ398R4bk2FnqQLoPndWW5VkKPlCE=
-github.com/hashicorp/consul/api v1.29.1 h1:UEwOjYJrd3lG1x5w7HxDRMGiAUPrb3f103EoeKuuEcc=
-github.com/hashicorp/consul/api v1.29.1/go.mod h1:lumfRkY/coLuqMICkI7Fh3ylMG31mQSRZyef2c5YvJI=
-github.com/hashicorp/consul/proto-public v0.6.1 h1:+uzH3olCrksXYWAYHKqK782CtK9scfqH+Unlw3UHhCg=
-github.com/hashicorp/consul/proto-public v0.6.1/go.mod h1:cXXbOg74KBNGajC+o8RlA502Esf0R9prcoJgiOX/2Tg=
+github.com/hashicorp/consul/api v1.29.2 h1:aYyRn8EdE2mSfG14S1+L9Qkjtz8RzmaWh6AcNGRNwPw=
+github.com/hashicorp/consul/api v1.29.2/go.mod h1:0YObcaLNDSbtlgzIRtmRXI1ZkeuK0trCBxwZQ4MYnIk=
+github.com/hashicorp/consul/proto-public v0.6.2 h1:+DA/3g/IiKlJZb88NBn0ZgXrxJp2NlvCZdEyl+qxvL0=
+github.com/hashicorp/consul/proto-public v0.6.2/go.mod h1:cXXbOg74KBNGajC+o8RlA502Esf0R9prcoJgiOX/2Tg=
github.com/hashicorp/consul/sdk v0.3.0/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
github.com/hashicorp/consul/sdk v0.16.1 h1:V8TxTnImoPD5cj0U9Spl0TUxcytjcbbJeADFF07KdHg=
github.com/hashicorp/consul/sdk v0.16.1/go.mod h1:fSXvwxB2hmh1FMZCNl6PwX0Q/1wdWtHJcZ7Ea5tns0s=
@@ -409,13 +409,13 @@ github.com/hashicorp/mdns v1.0.4/go.mod h1:mtBihi+LeNXGtG8L9dX59gAEa12BDtBQSp4v/
github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
github.com/hashicorp/memberlist v0.5.0 h1:EtYPN8DpAURiapus508I4n9CzHs2W+8NZGbmmR/prTM=
github.com/hashicorp/memberlist v0.5.0/go.mod h1:yvyXLpo0QaGE59Y7hDTsTzDD25JYBZ4mHgHUZ8lrOI0=
-github.com/hashicorp/nomad/api v0.0.0-20240604134157-e73d8bb1140d h1:KHq+mAzWSkumj4PDoXc5VZbycPGcmYu8tohgVLQ6SIc=
-github.com/hashicorp/nomad/api v0.0.0-20240604134157-e73d8bb1140d/go.mod h1:svtxn6QnrQ69P23VvIWMR34tg3vmwLz4UdUzm1dSCgE=
+github.com/hashicorp/nomad/api v0.0.0-20240717122358-3d93bd3778f3 h1:fgVfQ4AC1avVOnu2cfms8VAiD8lUq3vWI8mTocOXN/w=
+github.com/hashicorp/nomad/api v0.0.0-20240717122358-3d93bd3778f3/go.mod h1:svtxn6QnrQ69P23VvIWMR34tg3vmwLz4UdUzm1dSCgE=
github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
github.com/hashicorp/serf v0.10.1 h1:Z1H2J60yRKvfDYAOZLd2MU0ND4AH/WDz7xYHDWQsIPY=
github.com/hashicorp/serf v0.10.1/go.mod h1:yL2t6BqATOLGc5HF7qbFkTfXoPIY0WZdWHfEvMqbG+4=
-github.com/hetznercloud/hcloud-go/v2 v2.9.0 h1:s0N6R7Zoi2DPfMtUF5o9VeUBzTtHVY6MIkHOQnfu/AY=
-github.com/hetznercloud/hcloud-go/v2 v2.9.0/go.mod h1:qtW/TuU7Bs16ibXl/ktJarWqU2LwHr7eGlwoilHxtgg=
+github.com/hetznercloud/hcloud-go/v2 v2.10.2 h1:9gyTUPhfNbfbS40Spgij5mV5k37bOZgt8iHKCbfGs5I=
+github.com/hetznercloud/hcloud-go/v2 v2.10.2/go.mod h1:xQ+8KhIS62W0D78Dpi57jsufWh844gUw1az5OUvaeq8=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
@@ -454,8 +454,8 @@ github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
-github.com/klauspost/compress v1.17.8 h1:YcnTYrq7MikUT7k0Yb5eceMmALQPYBW/Xltxn0NAMnU=
-github.com/klauspost/compress v1.17.8/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
+github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
+github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b h1:udzkj9S/zlT5X367kqJis0QP7YMxobob6zhzq6Yre00=
github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b/go.mod h1:pcaDhQK0/NJZEvtCO0qQPPropqV0sJOJ6YW7X+9kRwM=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
@@ -472,8 +472,8 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM=
github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4=
-github.com/linode/linodego v1.35.0 h1:rIhUeCHBLEDlkoRnOTwzSGzljQ3ksXwLxacmXnrV+Do=
-github.com/linode/linodego v1.35.0/go.mod h1:JxuhOEAMfSxun6RU5/MgTKH2GGTmFrhKRj3wL1NFin0=
+github.com/linode/linodego v1.37.0 h1:B/2Spzv9jYXzKA+p+GD8fVCNJ7Wuw6P91ZDD9eCkkso=
+github.com/linode/linodego v1.37.0/go.mod h1:L7GXKFD3PoN2xSEtFc04wIXP5WK65O10jYQx0PQISWQ=
github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
@@ -500,8 +500,8 @@ github.com/maxatome/go-testdeep v1.12.0/go.mod h1:lPZc/HAcJMP92l7yI6TRz1aZN5URwU
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso=
github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI=
-github.com/miekg/dns v1.1.59 h1:C9EXc/UToRwKLhK5wKU/I4QVsBUc8kE6MkHBkeypWZs=
-github.com/miekg/dns v1.1.59/go.mod h1:nZpewl5p6IvctfgrckopVx2OlSEHPRO/U4SYkRklrEk=
+github.com/miekg/dns v1.1.61 h1:nLxbwF3XxhwVSm8g9Dghm9MHPaUZuqhPiGL+675ZmEs=
+github.com/miekg/dns v1.1.61/go.mod h1:mnAarhS3nWaW+NVP2wTkYVIZyHNJ098SJZUki3eykwQ=
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
github.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI=
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
@@ -573,8 +573,8 @@ github.com/openzipkin-contrib/zipkin-go-opentracing v0.4.5/go.mod h1:/wsWhb9smxS
github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw=
github.com/openzipkin/zipkin-go v0.2.1/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4=
github.com/openzipkin/zipkin-go v0.2.2/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4=
-github.com/ovh/go-ovh v1.5.1 h1:P8O+7H+NQuFK9P/j4sFW5C0fvSS2DnHYGPwdVCp45wI=
-github.com/ovh/go-ovh v1.5.1/go.mod h1:cTVDnl94z4tl8pP1uZ/8jlVxntjSIf09bNcQ5TJSC7c=
+github.com/ovh/go-ovh v1.6.0 h1:ixLOwxQdzYDx296sXcgS35TOPEahJkpjMGtzPadCjQI=
+github.com/ovh/go-ovh v1.6.0/go.mod h1:cTVDnl94z4tl8pP1uZ/8jlVxntjSIf09bNcQ5TJSC7c=
github.com/pact-foundation/pact-go v1.0.4/go.mod h1:uExwJY4kCzNPcHRj+hCR/HBbOOIwwtUjcrb0b5/5kLM=
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY=
@@ -625,8 +625,8 @@ github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8b
github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc=
github.com/prometheus/common v0.29.0/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls=
-github.com/prometheus/common v0.54.0 h1:ZlZy0BgJhTwVZUn7dLOkwCZHUkrAqd3WYtcFCWnM1D8=
-github.com/prometheus/common v0.54.0/go.mod h1:/TQgMJP5CuVYveyT7n/0Ix8yLNNXy9yRSkhnLTHPDIQ=
+github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc=
+github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8=
github.com/prometheus/common/assets v0.2.0 h1:0P5OrzoHrYBOSM1OigWL3mY8ZvV2N4zIE/5AahrSrfM=
github.com/prometheus/common/assets v0.2.0/go.mod h1:D17UVUE12bHbim7HzwUvtqm6gwBEaDQ0F+hIGbFbccI=
github.com/prometheus/common/sigv4 v0.1.0 h1:qoVebwtwwEhS85Czm2dSROY5fTo2PAPEVdDeppTwGX4=
@@ -639,8 +639,8 @@ github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsT
github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
-github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
-github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
+github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
+github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
@@ -650,8 +650,8 @@ github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E=
-github.com/scaleway/scaleway-sdk-go v1.0.0-beta.27 h1:yGAraK1uUjlhSXgNMIy8o/J4LFNcy7yeipBqt9N9mVg=
-github.com/scaleway/scaleway-sdk-go v1.0.0-beta.27/go.mod h1:fCa7OJZ/9DRTnOKmxvT6pn+LPWUptQAmHF/SBJUGEcg=
+github.com/scaleway/scaleway-sdk-go v1.0.0-beta.29 h1:BkTk4gynLjguayxrYxZoMZjBnAOh7ntQvUkOFmkMqPU=
+github.com/scaleway/scaleway-sdk-go v1.0.0-beta.29/go.mod h1:fCa7OJZ/9DRTnOKmxvT6pn+LPWUptQAmHF/SBJUGEcg=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 h1:nn5Wsu0esKSJiIVhscUtVbo7ada43DJhG55ua/hjS5I=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/shoenig/test v1.7.1 h1:UJcjSAI3aUKx52kfcfhblgyhZceouhvvs3OYdWgn+PY=
@@ -694,6 +694,7 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
+github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
@@ -723,28 +724,28 @@ go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
-go.opentelemetry.io/collector/pdata v1.11.0 h1:rzYyV1zfTQQz1DI9hCiaKyyaczqawN75XO9mdXmR/hE=
-go.opentelemetry.io/collector/pdata v1.11.0/go.mod h1:IHxHsp+Jq/xfjORQMDJjSH6jvedOSTOyu3nbxqhWSYE=
-go.opentelemetry.io/collector/semconv v0.104.0 h1:dUvajnh+AYJLEW/XOPk0T0BlwltSdi3vrjO7nSOos3k=
-go.opentelemetry.io/collector/semconv v0.104.0/go.mod h1:yMVUCNoQPZVq/IPfrHrnntZTWsLf5YGZ7qwKulIl5hw=
-go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.52.0 h1:9l89oX4ba9kHbBol3Xin3leYJ+252h0zszDtBwyKe2A=
-go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.52.0/go.mod h1:XLZfZboOJWHNKUv7eH0inh0E9VV6eWDFB/9yJyTLPp0=
-go.opentelemetry.io/otel v1.27.0 h1:9BZoF3yMK/O1AafMiQTVu0YDj5Ea4hPhxCs7sGva+cg=
-go.opentelemetry.io/otel v1.27.0/go.mod h1:DMpAK8fzYRzs+bi3rS5REupisuqTheUlSZJ1WnZaPAQ=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.27.0 h1:R9DE4kQ4k+YtfLI2ULwX82VtNQ2J8yZmA7ZIF/D+7Mc=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.27.0/go.mod h1:OQFyQVrDlbe+R7xrEyDr/2Wr67Ol0hRUgsfA+V5A95s=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0 h1:qFffATk0X+HD+f1Z8lswGiOQYKHRlzfmdJm0wEaVrFA=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0/go.mod h1:MOiCmryaYtc+V0Ei+Tx9o5S1ZjA7kzLucuVuyzBZloQ=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.27.0 h1:QY7/0NeRPKlzusf40ZE4t1VlMKbqSNT7cJRYzWuja0s=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.27.0/go.mod h1:HVkSiDhTM9BoUJU8qE6j2eSWLLXvi1USXjyd2BXT8PY=
-go.opentelemetry.io/otel/metric v1.27.0 h1:hvj3vdEKyeCi4YaYfNjv2NUje8FqKqUY8IlF0FxV/ik=
-go.opentelemetry.io/otel/metric v1.27.0/go.mod h1:mVFgmRlhljgBiuk/MP/oKylr4hs85GZAylncepAX/ak=
-go.opentelemetry.io/otel/sdk v1.27.0 h1:mlk+/Y1gLPLn84U4tI8d3GNJmGT/eXe3ZuOXN9kTWmI=
-go.opentelemetry.io/otel/sdk v1.27.0/go.mod h1:Ha9vbLwJE6W86YstIywK2xFfPjbWlCuwPtMkKdz/Y4A=
-go.opentelemetry.io/otel/trace v1.27.0 h1:IqYb813p7cmbHk0a5y6pD5JPakbVfftRXABGt5/Rscw=
-go.opentelemetry.io/otel/trace v1.27.0/go.mod h1:6RiD1hkAprV4/q+yd2ln1HG9GoPx39SuvvstaLBl+l4=
-go.opentelemetry.io/proto/otlp v1.2.0 h1:pVeZGk7nXDC9O2hncA6nHldxEjm6LByfA2aN8IOkz94=
-go.opentelemetry.io/proto/otlp v1.2.0/go.mod h1:gGpR8txAl5M03pDhMC79G6SdqNV26naRm/KDsgaHD8A=
+go.opentelemetry.io/collector/pdata v1.12.0 h1:Xx5VK1p4VO0md8MWm2icwC1MnJ7f8EimKItMWw46BmA=
+go.opentelemetry.io/collector/pdata v1.12.0/go.mod h1:MYeB0MmMAxeM0hstCFrCqWLzdyeYySim2dG6pDT6nYI=
+go.opentelemetry.io/collector/semconv v0.105.0 h1:8p6dZ3JfxFTjbY38d8xlQGB1TQ3nPUvs+D0RERniZ1g=
+go.opentelemetry.io/collector/semconv v0.105.0/go.mod h1:yMVUCNoQPZVq/IPfrHrnntZTWsLf5YGZ7qwKulIl5hw=
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 h1:4K4tsIXefpVJtvA/8srF4V4y0akAoPHkIslgAkjixJA=
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0/go.mod h1:jjdQuTGVsXV4vSs+CJ2qYDeDPf9yIJV23qlIzBm73Vg=
+go.opentelemetry.io/otel v1.28.0 h1:/SqNcYk+idO0CxKEUOtKQClMK/MimZihKYMruSMViUo=
+go.opentelemetry.io/otel v1.28.0/go.mod h1:q68ijF8Fc8CnMHKyzqL6akLO46ePnjkgfIMIjUIX9z4=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 h1:3Q/xZUyC1BBkualc9ROb4G8qkH90LXEIICcs5zv1OYY=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0/go.mod h1:s75jGIWA9OfCMzF0xr+ZgfrB5FEbbV7UuYo32ahUiFI=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.28.0 h1:R3X6ZXmNPRR8ul6i3WgFURCHzaXjHdm0karRG/+dj3s=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.28.0/go.mod h1:QWFXnDavXWwMx2EEcZsf3yxgEKAqsxQ+Syjp+seyInw=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.28.0 h1:j9+03ymgYhPKmeXGk5Zu+cIZOlVzd9Zv7QIiyItjFBU=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.28.0/go.mod h1:Y5+XiUG4Emn1hTfciPzGPJaSI+RpDts6BnCIir0SLqk=
+go.opentelemetry.io/otel/metric v1.28.0 h1:f0HGvSl1KRAU1DLgLGFjrwVyismPlnuU6JD6bOeuA5Q=
+go.opentelemetry.io/otel/metric v1.28.0/go.mod h1:Fb1eVBFZmLVTMb6PPohq3TO9IIhUisDsbJoL/+uQW4s=
+go.opentelemetry.io/otel/sdk v1.28.0 h1:b9d7hIry8yZsgtbmM0DKyPWMMUMlK9NEKuIG4aBqWyE=
+go.opentelemetry.io/otel/sdk v1.28.0/go.mod h1:oYj7ClPUA7Iw3m+r7GeEjz0qckQRJK2B8zjcZEfu7Pg=
+go.opentelemetry.io/otel/trace v1.28.0 h1:GhQ9cUuQGmNDd5BTCP2dAvv75RdMxEfTmYejp+lkx9g=
+go.opentelemetry.io/otel/trace v1.28.0/go.mod h1:jPyXzNPg6da9+38HEwElrQiHlVMTnVfM3/yv2OlIHaI=
+go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0=
+go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8=
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
@@ -773,8 +774,8 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y
golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
-golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI=
-golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
+golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
+golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@@ -809,8 +810,8 @@ golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
-golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0=
-golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
+golang.org/x/mod v0.19.0 h1:fEdghXQSo20giMthA7cd28ZC+jts4amQ3YMXiP5oMQ8=
+golang.org/x/mod v0.19.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -856,8 +857,8 @@ golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
-golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ=
-golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
+golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
+golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
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=
@@ -946,16 +947,16 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
-golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws=
-golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
+golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
-golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA=
-golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0=
+golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk=
+golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4=
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=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -1025,8 +1026,8 @@ golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
-golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA=
-golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c=
+golang.org/x/tools v0.23.0 h1:SGsXPZ+2l4JsgaCKkx+FQ9YZ5XEtA1GZYuoDjenLjvg=
+golang.org/x/tools v0.23.0/go.mod h1:pnu6ufv6vQkll6szChhK3C3L/ruaIv5eBeztNG8wtsI=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -1046,8 +1047,8 @@ google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/
google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
-google.golang.org/api v0.183.0 h1:PNMeRDwo1pJdgNcFQ9GstuLe/noWKIc89pRWRLMvLwE=
-google.golang.org/api v0.183.0/go.mod h1:q43adC5/pHoSZTx5h2mSmdF7NcyfW9JuDyIOJAgS9ZQ=
+google.golang.org/api v0.188.0 h1:51y8fJ/b1AaaBRJr4yWm96fPcuxSo0JcegXE3DaHQHw=
+google.golang.org/api v0.188.0/go.mod h1:VR0d+2SIiWOYG3r/jdm7adPW9hI2aRv9ETOSCQ9Beag=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
@@ -1084,10 +1085,10 @@ google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1m
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157 h1:7whR9kGa5LUwFtpLm2ArCEejtnxlGeLbAyjFY8sGNFw=
-google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157/go.mod h1:99sLkeliLXfdj2J75X3Ho+rrVCaJze0uwN7zDDkjPVU=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157 h1:Zy9XzmMEflZ/MAaA7vNcoebnRAld7FsPW1EeBB7V0m8=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157/go.mod h1:EfXuqaE1J41VCDicxHzUDm+8rk+7ZdXzHV0IhO/I6s0=
+google.golang.org/genproto/googleapis/api v0.0.0-20240711142825-46eb208f015d h1:kHjw/5UfflP/L5EbledDrcG4C2597RtymmGRZvHiCuY=
+google.golang.org/genproto/googleapis/api v0.0.0-20240711142825-46eb208f015d/go.mod h1:mw8MG/Qz5wfgYr6VqVCiZcHe/GJEfI+oGGDCohaVgB0=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20240708141625-4ad9e859172b h1:04+jVzTs2XBnOZcPsLnmrTGqltqJbZQ1Ey26hjYdQQ0=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20240708141625-4ad9e859172b/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY=
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=
@@ -1106,8 +1107,8 @@ google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3Iji
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
-google.golang.org/grpc v1.64.0 h1:KH3VH9y/MgNQg1dE7b3XfVK0GsPSIzJwdF617gUSbvY=
-google.golang.org/grpc v1.64.0/go.mod h1:oxjF8E3FBnjp+/gVFYdWacaLDx9na1aqy9oovLpxQYg=
+google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc=
+google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ=
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=
From 8f9069f41b20915ee9db856db3881c48b4dcf4cf Mon Sep 17 00:00:00 2001
From: Bryan Boreham
Date: Wed, 17 Jul 2024 17:00:50 +0100
Subject: [PATCH 11/44] Bump Otel semconv version to 1.26.0
Signed-off-by: Bryan Boreham
---
tracing/tracing.go | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/tracing/tracing.go b/tracing/tracing.go
index 3b1c61532..6b9319ecb 100644
--- a/tracing/tracing.go
+++ b/tracing/tracing.go
@@ -30,7 +30,7 @@ import (
"go.opentelemetry.io/otel/propagation"
"go.opentelemetry.io/otel/sdk/resource"
tracesdk "go.opentelemetry.io/otel/sdk/trace"
- semconv "go.opentelemetry.io/otel/semconv/v1.25.0"
+ semconv "go.opentelemetry.io/otel/semconv/v1.26.0"
"go.opentelemetry.io/otel/trace"
"go.opentelemetry.io/otel/trace/noop"
"google.golang.org/grpc/credentials"
From ce3bd4abea1f395d7f1fcfe01ded0daeaac79fdd Mon Sep 17 00:00:00 2001
From: Bryan Boreham
Date: Wed, 17 Jul 2024 17:03:32 +0100
Subject: [PATCH 12/44] Update for Docker deprecation
Signed-off-by: Bryan Boreham
---
discovery/moby/network.go | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/discovery/moby/network.go b/discovery/moby/network.go
index 794d2e607..ea1ca66bc 100644
--- a/discovery/moby/network.go
+++ b/discovery/moby/network.go
@@ -17,7 +17,7 @@ import (
"context"
"strconv"
- "github.com/docker/docker/api/types"
+ "github.com/docker/docker/api/types/network"
"github.com/docker/docker/client"
"github.com/prometheus/prometheus/util/strutil"
@@ -34,7 +34,7 @@ const (
)
func getNetworksLabels(ctx context.Context, client *client.Client, labelPrefix string) (map[string]map[string]string, error) {
- networks, err := client.NetworkList(ctx, types.NetworkListOptions{})
+ networks, err := client.NetworkList(ctx, network.ListOptions{})
if err != nil {
return nil, err
}
From 932918cd3fe003d0f4fd0fe153b610a068fe9fea Mon Sep 17 00:00:00 2001
From: Arve Knudsen
Date: Thu, 18 Jul 2024 10:40:47 +0200
Subject: [PATCH 13/44] OTLPConfig.UnmarshalYAML: Return error on invalid input
Signed-off-by: Arve Knudsen
---
config/config.go | 22 ++++++++++---------
config/config_test.go | 22 +++++++++++++------
.../otlp_sanitize_resource_attributes.bad.yml | 2 ++
...otlp_sanitize_resource_attributes.good.yml | 2 +-
4 files changed, 30 insertions(+), 18 deletions(-)
create mode 100644 config/testdata/otlp_sanitize_resource_attributes.bad.yml
diff --git a/config/config.go b/config/config.go
index fd2e6e06c..913983881 100644
--- a/config/config.go
+++ b/config/config.go
@@ -19,7 +19,6 @@ import (
"net/url"
"os"
"path/filepath"
- "slices"
"sort"
"strconv"
"strings"
@@ -1324,17 +1323,20 @@ func (c *OTLPConfig) UnmarshalYAML(unmarshal func(interface{}) error) error {
}
seen := map[string]struct{}{}
- i := 0
- for i < len(c.PromoteResourceAttributes) {
- s := strings.TrimSpace(c.PromoteResourceAttributes[i])
- if _, exists := seen[s]; exists {
- c.PromoteResourceAttributes = slices.Delete(c.PromoteResourceAttributes, i, i+1)
+ var err error
+ for i, attr := range c.PromoteResourceAttributes {
+ attr = strings.TrimSpace(attr)
+ if attr == "" {
+ err = errors.Join(err, fmt.Errorf("empty promoted OTel resource attribute"))
+ continue
+ }
+ if _, exists := seen[attr]; exists {
+ err = errors.Join(err, fmt.Errorf("duplicated promoted OTel resource attribute %q", attr))
continue
}
- seen[s] = struct{}{}
- c.PromoteResourceAttributes[i] = s
- i++
+ seen[attr] = struct{}{}
+ c.PromoteResourceAttributes[i] = attr
}
- return nil
+ return err
}
diff --git a/config/config_test.go b/config/config_test.go
index 5822d2ceb..b684fdb50 100644
--- a/config/config_test.go
+++ b/config/config_test.go
@@ -1478,15 +1478,23 @@ func TestRemoteWriteRetryOnRateLimit(t *testing.T) {
}
func TestOTLPSanitizeResourceAttributes(t *testing.T) {
- want, err := LoadFile(filepath.Join("testdata", "otlp_sanitize_resource_attributes.good.yml"), false, false, log.NewNopLogger())
- require.NoError(t, err)
+ t.Run("good config", func(t *testing.T) {
+ want, err := LoadFile(filepath.Join("testdata", "otlp_sanitize_resource_attributes.good.yml"), false, false, log.NewNopLogger())
+ require.NoError(t, err)
- out, err := yaml.Marshal(want)
- require.NoError(t, err)
- var got Config
- require.NoError(t, yaml.UnmarshalStrict(out, &got))
+ out, err := yaml.Marshal(want)
+ require.NoError(t, err)
+ var got Config
+ require.NoError(t, yaml.UnmarshalStrict(out, &got))
- require.Equal(t, []string{"k8s.cluster.name", "k8s.job.name", "k8s.namespace.name"}, got.OTLPConfig.PromoteResourceAttributes)
+ require.Equal(t, []string{"k8s.cluster.name", "k8s.job.name", "k8s.namespace.name"}, got.OTLPConfig.PromoteResourceAttributes)
+ })
+
+ t.Run("bad config", func(t *testing.T) {
+ _, err := LoadFile(filepath.Join("testdata", "otlp_sanitize_resource_attributes.bad.yml"), false, false, log.NewNopLogger())
+ require.ErrorContains(t, err, `duplicated promoted OTel resource attribute "k8s.job.name"`)
+ require.ErrorContains(t, err, `empty promoted OTel resource attribute`)
+ })
}
func TestLoadConfig(t *testing.T) {
diff --git a/config/testdata/otlp_sanitize_resource_attributes.bad.yml b/config/testdata/otlp_sanitize_resource_attributes.bad.yml
new file mode 100644
index 000000000..37ec5d120
--- /dev/null
+++ b/config/testdata/otlp_sanitize_resource_attributes.bad.yml
@@ -0,0 +1,2 @@
+otlp:
+ promote_resource_attributes: ["k8s.cluster.name", " k8s.job.name ", "k8s.namespace.name", "k8s.job.name", ""]
diff --git a/config/testdata/otlp_sanitize_resource_attributes.good.yml b/config/testdata/otlp_sanitize_resource_attributes.good.yml
index ce91302fe..67247e774 100644
--- a/config/testdata/otlp_sanitize_resource_attributes.good.yml
+++ b/config/testdata/otlp_sanitize_resource_attributes.good.yml
@@ -1,2 +1,2 @@
otlp:
- promote_resource_attributes: ["k8s.cluster.name", "k8s.job.name", "k8s.namespace.name", " k8s.job.name "]
+ promote_resource_attributes: ["k8s.cluster.name", " k8s.job.name ", "k8s.namespace.name"]
From a6fa8a471c58b4c3dc2027bcd03bdf42cd9bdb49 Mon Sep 17 00:00:00 2001
From: beorn7
Date: Thu, 18 Jul 2024 12:30:55 +0200
Subject: [PATCH 14/44] docs: Correct and clarify histogram bucket and
resolution limits
Previously, the config doc suggested that scrapes are simply failed if
the bucket limit is exceeded. However, instead Prometheus first
attempts to reduce the resolution. This will succeed in almost all
non-pathological cases. However, in the rare case that it doesn't, the
scrape will be failed after all.
For the resolution limit AKA `native_histogram_min_bucket_factor`, the
logic is a bit different. Once the highest factor, i.e. schema -4, is
reached, the scrape will still succeed.
Signed-off-by: beorn7
---
docs/configuration/configuration.md | 8 +++++---
1 file changed, 5 insertions(+), 3 deletions(-)
diff --git a/docs/configuration/configuration.md b/docs/configuration/configuration.md
index 35976871b..ff24082e4 100644
--- a/docs/configuration/configuration.md
+++ b/docs/configuration/configuration.md
@@ -458,13 +458,15 @@ metric_relabel_configs:
[ keep_dropped_targets: | default = 0 ]
# Limit on total number of positive and negative buckets allowed in a single
-# native histogram. If this is exceeded, the entire scrape will be treated as
-# failed. 0 means no limit.
+# native histogram. The resolution of a histogram with more buckets will be
+# reduced until the number of buckets is within the limit. If the limit cannot
+# be reached, the scrape will fail.
+# 0 means no limit.
[ native_histogram_bucket_limit: | default = 0 ]
# Lower limit for the growth factor of one bucket to the next in each native
# histogram. The resolution of a histogram with a lower growth factor will be
-# reduced until it is within the limit.
+# reduced as much as possible until it is within the limit.
# To set an upper limit for the schema (equivalent to "scale" in OTel's
# exponential histograms), use the following factor limits:
#
From a60e5ce362f53802b20230add513e586e5775f91 Mon Sep 17 00:00:00 2001
From: Bartlomiej Plotka
Date: Fri, 19 Jul 2024 19:53:40 +0200
Subject: [PATCH 15/44] [PRW 2.0] Added Sender and RW Handler support for
Response Stats. (#14444)
* [PRW 2.0] Added Sender support for Response Stats.
Chained on top of https://github.com/prometheus/prometheus/pull/14427
Fixes https://github.com/prometheus/prometheus/issues/14359
Signed-off-by: bwplotka
* Addressed comments.
Signed-off-by: bwplotka
* move write stats to it's own file
Signed-off-by: Callum Styan
* Clean up header usage
Signed-off-by: Callum Styan
* add missing license to new stats file
Signed-off-by: Callum Styan
* Addressed all comments.
Signed-off-by: bwplotka
---------
Signed-off-by: bwplotka
Signed-off-by: Callum Styan
Co-authored-by: Callum Styan
---
CHANGELOG.md | 3 +
cmd/promtool/metrics.go | 3 +-
storage/remote/client.go | 31 ++++---
storage/remote/client_test.go | 8 +-
storage/remote/queue_manager.go | 125 ++++++++++++++++++++-------
storage/remote/queue_manager_test.go | 50 +++++------
storage/remote/stats.go | 107 +++++++++++++++++++++++
storage/remote/write_handler.go | 60 +++++--------
storage/remote/write_handler_test.go | 16 ++--
9 files changed, 276 insertions(+), 127 deletions(-)
create mode 100644 storage/remote/stats.go
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d5a91e900..e7314d041 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,9 @@
## unreleased
+* [FEATURE] Remote-Write: Add sender and receiver support for [Remote Write 2.0-rc.2](https://prometheus.io/docs/specs/remote_write_spec_2_0/) specification #14395 #14427 #14444
+* [ENHANCEMENT] Remote-Write: 1.x messages against Remote Write 2.x Receivers will have now correct values for `prometheus_storage__failed_total` in case of partial errors #14444
+
## 2.53.1 / 2024-07-10
Fix a bug which would drop samples in remote-write if the sending flow stalled
diff --git a/cmd/promtool/metrics.go b/cmd/promtool/metrics.go
index 46246b672..6d162f459 100644
--- a/cmd/promtool/metrics.go
+++ b/cmd/promtool/metrics.go
@@ -101,6 +101,7 @@ func PushMetrics(url *url.URL, roundTripper http.RoundTripper, headers map[strin
return successExitCode
}
+// TODO(bwplotka): Add PRW 2.0 support.
func parseAndPushMetrics(client *remote.Client, data []byte, labels map[string]string) bool {
metricsData, err := fmtutil.MetricTextToWriteRequest(bytes.NewReader(data), labels)
if err != nil {
@@ -116,7 +117,7 @@ func parseAndPushMetrics(client *remote.Client, data []byte, labels map[string]s
// Encode the request body into snappy encoding.
compressed := snappy.Encode(nil, raw)
- err = client.Store(context.Background(), compressed, 0)
+ _, err = client.Store(context.Background(), compressed, 0)
if err != nil {
fmt.Fprintln(os.Stderr, " FAILED:", err)
return false
diff --git a/storage/remote/client.go b/storage/remote/client.go
index eff44c606..17caf7be9 100644
--- a/storage/remote/client.go
+++ b/storage/remote/client.go
@@ -14,7 +14,6 @@
package remote
import (
- "bufio"
"bytes"
"context"
"fmt"
@@ -235,12 +234,12 @@ type RecoverableError struct {
// Store sends a batch of samples to the HTTP endpoint, the request is the proto marshalled
// and encoded bytes from codec.go.
-func (c *Client) Store(ctx context.Context, req []byte, attempt int) error {
+func (c *Client) Store(ctx context.Context, req []byte, attempt int) (WriteResponseStats, error) {
httpReq, err := http.NewRequest(http.MethodPost, c.urlString, bytes.NewReader(req))
if err != nil {
// Errors from NewRequest are from unparsable URLs, so are not
// recoverable.
- return err
+ return WriteResponseStats{}, err
}
httpReq.Header.Add("Content-Encoding", string(c.writeCompression))
@@ -267,28 +266,34 @@ func (c *Client) Store(ctx context.Context, req []byte, attempt int) error {
if err != nil {
// Errors from Client.Do are from (for example) network errors, so are
// recoverable.
- return RecoverableError{err, defaultBackoff}
+ return WriteResponseStats{}, RecoverableError{err, defaultBackoff}
}
defer func() {
io.Copy(io.Discard, httpResp.Body)
httpResp.Body.Close()
}()
+ // TODO(bwplotka): Pass logger and emit debug on error?
+ // Parsing error means there were some response header values we can't parse,
+ // we can continue handling.
+ rs, _ := ParseWriteResponseStats(httpResp)
+
//nolint:usestdlibvars
- if httpResp.StatusCode/100 != 2 {
- scanner := bufio.NewScanner(io.LimitReader(httpResp.Body, maxErrMsgLen))
- line := ""
- if scanner.Scan() {
- line = scanner.Text()
- }
- err = fmt.Errorf("server returned HTTP status %s: %s", httpResp.Status, line)
+ if httpResp.StatusCode/100 == 2 {
+ return rs, nil
}
+
+ // Handling errors e.g. read potential error in the body.
+ // TODO(bwplotka): Pass logger and emit debug on error?
+ body, _ := io.ReadAll(io.LimitReader(httpResp.Body, maxErrMsgLen))
+ err = fmt.Errorf("server returned HTTP status %s: %s", httpResp.Status, body)
+
//nolint:usestdlibvars
if httpResp.StatusCode/100 == 5 ||
(c.retryOnRateLimit && httpResp.StatusCode == http.StatusTooManyRequests) {
- return RecoverableError{err, retryAfterDuration(httpResp.Header.Get("Retry-After"))}
+ return rs, RecoverableError{err, retryAfterDuration(httpResp.Header.Get("Retry-After"))}
}
- return err
+ return rs, err
}
// retryAfterDuration returns the duration for the Retry-After header. In case of any errors, it
diff --git a/storage/remote/client_test.go b/storage/remote/client_test.go
index 2acb8e279..9184ce100 100644
--- a/storage/remote/client_test.go
+++ b/storage/remote/client_test.go
@@ -73,7 +73,7 @@ func TestStoreHTTPErrorHandling(t *testing.T) {
c, err := NewWriteClient(hash, conf)
require.NoError(t, err)
- err = c.Store(context.Background(), []byte{}, 0)
+ _, err = c.Store(context.Background(), []byte{}, 0)
if test.err != nil {
require.EqualError(t, err, test.err.Error())
} else {
@@ -133,7 +133,7 @@ func TestClientRetryAfter(t *testing.T) {
c := getClient(getClientConfig(serverURL, tc.retryOnRateLimit))
var recErr RecoverableError
- err = c.Store(context.Background(), []byte{}, 0)
+ _, err = c.Store(context.Background(), []byte{}, 0)
require.Equal(t, tc.expectedRecoverable, errors.As(err, &recErr), "Mismatch in expected recoverable error status.")
if tc.expectedRecoverable {
require.Equal(t, tc.expectedRetryAfter, recErr.retryAfter)
@@ -169,7 +169,7 @@ func TestRetryAfterDuration(t *testing.T) {
}
}
-func TestClientHeaders(t *testing.T) {
+func TestClientCustomHeaders(t *testing.T) {
headersToSend := map[string]string{"Foo": "Bar", "Baz": "qux"}
var called bool
@@ -203,7 +203,7 @@ func TestClientHeaders(t *testing.T) {
c, err := NewWriteClient("c", conf)
require.NoError(t, err)
- err = c.Store(context.Background(), []byte{}, 0)
+ _, err = c.Store(context.Background(), []byte{}, 0)
require.NoError(t, err)
require.True(t, called, "The remote server wasn't called")
diff --git a/storage/remote/queue_manager.go b/storage/remote/queue_manager.go
index 5bafb9da2..5b59288e6 100644
--- a/storage/remote/queue_manager.go
+++ b/storage/remote/queue_manager.go
@@ -391,7 +391,7 @@ func (m *queueManagerMetrics) unregister() {
// external timeseries database.
type WriteClient interface {
// Store stores the given samples in the remote storage.
- Store(ctx context.Context, req []byte, retryAttempt int) error
+ Store(ctx context.Context, req []byte, retryAttempt int) (WriteResponseStats, error)
// Name uniquely identifies the remote storage.
Name() string
// Endpoint is the remote read or write endpoint for the storage client.
@@ -597,14 +597,15 @@ func (t *QueueManager) sendMetadataWithBackoff(ctx context.Context, metadata []p
}
begin := time.Now()
- err := t.storeClient.Store(ctx, req, try)
+ // Ignoring WriteResponseStats, because there is nothing for metadata, since it's
+ // embedded in v2 calls now, and we do v1 here.
+ _, err := t.storeClient.Store(ctx, req, try)
t.metrics.sentBatchDuration.Observe(time.Since(begin).Seconds())
if err != nil {
span.RecordError(err)
return err
}
-
return nil
}
@@ -1661,8 +1662,8 @@ func populateTimeSeries(batch []timeSeries, pendingData []prompb.TimeSeries, sen
func (s *shards) sendSamples(ctx context.Context, samples []prompb.TimeSeries, sampleCount, exemplarCount, histogramCount int, pBuf *proto.Buffer, buf *[]byte, enc Compression) error {
begin := time.Now()
- err := s.sendSamplesWithBackoff(ctx, samples, sampleCount, exemplarCount, histogramCount, 0, pBuf, buf, enc)
- s.updateMetrics(ctx, err, sampleCount, exemplarCount, histogramCount, 0, time.Since(begin))
+ rs, err := s.sendSamplesWithBackoff(ctx, samples, sampleCount, exemplarCount, histogramCount, 0, pBuf, buf, enc)
+ s.updateMetrics(ctx, err, sampleCount, exemplarCount, histogramCount, 0, rs, time.Since(begin))
return err
}
@@ -1670,17 +1671,29 @@ func (s *shards) sendSamples(ctx context.Context, samples []prompb.TimeSeries, s
// See https://github.com/prometheus/prometheus/issues/14409
func (s *shards) sendV2Samples(ctx context.Context, samples []writev2.TimeSeries, labels []string, sampleCount, exemplarCount, histogramCount, metadataCount int, pBuf, buf *[]byte, enc Compression) error {
begin := time.Now()
- err := s.sendV2SamplesWithBackoff(ctx, samples, labels, sampleCount, exemplarCount, histogramCount, metadataCount, pBuf, buf, enc)
- s.updateMetrics(ctx, err, sampleCount, exemplarCount, histogramCount, metadataCount, time.Since(begin))
+ rs, err := s.sendV2SamplesWithBackoff(ctx, samples, labels, sampleCount, exemplarCount, histogramCount, metadataCount, pBuf, buf, enc)
+ s.updateMetrics(ctx, err, sampleCount, exemplarCount, histogramCount, metadataCount, rs, time.Since(begin))
return err
}
-func (s *shards) updateMetrics(_ context.Context, err error, sampleCount, exemplarCount, histogramCount, metadataCount int, duration time.Duration) {
+func (s *shards) updateMetrics(_ context.Context, err error, sampleCount, exemplarCount, histogramCount, metadataCount int, rs WriteResponseStats, duration time.Duration) {
+ // Partial errors may happen -- account for that.
+ sampleDiff := sampleCount - rs.Samples
+ if sampleDiff > 0 {
+ s.qm.metrics.failedSamplesTotal.Add(float64(sampleDiff))
+ }
+ histogramDiff := histogramCount - rs.Histograms
+ if histogramDiff > 0 {
+ s.qm.metrics.failedHistogramsTotal.Add(float64(histogramDiff))
+ }
+ exemplarDiff := exemplarCount - rs.Exemplars
+ if exemplarDiff > 0 {
+ s.qm.metrics.failedExemplarsTotal.Add(float64(exemplarDiff))
+ }
if err != nil {
- level.Error(s.qm.logger).Log("msg", "non-recoverable error", "count", sampleCount, "exemplarCount", exemplarCount, "histogramCount", histogramCount, "err", err)
- s.qm.metrics.failedSamplesTotal.Add(float64(sampleCount))
- s.qm.metrics.failedExemplarsTotal.Add(float64(exemplarCount))
- s.qm.metrics.failedHistogramsTotal.Add(float64(histogramCount))
+ level.Error(s.qm.logger).Log("msg", "non-recoverable error", "failedSampleCount", sampleDiff, "failedHistogramCount", histogramDiff, "failedExemplarCount", exemplarDiff, "err", err)
+ } else if sampleDiff+exemplarDiff+histogramDiff > 0 {
+ level.Error(s.qm.logger).Log("msg", "we got 2xx status code from the Receiver yet statistics indicate some dat was not written; investigation needed", "failedSampleCount", sampleDiff, "failedHistogramCount", histogramDiff, "failedExemplarCount", exemplarDiff)
}
// These counters are used to calculate the dynamic sharding, and as such
@@ -1688,6 +1701,7 @@ func (s *shards) updateMetrics(_ context.Context, err error, sampleCount, exempl
s.qm.dataOut.incr(int64(sampleCount + exemplarCount + histogramCount + metadataCount))
s.qm.dataOutDuration.incr(int64(duration))
s.qm.lastSendTimestamp.Store(time.Now().Unix())
+
// Pending samples/exemplars/histograms also should be subtracted, as an error means
// they will not be retried.
s.qm.metrics.pendingSamples.Sub(float64(sampleCount))
@@ -1699,19 +1713,29 @@ func (s *shards) updateMetrics(_ context.Context, err error, sampleCount, exempl
}
// sendSamples to the remote storage with backoff for recoverable errors.
-func (s *shards) sendSamplesWithBackoff(ctx context.Context, samples []prompb.TimeSeries, sampleCount, exemplarCount, histogramCount, metadataCount int, pBuf *proto.Buffer, buf *[]byte, enc Compression) error {
+func (s *shards) sendSamplesWithBackoff(ctx context.Context, samples []prompb.TimeSeries, sampleCount, exemplarCount, histogramCount, metadataCount int, pBuf *proto.Buffer, buf *[]byte, enc Compression) (WriteResponseStats, error) {
// Build the WriteRequest with no metadata.
req, highest, lowest, err := buildWriteRequest(s.qm.logger, samples, nil, pBuf, buf, nil, enc)
s.qm.buildRequestLimitTimestamp.Store(lowest)
if err != nil {
// Failing to build the write request is non-recoverable, since it will
// only error if marshaling the proto to bytes fails.
- return err
+ return WriteResponseStats{}, err
}
reqSize := len(req)
*buf = req
+ // Since we retry writes via attemptStore and sendWriteRequestWithBackoff we need
+ // to track the total amount of accepted data across the various attempts.
+ accumulatedStats := WriteResponseStats{}
+ var accumulatedStatsMu sync.Mutex
+ addStats := func(rs WriteResponseStats) {
+ accumulatedStatsMu.Lock()
+ accumulatedStats = accumulatedStats.Add(rs)
+ accumulatedStatsMu.Unlock()
+ }
+
// An anonymous function allows us to defer the completion of our per-try spans
// without causing a memory leak, and it has the nice effect of not propagating any
// parameters for sendSamplesWithBackoff/3.
@@ -1759,15 +1783,19 @@ func (s *shards) sendSamplesWithBackoff(ctx context.Context, samples []prompb.Ti
s.qm.metrics.exemplarsTotal.Add(float64(exemplarCount))
s.qm.metrics.histogramsTotal.Add(float64(histogramCount))
s.qm.metrics.metadataTotal.Add(float64(metadataCount))
- err := s.qm.client().Store(ctx, *buf, try)
+ // Technically for v1, we will likely have empty response stats, but for
+ // newer Receivers this might be not, so used it in a best effort.
+ rs, err := s.qm.client().Store(ctx, *buf, try)
s.qm.metrics.sentBatchDuration.Observe(time.Since(begin).Seconds())
+ // TODO(bwplotka): Revisit this once we have Receivers doing retriable partial error
+ // so far we don't have those, so it's ok to potentially skew statistics.
+ addStats(rs)
- if err != nil {
- span.RecordError(err)
- return err
+ if err == nil {
+ return nil
}
-
- return nil
+ span.RecordError(err)
+ return err
}
onRetry := func() {
@@ -1780,29 +1808,48 @@ func (s *shards) sendSamplesWithBackoff(ctx context.Context, samples []prompb.Ti
if errors.Is(err, context.Canceled) {
// When there is resharding, we cancel the context for this queue, which means the data is not sent.
// So we exit early to not update the metrics.
- return err
+ return accumulatedStats, err
}
s.qm.metrics.sentBytesTotal.Add(float64(reqSize))
s.qm.metrics.highestSentTimestamp.Set(float64(highest / 1000))
- return err
+ if err == nil && !accumulatedStats.Confirmed {
+ // No 2.0 response headers, and we sent v1 message, so likely it's 1.0 Receiver.
+ // Assume success, don't rely on headers.
+ return WriteResponseStats{
+ Samples: sampleCount,
+ Histograms: histogramCount,
+ Exemplars: exemplarCount,
+ }, nil
+ }
+ return accumulatedStats, err
}
// sendV2Samples to the remote storage with backoff for recoverable errors.
-func (s *shards) sendV2SamplesWithBackoff(ctx context.Context, samples []writev2.TimeSeries, labels []string, sampleCount, exemplarCount, histogramCount, metadataCount int, pBuf, buf *[]byte, enc Compression) error {
+func (s *shards) sendV2SamplesWithBackoff(ctx context.Context, samples []writev2.TimeSeries, labels []string, sampleCount, exemplarCount, histogramCount, metadataCount int, pBuf, buf *[]byte, enc Compression) (WriteResponseStats, error) {
// Build the WriteRequest with no metadata.
req, highest, lowest, err := buildV2WriteRequest(s.qm.logger, samples, labels, pBuf, buf, nil, enc)
s.qm.buildRequestLimitTimestamp.Store(lowest)
if err != nil {
// Failing to build the write request is non-recoverable, since it will
// only error if marshaling the proto to bytes fails.
- return err
+ return WriteResponseStats{}, err
}
reqSize := len(req)
*buf = req
+ // Since we retry writes via attemptStore and sendWriteRequestWithBackoff we need
+ // to track the total amount of accepted data across the various attempts.
+ accumulatedStats := WriteResponseStats{}
+ var accumulatedStatsMu sync.Mutex
+ addStats := func(rs WriteResponseStats) {
+ accumulatedStatsMu.Lock()
+ accumulatedStats = accumulatedStats.Add(rs)
+ accumulatedStatsMu.Unlock()
+ }
+
// An anonymous function allows us to defer the completion of our per-try spans
// without causing a memory leak, and it has the nice effect of not propagating any
// parameters for sendSamplesWithBackoff/3.
@@ -1850,15 +1897,28 @@ func (s *shards) sendV2SamplesWithBackoff(ctx context.Context, samples []writev2
s.qm.metrics.exemplarsTotal.Add(float64(exemplarCount))
s.qm.metrics.histogramsTotal.Add(float64(histogramCount))
s.qm.metrics.metadataTotal.Add(float64(metadataCount))
- err := s.qm.client().Store(ctx, *buf, try)
+ rs, err := s.qm.client().Store(ctx, *buf, try)
s.qm.metrics.sentBatchDuration.Observe(time.Since(begin).Seconds())
+ // TODO(bwplotka): Revisit this once we have Receivers doing retriable partial error
+ // so far we don't have those, so it's ok to potentially skew statistics.
+ addStats(rs)
- if err != nil {
- span.RecordError(err)
- return err
+ if err == nil {
+ // Check the case mentioned in PRW 2.0
+ // https://prometheus.io/docs/specs/remote_write_spec_2_0/#required-written-response-headers.
+ if sampleCount+histogramCount+exemplarCount > 0 && rs.NoDataWritten() {
+ err = fmt.Errorf("sent v2 request with %v samples, %v histograms and %v exemplars; got 2xx, but PRW 2.0 response header statistics indicate %v samples, %v histograms and %v exemplars were accepted;"+
+ " assumining failure e.g. the target only supports PRW 1.0 prometheus.WriteRequest, but does not check the Content-Type header correctly",
+ sampleCount, histogramCount, exemplarCount,
+ rs.Samples, rs.Histograms, rs.Exemplars,
+ )
+ span.RecordError(err)
+ return err
+ }
+ return nil
}
-
- return nil
+ span.RecordError(err)
+ return err
}
onRetry := func() {
@@ -1871,13 +1931,12 @@ func (s *shards) sendV2SamplesWithBackoff(ctx context.Context, samples []writev2
if errors.Is(err, context.Canceled) {
// When there is resharding, we cancel the context for this queue, which means the data is not sent.
// So we exit early to not update the metrics.
- return err
+ return accumulatedStats, err
}
s.qm.metrics.sentBytesTotal.Add(float64(reqSize))
s.qm.metrics.highestSentTimestamp.Set(float64(highest / 1000))
-
- return err
+ return accumulatedStats, err
}
func populateV2TimeSeries(symbolTable *writev2.SymbolsTable, batch []timeSeries, pendingData []writev2.TimeSeries, sendExemplars, sendNativeHistograms bool) (int, int, int, int) {
diff --git a/storage/remote/queue_manager_test.go b/storage/remote/queue_manager_test.go
index 5227c2d6a..7343184fc 100644
--- a/storage/remote/queue_manager_test.go
+++ b/storage/remote/queue_manager_test.go
@@ -118,10 +118,10 @@ func TestBasicContentNegotiation(t *testing.T) {
expectFail: true,
},
{
- name: "v2 talks to v1 that tries to unmarshal v2 payload with v1 proto",
+ name: "v2 talks to (broken) v1 that tries to unmarshal v2 payload with v1 proto",
senderProtoMsg: config.RemoteWriteProtoMsgV2, receiverProtoMsg: config.RemoteWriteProtoMsgV1,
injectErrs: []error{nil},
- expectFail: true, // invalid request, no timeseries
+ expectFail: true, // We detect this thanks to https://github.com/prometheus/prometheus/issues/14359
},
// Opposite, v1 talking to v2 only server.
{
@@ -130,12 +130,6 @@ func TestBasicContentNegotiation(t *testing.T) {
injectErrs: []error{errors.New("pretend unrecoverable err")},
expectFail: true,
},
- {
- name: "v1 talks to (broken) v2 that tries to unmarshal v1 payload with v2 proto",
- senderProtoMsg: config.RemoteWriteProtoMsgV1, receiverProtoMsg: config.RemoteWriteProtoMsgV2,
- injectErrs: []error{nil},
- expectFail: true, // invalid request, no timeseries
- },
} {
t.Run(tc.name, func(t *testing.T) {
dir := t.TempDir()
@@ -182,7 +176,6 @@ func TestBasicContentNegotiation(t *testing.T) {
if !tc.expectFail {
// No error expected, so wait for data.
c.waitForExpectedData(t, 5*time.Second)
- require.Equal(t, 1, c.writesReceived)
require.Equal(t, 0.0, client_testutil.ToFloat64(qm.metrics.failedSamplesTotal))
} else {
// Wait for failure to be recorded in metrics.
@@ -190,11 +183,10 @@ func TestBasicContentNegotiation(t *testing.T) {
defer cancel()
require.NoError(t, runutil.Retry(500*time.Millisecond, ctx.Done(), func() error {
if client_testutil.ToFloat64(qm.metrics.failedSamplesTotal) != 1.0 {
- return errors.New("expected one sample failed in qm metrics")
+ return fmt.Errorf("expected one sample failed in qm metrics; got %v", client_testutil.ToFloat64(qm.metrics.failedSamplesTotal))
}
return nil
}))
- require.Equal(t, 0, c.writesReceived)
}
// samplesTotal means attempts.
@@ -764,10 +756,10 @@ func TestDisableReshardOnRetry(t *testing.T) {
metrics = newQueueManagerMetrics(nil, "", "")
client = &MockWriteClient{
- StoreFunc: func(ctx context.Context, b []byte, i int) error {
+ StoreFunc: func(ctx context.Context, b []byte, i int) (WriteResponseStats, error) {
onStoreCalled()
- return RecoverableError{
+ return WriteResponseStats{}, RecoverableError{
error: fmt.Errorf("fake error"),
retryAfter: model.Duration(retryAfter),
}
@@ -1113,14 +1105,14 @@ func (c *TestWriteClient) SetReturnError(err error) {
c.returnError = err
}
-func (c *TestWriteClient) Store(_ context.Context, req []byte, _ int) error {
+func (c *TestWriteClient) Store(_ context.Context, req []byte, _ int) (WriteResponseStats, error) {
c.mtx.Lock()
defer c.mtx.Unlock()
if c.storeWait > 0 {
time.Sleep(c.storeWait)
}
if c.returnError != nil {
- return c.returnError
+ return WriteResponseStats{}, c.returnError
}
// nil buffers are ok for snappy, ignore cast error.
if c.buf != nil {
@@ -1130,14 +1122,14 @@ func (c *TestWriteClient) Store(_ context.Context, req []byte, _ int) error {
reqBuf, err := snappy.Decode(c.buf, req)
c.buf = reqBuf
if err != nil {
- return err
+ return WriteResponseStats{}, err
}
// Check if we've been told to inject err for this call.
if len(c.injectedErrs) > 0 {
c.currErr++
if err = c.injectedErrs[c.currErr]; err != nil {
- return err
+ return WriteResponseStats{}, err
}
}
@@ -1156,13 +1148,10 @@ func (c *TestWriteClient) Store(_ context.Context, req []byte, _ int) error {
}
}
if err != nil {
- return err
- }
-
- if len(reqProto.Timeseries) == 0 && len(reqProto.Metadata) == 0 {
- return errors.New("invalid request, no timeseries")
+ return WriteResponseStats{}, err
}
+ rs := WriteResponseStats{}
b := labels.NewScratchBuilder(0)
for _, ts := range reqProto.Timeseries {
labels := ts.ToLabels(&b, nil)
@@ -1170,10 +1159,12 @@ func (c *TestWriteClient) Store(_ context.Context, req []byte, _ int) error {
if len(ts.Samples) > 0 {
c.receivedSamples[tsID] = append(c.receivedSamples[tsID], ts.Samples...)
}
+ rs.Samples += len(ts.Samples)
if len(ts.Exemplars) > 0 {
c.receivedExemplars[tsID] = append(c.receivedExemplars[tsID], ts.Exemplars...)
}
+ rs.Exemplars += len(ts.Exemplars)
for _, h := range ts.Histograms {
if h.IsFloatHistogram() {
@@ -1182,13 +1173,14 @@ func (c *TestWriteClient) Store(_ context.Context, req []byte, _ int) error {
c.receivedHistograms[tsID] = append(c.receivedHistograms[tsID], h)
}
}
+ rs.Histograms += len(ts.Histograms)
}
for _, m := range reqProto.Metadata {
c.receivedMetadata[m.MetricFamilyName] = append(c.receivedMetadata[m.MetricFamilyName], m)
}
c.writesReceived++
- return nil
+ return rs, nil
}
func (c *TestWriteClient) Name() string {
@@ -1256,10 +1248,10 @@ func NewTestBlockedWriteClient() *TestBlockingWriteClient {
return &TestBlockingWriteClient{}
}
-func (c *TestBlockingWriteClient) Store(ctx context.Context, _ []byte, _ int) error {
+func (c *TestBlockingWriteClient) Store(ctx context.Context, _ []byte, _ int) (WriteResponseStats, error) {
c.numCalls.Inc()
<-ctx.Done()
- return nil
+ return WriteResponseStats{}, nil
}
func (c *TestBlockingWriteClient) NumCalls() uint64 {
@@ -1278,19 +1270,19 @@ func (c *TestBlockingWriteClient) Endpoint() string {
type NopWriteClient struct{}
func NewNopWriteClient() *NopWriteClient { return &NopWriteClient{} }
-func (c *NopWriteClient) Store(context.Context, []byte, int) error {
- return nil
+func (c *NopWriteClient) Store(context.Context, []byte, int) (WriteResponseStats, error) {
+ return WriteResponseStats{}, nil
}
func (c *NopWriteClient) Name() string { return "nopwriteclient" }
func (c *NopWriteClient) Endpoint() string { return "http://test-remote.com/1234" }
type MockWriteClient struct {
- StoreFunc func(context.Context, []byte, int) error
+ StoreFunc func(context.Context, []byte, int) (WriteResponseStats, error)
NameFunc func() string
EndpointFunc func() string
}
-func (c *MockWriteClient) Store(ctx context.Context, bb []byte, n int) error {
+func (c *MockWriteClient) Store(ctx context.Context, bb []byte, n int) (WriteResponseStats, error) {
return c.StoreFunc(ctx, bb, n)
}
func (c *MockWriteClient) Name() string { return c.NameFunc() }
diff --git a/storage/remote/stats.go b/storage/remote/stats.go
new file mode 100644
index 000000000..89d00ffc3
--- /dev/null
+++ b/storage/remote/stats.go
@@ -0,0 +1,107 @@
+// Copyright 2024 The Prometheus Authors
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package remote
+
+import (
+ "errors"
+ "net/http"
+ "strconv"
+)
+
+const (
+ rw20WrittenSamplesHeader = "X-Prometheus-Remote-Write-Samples-Written"
+ rw20WrittenHistogramsHeader = "X-Prometheus-Remote-Write-Histograms-Written"
+ rw20WrittenExemplarsHeader = "X-Prometheus-Remote-Write-Exemplars-Written"
+)
+
+// WriteResponseStats represents the response write statistics specified in https://github.com/prometheus/docs/pull/2486
+type WriteResponseStats struct {
+ // Samples represents X-Prometheus-Remote-Write-Written-Samples
+ Samples int
+ // Histograms represents X-Prometheus-Remote-Write-Written-Histograms
+ Histograms int
+ // Exemplars represents X-Prometheus-Remote-Write-Written-Exemplars
+ Exemplars int
+
+ // Confirmed means we can trust those statistics from the point of view
+ // of the PRW 2.0 spec. When parsed from headers, it means we got at least one
+ // response header from the Receiver to confirm those numbers, meaning it must
+ // be a at least 2.0 Receiver. See ParseWriteResponseStats for details.
+ Confirmed bool
+}
+
+// NoDataWritten returns true if statistics indicate no data was written.
+func (s WriteResponseStats) NoDataWritten() bool {
+ return (s.Samples + s.Histograms + s.Exemplars) == 0
+}
+
+// AllSamples returns both float and histogram sample numbers.
+func (s WriteResponseStats) AllSamples() int {
+ return s.Samples + s.Histograms
+}
+
+// Add returns the sum of this WriteResponseStats plus the given WriteResponseStats.
+func (s WriteResponseStats) Add(rs WriteResponseStats) WriteResponseStats {
+ s.Confirmed = rs.Confirmed
+ s.Samples += rs.Samples
+ s.Histograms += rs.Histograms
+ s.Exemplars += rs.Exemplars
+ return s
+}
+
+// SetHeaders sets response headers in a given response writer.
+// Make sure to use it before http.ResponseWriter.WriteHeader and .Write.
+func (s WriteResponseStats) SetHeaders(w http.ResponseWriter) {
+ h := w.Header()
+ h.Set(rw20WrittenSamplesHeader, strconv.Itoa(s.Samples))
+ h.Set(rw20WrittenHistogramsHeader, strconv.Itoa(s.Histograms))
+ h.Set(rw20WrittenExemplarsHeader, strconv.Itoa(s.Exemplars))
+}
+
+// ParseWriteResponseStats returns WriteResponseStats parsed from the response headers.
+//
+// As per 2.0 spec, missing header means 0. However, abrupt HTTP errors, 1.0 Receivers
+// or buggy 2.0 Receivers might result in no response headers specified and that
+// might NOT necessarily mean nothing was written. To represent that we set
+// s.Confirmed = true only when see at least on response header.
+//
+// Error is returned when any of the header fails to parse as int64.
+func ParseWriteResponseStats(r *http.Response) (s WriteResponseStats, err error) {
+ var (
+ errs []error
+ h = r.Header
+ )
+ if v := h.Get(rw20WrittenSamplesHeader); v != "" { // Empty means zero.
+ s.Confirmed = true
+ if s.Samples, err = strconv.Atoi(v); err != nil {
+ s.Samples = 0
+ errs = append(errs, err)
+ }
+ }
+ if v := h.Get(rw20WrittenHistogramsHeader); v != "" { // Empty means zero.
+ s.Confirmed = true
+ if s.Histograms, err = strconv.Atoi(v); err != nil {
+ s.Histograms = 0
+ errs = append(errs, err)
+ }
+ }
+ if v := h.Get(rw20WrittenExemplarsHeader); v != "" { // Empty means zero.
+ s.Confirmed = true
+ if s.Exemplars, err = strconv.Atoi(v); err != nil {
+ s.Exemplars = 0
+ errs = append(errs, err)
+ }
+ }
+ return s, errors.Join(errs...)
+}
diff --git a/storage/remote/write_handler.go b/storage/remote/write_handler.go
index d82237371..6756bf0ab 100644
--- a/storage/remote/write_handler.go
+++ b/storage/remote/write_handler.go
@@ -19,7 +19,6 @@ import (
"fmt"
"io"
"net/http"
- "strconv"
"strings"
"time"
@@ -201,7 +200,7 @@ func (h *writeHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
respStats, errHTTPCode, err := h.writeV2(r.Context(), &req)
// Set required X-Prometheus-Remote-Write-Written-* response headers, in all cases.
- respStats.SetResponseHeaders(w.Header())
+ respStats.SetHeaders(w)
if err != nil {
if errHTTPCode/5 == 100 { // 5xx
@@ -318,24 +317,6 @@ func (h *writeHandler) appendV1Histograms(app storage.Appender, hh []prompb.Hist
return nil
}
-const (
- prw20WrittenSamplesHeader = "X-Prometheus-Remote-Write-Written-Samples"
- rw20WrittenHistogramsHeader = "X-Prometheus-Remote-Write-Written-Histograms"
- rw20WrittenExemplarsHeader = "X-Prometheus-Remote-Write-Written-Exemplars"
-)
-
-type responseStats struct {
- samples int
- histograms int
- exemplars int
-}
-
-func (s responseStats) SetResponseHeaders(h http.Header) {
- h.Set(prw20WrittenSamplesHeader, strconv.Itoa(s.samples))
- h.Set(rw20WrittenHistogramsHeader, strconv.Itoa(s.histograms))
- h.Set(rw20WrittenExemplarsHeader, strconv.Itoa(s.exemplars))
-}
-
// writeV2 is similar to write, but it works with v2 proto message,
// allows partial 4xx writes and gathers statistics.
//
@@ -345,14 +326,14 @@ func (s responseStats) SetResponseHeaders(h http.Header) {
//
// NOTE(bwplotka): TSDB storage is NOT idempotent, so we don't allow "partial retry-able" errors.
// Once we have 5xx type of error, we immediately stop and rollback all appends.
-func (h *writeHandler) writeV2(ctx context.Context, req *writev2.Request) (_ responseStats, errHTTPCode int, _ error) {
+func (h *writeHandler) writeV2(ctx context.Context, req *writev2.Request) (_ WriteResponseStats, errHTTPCode int, _ error) {
app := &timeLimitAppender{
Appender: h.appendable.Appender(ctx),
maxTime: timestamp.FromTime(time.Now().Add(maxAheadTime)),
}
- rs := responseStats{}
- samplesWithoutMetadata, errHTTPCode, err := h.appendV2(app, req, &rs)
+ s := WriteResponseStats{}
+ samplesWithoutMetadata, errHTTPCode, err := h.appendV2(app, req, &s)
if err != nil {
if errHTTPCode/5 == 100 {
// On 5xx, we always rollback, because we expect
@@ -360,29 +341,29 @@ func (h *writeHandler) writeV2(ctx context.Context, req *writev2.Request) (_ res
if rerr := app.Rollback(); rerr != nil {
level.Error(h.logger).Log("msg", "writev2 rollback failed on retry-able error", "err", rerr)
}
- return responseStats{}, errHTTPCode, err
+ return WriteResponseStats{}, errHTTPCode, err
}
// Non-retriable (e.g. bad request error case). Can be partially written.
commitErr := app.Commit()
if commitErr != nil {
// Bad requests does not matter as we have internal error (retryable).
- return responseStats{}, http.StatusInternalServerError, commitErr
+ return WriteResponseStats{}, http.StatusInternalServerError, commitErr
}
// Bad request error happened, but rest of data (if any) was written.
h.samplesAppendedWithoutMetadata.Add(float64(samplesWithoutMetadata))
- return rs, errHTTPCode, err
+ return s, errHTTPCode, err
}
// All good just commit.
if err := app.Commit(); err != nil {
- return responseStats{}, http.StatusInternalServerError, err
+ return WriteResponseStats{}, http.StatusInternalServerError, err
}
h.samplesAppendedWithoutMetadata.Add(float64(samplesWithoutMetadata))
- return rs, 0, nil
+ return s, 0, nil
}
-func (h *writeHandler) appendV2(app storage.Appender, req *writev2.Request, rs *responseStats) (samplesWithoutMetadata, errHTTPCode int, err error) {
+func (h *writeHandler) appendV2(app storage.Appender, req *writev2.Request, rs *WriteResponseStats) (samplesWithoutMetadata, errHTTPCode int, err error) {
var (
badRequestErrs []error
outOfOrderExemplarErrs, samplesWithInvalidLabels int
@@ -400,14 +381,14 @@ func (h *writeHandler) appendV2(app storage.Appender, req *writev2.Request, rs *
continue
}
- allSamplesSoFar := rs.samples + rs.histograms
+ allSamplesSoFar := rs.AllSamples()
var ref storage.SeriesRef
// Samples.
for _, s := range ts.Samples {
ref, err = app.Append(ref, ls, s.GetTimestamp(), s.GetValue())
if err == nil {
- rs.samples++
+ rs.Samples++
continue
}
// Handle append error.
@@ -431,7 +412,7 @@ func (h *writeHandler) appendV2(app storage.Appender, req *writev2.Request, rs *
ref, err = app.AppendHistogram(ref, ls, hp.Timestamp, hp.ToIntHistogram(), nil)
}
if err == nil {
- rs.histograms++
+ rs.Histograms++
continue
}
// Handle append error.
@@ -453,18 +434,19 @@ func (h *writeHandler) appendV2(app storage.Appender, req *writev2.Request, rs *
e := ep.ToExemplar(&b, req.Symbols)
ref, err = app.AppendExemplar(ref, ls, e)
if err == nil {
- rs.exemplars++
+ rs.Exemplars++
continue
}
// Handle append error.
- // TODO(bwplotka): I left the logic as in v1, but we might want to make it consistent with samples and histograms.
- // Since exemplar storage is still experimental, we don't fail in anyway, the request on ingestion errors.
if errors.Is(err, storage.ErrOutOfOrderExemplar) {
- outOfOrderExemplarErrs++
- level.Debug(h.logger).Log("msg", "Out of order exemplar", "err", err.Error(), "series", ls.String(), "exemplar", fmt.Sprintf("%+v", e))
+ outOfOrderExemplarErrs++ // Maintain old metrics, but technically not needed, given we fail here.
+ level.Error(h.logger).Log("msg", "Out of order exemplar", "err", err.Error(), "series", ls.String(), "exemplar", fmt.Sprintf("%+v", e))
+ badRequestErrs = append(badRequestErrs, fmt.Errorf("%w for series %v", err, ls.String()))
continue
}
- level.Debug(h.logger).Log("msg", "Error while adding exemplar in AppendExemplar", "series", ls.String(), "exemplar", fmt.Sprintf("%+v", e), "err", err)
+ // TODO(bwplotka): Add strict mode which would trigger rollback of everything if needed.
+ // For now we keep the previously released flow (just error not debug leve) of dropping them without rollback and 5xx.
+ level.Error(h.logger).Log("msg", "failed to ingest exemplar, emitting error log, but no error for PRW caller", "err", err.Error(), "series", ls.String(), "exemplar", fmt.Sprintf("%+v", e))
}
m := ts.ToMetadata(req.Symbols)
@@ -472,7 +454,7 @@ func (h *writeHandler) appendV2(app storage.Appender, req *writev2.Request, rs *
level.Debug(h.logger).Log("msg", "error while updating metadata from remote write", "err", err)
// Metadata is attached to each series, so since Prometheus does not reject sample without metadata information,
// we don't report remote write error either. We increment metric instead.
- samplesWithoutMetadata += (rs.samples + rs.histograms) - allSamplesSoFar
+ samplesWithoutMetadata += rs.AllSamples() - allSamplesSoFar
}
}
diff --git a/storage/remote/write_handler_test.go b/storage/remote/write_handler_test.go
index 9b5fb1a6e..af2229b9a 100644
--- a/storage/remote/write_handler_test.go
+++ b/storage/remote/write_handler_test.go
@@ -398,7 +398,7 @@ func TestRemoteWriteHandler_V2Message(t *testing.T) {
{
desc: "Partial write; skipped exemplar; exemplar storage errs are noop",
input: writeV2RequestFixture.Timeseries,
- appendExemplarErr: errors.New("some exemplar append error"),
+ appendExemplarErr: errors.New("some exemplar internal append error"),
expectedCode: http.StatusNoContent,
},
@@ -449,9 +449,9 @@ func TestRemoteWriteHandler_V2Message(t *testing.T) {
if tc.expectedCode == http.StatusInternalServerError {
// We don't expect writes for partial writes with retry-able code.
- expectHeaderValue(t, 0, resp.Header.Get("X-Prometheus-Remote-Write-Written-Samples"))
- expectHeaderValue(t, 0, resp.Header.Get("X-Prometheus-Remote-Write-Written-Histograms"))
- expectHeaderValue(t, 0, resp.Header.Get("X-Prometheus-Remote-Write-Written-Exemplars"))
+ expectHeaderValue(t, 0, resp.Header.Get(rw20WrittenSamplesHeader))
+ expectHeaderValue(t, 0, resp.Header.Get(rw20WrittenHistogramsHeader))
+ expectHeaderValue(t, 0, resp.Header.Get(rw20WrittenExemplarsHeader))
require.Empty(t, len(appendable.samples))
require.Empty(t, len(appendable.histograms))
@@ -462,12 +462,12 @@ func TestRemoteWriteHandler_V2Message(t *testing.T) {
// Double check mandatory 2.0 stats.
// writeV2RequestFixture has 2 series with 1 sample, 2 histograms, 1 exemplar each.
- expectHeaderValue(t, 2, resp.Header.Get("X-Prometheus-Remote-Write-Written-Samples"))
- expectHeaderValue(t, 4, resp.Header.Get("X-Prometheus-Remote-Write-Written-Histograms"))
+ expectHeaderValue(t, 2, resp.Header.Get(rw20WrittenSamplesHeader))
+ expectHeaderValue(t, 4, resp.Header.Get(rw20WrittenHistogramsHeader))
if tc.appendExemplarErr != nil {
- expectHeaderValue(t, 0, resp.Header.Get("X-Prometheus-Remote-Write-Written-Exemplars"))
+ expectHeaderValue(t, 0, resp.Header.Get(rw20WrittenExemplarsHeader))
} else {
- expectHeaderValue(t, 2, resp.Header.Get("X-Prometheus-Remote-Write-Written-Exemplars"))
+ expectHeaderValue(t, 2, resp.Header.Get(rw20WrittenExemplarsHeader))
}
// Double check what was actually appended.
From 465891cc5686f207a27e7cd1f9718062a5a83aba Mon Sep 17 00:00:00 2001
From: gotjosh
Date: Mon, 22 Jul 2024 14:11:18 +0100
Subject: [PATCH 16/44] Rules: Refactor concurrency controller interface
(#14491)
* Rules: Refactor concurrency controller interface
Even though the main purpose of this refactor is to modify the interface of the concurrency controller to accept a Context. I did two drive-by modifications that I think are sensible:
1. I have moved the check for dependencies on rules to the controller itself - this aligns with how the controller should behave as it is a deciding factor on wether we should run concurrently or not.
2. I cleaned up some unused methods from the days of the old interface before #13527 changed it.
Signed-off-by: gotjosh
---------
Signed-off-by: gotjosh
---
rules/group.go | 10 ++-------
rules/manager.go | 56 ++++++++++++++++--------------------------------
2 files changed, 20 insertions(+), 46 deletions(-)
diff --git a/rules/group.go b/rules/group.go
index 0bc219a11..201d3a67d 100644
--- a/rules/group.go
+++ b/rules/group.go
@@ -621,14 +621,12 @@ func (g *Group) Eval(ctx context.Context, ts time.Time) {
}
}
- // If the rule has no dependencies, it can run concurrently because no other rules in this group depend on its output.
- // Try run concurrently if there are slots available.
- if ctrl := g.concurrencyController; isRuleEligibleForConcurrentExecution(rule) && ctrl.Allow() {
+ if ctrl := g.concurrencyController; ctrl.Allow(ctx, g, rule) {
wg.Add(1)
go eval(i, rule, func() {
wg.Done()
- ctrl.Done()
+ ctrl.Done(ctx)
})
} else {
eval(i, rule, nil)
@@ -1094,7 +1092,3 @@ func buildDependencyMap(rules []Rule) dependencyMap {
return dependencies
}
-
-func isRuleEligibleForConcurrentExecution(rule Rule) bool {
- return rule.NoDependentRules() && rule.NoDependencyRules()
-}
diff --git a/rules/manager.go b/rules/manager.go
index ab33c3c7d..9e5b33fbc 100644
--- a/rules/manager.go
+++ b/rules/manager.go
@@ -457,67 +457,47 @@ func (c ruleDependencyController) AnalyseRules(rules []Rule) {
// Its purpose is to bound the amount of concurrency in rule evaluations to avoid overwhelming the Prometheus
// server with additional query load. Concurrency is controlled globally, not on a per-group basis.
type RuleConcurrencyController interface {
- // Allow determines whether any concurrent evaluation slots are available.
- // If Allow() returns true, then Done() must be called to release the acquired slot.
- Allow() bool
+ // Allow determines if the given rule is allowed to be evaluated concurrently.
+ // If Allow() returns true, then Done() must be called to release the acquired slot and corresponding cleanup is done.
+ // It is important that both *Group and Rule are not retained and only be used for the duration of the call.
+ Allow(ctx context.Context, group *Group, rule Rule) bool
// Done releases a concurrent evaluation slot.
- Done()
+ Done(ctx context.Context)
}
// concurrentRuleEvalController holds a weighted semaphore which controls the concurrent evaluation of rules.
type concurrentRuleEvalController struct {
- sema *semaphore.Weighted
- depMapsMu sync.Mutex
- depMaps map[*Group]dependencyMap
+ sema *semaphore.Weighted
}
func newRuleConcurrencyController(maxConcurrency int64) RuleConcurrencyController {
return &concurrentRuleEvalController{
- sema: semaphore.NewWeighted(maxConcurrency),
- depMaps: map[*Group]dependencyMap{},
+ sema: semaphore.NewWeighted(maxConcurrency),
}
}
-func (c *concurrentRuleEvalController) RuleEligible(g *Group, r Rule) bool {
- c.depMapsMu.Lock()
- defer c.depMapsMu.Unlock()
-
- depMap, found := c.depMaps[g]
- if !found {
- depMap = buildDependencyMap(g.rules)
- c.depMaps[g] = depMap
+func (c *concurrentRuleEvalController) Allow(_ context.Context, _ *Group, rule Rule) bool {
+ // To allow a rule to be executed concurrently, we need 3 conditions:
+ // 1. The rule must not have any rules that depend on it.
+ // 2. The rule itself must not depend on any other rules.
+ // 3. If 1 & 2 are true, then and only then we should try to acquire the concurrency slot.
+ if rule.NoDependentRules() && rule.NoDependencyRules() {
+ return c.sema.TryAcquire(1)
}
- return depMap.isIndependent(r)
+ return false
}
-func (c *concurrentRuleEvalController) Allow() bool {
- return c.sema.TryAcquire(1)
-}
-
-func (c *concurrentRuleEvalController) Done() {
+func (c *concurrentRuleEvalController) Done(_ context.Context) {
c.sema.Release(1)
}
-func (c *concurrentRuleEvalController) Invalidate() {
- c.depMapsMu.Lock()
- defer c.depMapsMu.Unlock()
-
- // Clear out the memoized dependency maps because some or all groups may have been updated.
- c.depMaps = map[*Group]dependencyMap{}
-}
-
// sequentialRuleEvalController is a RuleConcurrencyController that runs every rule sequentially.
type sequentialRuleEvalController struct{}
-func (c sequentialRuleEvalController) RuleEligible(_ *Group, _ Rule) bool {
+func (c sequentialRuleEvalController) Allow(_ context.Context, _ *Group, _ Rule) bool {
return false
}
-func (c sequentialRuleEvalController) Allow() bool {
- return false
-}
-
-func (c sequentialRuleEvalController) Done() {}
-func (c sequentialRuleEvalController) Invalidate() {}
+func (c sequentialRuleEvalController) Done(_ context.Context) {}
From 23307b02c58adc56695844b9a851578fb6c26677 Mon Sep 17 00:00:00 2001
From: Bryan Boreham
Date: Mon, 22 Jul 2024 14:33:59 +0100
Subject: [PATCH 17/44] [TESTS] Storage: Improve MergeQuerier tests
`TestMergeQuerierWithSecondaries_ErrorHandling` now tests `NewMergeQuerier`
rather than creating the data structure directly. This means we now test
short-circuiting when only a single querier is required.
Merge `mockGenericQuerier` into `mockQuerier`.
Replace `unwrapMockGenericQuerier` with a visitor pattern.
No change in functionality intended.
Signed-off-by: Bryan Boreham
---
storage/merge_test.go | 284 +++++++++++++++++++++---------------------
1 file changed, 139 insertions(+), 145 deletions(-)
diff --git a/storage/merge_test.go b/storage/merge_test.go
index 7619af3c1..a3cba9bb9 100644
--- a/storage/merge_test.go
+++ b/storage/merge_test.go
@@ -912,9 +912,23 @@ func TestConcatenatingChunkIterator(t *testing.T) {
}
type mockQuerier struct {
- LabelQuerier
+ mtx sync.Mutex
- toReturn []Series
+ toReturn []Series // Response for Select.
+
+ closed bool
+ labelNamesCalls int
+ labelNamesRequested []labelNameRequest
+ sortedSeriesRequested []bool
+
+ resp []string // Response for LabelNames and LabelValues; turned into Select response if toReturn is not supplied.
+ warnings annotations.Annotations
+ err error
+}
+
+type labelNameRequest struct {
+ name string
+ matchers []*labels.Matcher
}
type seriesByLabel []Series
@@ -924,13 +938,47 @@ func (a seriesByLabel) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a seriesByLabel) Less(i, j int) bool { return labels.Compare(a[i].Labels(), a[j].Labels()) < 0 }
func (m *mockQuerier) Select(_ context.Context, sortSeries bool, _ *SelectHints, _ ...*labels.Matcher) SeriesSet {
- cpy := make([]Series, len(m.toReturn))
- copy(cpy, m.toReturn)
+ m.mtx.Lock()
+ defer m.mtx.Unlock()
+ m.sortedSeriesRequested = append(m.sortedSeriesRequested, sortSeries)
+
+ var ret []Series
+ if len(m.toReturn) > 0 {
+ ret = make([]Series, len(m.toReturn))
+ copy(ret, m.toReturn)
+ } else if len(m.resp) > 0 {
+ ret = make([]Series, 0, len(m.resp))
+ for _, l := range m.resp {
+ ret = append(ret, NewListSeries(labels.FromStrings("test", string(l)), nil))
+ }
+ }
if sortSeries {
- sort.Sort(seriesByLabel(cpy))
+ sort.Sort(seriesByLabel(ret))
}
- return NewMockSeriesSet(cpy...)
+ return &mockSeriesSet{idx: -1, series: ret, warnings: m.warnings, err: m.err}
+}
+
+func (m *mockQuerier) LabelValues(_ context.Context, name string, hints *LabelHints, matchers ...*labels.Matcher) ([]string, annotations.Annotations, error) {
+ m.mtx.Lock()
+ m.labelNamesRequested = append(m.labelNamesRequested, labelNameRequest{
+ name: name,
+ matchers: matchers,
+ })
+ m.mtx.Unlock()
+ return m.resp, m.warnings, m.err
+}
+
+func (m *mockQuerier) LabelNames(context.Context, *LabelHints, ...*labels.Matcher) ([]string, annotations.Annotations, error) {
+ m.mtx.Lock()
+ m.labelNamesCalls++
+ m.mtx.Unlock()
+ return m.resp, m.warnings, m.err
+}
+
+func (m *mockQuerier) Close() error {
+ m.closed = true
+ return nil
}
type mockChunkQuerier struct {
@@ -960,6 +1008,9 @@ func (m *mockChunkQuerier) Select(_ context.Context, sortSeries bool, _ *SelectH
type mockSeriesSet struct {
idx int
series []Series
+
+ warnings annotations.Annotations
+ err error
}
func NewMockSeriesSet(series ...Series) SeriesSet {
@@ -970,15 +1021,18 @@ func NewMockSeriesSet(series ...Series) SeriesSet {
}
func (m *mockSeriesSet) Next() bool {
+ if m.err != nil {
+ return false
+ }
m.idx++
return m.idx < len(m.series)
}
func (m *mockSeriesSet) At() Series { return m.series[m.idx] }
-func (m *mockSeriesSet) Err() error { return nil }
+func (m *mockSeriesSet) Err() error { return m.err }
-func (m *mockSeriesSet) Warnings() annotations.Annotations { return nil }
+func (m *mockSeriesSet) Warnings() annotations.Annotations { return m.warnings }
type mockChunkSeriesSet struct {
idx int
@@ -1336,105 +1390,44 @@ func BenchmarkMergeSeriesSet(b *testing.B) {
}
}
-type mockGenericQuerier struct {
- mtx sync.Mutex
-
- closed bool
- labelNamesCalls int
- labelNamesRequested []labelNameRequest
- sortedSeriesRequested []bool
-
- resp []string
- warnings annotations.Annotations
- err error
-}
-
-type labelNameRequest struct {
- name string
- matchers []*labels.Matcher
-}
-
-func (m *mockGenericQuerier) Select(_ context.Context, b bool, _ *SelectHints, _ ...*labels.Matcher) genericSeriesSet {
- m.mtx.Lock()
- m.sortedSeriesRequested = append(m.sortedSeriesRequested, b)
- m.mtx.Unlock()
- return &mockGenericSeriesSet{resp: m.resp, warnings: m.warnings, err: m.err}
-}
-
-func (m *mockGenericQuerier) LabelValues(_ context.Context, name string, hints *LabelHints, matchers ...*labels.Matcher) ([]string, annotations.Annotations, error) {
- m.mtx.Lock()
- m.labelNamesRequested = append(m.labelNamesRequested, labelNameRequest{
- name: name,
- matchers: matchers,
- })
- m.mtx.Unlock()
- return m.resp, m.warnings, m.err
-}
-
-func (m *mockGenericQuerier) LabelNames(context.Context, *LabelHints, ...*labels.Matcher) ([]string, annotations.Annotations, error) {
- m.mtx.Lock()
- m.labelNamesCalls++
- m.mtx.Unlock()
- return m.resp, m.warnings, m.err
-}
-
-func (m *mockGenericQuerier) Close() error {
- m.closed = true
- return nil
-}
-
-type mockGenericSeriesSet struct {
- resp []string
- warnings annotations.Annotations
- err error
-
- curr int
-}
-
-func (m *mockGenericSeriesSet) Next() bool {
- if m.err != nil {
- return false
+func visitMockQueriers(t *testing.T, qr Querier, f func(t *testing.T, q *mockQuerier)) int {
+ count := 0
+ switch x := qr.(type) {
+ case *mockQuerier:
+ count++
+ f(t, x)
+ case *querierAdapter:
+ count += visitMockQueriersInGenericQuerier(t, x.genericQuerier, f)
}
- if m.curr >= len(m.resp) {
- return false
+ return count
+}
+
+func visitMockQueriersInGenericQuerier(t *testing.T, g genericQuerier, f func(t *testing.T, q *mockQuerier)) int {
+ count := 0
+ switch x := g.(type) {
+ case *mergeGenericQuerier:
+ for _, q := range x.queriers {
+ count += visitMockQueriersInGenericQuerier(t, q, f)
+ }
+ case *genericQuerierAdapter:
+ // Visitor for chunkQuerier not implemented.
+ count += visitMockQueriers(t, x.q, f)
+ case *secondaryQuerier:
+ count += visitMockQueriersInGenericQuerier(t, x.genericQuerier, f)
}
- m.curr++
- return true
+ return count
}
-func (m *mockGenericSeriesSet) Err() error { return m.err }
-func (m *mockGenericSeriesSet) Warnings() annotations.Annotations { return m.warnings }
-
-func (m *mockGenericSeriesSet) At() Labels {
- return mockLabels(m.resp[m.curr-1])
-}
-
-type mockLabels string
-
-func (l mockLabels) Labels() labels.Labels {
- return labels.FromStrings("test", string(l))
-}
-
-func unwrapMockGenericQuerier(t *testing.T, qr genericQuerier) *mockGenericQuerier {
- m, ok := qr.(*mockGenericQuerier)
- if !ok {
- s, ok := qr.(*secondaryQuerier)
- require.True(t, ok, "expected secondaryQuerier got something else")
- m, ok = s.genericQuerier.(*mockGenericQuerier)
- require.True(t, ok, "expected mockGenericQuerier got something else")
- }
- return m
-}
-
-func TestMergeGenericQuerierWithSecondaries_ErrorHandling(t *testing.T) {
+func TestMergeQuerierWithSecondaries_ErrorHandling(t *testing.T) {
var (
errStorage = errors.New("storage error")
warnStorage = errors.New("storage warning")
ctx = context.Background()
)
for _, tcase := range []struct {
- name string
- queriers []genericQuerier
+ name string
+ primaries []Querier
+ secondaries []Querier
expectedSelectsSeries []labels.Labels
expectedLabels []string
@@ -1443,10 +1436,8 @@ func TestMergeGenericQuerierWithSecondaries_ErrorHandling(t *testing.T) {
expectedErrs [4]error
}{
{
- // NewMergeQuerier will not create a mergeGenericQuerier
- // with just one querier inside, but we can test it anyway.
- name: "one successful primary querier",
- queriers: []genericQuerier{&mockGenericQuerier{resp: []string{"a", "b"}, warnings: nil, err: nil}},
+ name: "one successful primary querier",
+ primaries: []Querier{&mockQuerier{resp: []string{"a", "b"}, warnings: nil, err: nil}},
expectedSelectsSeries: []labels.Labels{
labels.FromStrings("test", "a"),
labels.FromStrings("test", "b"),
@@ -1455,9 +1446,9 @@ func TestMergeGenericQuerierWithSecondaries_ErrorHandling(t *testing.T) {
},
{
name: "multiple successful primary queriers",
- queriers: []genericQuerier{
- &mockGenericQuerier{resp: []string{"a", "b"}, warnings: nil, err: nil},
- &mockGenericQuerier{resp: []string{"b", "c"}, warnings: nil, err: nil},
+ primaries: []Querier{
+ &mockQuerier{resp: []string{"a", "b"}, warnings: nil, err: nil},
+ &mockQuerier{resp: []string{"b", "c"}, warnings: nil, err: nil},
},
expectedSelectsSeries: []labels.Labels{
labels.FromStrings("test", "a"),
@@ -1468,15 +1459,17 @@ func TestMergeGenericQuerierWithSecondaries_ErrorHandling(t *testing.T) {
},
{
name: "one failed primary querier",
- queriers: []genericQuerier{&mockGenericQuerier{warnings: nil, err: errStorage}},
+ primaries: []Querier{&mockQuerier{warnings: nil, err: errStorage}},
expectedErrs: [4]error{errStorage, errStorage, errStorage, errStorage},
},
{
name: "one successful primary querier with successful secondaries",
- queriers: []genericQuerier{
- &mockGenericQuerier{resp: []string{"a", "b"}, warnings: nil, err: nil},
- &secondaryQuerier{genericQuerier: &mockGenericQuerier{resp: []string{"b"}, warnings: nil, err: nil}},
- &secondaryQuerier{genericQuerier: &mockGenericQuerier{resp: []string{"c"}, warnings: nil, err: nil}},
+ primaries: []Querier{
+ &mockQuerier{resp: []string{"a", "b"}, warnings: nil, err: nil},
+ },
+ secondaries: []Querier{
+ &mockQuerier{resp: []string{"b"}, warnings: nil, err: nil},
+ &mockQuerier{resp: []string{"c"}, warnings: nil, err: nil},
},
expectedSelectsSeries: []labels.Labels{
labels.FromStrings("test", "a"),
@@ -1487,10 +1480,12 @@ func TestMergeGenericQuerierWithSecondaries_ErrorHandling(t *testing.T) {
},
{
name: "one successful primary querier with empty response and successful secondaries",
- queriers: []genericQuerier{
- &mockGenericQuerier{resp: []string{}, warnings: nil, err: nil},
- &secondaryQuerier{genericQuerier: &mockGenericQuerier{resp: []string{"b"}, warnings: nil, err: nil}},
- &secondaryQuerier{genericQuerier: &mockGenericQuerier{resp: []string{"c"}, warnings: nil, err: nil}},
+ primaries: []Querier{
+ &mockQuerier{resp: []string{}, warnings: nil, err: nil},
+ },
+ secondaries: []Querier{
+ &mockQuerier{resp: []string{"b"}, warnings: nil, err: nil},
+ &mockQuerier{resp: []string{"c"}, warnings: nil, err: nil},
},
expectedSelectsSeries: []labels.Labels{
labels.FromStrings("test", "b"),
@@ -1500,19 +1495,23 @@ func TestMergeGenericQuerierWithSecondaries_ErrorHandling(t *testing.T) {
},
{
name: "one failed primary querier with successful secondaries",
- queriers: []genericQuerier{
- &mockGenericQuerier{warnings: nil, err: errStorage},
- &secondaryQuerier{genericQuerier: &mockGenericQuerier{resp: []string{"b"}, warnings: nil, err: nil}},
- &secondaryQuerier{genericQuerier: &mockGenericQuerier{resp: []string{"c"}, warnings: nil, err: nil}},
+ primaries: []Querier{
+ &mockQuerier{warnings: nil, err: errStorage},
+ },
+ secondaries: []Querier{
+ &mockQuerier{resp: []string{"b"}, warnings: nil, err: nil},
+ &mockQuerier{resp: []string{"c"}, warnings: nil, err: nil},
},
expectedErrs: [4]error{errStorage, errStorage, errStorage, errStorage},
},
{
name: "one successful primary querier with failed secondaries",
- queriers: []genericQuerier{
- &mockGenericQuerier{resp: []string{"a"}, warnings: nil, err: nil},
- &secondaryQuerier{genericQuerier: &mockGenericQuerier{resp: []string{"b"}, warnings: nil, err: errStorage}},
- &secondaryQuerier{genericQuerier: &mockGenericQuerier{resp: []string{"c"}, warnings: nil, err: errStorage}},
+ primaries: []Querier{
+ &mockQuerier{resp: []string{"a"}, warnings: nil, err: nil},
+ },
+ secondaries: []Querier{
+ &mockQuerier{resp: []string{"b"}, warnings: nil, err: errStorage},
+ &mockQuerier{resp: []string{"c"}, warnings: nil, err: errStorage},
},
expectedSelectsSeries: []labels.Labels{
labels.FromStrings("test", "a"),
@@ -1522,9 +1521,11 @@ func TestMergeGenericQuerierWithSecondaries_ErrorHandling(t *testing.T) {
},
{
name: "successful queriers with warnings",
- queriers: []genericQuerier{
- &mockGenericQuerier{resp: []string{"a"}, warnings: annotations.New().Add(warnStorage), err: nil},
- &secondaryQuerier{genericQuerier: &mockGenericQuerier{resp: []string{"b"}, warnings: annotations.New().Add(warnStorage), err: nil}},
+ primaries: []Querier{
+ &mockQuerier{resp: []string{"a"}, warnings: annotations.New().Add(warnStorage), err: nil},
+ },
+ secondaries: []Querier{
+ &mockQuerier{resp: []string{"b"}, warnings: annotations.New().Add(warnStorage), err: nil},
},
expectedSelectsSeries: []labels.Labels{
labels.FromStrings("test", "a"),
@@ -1535,10 +1536,7 @@ func TestMergeGenericQuerierWithSecondaries_ErrorHandling(t *testing.T) {
},
} {
t.Run(tcase.name, func(t *testing.T) {
- q := &mergeGenericQuerier{
- queriers: tcase.queriers,
- mergeFn: func(l ...Labels) Labels { return l[0] },
- }
+ q := NewMergeQuerier(tcase.primaries, tcase.secondaries, func(s ...Series) Series { return s[0] })
t.Run("Select", func(t *testing.T) {
res := q.Select(context.Background(), false, nil)
@@ -1551,11 +1549,13 @@ func TestMergeGenericQuerierWithSecondaries_ErrorHandling(t *testing.T) {
require.ErrorIs(t, res.Err(), tcase.expectedErrs[0], "expected error doesn't match")
require.Equal(t, tcase.expectedSelectsSeries, lbls)
- for _, qr := range q.queriers {
- m := unwrapMockGenericQuerier(t, qr)
- // mergeGenericQuerier forces all Selects to be sorted.
- require.Equal(t, []bool{true}, m.sortedSeriesRequested)
- }
+ n := visitMockQueriers(t, q, func(t *testing.T, m *mockQuerier) {
+ // Single queries should be unsorted; merged queries sorted.
+ exp := len(tcase.primaries)+len(tcase.secondaries) > 1
+ require.Equal(t, []bool{exp}, m.sortedSeriesRequested)
+ })
+ // Check we visited all queriers.
+ require.Equal(t, len(tcase.primaries)+len(tcase.secondaries), n)
})
t.Run("LabelNames", func(t *testing.T) {
res, w, err := q.LabelNames(ctx, nil)
@@ -1566,11 +1566,9 @@ func TestMergeGenericQuerierWithSecondaries_ErrorHandling(t *testing.T) {
if err != nil {
return
}
- for _, qr := range q.queriers {
- m := unwrapMockGenericQuerier(t, qr)
-
+ visitMockQueriers(t, q, func(t *testing.T, m *mockQuerier) {
require.Equal(t, 1, m.labelNamesCalls)
- }
+ })
})
t.Run("LabelValues", func(t *testing.T) {
res, w, err := q.LabelValues(ctx, "test", nil)
@@ -1581,11 +1579,9 @@ func TestMergeGenericQuerierWithSecondaries_ErrorHandling(t *testing.T) {
if err != nil {
return
}
- for _, qr := range q.queriers {
- m := unwrapMockGenericQuerier(t, qr)
-
+ visitMockQueriers(t, q, func(t *testing.T, m *mockQuerier) {
require.Equal(t, []labelNameRequest{{name: "test"}}, m.labelNamesRequested)
- }
+ })
})
t.Run("LabelValuesWithMatchers", func(t *testing.T) {
matcher := labels.MustNewMatcher(labels.MatchEqual, "otherLabel", "someValue")
@@ -1597,14 +1593,12 @@ func TestMergeGenericQuerierWithSecondaries_ErrorHandling(t *testing.T) {
if err != nil {
return
}
- for _, qr := range q.queriers {
- m := unwrapMockGenericQuerier(t, qr)
-
+ visitMockQueriers(t, q, func(t *testing.T, m *mockQuerier) {
require.Equal(t, []labelNameRequest{
{name: "test"},
{name: "test2", matchers: []*labels.Matcher{matcher}},
}, m.labelNamesRequested)
- }
+ })
})
})
}
From 677cdcdcecc3826390461b9574b882ebb9a42143 Mon Sep 17 00:00:00 2001
From: Bryan Boreham
Date: Mon, 22 Jul 2024 15:01:00 +0100
Subject: [PATCH 18/44] [TEST] Storage: ignore difference between nil and empty
We need this for subsequent changes.
Signed-off-by: Bryan Boreham
---
storage/merge_test.go | 15 ++++++++++++---
1 file changed, 12 insertions(+), 3 deletions(-)
diff --git a/storage/merge_test.go b/storage/merge_test.go
index a3cba9bb9..dae338a00 100644
--- a/storage/merge_test.go
+++ b/storage/merge_test.go
@@ -1561,7 +1561,7 @@ func TestMergeQuerierWithSecondaries_ErrorHandling(t *testing.T) {
res, w, err := q.LabelNames(ctx, nil)
require.Subset(t, tcase.expectedWarnings, w)
require.ErrorIs(t, err, tcase.expectedErrs[1], "expected error doesn't match")
- require.Equal(t, tcase.expectedLabels, res)
+ requireEqualSlice(t, tcase.expectedLabels, res)
if err != nil {
return
@@ -1574,7 +1574,7 @@ func TestMergeQuerierWithSecondaries_ErrorHandling(t *testing.T) {
res, w, err := q.LabelValues(ctx, "test", nil)
require.Subset(t, tcase.expectedWarnings, w)
require.ErrorIs(t, err, tcase.expectedErrs[2], "expected error doesn't match")
- require.Equal(t, tcase.expectedLabels, res)
+ requireEqualSlice(t, tcase.expectedLabels, res)
if err != nil {
return
@@ -1588,7 +1588,7 @@ func TestMergeQuerierWithSecondaries_ErrorHandling(t *testing.T) {
res, w, err := q.LabelValues(ctx, "test2", nil, matcher)
require.Subset(t, tcase.expectedWarnings, w)
require.ErrorIs(t, err, tcase.expectedErrs[3], "expected error doesn't match")
- require.Equal(t, tcase.expectedLabels, res)
+ requireEqualSlice(t, tcase.expectedLabels, res)
if err != nil {
return
@@ -1604,6 +1604,15 @@ func TestMergeQuerierWithSecondaries_ErrorHandling(t *testing.T) {
}
}
+// Check slice but ignore difference between nil and empty.
+func requireEqualSlice[T any](t require.TestingT, a, b []T, msgAndArgs ...interface{}) {
+ if len(a) == 0 {
+ require.Empty(t, b, msgAndArgs...)
+ } else {
+ require.Equal(t, a, b, msgAndArgs...)
+ }
+}
+
type errIterator struct {
err error
}
From 0ae881739bab56f91ad95e7b3d0dc7e470b9ce2e Mon Sep 17 00:00:00 2001
From: Bryan Boreham
Date: Mon, 22 Jul 2024 15:02:52 +0100
Subject: [PATCH 19/44] [TEST] Storage: check MergeQuerier with nil primary
This test fails on current code.
Signed-off-by: Bryan Boreham
---
storage/merge_test.go | 19 +++++++++++++++++++
1 file changed, 19 insertions(+)
diff --git a/storage/merge_test.go b/storage/merge_test.go
index dae338a00..488edb2e6 100644
--- a/storage/merge_test.go
+++ b/storage/merge_test.go
@@ -1504,6 +1504,25 @@ func TestMergeQuerierWithSecondaries_ErrorHandling(t *testing.T) {
},
expectedErrs: [4]error{errStorage, errStorage, errStorage, errStorage},
},
+ {
+ name: "nil primary querier with failed secondary",
+ primaries: nil,
+ secondaries: []Querier{
+ &mockQuerier{resp: []string{"b"}, warnings: nil, err: errStorage},
+ },
+ expectedLabels: []string{},
+ expectedWarnings: annotations.New().Add(errStorage),
+ },
+ {
+ name: "nil primary querier with two failed secondaries",
+ primaries: nil,
+ secondaries: []Querier{
+ &mockQuerier{resp: []string{"b"}, warnings: nil, err: errStorage},
+ &mockQuerier{resp: []string{"c"}, warnings: nil, err: errStorage},
+ },
+ expectedLabels: []string{},
+ expectedWarnings: annotations.New().Add(errStorage),
+ },
{
name: "one successful primary querier with failed secondaries",
primaries: []Querier{
From 90d793e8c5e8f8c9823cfa9942e4e08019b37a6d Mon Sep 17 00:00:00 2001
From: Bryan Boreham
Date: Mon, 22 Jul 2024 15:33:07 +0100
Subject: [PATCH 20/44] [BUGFIX] Storage: Single secondary querier errors
should be warnings.
Signed-off-by: Bryan Boreham
---
storage/merge.go | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/storage/merge.go b/storage/merge.go
index 194494b6a..66c4c3ed3 100644
--- a/storage/merge.go
+++ b/storage/merge.go
@@ -51,7 +51,7 @@ func NewMergeQuerier(primaries, secondaries []Querier, mergeFn VerticalSeriesMer
case len(primaries) == 1 && len(secondaries) == 0:
return primaries[0]
case len(primaries) == 0 && len(secondaries) == 1:
- return secondaries[0]
+ return &querierAdapter{newSecondaryQuerierFrom(secondaries[0])}
}
queriers := make([]genericQuerier, 0, len(primaries)+len(secondaries))
@@ -89,7 +89,7 @@ func NewMergeChunkQuerier(primaries, secondaries []ChunkQuerier, mergeFn Vertica
case len(primaries) == 1 && len(secondaries) == 0:
return primaries[0]
case len(primaries) == 0 && len(secondaries) == 1:
- return secondaries[0]
+ return &chunkQuerierAdapter{newSecondaryQuerierFromChunk(secondaries[0])}
}
queriers := make([]genericQuerier, 0, len(primaries)+len(secondaries))
From 12bd92a25ccc6516f7cda9f53b5798fb5992a6c3 Mon Sep 17 00:00:00 2001
From: Bryan Boreham
Date: Mon, 22 Jul 2024 15:35:09 +0100
Subject: [PATCH 21/44] [ENHANCEMENT] Storage: Short-circuit merge of single
querier with no-op queriers
Filter before checking whether there is only one.
Signed-off-by: Bryan Boreham
---
storage/merge.go | 46 ++++++++++++++++++++++++++++++++--------------
1 file changed, 32 insertions(+), 14 deletions(-)
diff --git a/storage/merge.go b/storage/merge.go
index 66c4c3ed3..2424b26ab 100644
--- a/storage/merge.go
+++ b/storage/merge.go
@@ -45,8 +45,11 @@ type mergeGenericQuerier struct {
//
// In case of overlaps between the data given by primaries' and secondaries' Selects, merge function will be used.
func NewMergeQuerier(primaries, secondaries []Querier, mergeFn VerticalSeriesMergeFunc) Querier {
+ primaries = filterQueriers(primaries)
+ secondaries = filterQueriers(secondaries)
+
switch {
- case len(primaries)+len(secondaries) == 0:
+ case len(primaries) == 0 && len(secondaries) == 0:
return noopQuerier{}
case len(primaries) == 1 && len(secondaries) == 0:
return primaries[0]
@@ -56,14 +59,10 @@ func NewMergeQuerier(primaries, secondaries []Querier, mergeFn VerticalSeriesMer
queriers := make([]genericQuerier, 0, len(primaries)+len(secondaries))
for _, q := range primaries {
- if _, ok := q.(noopQuerier); !ok && q != nil {
- queriers = append(queriers, newGenericQuerierFrom(q))
- }
+ queriers = append(queriers, newGenericQuerierFrom(q))
}
for _, q := range secondaries {
- if _, ok := q.(noopQuerier); !ok && q != nil {
- queriers = append(queriers, newSecondaryQuerierFrom(q))
- }
+ queriers = append(queriers, newSecondaryQuerierFrom(q))
}
concurrentSelect := false
@@ -77,12 +76,25 @@ func NewMergeQuerier(primaries, secondaries []Querier, mergeFn VerticalSeriesMer
}}
}
+func filterQueriers(qs []Querier) []Querier {
+ ret := make([]Querier, 0, len(qs))
+ for _, q := range qs {
+ if _, ok := q.(noopQuerier); !ok && q != nil {
+ ret = append(ret, q)
+ }
+ }
+ return ret
+}
+
// NewMergeChunkQuerier returns a new Chunk Querier that merges results of given primary and secondary chunk queriers.
// See NewFanout commentary to learn more about primary vs secondary differences.
//
// In case of overlaps between the data given by primaries' and secondaries' Selects, merge function will be used.
// TODO(bwplotka): Currently merge will compact overlapping chunks with bigger chunk, without limit. Split it: https://github.com/prometheus/tsdb/issues/670
func NewMergeChunkQuerier(primaries, secondaries []ChunkQuerier, mergeFn VerticalChunkSeriesMergeFunc) ChunkQuerier {
+ primaries = filterChunkQueriers(primaries)
+ secondaries = filterChunkQueriers(secondaries)
+
switch {
case len(primaries) == 0 && len(secondaries) == 0:
return noopChunkQuerier{}
@@ -94,14 +106,10 @@ func NewMergeChunkQuerier(primaries, secondaries []ChunkQuerier, mergeFn Vertica
queriers := make([]genericQuerier, 0, len(primaries)+len(secondaries))
for _, q := range primaries {
- if _, ok := q.(noopChunkQuerier); !ok && q != nil {
- queriers = append(queriers, newGenericQuerierFromChunk(q))
- }
+ queriers = append(queriers, newGenericQuerierFromChunk(q))
}
- for _, querier := range secondaries {
- if _, ok := querier.(noopChunkQuerier); !ok && querier != nil {
- queriers = append(queriers, newSecondaryQuerierFromChunk(querier))
- }
+ for _, q := range secondaries {
+ queriers = append(queriers, newSecondaryQuerierFromChunk(q))
}
concurrentSelect := false
@@ -115,6 +123,16 @@ func NewMergeChunkQuerier(primaries, secondaries []ChunkQuerier, mergeFn Vertica
}}
}
+func filterChunkQueriers(qs []ChunkQuerier) []ChunkQuerier {
+ ret := make([]ChunkQuerier, 0, len(qs))
+ for _, q := range qs {
+ if _, ok := q.(noopChunkQuerier); !ok && q != nil {
+ ret = append(ret, q)
+ }
+ }
+ return ret
+}
+
// Select returns a set of series that matches the given label matchers.
func (q *mergeGenericQuerier) Select(ctx context.Context, sortSeries bool, hints *SelectHints, matchers ...*labels.Matcher) genericSeriesSet {
seriesSets := make([]genericSeriesSet, 0, len(q.queriers))
From cc7dcf5afea759263ca0fa555f70741b76ce4df6 Mon Sep 17 00:00:00 2001
From: Bryan Boreham
Date: Mon, 22 Jul 2024 16:17:35 +0100
Subject: [PATCH 22/44] [DOCS] Querying basics: explain range and instant
queries
I often see people ask questions that indicate they don't understand
this point, and launching into "instant vector" and "range vector" is
likely to point them in the wrong direction.
Remove the admonishment that the reader mustn't confuse these things.
Remove mention of "inferred sample timestamps" that is never explained.
Signed-off-by: Bryan Boreham
---
docs/querying/basics.md | 16 ++++++++++------
1 file changed, 10 insertions(+), 6 deletions(-)
diff --git a/docs/querying/basics.md b/docs/querying/basics.md
index 1c72adb3e..304c9f07d 100644
--- a/docs/querying/basics.md
+++ b/docs/querying/basics.md
@@ -8,9 +8,15 @@ sort_rank: 1
Prometheus provides a functional query language called PromQL (Prometheus Query
Language) that lets the user select and aggregate time series data in real
-time. The result of an expression can either be shown as a graph, viewed as
-tabular data in Prometheus's expression browser, or consumed by external
-systems via the [HTTP API](api.md).
+time.
+
+When you send a query request to Prometheus, it can be an _instant query_, evaluated at one point in time,
+or a _range query_ at equally-spaced steps between a start and an end time. PromQL works exactly the same
+in each cases; the range query is just like an instant query run multiple times at different timestamps.
+
+In the Prometheus UI, the "Table" tab is for instant queries and the "Graph" tab is for range queries.
+
+Other programs can fetch the result of a PromQL expression via the [HTTP API](api.md).
## Examples
@@ -94,9 +100,7 @@ Examples:
## Time series selectors
-Time series selectors are responsible for selecting the times series and raw or inferred sample timestamps and values.
-
-Time series *selectors* are not to be confused with higher level concept of instant and range *queries* that can execute the time series *selectors*. A higher level instant query would evaluate the given selector at one point in time, however the range query would evaluate the selector at multiple different times in between a minimum and maximum timestamp at regular steps.
+These are the basic building-blocks that instruct PromQL what data to fetch.
### Instant vector selectors
From c037a3df844d15b5e38be7eaa28f842c5ee07ee2 Mon Sep 17 00:00:00 2001
From: Bryan Boreham
Date: Mon, 22 Jul 2024 16:34:42 +0100
Subject: [PATCH 23/44] lint
Signed-off-by: Bryan Boreham
---
storage/merge_test.go | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/storage/merge_test.go b/storage/merge_test.go
index 488edb2e6..b145743c8 100644
--- a/storage/merge_test.go
+++ b/storage/merge_test.go
@@ -949,7 +949,7 @@ func (m *mockQuerier) Select(_ context.Context, sortSeries bool, _ *SelectHints,
} else if len(m.resp) > 0 {
ret = make([]Series, 0, len(m.resp))
for _, l := range m.resp {
- ret = append(ret, NewListSeries(labels.FromStrings("test", string(l)), nil))
+ ret = append(ret, NewListSeries(labels.FromStrings("test", l), nil))
}
}
if sortSeries {
From be7a4c9b83a9f074f823d12e3d58338407fe76a1 Mon Sep 17 00:00:00 2001
From: Filip Petkovski
Date: Fri, 26 Jul 2024 09:49:57 +0200
Subject: [PATCH 24/44] Ignore stale histograms for counter reset detection
The histogram stats decoder keeps track of the last seen histogram sample
in order to properly detect counter resets. We are seeing an issue where
a histogram with UnknownResetHint gets treated as a counter reset when it follows
a stale histogram sample.
I believe that this is incorrect since stale samples should be completely ignored
in PromQL. As a result, they should not be stored in the histogram stats iterator
and the counter reset detection needs to be done against the last non-stale sample.
Signed-off-by: Filip Petkovski
---
promql/histogram_stats_iterator.go | 2 -
promql/histogram_stats_iterator_test.go | 123 +++++++++++++++---------
tsdb/tsdbutil/histogram.go | 10 +-
3 files changed, 84 insertions(+), 51 deletions(-)
diff --git a/promql/histogram_stats_iterator.go b/promql/histogram_stats_iterator.go
index dfafea5f8..0a5f67ae7 100644
--- a/promql/histogram_stats_iterator.go
+++ b/promql/histogram_stats_iterator.go
@@ -48,7 +48,6 @@ func (f *histogramStatsIterator) AtHistogram(h *histogram.Histogram) (int64, *hi
var t int64
t, f.currentH = f.Iterator.AtHistogram(f.currentH)
if value.IsStaleNaN(f.currentH.Sum) {
- f.setLastH(f.currentH)
h = &histogram.Histogram{Sum: f.currentH.Sum}
return t, h
}
@@ -77,7 +76,6 @@ func (f *histogramStatsIterator) AtFloatHistogram(fh *histogram.FloatHistogram)
var t int64
t, f.currentFH = f.Iterator.AtFloatHistogram(f.currentFH)
if value.IsStaleNaN(f.currentFH.Sum) {
- f.setLastFH(f.currentFH)
return t, &histogram.FloatHistogram{Sum: f.currentFH.Sum}
}
diff --git a/promql/histogram_stats_iterator_test.go b/promql/histogram_stats_iterator_test.go
index b71a9d602..d5c081348 100644
--- a/promql/histogram_stats_iterator_test.go
+++ b/promql/histogram_stats_iterator_test.go
@@ -14,62 +14,99 @@
package promql
import (
+ "fmt"
+ "math"
"testing"
"github.com/stretchr/testify/require"
"github.com/prometheus/prometheus/model/histogram"
"github.com/prometheus/prometheus/model/labels"
+ "github.com/prometheus/prometheus/model/value"
"github.com/prometheus/prometheus/tsdb/chunkenc"
"github.com/prometheus/prometheus/tsdb/tsdbutil"
)
func TestHistogramStatsDecoding(t *testing.T) {
- histograms := []*histogram.Histogram{
- tsdbutil.GenerateTestHistogram(0),
- tsdbutil.GenerateTestHistogram(1),
- tsdbutil.GenerateTestHistogram(2),
- tsdbutil.GenerateTestHistogram(2),
- }
- histograms[0].CounterResetHint = histogram.NotCounterReset
- histograms[1].CounterResetHint = histogram.UnknownCounterReset
- histograms[2].CounterResetHint = histogram.CounterReset
- histograms[3].CounterResetHint = histogram.UnknownCounterReset
-
- expectedHints := []histogram.CounterResetHint{
- histogram.NotCounterReset,
- histogram.NotCounterReset,
- histogram.CounterReset,
- histogram.NotCounterReset,
+ cases := []struct {
+ name string
+ histograms []*histogram.Histogram
+ expectedHints []histogram.CounterResetHint
+ }{
+ {
+ name: "unknown counter reset triggers detection",
+ histograms: []*histogram.Histogram{
+ tsdbutil.GenerateTestHistogramWithHint(0, histogram.NotCounterReset),
+ tsdbutil.GenerateTestHistogramWithHint(1, histogram.UnknownCounterReset),
+ tsdbutil.GenerateTestHistogramWithHint(2, histogram.CounterReset),
+ tsdbutil.GenerateTestHistogramWithHint(2, histogram.UnknownCounterReset),
+ },
+ expectedHints: []histogram.CounterResetHint{
+ histogram.NotCounterReset,
+ histogram.NotCounterReset,
+ histogram.CounterReset,
+ histogram.NotCounterReset,
+ },
+ },
+ {
+ name: "stale sample before unknown reset hint",
+ histograms: []*histogram.Histogram{
+ tsdbutil.GenerateTestHistogramWithHint(0, histogram.NotCounterReset),
+ tsdbutil.GenerateTestHistogramWithHint(1, histogram.UnknownCounterReset),
+ {Sum: math.Float64frombits(value.StaleNaN)},
+ tsdbutil.GenerateTestHistogramWithHint(1, histogram.UnknownCounterReset),
+ },
+ expectedHints: []histogram.CounterResetHint{
+ histogram.NotCounterReset,
+ histogram.NotCounterReset,
+ histogram.UnknownCounterReset,
+ histogram.NotCounterReset,
+ },
+ },
}
- t.Run("histogram_stats", func(t *testing.T) {
- decodedStats := make([]*histogram.Histogram, 0)
- statsIterator := NewHistogramStatsIterator(newHistogramSeries(histograms).Iterator(nil))
- for statsIterator.Next() != chunkenc.ValNone {
- _, h := statsIterator.AtHistogram(nil)
- decodedStats = append(decodedStats, h)
- }
- for i := 0; i < len(histograms); i++ {
- require.Equal(t, expectedHints[i], decodedStats[i].CounterResetHint)
- require.Equal(t, histograms[i].Count, decodedStats[i].Count)
- require.Equal(t, histograms[i].Sum, decodedStats[i].Sum)
- }
- })
- t.Run("float_histogram_stats", func(t *testing.T) {
- decodedStats := make([]*histogram.FloatHistogram, 0)
- statsIterator := NewHistogramStatsIterator(newHistogramSeries(histograms).Iterator(nil))
- for statsIterator.Next() != chunkenc.ValNone {
- _, h := statsIterator.AtFloatHistogram(nil)
- decodedStats = append(decodedStats, h)
- }
- for i := 0; i < len(histograms); i++ {
- fh := histograms[i].ToFloat(nil)
- require.Equal(t, expectedHints[i], decodedStats[i].CounterResetHint)
- require.Equal(t, fh.Count, decodedStats[i].Count)
- require.Equal(t, fh.Sum, decodedStats[i].Sum)
- }
- })
+ for _, tc := range cases {
+ t.Run(tc.name, func(t *testing.T) {
+ t.Run("histogram_stats", func(t *testing.T) {
+ decodedStats := make([]*histogram.Histogram, 0)
+ statsIterator := NewHistogramStatsIterator(newHistogramSeries(tc.histograms).Iterator(nil))
+ for statsIterator.Next() != chunkenc.ValNone {
+ _, h := statsIterator.AtHistogram(nil)
+ decodedStats = append(decodedStats, h)
+ }
+ for i := 0; i < len(tc.histograms); i++ {
+ require.Equal(t, tc.expectedHints[i], decodedStats[i].CounterResetHint, fmt.Sprintf("mismatch in counter reset hint for histogram %d", i))
+ h := tc.histograms[i]
+ if value.IsStaleNaN(h.Sum) {
+ require.True(t, value.IsStaleNaN(decodedStats[i].Sum))
+ require.Equal(t, uint64(0), decodedStats[i].Count)
+ } else {
+ require.Equal(t, tc.histograms[i].Count, decodedStats[i].Count)
+ require.Equal(t, tc.histograms[i].Sum, decodedStats[i].Sum)
+ }
+ }
+ })
+ t.Run("float_histogram_stats", func(t *testing.T) {
+ decodedStats := make([]*histogram.FloatHistogram, 0)
+ statsIterator := NewHistogramStatsIterator(newHistogramSeries(tc.histograms).Iterator(nil))
+ for statsIterator.Next() != chunkenc.ValNone {
+ _, h := statsIterator.AtFloatHistogram(nil)
+ decodedStats = append(decodedStats, h)
+ }
+ for i := 0; i < len(tc.histograms); i++ {
+ require.Equal(t, tc.expectedHints[i], decodedStats[i].CounterResetHint)
+ fh := tc.histograms[i].ToFloat(nil)
+ if value.IsStaleNaN(fh.Sum) {
+ require.True(t, value.IsStaleNaN(decodedStats[i].Sum))
+ require.Equal(t, float64(0), decodedStats[i].Count)
+ } else {
+ require.Equal(t, fh.Count, decodedStats[i].Count)
+ require.Equal(t, fh.Sum, decodedStats[i].Sum)
+ }
+ }
+ })
+ })
+ }
}
type histogramSeries struct {
diff --git a/tsdb/tsdbutil/histogram.go b/tsdb/tsdbutil/histogram.go
index 3c7349cf7..ce934a638 100644
--- a/tsdb/tsdbutil/histogram.go
+++ b/tsdb/tsdbutil/histogram.go
@@ -30,12 +30,10 @@ func GenerateTestHistograms(n int) (r []*histogram.Histogram) {
return r
}
-func GenerateTestHistogramsWithUnknownResetHint(n int) []*histogram.Histogram {
- hs := GenerateTestHistograms(n)
- for i := range hs {
- hs[i].CounterResetHint = histogram.UnknownCounterReset
- }
- return hs
+func GenerateTestHistogramWithHint(n int, hint histogram.CounterResetHint) *histogram.Histogram {
+ h := GenerateTestHistogram(n)
+ h.CounterResetHint = hint
+ return h
}
// GenerateTestHistogram but it is up to the user to set any known counter reset hint.
From 6e89250a5d937485a140c6ba6dcdb35d2db51cd0 Mon Sep 17 00:00:00 2001
From: Bryan Boreham
Date: Fri, 26 Jul 2024 09:49:25 +0100
Subject: [PATCH 25/44] Revert "Chunked remote read: close the querier earlier"
Believed to trigger segmentation faults due to memory-mapped block
data still being accessed by iterators after the querier is closed.
Signed-off-by: Bryan Boreham
---
storage/remote/read_handler.go | 53 ++++++++++++++--------------------
1 file changed, 21 insertions(+), 32 deletions(-)
diff --git a/storage/remote/read_handler.go b/storage/remote/read_handler.go
index 2a00ce897..ffc64c9c3 100644
--- a/storage/remote/read_handler.go
+++ b/storage/remote/read_handler.go
@@ -202,16 +202,34 @@ func (h *readHandler) remoteReadStreamedXORChunks(ctx context.Context, w http.Re
return err
}
- chunks := h.getChunkSeriesSet(ctx, query, filteredMatchers)
- if err := chunks.Err(); err != nil {
+ querier, err := h.queryable.ChunkQuerier(query.StartTimestampMs, query.EndTimestampMs)
+ if err != nil {
return err
}
+ defer func() {
+ if err := querier.Close(); err != nil {
+ level.Warn(h.logger).Log("msg", "Error on chunk querier close", "err", err.Error())
+ }
+ }()
+
+ var hints *storage.SelectHints
+ if query.Hints != nil {
+ hints = &storage.SelectHints{
+ Start: query.Hints.StartMs,
+ End: query.Hints.EndMs,
+ Step: query.Hints.StepMs,
+ Func: query.Hints.Func,
+ Grouping: query.Hints.Grouping,
+ Range: query.Hints.RangeMs,
+ By: query.Hints.By,
+ }
+ }
ws, err := StreamChunkedReadResponses(
NewChunkedWriter(w, f),
int64(i),
// The streaming API has to provide the series sorted.
- chunks,
+ querier.Select(ctx, true, hints, filteredMatchers...),
sortedExternalLabels,
h.remoteReadMaxBytesInFrame,
h.marshalPool,
@@ -236,35 +254,6 @@ func (h *readHandler) remoteReadStreamedXORChunks(ctx context.Context, w http.Re
}
}
-// getChunkSeriesSet executes a query to retrieve a ChunkSeriesSet,
-// encapsulating the operation in its own function to ensure timely release of
-// the querier resources.
-func (h *readHandler) getChunkSeriesSet(ctx context.Context, query *prompb.Query, filteredMatchers []*labels.Matcher) storage.ChunkSeriesSet {
- querier, err := h.queryable.ChunkQuerier(query.StartTimestampMs, query.EndTimestampMs)
- if err != nil {
- return storage.ErrChunkSeriesSet(err)
- }
- defer func() {
- if err := querier.Close(); err != nil {
- level.Warn(h.logger).Log("msg", "Error on chunk querier close", "err", err.Error())
- }
- }()
-
- var hints *storage.SelectHints
- if query.Hints != nil {
- hints = &storage.SelectHints{
- Start: query.Hints.StartMs,
- End: query.Hints.EndMs,
- Step: query.Hints.StepMs,
- Func: query.Hints.Func,
- Grouping: query.Hints.Grouping,
- Range: query.Hints.RangeMs,
- By: query.Hints.By,
- }
- }
- return querier.Select(ctx, true, hints, filteredMatchers...)
-}
-
// filterExtLabelsFromMatchers change equality matchers which match external labels
// to a matcher that looks for an empty label,
// as that label should not be present in the storage.
From d4f098ae80fb276153efc757e373c813163da0e8 Mon Sep 17 00:00:00 2001
From: Marco Pracucci
Date: Fri, 26 Jul 2024 14:55:39 +0200
Subject: [PATCH 26/44] Fix relabel.Regexp zero value marshalling (#14517)
Signed-off-by: Marco Pracucci
---
model/relabel/relabel.go | 4 ++++
model/relabel/relabel_test.go | 13 +++++++++++++
2 files changed, 17 insertions(+)
diff --git a/model/relabel/relabel.go b/model/relabel/relabel.go
index 4f33edda4..a88046596 100644
--- a/model/relabel/relabel.go
+++ b/model/relabel/relabel.go
@@ -213,6 +213,10 @@ func (re Regexp) IsZero() bool {
// String returns the original string used to compile the regular expression.
func (re Regexp) String() string {
+ if re.Regexp == nil {
+ return ""
+ }
+
str := re.Regexp.String()
// Trim the anchor `^(?:` prefix and `)$` suffix.
return str[4 : len(str)-2]
diff --git a/model/relabel/relabel_test.go b/model/relabel/relabel_test.go
index 0f11f7068..fc9952134 100644
--- a/model/relabel/relabel_test.go
+++ b/model/relabel/relabel_test.go
@@ -900,3 +900,16 @@ action: replace
})
}
}
+
+func TestRegexp_ShouldMarshalAndUnmarshalZeroValue(t *testing.T) {
+ var zero Regexp
+
+ marshalled, err := yaml.Marshal(&zero)
+ require.NoError(t, err)
+ require.Equal(t, "null\n", string(marshalled))
+
+ var unmarshalled Regexp
+ err = yaml.Unmarshal(marshalled, &unmarshalled)
+ require.NoError(t, err)
+ require.Nil(t, unmarshalled.Regexp)
+}
From fe12924638d433c99b51f9acb1d7ebb9c1f40881 Mon Sep 17 00:00:00 2001
From: Kushal shukla <85934954+kushalShukla-web@users.noreply.github.com>
Date: Mon, 29 Jul 2024 07:28:08 -0400
Subject: [PATCH 27/44] promtool: JUnit-Format XML Test Results (#14506)
* Junit compatible output
Signed-off-by: Kushal Shukla
---
cmd/promtool/main.go | 7 ++-
cmd/promtool/unittest.go | 40 +++++++++++++----
cmd/promtool/unittest_test.go | 50 +++++++++++++++++++++
docs/command-line/promtool.md | 9 ++++
util/junitxml/junitxml.go | 81 ++++++++++++++++++++++++++++++++++
util/junitxml/junitxml_test.go | 66 +++++++++++++++++++++++++++
6 files changed, 243 insertions(+), 10 deletions(-)
create mode 100644 util/junitxml/junitxml.go
create mode 100644 util/junitxml/junitxml_test.go
diff --git a/cmd/promtool/main.go b/cmd/promtool/main.go
index e1d275e97..1c8e1dd1c 100644
--- a/cmd/promtool/main.go
+++ b/cmd/promtool/main.go
@@ -204,6 +204,7 @@ func main() {
pushMetricsHeaders := pushMetricsCmd.Flag("header", "Prometheus remote write header.").StringMap()
testCmd := app.Command("test", "Unit testing.")
+ junitOutFile := testCmd.Flag("junit", "File path to store JUnit XML test results.").OpenFile(os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o644)
testRulesCmd := testCmd.Command("rules", "Unit tests for rules.")
testRulesRun := testRulesCmd.Flag("run", "If set, will only run test groups whose names match the regular expression. Can be specified multiple times.").Strings()
testRulesFiles := testRulesCmd.Arg(
@@ -378,7 +379,11 @@ func main() {
os.Exit(QueryLabels(serverURL, httpRoundTripper, *queryLabelsMatch, *queryLabelsName, *queryLabelsBegin, *queryLabelsEnd, p))
case testRulesCmd.FullCommand():
- os.Exit(RulesUnitTest(
+ results := io.Discard
+ if *junitOutFile != nil {
+ results = *junitOutFile
+ }
+ os.Exit(RulesUnitTestResult(results,
promqltest.LazyLoaderOpts{
EnableAtModifier: true,
EnableNegativeOffset: true,
diff --git a/cmd/promtool/unittest.go b/cmd/promtool/unittest.go
index 5451c5296..7030635d1 100644
--- a/cmd/promtool/unittest.go
+++ b/cmd/promtool/unittest.go
@@ -18,6 +18,7 @@ import (
"encoding/json"
"errors"
"fmt"
+ "io"
"os"
"path/filepath"
"sort"
@@ -29,9 +30,10 @@ import (
"github.com/google/go-cmp/cmp"
"github.com/grafana/regexp"
"github.com/nsf/jsondiff"
- "github.com/prometheus/common/model"
"gopkg.in/yaml.v2"
+ "github.com/prometheus/common/model"
+
"github.com/prometheus/prometheus/model/histogram"
"github.com/prometheus/prometheus/model/labels"
"github.com/prometheus/prometheus/promql"
@@ -39,12 +41,18 @@ import (
"github.com/prometheus/prometheus/promql/promqltest"
"github.com/prometheus/prometheus/rules"
"github.com/prometheus/prometheus/storage"
+ "github.com/prometheus/prometheus/util/junitxml"
)
// RulesUnitTest does unit testing of rules based on the unit testing files provided.
// More info about the file format can be found in the docs.
func RulesUnitTest(queryOpts promqltest.LazyLoaderOpts, runStrings []string, diffFlag bool, files ...string) int {
+ return RulesUnitTestResult(io.Discard, queryOpts, runStrings, diffFlag, files...)
+}
+
+func RulesUnitTestResult(results io.Writer, queryOpts promqltest.LazyLoaderOpts, runStrings []string, diffFlag bool, files ...string) int {
failed := false
+ junit := &junitxml.JUnitXML{}
var run *regexp.Regexp
if runStrings != nil {
@@ -52,7 +60,7 @@ func RulesUnitTest(queryOpts promqltest.LazyLoaderOpts, runStrings []string, dif
}
for _, f := range files {
- if errs := ruleUnitTest(f, queryOpts, run, diffFlag); errs != nil {
+ if errs := ruleUnitTest(f, queryOpts, run, diffFlag, junit.Suite(f)); errs != nil {
fmt.Fprintln(os.Stderr, " FAILED:")
for _, e := range errs {
fmt.Fprintln(os.Stderr, e.Error())
@@ -64,25 +72,30 @@ func RulesUnitTest(queryOpts promqltest.LazyLoaderOpts, runStrings []string, dif
}
fmt.Println()
}
+ err := junit.WriteXML(results)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "failed to write JUnit XML: %s\n", err)
+ }
if failed {
return failureExitCode
}
return successExitCode
}
-func ruleUnitTest(filename string, queryOpts promqltest.LazyLoaderOpts, run *regexp.Regexp, diffFlag bool) []error {
- fmt.Println("Unit Testing: ", filename)
-
+func ruleUnitTest(filename string, queryOpts promqltest.LazyLoaderOpts, run *regexp.Regexp, diffFlag bool, ts *junitxml.TestSuite) []error {
b, err := os.ReadFile(filename)
if err != nil {
+ ts.Abort(err)
return []error{err}
}
var unitTestInp unitTestFile
if err := yaml.UnmarshalStrict(b, &unitTestInp); err != nil {
+ ts.Abort(err)
return []error{err}
}
if err := resolveAndGlobFilepaths(filepath.Dir(filename), &unitTestInp); err != nil {
+ ts.Abort(err)
return []error{err}
}
@@ -91,29 +104,38 @@ func ruleUnitTest(filename string, queryOpts promqltest.LazyLoaderOpts, run *reg
}
evalInterval := time.Duration(unitTestInp.EvaluationInterval)
-
+ ts.Settime(time.Now().Format("2006-01-02T15:04:05"))
// Giving number for groups mentioned in the file for ordering.
// Lower number group should be evaluated before higher number group.
groupOrderMap := make(map[string]int)
for i, gn := range unitTestInp.GroupEvalOrder {
if _, ok := groupOrderMap[gn]; ok {
- return []error{fmt.Errorf("group name repeated in evaluation order: %s", gn)}
+ err := fmt.Errorf("group name repeated in evaluation order: %s", gn)
+ ts.Abort(err)
+ return []error{err}
}
groupOrderMap[gn] = i
}
// Testing.
var errs []error
- for _, t := range unitTestInp.Tests {
+ for i, t := range unitTestInp.Tests {
if !matchesRun(t.TestGroupName, run) {
continue
}
-
+ testname := t.TestGroupName
+ if testname == "" {
+ testname = fmt.Sprintf("unnamed#%d", i)
+ }
+ tc := ts.Case(testname)
if t.Interval == 0 {
t.Interval = unitTestInp.EvaluationInterval
}
ers := t.test(evalInterval, groupOrderMap, queryOpts, diffFlag, unitTestInp.RuleFiles...)
if ers != nil {
+ for _, e := range ers {
+ tc.Fail(e.Error())
+ }
errs = append(errs, ers...)
}
}
diff --git a/cmd/promtool/unittest_test.go b/cmd/promtool/unittest_test.go
index 2dbd5a4e5..9bbac28e9 100644
--- a/cmd/promtool/unittest_test.go
+++ b/cmd/promtool/unittest_test.go
@@ -14,11 +14,15 @@
package main
import (
+ "bytes"
+ "encoding/xml"
+ "fmt"
"testing"
"github.com/stretchr/testify/require"
"github.com/prometheus/prometheus/promql/promqltest"
+ "github.com/prometheus/prometheus/util/junitxml"
)
func TestRulesUnitTest(t *testing.T) {
@@ -125,13 +129,59 @@ func TestRulesUnitTest(t *testing.T) {
want: 0,
},
}
+ reuseFiles := []string{}
+ reuseCount := [2]int{}
for _, tt := range tests {
+ if (tt.queryOpts == promqltest.LazyLoaderOpts{
+ EnableNegativeOffset: true,
+ } || tt.queryOpts == promqltest.LazyLoaderOpts{
+ EnableAtModifier: true,
+ }) {
+ reuseFiles = append(reuseFiles, tt.args.files...)
+ reuseCount[tt.want] += len(tt.args.files)
+ }
t.Run(tt.name, func(t *testing.T) {
if got := RulesUnitTest(tt.queryOpts, nil, false, tt.args.files...); got != tt.want {
t.Errorf("RulesUnitTest() = %v, want %v", got, tt.want)
}
})
}
+ t.Run("Junit xml output ", func(t *testing.T) {
+ var buf bytes.Buffer
+ if got := RulesUnitTestResult(&buf, promqltest.LazyLoaderOpts{}, nil, false, reuseFiles...); got != 1 {
+ t.Errorf("RulesUnitTestResults() = %v, want 1", got)
+ }
+ var test junitxml.JUnitXML
+ output := buf.Bytes()
+ err := xml.Unmarshal(output, &test)
+ if err != nil {
+ fmt.Println("error in decoding XML:", err)
+ return
+ }
+ var total int
+ var passes int
+ var failures int
+ var cases int
+ total = len(test.Suites)
+ if total != len(reuseFiles) {
+ t.Errorf("JUnit output had %d testsuite elements; expected %d\n", total, len(reuseFiles))
+ }
+
+ for _, i := range test.Suites {
+ if i.FailureCount == 0 {
+ passes++
+ } else {
+ failures++
+ }
+ cases += len(i.Cases)
+ }
+ if total != passes+failures {
+ t.Errorf("JUnit output mismatch: Total testsuites (%d) does not equal the sum of passes (%d) and failures (%d).", total, passes, failures)
+ }
+ if cases < total {
+ t.Errorf("JUnit output had %d suites without test cases\n", total-cases)
+ }
+ })
}
func TestRulesUnitTestRun(t *testing.T) {
diff --git a/docs/command-line/promtool.md b/docs/command-line/promtool.md
index 443cd3f0c..6bb80169a 100644
--- a/docs/command-line/promtool.md
+++ b/docs/command-line/promtool.md
@@ -442,6 +442,15 @@ Unit testing.
+#### Flags
+
+| Flag | Description |
+| --- | --- |
+| --junit
| File path to store JUnit XML test results. |
+
+
+
+
##### `promtool test rules`
Unit tests for rules.
diff --git a/util/junitxml/junitxml.go b/util/junitxml/junitxml.go
new file mode 100644
index 000000000..14e4b6dba
--- /dev/null
+++ b/util/junitxml/junitxml.go
@@ -0,0 +1,81 @@
+// Copyright 2024 The Prometheus Authors
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package junitxml
+
+import (
+ "encoding/xml"
+ "io"
+)
+
+type JUnitXML struct {
+ XMLName xml.Name `xml:"testsuites"`
+ Suites []*TestSuite `xml:"testsuite"`
+}
+
+type TestSuite struct {
+ Name string `xml:"name,attr"`
+ TestCount int `xml:"tests,attr"`
+ FailureCount int `xml:"failures,attr"`
+ ErrorCount int `xml:"errors,attr"`
+ SkippedCount int `xml:"skipped,attr"`
+ Timestamp string `xml:"timestamp,attr"`
+ Cases []*TestCase `xml:"testcase"`
+}
+type TestCase struct {
+ Name string `xml:"name,attr"`
+ Failures []string `xml:"failure,omitempty"`
+ Error string `xml:"error,omitempty"`
+}
+
+func (j *JUnitXML) WriteXML(h io.Writer) error {
+ return xml.NewEncoder(h).Encode(j)
+}
+
+func (j *JUnitXML) Suite(name string) *TestSuite {
+ ts := &TestSuite{Name: name}
+ j.Suites = append(j.Suites, ts)
+ return ts
+}
+
+func (ts *TestSuite) Fail(f string) {
+ ts.FailureCount++
+ curt := ts.lastCase()
+ curt.Failures = append(curt.Failures, f)
+}
+
+func (ts *TestSuite) lastCase() *TestCase {
+ if len(ts.Cases) == 0 {
+ ts.Case("unknown")
+ }
+ return ts.Cases[len(ts.Cases)-1]
+}
+
+func (ts *TestSuite) Case(name string) *TestSuite {
+ j := &TestCase{
+ Name: name,
+ }
+ ts.Cases = append(ts.Cases, j)
+ ts.TestCount++
+ return ts
+}
+
+func (ts *TestSuite) Settime(name string) {
+ ts.Timestamp = name
+}
+
+func (ts *TestSuite) Abort(e error) {
+ ts.ErrorCount++
+ curt := ts.lastCase()
+ curt.Error = e.Error()
+}
diff --git a/util/junitxml/junitxml_test.go b/util/junitxml/junitxml_test.go
new file mode 100644
index 000000000..ad4d0293d
--- /dev/null
+++ b/util/junitxml/junitxml_test.go
@@ -0,0 +1,66 @@
+// Copyright 2024 The Prometheus Authors
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package junitxml
+
+import (
+ "bytes"
+ "encoding/xml"
+ "errors"
+ "testing"
+)
+
+func TestJunitOutput(t *testing.T) {
+ var buf bytes.Buffer
+ var test JUnitXML
+ x := FakeTestSuites()
+ if err := x.WriteXML(&buf); err != nil {
+ t.Fatalf("Failed to encode XML: %v", err)
+ }
+
+ output := buf.Bytes()
+
+ err := xml.Unmarshal(output, &test)
+ if err != nil {
+ t.Errorf("Unmarshal failed with error: %v", err)
+ }
+ var total int
+ var cases int
+ total = len(test.Suites)
+ if total != 3 {
+ t.Errorf("JUnit output had %d testsuite elements; expected 3\n", total)
+ }
+ for _, i := range test.Suites {
+ cases += len(i.Cases)
+ }
+
+ if cases != 7 {
+ t.Errorf("JUnit output had %d testcase; expected 7\n", cases)
+ }
+}
+
+func FakeTestSuites() *JUnitXML {
+ ju := &JUnitXML{}
+ good := ju.Suite("all good")
+ good.Case("alpha")
+ good.Case("beta")
+ good.Case("gamma")
+ mixed := ju.Suite("mixed")
+ mixed.Case("good")
+ bad := mixed.Case("bad")
+ bad.Fail("once")
+ bad.Fail("twice")
+ mixed.Case("ugly").Abort(errors.New("buggy"))
+ ju.Suite("fast").Fail("fail early")
+ return ju
+}
From 2cd97c61e02ac9cf50e0fa4a72bbc61f8e128b8b Mon Sep 17 00:00:00 2001
From: Filip Petkovski
Date: Mon, 29 Jul 2024 14:53:32 +0200
Subject: [PATCH 28/44] Add more test cases
Signed-off-by: Filip Petkovski
---
promql/histogram_stats_iterator_test.go | 33 +++++++++++++++++++++++++
1 file changed, 33 insertions(+)
diff --git a/promql/histogram_stats_iterator_test.go b/promql/histogram_stats_iterator_test.go
index d5c081348..7a2953d3e 100644
--- a/promql/histogram_stats_iterator_test.go
+++ b/promql/histogram_stats_iterator_test.go
@@ -63,6 +63,39 @@ func TestHistogramStatsDecoding(t *testing.T) {
histogram.NotCounterReset,
},
},
+ {
+ name: "unknown counter reset at the beginning",
+ histograms: []*histogram.Histogram{
+ tsdbutil.GenerateTestHistogramWithHint(1, histogram.UnknownCounterReset),
+ },
+ expectedHints: []histogram.CounterResetHint{
+ histogram.NotCounterReset,
+ },
+ },
+ {
+ name: "detect real counter reset",
+ histograms: []*histogram.Histogram{
+ tsdbutil.GenerateTestHistogramWithHint(2, histogram.UnknownCounterReset),
+ tsdbutil.GenerateTestHistogramWithHint(1, histogram.UnknownCounterReset),
+ },
+ expectedHints: []histogram.CounterResetHint{
+ histogram.NotCounterReset,
+ histogram.CounterReset,
+ },
+ },
+ {
+ name: "detect real counter reset after stale NaN",
+ histograms: []*histogram.Histogram{
+ tsdbutil.GenerateTestHistogramWithHint(2, histogram.UnknownCounterReset),
+ {Sum: math.Float64frombits(value.StaleNaN)},
+ tsdbutil.GenerateTestHistogramWithHint(1, histogram.UnknownCounterReset),
+ },
+ expectedHints: []histogram.CounterResetHint{
+ histogram.NotCounterReset,
+ histogram.UnknownCounterReset,
+ histogram.CounterReset,
+ },
+ },
}
for _, tc := range cases {
From b7f2f3c3ac90f2347de6112c185a4e470e7ae8a6 Mon Sep 17 00:00:00 2001
From: Oleg Zaytsev
Date: Tue, 30 Jul 2024 10:19:56 +0200
Subject: [PATCH 29/44] Add BenchmarkLoadRealWLs
This benchmark runs on real WLs rather than fake generated ones.
Signed-off-by: Oleg Zaytsev
---
tsdb/head_test.go | 38 ++++++++++++++++++++++++++++++++++++++
1 file changed, 38 insertions(+)
diff --git a/tsdb/head_test.go b/tsdb/head_test.go
index c192c8a07..09927c23c 100644
--- a/tsdb/head_test.go
+++ b/tsdb/head_test.go
@@ -23,6 +23,7 @@ import (
"path"
"path/filepath"
"reflect"
+ "runtime/pprof"
"sort"
"strconv"
"strings"
@@ -89,6 +90,43 @@ func newTestHeadWithOptions(t testing.TB, compressWAL wlog.CompressionType, opts
return h, wal
}
+// BenchmarkLoadRealWLs will be skipped unless the BENCHMARK_LOAD_REAL_WLS_DIR environment variable is set.
+// BENCHMARK_LOAD_REAL_WLS_DIR should be the folder where `wal` and `chunks_head` are located.
+// Optionally, BENCHMARK_LOAD_REAL_WLS_PROFILE can be set to a file path to write a CPU profile.
+func BenchmarkLoadRealWLs(b *testing.B) {
+ dir := os.Getenv("BENCHMARK_LOAD_REAL_WLS_DIR")
+ if dir == "" {
+ b.Skipped()
+ }
+
+ profileFile := os.Getenv("BENCHMARK_LOAD_REAL_WLS_PROFILE")
+ if profileFile != "" {
+ b.Logf("Will profile in %s", profileFile)
+ f, err := os.Create(profileFile)
+ require.NoError(b, err)
+ b.Cleanup(func() { f.Close() })
+ require.NoError(b, pprof.StartCPUProfile(f))
+ b.Cleanup(pprof.StopCPUProfile)
+ }
+
+ wal, err := wlog.New(nil, nil, filepath.Join(dir, "wal"), wlog.CompressionNone)
+ require.NoError(b, err)
+ b.Cleanup(func() { wal.Close() })
+
+ wbl, err := wlog.New(nil, nil, filepath.Join(dir, "wbl"), wlog.CompressionNone)
+ require.NoError(b, err)
+ b.Cleanup(func() { wbl.Close() })
+
+ // Load the WAL.
+ for i := 0; i < b.N; i++ {
+ opts := DefaultHeadOptions()
+ opts.ChunkDirRoot = dir
+ h, err := NewHead(nil, nil, wal, wbl, opts, nil)
+ require.NoError(b, err)
+ h.Init(0)
+ }
+}
+
func BenchmarkCreateSeries(b *testing.B) {
series := genSeries(b.N, 10, 0, 0)
h, _ := newTestHead(b, 10000, wlog.CompressionNone, false)
From d8e1b6bdfd3c8cd02a38b21386453dac9b14da1b Mon Sep 17 00:00:00 2001
From: Oleg Zaytsev
Date: Tue, 30 Jul 2024 10:20:29 +0200
Subject: [PATCH 30/44] Store mmMaxTime in same field as seriesShard
We don't use seriesShard during DB initialization, so we can use the
same 8 bytes to store mmMaxTime, and save those during the rest of the
lifetime of the database.
This doesn't affect CPU performance.
Signed-off-by: Oleg Zaytsev
---
tsdb/head.go | 46 +++++++++++++++++++++++++++++++++++-----------
tsdb/head_read.go | 2 +-
tsdb/head_wal.go | 13 ++++++++-----
3 files changed, 44 insertions(+), 17 deletions(-)
diff --git a/tsdb/head.go b/tsdb/head.go
index b7bfaa0fd..1659e57a4 100644
--- a/tsdb/head.go
+++ b/tsdb/head.go
@@ -178,6 +178,7 @@ type HeadOptions struct {
WALReplayConcurrency int
// EnableSharding enables ShardedPostings() support in the Head.
+ // EnableSharding is temporarily disabled during Init().
EnableSharding bool
}
@@ -609,7 +610,7 @@ const cardinalityCacheExpirationTime = time.Duration(30) * time.Second
// Init loads data from the write ahead log and prepares the head for writes.
// It should be called before using an appender so that it
// limits the ingested samples to the head min valid time.
-func (h *Head) Init(minValidTime int64) error {
+func (h *Head) Init(minValidTime int64) (err error) {
h.minValidTime.Store(minValidTime)
defer func() {
h.postings.EnsureOrder(h.opts.WALReplayConcurrency)
@@ -623,6 +624,24 @@ func (h *Head) Init(minValidTime int64) error {
}
}()
+ // If sharding is enabled, disable it while initializing, and calculate the shards later.
+ // We're going to use that field for other purposes during WAL replay,
+ // so we don't want to waste time on calculating the shard that we're going to lose anyway.
+ if h.opts.EnableSharding {
+ h.opts.EnableSharding = false
+ defer func() {
+ if err == nil {
+ h.opts.EnableSharding = true
+ // No locking is needed here as nobody should be writing while we're in Init.
+ for _, stripe := range h.series.series {
+ for _, s := range stripe {
+ s.shardHashOrMemoryMappedMaxTime = labels.StableHash(s.lset)
+ }
+ }
+ }
+ }()
+ }
+
level.Info(h.logger).Log("msg", "Replaying on-disk memory mappable chunks if any")
start := time.Now()
@@ -683,7 +702,6 @@ func (h *Head) Init(minValidTime int64) error {
mmappedChunks map[chunks.HeadSeriesRef][]*mmappedChunk
oooMmappedChunks map[chunks.HeadSeriesRef][]*mmappedChunk
lastMmapRef chunks.ChunkDiskMapperRef
- err error
mmapChunkReplayDuration time.Duration
)
@@ -2068,9 +2086,11 @@ type memSeries struct {
ref chunks.HeadSeriesRef
meta *metadata.Metadata
- // Series labels hash to use for sharding purposes. The value is always 0 when sharding has not
- // been explicitly enabled in TSDB.
- shardHash uint64
+ // Series labels hash to use for sharding purposes.
+ // The value is always 0 when sharding has not been explicitly enabled in TSDB.
+ // While the WAL replay the value stored here is the max time of any mmapped chunk,
+ // and the shard hash is re-calculated after WAL replay is complete.
+ shardHashOrMemoryMappedMaxTime uint64
// Everything after here should only be accessed with the lock held.
sync.Mutex
@@ -2095,8 +2115,6 @@ type memSeries struct {
ooo *memSeriesOOOFields
- mmMaxTime int64 // Max time of any mmapped chunk, only used during WAL replay.
-
nextAt int64 // Timestamp at which to cut the next chunk.
histogramChunkHasComputedEndTime bool // True if nextAt has been predicted for the current histograms chunk; false otherwise.
pendingCommit bool // Whether there are samples waiting to be committed to this series.
@@ -2127,10 +2145,10 @@ type memSeriesOOOFields struct {
func newMemSeries(lset labels.Labels, id chunks.HeadSeriesRef, shardHash uint64, isolationDisabled bool) *memSeries {
s := &memSeries{
- lset: lset,
- ref: id,
- nextAt: math.MinInt64,
- shardHash: shardHash,
+ lset: lset,
+ ref: id,
+ nextAt: math.MinInt64,
+ shardHashOrMemoryMappedMaxTime: shardHash,
}
if !isolationDisabled {
s.txs = newTxRing(0)
@@ -2218,6 +2236,12 @@ func (s *memSeries) truncateChunksBefore(mint int64, minOOOMmapRef chunks.ChunkD
return removedInOrder + removedOOO
}
+// shardHash returns the shard hash of the series, only available after WAL replay.
+func (s *memSeries) shardHash() uint64 { return s.shardHashOrMemoryMappedMaxTime }
+
+// mmMaxTime returns the max time of any mmapped chunk in the series, only available during WAL replay.
+func (s *memSeries) mmMaxTime() int64 { return int64(s.shardHashOrMemoryMappedMaxTime) }
+
// cleanupAppendIDsBelow cleans up older appendIDs. Has to be called after
// acquiring lock.
func (s *memSeries) cleanupAppendIDsBelow(bound uint64) {
diff --git a/tsdb/head_read.go b/tsdb/head_read.go
index 9ba8785ad..3a50f316b 100644
--- a/tsdb/head_read.go
+++ b/tsdb/head_read.go
@@ -170,7 +170,7 @@ func (h *headIndexReader) ShardedPostings(p index.Postings, shardIndex, shardCou
}
// Check if the series belong to the shard.
- if s.shardHash%shardCount != shardIndex {
+ if s.shardHash()%shardCount != shardIndex {
continue
}
diff --git a/tsdb/head_wal.go b/tsdb/head_wal.go
index 787cb7c26..2852709a0 100644
--- a/tsdb/head_wal.go
+++ b/tsdb/head_wal.go
@@ -435,6 +435,8 @@ Outer:
return nil
}
+func minInt64() int64 { return math.MinInt64 }
+
// resetSeriesWithMMappedChunks is only used during the WAL replay.
func (h *Head) resetSeriesWithMMappedChunks(mSeries *memSeries, mmc, oooMmc []*mmappedChunk, walSeriesRef chunks.HeadSeriesRef) (overlapped bool) {
if mSeries.ref != walSeriesRef {
@@ -481,10 +483,11 @@ func (h *Head) resetSeriesWithMMappedChunks(mSeries *memSeries, mmc, oooMmc []*m
}
// Cache the last mmapped chunk time, so we can skip calling append() for samples it will reject.
if len(mmc) == 0 {
- mSeries.mmMaxTime = math.MinInt64
+ mSeries.shardHashOrMemoryMappedMaxTime = uint64(minInt64())
} else {
- mSeries.mmMaxTime = mmc[len(mmc)-1].maxTime
- h.updateMinMaxTime(mmc[0].minTime, mSeries.mmMaxTime)
+ mmMaxTime := mmc[len(mmc)-1].maxTime
+ mSeries.shardHashOrMemoryMappedMaxTime = uint64(mmMaxTime)
+ h.updateMinMaxTime(mmc[0].minTime, mmMaxTime)
}
if len(oooMmc) != 0 {
// Mint and maxt can be in any chunk, they are not sorted.
@@ -585,7 +588,7 @@ func (wp *walSubsetProcessor) processWALSamples(h *Head, mmappedChunks, oooMmapp
unknownRefs++
continue
}
- if s.T <= ms.mmMaxTime {
+ if s.T <= ms.mmMaxTime() {
continue
}
if _, chunkCreated := ms.append(s.T, s.V, 0, appendChunkOpts); chunkCreated {
@@ -614,7 +617,7 @@ func (wp *walSubsetProcessor) processWALSamples(h *Head, mmappedChunks, oooMmapp
unknownHistogramRefs++
continue
}
- if s.t <= ms.mmMaxTime {
+ if s.t <= ms.mmMaxTime() {
continue
}
var chunkCreated bool
From 0300ad58a97098674ca4757c79a74a05e9c33322 Mon Sep 17 00:00:00 2001
From: Oleg Zaytsev
Date: Tue, 30 Jul 2024 11:31:31 +0200
Subject: [PATCH 31/44] Revert the option regardless of error
Signed-off-by: Oleg Zaytsev
---
tsdb/head.go | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/tsdb/head.go b/tsdb/head.go
index 1659e57a4..9d81b24ae 100644
--- a/tsdb/head.go
+++ b/tsdb/head.go
@@ -630,8 +630,8 @@ func (h *Head) Init(minValidTime int64) (err error) {
if h.opts.EnableSharding {
h.opts.EnableSharding = false
defer func() {
+ h.opts.EnableSharding = true
if err == nil {
- h.opts.EnableSharding = true
// No locking is needed here as nobody should be writing while we're in Init.
for _, stripe := range h.series.series {
for _, s := range stripe {
From 6cef8698c27b99263efcbe5025846187cf4358f7 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Tue, 30 Jul 2024 13:30:49 +0200
Subject: [PATCH 32/44] build(deps-dev): bump @lezer/generator from 1.7.0 to
1.7.1 in /web/ui (#14382)
Bumps [@lezer/generator](https://github.com/lezer-parser/generator) from 1.7.0 to 1.7.1.
- [Changelog](https://github.com/lezer-parser/generator/blob/main/CHANGELOG.md)
- [Commits](https://github.com/lezer-parser/generator/compare/1.7.0...1.7.1)
---
updated-dependencies:
- dependency-name: "@lezer/generator"
dependency-type: direct:development
update-type: version-update:semver-patch
...
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
web/ui/module/lezer-promql/package.json | 2 +-
web/ui/package-lock.json | 8 ++++----
2 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/web/ui/module/lezer-promql/package.json b/web/ui/module/lezer-promql/package.json
index cbd03ae2b..43a5c44fa 100644
--- a/web/ui/module/lezer-promql/package.json
+++ b/web/ui/module/lezer-promql/package.json
@@ -30,7 +30,7 @@
"test": "NODE_OPTIONS=--experimental-vm-modules jest"
},
"devDependencies": {
- "@lezer/generator": "^1.7.0",
+ "@lezer/generator": "^1.7.1",
"@lezer/highlight": "^1.2.0",
"@lezer/lr": "^1.4.1"
},
diff --git a/web/ui/package-lock.json b/web/ui/package-lock.json
index 62ac34e43..2028c3402 100644
--- a/web/ui/package-lock.json
+++ b/web/ui/package-lock.json
@@ -72,7 +72,7 @@
"version": "0.53.1",
"license": "Apache-2.0",
"devDependencies": {
- "@lezer/generator": "^1.7.0",
+ "@lezer/generator": "^1.7.1",
"@lezer/highlight": "^1.2.0",
"@lezer/lr": "^1.4.1"
},
@@ -3371,9 +3371,9 @@
"integrity": "sha512-yemX0ZD2xS/73llMZIK6KplkjIjf2EvAHcinDi/TfJ9hS25G0388+ClHt6/3but0oOxinTcQHJLDXh6w1crzFQ=="
},
"node_modules/@lezer/generator": {
- "version": "1.7.0",
- "resolved": "https://registry.npmjs.org/@lezer/generator/-/generator-1.7.0.tgz",
- "integrity": "sha512-IJ16tx3biLKlCXUzcK4v8S10AVa2BSM2rB12rtAL6f1hL2TS/HQQlGCoWRvanlL2J4mCYEEIv9uG7n4kVMkVDA==",
+ "version": "1.7.1",
+ "resolved": "https://registry.npmjs.org/@lezer/generator/-/generator-1.7.1.tgz",
+ "integrity": "sha512-MgPJN9Si+ccxzXl3OAmCeZuUKw4XiPl4y664FX/hnnyG9CTqUPq65N3/VGPA2jD23D7QgMTtNqflta+cPN+5mQ==",
"dev": true,
"dependencies": {
"@lezer/common": "^1.1.0",
From 84b819a69f375dc66ea41302a56e44975c0317e3 Mon Sep 17 00:00:00 2001
From: Max Amin
Date: Tue, 30 Jul 2024 11:25:19 -0400
Subject: [PATCH 33/44] feat: add Google cloud roundtripper for remote write
(#14346)
* feat: Google Auth for remote write
Signed-off-by: Max Amin
---------
Signed-off-by: Max Amin
---
config/config.go | 36 +++++++++++++-----
config/config_test.go | 2 +-
docs/configuration/configuration.md | 16 ++++++--
promql/engine_test.go | 3 +-
rules/manager_test.go | 3 +-
storage/remote/client.go | 9 +++++
storage/remote/googleiam/googleiam.go | 54 +++++++++++++++++++++++++++
storage/remote/write.go | 1 +
tsdb/db_test.go | 5 ++-
9 files changed, 110 insertions(+), 19 deletions(-)
create mode 100644 storage/remote/googleiam/googleiam.go
diff --git a/config/config.go b/config/config.go
index 913983881..8a6216146 100644
--- a/config/config.go
+++ b/config/config.go
@@ -37,6 +37,7 @@ import (
"github.com/prometheus/prometheus/model/labels"
"github.com/prometheus/prometheus/model/relabel"
"github.com/prometheus/prometheus/storage/remote/azuread"
+ "github.com/prometheus/prometheus/storage/remote/googleiam"
)
var (
@@ -1123,6 +1124,7 @@ type RemoteWriteConfig struct {
MetadataConfig MetadataConfig `yaml:"metadata_config,omitempty"`
SigV4Config *sigv4.SigV4Config `yaml:"sigv4,omitempty"`
AzureADConfig *azuread.AzureADConfig `yaml:"azuread,omitempty"`
+ GoogleIAMConfig *googleiam.Config `yaml:"google_iam,omitempty"`
}
// SetDirectory joins any relative file paths with dir.
@@ -1160,17 +1162,33 @@ func (c *RemoteWriteConfig) UnmarshalYAML(unmarshal func(interface{}) error) err
return err
}
- httpClientConfigAuthEnabled := c.HTTPClientConfig.BasicAuth != nil ||
- c.HTTPClientConfig.Authorization != nil || c.HTTPClientConfig.OAuth2 != nil
+ return validateAuthConfigs(c)
+}
- if httpClientConfigAuthEnabled && (c.SigV4Config != nil || c.AzureADConfig != nil) {
- return fmt.Errorf("at most one of basic_auth, authorization, oauth2, sigv4, & azuread must be configured")
+// validateAuthConfigs validates that at most one of basic_auth, authorization, oauth2, sigv4, azuread or google_iam must be configured.
+func validateAuthConfigs(c *RemoteWriteConfig) error {
+ var authConfigured []string
+ if c.HTTPClientConfig.BasicAuth != nil {
+ authConfigured = append(authConfigured, "basic_auth")
}
-
- if c.SigV4Config != nil && c.AzureADConfig != nil {
- return fmt.Errorf("at most one of basic_auth, authorization, oauth2, sigv4, & azuread must be configured")
+ if c.HTTPClientConfig.Authorization != nil {
+ authConfigured = append(authConfigured, "authorization")
+ }
+ if c.HTTPClientConfig.OAuth2 != nil {
+ authConfigured = append(authConfigured, "oauth2")
+ }
+ if c.SigV4Config != nil {
+ authConfigured = append(authConfigured, "sigv4")
+ }
+ if c.AzureADConfig != nil {
+ authConfigured = append(authConfigured, "azuread")
+ }
+ if c.GoogleIAMConfig != nil {
+ authConfigured = append(authConfigured, "google_iam")
+ }
+ if len(authConfigured) > 1 {
+ return fmt.Errorf("at most one of basic_auth, authorization, oauth2, sigv4, azuread or google_iam must be configured. Currently configured: %v", authConfigured)
}
-
return nil
}
@@ -1189,7 +1207,7 @@ func validateHeadersForTracing(headers map[string]string) error {
func validateHeaders(headers map[string]string) error {
for header := range headers {
if strings.ToLower(header) == "authorization" {
- return errors.New("authorization header must be changed via the basic_auth, authorization, oauth2, sigv4, or azuread parameter")
+ return errors.New("authorization header must be changed via the basic_auth, authorization, oauth2, sigv4, azuread or google_iam parameter")
}
if _, ok := reservedHeaders[strings.ToLower(header)]; ok {
return fmt.Errorf("%s is a reserved header. It must not be changed", header)
diff --git a/config/config_test.go b/config/config_test.go
index b684fdb50..9b074bef1 100644
--- a/config/config_test.go
+++ b/config/config_test.go
@@ -1826,7 +1826,7 @@ var expectedErrors = []struct {
},
{
filename: "remote_write_authorization_header.bad.yml",
- errMsg: `authorization header must be changed via the basic_auth, authorization, oauth2, sigv4, or azuread parameter`,
+ errMsg: `authorization header must be changed via the basic_auth, authorization, oauth2, sigv4, azuread or google_iam parameter`,
},
{
filename: "remote_write_wrong_msg.bad.yml",
diff --git a/docs/configuration/configuration.md b/docs/configuration/configuration.md
index 5aa57b3ba..313a7f2f3 100644
--- a/docs/configuration/configuration.md
+++ b/docs/configuration/configuration.md
@@ -3401,8 +3401,8 @@ authorization:
# It is mutually exclusive with `credentials`.
[ credentials_file: ]
-# Optionally configures AWS's Signature Verification 4 signing process to
-# sign requests. Cannot be set at the same time as basic_auth, authorization, or oauth2.
+# Optionally configures AWS's Signature Verification 4 signing process to sign requests.
+# Cannot be set at the same time as basic_auth, authorization, oauth2, azuread or google_iam.
# To use the default credentials from the AWS SDK, use `sigv4: {}`.
sigv4:
# The AWS region. If blank, the region from the default credentials chain
@@ -3655,12 +3655,12 @@ sigv4:
[ role_arn: ]
# Optional OAuth 2.0 configuration.
-# Cannot be used at the same time as basic_auth, authorization, sigv4, or azuread.
+# Cannot be used at the same time as basic_auth, authorization, sigv4, azuread or google_iam.
oauth2:
[ ]
# Optional AzureAD configuration.
-# Cannot be used at the same time as basic_auth, authorization, oauth2, or sigv4.
+# Cannot be used at the same time as basic_auth, authorization, oauth2, sigv4 or google_iam.
azuread:
# The Azure Cloud. Options are 'AzurePublic', 'AzureChina', or 'AzureGovernment'.
[ cloud: | default = AzurePublic ]
@@ -3680,6 +3680,14 @@ azuread:
[ sdk:
[ tenant_id: ] ]
+# WARNING: Remote write is NOT SUPPORTED by Google Cloud. This configuration is reserved for future use.
+# Optional Google Cloud Monitoring configuration.
+# Cannot be used at the same time as basic_auth, authorization, oauth2, sigv4 or azuread.
+# To use the default credentials from the Google Cloud SDK, use `google_iam: {}`.
+google_iam:
+ # Service account key with monitoring write permessions.
+ credentials_file:
+
# Configures the remote write request's TLS settings.
tls_config:
[ ]
diff --git a/promql/engine_test.go b/promql/engine_test.go
index 523c0613d..8e618d435 100644
--- a/promql/engine_test.go
+++ b/promql/engine_test.go
@@ -26,7 +26,6 @@ import (
"time"
"github.com/stretchr/testify/require"
- "go.uber.org/goleak"
"github.com/prometheus/prometheus/model/histogram"
"github.com/prometheus/prometheus/model/labels"
@@ -51,7 +50,7 @@ const (
func TestMain(m *testing.M) {
// Enable experimental functions testing
parser.EnableExperimentalFunctions = true
- goleak.VerifyTestMain(m)
+ testutil.TolerantVerifyLeak(m)
}
func TestQueryConcurrency(t *testing.T) {
diff --git a/rules/manager_test.go b/rules/manager_test.go
index 51239e6c9..9865cbdfe 100644
--- a/rules/manager_test.go
+++ b/rules/manager_test.go
@@ -32,7 +32,6 @@ import (
"github.com/prometheus/common/model"
"github.com/stretchr/testify/require"
"go.uber.org/atomic"
- "go.uber.org/goleak"
"gopkg.in/yaml.v2"
"github.com/prometheus/prometheus/model/labels"
@@ -50,7 +49,7 @@ import (
)
func TestMain(m *testing.M) {
- goleak.VerifyTestMain(m)
+ prom_testutil.TolerantVerifyLeak(m)
}
func TestAlertingRule(t *testing.T) {
diff --git a/storage/remote/client.go b/storage/remote/client.go
index 17caf7be9..11e423b6a 100644
--- a/storage/remote/client.go
+++ b/storage/remote/client.go
@@ -37,6 +37,7 @@ import (
"github.com/prometheus/prometheus/config"
"github.com/prometheus/prometheus/prompb"
"github.com/prometheus/prometheus/storage/remote/azuread"
+ "github.com/prometheus/prometheus/storage/remote/googleiam"
)
const maxErrMsgLen = 1024
@@ -131,6 +132,7 @@ type ClientConfig struct {
HTTPClientConfig config_util.HTTPClientConfig
SigV4Config *sigv4.SigV4Config
AzureADConfig *azuread.AzureADConfig
+ GoogleIAMConfig *googleiam.Config
Headers map[string]string
RetryOnRateLimit bool
WriteProtoMsg config.RemoteWriteProtoMsg
@@ -192,6 +194,13 @@ func NewWriteClient(name string, conf *ClientConfig) (WriteClient, error) {
}
}
+ if conf.GoogleIAMConfig != nil {
+ t, err = googleiam.NewRoundTripper(conf.GoogleIAMConfig, t)
+ if err != nil {
+ return nil, err
+ }
+ }
+
writeProtoMsg := config.RemoteWriteProtoMsgV1
if conf.WriteProtoMsg != "" {
writeProtoMsg = conf.WriteProtoMsg
diff --git a/storage/remote/googleiam/googleiam.go b/storage/remote/googleiam/googleiam.go
new file mode 100644
index 000000000..acf3bd5a6
--- /dev/null
+++ b/storage/remote/googleiam/googleiam.go
@@ -0,0 +1,54 @@
+// Copyright 2024 The Prometheus Authors
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package googleiam provides an http.RoundTripper that attaches an Google Cloud accessToken
+// to remote write requests.
+package googleiam
+
+import (
+ "context"
+ "fmt"
+ "net/http"
+
+ "golang.org/x/oauth2/google"
+ "google.golang.org/api/option"
+ apihttp "google.golang.org/api/transport/http"
+)
+
+type Config struct {
+ CredentialsFile string `yaml:"credentials_file,omitempty"`
+}
+
+// NewRoundTripper creates a round tripper that adds Google Cloud Monitoring authorization to calls
+// using either a credentials file or the default credentials.
+func NewRoundTripper(cfg *Config, next http.RoundTripper) (http.RoundTripper, error) {
+ if next == nil {
+ next = http.DefaultTransport
+ }
+ const scopes = "https://www.googleapis.com/auth/monitoring.write"
+ ctx := context.Background()
+ opts := []option.ClientOption{
+ option.WithScopes(scopes),
+ }
+ if cfg.CredentialsFile != "" {
+ opts = append(opts, option.WithCredentialsFile(cfg.CredentialsFile))
+ } else {
+ creds, err := google.FindDefaultCredentials(ctx, scopes)
+ if err != nil {
+ return nil, fmt.Errorf("error finding default Google credentials: %w", err)
+ }
+ opts = append(opts, option.WithCredentials(creds))
+ }
+
+ return apihttp.NewTransport(ctx, next, opts...)
+}
diff --git a/storage/remote/write.go b/storage/remote/write.go
index 81902a8f1..3d2f1fdfc 100644
--- a/storage/remote/write.go
+++ b/storage/remote/write.go
@@ -176,6 +176,7 @@ func (rws *WriteStorage) ApplyConfig(conf *config.Config) error {
HTTPClientConfig: rwConf.HTTPClientConfig,
SigV4Config: rwConf.SigV4Config,
AzureADConfig: rwConf.AzureADConfig,
+ GoogleIAMConfig: rwConf.GoogleIAMConfig,
Headers: rwConf.Headers,
RetryOnRateLimit: rwConf.QueueConfig.RetryOnRateLimit,
})
diff --git a/tsdb/db_test.go b/tsdb/db_test.go
index c0edafe08..c8dad8699 100644
--- a/tsdb/db_test.go
+++ b/tsdb/db_test.go
@@ -63,7 +63,10 @@ func TestMain(m *testing.M) {
flag.Parse()
defaultIsolationDisabled = !isolationEnabled
- goleak.VerifyTestMain(m, goleak.IgnoreTopFunction("github.com/prometheus/prometheus/tsdb.(*SegmentWAL).cut.func1"), goleak.IgnoreTopFunction("github.com/prometheus/prometheus/tsdb.(*SegmentWAL).cut.func2"))
+ goleak.VerifyTestMain(m,
+ goleak.IgnoreTopFunction("github.com/prometheus/prometheus/tsdb.(*SegmentWAL).cut.func1"),
+ goleak.IgnoreTopFunction("github.com/prometheus/prometheus/tsdb.(*SegmentWAL).cut.func2"),
+ goleak.IgnoreTopFunction("go.opencensus.io/stats/view.(*worker).start"))
}
func openTestDB(t testing.TB, opts *Options, rngs []int64) (db *DB) {
From 15618157321f988e069cdaa955422b24632f5743 Mon Sep 17 00:00:00 2001
From: Callum Styan
Date: Tue, 30 Jul 2024 14:08:28 -0700
Subject: [PATCH 34/44] remote write: increase time threshold for resharding
(#14450)
Don't reshard if we haven't successfully sent a sample in the last
shardUpdateDuration seconds.
Signed-off-by: Callum Styan
Co-authored-by: kushagra Shukla
---
storage/remote/queue_manager.go | 6 +++---
storage/remote/queue_manager_test.go | 13 ++++++++-----
2 files changed, 11 insertions(+), 8 deletions(-)
diff --git a/storage/remote/queue_manager.go b/storage/remote/queue_manager.go
index 5b59288e6..17ff1850f 100644
--- a/storage/remote/queue_manager.go
+++ b/storage/remote/queue_manager.go
@@ -1109,9 +1109,9 @@ func (t *QueueManager) shouldReshard(desiredShards int) bool {
if desiredShards == t.numShards {
return false
}
- // We shouldn't reshard if Prometheus hasn't been able to send to the
- // remote endpoint successfully within some period of time.
- minSendTimestamp := time.Now().Add(-2 * time.Duration(t.cfg.BatchSendDeadline)).Unix()
+ // We shouldn't reshard if Prometheus hasn't been able to send
+ // since the last time it checked if it should reshard.
+ minSendTimestamp := time.Now().Add(-1 * shardUpdateDuration).Unix()
lsts := t.lastSendTimestamp.Load()
if lsts < minSendTimestamp {
level.Warn(t.logger).Log("msg", "Skipping resharding, last successful send was beyond threshold", "lastSendTimestamp", lsts, "minSendTimestamp", minSendTimestamp)
diff --git a/storage/remote/queue_manager_test.go b/storage/remote/queue_manager_test.go
index 7343184fc..1c06173a5 100644
--- a/storage/remote/queue_manager_test.go
+++ b/storage/remote/queue_manager_test.go
@@ -703,32 +703,35 @@ func TestShouldReshard(t *testing.T) {
startingShards int
samplesIn, samplesOut, lastSendTimestamp int64
expectedToReshard bool
+ sendDeadline model.Duration
}
cases := []testcase{
{
- // Resharding shouldn't take place if the last successful send was > batch send deadline*2 seconds ago.
+ // resharding shouldn't take place if we haven't successfully sent
+ // since the last shardUpdateDuration, even if the send deadline is very low
startingShards: 10,
samplesIn: 1000,
samplesOut: 10,
- lastSendTimestamp: time.Now().Unix() - int64(3*time.Duration(config.DefaultQueueConfig.BatchSendDeadline)/time.Second),
+ lastSendTimestamp: time.Now().Unix() - int64(shardUpdateDuration),
expectedToReshard: false,
+ sendDeadline: model.Duration(100 * time.Millisecond),
},
{
- startingShards: 5,
+ startingShards: 10,
samplesIn: 1000,
samplesOut: 10,
lastSendTimestamp: time.Now().Unix(),
expectedToReshard: true,
+ sendDeadline: config.DefaultQueueConfig.BatchSendDeadline,
},
}
for _, c := range cases {
- _, m := newTestClientAndQueueManager(t, defaultFlushDeadline, config.RemoteWriteProtoMsgV1)
+ _, m := newTestClientAndQueueManager(t, time.Duration(c.sendDeadline), config.RemoteWriteProtoMsgV1)
m.numShards = c.startingShards
m.dataIn.incr(c.samplesIn)
m.dataOut.incr(c.samplesOut)
m.lastSendTimestamp.Store(c.lastSendTimestamp)
-
m.Start()
desiredShards := m.calculateDesiredShards()
From 2880ee8e46e2c49e5155523b30b7878d7cc65ae8 Mon Sep 17 00:00:00 2001
From: Matthieu MOREL
Date: Thu, 25 Jan 2024 07:29:48 +0100
Subject: [PATCH 35/44] chore: provide OSSF security insight
Signed-off-by: Matthieu MOREL
---
README.md | 3 ++-
SECURITY-INSIGHTS.yml | 48 +++++++++++++++++++++++++++++++++++++++++++
2 files changed, 50 insertions(+), 1 deletion(-)
create mode 100644 SECURITY-INSIGHTS.yml
diff --git a/README.md b/README.md
index cd14ed2ec..df974e109 100644
--- a/README.md
+++ b/README.md
@@ -12,9 +12,10 @@ examples and guides.
[![Docker Pulls](https://img.shields.io/docker/pulls/prom/prometheus.svg?maxAge=604800)][hub]
[![Go Report Card](https://goreportcard.com/badge/github.com/prometheus/prometheus)](https://goreportcard.com/report/github.com/prometheus/prometheus)
[![CII Best Practices](https://bestpractices.coreinfrastructure.org/projects/486/badge)](https://bestpractices.coreinfrastructure.org/projects/486)
+[![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/prometheus/prometheus/badge)](https://securityscorecards.dev/viewer/?uri=github.com/prometheus/prometheus)
+[![CLOMonitor](https://img.shields.io/endpoint?url=https://clomonitor.io/api/projects/cncf/prometheus/badge)](https://clomonitor.io/projects/cncf/prometheus)
[![Gitpod ready-to-code](https://img.shields.io/badge/Gitpod-ready--to--code-blue?logo=gitpod)](https://gitpod.io/#https://github.com/prometheus/prometheus)
[![Fuzzing Status](https://oss-fuzz-build-logs.storage.googleapis.com/badges/prometheus.svg)](https://bugs.chromium.org/p/oss-fuzz/issues/list?sort=-opened&can=1&q=proj:prometheus)
-[![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/prometheus/prometheus/badge)](https://securityscorecards.dev/viewer/?uri=github.com/prometheus/prometheus)
diff --git a/SECURITY-INSIGHTS.yml b/SECURITY-INSIGHTS.yml
new file mode 100644
index 000000000..009b35621
--- /dev/null
+++ b/SECURITY-INSIGHTS.yml
@@ -0,0 +1,48 @@
+header:
+ schema-version: '1.0.0'
+ expiration-date: '2025-07-30T01:00:00.000Z'
+ last-updated: '2024-07-30'
+ last-reviewed: '2024-07-30'
+ project-url: https://github.com/prometheus/prometheus
+ changelog: https://github.com/prometheus/prometheus/blob/main/CHANGELOG.md
+ license: https://github.com/prometheus/prometheus/blob/main/LICENSE
+project-lifecycle:
+ status: active
+ bug-fixes-only: false
+ core-maintainers:
+ - https://github.com/prometheus/prometheus/blob/main/MAINTAINERS.md
+contribution-policy:
+ accepts-pull-requests: true
+ accepts-automated-pull-requests: true
+dependencies:
+ third-party-packages: true
+ dependencies-lists:
+ - https://github.com/prometheus/prometheus/blob/main/go.mod
+ - https://github.com/prometheus/prometheus/blob/main/web/ui/package.json
+ env-dependencies-policy:
+ policy-url: https://github.com/prometheus/prometheus/blob/main/CONTRIBUTING.md#dependency-management
+distribution-points:
+ - https://github.com/prometheus/prometheus/releases
+documentation:
+ - https://prometheus.io/docs/introduction/overview/
+security-contacts:
+ - type: email
+ value: prometheus-team@googlegroups.com
+security-testing:
+ - tool-type: sca
+ tool-name: Dependabot
+ tool-version: latest
+ integration:
+ ad-hoc: false
+ ci: true
+ before-release: true
+ - tool-type: sast
+ tool-name: CodeQL
+ tool-version: latest
+ integration:
+ ad-hoc: false
+ ci: true
+ before-release: true
+vulnerability-reporting:
+ accepts-vulnerability-reports: true
+ security-policy: https://github.com/prometheus/prometheus/security/policy
From 7fab72a280f139170a14e6f6a21f6396fa02899e Mon Sep 17 00:00:00 2001
From: Charles Korn
Date: Wed, 31 Jul 2024 17:53:05 +1000
Subject: [PATCH 36/44] promqltest: add support for setting counter reset hint
on histogram samples (#14537)
* promqltest: add support for setting counter reset hint on histogram samples
Signed-off-by: Charles Korn
---
docs/configuration/unit_testing_rules.md | 4 +-
promql/parser/generated_parser.y | 17 +-
promql/parser/generated_parser.y.go | 797 ++++++++++++-----------
promql/parser/lex.go | 33 +-
promql/parser/parse.go | 22 +
promql/parser/parse_test.go | 75 ++-
6 files changed, 531 insertions(+), 417 deletions(-)
diff --git a/docs/configuration/unit_testing_rules.md b/docs/configuration/unit_testing_rules.md
index 163fcb91f..7fc676a25 100644
--- a/docs/configuration/unit_testing_rules.md
+++ b/docs/configuration/unit_testing_rules.md
@@ -92,7 +92,7 @@ series:
#
# Native histogram notation:
# Native histograms can be used instead of floating point numbers using the following notation:
-# {{schema:1 sum:-0.3 count:3.1 z_bucket:7.1 z_bucket_w:0.05 buckets:[5.1 10 7] offset:-3 n_buckets:[4.1 5] n_offset:-5}}
+# {{schema:1 sum:-0.3 count:3.1 z_bucket:7.1 z_bucket_w:0.05 buckets:[5.1 10 7] offset:-3 n_buckets:[4.1 5] n_offset:-5 counter_reset_hint:gauge}}
# Native histograms support the same expanding notation as floating point numbers, i.e. 'axn', 'a+bxn' and 'a-bxn'.
# All properties are optional and default to 0. The order is not important. The following properties are supported:
# - schema (int):
@@ -119,6 +119,8 @@ series:
# Observation counts in negative buckets. Each represents an absolute count.
# - n_offset (int):
# The starting index of the first entry in the negative buckets.
+# - counter_reset_hint (one of 'unknown', 'reset', 'not_reset' or 'gauge')
+# The counter reset hint associated with this histogram. Defaults to 'unknown' if not set.
values:
```
diff --git a/promql/parser/generated_parser.y b/promql/parser/generated_parser.y
index b99e67424..b8e6aa373 100644
--- a/promql/parser/generated_parser.y
+++ b/promql/parser/generated_parser.y
@@ -84,6 +84,7 @@ NEGATIVE_BUCKETS_DESC
ZERO_BUCKET_DESC
ZERO_BUCKET_WIDTH_DESC
CUSTOM_VALUES_DESC
+COUNTER_RESET_HINT_DESC
%token histogramDescEnd
// Operators.
@@ -149,6 +150,14 @@ START
END
%token preprocessorEnd
+// Counter reset hints.
+%token counterResetHintsStart
+%token -
+UNKNOWN_COUNTER_RESET
+COUNTER_RESET
+NOT_COUNTER_RESET
+GAUGE_TYPE
+%token counterResetHintsEnd
// Start symbols for the generated parser.
%token startSymbolsStart
@@ -163,7 +172,7 @@ START_METRIC_SELECTOR
// Type definitions for grammar rules.
%type label_match_list
%type label_matcher
-%type
- aggregate_op grouping_label match_op maybe_label metric_identifier unary_op at_modifier_preprocessors string_identifier
+%type
- aggregate_op grouping_label match_op maybe_label metric_identifier unary_op at_modifier_preprocessors string_identifier counter_reset_hint
%type label_set metric
%type label_set_list
%type