Repository: open-telemetry/opentelemetry-collector-contrib
Pull request #35888: [exporter/prometheusremotewrite] feat: prom rw exporter add support for rw2
Author: login: jmichalek132
Labels: name: ready to merge
description: Code review completed; ready to merge by maintainers
name: exporter/prometheusremotewrite
name: pkg/translator/prometheus
Created: 2024-10-20T21:25:00Z | Merged: 2025-05-09T09:15:21Z

<!--Ex. Fixing a bug - Describe the bug and how this fixes the issue.
Ex. Adding a feature - Explain what this achieves.-->
#### Description

Draft PR for adding rw2 support in the prometheus remote write exporter.
Very much a draft, not full implementation of the spec with a lot of code duplication and no tests WIP.

TODO:

- [x] changelog entry
- [x] feature flag on top of config options
- [x] rip out batching for now
- [x] enum instead of bool for RW2
- [x] update exporter readme
- [x] Validate supported enum value set
- [x] Check for TODOs 
- [x] unit tests
- [x] going over the spec and making changes to be rw2 compliant -> follow up PR
- [x] try to reduce duplicate code

<!-- Issue number (e.g. #1234) or full URL to issue, if applicable. -->
#### Link to tracking issue #33661
Fixes

<!--Describe what testing was performed and which tests were added.-->
#### Testing

<!--Describe the documentation added.-->
#### Documentation

<!--Please delete paragraphs that you did not use before submitting.-->



Diff:
diff --git a/.chloggen/jm-prom-rw-exporter-add-support-for-rw2.yaml b/.chloggen/jm-prom-rw-exporter-add-support-for-rw2.yaml
new file mode 100644
index 0000000000000..dbd2e5c7a29c7
--- /dev/null
+++ b/.chloggen/jm-prom-rw-exporter-add-support-for-rw2.yaml
@@ -0,0 +1,27 @@
+# Use this changelog template to create an entry for release notes.
+
+# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix'
+change_type: enhancement
+
+# The name of the component, or a single word describing the area of concern, (e.g. filelogreceiver)
+component: prometheusremotewriteexporter
+
+# A brief description of the change.  Surround your text with quotes ("") if it needs to start with a backtick (`).
+note: Add `exporter.prometheusremotewritexporter.enableSendingRW2` feature gate and configuration to the exporter to send Prometheus remote write 2.0 version.
+
+# Mandatory: One or more tracking issues related to the change. You can use the PR number here if no issue exists.
+issues: [33661]
+
+# (Optional) One or more lines of additional information to render under the primary note.
+# These lines will be padded with 2 spaces and then inserted directly into the document.
+# Use pipe (|) for multiline entries.
+subtext: WARNING! PRW 2.0 support for the exporter is still under development and not ready for usage.
+
+# If your change doesn't affect end users or the exported elements of any package,
+# you should instead start your pull request title with [chore] or use the "Skip Changelog" label.
+# Optional: The change log or logs in which this entry should be included.
+# e.g. '[user]' or '[user, api]'
+# Include 'user' if the change is relevant to end users.
+# Include 'api' if there is a change to a library API.
+# Default: '[user]'
+change_logs: [user, api]
diff --git a/exporter/prometheusremotewriteexporter/README.md b/exporter/prometheusremotewriteexporter/README.md
index c5c38d06587a0..b979f56cf0d20 100644
--- a/exporter/prometheusremotewriteexporter/README.md
+++ b/exporter/prometheusremotewriteexporter/README.md
@@ -51,7 +51,7 @@ The following settings can be optionally configured:
   - *Note the following headers cannot be changed: `Content-Encoding`, `Content-Type`, `X-Prometheus-Remote-Write-Version`, and `User-Agent`.*
 - `namespace`: prefix attached to each exported metric name.
 - `add_metric_suffixes`: If set to false, type and unit suffixes will not be added to metrics. Default: true.
-- `send_metadata`: If set to true, prometheus metadata will be generated and sent. Default: false.
+- `send_metadata`: If set to true, prometheus metadata will be generated and sent. Default: false. This option is ignored when using PRW 2.0, which always includes metadata.
 - `remote_write_queue`: fine tuning for queueing and sending of the outgoing remote writes.
   - `enabled`: enable the sending queue (default: `true`)
   - `queue_size`: number of OTLP metrics that can be queued. Ignored if `enabled` is `false` (default: `10000`)
@@ -64,6 +64,11 @@ The following settings can be optionally configured:
   samples to be sent to the remote write endpoint. If the batch size is larger
   than this value, it will be split into multiple batches.
 - `max_batch_request_parallelism` (default = `5`): Maximum parallelism allowed for a single request bigger than `max_batch_size_bytes`.
+- `protobuf_message` (default = `prometheus.WriteRequest`): 
+  - Protobuf message to use when writing to the remote write endpoint. This option is ignored unless the `exporter.prometheusremotewritexporter.enableSendingRW2` feature gate is enabled.
+  - `prometheus.WriteRequest` is the message used in [Remote Write 1.0](https://prometheus.io/docs/specs/remote_write_spec/).
+  - `io.prometheus.write.v2.Request` is the message used in [Remote Write 2.0](https://prometheus.io/docs/specs/remote_write_spec_2_0/). It is more efficient, always includes metadata, and adds support for the created timestamp and native histograms. Your remote storage provider must support PRW 2.0 to be able to use this message. PRW 2.0 support is currently **In Development** and is only partially implemented, thus, not ready for usage.
+
 
 Example:
 
diff --git a/exporter/prometheusremotewriteexporter/config.go b/exporter/prometheusremotewriteexporter/config.go
index fad52fa27c703..4b25d658dca65 100644
--- a/exporter/prometheusremotewriteexporter/config.go
+++ b/exporter/prometheusremotewriteexporter/config.go
@@ -5,7 +5,9 @@ package prometheusremotewriteexporter // import "github.com/open-telemetry/opent
 
 import (
 	"errors"
+	"fmt"
 
+	"github.com/prometheus/prometheus/config"
 	"go.opentelemetry.io/collector/component"
 	"go.opentelemetry.io/collector/config/confighttp"
 	"go.opentelemetry.io/collector/config/configretry"
@@ -50,8 +52,11 @@ type Config struct {
 	// AddMetricSuffixes controls whether unit and type suffixes are added to metrics on export
 	AddMetricSuffixes bool `mapstructure:"add_metric_suffixes"`
 
-	// SendMetadata controls whether prometheus metadata will be generated and sent
+	// SendMetadata controls whether prometheus metadata will be generated and sent, this option is ignored when using PRW 2.0, which always includes metadata.
 	SendMetadata bool `mapstructure:"send_metadata"`
+
+	// RemoteWriteProtoMsg controls whether prometheus remote write v1 or v2 is sent.
+	RemoteWriteProtoMsg config.RemoteWriteProtoMsg `mapstructure:"protobuf_message,omitempty"`
 }
 
 type CreatedMetric struct {
@@ -118,5 +123,14 @@ func (cfg *Config) Validate() error {
 		return errors.New("compression type must be snappy")
 	}
 
+	err := cfg.RemoteWriteProtoMsg.Validate()
+	if err != nil {
+		return err
+	}
+
+	if !enableSendingRW2FeatureGate.IsEnabled() && cfg.RemoteWriteProtoMsg == config.RemoteWriteProtoMsgV2 {
+		return fmt.Errorf("remote write v2 is only supported with the feature gate %s", enableSendingRW2FeatureGate.ID())
+	}
+
 	return nil
 }
diff --git a/exporter/prometheusremotewriteexporter/config_test.go b/exporter/prometheusremotewriteexporter/config_test.go
index e2a54d26fe2de..bdcf5e4508dd2 100644
--- a/exporter/prometheusremotewriteexporter/config_test.go
+++ b/exporter/prometheusremotewriteexporter/config_test.go
@@ -9,6 +9,7 @@ import (
 	"time"
 
 	"github.com/cenkalti/backoff/v4"
+	"github.com/prometheus/prometheus/config"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 	"go.opentelemetry.io/collector/component"
@@ -81,6 +82,7 @@ func TestLoadConfig(t *testing.T) {
 				TargetInfo: &TargetInfo{
 					Enabled: true,
 				},
+				RemoteWriteProtoMsg: config.RemoteWriteProtoMsgV1,
 			},
 		},
 		{
diff --git a/exporter/prometheusremotewriteexporter/exporter.go b/exporter/prometheusremotewriteexporter/exporter.go
index 81b9bb3b9b354..1a9b9a31f8987 100644
--- a/exporter/prometheusremotewriteexporter/exporter.go
+++ b/exporter/prometheusremotewriteexporter/exporter.go
@@ -18,6 +18,7 @@ import (
 	"github.com/cenkalti/backoff/v4"
 	"github.com/gogo/protobuf/proto"
 	"github.com/golang/snappy"
+	"github.com/prometheus/prometheus/config"
 	"github.com/prometheus/prometheus/prompb"
 	"go.opentelemetry.io/collector/component"
 	"go.opentelemetry.io/collector/config/confighttp"
@@ -80,20 +81,21 @@ var bufferPool = sync.Pool{
 
 // prwExporter converts OTLP metrics to Prometheus remote write TimeSeries and sends them to a remote endpoint.
 type prwExporter struct {
-	endpointURL       *url.URL
-	client            *http.Client
-	wg                *sync.WaitGroup
-	closeChan         chan struct{}
-	concurrency       int
-	userAgentHeader   string
-	maxBatchSizeBytes int
-	clientSettings    *confighttp.ClientConfig
-	settings          component.TelemetrySettings
-	retrySettings     configretry.BackOffConfig
-	retryOnHTTP429    bool
-	wal               *prweWAL
-	exporterSettings  prometheusremotewrite.Settings
-	telemetry         prwTelemetry
+	endpointURL         *url.URL
+	client              *http.Client
+	wg                  *sync.WaitGroup
+	closeChan           chan struct{}
+	concurrency         int
+	userAgentHeader     string
+	maxBatchSizeBytes   int
+	clientSettings      *confighttp.ClientConfig
+	settings            component.TelemetrySettings
+	retrySettings       configretry.BackOffConfig
+	retryOnHTTP429      bool
+	wal                 *prweWAL
+	exporterSettings    prometheusremotewrite.Settings
+	telemetry           prwTelemetry
+	RemoteWriteProtoMsg config.RemoteWriteProtoMsg
 
 	// When concurrency is enabled, concurrent goroutines would potentially
 	// fight over the same batchState object. To avoid this, we use a pool
@@ -133,6 +135,10 @@ func newPRWExporter(cfg *Config, set exporter.Settings) (*prwExporter, error) {
 		return nil, err
 	}
 
+	if err := config.RemoteWriteProtoMsg.Validate(cfg.RemoteWriteProtoMsg); err != nil {
+		return nil, err
+	}
+
 	userAgentHeader := fmt.Sprintf("%s/%s", strings.ReplaceAll(strings.ToLower(set.BuildInfo.Description), " ", "-"), set.BuildInfo.Version)
 
 	concurrency := 5
@@ -147,16 +153,17 @@ func newPRWExporter(cfg *Config, set exporter.Settings) (*prwExporter, error) {
 	telemetry.setNumberConsumer(context.Background(), int64(concurrency))
 
 	prwe := &prwExporter{
-		endpointURL:       endpointURL,
-		wg:                new(sync.WaitGroup),
-		closeChan:         make(chan struct{}),
-		userAgentHeader:   userAgentHeader,
-		maxBatchSizeBytes: cfg.MaxBatchSizeBytes,
-		concurrency:       concurrency,
-		clientSettings:    &cfg.ClientConfig,
-		settings:          set.TelemetrySettings,
-		retrySettings:     cfg.BackOffConfig,
-		retryOnHTTP429:    retryOn429FeatureGate.IsEnabled(),
+		endpointURL:         endpointURL,
+		wg:                  new(sync.WaitGroup),
+		closeChan:           make(chan struct{}),
+		userAgentHeader:     userAgentHeader,
+		maxBatchSizeBytes:   cfg.MaxBatchSizeBytes,
+		concurrency:         cfg.RemoteWriteQueue.NumConsumers,
+		clientSettings:      &cfg.ClientConfig,
+		settings:            set.TelemetrySettings,
+		retrySettings:       cfg.BackOffConfig,
+		retryOnHTTP429:      retryOn429FeatureGate.IsEnabled(),
+		RemoteWriteProtoMsg: cfg.RemoteWriteProtoMsg,
 		exporterSettings: prometheusremotewrite.Settings{
 			Namespace:         cfg.Namespace,
 			ExternalLabels:    sanitizedLabels,
@@ -168,6 +175,8 @@ func newPRWExporter(cfg *Config, set exporter.Settings) (*prwExporter, error) {
 		batchStatePool: sync.Pool{New: func() any { return newBatchTimeServicesState() }},
 	}
 
+	prwe.settings.Logger.Info("starting prometheus remote write exporter", zap.Any("ProtoMsg", cfg.RemoteWriteProtoMsg))
+
 	prwe.wal = newWAL(cfg.WAL, prwe.export)
 	return prwe, nil
 }
@@ -201,6 +210,23 @@ func (prwe *prwExporter) Shutdown(context.Context) error {
 	return err
 }
 
+func (prwe *prwExporter) pushMetricsV1(ctx context.Context, md pmetric.Metrics) error {
+	tsMap, err := prometheusremotewrite.FromMetrics(md, prwe.exporterSettings)
+
+	prwe.telemetry.recordTranslatedTimeSeries(ctx, len(tsMap))
+
+	var m []*prompb.MetricMetadata
+	if prwe.exporterSettings.SendMetadata {
+		m = prometheusremotewrite.OtelMetricsToMetadata(md, prwe.exporterSettings.AddMetricSuffixes)
+	}
+	if err != nil {
+		prwe.telemetry.recordTranslationFailure(ctx)
+		prwe.settings.Logger.Debug("failed to translate metrics, exporting remaining metrics", zap.Error(err), zap.Int("translated", len(tsMap)))
+	}
+	// Call export even if a conversion error, since there may be points that were successfully converted.
+	return prwe.handleExport(ctx, tsMap, m)
+}
+
 // PushMetrics converts metrics to Prometheus remote write TimeSeries and send to remote endpoint. It maintain a map of
 // TimeSeries, validates and handles each individual metric, adding the converted TimeSeries to the map, and finally
 // exports the map.
@@ -213,21 +239,21 @@ func (prwe *prwExporter) PushMetrics(ctx context.Context, md pmetric.Metrics) er
 		return errors.New("shutdown has been called")
 	default:
 
-		tsMap, err := prometheusremotewrite.FromMetrics(md, prwe.exporterSettings)
-		if err != nil {
-			prwe.telemetry.recordTranslationFailure(ctx)
-			prwe.settings.Logger.Debug("failed to translate metrics, exporting remaining metrics", zap.Error(err), zap.Int("translated", len(tsMap)))
+		// If feature flag not enabled support only RW1.
+		if !enableSendingRW2FeatureGate.IsEnabled() {
+			return prwe.pushMetricsV1(ctx, md)
 		}
 
-		prwe.telemetry.recordTranslatedTimeSeries(ctx, len(tsMap))
+		// If feature flag was enabled check if we want to send RW1 or RW2.
+		switch prwe.RemoteWriteProtoMsg {
+		case config.RemoteWriteProtoMsgV1:
+			return prwe.pushMetricsV1(ctx, md)
+		case config.RemoteWriteProtoMsgV2:
+			return prwe.pushMetricsV2(ctx, md)
 
-		var m []*prompb.MetricMetadata
-		if prwe.exporterSettings.SendMetadata {
-			m = prometheusremotewrite.OtelMetricsToMetadata(md, prwe.exporterSettings.AddMetricSuffixes)
+		default:
+			return fmt.Errorf("unsupported remote-write protobuf message: %v", prwe.RemoteWriteProtoMsg)
 		}
-
-		// Call export even if a conversion error, since there may be points that were successfully converted.
-		return prwe.handleExport(ctx, tsMap, m)
 	}
 }
 
@@ -298,7 +324,19 @@ func (prwe *prwExporter) export(ctx context.Context, requests []*prompb.WriteReq
 					if !ok {
 						return
 					}
-					if errExecute := prwe.execute(ctx, request); errExecute != nil {
+
+					buf := bufferPool.Get().(*buffer)
+					buf.protobuf.Reset()
+					defer bufferPool.Put(buf)
+
+					if errMarshal := buf.protobuf.Marshal(request); errMarshal != nil {
+						mu.Lock()
+						errs = multierr.Append(errs, consumererror.NewPermanent(errMarshal))
+						mu.Unlock()
+						return
+					}
+
+					if errExecute := prwe.execute(ctx, buf); errExecute != nil {
 						mu.Lock()
 						errs = multierr.Append(errs, consumererror.NewPermanent(errExecute))
 						mu.Unlock()
@@ -312,16 +350,7 @@ func (prwe *prwExporter) export(ctx context.Context, requests []*prompb.WriteReq
 	return errs
 }
 
-func (prwe *prwExporter) execute(ctx context.Context, writeReq *prompb.WriteRequest) error {
-	buf := bufferPool.Get().(*buffer)
-	buf.protobuf.Reset()
-	defer bufferPool.Put(buf)
-
-	// Uses proto.Marshal to convert the WriteRequest into bytes array
-	errMarshal := buf.protobuf.Marshal(writeReq)
-	if errMarshal != nil {
-		return consumererror.NewPermanent(errMarshal)
-	}
+func (prwe *prwExporter) execute(ctx context.Context, buf *buffer) error {
 	// If we don't pass a buffer large enough, Snappy Encode function will not use it and instead will allocate a new buffer.
 	// Manually grow the buffer to make sure Snappy uses it and we can re-use it afterwards.
 	maxCompressedLen := snappy.MaxEncodedLen(len(buf.protobuf.Bytes()))
@@ -354,10 +383,20 @@ func (prwe *prwExporter) execute(ctx context.Context, writeReq *prompb.WriteRequ
 		// Add necessary headers specified by:
 		// https://cortexmetrics.io/docs/apis/#remote-api
 		req.Header.Add("Content-Encoding", "snappy")
-		req.Header.Set("Content-Type", "application/x-protobuf")
-		req.Header.Set("X-Prometheus-Remote-Write-Version", "0.1.0")
 		req.Header.Set("User-Agent", prwe.userAgentHeader)
 
+		switch {
+		// If feature flag not enabled support only RW1
+		case !enableSendingRW2FeatureGate.IsEnabled(), prwe.RemoteWriteProtoMsg == config.RemoteWriteProtoMsgV1:
+			req.Header.Set("Content-Type", "application/x-protobuf")
+			req.Header.Set("X-Prometheus-Remote-Write-Version", "0.1.0")
+		case prwe.RemoteWriteProtoMsg == config.RemoteWriteProtoMsgV2:
+			req.Header.Set("Content-Type", "application/x-protobuf;proto=io.prometheus.write.v2.Request")
+			req.Header.Set("X-Prometheus-Remote-Write-Version", "2.0.0")
+		default:
+			return fmt.Errorf("unsupported remote-write protobuf message: %v (should be validated earlier)", prwe.RemoteWriteProtoMsg)
+		}
+
 		resp, err := prwe.client.Do(req)
 		prwe.telemetry.recordRemoteWriteSentBatch(ctx)
 		if err != nil {
diff --git a/exporter/prometheusremotewriteexporter/exporter_concurrency_test.go b/exporter/prometheusremotewriteexporter/exporter_concurrency_test.go
index 3da5ce69abe0f..b2f4cf1645e5e 100644
--- a/exporter/prometheusremotewriteexporter/exporter_concurrency_test.go
+++ b/exporter/prometheusremotewriteexporter/exporter_concurrency_test.go
@@ -16,6 +16,7 @@ import (
 
 	"github.com/gogo/protobuf/proto"
 	"github.com/golang/snappy"
+	"github.com/prometheus/prometheus/config"
 	"github.com/prometheus/prometheus/prompb"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
@@ -109,7 +110,8 @@ func Test_PushMetricsConcurrent(t *testing.T) {
 		TargetInfo: &TargetInfo{
 			Enabled: true,
 		},
-		BackOffConfig: retrySettings,
+		BackOffConfig:       retrySettings,
+		RemoteWriteProtoMsg: config.RemoteWriteProtoMsgV1,
 	}
 
 	assert.NotNil(t, cfg)
diff --git a/exporter/prometheusremotewriteexporter/exporter_test.go b/exporter/prometheusremotewriteexporter/exporter_test.go
index 2e376b293f0d5..62cf5511818b2 100644
--- a/exporter/prometheusremotewriteexporter/exporter_test.go
+++ b/exporter/prometheusremotewriteexporter/exporter_test.go
@@ -19,8 +19,10 @@ import (
 
 	"github.com/gogo/protobuf/proto"
 	"github.com/golang/snappy"
+	"github.com/prometheus/prometheus/config"
 	"github.com/prometheus/prometheus/model/value"
 	"github.com/prometheus/prometheus/prompb"
+	writev2 "github.com/prometheus/prometheus/prompb/io/prometheus/write/v2"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 	"go.opentelemetry.io/collector/component"
@@ -42,6 +44,7 @@ import (
 
 	"github.com/open-telemetry/opentelemetry-collector-contrib/exporter/prometheusremotewriteexporter/internal/metadata"
 	"github.com/open-telemetry/opentelemetry-collector-contrib/exporter/prometheusremotewriteexporter/internal/metadatatest"
+	"github.com/open-telemetry/opentelemetry-collector-contrib/internal/common/testutil"
 	"github.com/open-telemetry/opentelemetry-collector-contrib/internal/coreinternal/testdata"
 )
 
@@ -120,6 +123,7 @@ func Test_NewPRWExporter(t *testing.T) {
 			cfg.ExternalLabels = tt.externalLabels
 			cfg.Namespace = tt.namespace
 			cfg.RemoteWriteQueue.NumConsumers = 1
+			cfg.RemoteWriteProtoMsg = config.RemoteWriteProtoMsgV1
 			prwe, err := newPRWExporter(cfg, tt.set)
 
 			if tt.returnErrorOnCreate {
@@ -209,6 +213,7 @@ func Test_Start(t *testing.T) {
 			cfg.Namespace = tt.namespace
 			cfg.RemoteWriteQueue.NumConsumers = 1
 			cfg.ClientConfig = tt.clientSettings
+			cfg.RemoteWriteProtoMsg = config.RemoteWriteProtoMsgV1
 
 			prwe, err := newPRWExporter(cfg, tt.set)
 			assert.NoError(t, err)
@@ -448,36 +453,50 @@ func Test_PushMetrics(t *testing.T) {
 
 	staleNaNSumBatch := getMetricsFromMetricList(staleNaNMetrics[staleNaNSum])
 
-	checkFunc := func(t *testing.T, r *http.Request, expected int, isStaleMarker bool) {
+	checkFunc := func(t *testing.T, r *http.Request, expected int, isStaleMarker bool, enableSendingRW2 bool) {
 		body, err := io.ReadAll(r.Body)
 		require.NoError(t, err)
 
 		buf := make([]byte, len(body))
 		dest, err := snappy.Decode(buf, body)
-		assert.Equal(t, "0.1.0", r.Header.Get("x-prometheus-remote-write-version"))
+		if enableSendingRW2 {
+			assert.Equal(t, "2.0.0", r.Header.Get("x-prometheus-remote-write-version"))
+			assert.Equal(t, "application/x-protobuf;proto=io.prometheus.write.v2.Request", r.Header.Get("Content-Type"))
+		} else {
+			assert.Equal(t, "0.1.0", r.Header.Get("x-prometheus-remote-write-version"))
+		}
 		assert.Equal(t, "snappy", r.Header.Get("content-encoding"))
 		assert.Equal(t, "opentelemetry-collector/1.0", r.Header.Get("User-Agent"))
 		assert.NotNil(t, r.Header.Get("tenant-id"))
 		require.NoError(t, err)
-		wr := &prompb.WriteRequest{}
-		ok := proto.Unmarshal(dest, wr)
-		require.NoError(t, ok)
-		assert.Len(t, wr.Timeseries, expected)
-		if isStaleMarker {
-			assert.True(t, value.IsStaleNaN(wr.Timeseries[0].Samples[0].Value))
+		if enableSendingRW2 {
+			wr := &writev2.Request{}
+			ok := proto.Unmarshal(dest, wr)
+			require.NoError(t, ok)
+			assert.Len(t, wr.Timeseries, expected)
+			// TODO check labels
+		} else {
+			wr := &prompb.WriteRequest{}
+			ok := proto.Unmarshal(dest, wr)
+			require.NoError(t, ok)
+			assert.Len(t, wr.Timeseries, expected)
+			if isStaleMarker {
+				assert.True(t, value.IsStaleNaN(wr.Timeseries[0].Samples[0].Value))
+			}
 		}
 	}
 
 	tests := []struct {
 		name                       string
 		metrics                    pmetric.Metrics
-		reqTestFunc                func(t *testing.T, r *http.Request, expected int, isStaleMarker bool)
+		reqTestFunc                func(t *testing.T, r *http.Request, expected int, isStaleMarker bool, enableSendingRW2 bool)
 		expectedTimeSeries         int
 		httpResponseCode           int
 		returnErr                  bool
 		isStaleMarker              bool
 		skipForWAL                 bool
 		expectedFailedTranslations int
+		enableSendingRW2           bool
 	}{
 		{
 			name:                       "invalid_type_case",
@@ -508,6 +527,14 @@ func Test_PushMetrics(t *testing.T) {
 			expectedTimeSeries: 2,
 			httpResponseCode:   http.StatusAccepted,
 		},
+		{
+			name:               "doubleGauge_case_rw2",
+			metrics:            doubleGaugeBatch,
+			reqTestFunc:        checkFunc,
+			expectedTimeSeries: 2,
+			httpResponseCode:   http.StatusAccepted,
+			enableSendingRW2:   true,
+		},
 		{
 			name:               "intGauge_case",
 			metrics:            intGaugeBatch,
@@ -682,14 +709,15 @@ func Test_PushMetrics(t *testing.T) {
 		}
 		t.Run(name, func(t *testing.T) {
 			for _, tt := range tests {
-				if useWAL && tt.skipForWAL {
+				// skip WAL for rw2 cases as rw2 doesn't currently support WAL
+				if useWAL && (tt.skipForWAL || tt.enableSendingRW2) {
 					t.Skip("test not supported when using WAL")
 				}
 				t.Run(tt.name, func(t *testing.T) {
 					t.Parallel()
 					server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 						if tt.reqTestFunc != nil {
-							tt.reqTestFunc(t, r, tt.expectedTimeSeries, tt.isStaleMarker)
+							tt.reqTestFunc(t, r, tt.expectedTimeSeries, tt.isStaleMarker, tt.enableSendingRW2)
 						}
 						w.WriteHeader(tt.httpResponseCode)
 					}))
@@ -718,6 +746,15 @@ func Test_PushMetrics(t *testing.T) {
 						BackOffConfig: retrySettings,
 					}
 
+					if tt.enableSendingRW2 {
+						cfg.RemoteWriteProtoMsg = config.RemoteWriteProtoMsgV2
+						oldValue := enableSendingRW2FeatureGate.IsEnabled()
+						testutil.SetFeatureGateForTest(t, enableSendingRW2FeatureGate, tt.enableSendingRW2)
+						defer testutil.SetFeatureGateForTest(t, enableSendingRW2FeatureGate, oldValue)
+					} else {
+						cfg.RemoteWriteProtoMsg = config.RemoteWriteProtoMsgV1
+					}
+
 					if useWAL {
 						cfg.WAL = &WALConfig{
 							Directory: t.TempDir(),
@@ -1156,8 +1193,17 @@ func TestRetries(t *testing.T) {
 				},
 				telemetry: telemetry,
 			}
+			buf := bufferPool.Get().(*buffer)
+			buf.protobuf.Reset()
+			defer bufferPool.Put(buf)
+
+			errMarshal := buf.protobuf.Marshal(&prompb.WriteRequest{})
+			if errMarshal != nil {
+				require.NoError(t, errMarshal)
+				return
+			}
 
-			err = exporter.execute(tt.ctx, &prompb.WriteRequest{})
+			err = exporter.execute(tt.ctx, buf)
 			tt.assertError(t, err)
 			tt.assertErrorType(t, err)
 			assert.Equal(t, tt.expectedAttempts, totalAttempts)
@@ -1251,7 +1297,16 @@ func benchmarkExecute(b *testing.B, numSample int) {
 	b.ReportAllocs()
 	b.ResetTimer()
 	for _, req := range reqs {
-		err := exporter.execute(ctx, req)
+		buf := bufferPool.Get().(*buffer)
+		buf.protobuf.Reset()
+		defer bufferPool.Put(buf)
+
+		errMarshal := buf.protobuf.Marshal(req)
+		if errMarshal != nil {
+			require.NoError(b, errMarshal)
+			return
+		}
+		err := exporter.execute(ctx, buf)
 		require.NoError(b, err)
 	}
 }
diff --git a/exporter/prometheusremotewriteexporter/exporter_v2.go b/exporter/prometheusremotewriteexporter/exporter_v2.go
new file mode 100644
index 0000000000000..b056471982be8
--- /dev/null
+++ b/exporter/prometheusremotewriteexporter/exporter_v2.go
@@ -0,0 +1,118 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+package prometheusremotewriteexporter // import "github.com/open-telemetry/opentelemetry-collector-contrib/exporter/prometheusremotewriteexporter"
+
+import (
+	"context"
+	"math"
+	"sync"
+
+	writev2 "github.com/prometheus/prometheus/prompb/io/prometheus/write/v2"
+	"go.opentelemetry.io/collector/consumer/consumererror"
+	"go.opentelemetry.io/collector/pdata/pmetric"
+	"go.uber.org/multierr"
+	"go.uber.org/zap"
+
+	"github.com/open-telemetry/opentelemetry-collector-contrib/pkg/translator/prometheusremotewrite"
+)
+
+func (prwe *prwExporter) pushMetricsV2(ctx context.Context, md pmetric.Metrics) error {
+	tsMap, symbolsTable, err := prometheusremotewrite.FromMetricsV2(md, prwe.exporterSettings)
+
+	prwe.telemetry.recordTranslatedTimeSeries(ctx, len(tsMap))
+
+	if err != nil {
+		prwe.telemetry.recordTranslationFailure(ctx)
+		prwe.settings.Logger.Debug("failed to translate metrics, exporting remaining metrics", zap.Error(err), zap.Int("translated", len(tsMap)))
+	}
+	// Call export even if a conversion error, since there may be points that were successfully converted.
+	return prwe.handleExportV2(ctx, symbolsTable, tsMap)
+}
+
+// exportV2 sends a Snappy-compressed writev2.Request containing writev2.TimeSeries to a remote write endpoint.
+func (prwe *prwExporter) exportV2(ctx context.Context, requests []*writev2.Request) error {
+	input := make(chan *writev2.Request, len(requests))
+	for _, request := range requests {
+		input <- request
+	}
+	close(input)
+
+	var wg sync.WaitGroup
+
+	concurrencyLimit := int(math.Min(float64(prwe.concurrency), float64(len(requests))))
+	wg.Add(concurrencyLimit) // used to wait for workers to be finished
+
+	var mu sync.Mutex
+	var errs error
+	// Run concurrencyLimit of workers until there
+	// is no more requests to execute in the input channel.
+	for i := 0; i < concurrencyLimit; i++ {
+		go func() {
+			defer wg.Done()
+			for {
+				select {
+				case <-ctx.Done(): // Check firstly to ensure that the context wasn't cancelled.
+					return
+
+				case request, ok := <-input:
+					if !ok {
+						return
+					}
+
+					buf := bufferPool.Get().(*buffer)
+					buf.protobuf.Reset()
+
+					errMarshal := buf.protobuf.Marshal(request)
+					if errMarshal != nil {
+						mu.Lock()
+						errs = multierr.Append(errs, consumererror.NewPermanent(errMarshal))
+						mu.Unlock()
+						bufferPool.Put(buf)
+						return
+					}
+
+					if errExecute := prwe.execute(ctx, buf); errExecute != nil {
+						mu.Lock()
+						errs = multierr.Append(errs, consumererror.NewPermanent(errExecute))
+						mu.Unlock()
+					}
+					bufferPool.Put(buf)
+				}
+			}
+		}()
+	}
+	wg.Wait()
+
+	return errs
+}
+
+func (prwe *prwExporter) handleExportV2(ctx context.Context, symbolsTable writev2.SymbolsTable, tsMap map[string]*writev2.TimeSeries) error {
+	// There are no metrics to export, so return.
+	if len(tsMap) == 0 {
+		return nil
+	}
+
+	// TODO implement batching
+	// TODO how do we handle symbolsTable with batching?
+	requests := make([]*writev2.Request, 0)
+	tsArray := make([]writev2.TimeSeries, 0, len(tsMap))
+	for _, v := range tsMap {
+		tsArray = append(tsArray, *v)
+	}
+
+	requests = append(requests, &writev2.Request{
+		// TODO sort
+		// Prometheus requires time series to be sorted by Timestamp to avoid out of order problems.
+		// See:
+		// * https://github.com/open-telemetry/wg-prometheus/issues/10
+		// * https://github.com/open-telemetry/opentelemetry-collector/issues/2315
+		// Timeseries: orderBySampleTimestamp(tsArray),
+		Timeseries: tsArray,
+		Symbols:    symbolsTable.Symbols(),
+	})
+
+	// TODO implement WAl support, can be done after #15277 is fixed
+
+	return prwe.exportV2(ctx, requests)
+}
diff --git a/exporter/prometheusremotewriteexporter/factory.go b/exporter/prometheusremotewriteexporter/factory.go
index eb1af2f21923d..e5c91b0cd6260 100644
--- a/exporter/prometheusremotewriteexporter/factory.go
+++ b/exporter/prometheusremotewriteexporter/factory.go
@@ -8,6 +8,7 @@ import (
 	"errors"
 	"time"
 
+	"github.com/prometheus/prometheus/config"
 	"go.opentelemetry.io/collector/component"
 	"go.opentelemetry.io/collector/config/confighttp"
 	"go.opentelemetry.io/collector/config/configretry"
@@ -33,6 +34,13 @@ var enableMultipleWorkersFeatureGate = featuregate.GlobalRegistry().MustRegister
 		" spawn multiple workers/goroutines to handle incoming metrics batches concurrently"),
 )
 
+var enableSendingRW2FeatureGate = featuregate.GlobalRegistry().MustRegister(
+	"exporter.prometheusremotewritexporter.enableSendingRW2",
+	featuregate.StageAlpha,
+	featuregate.WithRegisterFromVersion("v0.125.0"),
+	featuregate.WithRegisterDescription("When enabled, the Prometheus remote write exporter will support sending rw2. Extra configuration is still required besides enabling this feature gate."),
+)
+
 // NewFactory creates a new Prometheus Remote Write exporter.
 func NewFactory() exporter.Factory {
 	return exporter.NewFactory(
@@ -103,11 +111,12 @@ func createDefaultConfig() component.Config {
 		MaxBatchSizeBytes: 3000000,
 		// To set this as default once `exporter.prometheusremotewritexporter.EnableMultipleWorkers` is removed
 		// MaxBatchRequestParallelism: 5,
-		TimeoutSettings:   exporterhelper.NewDefaultTimeoutConfig(),
-		BackOffConfig:     retrySettings,
-		AddMetricSuffixes: true,
-		SendMetadata:      false,
-		ClientConfig:      clientConfig,
+		TimeoutSettings:     exporterhelper.NewDefaultTimeoutConfig(),
+		BackOffConfig:       retrySettings,
+		AddMetricSuffixes:   true,
+		SendMetadata:        false,
+		RemoteWriteProtoMsg: config.RemoteWriteProtoMsgV1,
+		ClientConfig:        clientConfig,
 		// TODO(jbd): Adjust the default queue size.
 		RemoteWriteQueue: RemoteWriteQueue{
 			Enabled:      true,
diff --git a/exporter/prometheusremotewriteexporter/generated_package_test.go b/exporter/prometheusremotewriteexporter/generated_package_test.go
index aa7545f2a7467..f8fc9b4530b7c 100644
--- a/exporter/prometheusremotewriteexporter/generated_package_test.go
+++ b/exporter/prometheusremotewriteexporter/generated_package_test.go
@@ -9,5 +9,5 @@ import (
 )
 
 func TestMain(m *testing.M) {
-	goleak.VerifyTestMain(m)
+	goleak.VerifyTestMain(m, goleak.IgnoreTopFunction("go.opencensus.io/stats/view.(*worker).start"))
 }
diff --git a/exporter/prometheusremotewriteexporter/go.mod b/exporter/prometheusremotewriteexporter/go.mod
index 2d73973ae925b..2711c62e9f837 100644
--- a/exporter/prometheusremotewriteexporter/go.mod
+++ b/exporter/prometheusremotewriteexporter/go.mod
@@ -6,6 +6,7 @@ require (
 	github.com/cenkalti/backoff/v4 v4.3.0
 	github.com/gogo/protobuf v1.3.2
 	github.com/golang/snappy v1.0.0
+	github.com/open-telemetry/opentelemetry-collector-contrib/internal/common v0.125.0
 	github.com/open-telemetry/opentelemetry-collector-contrib/internal/coreinternal v0.125.0
 	github.com/open-telemetry/opentelemetry-collector-contrib/pkg/resourcetotelemetry v0.125.0
 	github.com/open-telemetry/opentelemetry-collector-contrib/pkg/translator/prometheus v0.125.0
@@ -40,6 +41,16 @@ require (
 require github.com/fsnotify/fsnotify v1.9.0 // indirect
 
 require (
+	cloud.google.com/go/auth v0.9.5 // indirect
+	cloud.google.com/go/auth/oauth2adapt v0.2.4 // indirect
+	cloud.google.com/go/compute/metadata v0.6.0 // indirect
+	github.com/Azure/azure-sdk-for-go/sdk/azcore v1.14.0 // indirect
+	github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.7.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/alecthomas/units v0.0.0-20240626203959-61d1e3462e30 // indirect
+	github.com/aws/aws-sdk-go v1.55.5 // indirect
+	github.com/beorn7/perks v1.0.1 // indirect
 	github.com/cenkalti/backoff/v5 v5.0.2 // indirect
 	github.com/cespare/xxhash/v2 v2.3.0 // indirect
 	github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
@@ -49,28 +60,42 @@ require (
 	github.com/go-logr/stdr v1.2.2 // indirect
 	github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
 	github.com/gobwas/glob v0.2.3 // indirect
+	github.com/golang-jwt/jwt/v5 v5.2.1 // indirect
+	github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
 	github.com/google/go-tpm v0.9.4 // indirect
+	github.com/google/s2a-go v0.1.8 // indirect
 	github.com/google/uuid v1.6.0 // indirect
+	github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect
 	github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc // indirect
 	github.com/hashicorp/go-version v1.7.0 // indirect
+	github.com/jmespath/go-jmespath v0.4.0 // indirect
+	github.com/jpillora/backoff v1.0.0 // indirect
 	github.com/json-iterator/go v1.1.12 // indirect
 	github.com/klauspost/compress v1.18.0 // indirect
 	github.com/knadh/koanf/maps v0.1.2 // indirect
 	github.com/knadh/koanf/providers/confmap v1.0.0 // indirect
 	github.com/knadh/koanf/v2 v2.2.0 // indirect
+	github.com/kylelemons/godebug v1.1.0 // indirect
 	github.com/mitchellh/copystructure v1.2.0 // indirect
 	github.com/mitchellh/reflectwalk v1.0.2 // indirect
 	github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
 	github.com/modern-go/reflect2 v1.0.2 // indirect
+	github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
+	github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f // indirect
 	github.com/pierrec/lz4/v4 v4.1.22 // indirect
+	github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
 	github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
+	github.com/prometheus/client_golang v1.20.5 // indirect
 	github.com/prometheus/client_model v0.6.1 // indirect
 	github.com/prometheus/common v0.62.0 // indirect
+	github.com/prometheus/common/sigv4 v0.1.0 // indirect
+	github.com/prometheus/procfs v0.15.1 // indirect
 	github.com/rs/cors v1.11.1 // indirect
 	github.com/tidwall/gjson v1.10.2 // indirect
 	github.com/tidwall/match v1.1.1 // indirect
 	github.com/tidwall/pretty v1.2.0 // indirect
 	github.com/tidwall/tinylru v1.1.0 // indirect
+	go.opencensus.io v0.24.0 // indirect
 	go.opentelemetry.io/auto/sdk v1.1.0 // indirect
 	go.opentelemetry.io/collector/client v1.31.1-0.20250508034258-ac520a5c14cc // indirect
 	go.opentelemetry.io/collector/config/configauth v0.125.1-0.20250508034258-ac520a5c14cc // indirect
@@ -95,12 +120,20 @@ require (
 	go.opentelemetry.io/otel/log v0.11.0 // indirect
 	golang.org/x/crypto v0.38.0 // indirect
 	golang.org/x/net v0.40.0 // indirect
+	golang.org/x/oauth2 v0.26.0 // indirect
 	golang.org/x/sys v0.33.0 // indirect
 	golang.org/x/text v0.25.0 // indirect
+	golang.org/x/time v0.6.0 // indirect
+	google.golang.org/api v0.199.0 // indirect
 	google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a // indirect
 	google.golang.org/grpc v1.72.0 // indirect
 	google.golang.org/protobuf v1.36.6 // indirect
+	gopkg.in/yaml.v2 v2.4.0 // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
+	k8s.io/apimachinery v0.31.1 // indirect
+	k8s.io/client-go v0.31.1 // indirect
+	k8s.io/klog/v2 v2.130.1 // indirect
+	k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 // indirect
 	sigs.k8s.io/yaml v1.4.0 // indirect
 )
 
diff --git a/exporter/prometheusremotewriteexporter/go.sum b/exporter/prometheusremotewriteexporter/go.sum
index 8e3e4b6c5de84..9aee2ee2751fc 100644
--- a/exporter/prometheusremotewriteexporter/go.sum
+++ b/exporter/prometheusremotewriteexporter/go.sum
@@ -1,13 +1,118 @@
+cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
+cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
+cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
+cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
+cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
+cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
+cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
+cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
+cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
+cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
+cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
+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.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
+cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
+cloud.google.com/go/auth v0.9.5 h1:4CTn43Eynw40aFVr3GpPqsQponx2jv0BQpjvajsbbzw=
+cloud.google.com/go/auth v0.9.5/go.mod h1:Xo0n7n66eHyOWWCnitop6870Ilwo3PiZyodVkkH1xWM=
+cloud.google.com/go/auth/oauth2adapt v0.2.4 h1:0GWE/FUsXhf6C+jAkWgYm7X9tK8cuEIfy19DBn6B6bY=
+cloud.google.com/go/auth/oauth2adapt v0.2.4/go.mod h1:jC/jOpwFP6JBxhB3P5Rr0a9HLMC/Pe3eaL4NmdvqPtc=
+cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
+cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
+cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
+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.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I=
+cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg=
+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=
+cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
+cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
+cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
+cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
+cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
+cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
+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.14.0 h1:nyQWyZvwGTvunIMxi1Y9uXkcyr+I7TeNrr/foo4Kpk8=
+github.com/Azure/azure-sdk-for-go/sdk/azcore v1.14.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/network/armnetwork/v4 v4.3.0 h1:bXwSugBiSbgtz7rOtbfGf+woewp4f06orW9OP5BjHLA=
+github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v4 v4.3.0/go.mod h1:Y/HgrePTmGy9HjdSGTqZNa+apUpTVIEVKXJyARP2lrk=
+github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 h1:XHOnouVk1mxXfQidrMEnLlPk9UMeRtyBTnEFtxkV0kU=
+github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
+github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
+github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
+github.com/Code-Hex/go-generics-cache v1.5.1 h1:6vhZGc5M7Y/YD8cIUcY8kcuQLB4cHR7U+0KMqAA0KcU=
+github.com/Code-Hex/go-generics-cache v1.5.1/go.mod h1:qxcC9kRVrct9rHeiYpFWSoW1vxyillCVzX13KZG8dl4=
+github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
+github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
+github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
+github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
+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-20240626203959-61d1e3462e30 h1:t3eaIm0rUkzbrIewtiFmMK5RXHej2XnoXNhxVsAYUfg=
+github.com/alecthomas/units v0.0.0-20240626203959-61d1e3462e30/go.mod h1:fvzegU4vN3H1qMT+8wDmzjAcDONcgo2/SZ/TyfdUOFs=
+github.com/armon/go-metrics v0.4.1 h1:hR91U9KYmb6bLBYLQjyM+3j+rcd/UhE+G78SFnF8gJA=
+github.com/armon/go-metrics v0.4.1/go.mod h1:E6amYzXo6aW1tqzoZGT755KkbgrJsSdpwZ+3JqfkOG4=
+github.com/aws/aws-sdk-go v1.38.35/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro=
+github.com/aws/aws-sdk-go v1.55.5 h1:KKUZBfBoyqy5d3swXyiC7Q76ic40rYcbqH7qjh59kzU=
+github.com/aws/aws-sdk-go v1.55.5/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU=
+github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
+github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
+github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
+github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
 github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
 github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
 github.com/cenkalti/backoff/v5 v5.0.2 h1:rIfFVxEf1QsI7E1ZHfp/B4DF/6QBAUhmgkxc0H7Zss8=
 github.com/cenkalti/backoff/v5 v5.0.2/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
+github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
+github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
 github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
 github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
+github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
+github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
+github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
+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-20250121191232-2f005788dc42 h1:Om6kYQYDUk5wWbT0t0q6pvyM49i9XZAv9dDrkDA7gjk=
+github.com/cncf/xds/go v0.0.0-20250121191232-2f005788dc42/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/digitalocean/godo v1.126.0 h1:+Znh7VMQj/E8ArbjWnc7OKGjWfzC+I8OCSRp7r1MdD8=
+github.com/digitalocean/godo v1.126.0/go.mod h1:PU8JB6I1XYkQIdHFop8lLAY9ojp6M0XcU0TWaQSxbrc=
+github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
+github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
+github.com/docker/docker v28.0.1+incompatible h1:FCHjSRdXhNRFjlHMTv4jUNlIBbTeRjrWfeFuJp7jpo0=
+github.com/docker/docker v28.0.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
+github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
+github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
+github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
+github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
+github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g=
+github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
+github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
+github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
+github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
+github.com/envoyproxy/go-control-plane v0.13.4 h1:zEqyPVyku6IvWCFwux4x9RxkLOMUL+1vC9xUFv5l2/M=
+github.com/envoyproxy/go-control-plane/envoy v1.32.4 h1:jb83lalDRZSpPWW2Z7Mck/8kXZ5CQAFYVjQcdVIr83A=
+github.com/envoyproxy/go-control-plane/envoy v1.32.4/go.mod h1:Gzjc5k8JcJswLjAx1Zm+wSYE20UrLtt7JZMWiWQXQEw=
+github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
+github.com/envoyproxy/protoc-gen-validate v1.2.1 h1:DEo3O99U8j4hBFwbJfrz9VtgcDfUKS7KJ7spH3d86P8=
+github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU=
+github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
+github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
 github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
 github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
 github.com/foxboron/go-tpm-keyfiles v0.0.0-20250323135004-b31fac66206e h1:2jjYsGgM13xId2Ku+UGDQTO5It50LhT6lljiVJvBj1Y=
@@ -16,37 +121,176 @@ github.com/foxboron/swtpm_test v0.0.0-20230726224112-46aaafdf7006 h1:50sW4r0Pcvl
 github.com/foxboron/swtpm_test v0.0.0-20230726224112-46aaafdf7006/go.mod h1:eIXCMsMYCaqq9m1KSSxXwQG11krpuNPGP3k0uaWrbas=
 github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
 github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
+github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E=
+github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
+github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
+github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
+github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
+github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
+github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
+github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
+github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
+github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
+github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
 github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
 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/jsonpointer v0.20.2 h1:mQc3nmndL8ZBzStEo3JYF8wzmeWffDH4VbXz58sAx6Q=
+github.com/go-openapi/jsonpointer v0.20.2/go.mod h1:bHen+N0u1KEO3YlmqOjTT9Adn1RfD91Ar825/PuiRVs=
+github.com/go-openapi/jsonreference v0.20.4 h1:bKlDxQxQJgwpUSgOENiMPzCTBVuc7vTdXSSgNeAhojU=
+github.com/go-openapi/jsonreference v0.20.4/go.mod h1:5pZJyJP2MnYCpoeoMAql78cCHauHj0V9Lhc506VOpw4=
+github.com/go-openapi/swag v0.22.9 h1:XX2DssF+mQKM2DHsbgZK74y/zj4mo9I99+89xUmuZCE=
+github.com/go-openapi/swag v0.22.9/go.mod h1:3/OXnFfnMAwBD099SwYRk7GD3xOrr1iL7d/XNLXVVwE=
+github.com/go-resty/resty/v2 v2.13.1 h1:x+LHXBI2nMB1vqndymf26quycC4aggYJ7DECYbiz03g=
+github.com/go-resty/resty/v2 v2.13.1/go.mod h1:GznXlLxkq6Nh4sU59rPmUw3VtgpO3aS96ORAI6Q7d+0=
+github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
 github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss=
 github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
+github.com/go-zookeeper/zk v1.0.4 h1:DPzxraQx7OrPyXq2phlGlNSIyWEsAox0RJmjTseMV6I=
+github.com/go-zookeeper/zk v1.0.4/go.mod h1:nOB03cncLtlp4t+UAkGSV+9beXP/akpekBwL+UX1Qcw=
 github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
 github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
+github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
 github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
 github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
+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/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
+github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
+github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
+github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
+github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
+github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
+github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
+github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
+github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
+github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
+github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
+github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
+github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
+github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
+github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
+github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
+github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
+github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
+github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
 github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
 github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
 github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs=
 github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
+github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
+github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
+github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I=
+github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U=
+github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
+github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
 github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
 github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
+github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
+github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
 github.com/google/go-tpm v0.9.4 h1:awZRf9FwOeTunQmHoDYSHJps3ie6f1UlhS1fOdPEt1I=
 github.com/google/go-tpm v0.9.4/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY=
 github.com/google/go-tpm-tools v0.4.4 h1:oiQfAIkc6xTy9Fl5NKTeTJkBTlXdHsxAofmQyxBKY98=
 github.com/google/go-tpm-tools v0.4.4/go.mod h1:T8jXkp2s+eltnCDIsXR84/MTcVU9Ja7bh3Mit0pa4AY=
 github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
+github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
+github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
+github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
+github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
+github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
+github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
+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/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
+github.com/google/s2a-go v0.1.8 h1:zZDs9gcbt9ZPLV0ndSyQk6Kacx2g/X+SKYovpnz3SMM=
+github.com/google/s2a-go v0.1.8/go.mod h1:6iNWHTpQ+nfNRN5E00MSdfDwVesa8hhS32PhPO8deJA=
+github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
 github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/googleapis/enterprise-certificate-proxy v0.3.4 h1:XYIDZApgAnrN1c855gTgghdIA6Stxb52D5RnLI1SLyw=
+github.com/googleapis/enterprise-certificate-proxy v0.3.4/go.mod h1:YKe7cfqYXjKGpGvmSg28/fFvhNzinZQm8DGnaburhGA=
+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/gophercloud/gophercloud v1.14.1 h1:DTCNaTVGl8/cFu58O1JwWgis9gtISAFONqpMKNg/Vpw=
+github.com/gophercloud/gophercloud v1.14.1/go.mod h1:aAVqcocTSXh2vYFZ1JTvx4EQmfgzxRcNupUfxZbBNDM=
+github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
+github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
 github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc h1:GN2Lv3MGO7AS6PrRoT6yV5+wkrOpcszoIsO4+4ds248=
 github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc/go.mod h1:+JKpmjMGhpgPL+rXZ5nsZieVzvarn86asRlBg4uNGnk=
+github.com/hashicorp/consul/api v1.29.4 h1:P6slzxDLBOxUSj3fWo2o65VuKtbtOXFi7TSSgtXutuE=
+github.com/hashicorp/consul/api v1.29.4/go.mod h1:HUlfw+l2Zy68ceJavv2zAyArl2fqhGWnMycyt56sBgg=
+github.com/hashicorp/cronexpr v1.1.2 h1:wG/ZYIKT+RT3QkOdgYc+xsKWVRgnxJ1OJtjjy84fJ9A=
+github.com/hashicorp/cronexpr v1.1.2/go.mod h1:P4wA0KBl9C5q2hABiMO7cp6jcIg96CDh1Efb3g1PWA4=
+github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
+github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
+github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
+github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
+github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=
+github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
+github.com/hashicorp/go-immutable-radix v1.3.1 h1:DKHmCUm2hRBK510BaiZlwvpD40f8bJFeZnpfm2KLowc=
+github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
+github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
+github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
+github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU=
+github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk=
+github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc=
+github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8=
 github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY=
 github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
+github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
+github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
+github.com/hashicorp/golang-lru v0.6.0 h1:uL2shRDx7RTrOrTCUZEGP/wJUFiUI8QT6E7z5o8jga4=
+github.com/hashicorp/golang-lru v0.6.0/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
+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.10.1 h1:Z1H2J60yRKvfDYAOZLd2MU0ND4AH/WDz7xYHDWQsIPY=
+github.com/hashicorp/serf v0.10.1/go.mod h1:yL2t6BqATOLGc5HF7qbFkTfXoPIY0WZdWHfEvMqbG+4=
+github.com/hetznercloud/hcloud-go/v2 v2.13.1 h1:jq0GP4QaYE5d8xR/Zw17s9qoaESRJMXfGmtD1a/qckQ=
+github.com/hetznercloud/hcloud-go/v2 v2.13.1/go.mod h1:dhix40Br3fDiBhwaSG/zgaYOFFddpfBm/6R1Zz0IiF0=
+github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
+github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4=
+github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY=
+github.com/ionos-cloud/sdk-go/v6 v6.2.1 h1:mxxN+frNVmbFrmmFfXnBC3g2USYJrl6mc1LW2iNYbFY=
+github.com/ionos-cloud/sdk-go/v6 v6.2.1/go.mod h1:SXrO9OGyWjd2rZhAhEpdYN6VUAODzzqRdqA9BCviQtI=
+github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
+github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
+github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
+github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
+github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
+github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
+github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA=
+github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
+github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
+github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
+github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
 github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
 github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
+github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
+github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
+github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
+github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
 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.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
@@ -57,36 +301,127 @@ github.com/knadh/koanf/providers/confmap v1.0.0 h1:mHKLJTE7iXEys6deO5p6olAiZdG5z
 github.com/knadh/koanf/providers/confmap v1.0.0/go.mod h1:txHYHiI2hAtF0/0sCmcuol4IDcuQbKTybiB1nOcUo1A=
 github.com/knadh/koanf/v2 v2.2.0 h1:FZFwd9bUjpb8DyCWARUBy5ovuhDs1lI87dOEn2K8UVU=
 github.com/knadh/koanf/v2 v2.2.0/go.mod h1:PSFru3ufQgTsI7IF+95rf9s8XA1+aHxKuO/W+dPoHEY=
+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=
+github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
+github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
+github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
 github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
 github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
+github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
 github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
 github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
+github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
+github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
+github.com/linode/linodego v1.41.0 h1:GcP7JIBr9iLRJ9FwAtb9/WCT1DuPJS/xUApapfdjtiY=
+github.com/linode/linodego v1.41.0/go.mod h1:Ow4/XZ0yvWBzt3iAHwchvhSx30AyLintsSMvvQ2/SJY=
+github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
+github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
+github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
+github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
+github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
+github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
+github.com/miekg/dns v1.1.62 h1:cN8OuEF1/x5Rq6Np+h1epln8OiyPWV+lROx9LxcGgIQ=
+github.com/miekg/dns v1.1.62/go.mod h1:mvDlcItzm+br7MToIKqkglaGhlFMHJ9DTNNWONWXbNQ=
 github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
 github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
+github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
+github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
+github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
+github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
 github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
 github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
+github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
+github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
 github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
 github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
 github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
+github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
 github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
 github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
+github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
+github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
+github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
+github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+SVif2QVs3tOP0zanoHgBEVAwHxUSIzRqU=
+github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
+github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
+github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
+github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
+github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
+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/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU=
 github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
+github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
+github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
+github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
+github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo=
+github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
 github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
+github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
+github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
+github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=
+github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y=
+github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE=
+github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
+github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
+github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
+github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
 github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
 github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
+github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
+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.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io=
 github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I=
+github.com/prometheus/common/sigv4 v0.1.0 h1:qoVebwtwwEhS85Czm2dSROY5fTo2PAPEVdDeppTwGX4=
+github.com/prometheus/common/sigv4 v0.1.0/go.mod h1:2Jkxxk9yYvCkE5G1sQT7GuEXm57JrvHu9k5YwTjsNtI=
+github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
+github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
+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.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
+github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
 github.com/prometheus/prometheus v0.300.1 h1:9KKcTTq80gkzmXW0Et/QCFSrBPgmwiS3Hlcxc6o8KlM=
 github.com/prometheus/prometheus v0.300.1/go.mod h1:gtTPY/XVyCdqqnjA3NzDMb0/nc5H9hOu1RMame+gHyM=
+github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
 github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
 github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
 github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA=
 github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
+github.com/scaleway/scaleway-sdk-go v1.0.0-beta.30 h1:yoKAVkEVwAqbGbR8n87rHQ1dulL25rKloGadb3vm770=
+github.com/scaleway/scaleway-sdk-go v1.0.0-beta.30/go.mod h1:sH0u6fq6x4R5M7WxkoQFY/o7UaiItec0o1LinLCJNq8=
+github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
+github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
+github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
+github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
+github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
+github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
+github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
+github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
+github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
 github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
+github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+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/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
 github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
 github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
 github.com/tidwall/gjson v1.10.2 h1:APbLGOM0rrEkd8WBw9C24nllro4ajFuJu0Sc9hRz8Bo=
@@ -99,8 +434,21 @@ github.com/tidwall/tinylru v1.1.0 h1:XY6IUfzVTU9rpwdhKUF6nQdChgCdGjkMfLzbWyiau6I
 github.com/tidwall/tinylru v1.1.0/go.mod h1:3+bX+TJ2baOLMWTnlyNWHh4QMnFyARg2TLTQ6OFbzw8=
 github.com/tidwall/wal v1.1.8 h1:2qDSGdAdjaY3PEvHRva+9UFqgk+ef7cOiW1Qn5JH1y0=
 github.com/tidwall/wal v1.1.8/go.mod h1:r6lR1j27W9EPalgHiB7zLJDYu3mzW5BQP5KrzBpYY/E=
+github.com/vultr/govultr/v2 v2.17.2 h1:gej/rwr91Puc/tgh+j33p/BLR16UrIPnSr+AIwYWZQs=
+github.com/vultr/govultr/v2 v2.17.2/go.mod h1:ZFOKGWmgjytfyjeyAdhQlSWwTjh2ig+X49cAp50dzXI=
+github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
+github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
+github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
+go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
+go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
+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/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
 go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
 go.opentelemetry.io/collector/client v1.31.1-0.20250508034258-ac520a5c14cc h1:VRsC6MGUxMQkh9yV5IzuUVEd6OdBWLixipoxaSf1uus=
@@ -191,57 +539,345 @@ go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5J
 go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w=
 go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs=
 go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
+go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
+go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
 go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
 go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
 go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
 go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
 go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
 go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
+golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
 golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
+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=
+golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
+golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
+golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
+golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
+golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
+golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
+golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
+golang.org/x/exp v0.0.0-20240119083558-1b970713d09a h1:Q8/wZp0KX97QFTc2ywcOE0YRjZPVIx+MXInMzdvQqcA=
+golang.org/x/exp v0.0.0-20240119083558-1b970713d09a/go.mod h1:idGWGoKP1toJGkd5/ig9ZLuPcZBC3ewk7SzmH0uou08=
+golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
+golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
+golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
+golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
+golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
+golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
+golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
+golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
+golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
+golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
+golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
+golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
+golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
+golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
 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.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0=
+golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
+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-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
+golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
+golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
+golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
 golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
 golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
 golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
+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=
+golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
+golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
+golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
+golang.org/x/oauth2 v0.26.0 h1:afQXWNNaeC4nvZ0Ed9XvCCzXM6UHJG7iCg0W4fPqSBE=
+golang.org/x/oauth2 v0.26.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
+golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
+golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
+golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
 golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
+golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
+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=
+golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
 golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
+golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U=
+golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
+golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
+golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
+golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
+golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
+golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
+golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
+golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
+golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
+golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
+golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
 golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
+golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
+golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
 golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ=
+golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0=
 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=
 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
+google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
+google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
+google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
+google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
+google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
+google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
+google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
+google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
+google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
+google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
+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.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
+google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
+google.golang.org/api v0.199.0 h1:aWUXClp+VFJmqE0JPvpZOK3LDQMyFKYIow4etYd9qxs=
+google.golang.org/api v0.199.0/go.mod h1:ohG4qSztDJmZdjK/Ar6MhbAmb/Rpi4JHOqagsh90K28=
+google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
+google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
+google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
+google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
+google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
+google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
+google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
+google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
+google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
+google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
+google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
+google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
+google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
+google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
+google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
+google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
+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-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20240903143218-8af14fe29dc1 h1:BulPr26Jqjnd4eYDVe+YvyR7Yc2vJGkO5/0UxD0/jZU=
+google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a h1:nwKuGPlUAt+aR+pcrkfFRrTU1BVrSmYyYMxYbUIVHr0=
+google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a/go.mod h1:3kWAYMk1I75K4vykHtKt2ycnOgpA6974V7bREqbsenU=
 google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a h1:51aaUVRocpvUOSQKM6Q7VuoaktNIaMCLuhZB6DKksq4=
 google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a/go.mod h1:uRxBH1mhmO8PGhU89cMcHaXKZqO+OfakD8QQO0oYwlQ=
+google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
+google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
+google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
+google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
+google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
+google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
+google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
+google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
+google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
+google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
+google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
+google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
+google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
 google.golang.org/grpc v1.72.0 h1:S7UkcVa60b5AAQTaO6ZKamFp1zMZSU0fGDK2WZLbBnM=
 google.golang.org/grpc v1.72.0/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM=
+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=
+google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
+google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
+google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
+google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
+google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
 google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
 google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
+gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
+gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
+gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
+gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
+gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
+gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
+gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
 gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
+honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
+honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
+k8s.io/api v0.31.1 h1:Xe1hX/fPW3PXYYv8BlozYqw63ytA92snr96zMW9gWTU=
+k8s.io/api v0.31.1/go.mod h1:sbN1g6eY6XVLeqNsZGLnI5FwVseTrZX7Fv3O26rhAaI=
+k8s.io/apimachinery v0.31.1 h1:mhcUBbj7KUjaVhyXILglcVjuS4nYXiwC+KKFBgIVy7U=
+k8s.io/apimachinery v0.31.1/go.mod h1:rsPdaZJfTfLsNJSQzNHQvYoTmxhoOEofxtOsF3rtsMo=
+k8s.io/client-go v0.31.1 h1:f0ugtWSbWpxHR7sjVpQwuvw9a3ZKLXX0u0itkFXufb0=
+k8s.io/client-go v0.31.1/go.mod h1:sKI8871MJN2OyeqRlmA4W4KM9KBdBUpDLu/43eGemCg=
+k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
+k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
+k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 h1:BZqlfIlq5YbRMFko6/PM7FjZpUb45WallggurYhKGag=
+k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340/go.mod h1:yD4MZYeKMBwQKVht279WycxKyM84kkAx2DPrTXaeb98=
+k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 h1:pUdcCO1Lk/tbT5ztQWOBi5HBgbBP1J8+AsQnQCKsi8A=
+k8s.io/utils v0.0.0-20240711033017-18e509b52bc8/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
+rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
+rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
+rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
+sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo=
+sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0=
+sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4=
+sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08=
 sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
 sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=
diff --git a/exporter/prometheusremotewriteexporter/metadata.yaml b/exporter/prometheusremotewriteexporter/metadata.yaml
index 089c1afe1c03c..b38e927177fb9 100644
--- a/exporter/prometheusremotewriteexporter/metadata.yaml
+++ b/exporter/prometheusremotewriteexporter/metadata.yaml
@@ -10,6 +10,11 @@ status:
 
 tests:
   expect_consumer_error: true
+  goleak:
+    ignore:
+      top:
+        # See https://github.com/census-instrumentation/opencensus-go/issues/1191 for more information.
+        - "go.opencensus.io/stats/view.(*worker).start"
 
 telemetry:
   metrics:
diff --git a/exporter/prometheusremotewriteexporter/wal_test.go b/exporter/prometheusremotewriteexporter/wal_test.go
index db27b969a5cfd..821a3975e45c2 100644
--- a/exporter/prometheusremotewriteexporter/wal_test.go
+++ b/exporter/prometheusremotewriteexporter/wal_test.go
@@ -14,6 +14,7 @@ import (
 
 	"github.com/gogo/protobuf/proto"
 	"github.com/golang/snappy"
+	"github.com/prometheus/prometheus/config"
 	"github.com/prometheus/prometheus/prompb"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
@@ -166,7 +167,8 @@ func TestExportWithWALEnabled(t *testing.T) {
 		WAL: &WALConfig{
 			Directory: t.TempDir(),
 		},
-		TargetInfo: &TargetInfo{}, // Declared just to avoid nil pointer dereference.
+		TargetInfo:          &TargetInfo{}, // Declared just to avoid nil pointer dereference.
+		RemoteWriteProtoMsg: config.RemoteWriteProtoMsgV1,
 	}
 	buildInfo := component.BuildInfo{
 		Description: "OpenTelemetry Collector",


Reviews:
user: login: ArthurSens
state: COMMENTED
author_association: MEMBER
submitted_at: 2024-10-21T17:59:27Z
user: login: jmichalek132
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2024-10-21T18:06:03Z
user: login: dashpole
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2024-10-25T18:09:56Z
user: login: dashpole
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2024-10-25T18:10:47Z
user: login: dashpole
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2024-10-25T18:11:26Z
user: login: jmichalek132
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2024-10-25T19:30:40Z
user: login: jmichalek132
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2024-10-25T19:30:48Z
user: login: jmichalek132
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2024-10-25T20:01:27Z
user: login: dashpole
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2024-10-25T20:06:35Z
user: login: dashpole
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2024-10-25T20:09:10Z
user: login: dashpole
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2024-10-25T20:10:01Z
user: login: dashpole
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2024-10-25T20:10:17Z
user: login: dashpole
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2024-10-25T20:12:16Z
user: login: dashpole
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2024-10-25T20:12:39Z
user: login: dashpole
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2024-10-25T20:12:49Z
user: login: dashpole
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2024-10-25T20:13:05Z
user: login: jmichalek132
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2024-10-26T11:37:12Z
user: login: jmichalek132
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2024-10-28T20:13:12Z
user: login: ArthurSens
body: Couldn't find time for a deep review, this is just from a quick read :)
state: COMMENTED
author_association: MEMBER
submitted_at: 2024-10-28T22:24:09Z
user: login: dashpole
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2024-10-29T12:26:34Z
user: login: jmichalek132
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2024-10-30T15:32:16Z
user: login: jmichalek132
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2024-10-30T15:37:21Z
user: login: jmichalek132
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2024-10-30T15:43:29Z
user: login: jmichalek132
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2024-10-30T15:43:33Z
user: login: jmichalek132
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2024-10-30T15:46:20Z
user: login: jmichalek132
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2024-10-30T15:46:38Z
user: login: jmichalek132
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2024-10-30T17:09:09Z
user: login: jmichalek132
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2024-10-30T17:10:34Z
user: login: aknuds1
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2024-10-30T17:27:25Z
user: login: aknuds1
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2024-10-30T17:29:13Z
user: login: dashpole
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2024-10-30T19:21:25Z
user: login: jmichalek132
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2024-10-30T19:30:34Z
user: login: jmichalek132
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2024-10-31T16:11:16Z
user: login: jmichalek132
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2024-10-31T17:07:44Z
user: login: jmichalek132
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2024-10-31T17:27:36Z
user: login: dashpole
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2024-10-31T17:32:14Z
user: login: ArthurSens
state: COMMENTED
author_association: MEMBER
submitted_at: 2024-10-31T17:37:42Z
user: login: jmichalek132
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2024-10-31T17:47:18Z
user: login: dashpole
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2024-10-31T17:54:15Z
user: login: ArthurSens
body: Thanks a lot for the work here, I think we're pretty close :)
state: COMMENTED
author_association: MEMBER
submitted_at: 2024-11-01T18:22:53Z
user: login: jmichalek132
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2024-11-12T14:25:27Z
user: login: jmichalek132
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2024-11-12T14:41:00Z
user: login: jmichalek132
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2024-11-12T14:41:21Z
user: login: jmichalek132
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2024-11-12T14:42:56Z
user: login: jmichalek132
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2024-11-12T14:48:06Z
user: login: dashpole
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2024-11-12T14:51:09Z
user: login: jmichalek132
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2024-11-12T15:37:33Z
user: login: dashpole
body: looking good
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2024-11-12T17:35:26Z
user: login: jmichalek132
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2024-11-13T10:10:02Z
user: login: jmichalek132
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2024-11-13T10:22:51Z
user: login: jmichalek132
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2024-11-13T12:44:10Z
user: login: jmichalek132
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2024-11-13T12:55:59Z
user: login: dashpole
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2024-11-13T14:26:14Z
user: login: dashpole
body: I would like @ArthurSens to approve as well before merging
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2024-11-13T14:27:03Z
user: login: ArthurSens
body: LGTM, just address the comment as David said and there is a failing check asking you to run `make generate` :)
state: COMMENTED
author_association: MEMBER
submitted_at: 2024-11-13T14:37:37Z
user: login: jmichalek132
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2024-11-13T15:13:16Z
user: login: jmichalek132
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2024-11-13T15:21:09Z
user: login: dashpole
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2024-11-13T15:42:25Z
user: login: dashpole
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2024-11-13T15:42:34Z
user: login: ArthurSens
state: APPROVED
author_association: MEMBER
submitted_at: 2024-11-13T17:03:45Z
user: login: ArthurSens
state: COMMENTED
author_association: MEMBER
submitted_at: 2024-11-14T13:56:13Z
user: login: jmichalek132
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2024-11-14T14:01:32Z
user: login: ArthurSens
state: COMMENTED
author_association: MEMBER
submitted_at: 2024-11-14T14:07:28Z
user: login: ArthurSens
state: APPROVED
author_association: MEMBER
submitted_at: 2024-11-14T14:07:38Z
user: login: jmichalek132
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2024-11-14T14:13:04Z
user: login: aknuds1
body: Generally looks good, but please see comments.
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2024-11-20T17:49:16Z
user: login: bwplotka
body: Nice work! Some comments around readability mostly.
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2024-11-25T17:10:46Z
user: login: ArthurSens
state: COMMENTED
author_association: MEMBER
submitted_at: 2024-11-25T17:13:24Z
user: login: Aneurysm9
state: COMMENTED
author_association: MEMBER
submitted_at: 2024-11-25T22:01:03Z
user: login: bwplotka
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2024-11-26T16:14:37Z
user: login: jmichalek132
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2024-11-27T16:03:54Z
user: login: jmichalek132
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2024-11-27T16:10:11Z
user: login: jmichalek132
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2024-11-27T16:16:44Z
user: login: jmichalek132
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2024-11-27T17:05:54Z
user: login: jmichalek132
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2024-11-27T17:14:10Z
user: login: jmichalek132
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2024-11-27T17:20:58Z
user: login: aknuds1
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2024-12-02T17:04:55Z
user: login: bwplotka
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2024-12-03T09:39:41Z
user: login: bwplotka
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2024-12-03T09:40:17Z
user: login: bwplotka
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2024-12-03T09:41:09Z
user: login: bwplotka
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2024-12-03T09:43:20Z
user: login: bwplotka
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2024-12-03T09:44:57Z
user: login: bwplotka
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2024-12-03T09:45:43Z
user: login: jmichalek132
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-02T16:28:16Z
user: login: jmichalek132
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-02T16:28:47Z
user: login: jmichalek132
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-02T16:30:36Z
user: login: ArthurSens
body: Some small coments, but I've tried testing the whole thing end to end and it's working :)
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-07T18:21:04Z
user: login: jmichalek132
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T15:13:05Z
user: login: ywwg
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T16:50:45Z
user: login: jmichalek132
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T17:15:35Z
user: login: ArthurSens
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-08T18:03:16Z
user: login: ArthurSens
body: LGTM, thanks for continuing the work here even after all these months!
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-08T18:06:07Z

Review comments:
diff_hunk: @@ -52,6 +52,9 @@ type Config struct {
 
 	// SendMetadata controls whether prometheus metadata will be generated and sent
 	SendMetadata bool `mapstructure:"send_metadata"`
+
+	// SendRW2 controls whether prometheus remote write v1 or v2 is sent
+	SendRW2 bool `mapstructure:"send_remote_write_v2"`
path: exporter/prometheusremotewriteexporter/config.go
user: login: ArthurSens
body: I wouldn't put a boolean here, but an ENUM. We expect PRW to evolve into 2.1, 2.2, etc. If we add a boolean, which version of PRv2 are we sending?
created_at: 2024-10-21T17:59:27Z
author_association: MEMBER
diff_hunk: @@ -52,6 +52,9 @@ type Config struct {
 
 	// SendMetadata controls whether prometheus metadata will be generated and sent
 	SendMetadata bool `mapstructure:"send_metadata"`
+
+	// SendRW2 controls whether prometheus remote write v1 or v2 is sent
+	SendRW2 bool `mapstructure:"send_remote_write_v2"`
path: exporter/prometheusremotewriteexporter/config.go
user: login: jmichalek132
body: will updated
created_at: 2024-10-21T18:06:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -203,28 +219,53 @@ func validateAndSanitizeExternalLabels(cfg *Config) (map[string]string, error) {
 	return sanitizedLabels, nil
 }
 
-func (prwe *prwExporter) handleExport(ctx context.Context, tsMap map[string]*prompb.TimeSeries, m []*prompb.MetricMetadata) error {
+func (prwe *prwExporter) handleExport(ctx context.Context, tsMap map[string]*prompb.TimeSeries, m []*prompb.MetricMetadata, symbolsTable writev2.SymbolsTable, tsMapv2 map[string]*writev2.TimeSeries) error {
path: exporter/prometheusremotewriteexporter/exporter.go
user: login: dashpole
body: I don't like this.  We shouldn't have functions where we expect different parameters to be provided vs nil depending on scenarios.  Instead, please add handleExportV2, and move common code between the functions to helper functions.
created_at: 2024-10-25T18:09:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -270,9 +311,15 @@ func (prwe *prwExporter) export(ctx context.Context, requests []*prompb.WriteReq
 	return errs
 }
 
-func (prwe *prwExporter) execute(ctx context.Context, writeReq *prompb.WriteRequest) error {
+func (prwe *prwExporter) execute(ctx context.Context, writeReq *prompb.WriteRequest, writeReqv2 *writev2.Request, v2 bool) error {
path: exporter/prometheusremotewriteexporter/exporter.go
user: login: dashpole
body: maybe just change this function to accept `data []byte`, rather than two different args?
created_at: 2024-10-25T18:10:46Z
author_association: CONTRIBUTOR
diff_hunk: @@ -270,9 +311,15 @@ func (prwe *prwExporter) export(ctx context.Context, requests []*prompb.WriteReq
 	return errs
 }
 
-func (prwe *prwExporter) execute(ctx context.Context, writeReq *prompb.WriteRequest) error {
+func (prwe *prwExporter) execute(ctx context.Context, writeReq *prompb.WriteRequest, writeReqv2 *writev2.Request, v2 bool) error {
path: exporter/prometheusremotewriteexporter/exporter.go
user: login: dashpole
body: we shouldn't need to pass `v2 bool` to functions.  Instead, just use `prwe.exporterSettings.SendRW2` within each function as needed.
created_at: 2024-10-25T18:11:26Z
author_association: CONTRIBUTOR
diff_hunk: @@ -270,9 +311,15 @@ func (prwe *prwExporter) export(ctx context.Context, requests []*prompb.WriteReq
 	return errs
 }
 
-func (prwe *prwExporter) execute(ctx context.Context, writeReq *prompb.WriteRequest) error {
+func (prwe *prwExporter) execute(ctx context.Context, writeReq *prompb.WriteRequest, writeReqv2 *writev2.Request, v2 bool) error {
path: exporter/prometheusremotewriteexporter/exporter.go
user: login: jmichalek132
body: fixed
created_at: 2024-10-25T19:30:40Z
author_association: CONTRIBUTOR
diff_hunk: @@ -270,9 +311,15 @@ func (prwe *prwExporter) export(ctx context.Context, requests []*prompb.WriteReq
 	return errs
 }
 
-func (prwe *prwExporter) execute(ctx context.Context, writeReq *prompb.WriteRequest) error {
+func (prwe *prwExporter) execute(ctx context.Context, writeReq *prompb.WriteRequest, writeReqv2 *writev2.Request, v2 bool) error {
path: exporter/prometheusremotewriteexporter/exporter.go
user: login: jmichalek132
body: implemented
created_at: 2024-10-25T19:30:48Z
author_association: CONTRIBUTOR
diff_hunk: @@ -52,6 +52,9 @@ type Config struct {
 
 	// SendMetadata controls whether prometheus metadata will be generated and sent
 	SendMetadata bool `mapstructure:"send_metadata"`
+
+	// SendRW2 controls whether prometheus remote write v1 or v2 is sent
+	SendRW2 bool `mapstructure:"send_remote_write_v2"`
path: exporter/prometheusremotewriteexporter/config.go
user: login: jmichalek132
body: fixed
created_at: 2024-10-25T20:01:27Z
author_association: CONTRIBUTOR
diff_hunk: @@ -172,22 +174,43 @@ func (prwe *prwExporter) PushMetrics(ctx context.Context, md pmetric.Metrics) er
 	case <-prwe.closeChan:
 		return errors.New("shutdown has been called")
 	default:
+		var tsMap map[string]*prompb.TimeSeries
path: exporter/prometheusremotewriteexporter/exporter.go
user: login: dashpole
body: you don't need to predefine all of these anymore
created_at: 2024-10-25T20:06:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -17,13 +17,34 @@ import (
 	prometheustranslator "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/translator/prometheus"
 )
 
+// RemoteWriteProtoMsg represents the known protobuf message for the remote write
+// 1.0 and 2.0 specs.
+type RemoteWriteProtoMsg string
+
+var (
+	// RemoteWriteProtoMsgV1 represents the `prometheus.WriteRequest` protobuf
+	// message introduced in the https://prometheus.io/docs/specs/remote_write_spec/,
+	// which will eventually be deprecated.
+	//
+	// NOTE: This string is used for both HTTP header values and config value, so don't change
+	// this reference.
+	RemoteWriteProtoMsgV1 RemoteWriteProtoMsg = "prometheus.WriteRequest"
+	// RemoteWriteProtoMsgV2 represents the `io.prometheus.write.v2.Request` protobuf
+	// message introduced in https://prometheus.io/docs/specs/remote_write_spec_2_0/
+	//
+	// NOTE: This string is used for both HTTP header values and config value, so don't change
+	// this reference.
+	RemoteWriteProtoMsgV2 RemoteWriteProtoMsg = "io.prometheus.write.v2.Request"
+)
+
 type Settings struct {
 	Namespace           string
 	ExternalLabels      map[string]string
 	DisableTargetInfo   bool
 	ExportCreatedMetric bool
 	AddMetricSuffixes   bool
 	SendMetadata        bool
+	RemoteWriteProtoMsg RemoteWriteProtoMsg
path: pkg/translator/prometheusremotewrite/metrics_to_prw.go
user: login: dashpole
body: why is this part of settings?  We already have different functions for FromMetrics and FromMetricsV2
created_at: 2024-10-25T20:09:10Z
author_association: CONTRIBUTOR
diff_hunk: @@ -52,6 +53,9 @@ type Config struct {
 
 	// SendMetadata controls whether prometheus metadata will be generated and sent
 	SendMetadata bool `mapstructure:"send_metadata"`
+
+	// SendRW2 controls whether prometheus remote write v1 or v2 is sent
+	RemoteWriteProtoMsg prometheusremotewrite.RemoteWriteProtoMsg `mapstructure:"protobuf_message,omitempty"`
path: exporter/prometheusremotewriteexporter/config.go
user: login: dashpole
body: Does this match the export configuration for the prometheus server?
created_at: 2024-10-25T20:10:01Z
author_association: CONTRIBUTOR
diff_hunk: @@ -52,6 +53,9 @@ type Config struct {
 
 	// SendMetadata controls whether prometheus metadata will be generated and sent
 	SendMetadata bool `mapstructure:"send_metadata"`
+
+	// SendRW2 controls whether prometheus remote write v1 or v2 is sent
path: exporter/prometheusremotewriteexporter/config.go
user: login: dashpole
body: ```suggestion
	// RemoteWriteProtoMsg controls whether prometheus remote write v1 or v2 is sent
```
created_at: 2024-10-25T20:10:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -84,14 +85,15 @@ func createDefaultConfig() component.Config {
 	clientConfig.Timeout = exporterhelper.NewDefaultTimeoutConfig().Timeout
 
 	return &Config{
-		Namespace:         "",
-		ExternalLabels:    map[string]string{},
-		MaxBatchSizeBytes: 3000000,
-		TimeoutSettings:   exporterhelper.NewDefaultTimeoutConfig(),
-		BackOffConfig:     retrySettings,
-		AddMetricSuffixes: true,
-		SendMetadata:      false,
-		ClientConfig:      clientConfig,
+		Namespace:           "",
+		ExternalLabels:      map[string]string{},
+		MaxBatchSizeBytes:   3000000,
+		TimeoutSettings:     exporterhelper.NewDefaultTimeoutConfig(),
+		BackOffConfig:       retrySettings,
+		AddMetricSuffixes:   true,
+		SendMetadata:        false,
+		RemoteWriteProtoMsg: prometheusremotewrite.RemoteWriteProtoMsgV1,
path: exporter/prometheusremotewriteexporter/factory.go
user: login: dashpole
body: We should validate this field to make sure it is set to one of the enum options, if it is set.
created_at: 2024-10-25T20:12:16Z
author_association: CONTRIBUTOR
diff_hunk: @@ -7,15 +7,14 @@ import (
 	"testing"
 
 	"github.com/stretchr/testify/require"
-	"go.opentelemetry.io/otel/metric"
-	sdkmetric "go.opentelemetry.io/otel/sdk/metric"
-	"go.opentelemetry.io/otel/sdk/metric/metricdata"
-	"go.opentelemetry.io/otel/sdk/metric/metricdata/metricdatatest"
-
 	"go.opentelemetry.io/collector/component"
 	"go.opentelemetry.io/collector/config/configtelemetry"
 	"go.opentelemetry.io/collector/exporter"
 	"go.opentelemetry.io/collector/exporter/exportertest"
+	"go.opentelemetry.io/otel/metric"
path: exporter/prometheusremotewriteexporter/generated_component_telemetry_test.go
user: login: dashpole
body: these changes seem unneeded?
created_at: 2024-10-25T20:12:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -5,11 +5,10 @@ package metadata
 import (
 	"errors"
 
-	"go.opentelemetry.io/otel/metric"
-	"go.opentelemetry.io/otel/trace"
-
 	"go.opentelemetry.io/collector/component"
 	"go.opentelemetry.io/collector/config/configtelemetry"
+	"go.opentelemetry.io/otel/metric"
path: exporter/prometheusremotewriteexporter/internal/metadata/generated_telemetry.go
user: login: dashpole
body: This changes seem unneeded?
created_at: 2024-10-25T20:12:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -6,15 +6,14 @@ import (
 	"testing"
 
 	"github.com/stretchr/testify/require"
+	"go.opentelemetry.io/collector/component"
path: exporter/prometheusremotewriteexporter/internal/metadata/generated_telemetry_test.go
user: login: dashpole
body: Also unneeded changes
created_at: 2024-10-25T20:13:04Z
author_association: CONTRIBUTOR
diff_hunk: @@ -172,22 +174,43 @@ func (prwe *prwExporter) PushMetrics(ctx context.Context, md pmetric.Metrics) er
 	case <-prwe.closeChan:
 		return errors.New("shutdown has been called")
 	default:
+		var tsMap map[string]*prompb.TimeSeries
path: exporter/prometheusremotewriteexporter/exporter.go
user: login: jmichalek132
body: fixed
created_at: 2024-10-26T11:37:12Z
author_association: CONTRIBUTOR
diff_hunk: @@ -17,13 +17,34 @@ import (
 	prometheustranslator "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/translator/prometheus"
 )
 
+// RemoteWriteProtoMsg represents the known protobuf message for the remote write
+// 1.0 and 2.0 specs.
+type RemoteWriteProtoMsg string
+
+var (
+	// RemoteWriteProtoMsgV1 represents the `prometheus.WriteRequest` protobuf
+	// message introduced in the https://prometheus.io/docs/specs/remote_write_spec/,
+	// which will eventually be deprecated.
+	//
+	// NOTE: This string is used for both HTTP header values and config value, so don't change
+	// this reference.
+	RemoteWriteProtoMsgV1 RemoteWriteProtoMsg = "prometheus.WriteRequest"
+	// RemoteWriteProtoMsgV2 represents the `io.prometheus.write.v2.Request` protobuf
+	// message introduced in https://prometheus.io/docs/specs/remote_write_spec_2_0/
+	//
+	// NOTE: This string is used for both HTTP header values and config value, so don't change
+	// this reference.
+	RemoteWriteProtoMsgV2 RemoteWriteProtoMsg = "io.prometheus.write.v2.Request"
+)
+
 type Settings struct {
 	Namespace           string
 	ExternalLabels      map[string]string
 	DisableTargetInfo   bool
 	ExportCreatedMetric bool
 	AddMetricSuffixes   bool
 	SendMetadata        bool
+	RemoteWriteProtoMsg RemoteWriteProtoMsg
path: pkg/translator/prometheusremotewrite/metrics_to_prw.go
user: login: jmichalek132
body: Moved it.
created_at: 2024-10-28T20:13:12Z
author_association: CONTRIBUTOR
diff_hunk: @@ -53,6 +53,26 @@ func (p *prwTelemetryOtel) recordTranslatedTimeSeries(ctx context.Context, numTS
 	p.telemetryBuilder.ExporterPrometheusremotewriteTranslatedTimeSeries.Add(ctx, int64(numTS), metric.WithAttributes(p.otelAttrs...))
 }
 
+// RemoteWriteProtoMsg represents the known protobuf message for the remote write
+// 1.0 and 2.0 specs.
+type RemoteWriteProtoMsg string
+
+var (
+	// RemoteWriteProtoMsgV1 represents the `prometheus.WriteRequest` protobuf
+	// message introduced in the https://prometheus.io/docs/specs/remote_write_spec/,
+	// which will eventually be deprecated.
+	//
+	// NOTE: This string is used for both HTTP header values and config value, so don't change
+	// this reference.
+	RemoteWriteProtoMsgV1 RemoteWriteProtoMsg = "prometheus.WriteRequest"
+	// RemoteWriteProtoMsgV2 represents the `io.prometheus.write.v2.Request` protobuf
+	// message introduced in https://prometheus.io/docs/specs/remote_write_spec_2_0/
+	//
+	// NOTE: This string is used for both HTTP header values and config value, so don't change
+	// this reference.
+	RemoteWriteProtoMsgV2 RemoteWriteProtoMsg = "io.prometheus.write.v2.Request"
path: exporter/prometheusremotewriteexporter/exporter.go
user: login: ArthurSens
body: Isn't this too much detailed? An OTel collector user that doesn't have the knowledge that we have might not understand what this means, right? What do you think of just using 1.0.0, 2.0.0. Or even just 1.0/2.0? 
created_at: 2024-10-28T22:18:45Z
author_association: MEMBER
diff_hunk: @@ -52,6 +52,9 @@ type Config struct {
 
 	// SendMetadata controls whether prometheus metadata will be generated and sent
 	SendMetadata bool `mapstructure:"send_metadata"`
+
+	// RemoteWriteProtoMsg controls whether prometheus remote write v1 or v2 is sent
+	RemoteWriteProtoMsg RemoteWriteProtoMsg `mapstructure:"protobuf_message,omitempty"`
path: exporter/prometheusremotewriteexporter/config.go
user: login: ArthurSens
body: Similar to the other comment, maybe we can simplify this to the user
```suggestion
	RemoteWriteVersion RemoteWriteVersion `mapstructure:"remote_write_version,omitempty"`
```
created_at: 2024-10-28T22:19:27Z
author_association: MEMBER
diff_hunk: @@ -172,22 +214,28 @@ func (prwe *prwExporter) PushMetrics(ctx context.Context, md pmetric.Metrics) er
 	case <-prwe.closeChan:
 		return errors.New("shutdown has been called")
 	default:
-
-		tsMap, err := prometheusremotewrite.FromMetrics(md, prwe.exporterSettings)
-		if err != nil {
-			prwe.telemetry.recordTranslationFailure(ctx)
-			prwe.settings.Logger.Debug("failed to translate metrics, exporting remaining metrics", zap.Error(err), zap.Int("translated", len(tsMap)))
-		}
-
-		prwe.telemetry.recordTranslatedTimeSeries(ctx, len(tsMap))
-
-		var m []*prompb.MetricMetadata
-		if prwe.exporterSettings.SendMetadata {
-			m = prometheusremotewrite.OtelMetricsToMetadata(md, prwe.exporterSettings.AddMetricSuffixes)
+		if prwe.enableSendingRW2 {
path: exporter/prometheusremotewriteexporter/exporter.go
user: login: ArthurSens
body: This looks incorrect 🤔 

If `enableSendingRW2` is false, then we never call `pushMetricsV1`
created_at: 2024-10-28T22:20:58Z
author_association: MEMBER
diff_hunk: @@ -172,22 +214,28 @@ func (prwe *prwExporter) PushMetrics(ctx context.Context, md pmetric.Metrics) er
 	case <-prwe.closeChan:
 		return errors.New("shutdown has been called")
 	default:
-
-		tsMap, err := prometheusremotewrite.FromMetrics(md, prwe.exporterSettings)
-		if err != nil {
-			prwe.telemetry.recordTranslationFailure(ctx)
-			prwe.settings.Logger.Debug("failed to translate metrics, exporting remaining metrics", zap.Error(err), zap.Int("translated", len(tsMap)))
-		}
-
-		prwe.telemetry.recordTranslatedTimeSeries(ctx, len(tsMap))
-
-		var m []*prompb.MetricMetadata
-		if prwe.exporterSettings.SendMetadata {
-			m = prometheusremotewrite.OtelMetricsToMetadata(md, prwe.exporterSettings.AddMetricSuffixes)
+		if prwe.enableSendingRW2 {
path: exporter/prometheusremotewriteexporter/exporter.go
user: login: ArthurSens
body: Ah ok, I see the else few lines below. Hmmm, do we really need this boolean? Maybe we could get away only with the field that chooses the PRW version and a big warning that v2 isn't working yet?
created_at: 2024-10-28T22:22:43Z
author_association: MEMBER
diff_hunk: @@ -172,22 +214,28 @@ func (prwe *prwExporter) PushMetrics(ctx context.Context, md pmetric.Metrics) er
 	case <-prwe.closeChan:
 		return errors.New("shutdown has been called")
 	default:
-
-		tsMap, err := prometheusremotewrite.FromMetrics(md, prwe.exporterSettings)
-		if err != nil {
-			prwe.telemetry.recordTranslationFailure(ctx)
-			prwe.settings.Logger.Debug("failed to translate metrics, exporting remaining metrics", zap.Error(err), zap.Int("translated", len(tsMap)))
-		}
-
-		prwe.telemetry.recordTranslatedTimeSeries(ctx, len(tsMap))
-
-		var m []*prompb.MetricMetadata
-		if prwe.exporterSettings.SendMetadata {
-			m = prometheusremotewrite.OtelMetricsToMetadata(md, prwe.exporterSettings.AddMetricSuffixes)
+		if prwe.enableSendingRW2 {
path: exporter/prometheusremotewriteexporter/exporter.go
user: login: dashpole
body: Part of it may be the indentation.  For feature gates, I usually like to structure code like:

```golang
if !enableSendingRW2FeatureGate.IsEnabled() {
    // legacy behavior if the feature gate is disabled
    return prwe.pushMetricsV1(ctx, md)
}
// new behavior here
```

Because it makes it easy to remove the feature gate in the future.
created_at: 2024-10-29T12:26:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -53,6 +53,26 @@ func (p *prwTelemetryOtel) recordTranslatedTimeSeries(ctx context.Context, numTS
 	p.telemetryBuilder.ExporterPrometheusremotewriteTranslatedTimeSeries.Add(ctx, int64(numTS), metric.WithAttributes(p.otelAttrs...))
 }
 
+// RemoteWriteProtoMsg represents the known protobuf message for the remote write
+// 1.0 and 2.0 specs.
+type RemoteWriteProtoMsg string
+
+var (
+	// RemoteWriteProtoMsgV1 represents the `prometheus.WriteRequest` protobuf
+	// message introduced in the https://prometheus.io/docs/specs/remote_write_spec/,
+	// which will eventually be deprecated.
+	//
+	// NOTE: This string is used for both HTTP header values and config value, so don't change
+	// this reference.
+	RemoteWriteProtoMsgV1 RemoteWriteProtoMsg = "prometheus.WriteRequest"
+	// RemoteWriteProtoMsgV2 represents the `io.prometheus.write.v2.Request` protobuf
+	// message introduced in https://prometheus.io/docs/specs/remote_write_spec_2_0/
+	//
+	// NOTE: This string is used for both HTTP header values and config value, so don't change
+	// this reference.
+	RemoteWriteProtoMsgV2 RemoteWriteProtoMsg = "io.prometheus.write.v2.Request"
path: exporter/prometheusremotewriteexporter/exporter.go
user: login: jmichalek132
body: I do like copying what prometheus does, but maybe if we document in the readme it would work?
created_at: 2024-10-30T15:32:16Z
author_association: CONTRIBUTOR
diff_hunk: @@ -172,22 +214,28 @@ func (prwe *prwExporter) PushMetrics(ctx context.Context, md pmetric.Metrics) er
 	case <-prwe.closeChan:
 		return errors.New("shutdown has been called")
 	default:
-
-		tsMap, err := prometheusremotewrite.FromMetrics(md, prwe.exporterSettings)
-		if err != nil {
-			prwe.telemetry.recordTranslationFailure(ctx)
-			prwe.settings.Logger.Debug("failed to translate metrics, exporting remaining metrics", zap.Error(err), zap.Int("translated", len(tsMap)))
-		}
-
-		prwe.telemetry.recordTranslatedTimeSeries(ctx, len(tsMap))
-
-		var m []*prompb.MetricMetadata
-		if prwe.exporterSettings.SendMetadata {
-			m = prometheusremotewrite.OtelMetricsToMetadata(md, prwe.exporterSettings.AddMetricSuffixes)
+		if prwe.enableSendingRW2 {
path: exporter/prometheusremotewriteexporter/exporter.go
user: login: jmichalek132
body: applied this :).
created_at: 2024-10-30T15:37:21Z
author_association: CONTRIBUTOR
diff_hunk: @@ -7,15 +7,14 @@ import (
 	"testing"
 
 	"github.com/stretchr/testify/require"
-	"go.opentelemetry.io/otel/metric"
-	sdkmetric "go.opentelemetry.io/otel/sdk/metric"
-	"go.opentelemetry.io/otel/sdk/metric/metricdata"
-	"go.opentelemetry.io/otel/sdk/metric/metricdata/metricdatatest"
-
 	"go.opentelemetry.io/collector/component"
 	"go.opentelemetry.io/collector/config/configtelemetry"
 	"go.opentelemetry.io/collector/exporter"
 	"go.opentelemetry.io/collector/exporter/exportertest"
+	"go.opentelemetry.io/otel/metric"
path: exporter/prometheusremotewriteexporter/generated_component_telemetry_test.go
user: login: jmichalek132
body: fixed
created_at: 2024-10-30T15:43:28Z
author_association: CONTRIBUTOR
diff_hunk: @@ -6,15 +6,14 @@ import (
 	"testing"
 
 	"github.com/stretchr/testify/require"
+	"go.opentelemetry.io/collector/component"
path: exporter/prometheusremotewriteexporter/internal/metadata/generated_telemetry_test.go
user: login: jmichalek132
body: fixed
created_at: 2024-10-30T15:43:33Z
author_association: CONTRIBUTOR
diff_hunk: @@ -52,6 +53,9 @@ type Config struct {
 
 	// SendMetadata controls whether prometheus metadata will be generated and sent
 	SendMetadata bool `mapstructure:"send_metadata"`
+
+	// SendRW2 controls whether prometheus remote write v1 or v2 is sent
+	RemoteWriteProtoMsg prometheusremotewrite.RemoteWriteProtoMsg `mapstructure:"protobuf_message,omitempty"`
path: exporter/prometheusremotewriteexporter/config.go
user: login: jmichalek132
body: Yes https://github.com/prometheus/prometheus/blob/20fdc8f541274aa117dafe974c2118c07f05d8a6/config/config.go#L1166
created_at: 2024-10-30T15:46:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -5,11 +5,10 @@ package metadata
 import (
 	"errors"
 
-	"go.opentelemetry.io/otel/metric"
-	"go.opentelemetry.io/otel/trace"
-
 	"go.opentelemetry.io/collector/component"
 	"go.opentelemetry.io/collector/config/configtelemetry"
+	"go.opentelemetry.io/otel/metric"
path: exporter/prometheusremotewriteexporter/internal/metadata/generated_telemetry.go
user: login: jmichalek132
body: fixed
created_at: 2024-10-30T15:46:38Z
author_association: CONTRIBUTOR
diff_hunk: @@ -53,6 +53,26 @@ func (p *prwTelemetryOtel) recordTranslatedTimeSeries(ctx context.Context, numTS
 	p.telemetryBuilder.ExporterPrometheusremotewriteTranslatedTimeSeries.Add(ctx, int64(numTS), metric.WithAttributes(p.otelAttrs...))
 }
 
+// RemoteWriteProtoMsg represents the known protobuf message for the remote write
+// 1.0 and 2.0 specs.
+type RemoteWriteProtoMsg string
+
+var (
+	// RemoteWriteProtoMsgV1 represents the `prometheus.WriteRequest` protobuf
+	// message introduced in the https://prometheus.io/docs/specs/remote_write_spec/,
+	// which will eventually be deprecated.
+	//
+	// NOTE: This string is used for both HTTP header values and config value, so don't change
+	// this reference.
+	RemoteWriteProtoMsgV1 RemoteWriteProtoMsg = "prometheus.WriteRequest"
+	// RemoteWriteProtoMsgV2 represents the `io.prometheus.write.v2.Request` protobuf
+	// message introduced in https://prometheus.io/docs/specs/remote_write_spec_2_0/
+	//
+	// NOTE: This string is used for both HTTP header values and config value, so don't change
+	// this reference.
+	RemoteWriteProtoMsgV2 RemoteWriteProtoMsg = "io.prometheus.write.v2.Request"
path: exporter/prometheusremotewriteexporter/exporter.go
user: login: jmichalek132
body: In sync meeting we agreed to follow what prometheus does and go with this.

https://prometheus.io/docs/prometheus/latest/configuration/configuration/#remote_write
created_at: 2024-10-30T17:09:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -53,6 +53,26 @@ func (p *prwTelemetryOtel) recordTranslatedTimeSeries(ctx context.Context, numTS
 	p.telemetryBuilder.ExporterPrometheusremotewriteTranslatedTimeSeries.Add(ctx, int64(numTS), metric.WithAttributes(p.otelAttrs...))
 }
 
+// RemoteWriteProtoMsg represents the known protobuf message for the remote write
+// 1.0 and 2.0 specs.
+type RemoteWriteProtoMsg string
+
+var (
+	// RemoteWriteProtoMsgV1 represents the `prometheus.WriteRequest` protobuf
+	// message introduced in the https://prometheus.io/docs/specs/remote_write_spec/,
+	// which will eventually be deprecated.
+	//
+	// NOTE: This string is used for both HTTP header values and config value, so don't change
+	// this reference.
+	RemoteWriteProtoMsgV1 RemoteWriteProtoMsg = "prometheus.WriteRequest"
+	// RemoteWriteProtoMsgV2 represents the `io.prometheus.write.v2.Request` protobuf
+	// message introduced in https://prometheus.io/docs/specs/remote_write_spec_2_0/
+	//
+	// NOTE: This string is used for both HTTP header values and config value, so don't change
+	// this reference.
+	RemoteWriteProtoMsgV2 RemoteWriteProtoMsg = "io.prometheus.write.v2.Request"
path: exporter/prometheusremotewriteexporter/exporter.go
user: login: jmichalek132
body: @dashpole are you okay with this?
created_at: 2024-10-30T17:10:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -106,16 +128,18 @@ func newPRWExporter(cfg *Config, set exporter.Settings) (*prwExporter, error) {
 	userAgentHeader := fmt.Sprintf("%s/%s", strings.ReplaceAll(strings.ToLower(set.BuildInfo.Description), " ", "-"), set.BuildInfo.Version)
 
 	prwe := &prwExporter{
-		endpointURL:       endpointURL,
-		wg:                new(sync.WaitGroup),
-		closeChan:         make(chan struct{}),
-		userAgentHeader:   userAgentHeader,
-		maxBatchSizeBytes: cfg.MaxBatchSizeBytes,
-		concurrency:       cfg.RemoteWriteQueue.NumConsumers,
-		clientSettings:    &cfg.ClientConfig,
-		settings:          set.TelemetrySettings,
-		retrySettings:     cfg.BackOffConfig,
-		retryOnHTTP429:    retryOn429FeatureGate.IsEnabled(),
+		endpointURL:         endpointURL,
+		wg:                  new(sync.WaitGroup),
+		closeChan:           make(chan struct{}),
+		userAgentHeader:     userAgentHeader,
+		maxBatchSizeBytes:   cfg.MaxBatchSizeBytes,
+		concurrency:         cfg.RemoteWriteQueue.NumConsumers,
+		clientSettings:      &cfg.ClientConfig,
+		settings:            set.TelemetrySettings,
+		retrySettings:       cfg.BackOffConfig,
+		retryOnHTTP429:      retryOn429FeatureGate.IsEnabled(),
+		enableSendingRW2:    enableSendingRW2FeatureGate.IsEnabled(),
+		RemoteWriteProtoMsg: cfg.RemoteWriteProtoMsg,
path: exporter/prometheusremotewriteexporter/exporter.go
user: login: aknuds1
body: Instead of setting `cfg.RemoteWriteProtoMsg` directly, you should set `RemoteWriteProtoMsgV1 ` if `!enableSendingRW2`. That will save you from having to check the same elsewhere in the code, and things will be a lot simpler.
created_at: 2024-10-30T17:27:25Z
author_association: CONTRIBUTOR
diff_hunk: @@ -172,22 +214,27 @@ func (prwe *prwExporter) PushMetrics(ctx context.Context, md pmetric.Metrics) er
 	case <-prwe.closeChan:
 		return errors.New("shutdown has been called")
 	default:
-
-		tsMap, err := prometheusremotewrite.FromMetrics(md, prwe.exporterSettings)
-		if err != nil {
-			prwe.telemetry.recordTranslationFailure(ctx)
-			prwe.settings.Logger.Debug("failed to translate metrics, exporting remaining metrics", zap.Error(err), zap.Int("translated", len(tsMap)))
+		if !prwe.enableSendingRW2 {
+			return prwe.pushMetricsV1(ctx, md)
 		}
+		switch prwe.RemoteWriteProtoMsg {
+		case RemoteWriteProtoMsgV1:
+			return prwe.pushMetricsV1(ctx, md)
+		case RemoteWriteProtoMsgV2:
+			// RW2 case
+			tsMap, symbolsTable, err := prometheusremotewrite.FromMetricsV2(md, prwe.exporterSettings)
path: exporter/prometheusremotewriteexporter/exporter.go
user: login: aknuds1
body: Why not make a corresponding `prwe.pushMetricsV2` method for symmetry?
created_at: 2024-10-30T17:29:13Z
author_association: CONTRIBUTOR
diff_hunk: @@ -53,6 +53,26 @@ func (p *prwTelemetryOtel) recordTranslatedTimeSeries(ctx context.Context, numTS
 	p.telemetryBuilder.ExporterPrometheusremotewriteTranslatedTimeSeries.Add(ctx, int64(numTS), metric.WithAttributes(p.otelAttrs...))
 }
 
+// RemoteWriteProtoMsg represents the known protobuf message for the remote write
+// 1.0 and 2.0 specs.
+type RemoteWriteProtoMsg string
+
+var (
+	// RemoteWriteProtoMsgV1 represents the `prometheus.WriteRequest` protobuf
+	// message introduced in the https://prometheus.io/docs/specs/remote_write_spec/,
+	// which will eventually be deprecated.
+	//
+	// NOTE: This string is used for both HTTP header values and config value, so don't change
+	// this reference.
+	RemoteWriteProtoMsgV1 RemoteWriteProtoMsg = "prometheus.WriteRequest"
+	// RemoteWriteProtoMsgV2 represents the `io.prometheus.write.v2.Request` protobuf
+	// message introduced in https://prometheus.io/docs/specs/remote_write_spec_2_0/
+	//
+	// NOTE: This string is used for both HTTP header values and config value, so don't change
+	// this reference.
+	RemoteWriteProtoMsgV2 RemoteWriteProtoMsg = "io.prometheus.write.v2.Request"
path: exporter/prometheusremotewriteexporter/exporter.go
user: login: dashpole
body: yes, i'm OK with this.
created_at: 2024-10-30T19:21:25Z
author_association: CONTRIBUTOR
diff_hunk: @@ -52,6 +52,9 @@ type Config struct {
 
 	// SendMetadata controls whether prometheus metadata will be generated and sent
 	SendMetadata bool `mapstructure:"send_metadata"`
+
+	// RemoteWriteProtoMsg controls whether prometheus remote write v1 or v2 is sent
+	RemoteWriteProtoMsg RemoteWriteProtoMsg `mapstructure:"protobuf_message,omitempty"`
path: exporter/prometheusremotewriteexporter/config.go
user: login: jmichalek132
body: Keeping the original as per https://github.com/open-telemetry/opentelemetry-collector-contrib/pull/35888#discussion_r1823250027
created_at: 2024-10-30T19:30:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -172,22 +214,27 @@ func (prwe *prwExporter) PushMetrics(ctx context.Context, md pmetric.Metrics) er
 	case <-prwe.closeChan:
 		return errors.New("shutdown has been called")
 	default:
-
-		tsMap, err := prometheusremotewrite.FromMetrics(md, prwe.exporterSettings)
-		if err != nil {
-			prwe.telemetry.recordTranslationFailure(ctx)
-			prwe.settings.Logger.Debug("failed to translate metrics, exporting remaining metrics", zap.Error(err), zap.Int("translated", len(tsMap)))
+		if !prwe.enableSendingRW2 {
+			return prwe.pushMetricsV1(ctx, md)
 		}
+		switch prwe.RemoteWriteProtoMsg {
+		case RemoteWriteProtoMsgV1:
+			return prwe.pushMetricsV1(ctx, md)
+		case RemoteWriteProtoMsgV2:
+			// RW2 case
+			tsMap, symbolsTable, err := prometheusremotewrite.FromMetricsV2(md, prwe.exporterSettings)
path: exporter/prometheusremotewriteexporter/exporter.go
user: login: jmichalek132
body: Done.
created_at: 2024-10-31T16:11:16Z
author_association: CONTRIBUTOR
diff_hunk: @@ -106,16 +128,18 @@ func newPRWExporter(cfg *Config, set exporter.Settings) (*prwExporter, error) {
 	userAgentHeader := fmt.Sprintf("%s/%s", strings.ReplaceAll(strings.ToLower(set.BuildInfo.Description), " ", "-"), set.BuildInfo.Version)
 
 	prwe := &prwExporter{
-		endpointURL:       endpointURL,
-		wg:                new(sync.WaitGroup),
-		closeChan:         make(chan struct{}),
-		userAgentHeader:   userAgentHeader,
-		maxBatchSizeBytes: cfg.MaxBatchSizeBytes,
-		concurrency:       cfg.RemoteWriteQueue.NumConsumers,
-		clientSettings:    &cfg.ClientConfig,
-		settings:          set.TelemetrySettings,
-		retrySettings:     cfg.BackOffConfig,
-		retryOnHTTP429:    retryOn429FeatureGate.IsEnabled(),
+		endpointURL:         endpointURL,
+		wg:                  new(sync.WaitGroup),
+		closeChan:           make(chan struct{}),
+		userAgentHeader:     userAgentHeader,
+		maxBatchSizeBytes:   cfg.MaxBatchSizeBytes,
+		concurrency:         cfg.RemoteWriteQueue.NumConsumers,
+		clientSettings:      &cfg.ClientConfig,
+		settings:            set.TelemetrySettings,
+		retrySettings:       cfg.BackOffConfig,
+		retryOnHTTP429:      retryOn429FeatureGate.IsEnabled(),
+		enableSendingRW2:    enableSendingRW2FeatureGate.IsEnabled(),
+		RemoteWriteProtoMsg: cfg.RemoteWriteProtoMsg,
path: exporter/prometheusremotewriteexporter/exporter.go
user: login: jmichalek132
body: Do we want to go with this approach or this one David suggested in another comment?
https://github.com/open-telemetry/opentelemetry-collector-contrib/pull/35888#discussion_r1820700091
created_at: 2024-10-31T17:07:43Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,27 @@
+# Use this changelog template to create an entry for release notes.
+
+# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix'
+change_type: enhancement
+
+# The name of the component, or a single word describing the area of concern, (e.g. filelogreceiver)
+component: prometheusremotewriteexporter
+
+# A brief description of the change.  Surround your text with quotes ("") if it needs to start with a backtick (`).
+note: Add `exporter.prometheusremotewritexporter.enableSendingRW2` feature gate to and configure the exporter to send Prometheus remote write 2.0 version.
+
+# Mandatory: One or more tracking issues related to the change. You can use the PR number here if no issue exists.
+issues: [33661]
+
+# (Optional) One or more lines of additional information to render under the primary note.
+# These lines will be padded with 2 spaces and then inserted directly into the document.
+# Use pipe (|) for multiline entries.
+subtext: As the the support for rw2 is not fully implemented (this is only the initial implementation) we only recommend testing this.
path: .chloggen/jm-prom-rw-exporter-add-support-for-rw2.yaml
user: login: jmichalek132
body: Would welcome feedback on the subtext. Basically I feel like  we should call out the partial implementation and that people should only test it, not use it in production since it's not ready but also lot of things are not implemented.
created_at: 2024-10-31T17:27:36Z
author_association: CONTRIBUTOR
diff_hunk: @@ -106,16 +128,18 @@ func newPRWExporter(cfg *Config, set exporter.Settings) (*prwExporter, error) {
 	userAgentHeader := fmt.Sprintf("%s/%s", strings.ReplaceAll(strings.ToLower(set.BuildInfo.Description), " ", "-"), set.BuildInfo.Version)
 
 	prwe := &prwExporter{
-		endpointURL:       endpointURL,
-		wg:                new(sync.WaitGroup),
-		closeChan:         make(chan struct{}),
-		userAgentHeader:   userAgentHeader,
-		maxBatchSizeBytes: cfg.MaxBatchSizeBytes,
-		concurrency:       cfg.RemoteWriteQueue.NumConsumers,
-		clientSettings:    &cfg.ClientConfig,
-		settings:          set.TelemetrySettings,
-		retrySettings:     cfg.BackOffConfig,
-		retryOnHTTP429:    retryOn429FeatureGate.IsEnabled(),
+		endpointURL:         endpointURL,
+		wg:                  new(sync.WaitGroup),
+		closeChan:           make(chan struct{}),
+		userAgentHeader:     userAgentHeader,
+		maxBatchSizeBytes:   cfg.MaxBatchSizeBytes,
+		concurrency:         cfg.RemoteWriteQueue.NumConsumers,
+		clientSettings:      &cfg.ClientConfig,
+		settings:            set.TelemetrySettings,
+		retrySettings:       cfg.BackOffConfig,
+		retryOnHTTP429:      retryOn429FeatureGate.IsEnabled(),
+		enableSendingRW2:    enableSendingRW2FeatureGate.IsEnabled(),
+		RemoteWriteProtoMsg: cfg.RemoteWriteProtoMsg,
path: exporter/prometheusremotewriteexporter/exporter.go
user: login: dashpole
body: Do we think we will ever support more than two protocols at once?  I don't think so... If that is the case, a single bool is OK here, which encompasses the user config and the feature gate.
created_at: 2024-10-31T17:32:14Z
author_association: CONTRIBUTOR
diff_hunk: @@ -106,16 +128,18 @@ func newPRWExporter(cfg *Config, set exporter.Settings) (*prwExporter, error) {
 	userAgentHeader := fmt.Sprintf("%s/%s", strings.ReplaceAll(strings.ToLower(set.BuildInfo.Description), " ", "-"), set.BuildInfo.Version)
 
 	prwe := &prwExporter{
-		endpointURL:       endpointURL,
-		wg:                new(sync.WaitGroup),
-		closeChan:         make(chan struct{}),
-		userAgentHeader:   userAgentHeader,
-		maxBatchSizeBytes: cfg.MaxBatchSizeBytes,
-		concurrency:       cfg.RemoteWriteQueue.NumConsumers,
-		clientSettings:    &cfg.ClientConfig,
-		settings:          set.TelemetrySettings,
-		retrySettings:     cfg.BackOffConfig,
-		retryOnHTTP429:    retryOn429FeatureGate.IsEnabled(),
+		endpointURL:         endpointURL,
+		wg:                  new(sync.WaitGroup),
+		closeChan:           make(chan struct{}),
+		userAgentHeader:     userAgentHeader,
+		maxBatchSizeBytes:   cfg.MaxBatchSizeBytes,
+		concurrency:         cfg.RemoteWriteQueue.NumConsumers,
+		clientSettings:      &cfg.ClientConfig,
+		settings:            set.TelemetrySettings,
+		retrySettings:       cfg.BackOffConfig,
+		retryOnHTTP429:      retryOn429FeatureGate.IsEnabled(),
+		enableSendingRW2:    enableSendingRW2FeatureGate.IsEnabled(),
+		RemoteWriteProtoMsg: cfg.RemoteWriteProtoMsg,
path: exporter/prometheusremotewriteexporter/exporter.go
user: login: ArthurSens
body: I think we'll support more than 2 protocols, yes. Mostly because PRW will continue to evolve and not all receivers will be able to implement new versions as they come up. 
created_at: 2024-10-31T17:37:42Z
author_association: MEMBER
diff_hunk: @@ -84,14 +85,15 @@ func createDefaultConfig() component.Config {
 	clientConfig.Timeout = exporterhelper.NewDefaultTimeoutConfig().Timeout
 
 	return &Config{
-		Namespace:         "",
-		ExternalLabels:    map[string]string{},
-		MaxBatchSizeBytes: 3000000,
-		TimeoutSettings:   exporterhelper.NewDefaultTimeoutConfig(),
-		BackOffConfig:     retrySettings,
-		AddMetricSuffixes: true,
-		SendMetadata:      false,
-		ClientConfig:      clientConfig,
+		Namespace:           "",
+		ExternalLabels:      map[string]string{},
+		MaxBatchSizeBytes:   3000000,
+		TimeoutSettings:     exporterhelper.NewDefaultTimeoutConfig(),
+		BackOffConfig:       retrySettings,
+		AddMetricSuffixes:   true,
+		SendMetadata:        false,
+		RemoteWriteProtoMsg: prometheusremotewrite.RemoteWriteProtoMsgV1,
path: exporter/prometheusremotewriteexporter/factory.go
user: login: jmichalek132
body: Done.
created_at: 2024-10-31T17:47:18Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,27 @@
+# Use this changelog template to create an entry for release notes.
+
+# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix'
+change_type: enhancement
+
+# The name of the component, or a single word describing the area of concern, (e.g. filelogreceiver)
+component: prometheusremotewriteexporter
+
+# A brief description of the change.  Surround your text with quotes ("") if it needs to start with a backtick (`).
+note: Add `exporter.prometheusremotewritexporter.enableSendingRW2` feature gate to and configure the exporter to send Prometheus remote write 2.0 version.
+
+# Mandatory: One or more tracking issues related to the change. You can use the PR number here if no issue exists.
+issues: [33661]
+
+# (Optional) One or more lines of additional information to render under the primary note.
+# These lines will be padded with 2 spaces and then inserted directly into the document.
+# Use pipe (|) for multiline entries.
+subtext: As the the support for rw2 is not fully implemented (this is only the initial implementation) we only recommend testing this.
path: .chloggen/jm-prom-rw-exporter-add-support-for-rw2.yaml
user: login: dashpole
body: You can keep it simpler: PRW 2.0 support for the exporter is still under development.
created_at: 2024-10-31T17:54:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,27 @@
+# Use this changelog template to create an entry for release notes.
+
+# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix'
+change_type: enhancement
+
+# The name of the component, or a single word describing the area of concern, (e.g. filelogreceiver)
+component: prometheusremotewriteexporter
+
+# A brief description of the change.  Surround your text with quotes ("") if it needs to start with a backtick (`).
+note: Add `exporter.prometheusremotewritexporter.enableSendingRW2` feature gate to and configure the exporter to send Prometheus remote write 2.0 version.
path: .chloggen/jm-prom-rw-exporter-add-support-for-rw2.yaml
user: login: ArthurSens
body: ```suggestion
note: Add `exporter.prometheusremotewritexporter.enableSendingRW2` feature gate and configure the exporter to send Prometheus remote write 2.0 version.
```

created_at: 2024-10-31T22:47:44Z
author_association: MEMBER
diff_hunk: @@ -26,6 +27,13 @@ var retryOn429FeatureGate = featuregate.GlobalRegistry().MustRegister(
 	featuregate.WithRegisterDescription("When enabled, the Prometheus remote write exporter will retry 429 http status code. Requires exporter.prometheusremotewritexporter.metrics.RetryOn429 to be enabled."),
 )
 
+var enableSendingRW2FeatureGate = featuregate.GlobalRegistry().MustRegister(
+	"exporter.prometheusremotewritexporter.enableSendingRW2",
+	featuregate.StageAlpha,
+	featuregate.WithRegisterFromVersion("v0.113.0"),
+	featuregate.WithRegisterDescription("When enabled, the Prometheus remote write exporter will support sending rw2 if configured. Requires exporter.prometheusremotewritexporter.enableSendingRW2 to be enabled."),
path: exporter/prometheusremotewriteexporter/factory.go
user: login: ArthurSens
body: ```suggestion
	featuregate.WithRegisterDescription("When enabled, the Prometheus remote write exporter will support sending rw2. Extra configuration is still required besides enabling this feature gate."),
```
created_at: 2024-11-01T17:32:33Z
author_association: MEMBER
diff_hunk: @@ -26,6 +27,13 @@ var retryOn429FeatureGate = featuregate.GlobalRegistry().MustRegister(
 	featuregate.WithRegisterDescription("When enabled, the Prometheus remote write exporter will retry 429 http status code. Requires exporter.prometheusremotewritexporter.metrics.RetryOn429 to be enabled."),
 )
 
+var enableSendingRW2FeatureGate = featuregate.GlobalRegistry().MustRegister(
+	"exporter.prometheusremotewritexporter.enableSendingRW2",
+	featuregate.StageAlpha,
+	featuregate.WithRegisterFromVersion("v0.113.0"),
+	featuregate.WithRegisterDescription("When enabled, the Prometheus remote write exporter will support sending rw2 if configured. Requires exporter.prometheusremotewritexporter.enableSendingRW2 to be enabled."),
path: exporter/prometheusremotewriteexporter/factory.go
user: login: ArthurSens
body: Sounds weird to say that we need to enable the feature gate for it to take effect, in the description of the feature gate 🤔. 
created_at: 2024-11-01T17:33:28Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,113 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+package prometheusremotewriteexporter // import "github.com/open-telemetry/opentelemetry-collector-contrib/exporter/prometheusremotewriteexporter"
+
+import (
+	"context"
+	"math"
+	"sync"
+
+	"github.com/gogo/protobuf/proto"
+	writev2 "github.com/prometheus/prometheus/prompb/io/prometheus/write/v2"
+	"go.opentelemetry.io/collector/consumer/consumererror"
+	"go.opentelemetry.io/collector/pdata/pmetric"
+	"go.uber.org/multierr"
+	"go.uber.org/zap"
+
+	"github.com/open-telemetry/opentelemetry-collector-contrib/pkg/translator/prometheusremotewrite"
+)
+
+func (prwe *prwExporter) pushMetricsV2(ctx context.Context, md pmetric.Metrics) error {
+	tsMap, symbolsTable, err := prometheusremotewrite.FromMetricsV2(md, prwe.exporterSettings)
+
+	prwe.telemetry.recordTranslatedTimeSeries(ctx, len(tsMap))
+
+	if err != nil {
+		prwe.telemetry.recordTranslationFailure(ctx)
+		prwe.settings.Logger.Debug("failed to translate metrics, exporting remaining metrics", zap.Error(err), zap.Int("translated", len(tsMap)))
+	}
+	// Call export even if a conversion error, since there may be points that were successfully converted.
+	return prwe.handleExportV2(ctx, symbolsTable, tsMap)
+}
+
+// TODO update comment
+// export sends a Snappy-compressed WriteRequest containing TimeSeries to a remote write endpoint in order
+func (prwe *prwExporter) exportV2(ctx context.Context, requests []*writev2.Request) error {
+	input := make(chan *writev2.Request, len(requests))
+	for _, request := range requests {
+		input <- request
+	}
+	close(input)
+
+	var wg sync.WaitGroup
+
+	concurrencyLimit := int(math.Min(float64(prwe.concurrency), float64(len(requests))))
+	wg.Add(concurrencyLimit) // used to wait for workers to be finished
+
+	var mu sync.Mutex
+	var errs error
+	// Run concurrencyLimit of workers until there
+	// is no more requests to execute in the input channel.
+	for i := 0; i < concurrencyLimit; i++ {
+		go func() {
+			defer wg.Done()
+			for {
+				select {
+				case <-ctx.Done(): // Check firstly to ensure that the context wasn't cancelled.
+					return
+
+				case request, ok := <-input:
+					if !ok {
+						return
+					}
+
+					// Uses proto.Marshal to convert the WriteRequest into bytes array
+					data, errMarshal := proto.Marshal(request)
+					if errMarshal != nil {
+						errs = multierr.Append(errs, consumererror.NewPermanent(errMarshal))
+					}
+
+					if errExecute := prwe.execute(ctx, data); errExecute != nil {
+						mu.Lock()
+						errs = multierr.Append(errs, consumererror.NewPermanent(errExecute))
+						mu.Unlock()
+					}
+				}
+			}
+		}()
+	}
+	wg.Wait()
+
+	return errs
+}
+
+func (prwe *prwExporter) handleExportV2(ctx context.Context, symbolsTable writev2.SymbolsTable, tsMap map[string]*writev2.TimeSeries) error {
+	// There are no metrics to export, so return.
+	if len(tsMap) == 0 {
+		return nil
+	}
+
+	// TODO implement batching
+	// TODO how do we handle symbolsTable with batching?
+	requests := make([]*writev2.Request, 0)
+	tsArray := make([]writev2.TimeSeries, 0, len(tsMap))
+	for _, v := range tsMap {
+		tsArray = append(tsArray, *v)
+	}
+
+	requests = append(requests, &writev2.Request{
+		// TODO sort
+		// Prometheus requires time series to be sorted by Timestamp to avoid out of order problems.
+		// See:
+		// * https://github.com/open-telemetry/wg-prometheus/issues/10
+		// * https://github.com/open-telemetry/opentelemetry-collector/issues/2315
+		//Timeseries: orderBySampleTimestamp(tsArray),
path: exporter/prometheusremotewriteexporter/exporter_v2.go
user: login: ArthurSens
body: Is this something that we still need to do in this PR?
created_at: 2024-11-01T17:33:59Z
author_association: MEMBER
diff_hunk: @@ -103,19 +106,26 @@ func newPRWExporter(cfg *Config, set exporter.Settings) (*prwExporter, error) {
 		return nil, err
 	}
 
+	err = config.RemoteWriteProtoMsg.Validate(cfg.RemoteWriteProtoMsg)
path: exporter/prometheusremotewriteexporter/exporter.go
user: login: ArthurSens
body: If the protomessage is not set, an empty string, would this fail? Should we have a test to make sure we don't break people once we get this in?
created_at: 2024-11-01T17:40:07Z
author_association: MEMBER
diff_hunk: @@ -79,7 +80,8 @@ func TestLoadConfig(t *testing.T) {
 				TargetInfo: &TargetInfo{
 					Enabled: true,
 				},
-				CreatedMetric: &CreatedMetric{Enabled: true},
+				CreatedMetric:       &CreatedMetric{Enabled: true},
+				RemoteWriteProtoMsg: config.RemoteWriteProtoMsgV1,
path: exporter/prometheusremotewriteexporter/config_test.go
user: login: ArthurSens
body: If you added this here, it means the we can't start the collector with an empty string, right?
created_at: 2024-11-01T17:41:09Z
author_association: MEMBER
diff_hunk: @@ -173,21 +200,25 @@ func (prwe *prwExporter) PushMetrics(ctx context.Context, md pmetric.Metrics) er
 		return errors.New("shutdown has been called")
 	default:
 
-		tsMap, err := prometheusremotewrite.FromMetrics(md, prwe.exporterSettings)
-		if err != nil {
-			prwe.telemetry.recordTranslationFailure(ctx)
-			prwe.settings.Logger.Debug("failed to translate metrics, exporting remaining metrics", zap.Error(err), zap.Int("translated", len(tsMap)))
+		// If feature flag not enabled support only RW1
+		// TODO remove when feature flag removed
+		if !prwe.enableSendingRW2 {
+			return prwe.pushMetricsV1(ctx, md)
 		}
 
-		prwe.telemetry.recordTranslatedTimeSeries(ctx, len(tsMap))
+		// If feature flag enabled check if we want to send RW1 or RW2
+		switch prwe.RemoteWriteProtoMsg {
+		case config.RemoteWriteProtoMsgV1:
+			// Rw1 case
+			return prwe.pushMetricsV1(ctx, md)
+		case config.RemoteWriteProtoMsgV2:
+			// RW2 case
+			return prwe.pushMetricsV2(ctx, md)
 
-		var m []*prompb.MetricMetadata
-		if prwe.exporterSettings.SendMetadata {
-			m = prometheusremotewrite.OtelMetricsToMetadata(md, prwe.exporterSettings.AddMetricSuffixes)
+		default:
+			return errors.New("unexpected rw protobuf message")
path: exporter/prometheusremotewriteexporter/exporter.go
user: login: ArthurSens
body: ```suggestion
			return errors.New("unexpected remote-write protobuf message")
```
created_at: 2024-11-01T17:44:01Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,113 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+package prometheusremotewriteexporter // import "github.com/open-telemetry/opentelemetry-collector-contrib/exporter/prometheusremotewriteexporter"
+
+import (
+	"context"
+	"math"
+	"sync"
+
+	"github.com/gogo/protobuf/proto"
+	writev2 "github.com/prometheus/prometheus/prompb/io/prometheus/write/v2"
+	"go.opentelemetry.io/collector/consumer/consumererror"
+	"go.opentelemetry.io/collector/pdata/pmetric"
+	"go.uber.org/multierr"
+	"go.uber.org/zap"
+
+	"github.com/open-telemetry/opentelemetry-collector-contrib/pkg/translator/prometheusremotewrite"
+)
+
+func (prwe *prwExporter) pushMetricsV2(ctx context.Context, md pmetric.Metrics) error {
+	tsMap, symbolsTable, err := prometheusremotewrite.FromMetricsV2(md, prwe.exporterSettings)
+
+	prwe.telemetry.recordTranslatedTimeSeries(ctx, len(tsMap))
+
+	if err != nil {
+		prwe.telemetry.recordTranslationFailure(ctx)
+		prwe.settings.Logger.Debug("failed to translate metrics, exporting remaining metrics", zap.Error(err), zap.Int("translated", len(tsMap)))
+	}
+	// Call export even if a conversion error, since there may be points that were successfully converted.
+	return prwe.handleExportV2(ctx, symbolsTable, tsMap)
+}
+
+// TODO update comment
+// export sends a Snappy-compressed WriteRequest containing TimeSeries to a remote write endpoint in order
+func (prwe *prwExporter) exportV2(ctx context.Context, requests []*writev2.Request) error {
+	input := make(chan *writev2.Request, len(requests))
+	for _, request := range requests {
+		input <- request
+	}
+	close(input)
+
+	var wg sync.WaitGroup
+
+	concurrencyLimit := int(math.Min(float64(prwe.concurrency), float64(len(requests))))
+	wg.Add(concurrencyLimit) // used to wait for workers to be finished
+
+	var mu sync.Mutex
+	var errs error
+	// Run concurrencyLimit of workers until there
+	// is no more requests to execute in the input channel.
+	for i := 0; i < concurrencyLimit; i++ {
+		go func() {
+			defer wg.Done()
+			for {
+				select {
+				case <-ctx.Done(): // Check firstly to ensure that the context wasn't cancelled.
+					return
+
+				case request, ok := <-input:
+					if !ok {
+						return
+					}
+
+					// Uses proto.Marshal to convert the WriteRequest into bytes array
+					data, errMarshal := proto.Marshal(request)
+					if errMarshal != nil {
+						errs = multierr.Append(errs, consumererror.NewPermanent(errMarshal))
+					}
+
+					if errExecute := prwe.execute(ctx, data); errExecute != nil {
+						mu.Lock()
+						errs = multierr.Append(errs, consumererror.NewPermanent(errExecute))
+						mu.Unlock()
+					}
+				}
+			}
+		}()
+	}
+	wg.Wait()
+
+	return errs
+}
+
+func (prwe *prwExporter) handleExportV2(ctx context.Context, symbolsTable writev2.SymbolsTable, tsMap map[string]*writev2.TimeSeries) error {
+	// There are no metrics to export, so return.
+	if len(tsMap) == 0 {
+		return nil
+	}
+
+	// TODO implement batching
+	// TODO how do we handle symbolsTable with batching?
+	requests := make([]*writev2.Request, 0)
+	tsArray := make([]writev2.TimeSeries, 0, len(tsMap))
+	for _, v := range tsMap {
+		tsArray = append(tsArray, *v)
+	}
+
+	requests = append(requests, &writev2.Request{
+		// TODO sort
+		// Prometheus requires time series to be sorted by Timestamp to avoid out of order problems.
+		// See:
+		// * https://github.com/open-telemetry/wg-prometheus/issues/10
+		// * https://github.com/open-telemetry/opentelemetry-collector/issues/2315
+		//Timeseries: orderBySampleTimestamp(tsArray),
path: exporter/prometheusremotewriteexporter/exporter_v2.go
user: login: ArthurSens
body: Ok I just tested by building this PR and using telemetrygen to get metrics in prometheus.

I do see the error logs in Prometheus:
```
time=2024-11-01T15:11:42.637-03:00 level=ERROR source=/Users/arthursens/Documents/projetos/prometheus/storage/remote/write_handler.go:295 msg="Out of order sample from remote write" component=web err="duplicate sample for timestamp 1730484701374; overrides not allowed: existing 29914, new value 29917" series="{__name__=\"gen\"}" timestamp=1730484701374
```

Let's fix this in a followup PR though, no problems that it's broken now :)
created_at: 2024-11-01T18:14:13Z
author_association: MEMBER
diff_hunk: @@ -727,6 +755,14 @@ func Test_PushMetrics(t *testing.T) {
 						BackOffConfig: retrySettings,
 					}
 
+					if tt.enableSendingRW2 {
+						cfg.RemoteWriteProtoMsg = config.RemoteWriteProtoMsgV2
+						err := featuregate.GlobalRegistry().Set("exporter.prometheusremotewritexporter.enableSendingRW2", true)
path: exporter/prometheusremotewriteexporter/exporter_test.go
user: login: ArthurSens
body: Since this is a global variable, if you enable the feature gate here it will take effect in all subsequent tests.

A pattern I like to use is:

```go
oldValue := enableSendingRW2FeatureGate.IsEnabled()
testutil.SetFeatureGateForTest(t, enableSendingRW2FeatureGate, tt.enableSendingRW2)
defer testutil.SetFeatureGateForTest(t, enableSendingRW2FeatureGate, oldValue)
```
created_at: 2024-11-01T18:19:17Z
author_association: MEMBER
diff_hunk: @@ -103,19 +106,26 @@ func newPRWExporter(cfg *Config, set exporter.Settings) (*prwExporter, error) {
 		return nil, err
 	}
 
+	err = config.RemoteWriteProtoMsg.Validate(cfg.RemoteWriteProtoMsg)
+	if err != nil {
+		return nil, err
+	}
+
 	userAgentHeader := fmt.Sprintf("%s/%s", strings.ReplaceAll(strings.ToLower(set.BuildInfo.Description), " ", "-"), set.BuildInfo.Version)
 
 	prwe := &prwExporter{
path: exporter/prometheusremotewriteexporter/exporter.go
user: login: ArthurSens
body: Could we also add a logline somewhere in this function that tells us which version of remotewrite we're using? When running the collector from this PR I have no idea if I set things right and I'm using the protocol I wanted 😬 
created_at: 2024-11-01T18:22:35Z
author_association: MEMBER
diff_hunk: @@ -103,19 +106,26 @@ func newPRWExporter(cfg *Config, set exporter.Settings) (*prwExporter, error) {
 		return nil, err
 	}
 
+	err = config.RemoteWriteProtoMsg.Validate(cfg.RemoteWriteProtoMsg)
+	if err != nil {
+		return nil, err
+	}
+
 	userAgentHeader := fmt.Sprintf("%s/%s", strings.ReplaceAll(strings.ToLower(set.BuildInfo.Description), " ", "-"), set.BuildInfo.Version)
 
 	prwe := &prwExporter{
path: exporter/prometheusremotewriteexporter/exporter.go
user: login: jmichalek132
body: Do we want an info line at start saying which version will be used and maybe a debug one for each request?
created_at: 2024-11-12T14:25:27Z
author_association: CONTRIBUTOR
diff_hunk: @@ -103,19 +106,26 @@ func newPRWExporter(cfg *Config, set exporter.Settings) (*prwExporter, error) {
 		return nil, err
 	}
 
+	err = config.RemoteWriteProtoMsg.Validate(cfg.RemoteWriteProtoMsg)
path: exporter/prometheusremotewriteexporter/exporter.go
user: login: jmichalek132
body: It doesn't because we have v1 set in the default config https://github.com/jmichalek132/opentelemetry-collector-contrib/blob/4f04e82994bb865c8d4cf5e649d0e21311e0aaa7/exporter/prometheusremotewriteexporter/factory.go#L102
created_at: 2024-11-12T14:40:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -103,19 +106,26 @@ func newPRWExporter(cfg *Config, set exporter.Settings) (*prwExporter, error) {
 		return nil, err
 	}
 
+	err = config.RemoteWriteProtoMsg.Validate(cfg.RemoteWriteProtoMsg)
path: exporter/prometheusremotewriteexporter/exporter.go
user: login: jmichalek132
body: Sure I can add a test case for it if you think it's needed.
created_at: 2024-11-12T14:41:21Z
author_association: CONTRIBUTOR
diff_hunk: @@ -79,7 +80,8 @@ func TestLoadConfig(t *testing.T) {
 				TargetInfo: &TargetInfo{
 					Enabled: true,
 				},
-				CreatedMetric: &CreatedMetric{Enabled: true},
+				CreatedMetric:       &CreatedMetric{Enabled: true},
+				RemoteWriteProtoMsg: config.RemoteWriteProtoMsgV1,
path: exporter/prometheusremotewriteexporter/config_test.go
user: login: jmichalek132
body: As mentioned here we have a v1 as the default https://github.com/open-telemetry/opentelemetry-collector-contrib/pull/35888/#discussion_r1838223475
created_at: 2024-11-12T14:42:56Z
author_association: CONTRIBUTOR
diff_hunk: @@ -727,6 +755,14 @@ func Test_PushMetrics(t *testing.T) {
 						BackOffConfig: retrySettings,
 					}
 
+					if tt.enableSendingRW2 {
+						cfg.RemoteWriteProtoMsg = config.RemoteWriteProtoMsgV2
+						err := featuregate.GlobalRegistry().Set("exporter.prometheusremotewritexporter.enableSendingRW2", true)
path: exporter/prometheusremotewriteexporter/exporter_test.go
user: login: jmichalek132
body: fixed.
created_at: 2024-11-12T14:48:06Z
author_association: CONTRIBUTOR
diff_hunk: @@ -103,19 +106,26 @@ func newPRWExporter(cfg *Config, set exporter.Settings) (*prwExporter, error) {
 		return nil, err
 	}
 
+	err = config.RemoteWriteProtoMsg.Validate(cfg.RemoteWriteProtoMsg)
+	if err != nil {
+		return nil, err
+	}
+
 	userAgentHeader := fmt.Sprintf("%s/%s", strings.ReplaceAll(strings.ToLower(set.BuildInfo.Description), " ", "-"), set.BuildInfo.Version)
 
 	prwe := &prwExporter{
path: exporter/prometheusremotewriteexporter/exporter.go
user: login: dashpole
body: Will the version change between start and each request?  If not, lets just have an info one at start
created_at: 2024-11-12T14:51:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -103,19 +106,26 @@ func newPRWExporter(cfg *Config, set exporter.Settings) (*prwExporter, error) {
 		return nil, err
 	}
 
+	err = config.RemoteWriteProtoMsg.Validate(cfg.RemoteWriteProtoMsg)
+	if err != nil {
+		return nil, err
+	}
+
 	userAgentHeader := fmt.Sprintf("%s/%s", strings.ReplaceAll(strings.ToLower(set.BuildInfo.Description), " ", "-"), set.BuildInfo.Version)
 
 	prwe := &prwExporter{
path: exporter/prometheusremotewriteexporter/exporter.go
user: login: jmichalek132
body: added the log line.
created_at: 2024-11-12T15:37:33Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,27 @@
+# Use this changelog template to create an entry for release notes.
+
+# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix'
+change_type: enhancement
+
+# The name of the component, or a single word describing the area of concern, (e.g. filelogreceiver)
+component: prometheusremotewriteexporter
+
+# A brief description of the change.  Surround your text with quotes ("") if it needs to start with a backtick (`).
+note: Add `exporter.prometheusremotewritexporter.enableSendingRW2` feature gate and configure the exporter to send Prometheus remote write 2.0 version.
+
+# Mandatory: One or more tracking issues related to the change. You can use the PR number here if no issue exists.
+issues: [33661]
+
+# (Optional) One or more lines of additional information to render under the primary note.
+# These lines will be padded with 2 spaces and then inserted directly into the document.
+# Use pipe (|) for multiline entries.
+subtext: PRW 2.0 support for the exporter is still under development.
+
+# If your change doesn't affect end users or the exported elements of any package,
+# you should instead start your pull request title with [chore] or use the "Skip Changelog" label.
+# Optional: The change log or logs in which this entry should be included.
+# e.g. '[user]' or '[user, api]'
+# Include 'user' if the change is relevant to end users.
+# Include 'api' if there is a change to a library API.
+# Default: '[user]'
+change_logs: [user]
path: .chloggen/jm-prom-rw-exporter-add-support-for-rw2.yaml
user: login: dashpole
body: ```suggestion
change_logs: [user, api]
```
This includes changes to the exported Go API as well.
created_at: 2024-11-12T16:16:22Z
author_association: CONTRIBUTOR
diff_hunk: @@ -50,7 +50,7 @@ The following settings can be optionally configured:
   - *Note the following headers cannot be changed: `Content-Encoding`, `Content-Type`, `X-Prometheus-Remote-Write-Version`, and `User-Agent`.*
 - `namespace`: prefix attached to each exported metric name.
 - `add_metric_suffixes`: If set to false, type and unit suffixes will not be added to metrics. Default: true.
-- `send_metadata`: If set to true, prometheus metadata will be generated and sent. Default: false.
+- `send_metadata`: If set to true, prometheus metadata will be generated and sent. Default: false. (TODO mention noop for rw2)
path: exporter/prometheusremotewriteexporter/README.md
user: login: dashpole
body: ```suggestion
- `send_metadata`: If set to true, prometheus metadata will be generated and sent. Default: false. This option is ignored when using PRW 2.0, which already includes metadata.
```
created_at: 2024-11-12T16:17:16Z
author_association: CONTRIBUTOR
diff_hunk: @@ -66,6 +66,15 @@ The following settings can be optionally configured:
 - `max_batch_size_bytes` (default = `3000000` -> `~2.861 mb`): Maximum size of a batch of
   samples to be sent to the remote write endpoint. If the batch size is larger
   than this value, it will be split into multiple batches.
+- `protobuf_message` (default = `prometheus.WriteRequest`): 
+  - Protobuf message to use when writing to the remote write endpoint.
+  - The `prometheus.WriteRequest` represents the message introduced in Remote Write 1.0, which will be deprecated eventually.
+  - The `io.prometheus.write.v2.Request` was introduced in Remote Write 2.0 and replaces the former,
+    by improving efficiency and sending metadata, created timestamp and native histograms by default.
+  - Before changing this value, consult with your remote storage provider (or test) what message it supports.
+  - TODO mention feature flag and partial implementation
+
path: exporter/prometheusremotewriteexporter/README.md
user: login: dashpole
body: ```suggestion
- `protobuf_message` (default = `prometheus.WriteRequest`): 
  - Protobuf message to use when writing to the remote write endpoint. This option is ignored unless the `exporter.prometheusremotewritexporter.enableSendingRW2` feature gate is enabled.
  - `prometheus.WriteRequest` is the message used in [Remote Write 1.0](https://prometheus.io/docs/specs/remote_write_spec/).
  - `io.prometheus.write.v2.Request` is the message used in [Remote Write 2.0](https://prometheus.io/docs/specs/remote_write_spec_2_0/). It is more efficient, always includes metadata, and adds support for the created timestamp and native histograms. Your remote storage provider must support PRW 2.0 to be able to use this message. PRW 2.0 support is currently **In Development**, and is only partially implemented.
```
created_at: 2024-11-12T16:28:05Z
author_association: CONTRIBUTOR
diff_hunk: @@ -103,19 +106,26 @@ func newPRWExporter(cfg *Config, set exporter.Settings) (*prwExporter, error) {
 		return nil, err
 	}
 
+	err = config.RemoteWriteProtoMsg.Validate(cfg.RemoteWriteProtoMsg)
+	if err != nil {
+		return nil, err
+	}
+
 	userAgentHeader := fmt.Sprintf("%s/%s", strings.ReplaceAll(strings.ToLower(set.BuildInfo.Description), " ", "-"), set.BuildInfo.Version)
 
 	prwe := &prwExporter{
-		endpointURL:       endpointURL,
-		wg:                new(sync.WaitGroup),
-		closeChan:         make(chan struct{}),
-		userAgentHeader:   userAgentHeader,
-		maxBatchSizeBytes: cfg.MaxBatchSizeBytes,
-		concurrency:       cfg.RemoteWriteQueue.NumConsumers,
-		clientSettings:    &cfg.ClientConfig,
-		settings:          set.TelemetrySettings,
-		retrySettings:     cfg.BackOffConfig,
-		retryOnHTTP429:    retryOn429FeatureGate.IsEnabled(),
+		endpointURL:         endpointURL,
+		wg:                  new(sync.WaitGroup),
+		closeChan:           make(chan struct{}),
+		userAgentHeader:     userAgentHeader,
+		maxBatchSizeBytes:   cfg.MaxBatchSizeBytes,
+		concurrency:         cfg.RemoteWriteQueue.NumConsumers,
+		clientSettings:      &cfg.ClientConfig,
+		settings:            set.TelemetrySettings,
+		retrySettings:       cfg.BackOffConfig,
+		retryOnHTTP429:      retryOn429FeatureGate.IsEnabled(),
+		enableSendingRW2:    enableSendingRW2FeatureGate.IsEnabled(),
path: exporter/prometheusremotewriteexporter/exporter.go
user: login: dashpole
body: nit: I prefer calling `enableSendingRW2FeatureGate.IsEnabled()` directly in code, rather than adding it as a field on the prwExporter struct.  It is simpler, and ensures you are testing by setting the global feature gates (rather than testing a bool on the struct).
created_at: 2024-11-12T16:34:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -173,21 +202,25 @@ func (prwe *prwExporter) PushMetrics(ctx context.Context, md pmetric.Metrics) er
 		return errors.New("shutdown has been called")
 	default:
 
-		tsMap, err := prometheusremotewrite.FromMetrics(md, prwe.exporterSettings)
-		if err != nil {
-			prwe.telemetry.recordTranslationFailure(ctx)
-			prwe.settings.Logger.Debug("failed to translate metrics, exporting remaining metrics", zap.Error(err), zap.Int("translated", len(tsMap)))
+		// If feature flag not enabled support only RW1
+		// TODO remove when feature flag removed
path: exporter/prometheusremotewriteexporter/exporter.go
user: login: dashpole
body: TODO isn't necessary. At least to me, this is obvious.
```suggestion
		// If feature flag not enabled support only RW1
```
created_at: 2024-11-12T17:00:45Z
author_association: CONTRIBUTOR
diff_hunk: @@ -103,19 +106,26 @@ func newPRWExporter(cfg *Config, set exporter.Settings) (*prwExporter, error) {
 		return nil, err
 	}
 
+	err = config.RemoteWriteProtoMsg.Validate(cfg.RemoteWriteProtoMsg)
path: exporter/prometheusremotewriteexporter/exporter.go
user: login: dashpole
body: We should validate configuration in a Validate() function on the Config struct.  See https://github.com/search?q=repo%3Aopen-telemetry%2Fopentelemetry-collector-contrib%20Validate&type=code for other examples.
created_at: 2024-11-12T17:23:10Z
author_association: CONTRIBUTOR
diff_hunk: @@ -173,21 +202,25 @@ func (prwe *prwExporter) PushMetrics(ctx context.Context, md pmetric.Metrics) er
 		return errors.New("shutdown has been called")
 	default:
 
-		tsMap, err := prometheusremotewrite.FromMetrics(md, prwe.exporterSettings)
-		if err != nil {
-			prwe.telemetry.recordTranslationFailure(ctx)
-			prwe.settings.Logger.Debug("failed to translate metrics, exporting remaining metrics", zap.Error(err), zap.Int("translated", len(tsMap)))
+		// If feature flag not enabled support only RW1
+		// TODO remove when feature flag removed
+		if !prwe.enableSendingRW2 {
+			return prwe.pushMetricsV1(ctx, md)
 		}
 
-		prwe.telemetry.recordTranslatedTimeSeries(ctx, len(tsMap))
+		// If feature flag enabled check if we want to send RW1 or RW2
+		switch prwe.RemoteWriteProtoMsg {
+		case config.RemoteWriteProtoMsgV1:
+			// Rw1 case
+			return prwe.pushMetricsV1(ctx, md)
+		case config.RemoteWriteProtoMsgV2:
+			// RW2 case
+			return prwe.pushMetricsV2(ctx, md)
 
-		var m []*prompb.MetricMetadata
-		if prwe.exporterSettings.SendMetadata {
-			m = prometheusremotewrite.OtelMetricsToMetadata(md, prwe.exporterSettings.AddMetricSuffixes)
+		default:
+			return errors.New("unexpected remote-write protobuf message")
path: exporter/prometheusremotewriteexporter/exporter.go
user: login: dashpole
body: ```suggestion
			return fmt.Errorf("unsupported remote-write protobuf message: %v", prwe.RemoteWriteProtoMsg)
```
created_at: 2024-11-12T17:24:10Z
author_association: CONTRIBUTOR
diff_hunk: @@ -300,10 +332,24 @@ func (prwe *prwExporter) execute(ctx context.Context, writeReq *prompb.WriteRequ
 		// Add necessary headers specified by:
 		// https://cortexmetrics.io/docs/apis/#remote-api
 		req.Header.Add("Content-Encoding", "snappy")
-		req.Header.Set("Content-Type", "application/x-protobuf")
-		req.Header.Set("X-Prometheus-Remote-Write-Version", "0.1.0")
 		req.Header.Set("User-Agent", prwe.userAgentHeader)
 
+		// If feature flag not enabled support only RW1
+		// TODO remove when feature flag removed
path: exporter/prometheusremotewriteexporter/exporter.go
user: login: dashpole
body: ```suggestion
		// If feature flag not enabled support only RW1
```
same as above
created_at: 2024-11-12T17:33:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -103,19 +106,26 @@ func newPRWExporter(cfg *Config, set exporter.Settings) (*prwExporter, error) {
 		return nil, err
 	}
 
+	err = config.RemoteWriteProtoMsg.Validate(cfg.RemoteWriteProtoMsg)
path: exporter/prometheusremotewriteexporter/exporter.go
user: login: jmichalek132
body: Should I add the test case or can I mark this as resolved?
created_at: 2024-11-13T10:10:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -103,19 +106,26 @@ func newPRWExporter(cfg *Config, set exporter.Settings) (*prwExporter, error) {
 		return nil, err
 	}
 
+	err = config.RemoteWriteProtoMsg.Validate(cfg.RemoteWriteProtoMsg)
+	if err != nil {
+		return nil, err
+	}
+
 	userAgentHeader := fmt.Sprintf("%s/%s", strings.ReplaceAll(strings.ToLower(set.BuildInfo.Description), " ", "-"), set.BuildInfo.Version)
 
 	prwe := &prwExporter{
-		endpointURL:       endpointURL,
-		wg:                new(sync.WaitGroup),
-		closeChan:         make(chan struct{}),
-		userAgentHeader:   userAgentHeader,
-		maxBatchSizeBytes: cfg.MaxBatchSizeBytes,
-		concurrency:       cfg.RemoteWriteQueue.NumConsumers,
-		clientSettings:    &cfg.ClientConfig,
-		settings:          set.TelemetrySettings,
-		retrySettings:     cfg.BackOffConfig,
-		retryOnHTTP429:    retryOn429FeatureGate.IsEnabled(),
+		endpointURL:         endpointURL,
+		wg:                  new(sync.WaitGroup),
+		closeChan:           make(chan struct{}),
+		userAgentHeader:     userAgentHeader,
+		maxBatchSizeBytes:   cfg.MaxBatchSizeBytes,
+		concurrency:         cfg.RemoteWriteQueue.NumConsumers,
+		clientSettings:      &cfg.ClientConfig,
+		settings:            set.TelemetrySettings,
+		retrySettings:       cfg.BackOffConfig,
+		retryOnHTTP429:      retryOn429FeatureGate.IsEnabled(),
+		enableSendingRW2:    enableSendingRW2FeatureGate.IsEnabled(),
path: exporter/prometheusremotewriteexporter/exporter.go
user: login: jmichalek132
body: fixed
created_at: 2024-11-13T10:22:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -103,19 +106,26 @@ func newPRWExporter(cfg *Config, set exporter.Settings) (*prwExporter, error) {
 		return nil, err
 	}
 
+	err = config.RemoteWriteProtoMsg.Validate(cfg.RemoteWriteProtoMsg)
path: exporter/prometheusremotewriteexporter/exporter.go
user: login: jmichalek132
body: fixed
created_at: 2024-11-13T12:44:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -103,19 +106,26 @@ func newPRWExporter(cfg *Config, set exporter.Settings) (*prwExporter, error) {
 		return nil, err
 	}
 
+	err = config.RemoteWriteProtoMsg.Validate(cfg.RemoteWriteProtoMsg)
path: exporter/prometheusremotewriteexporter/exporter.go
user: login: jmichalek132
body: resolving as agreed on slack.
created_at: 2024-11-13T12:55:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -115,5 +119,10 @@ func (cfg *Config) Validate() error {
 		cfg.MaxBatchSizeBytes = 3000000
 	}
 
+	err := cfg.RemoteWriteProtoMsg.Validate()
+	if err != nil {
+		return err
+	}
path: exporter/prometheusremotewriteexporter/config.go
user: login: ArthurSens
body: Should we include a test for an invalid proto message? (completely fine to do in another PR)
created_at: 2024-11-13T13:00:03Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,113 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+package prometheusremotewriteexporter // import "github.com/open-telemetry/opentelemetry-collector-contrib/exporter/prometheusremotewriteexporter"
+
+import (
+	"context"
+	"math"
+	"sync"
+
+	"github.com/gogo/protobuf/proto"
+	writev2 "github.com/prometheus/prometheus/prompb/io/prometheus/write/v2"
+	"go.opentelemetry.io/collector/consumer/consumererror"
+	"go.opentelemetry.io/collector/pdata/pmetric"
+	"go.uber.org/multierr"
+	"go.uber.org/zap"
+
+	"github.com/open-telemetry/opentelemetry-collector-contrib/pkg/translator/prometheusremotewrite"
+)
+
+func (prwe *prwExporter) pushMetricsV2(ctx context.Context, md pmetric.Metrics) error {
+	tsMap, symbolsTable, err := prometheusremotewrite.FromMetricsV2(md, prwe.exporterSettings)
+
+	prwe.telemetry.recordTranslatedTimeSeries(ctx, len(tsMap))
+
+	if err != nil {
+		prwe.telemetry.recordTranslationFailure(ctx)
+		prwe.settings.Logger.Debug("failed to translate metrics, exporting remaining metrics", zap.Error(err), zap.Int("translated", len(tsMap)))
+	}
+	// Call export even if a conversion error, since there may be points that were successfully converted.
+	return prwe.handleExportV2(ctx, symbolsTable, tsMap)
+}
+
+// TODO update comment
path: exporter/prometheusremotewriteexporter/exporter_v2.go
user: login: dashpole
body: this should probably be addressed before we merge
created_at: 2024-11-13T14:26:14Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,113 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+package prometheusremotewriteexporter // import "github.com/open-telemetry/opentelemetry-collector-contrib/exporter/prometheusremotewriteexporter"
+
+import (
+	"context"
+	"math"
+	"sync"
+
+	"github.com/gogo/protobuf/proto"
+	writev2 "github.com/prometheus/prometheus/prompb/io/prometheus/write/v2"
+	"go.opentelemetry.io/collector/consumer/consumererror"
+	"go.opentelemetry.io/collector/pdata/pmetric"
+	"go.uber.org/multierr"
+	"go.uber.org/zap"
+
+	"github.com/open-telemetry/opentelemetry-collector-contrib/pkg/translator/prometheusremotewrite"
+)
+
+func (prwe *prwExporter) pushMetricsV2(ctx context.Context, md pmetric.Metrics) error {
+	tsMap, symbolsTable, err := prometheusremotewrite.FromMetricsV2(md, prwe.exporterSettings)
+
+	prwe.telemetry.recordTranslatedTimeSeries(ctx, len(tsMap))
+
+	if err != nil {
+		prwe.telemetry.recordTranslationFailure(ctx)
+		prwe.settings.Logger.Debug("failed to translate metrics, exporting remaining metrics", zap.Error(err), zap.Int("translated", len(tsMap)))
+	}
+	// Call export even if a conversion error, since there may be points that were successfully converted.
+	return prwe.handleExportV2(ctx, symbolsTable, tsMap)
+}
+
+// TODO update comment
path: exporter/prometheusremotewriteexporter/exporter_v2.go
user: login: jmichalek132
body: Updated let me know if it's okay.
created_at: 2024-11-13T15:13:16Z
author_association: CONTRIBUTOR
diff_hunk: @@ -115,5 +119,10 @@ func (cfg *Config) Validate() error {
 		cfg.MaxBatchSizeBytes = 3000000
 	}
 
+	err := cfg.RemoteWriteProtoMsg.Validate()
+	if err != nil {
+		return err
+	}
path: exporter/prometheusremotewriteexporter/config.go
user: login: jmichalek132
body: let's do this in follow up PR :) adding it to the list.
created_at: 2024-11-13T15:21:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,113 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+package prometheusremotewriteexporter // import "github.com/open-telemetry/opentelemetry-collector-contrib/exporter/prometheusremotewriteexporter"
+
+import (
+	"context"
+	"math"
+	"sync"
+
+	"github.com/gogo/protobuf/proto"
+	writev2 "github.com/prometheus/prometheus/prompb/io/prometheus/write/v2"
+	"go.opentelemetry.io/collector/consumer/consumererror"
+	"go.opentelemetry.io/collector/pdata/pmetric"
+	"go.uber.org/multierr"
+	"go.uber.org/zap"
+
+	"github.com/open-telemetry/opentelemetry-collector-contrib/pkg/translator/prometheusremotewrite"
+)
+
+func (prwe *prwExporter) pushMetricsV2(ctx context.Context, md pmetric.Metrics) error {
+	tsMap, symbolsTable, err := prometheusremotewrite.FromMetricsV2(md, prwe.exporterSettings)
+
+	prwe.telemetry.recordTranslatedTimeSeries(ctx, len(tsMap))
+
+	if err != nil {
+		prwe.telemetry.recordTranslationFailure(ctx)
+		prwe.settings.Logger.Debug("failed to translate metrics, exporting remaining metrics", zap.Error(err), zap.Int("translated", len(tsMap)))
+	}
+	// Call export even if a conversion error, since there may be points that were successfully converted.
+	return prwe.handleExportV2(ctx, symbolsTable, tsMap)
+}
+
+// TODO update comment
path: exporter/prometheusremotewriteexporter/exporter_v2.go
user: login: dashpole
body: lgtm
created_at: 2024-11-13T15:42:33Z
author_association: CONTRIBUTOR
diff_hunk: @@ -285,16 +325,7 @@ func (prwe *prwExporter) export(ctx context.Context, requests []*prompb.WriteReq
 	return errs
 }
 
-func (prwe *prwExporter) execute(ctx context.Context, writeReq *prompb.WriteRequest) error {
-	buf := bufferPool.Get().(*buffer)
-	buf.protobuf.Reset()
-	defer bufferPool.Put(buf)
-
-	// Uses proto.Marshal to convert the WriteRequest into bytes array
-	errMarshal := buf.protobuf.Marshal(writeReq)
-	if errMarshal != nil {
-		return consumererror.NewPermanent(errMarshal)
-	}
+func (prwe *prwExporter) execute(ctx context.Context, buf *buffer) error {
path: exporter/prometheusremotewriteexporter/exporter.go
user: login: ArthurSens
body: I didn't understand this change. Could you clarify why we are getting the buffer outside this?

This is being duplicated in several places now 🤔 
created_at: 2024-11-14T13:40:10Z
author_association: MEMBER
diff_hunk: @@ -285,16 +325,7 @@ func (prwe *prwExporter) export(ctx context.Context, requests []*prompb.WriteReq
 	return errs
 }
 
-func (prwe *prwExporter) execute(ctx context.Context, writeReq *prompb.WriteRequest) error {
-	buf := bufferPool.Get().(*buffer)
-	buf.protobuf.Reset()
-	defer bufferPool.Put(buf)
-
-	// Uses proto.Marshal to convert the WriteRequest into bytes array
-	errMarshal := buf.protobuf.Marshal(writeReq)
-	if errMarshal != nil {
-		return consumererror.NewPermanent(errMarshal)
-	}
+func (prwe *prwExporter) execute(ctx context.Context, buf *buffer) error {
path: exporter/prometheusremotewriteexporter/exporter.go
user: login: jmichalek132
body: the benefit is we can use a single `execute` function because both v1 and v2 pass it a buffer instead of passing it the request struct which is different between v1 and v2.
created_at: 2024-11-14T14:01:32Z
author_association: CONTRIBUTOR
diff_hunk: @@ -285,16 +325,7 @@ func (prwe *prwExporter) export(ctx context.Context, requests []*prompb.WriteReq
 	return errs
 }
 
-func (prwe *prwExporter) execute(ctx context.Context, writeReq *prompb.WriteRequest) error {
-	buf := bufferPool.Get().(*buffer)
-	buf.protobuf.Reset()
-	defer bufferPool.Put(buf)
-
-	// Uses proto.Marshal to convert the WriteRequest into bytes array
-	errMarshal := buf.protobuf.Marshal(writeReq)
-	if errMarshal != nil {
-		return consumererror.NewPermanent(errMarshal)
-	}
+func (prwe *prwExporter) execute(ctx context.Context, buf *buffer) error {
path: exporter/prometheusremotewriteexporter/exporter.go
user: login: ArthurSens
body: Ah ok after trying things out in your branch I understood that we need this because exportV1 and exportV2 have different request formats and we need to deal with a byte buffer one level above
created_at: 2024-11-14T14:07:27Z
author_association: MEMBER
diff_hunk: @@ -285,16 +325,7 @@ func (prwe *prwExporter) export(ctx context.Context, requests []*prompb.WriteReq
 	return errs
 }
 
-func (prwe *prwExporter) execute(ctx context.Context, writeReq *prompb.WriteRequest) error {
-	buf := bufferPool.Get().(*buffer)
-	buf.protobuf.Reset()
-	defer bufferPool.Put(buf)
-
-	// Uses proto.Marshal to convert the WriteRequest into bytes array
-	errMarshal := buf.protobuf.Marshal(writeReq)
-	if errMarshal != nil {
-		return consumererror.NewPermanent(errMarshal)
-	}
+func (prwe *prwExporter) execute(ctx context.Context, buf *buffer) error {
path: exporter/prometheusremotewriteexporter/exporter.go
user: login: jmichalek132
body: yeah so even moving it to a separate function won't work since you need to pass it the different request formats.
created_at: 2024-11-14T14:13:04Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,116 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+package prometheusremotewriteexporter // import "github.com/open-telemetry/opentelemetry-collector-contrib/exporter/prometheusremotewriteexporter"
+
+import (
+	"context"
+	"math"
+	"sync"
+
+	writev2 "github.com/prometheus/prometheus/prompb/io/prometheus/write/v2"
+	"go.opentelemetry.io/collector/consumer/consumererror"
+	"go.opentelemetry.io/collector/pdata/pmetric"
+	"go.uber.org/multierr"
+	"go.uber.org/zap"
+
+	"github.com/open-telemetry/opentelemetry-collector-contrib/pkg/translator/prometheusremotewrite"
+)
+
+func (prwe *prwExporter) pushMetricsV2(ctx context.Context, md pmetric.Metrics) error {
+	tsMap, symbolsTable, err := prometheusremotewrite.FromMetricsV2(md, prwe.exporterSettings)
+
+	prwe.telemetry.recordTranslatedTimeSeries(ctx, len(tsMap))
+
+	if err != nil {
+		prwe.telemetry.recordTranslationFailure(ctx)
+		prwe.settings.Logger.Debug("failed to translate metrics, exporting remaining metrics", zap.Error(err), zap.Int("translated", len(tsMap)))
+	}
+	// Call export even if a conversion error, since there may be points that were successfully converted.
+	return prwe.handleExportV2(ctx, symbolsTable, tsMap)
+}
+
+// export sends a Snappy-compressed writev2.Request containing writev2.TimeSeries to a remote write endpoint
+func (prwe *prwExporter) exportV2(ctx context.Context, requests []*writev2.Request) error {
+	input := make(chan *writev2.Request, len(requests))
+	for _, request := range requests {
+		input <- request
+	}
+	close(input)
+
+	var wg sync.WaitGroup
+
+	concurrencyLimit := int(math.Min(float64(prwe.concurrency), float64(len(requests))))
+	wg.Add(concurrencyLimit) // used to wait for workers to be finished
+
+	var mu sync.Mutex
+	var errs error
+	// Run concurrencyLimit of workers until there
+	// is no more requests to execute in the input channel.
+	for i := 0; i < concurrencyLimit; i++ {
+		go func() {
+			defer wg.Done()
+			for {
+				select {
+				case <-ctx.Done(): // Check firstly to ensure that the context wasn't cancelled.
+					return
+
+				case request, ok := <-input:
+					if !ok {
+						return
+					}
+
+					buf := bufferPool.Get().(*buffer)
+					buf.protobuf.Reset()
+					defer bufferPool.Put(buf)
path: exporter/prometheusremotewriteexporter/exporter_v2.go
user: login: aknuds1
body: Deferring the putting back of the buffer is probably not a good idea, since it happens within a loop. Only when the loop finishes do the buffers actually get put back. I would recommend instead putting the buffer back explicitly when done with it.
created_at: 2024-11-20T17:46:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,116 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+package prometheusremotewriteexporter // import "github.com/open-telemetry/opentelemetry-collector-contrib/exporter/prometheusremotewriteexporter"
+
+import (
+	"context"
+	"math"
+	"sync"
+
+	writev2 "github.com/prometheus/prometheus/prompb/io/prometheus/write/v2"
+	"go.opentelemetry.io/collector/consumer/consumererror"
+	"go.opentelemetry.io/collector/pdata/pmetric"
+	"go.uber.org/multierr"
+	"go.uber.org/zap"
+
+	"github.com/open-telemetry/opentelemetry-collector-contrib/pkg/translator/prometheusremotewrite"
+)
+
+func (prwe *prwExporter) pushMetricsV2(ctx context.Context, md pmetric.Metrics) error {
+	tsMap, symbolsTable, err := prometheusremotewrite.FromMetricsV2(md, prwe.exporterSettings)
+
+	prwe.telemetry.recordTranslatedTimeSeries(ctx, len(tsMap))
+
+	if err != nil {
+		prwe.telemetry.recordTranslationFailure(ctx)
+		prwe.settings.Logger.Debug("failed to translate metrics, exporting remaining metrics", zap.Error(err), zap.Int("translated", len(tsMap)))
+	}
+	// Call export even if a conversion error, since there may be points that were successfully converted.
+	return prwe.handleExportV2(ctx, symbolsTable, tsMap)
+}
+
+// export sends a Snappy-compressed writev2.Request containing writev2.TimeSeries to a remote write endpoint
+func (prwe *prwExporter) exportV2(ctx context.Context, requests []*writev2.Request) error {
+	input := make(chan *writev2.Request, len(requests))
+	for _, request := range requests {
+		input <- request
+	}
+	close(input)
+
+	var wg sync.WaitGroup
+
+	concurrencyLimit := int(math.Min(float64(prwe.concurrency), float64(len(requests))))
+	wg.Add(concurrencyLimit) // used to wait for workers to be finished
+
+	var mu sync.Mutex
+	var errs error
+	// Run concurrencyLimit of workers until there
+	// is no more requests to execute in the input channel.
+	for i := 0; i < concurrencyLimit; i++ {
+		go func() {
+			defer wg.Done()
+			for {
+				select {
+				case <-ctx.Done(): // Check firstly to ensure that the context wasn't cancelled.
+					return
+
+				case request, ok := <-input:
+					if !ok {
+						return
+					}
+
+					buf := bufferPool.Get().(*buffer)
+					buf.protobuf.Reset()
+					defer bufferPool.Put(buf)
+
+					// Uses proto.Marshal to convert the WriteRequest into bytes array
+					errMarshal := buf.protobuf.Marshal(request)
+					if errMarshal != nil {
+						errs = multierr.Append(errs, consumererror.NewPermanent(errMarshal))
path: exporter/prometheusremotewriteexporter/exporter_v2.go
user: login: aknuds1
body: Why isn't `mu` taken here, as on line 75? Is it an omission?
created_at: 2024-11-20T17:47:22Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,116 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+package prometheusremotewriteexporter // import "github.com/open-telemetry/opentelemetry-collector-contrib/exporter/prometheusremotewriteexporter"
+
+import (
+	"context"
+	"math"
+	"sync"
+
+	writev2 "github.com/prometheus/prometheus/prompb/io/prometheus/write/v2"
+	"go.opentelemetry.io/collector/consumer/consumererror"
+	"go.opentelemetry.io/collector/pdata/pmetric"
+	"go.uber.org/multierr"
+	"go.uber.org/zap"
+
+	"github.com/open-telemetry/opentelemetry-collector-contrib/pkg/translator/prometheusremotewrite"
+)
+
+func (prwe *prwExporter) pushMetricsV2(ctx context.Context, md pmetric.Metrics) error {
+	tsMap, symbolsTable, err := prometheusremotewrite.FromMetricsV2(md, prwe.exporterSettings)
+
+	prwe.telemetry.recordTranslatedTimeSeries(ctx, len(tsMap))
+
+	if err != nil {
+		prwe.telemetry.recordTranslationFailure(ctx)
+		prwe.settings.Logger.Debug("failed to translate metrics, exporting remaining metrics", zap.Error(err), zap.Int("translated", len(tsMap)))
+	}
+	// Call export even if a conversion error, since there may be points that were successfully converted.
+	return prwe.handleExportV2(ctx, symbolsTable, tsMap)
+}
+
+// export sends a Snappy-compressed writev2.Request containing writev2.TimeSeries to a remote write endpoint
+func (prwe *prwExporter) exportV2(ctx context.Context, requests []*writev2.Request) error {
+	input := make(chan *writev2.Request, len(requests))
+	for _, request := range requests {
+		input <- request
+	}
+	close(input)
+
+	var wg sync.WaitGroup
+
+	concurrencyLimit := int(math.Min(float64(prwe.concurrency), float64(len(requests))))
+	wg.Add(concurrencyLimit) // used to wait for workers to be finished
+
+	var mu sync.Mutex
+	var errs error
+	// Run concurrencyLimit of workers until there
+	// is no more requests to execute in the input channel.
+	for i := 0; i < concurrencyLimit; i++ {
+		go func() {
+			defer wg.Done()
+			for {
+				select {
+				case <-ctx.Done(): // Check firstly to ensure that the context wasn't cancelled.
+					return
+
+				case request, ok := <-input:
+					if !ok {
+						return
+					}
+
+					buf := bufferPool.Get().(*buffer)
+					buf.protobuf.Reset()
+					defer bufferPool.Put(buf)
+
+					// Uses proto.Marshal to convert the WriteRequest into bytes array
+					errMarshal := buf.protobuf.Marshal(request)
+					if errMarshal != nil {
+						errs = multierr.Append(errs, consumererror.NewPermanent(errMarshal))
+						return
+					}
+
+					if errExecute := prwe.execute(ctx, buf); errExecute != nil {
+						mu.Lock()
+						errs = multierr.Append(errs, consumererror.NewPermanent(errExecute))
+						mu.Unlock()
+					}
+				}
+			}
+		}()
+	}
+	wg.Wait()
+
+	return errs
+}
+
+func (prwe *prwExporter) handleExportV2(ctx context.Context, symbolsTable writev2.SymbolsTable, tsMap map[string]*writev2.TimeSeries) error {
+	// There are no metrics to export, so return.
+	if len(tsMap) == 0 {
+		return nil
+	}
+
+	// TODO implement batching
+	// TODO how do we handle symbolsTable with batching?
+	requests := make([]*writev2.Request, 0)
path: exporter/prometheusremotewriteexporter/exporter_v2.go
user: login: aknuds1
body: What's the point of making an array, if there's only one request?
created_at: 2024-11-20T17:48:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -66,6 +66,11 @@ The following settings can be optionally configured:
 - `max_batch_size_bytes` (default = `3000000` -> `~2.861 mb`): Maximum size of a batch of
   samples to be sent to the remote write endpoint. If the batch size is larger
   than this value, it will be split into multiple batches.
+- `protobuf_message` (default = `prometheus.WriteRequest`): 
path: exporter/prometheusremotewriteexporter/README.md
user: login: bwplotka
body: I don't want to confuse everyone, so this is FYI. I think it's fine to keep this version in place.

FYI: 
We might need to change naming for the similar in config in Prometheus. This is because we discuss different  serialization mechanisms than protobuf e.g. capnproto or arrow.

When that happens I believe we will have a new option (with protobufMessage deprecated) sp something like `content_type` and then `proto;prometheus.WriteRequest`, `proto;io.prometheus.write.v2.Request`. It will be still months until this happen, but just saying if you want to avoid renaming in future. The disadvantage of doing this change now is inconsistency with Prometheus config now or in future if different naming will be chosen vs what I just mentioned now, so it feels it's ok to keep this and deprecate in future.
created_at: 2024-11-25T16:56:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -54,6 +55,9 @@ type Config struct {
 
 	// SendMetadata controls whether prometheus metadata will be generated and sent
 	SendMetadata bool `mapstructure:"send_metadata"`
+
+	// RemoteWriteProtoMsg controls whether prometheus remote write v1 or v2 is sent
path: exporter/prometheusremotewriteexporter/config.go
user: login: bwplotka
body: ```suggestion
	// RemoteWriteProtoMsg controls whether prometheus remote write v1 or v2 is sent.
```
created_at: 2024-11-25T17:00:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -117,5 +121,10 @@ func (cfg *Config) Validate() error {
 		cfg.MaxBatchSizeBytes = 3000000
 	}
 
+	err := cfg.RemoteWriteProtoMsg.Validate()
+	if err != nil {
+		return err
+	}
+
 	return nil
path: exporter/prometheusremotewriteexporter/config.go
user: login: bwplotka
body: ```suggestion
	return cfg.RemoteWriteProtoMsg.Validate()
```
created_at: 2024-11-25T17:01:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -118,19 +120,25 @@ func newPRWExporter(cfg *Config, set exporter.Settings) (*prwExporter, error) {
 		return nil, err
 	}
 
+	err = config.RemoteWriteProtoMsg.Validate(cfg.RemoteWriteProtoMsg)
+	if err != nil {
+		return nil, err
+	}
path: exporter/prometheusremotewriteexporter/exporter.go
user: login: bwplotka
body: Bit more readable + scoped error (it's clear err value is not used somewhere else).

```suggestion
	if err := config.RemoteWriteProtoMsg.Validate(cfg.RemoteWriteProtoMsg); err != nil {
		return nil, err
	}
```
created_at: 2024-11-25T17:02:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -143,6 +151,7 @@ func newPRWExporter(cfg *Config, set exporter.Settings) (*prwExporter, error) {
 		batchTimeSeriesState: newBatchTimeSericesState(),
 	}
 
+	prwe.settings.Logger.Info("Running with prometheus remote write proto message", zap.Any("ProtoMsg", cfg.RemoteWriteProtoMsg))
path: exporter/prometheusremotewriteexporter/exporter.go
user: login: bwplotka
body: ```suggestion
	prwe.settings.Logger.Info("starting prometheus remote write exporter", zap.Any("ProtoMsg", cfg.RemoteWriteProtoMsg))
```

We don't need the elaborate string, because there will be "ProtoMsg=<proto mess>" in the log?
created_at: 2024-11-25T17:04:11Z
author_association: CONTRIBUTOR
diff_hunk: @@ -192,21 +218,23 @@ func (prwe *prwExporter) PushMetrics(ctx context.Context, md pmetric.Metrics) er
 		return errors.New("shutdown has been called")
 	default:
 
-		tsMap, err := prometheusremotewrite.FromMetrics(md, prwe.exporterSettings)
-		if err != nil {
-			prwe.telemetry.recordTranslationFailure(ctx)
-			prwe.settings.Logger.Debug("failed to translate metrics, exporting remaining metrics", zap.Error(err), zap.Int("translated", len(tsMap)))
+		// If feature flag not enabled support only RW1
path: exporter/prometheusremotewriteexporter/exporter.go
user: login: bwplotka
body: ```suggestion
		// If feature flag not enabled support only RW1.
```
created_at: 2024-11-25T17:05:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -192,21 +218,23 @@ func (prwe *prwExporter) PushMetrics(ctx context.Context, md pmetric.Metrics) er
 		return errors.New("shutdown has been called")
 	default:
 
-		tsMap, err := prometheusremotewrite.FromMetrics(md, prwe.exporterSettings)
-		if err != nil {
-			prwe.telemetry.recordTranslationFailure(ctx)
-			prwe.settings.Logger.Debug("failed to translate metrics, exporting remaining metrics", zap.Error(err), zap.Int("translated", len(tsMap)))
+		// If feature flag not enabled support only RW1
+		if !enableSendingRW2FeatureGate.IsEnabled() {
+			return prwe.pushMetricsV1(ctx, md)
 		}
 
-		prwe.telemetry.recordTranslatedTimeSeries(ctx, len(tsMap))
+		// If feature flag enabled check if we want to send RW1 or RW2
path: exporter/prometheusremotewriteexporter/exporter.go
user: login: bwplotka
body: ```suggestion
		// If feature flag was enabled check if we want to send RW1 or RW2.
```
created_at: 2024-11-25T17:05:33Z
author_association: CONTRIBUTOR
diff_hunk: @@ -192,21 +218,23 @@ func (prwe *prwExporter) PushMetrics(ctx context.Context, md pmetric.Metrics) er
 		return errors.New("shutdown has been called")
 	default:
 
-		tsMap, err := prometheusremotewrite.FromMetrics(md, prwe.exporterSettings)
-		if err != nil {
-			prwe.telemetry.recordTranslationFailure(ctx)
-			prwe.settings.Logger.Debug("failed to translate metrics, exporting remaining metrics", zap.Error(err), zap.Int("translated", len(tsMap)))
+		// If feature flag not enabled support only RW1
+		if !enableSendingRW2FeatureGate.IsEnabled() {
+			return prwe.pushMetricsV1(ctx, md)
 		}
 
-		prwe.telemetry.recordTranslatedTimeSeries(ctx, len(tsMap))
+		// If feature flag enabled check if we want to send RW1 or RW2
+		switch prwe.RemoteWriteProtoMsg {
+		case config.RemoteWriteProtoMsgV1:
+			// Rw1 case
path: exporter/prometheusremotewriteexporter/exporter.go
user: login: bwplotka
body: Too trivial commentary?
```suggestion
```
created_at: 2024-11-25T17:05:45Z
author_association: CONTRIBUTOR
diff_hunk: @@ -192,21 +218,23 @@ func (prwe *prwExporter) PushMetrics(ctx context.Context, md pmetric.Metrics) er
 		return errors.New("shutdown has been called")
 	default:
 
-		tsMap, err := prometheusremotewrite.FromMetrics(md, prwe.exporterSettings)
-		if err != nil {
-			prwe.telemetry.recordTranslationFailure(ctx)
-			prwe.settings.Logger.Debug("failed to translate metrics, exporting remaining metrics", zap.Error(err), zap.Int("translated", len(tsMap)))
+		// If feature flag not enabled support only RW1
+		if !enableSendingRW2FeatureGate.IsEnabled() {
+			return prwe.pushMetricsV1(ctx, md)
 		}
 
-		prwe.telemetry.recordTranslatedTimeSeries(ctx, len(tsMap))
+		// If feature flag enabled check if we want to send RW1 or RW2
+		switch prwe.RemoteWriteProtoMsg {
+		case config.RemoteWriteProtoMsgV1:
+			// Rw1 case
+			return prwe.pushMetricsV1(ctx, md)
+		case config.RemoteWriteProtoMsgV2:
+			// RW2 case
path: exporter/prometheusremotewriteexporter/exporter.go
user: login: bwplotka
body: ```suggestion
```
created_at: 2024-11-25T17:05:53Z
author_association: CONTRIBUTOR
diff_hunk: @@ -275,7 +302,19 @@ func (prwe *prwExporter) export(ctx context.Context, requests []*prompb.WriteReq
 					if !ok {
 						return
 					}
-					if errExecute := prwe.execute(ctx, request); errExecute != nil {
+
+					buf := bufferPool.Get().(*buffer)
+					buf.protobuf.Reset()
+					defer bufferPool.Put(buf)
+
+					// Uses proto.Marshal to convert the WriteRequest into bytes array
path: exporter/prometheusremotewriteexporter/exporter.go
user: login: bwplotka
body: ```suggestion
					// Uses proto.Marshal to convert the WriteRequest into bytes array.
```
created_at: 2024-11-25T17:07:06Z
author_association: CONTRIBUTOR
diff_hunk: @@ -275,7 +302,19 @@ func (prwe *prwExporter) export(ctx context.Context, requests []*prompb.WriteReq
 					if !ok {
 						return
 					}
-					if errExecute := prwe.execute(ctx, request); errExecute != nil {
+
+					buf := bufferPool.Get().(*buffer)
+					buf.protobuf.Reset()
+					defer bufferPool.Put(buf)
+
+					// Uses proto.Marshal to convert the WriteRequest into bytes array
+					errMarshal := buf.protobuf.Marshal(request)
+					if errMarshal != nil {
+						errs = multierr.Append(errs, consumererror.NewPermanent(errMarshal))
+						return
+					}
path: exporter/prometheusremotewriteexporter/exporter.go
user: login: bwplotka
body: ```suggestion
					if errMarshal := buf.protobuf.Marshal(request); errMarshal != nil {
						errs = multierr.Append(errs, consumererror.NewPermanent(errMarshal))
						return
					}
```
created_at: 2024-11-25T17:07:26Z
author_association: CONTRIBUTOR
diff_hunk: @@ -331,10 +361,23 @@ func (prwe *prwExporter) execute(ctx context.Context, writeReq *prompb.WriteRequ
 		// Add necessary headers specified by:
 		// https://cortexmetrics.io/docs/apis/#remote-api
 		req.Header.Add("Content-Encoding", "snappy")
-		req.Header.Set("Content-Type", "application/x-protobuf")
-		req.Header.Set("X-Prometheus-Remote-Write-Version", "0.1.0")
 		req.Header.Set("User-Agent", prwe.userAgentHeader)
 
+		// If feature flag not enabled support only RW1
+		if !enableSendingRW2FeatureGate.IsEnabled() {
+			req.Header.Set("Content-Type", "application/x-protobuf")
+			req.Header.Set("X-Prometheus-Remote-Write-Version", "0.1.0")
+		} else {
+			switch prwe.RemoteWriteProtoMsg {
+			case config.RemoteWriteProtoMsgV1:
+				req.Header.Set("Content-Type", "application/x-protobuf")
+				req.Header.Set("X-Prometheus-Remote-Write-Version", "0.1.0")
+			case config.RemoteWriteProtoMsgV2:
+				req.Header.Set("Content-Type", "application/x-protobuf;proto=io.prometheus.write.v2.Request")
+				req.Header.Set("X-Prometheus-Remote-Write-Version", "2.0.0")
+			}
path: exporter/prometheusremotewriteexporter/exporter.go
user: login: bwplotka
body: ```suggestion
		switch {
			case !enableSendingRW2FeatureGate.IsEnabled(), prwe.RemoteWriteProtoMsg == config.RemoteWriteProtoMsgV1:
				req.Header.Set("Content-Type", "application/x-protobuf")
				req.Header.Set("X-Prometheus-Remote-Write-Version", "0.1.0")
			case prwe.RemoteWriteProtoMsg == config.RemoteWriteProtoMsgV2:
				req.Header.Set("Content-Type", "application/x-protobuf;proto=io.prometheus.write.v2.Request")
				req.Header.Set("X-Prometheus-Remote-Write-Version", "2.0.0")
			default:
			        panic("proto message validated earlier")
			}
	
```
created_at: 2024-11-25T17:09:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,116 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+package prometheusremotewriteexporter // import "github.com/open-telemetry/opentelemetry-collector-contrib/exporter/prometheusremotewriteexporter"
+
+import (
+	"context"
+	"math"
+	"sync"
+
+	writev2 "github.com/prometheus/prometheus/prompb/io/prometheus/write/v2"
+	"go.opentelemetry.io/collector/consumer/consumererror"
+	"go.opentelemetry.io/collector/pdata/pmetric"
+	"go.uber.org/multierr"
+	"go.uber.org/zap"
+
+	"github.com/open-telemetry/opentelemetry-collector-contrib/pkg/translator/prometheusremotewrite"
+)
+
+func (prwe *prwExporter) pushMetricsV2(ctx context.Context, md pmetric.Metrics) error {
+	tsMap, symbolsTable, err := prometheusremotewrite.FromMetricsV2(md, prwe.exporterSettings)
+
+	prwe.telemetry.recordTranslatedTimeSeries(ctx, len(tsMap))
+
+	if err != nil {
+		prwe.telemetry.recordTranslationFailure(ctx)
+		prwe.settings.Logger.Debug("failed to translate metrics, exporting remaining metrics", zap.Error(err), zap.Int("translated", len(tsMap)))
+	}
+	// Call export even if a conversion error, since there may be points that were successfully converted.
+	return prwe.handleExportV2(ctx, symbolsTable, tsMap)
+}
+
+// export sends a Snappy-compressed writev2.Request containing writev2.TimeSeries to a remote write endpoint
path: exporter/prometheusremotewriteexporter/exporter_v2.go
user: login: bwplotka
body: ```suggestion
// exportV2 sends a Snappy-compressed writev2.Request containing writev2.TimeSeries to a remote write endpoint.
```
created_at: 2024-11-25T17:09:56Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,116 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+package prometheusremotewriteexporter // import "github.com/open-telemetry/opentelemetry-collector-contrib/exporter/prometheusremotewriteexporter"
+
+import (
+	"context"
+	"math"
+	"sync"
+
+	writev2 "github.com/prometheus/prometheus/prompb/io/prometheus/write/v2"
+	"go.opentelemetry.io/collector/consumer/consumererror"
+	"go.opentelemetry.io/collector/pdata/pmetric"
+	"go.uber.org/multierr"
+	"go.uber.org/zap"
+
+	"github.com/open-telemetry/opentelemetry-collector-contrib/pkg/translator/prometheusremotewrite"
+)
+
+func (prwe *prwExporter) pushMetricsV2(ctx context.Context, md pmetric.Metrics) error {
+	tsMap, symbolsTable, err := prometheusremotewrite.FromMetricsV2(md, prwe.exporterSettings)
+
+	prwe.telemetry.recordTranslatedTimeSeries(ctx, len(tsMap))
+
+	if err != nil {
+		prwe.telemetry.recordTranslationFailure(ctx)
+		prwe.settings.Logger.Debug("failed to translate metrics, exporting remaining metrics", zap.Error(err), zap.Int("translated", len(tsMap)))
+	}
+	// Call export even if a conversion error, since there may be points that were successfully converted.
+	return prwe.handleExportV2(ctx, symbolsTable, tsMap)
+}
+
+// export sends a Snappy-compressed writev2.Request containing writev2.TimeSeries to a remote write endpoint
+func (prwe *prwExporter) exportV2(ctx context.Context, requests []*writev2.Request) error {
+	input := make(chan *writev2.Request, len(requests))
+	for _, request := range requests {
+		input <- request
+	}
+	close(input)
+
+	var wg sync.WaitGroup
+
+	concurrencyLimit := int(math.Min(float64(prwe.concurrency), float64(len(requests))))
+	wg.Add(concurrencyLimit) // used to wait for workers to be finished
+
+	var mu sync.Mutex
+	var errs error
+	// Run concurrencyLimit of workers until there
+	// is no more requests to execute in the input channel.
+	for i := 0; i < concurrencyLimit; i++ {
+		go func() {
+			defer wg.Done()
+			for {
+				select {
+				case <-ctx.Done(): // Check firstly to ensure that the context wasn't cancelled.
+					return
+
+				case request, ok := <-input:
+					if !ok {
+						return
+					}
+
+					buf := bufferPool.Get().(*buffer)
+					buf.protobuf.Reset()
+					defer bufferPool.Put(buf)
path: exporter/prometheusremotewriteexporter/exporter_v2.go
user: login: bwplotka
body: Yup, anonymous closure could help
created_at: 2024-11-25T17:10:16Z
author_association: CONTRIBUTOR
diff_hunk: @@ -66,6 +66,11 @@ The following settings can be optionally configured:
 - `max_batch_size_bytes` (default = `3000000` -> `~2.861 mb`): Maximum size of a batch of
   samples to be sent to the remote write endpoint. If the batch size is larger
   than this value, it will be split into multiple batches.
+- `protobuf_message` (default = `prometheus.WriteRequest`): 
path: exporter/prometheusremotewriteexporter/README.md
user: login: ArthurSens
body: Changing it in Prometheus would be a big breaking change, wouldn't it? Can it be done before 4.0?

Either way, deprecation policies here in the collector are more relaxed. We can Deprecate and rename the field once Prometheus also changes :)
created_at: 2024-11-25T17:13:23Z
author_association: MEMBER
diff_hunk: @@ -66,6 +66,11 @@ The following settings can be optionally configured:
 - `max_batch_size_bytes` (default = `3000000` -> `~2.861 mb`): Maximum size of a batch of
   samples to be sent to the remote write endpoint. If the batch size is larger
   than this value, it will be split into multiple batches.
+- `protobuf_message` (default = `prometheus.WriteRequest`): 
path: exporter/prometheusremotewriteexporter/README.md
user: login: Aneurysm9
body: Does this configuration need to be exposed?  Can we try v2 and trip a latch that causes future requests to fall back to v1 if a `415` response is received?  That should retain flexibility to enhance the content negotiation capabilities in the future without making breaking configuration changes.
created_at: 2024-11-25T21:42:30Z
author_association: MEMBER
diff_hunk: @@ -103,19 +106,26 @@ func newPRWExporter(cfg *Config, set exporter.Settings) (*prwExporter, error) {
 		return nil, err
 	}
 
+	err = config.RemoteWriteProtoMsg.Validate(cfg.RemoteWriteProtoMsg)
path: exporter/prometheusremotewriteexporter/exporter.go
user: login: Aneurysm9
body: There should be no need to re-validate here if it has already been validated, no?
created_at: 2024-11-25T21:46:03Z
author_association: MEMBER
diff_hunk: @@ -331,10 +361,23 @@ func (prwe *prwExporter) execute(ctx context.Context, writeReq *prompb.WriteRequ
 		// Add necessary headers specified by:
 		// https://cortexmetrics.io/docs/apis/#remote-api
 		req.Header.Add("Content-Encoding", "snappy")
-		req.Header.Set("Content-Type", "application/x-protobuf")
-		req.Header.Set("X-Prometheus-Remote-Write-Version", "0.1.0")
 		req.Header.Set("User-Agent", prwe.userAgentHeader)
 
+		// If feature flag not enabled support only RW1
+		if !enableSendingRW2FeatureGate.IsEnabled() {
+			req.Header.Set("Content-Type", "application/x-protobuf")
+			req.Header.Set("X-Prometheus-Remote-Write-Version", "0.1.0")
+		} else {
+			switch prwe.RemoteWriteProtoMsg {
+			case config.RemoteWriteProtoMsgV1:
+				req.Header.Set("Content-Type", "application/x-protobuf")
+				req.Header.Set("X-Prometheus-Remote-Write-Version", "0.1.0")
+			case config.RemoteWriteProtoMsgV2:
+				req.Header.Set("Content-Type", "application/x-protobuf;proto=io.prometheus.write.v2.Request")
+				req.Header.Set("X-Prometheus-Remote-Write-Version", "2.0.0")
+			}
path: exporter/prometheusremotewriteexporter/exporter.go
user: login: Aneurysm9
body: We should not panic, even if we think this is an impossible condition due to previous validation.  That could negatively impact other components or pipelines running in the same collector process.  Instead, return an error.
created_at: 2024-11-25T21:55:15Z
author_association: MEMBER
diff_hunk: @@ -66,6 +66,11 @@ The following settings can be optionally configured:
 - `max_batch_size_bytes` (default = `3000000` -> `~2.861 mb`): Maximum size of a batch of
   samples to be sent to the remote write endpoint. If the batch size is larger
   than this value, it will be split into multiple batches.
+- `protobuf_message` (default = `prometheus.WriteRequest`): 
path: exporter/prometheusremotewriteexporter/README.md
user: login: bwplotka
body: Details are [in the proposal](https://github.com/prometheus/proposals/blob/3d85921828eeee033fd71f8730a1638bc5307f4a/proposals/2024-04-09_remote-write-20.md#no-automatic-content-negotiation) why both Prometheus server and spec do NOT define and implement automatic negotiation.

You can do v2 try first and fallback to v1 until restart, just make sure to check existence of v2 response headers. Some (broken) receivers e.g. old Prometheus, will respond 200 and consume no metrics for v2 request (this is why, among other reasons, response headers were added). Unfortunately you might get 4xx for other reasons than lack of v1, just be aware.

I think I would keep it manual and move default to v2 once adoption settles.

> Changing it in Prometheus would be a big breaking change, wouldn't it? Can it be done before 4.0?

We will deprecate `protobuf_message` and add `content_type` field or so. So old field will still work (in the limited form) -- likely. Still it might be next year or in 2y.
created_at: 2024-11-26T16:14:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -331,10 +361,23 @@ func (prwe *prwExporter) execute(ctx context.Context, writeReq *prompb.WriteRequ
 		// Add necessary headers specified by:
 		// https://cortexmetrics.io/docs/apis/#remote-api
 		req.Header.Add("Content-Encoding", "snappy")
-		req.Header.Set("Content-Type", "application/x-protobuf")
-		req.Header.Set("X-Prometheus-Remote-Write-Version", "0.1.0")
 		req.Header.Set("User-Agent", prwe.userAgentHeader)
 
+		// If feature flag not enabled support only RW1
+		if !enableSendingRW2FeatureGate.IsEnabled() {
+			req.Header.Set("Content-Type", "application/x-protobuf")
+			req.Header.Set("X-Prometheus-Remote-Write-Version", "0.1.0")
+		} else {
+			switch prwe.RemoteWriteProtoMsg {
+			case config.RemoteWriteProtoMsgV1:
+				req.Header.Set("Content-Type", "application/x-protobuf")
+				req.Header.Set("X-Prometheus-Remote-Write-Version", "0.1.0")
+			case config.RemoteWriteProtoMsgV2:
+				req.Header.Set("Content-Type", "application/x-protobuf;proto=io.prometheus.write.v2.Request")
+				req.Header.Set("X-Prometheus-Remote-Write-Version", "2.0.0")
+			}
path: exporter/prometheusremotewriteexporter/exporter.go
user: login: jmichalek132
body: addressed.
created_at: 2024-11-27T16:03:54Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,116 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+package prometheusremotewriteexporter // import "github.com/open-telemetry/opentelemetry-collector-contrib/exporter/prometheusremotewriteexporter"
+
+import (
+	"context"
+	"math"
+	"sync"
+
+	writev2 "github.com/prometheus/prometheus/prompb/io/prometheus/write/v2"
+	"go.opentelemetry.io/collector/consumer/consumererror"
+	"go.opentelemetry.io/collector/pdata/pmetric"
+	"go.uber.org/multierr"
+	"go.uber.org/zap"
+
+	"github.com/open-telemetry/opentelemetry-collector-contrib/pkg/translator/prometheusremotewrite"
+)
+
+func (prwe *prwExporter) pushMetricsV2(ctx context.Context, md pmetric.Metrics) error {
+	tsMap, symbolsTable, err := prometheusremotewrite.FromMetricsV2(md, prwe.exporterSettings)
+
+	prwe.telemetry.recordTranslatedTimeSeries(ctx, len(tsMap))
+
+	if err != nil {
+		prwe.telemetry.recordTranslationFailure(ctx)
+		prwe.settings.Logger.Debug("failed to translate metrics, exporting remaining metrics", zap.Error(err), zap.Int("translated", len(tsMap)))
+	}
+	// Call export even if a conversion error, since there may be points that were successfully converted.
+	return prwe.handleExportV2(ctx, symbolsTable, tsMap)
+}
+
+// export sends a Snappy-compressed writev2.Request containing writev2.TimeSeries to a remote write endpoint
+func (prwe *prwExporter) exportV2(ctx context.Context, requests []*writev2.Request) error {
+	input := make(chan *writev2.Request, len(requests))
+	for _, request := range requests {
+		input <- request
+	}
+	close(input)
+
+	var wg sync.WaitGroup
+
+	concurrencyLimit := int(math.Min(float64(prwe.concurrency), float64(len(requests))))
+	wg.Add(concurrencyLimit) // used to wait for workers to be finished
+
+	var mu sync.Mutex
+	var errs error
+	// Run concurrencyLimit of workers until there
+	// is no more requests to execute in the input channel.
+	for i := 0; i < concurrencyLimit; i++ {
+		go func() {
+			defer wg.Done()
+			for {
+				select {
+				case <-ctx.Done(): // Check firstly to ensure that the context wasn't cancelled.
+					return
+
+				case request, ok := <-input:
+					if !ok {
+						return
+					}
+
+					buf := bufferPool.Get().(*buffer)
+					buf.protobuf.Reset()
+					defer bufferPool.Put(buf)
+
+					// Uses proto.Marshal to convert the WriteRequest into bytes array
+					errMarshal := buf.protobuf.Marshal(request)
+					if errMarshal != nil {
+						errs = multierr.Append(errs, consumererror.NewPermanent(errMarshal))
+						return
+					}
+
+					if errExecute := prwe.execute(ctx, buf); errExecute != nil {
+						mu.Lock()
+						errs = multierr.Append(errs, consumererror.NewPermanent(errExecute))
+						mu.Unlock()
+					}
+				}
+			}
+		}()
+	}
+	wg.Wait()
+
+	return errs
+}
+
+func (prwe *prwExporter) handleExportV2(ctx context.Context, symbolsTable writev2.SymbolsTable, tsMap map[string]*writev2.TimeSeries) error {
+	// There are no metrics to export, so return.
+	if len(tsMap) == 0 {
+		return nil
+	}
+
+	// TODO implement batching
+	// TODO how do we handle symbolsTable with batching?
+	requests := make([]*writev2.Request, 0)
path: exporter/prometheusremotewriteexporter/exporter_v2.go
user: login: jmichalek132
body: In the initial implementation it's only 1 request but when we implement batching support it can be multiple, is it okay to leave it as is? So we don't have to change function signatures.
created_at: 2024-11-27T16:10:11Z
author_association: CONTRIBUTOR
diff_hunk: @@ -66,6 +66,11 @@ The following settings can be optionally configured:
 - `max_batch_size_bytes` (default = `3000000` -> `~2.861 mb`): Maximum size of a batch of
   samples to be sent to the remote write endpoint. If the batch size is larger
   than this value, it will be split into multiple batches.
+- `protobuf_message` (default = `prometheus.WriteRequest`): 
path: exporter/prometheusremotewriteexporter/README.md
user: login: jmichalek132
body: So okay to keep this as is?
created_at: 2024-11-27T16:16:44Z
author_association: CONTRIBUTOR
diff_hunk: @@ -66,6 +66,11 @@ The following settings can be optionally configured:
 - `max_batch_size_bytes` (default = `3000000` -> `~2.861 mb`): Maximum size of a batch of
   samples to be sent to the remote write endpoint. If the batch size is larger
   than this value, it will be split into multiple batches.
+- `protobuf_message` (default = `prometheus.WriteRequest`): 
path: exporter/prometheusremotewriteexporter/README.md
user: login: jmichalek132
body: Resolving this we can change it later.
created_at: 2024-11-27T17:05:54Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,116 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+package prometheusremotewriteexporter // import "github.com/open-telemetry/opentelemetry-collector-contrib/exporter/prometheusremotewriteexporter"
+
+import (
+	"context"
+	"math"
+	"sync"
+
+	writev2 "github.com/prometheus/prometheus/prompb/io/prometheus/write/v2"
+	"go.opentelemetry.io/collector/consumer/consumererror"
+	"go.opentelemetry.io/collector/pdata/pmetric"
+	"go.uber.org/multierr"
+	"go.uber.org/zap"
+
+	"github.com/open-telemetry/opentelemetry-collector-contrib/pkg/translator/prometheusremotewrite"
+)
+
+func (prwe *prwExporter) pushMetricsV2(ctx context.Context, md pmetric.Metrics) error {
+	tsMap, symbolsTable, err := prometheusremotewrite.FromMetricsV2(md, prwe.exporterSettings)
+
+	prwe.telemetry.recordTranslatedTimeSeries(ctx, len(tsMap))
+
+	if err != nil {
+		prwe.telemetry.recordTranslationFailure(ctx)
+		prwe.settings.Logger.Debug("failed to translate metrics, exporting remaining metrics", zap.Error(err), zap.Int("translated", len(tsMap)))
+	}
+	// Call export even if a conversion error, since there may be points that were successfully converted.
+	return prwe.handleExportV2(ctx, symbolsTable, tsMap)
+}
+
+// export sends a Snappy-compressed writev2.Request containing writev2.TimeSeries to a remote write endpoint
+func (prwe *prwExporter) exportV2(ctx context.Context, requests []*writev2.Request) error {
+	input := make(chan *writev2.Request, len(requests))
+	for _, request := range requests {
+		input <- request
+	}
+	close(input)
+
+	var wg sync.WaitGroup
+
+	concurrencyLimit := int(math.Min(float64(prwe.concurrency), float64(len(requests))))
+	wg.Add(concurrencyLimit) // used to wait for workers to be finished
+
+	var mu sync.Mutex
+	var errs error
+	// Run concurrencyLimit of workers until there
+	// is no more requests to execute in the input channel.
+	for i := 0; i < concurrencyLimit; i++ {
+		go func() {
+			defer wg.Done()
+			for {
+				select {
+				case <-ctx.Done(): // Check firstly to ensure that the context wasn't cancelled.
+					return
+
+				case request, ok := <-input:
+					if !ok {
+						return
+					}
+
+					buf := bufferPool.Get().(*buffer)
+					buf.protobuf.Reset()
+					defer bufferPool.Put(buf)
+
+					// Uses proto.Marshal to convert the WriteRequest into bytes array
+					errMarshal := buf.protobuf.Marshal(request)
+					if errMarshal != nil {
+						errs = multierr.Append(errs, consumererror.NewPermanent(errMarshal))
path: exporter/prometheusremotewriteexporter/exporter_v2.go
user: login: jmichalek132
body: Not sure the lock is even necessary will test it and either remove it or add it to both.
created_at: 2024-11-27T17:14:10Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,116 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+package prometheusremotewriteexporter // import "github.com/open-telemetry/opentelemetry-collector-contrib/exporter/prometheusremotewriteexporter"
+
+import (
+	"context"
+	"math"
+	"sync"
+
+	writev2 "github.com/prometheus/prometheus/prompb/io/prometheus/write/v2"
+	"go.opentelemetry.io/collector/consumer/consumererror"
+	"go.opentelemetry.io/collector/pdata/pmetric"
+	"go.uber.org/multierr"
+	"go.uber.org/zap"
+
+	"github.com/open-telemetry/opentelemetry-collector-contrib/pkg/translator/prometheusremotewrite"
+)
+
+func (prwe *prwExporter) pushMetricsV2(ctx context.Context, md pmetric.Metrics) error {
+	tsMap, symbolsTable, err := prometheusremotewrite.FromMetricsV2(md, prwe.exporterSettings)
+
+	prwe.telemetry.recordTranslatedTimeSeries(ctx, len(tsMap))
+
+	if err != nil {
+		prwe.telemetry.recordTranslationFailure(ctx)
+		prwe.settings.Logger.Debug("failed to translate metrics, exporting remaining metrics", zap.Error(err), zap.Int("translated", len(tsMap)))
+	}
+	// Call export even if a conversion error, since there may be points that were successfully converted.
+	return prwe.handleExportV2(ctx, symbolsTable, tsMap)
+}
+
+// export sends a Snappy-compressed writev2.Request containing writev2.TimeSeries to a remote write endpoint
+func (prwe *prwExporter) exportV2(ctx context.Context, requests []*writev2.Request) error {
+	input := make(chan *writev2.Request, len(requests))
+	for _, request := range requests {
+		input <- request
+	}
+	close(input)
+
+	var wg sync.WaitGroup
+
+	concurrencyLimit := int(math.Min(float64(prwe.concurrency), float64(len(requests))))
+	wg.Add(concurrencyLimit) // used to wait for workers to be finished
+
+	var mu sync.Mutex
+	var errs error
+	// Run concurrencyLimit of workers until there
+	// is no more requests to execute in the input channel.
+	for i := 0; i < concurrencyLimit; i++ {
+		go func() {
+			defer wg.Done()
+			for {
+				select {
+				case <-ctx.Done(): // Check firstly to ensure that the context wasn't cancelled.
+					return
+
+				case request, ok := <-input:
+					if !ok {
+						return
+					}
+
+					buf := bufferPool.Get().(*buffer)
+					buf.protobuf.Reset()
+					defer bufferPool.Put(buf)
path: exporter/prometheusremotewriteexporter/exporter_v2.go
user: login: jmichalek132
body: fixed
created_at: 2024-11-27T17:20:58Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,116 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+package prometheusremotewriteexporter // import "github.com/open-telemetry/opentelemetry-collector-contrib/exporter/prometheusremotewriteexporter"
+
+import (
+	"context"
+	"math"
+	"sync"
+
+	writev2 "github.com/prometheus/prometheus/prompb/io/prometheus/write/v2"
+	"go.opentelemetry.io/collector/consumer/consumererror"
+	"go.opentelemetry.io/collector/pdata/pmetric"
+	"go.uber.org/multierr"
+	"go.uber.org/zap"
+
+	"github.com/open-telemetry/opentelemetry-collector-contrib/pkg/translator/prometheusremotewrite"
+)
+
+func (prwe *prwExporter) pushMetricsV2(ctx context.Context, md pmetric.Metrics) error {
+	tsMap, symbolsTable, err := prometheusremotewrite.FromMetricsV2(md, prwe.exporterSettings)
+
+	prwe.telemetry.recordTranslatedTimeSeries(ctx, len(tsMap))
+
+	if err != nil {
+		prwe.telemetry.recordTranslationFailure(ctx)
+		prwe.settings.Logger.Debug("failed to translate metrics, exporting remaining metrics", zap.Error(err), zap.Int("translated", len(tsMap)))
+	}
+	// Call export even if a conversion error, since there may be points that were successfully converted.
+	return prwe.handleExportV2(ctx, symbolsTable, tsMap)
+}
+
+// export sends a Snappy-compressed writev2.Request containing writev2.TimeSeries to a remote write endpoint
+func (prwe *prwExporter) exportV2(ctx context.Context, requests []*writev2.Request) error {
+	input := make(chan *writev2.Request, len(requests))
+	for _, request := range requests {
+		input <- request
+	}
+	close(input)
+
+	var wg sync.WaitGroup
+
+	concurrencyLimit := int(math.Min(float64(prwe.concurrency), float64(len(requests))))
+	wg.Add(concurrencyLimit) // used to wait for workers to be finished
+
+	var mu sync.Mutex
+	var errs error
+	// Run concurrencyLimit of workers until there
+	// is no more requests to execute in the input channel.
+	for i := 0; i < concurrencyLimit; i++ {
+		go func() {
+			defer wg.Done()
+			for {
+				select {
+				case <-ctx.Done(): // Check firstly to ensure that the context wasn't cancelled.
+					return
+
+				case request, ok := <-input:
+					if !ok {
+						return
+					}
+
+					buf := bufferPool.Get().(*buffer)
+					buf.protobuf.Reset()
+					defer bufferPool.Put(buf)
+
+					// Uses proto.Marshal to convert the WriteRequest into bytes array
+					errMarshal := buf.protobuf.Marshal(request)
+					if errMarshal != nil {
+						errs = multierr.Append(errs, consumererror.NewPermanent(errMarshal))
+						return
+					}
+
+					if errExecute := prwe.execute(ctx, buf); errExecute != nil {
+						mu.Lock()
+						errs = multierr.Append(errs, consumererror.NewPermanent(errExecute))
+						mu.Unlock()
+					}
+				}
+			}
+		}()
+	}
+	wg.Wait()
+
+	return errs
+}
+
+func (prwe *prwExporter) handleExportV2(ctx context.Context, symbolsTable writev2.SymbolsTable, tsMap map[string]*writev2.TimeSeries) error {
+	// There are no metrics to export, so return.
+	if len(tsMap) == 0 {
+		return nil
+	}
+
+	// TODO implement batching
+	// TODO how do we handle symbolsTable with batching?
+	requests := make([]*writev2.Request, 0)
path: exporter/prometheusremotewriteexporter/exporter_v2.go
user: login: aknuds1
body: OK, makes sense.
created_at: 2024-12-02T17:04:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -331,10 +359,20 @@ func (prwe *prwExporter) execute(ctx context.Context, writeReq *prompb.WriteRequ
 		// Add necessary headers specified by:
 		// https://cortexmetrics.io/docs/apis/#remote-api
 		req.Header.Add("Content-Encoding", "snappy")
-		req.Header.Set("Content-Type", "application/x-protobuf")
-		req.Header.Set("X-Prometheus-Remote-Write-Version", "0.1.0")
 		req.Header.Set("User-Agent", prwe.userAgentHeader)
 
+		switch {
+		// If feature flag not enabled support only RW1
+		case !enableSendingRW2FeatureGate.IsEnabled(), prwe.RemoteWriteProtoMsg == config.RemoteWriteProtoMsgV1:
+			req.Header.Set("Content-Type", "application/x-protobuf")
+			req.Header.Set("X-Prometheus-Remote-Write-Version", "0.1.0")
+		case prwe.RemoteWriteProtoMsg == config.RemoteWriteProtoMsgV2:
+			req.Header.Set("Content-Type", "application/x-protobuf;proto=io.prometheus.write.v2.Request")
+			req.Header.Set("X-Prometheus-Remote-Write-Version", "2.0.0")
+		default:
+			return errors.New("proto message validated earlier")
path: exporter/prometheusremotewriteexporter/exporter.go
user: login: bwplotka
body: nit: if we do err check vs panic here, let's do it properly (it's true a later version of this code could by accident remove pre-validation.

```suggestion
			return fmt.Errorf("unsupported remote-write protobuf message: %v (should be validated earlier)", prwe.RemoteWriteProtoMsg)
```
created_at: 2024-12-03T09:39:40Z
author_association: CONTRIBUTOR
diff_hunk: @@ -450,38 +455,52 @@ func Test_PushMetrics(t *testing.T) {
 
 	staleNaNSumBatch := getMetricsFromMetricList(staleNaNMetrics[staleNaNSum])
 
-	checkFunc := func(t *testing.T, r *http.Request, expected int, isStaleMarker bool) {
+	checkFunc := func(t *testing.T, r *http.Request, expected int, isStaleMarker bool, enableSendingRW2 bool) {
 		body, err := io.ReadAll(r.Body)
 		if err != nil {
 			t.Fatal(err)
 		}
 
 		buf := make([]byte, len(body))
 		dest, err := snappy.Decode(buf, body)
-		assert.Equal(t, "0.1.0", r.Header.Get("x-prometheus-remote-write-version"))
+		if enableSendingRW2 {
+			assert.Equal(t, "2.0.0", r.Header.Get("x-prometheus-remote-write-version"))
+			assert.Equal(t, "application/x-protobuf;proto=io.prometheus.write.v2.Request", r.Header.Get("Content-Type"))
+		} else {
+			assert.Equal(t, "0.1.0", r.Header.Get("x-prometheus-remote-write-version"))
+		}
 		assert.Equal(t, "snappy", r.Header.Get("content-encoding"))
 		assert.Equal(t, "opentelemetry-collector/1.0", r.Header.Get("User-Agent"))
 		assert.NotNil(t, r.Header.Get("tenant-id"))
 		require.NoError(t, err)
-		wr := &prompb.WriteRequest{}
-		ok := proto.Unmarshal(dest, wr)
-		require.NoError(t, ok)
-		assert.Len(t, wr.Timeseries, expected)
-		if isStaleMarker {
-			assert.True(t, value.IsStaleNaN(wr.Timeseries[0].Samples[0].Value))
+		if enableSendingRW2 {
+			wr := &writev2.Request{}
+			ok := proto.Unmarshal(dest, wr)
+			require.NoError(t, ok)
+			assert.Len(t, wr.Timeseries, expected)
+			// TODO check labels
path: exporter/prometheusremotewriteexporter/exporter_test.go
user: login: bwplotka
body: Should there be a GH issue about it to not forget? Or todo now?
created_at: 2024-12-03T09:40:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1175,8 +1212,18 @@ func TestRetries(t *testing.T) {
 					Enabled: true,
 				},
 			}
+			buf := bufferPool.Get().(*buffer)
+			buf.protobuf.Reset()
+			defer bufferPool.Put(buf)
+
+			// Uses proto.Marshal to convert the WriteRequest into bytes array
path: exporter/prometheusremotewriteexporter/exporter_test.go
user: login: bwplotka
body: This comments repeats in multiple places. Is it really useful?
created_at: 2024-12-03T09:41:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,116 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+package prometheusremotewriteexporter // import "github.com/open-telemetry/opentelemetry-collector-contrib/exporter/prometheusremotewriteexporter"
+
+import (
+	"context"
+	"math"
+	"sync"
+
+	writev2 "github.com/prometheus/prometheus/prompb/io/prometheus/write/v2"
+	"go.opentelemetry.io/collector/consumer/consumererror"
+	"go.opentelemetry.io/collector/pdata/pmetric"
+	"go.uber.org/multierr"
+	"go.uber.org/zap"
+
+	"github.com/open-telemetry/opentelemetry-collector-contrib/pkg/translator/prometheusremotewrite"
+)
+
+func (prwe *prwExporter) pushMetricsV2(ctx context.Context, md pmetric.Metrics) error {
+	tsMap, symbolsTable, err := prometheusremotewrite.FromMetricsV2(md, prwe.exporterSettings)
+
+	prwe.telemetry.recordTranslatedTimeSeries(ctx, len(tsMap))
+
+	if err != nil {
+		prwe.telemetry.recordTranslationFailure(ctx)
+		prwe.settings.Logger.Debug("failed to translate metrics, exporting remaining metrics", zap.Error(err), zap.Int("translated", len(tsMap)))
+	}
+	// Call export even if a conversion error, since there may be points that were successfully converted.
+	return prwe.handleExportV2(ctx, symbolsTable, tsMap)
+}
+
+// export sends a Snappy-compressed writev2.Request containing writev2.TimeSeries to a remote write endpoint
+func (prwe *prwExporter) exportV2(ctx context.Context, requests []*writev2.Request) error {
+	input := make(chan *writev2.Request, len(requests))
+	for _, request := range requests {
+		input <- request
+	}
+	close(input)
+
+	var wg sync.WaitGroup
+
+	concurrencyLimit := int(math.Min(float64(prwe.concurrency), float64(len(requests))))
+	wg.Add(concurrencyLimit) // used to wait for workers to be finished
+
+	var mu sync.Mutex
+	var errs error
+	// Run concurrencyLimit of workers until there
+	// is no more requests to execute in the input channel.
+	for i := 0; i < concurrencyLimit; i++ {
+		go func() {
+			defer wg.Done()
+			for {
+				select {
+				case <-ctx.Done(): // Check firstly to ensure that the context wasn't cancelled.
+					return
+
+				case request, ok := <-input:
+					if !ok {
+						return
+					}
+
+					buf := bufferPool.Get().(*buffer)
+					buf.protobuf.Reset()
+					defer bufferPool.Put(buf)
+
+					// Uses proto.Marshal to convert the WriteRequest into bytes array
+					errMarshal := buf.protobuf.Marshal(request)
+					if errMarshal != nil {
+						errs = multierr.Append(errs, consumererror.NewPermanent(errMarshal))
path: exporter/prometheusremotewriteexporter/exporter_v2.go
user: login: bwplotka
body: I believe it's a race if you write to shared `errs` from multiple goroutines. You need to either lock it, or use channel or some other concurrency structure like `errgroup`. Locking like you did in 75 is fine.
created_at: 2024-12-03T09:43:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -275,7 +301,18 @@ func (prwe *prwExporter) export(ctx context.Context, requests []*prompb.WriteReq
 					if !ok {
 						return
 					}
-					if errExecute := prwe.execute(ctx, request); errExecute != nil {
+
+					buf := bufferPool.Get().(*buffer)
+					buf.protobuf.Reset()
+					defer bufferPool.Put(buf)
+
+					// Uses proto.Marshal to convert the WriteRequest into bytes array.
+					if errMarshal := buf.protobuf.Marshal(request); errMarshal != nil {
+						errs = multierr.Append(errs, consumererror.NewPermanent(errMarshal))
path: exporter/prometheusremotewriteexporter/exporter.go
user: login: bwplotka
body: ditto: risk of race here (errs shared by multiple goroutines and not locked)
created_at: 2024-12-03T09:44:56Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,117 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+package prometheusremotewriteexporter // import "github.com/open-telemetry/opentelemetry-collector-contrib/exporter/prometheusremotewriteexporter"
+
+import (
+	"context"
+	"math"
+	"sync"
+
+	writev2 "github.com/prometheus/prometheus/prompb/io/prometheus/write/v2"
+	"go.opentelemetry.io/collector/consumer/consumererror"
+	"go.opentelemetry.io/collector/pdata/pmetric"
+	"go.uber.org/multierr"
+	"go.uber.org/zap"
+
+	"github.com/open-telemetry/opentelemetry-collector-contrib/pkg/translator/prometheusremotewrite"
+)
+
+func (prwe *prwExporter) pushMetricsV2(ctx context.Context, md pmetric.Metrics) error {
+	tsMap, symbolsTable, err := prometheusremotewrite.FromMetricsV2(md, prwe.exporterSettings)
+
+	prwe.telemetry.recordTranslatedTimeSeries(ctx, len(tsMap))
+
+	if err != nil {
+		prwe.telemetry.recordTranslationFailure(ctx)
+		prwe.settings.Logger.Debug("failed to translate metrics, exporting remaining metrics", zap.Error(err), zap.Int("translated", len(tsMap)))
+	}
+	// Call export even if a conversion error, since there may be points that were successfully converted.
+	return prwe.handleExportV2(ctx, symbolsTable, tsMap)
+}
+
+// exportV2 sends a Snappy-compressed writev2.Request containing writev2.TimeSeries to a remote write endpoint.
+func (prwe *prwExporter) exportV2(ctx context.Context, requests []*writev2.Request) error {
+	input := make(chan *writev2.Request, len(requests))
+	for _, request := range requests {
+		input <- request
+	}
+	close(input)
+
+	var wg sync.WaitGroup
+
+	concurrencyLimit := int(math.Min(float64(prwe.concurrency), float64(len(requests))))
+	wg.Add(concurrencyLimit) // used to wait for workers to be finished
+
+	var mu sync.Mutex
+	var errs error
+	// Run concurrencyLimit of workers until there
+	// is no more requests to execute in the input channel.
+	for i := 0; i < concurrencyLimit; i++ {
+		go func() {
+			defer wg.Done()
+			for {
+				select {
+				case <-ctx.Done(): // Check firstly to ensure that the context wasn't cancelled.
+					return
+
+				case request, ok := <-input:
+					if !ok {
+						return
+					}
+
+					buf := bufferPool.Get().(*buffer)
+					buf.protobuf.Reset()
+
+					// Uses proto.Marshal to convert the WriteRequest into bytes array
+					errMarshal := buf.protobuf.Marshal(request)
+					if errMarshal != nil {
+						errs = multierr.Append(errs, consumererror.NewPermanent(errMarshal))
+						bufferPool.Put(buf)
+						return
+					}
+
+					if errExecute := prwe.execute(ctx, buf); errExecute != nil {
+						mu.Lock()
+						errs = multierr.Append(errs, consumererror.NewPermanent(errExecute))
+						mu.Unlock()
+					}
+					bufferPool.Put(buf)
+				}
+			}
+		}()
+	}
+	wg.Wait()
+
+	return errs
+}
+
+func (prwe *prwExporter) handleExportV2(ctx context.Context, symbolsTable writev2.SymbolsTable, tsMap map[string]*writev2.TimeSeries) error {
+	// There are no metrics to export, so return.
+	if len(tsMap) == 0 {
+		return nil
+	}
+
+	// TODO implement batching
+	// TODO how do we handle symbolsTable with batching?
path: exporter/prometheusremotewriteexporter/exporter_v2.go
user: login: bwplotka
body: You have to build separate symbol tables or use a bigger single one, whatever is cheaper.
created_at: 2024-12-03T09:45:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1175,8 +1212,18 @@ func TestRetries(t *testing.T) {
 					Enabled: true,
 				},
 			}
+			buf := bufferPool.Get().(*buffer)
+			buf.protobuf.Reset()
+			defer bufferPool.Put(buf)
+
+			// Uses proto.Marshal to convert the WriteRequest into bytes array
path: exporter/prometheusremotewriteexporter/exporter_test.go
user: login: jmichalek132
body: removed it.
created_at: 2025-05-02T16:28:16Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,117 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+package prometheusremotewriteexporter // import "github.com/open-telemetry/opentelemetry-collector-contrib/exporter/prometheusremotewriteexporter"
+
+import (
+	"context"
+	"math"
+	"sync"
+
+	writev2 "github.com/prometheus/prometheus/prompb/io/prometheus/write/v2"
+	"go.opentelemetry.io/collector/consumer/consumererror"
+	"go.opentelemetry.io/collector/pdata/pmetric"
+	"go.uber.org/multierr"
+	"go.uber.org/zap"
+
+	"github.com/open-telemetry/opentelemetry-collector-contrib/pkg/translator/prometheusremotewrite"
+)
+
+func (prwe *prwExporter) pushMetricsV2(ctx context.Context, md pmetric.Metrics) error {
+	tsMap, symbolsTable, err := prometheusremotewrite.FromMetricsV2(md, prwe.exporterSettings)
+
+	prwe.telemetry.recordTranslatedTimeSeries(ctx, len(tsMap))
+
+	if err != nil {
+		prwe.telemetry.recordTranslationFailure(ctx)
+		prwe.settings.Logger.Debug("failed to translate metrics, exporting remaining metrics", zap.Error(err), zap.Int("translated", len(tsMap)))
+	}
+	// Call export even if a conversion error, since there may be points that were successfully converted.
+	return prwe.handleExportV2(ctx, symbolsTable, tsMap)
+}
+
+// exportV2 sends a Snappy-compressed writev2.Request containing writev2.TimeSeries to a remote write endpoint.
+func (prwe *prwExporter) exportV2(ctx context.Context, requests []*writev2.Request) error {
+	input := make(chan *writev2.Request, len(requests))
+	for _, request := range requests {
+		input <- request
+	}
+	close(input)
+
+	var wg sync.WaitGroup
+
+	concurrencyLimit := int(math.Min(float64(prwe.concurrency), float64(len(requests))))
+	wg.Add(concurrencyLimit) // used to wait for workers to be finished
+
+	var mu sync.Mutex
+	var errs error
+	// Run concurrencyLimit of workers until there
+	// is no more requests to execute in the input channel.
+	for i := 0; i < concurrencyLimit; i++ {
+		go func() {
+			defer wg.Done()
+			for {
+				select {
+				case <-ctx.Done(): // Check firstly to ensure that the context wasn't cancelled.
+					return
+
+				case request, ok := <-input:
+					if !ok {
+						return
+					}
+
+					buf := bufferPool.Get().(*buffer)
+					buf.protobuf.Reset()
+
+					// Uses proto.Marshal to convert the WriteRequest into bytes array
+					errMarshal := buf.protobuf.Marshal(request)
+					if errMarshal != nil {
+						errs = multierr.Append(errs, consumererror.NewPermanent(errMarshal))
+						bufferPool.Put(buf)
+						return
+					}
+
+					if errExecute := prwe.execute(ctx, buf); errExecute != nil {
+						mu.Lock()
+						errs = multierr.Append(errs, consumererror.NewPermanent(errExecute))
+						mu.Unlock()
+					}
+					bufferPool.Put(buf)
+				}
+			}
+		}()
+	}
+	wg.Wait()
+
+	return errs
+}
+
+func (prwe *prwExporter) handleExportV2(ctx context.Context, symbolsTable writev2.SymbolsTable, tsMap map[string]*writev2.TimeSeries) error {
+	// There are no metrics to export, so return.
+	if len(tsMap) == 0 {
+		return nil
+	}
+
+	// TODO implement batching
+	// TODO how do we handle symbolsTable with batching?
path: exporter/prometheusremotewriteexporter/exporter_v2.go
user: login: jmichalek132
body: Will be handled in separate PR.
created_at: 2025-05-02T16:28:47Z
author_association: CONTRIBUTOR
diff_hunk: @@ -275,7 +301,18 @@ func (prwe *prwExporter) export(ctx context.Context, requests []*prompb.WriteReq
 					if !ok {
 						return
 					}
-					if errExecute := prwe.execute(ctx, request); errExecute != nil {
+
+					buf := bufferPool.Get().(*buffer)
+					buf.protobuf.Reset()
+					defer bufferPool.Put(buf)
+
+					// Uses proto.Marshal to convert the WriteRequest into bytes array.
+					if errMarshal := buf.protobuf.Marshal(request); errMarshal != nil {
+						errs = multierr.Append(errs, consumererror.NewPermanent(errMarshal))
path: exporter/prometheusremotewriteexporter/exporter.go
user: login: jmichalek132
body: Added locks.
created_at: 2025-05-02T16:30:36Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,27 @@
+# Use this changelog template to create an entry for release notes.
+
+# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix'
+change_type: enhancement
+
+# The name of the component, or a single word describing the area of concern, (e.g. filelogreceiver)
+component: prometheusremotewriteexporter
+
+# A brief description of the change.  Surround your text with quotes ("") if it needs to start with a backtick (`).
+note: Add `exporter.prometheusremotewritexporter.enableSendingRW2` feature gate and configure the exporter to send Prometheus remote write 2.0 version.
path: .chloggen/jm-prom-rw-exporter-add-support-for-rw2.yaml
user: login: ArthurSens
body: ```suggestion
note: Add `exporter.prometheusremotewritexporter.enableSendingRW2` feature gate and configuration to the exporter to send Prometheus remote write 2.0 version.
```
created_at: 2025-05-07T17:43:34Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,27 @@
+# Use this changelog template to create an entry for release notes.
+
+# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix'
+change_type: enhancement
+
+# The name of the component, or a single word describing the area of concern, (e.g. filelogreceiver)
+component: prometheusremotewriteexporter
+
+# A brief description of the change.  Surround your text with quotes ("") if it needs to start with a backtick (`).
+note: Add `exporter.prometheusremotewritexporter.enableSendingRW2` feature gate and configure the exporter to send Prometheus remote write 2.0 version.
+
+# Mandatory: One or more tracking issues related to the change. You can use the PR number here if no issue exists.
+issues: [33661]
+
+# (Optional) One or more lines of additional information to render under the primary note.
+# These lines will be padded with 2 spaces and then inserted directly into the document.
+# Use pipe (|) for multiline entries.
+subtext: PRW 2.0 support for the exporter is still under development.
path: .chloggen/jm-prom-rw-exporter-add-support-for-rw2.yaml
user: login: ArthurSens
body: ```suggestion
subtext: WARNING! PRW 2.0 support for the exporter is still under development and not ready for usage.
```
created_at: 2025-05-07T17:43:59Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,27 @@
+# Use this changelog template to create an entry for release notes.
+
+# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix'
+change_type: enhancement
+
+# The name of the component, or a single word describing the area of concern, (e.g. filelogreceiver)
+component: prometheusremotewriteexporter
+
+# A brief description of the change.  Surround your text with quotes ("") if it needs to start with a backtick (`).
+note: Add `exporter.prometheusremotewritexporter.enableSendingRW2` feature gate and configure the exporter to send Prometheus remote write 2.0 version.
+
+# Mandatory: One or more tracking issues related to the change. You can use the PR number here if no issue exists.
+issues: [33661]
+
+# (Optional) One or more lines of additional information to render under the primary note.
+# These lines will be padded with 2 spaces and then inserted directly into the document.
+# Use pipe (|) for multiline entries.
+subtext: PRW 2.0 support for the exporter is still under development.
path: .chloggen/jm-prom-rw-exporter-add-support-for-rw2.yaml
user: login: ArthurSens
body: let's make this very clear 😅 
created_at: 2025-05-07T17:44:27Z
author_association: MEMBER
diff_hunk: @@ -64,6 +64,11 @@ The following settings can be optionally configured:
   samples to be sent to the remote write endpoint. If the batch size is larger
   than this value, it will be split into multiple batches.
 - `max_batch_request_parallelism` (default = `5`): Maximum parallelism allowed for a single request bigger than `max_batch_size_bytes`.
+- `protobuf_message` (default = `prometheus.WriteRequest`): 
+  - Protobuf message to use when writing to the remote write endpoint. This option is ignored unless the `exporter.prometheusremotewritexporter.enableSendingRW2` feature gate is enabled.
+  - `prometheus.WriteRequest` is the message used in [Remote Write 1.0](https://prometheus.io/docs/specs/remote_write_spec/).
+  - `io.prometheus.write.v2.Request` is the message used in [Remote Write 2.0](https://prometheus.io/docs/specs/remote_write_spec_2_0/). It is more efficient, always includes metadata, and adds support for the created timestamp and native histograms. Your remote storage provider must support PRW 2.0 to be able to use this message. PRW 2.0 support is currently **In Development**, and is only partially implemented.
path: exporter/prometheusremotewriteexporter/README.md
user: login: ArthurSens
body: ```suggestion
  - `io.prometheus.write.v2.Request` is the message used in [Remote Write 2.0](https://prometheus.io/docs/specs/remote_write_spec_2_0/). It is more efficient, always includes metadata, and adds support for the created timestamp and native histograms. Your remote storage provider must support PRW 2.0 to be able to use this message. PRW 2.0 support is currently **In Development** and is only partially implemented, thus, not ready for usage.
```
created_at: 2025-05-07T17:45:56Z
author_association: MEMBER
diff_hunk: @@ -33,6 +34,13 @@ var enableMultipleWorkersFeatureGate = featuregate.GlobalRegistry().MustRegister
 		" spawn multiple workers/goroutines to handle incoming metrics batches concurrently"),
 )
 
+var enableSendingRW2FeatureGate = featuregate.GlobalRegistry().MustRegister(
+	"exporter.prometheusremotewritexporter.enableSendingRW2",
+	featuregate.StageAlpha,
+	featuregate.WithRegisterFromVersion("v0.113.0"),
path: exporter/prometheusremotewriteexporter/factory.go
user: login: ArthurSens
body: ```suggestion
	featuregate.WithRegisterFromVersion("v0.125.0"),
```
created_at: 2025-05-07T17:56:06Z
author_association: MEMBER
diff_hunk: @@ -118,5 +122,5 @@ func (cfg *Config) Validate() error {
 		return errors.New("compression type must be snappy")
 	}
 
-	return nil
+	return cfg.RemoteWriteProtoMsg.Validate()
path: exporter/prometheusremotewriteexporter/config.go
user: login: ArthurSens
body: I tried starting the component using `protobuf_message: io.prometheus.write.v2.Request` without setting the feature flag. I got no errors during startup.

I assume we're currently silently using PRWv1 when the feature flag isn't enabled. Could we error the config validation if the feature flag isn't enabled? Or at the bare minimum log that we're switching to v1. I'd prefer a startup failure though 😬 
created_at: 2025-05-07T18:13:36Z
author_association: MEMBER
diff_hunk: @@ -118,5 +122,5 @@ func (cfg *Config) Validate() error {
 		return errors.New("compression type must be snappy")
 	}
 
-	return nil
+	return cfg.RemoteWriteProtoMsg.Validate()
path: exporter/prometheusremotewriteexporter/config.go
user: login: jmichalek132
body: Your assumption is right you can see that here https://github.com/open-telemetry/opentelemetry-collector-contrib/pull/35888/files#diff-2631d8a8855fdc08cf077c0ab99544147ca73eec572bc871d8fe717e826eda38R243

I added the logic you wanted in the last commit.
created_at: 2025-05-08T15:13:05Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,118 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+package prometheusremotewriteexporter // import "github.com/open-telemetry/opentelemetry-collector-contrib/exporter/prometheusremotewriteexporter"
+
+import (
+	"context"
+	"math"
+	"sync"
+
+	writev2 "github.com/prometheus/prometheus/prompb/io/prometheus/write/v2"
+	"go.opentelemetry.io/collector/consumer/consumererror"
+	"go.opentelemetry.io/collector/pdata/pmetric"
+	"go.uber.org/multierr"
+	"go.uber.org/zap"
+
+	"github.com/open-telemetry/opentelemetry-collector-contrib/pkg/translator/prometheusremotewrite"
+)
+
+func (prwe *prwExporter) pushMetricsV2(ctx context.Context, md pmetric.Metrics) error {
+	tsMap, symbolsTable, err := prometheusremotewrite.FromMetricsV2(md, prwe.exporterSettings)
+
+	prwe.telemetry.recordTranslatedTimeSeries(ctx, len(tsMap))
+
+	if err != nil {
+		prwe.telemetry.recordTranslationFailure(ctx)
+		prwe.settings.Logger.Debug("failed to translate metrics, exporting remaining metrics", zap.Error(err), zap.Int("translated", len(tsMap)))
+	}
+	// Call export even if a conversion error, since there may be points that were successfully converted.
+	return prwe.handleExportV2(ctx, symbolsTable, tsMap)
+}
+
+// exportV2 sends a Snappy-compressed writev2.Request containing writev2.TimeSeries to a remote write endpoint.
+func (prwe *prwExporter) exportV2(ctx context.Context, requests []*writev2.Request) error {
+	input := make(chan *writev2.Request, len(requests))
+	for _, request := range requests {
+		input <- request
+	}
+	close(input)
+
+	var wg sync.WaitGroup
+
+	concurrencyLimit := int(math.Min(float64(prwe.concurrency), float64(len(requests))))
+	wg.Add(concurrencyLimit) // used to wait for workers to be finished
+
+	var mu sync.Mutex
+	var errs error
+	// Run concurrencyLimit of workers until there
+	// is no more requests to execute in the input channel.
+	for i := 0; i < concurrencyLimit; i++ {
+		go func() {
+			defer wg.Done()
+			for {
+				select {
+				case <-ctx.Done(): // Check firstly to ensure that the context wasn't cancelled.
+					return
+
+				case request, ok := <-input:
+					if !ok {
+						return
+					}
+
+					buf := bufferPool.Get().(*buffer)
+					buf.protobuf.Reset()
+
+					errMarshal := buf.protobuf.Marshal(request)
+					if errMarshal != nil {
+						mu.Lock()
+						errs = multierr.Append(errs, consumererror.NewPermanent(errMarshal))
+						mu.Unlock()
+						bufferPool.Put(buf)
+						return
+					}
+
+					if errExecute := prwe.execute(ctx, buf); errExecute != nil {
+						mu.Lock()
+						errs = multierr.Append(errs, consumererror.NewPermanent(errExecute))
+						mu.Unlock()
+					}
+					bufferPool.Put(buf)
+				}
+			}
+		}()
+	}
+	wg.Wait()
+
+	return errs
+}
+
+func (prwe *prwExporter) handleExportV2(ctx context.Context, symbolsTable writev2.SymbolsTable, tsMap map[string]*writev2.TimeSeries) error {
+	// There are no metrics to export, so return.
+	if len(tsMap) == 0 {
+		return nil
+	}
+
+	// TODO implement batching
+	// TODO how do we handle symbolsTable with batching?
+	requests := make([]*writev2.Request, 0)
+	tsArray := make([]writev2.TimeSeries, 0, len(tsMap))
+	for _, v := range tsMap {
+		tsArray = append(tsArray, *v)
+	}
+
+	requests = append(requests, &writev2.Request{
+		// TODO sort
+		// Prometheus requires time series to be sorted by Timestamp to avoid out of order problems.
+		// See:
+		// * https://github.com/open-telemetry/wg-prometheus/issues/10
+		// * https://github.com/open-telemetry/opentelemetry-collector/issues/2315
+		// Timeseries: orderBySampleTimestamp(tsArray),
+		Timeseries: tsArray,
+		Symbols:    symbolsTable.Symbols(),
+	})
+
+	// TODO implement WAl support, can be done after #15277 is fixed
path: exporter/prometheusremotewriteexporter/exporter_v2.go
user: login: ywwg
body: is this meant to still be commented?
created_at: 2025-05-08T16:50:28Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,118 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+package prometheusremotewriteexporter // import "github.com/open-telemetry/opentelemetry-collector-contrib/exporter/prometheusremotewriteexporter"
+
+import (
+	"context"
+	"math"
+	"sync"
+
+	writev2 "github.com/prometheus/prometheus/prompb/io/prometheus/write/v2"
+	"go.opentelemetry.io/collector/consumer/consumererror"
+	"go.opentelemetry.io/collector/pdata/pmetric"
+	"go.uber.org/multierr"
+	"go.uber.org/zap"
+
+	"github.com/open-telemetry/opentelemetry-collector-contrib/pkg/translator/prometheusremotewrite"
+)
+
+func (prwe *prwExporter) pushMetricsV2(ctx context.Context, md pmetric.Metrics) error {
+	tsMap, symbolsTable, err := prometheusremotewrite.FromMetricsV2(md, prwe.exporterSettings)
+
+	prwe.telemetry.recordTranslatedTimeSeries(ctx, len(tsMap))
+
+	if err != nil {
+		prwe.telemetry.recordTranslationFailure(ctx)
+		prwe.settings.Logger.Debug("failed to translate metrics, exporting remaining metrics", zap.Error(err), zap.Int("translated", len(tsMap)))
+	}
+	// Call export even if a conversion error, since there may be points that were successfully converted.
+	return prwe.handleExportV2(ctx, symbolsTable, tsMap)
+}
+
+// exportV2 sends a Snappy-compressed writev2.Request containing writev2.TimeSeries to a remote write endpoint.
+func (prwe *prwExporter) exportV2(ctx context.Context, requests []*writev2.Request) error {
+	input := make(chan *writev2.Request, len(requests))
+	for _, request := range requests {
+		input <- request
+	}
+	close(input)
+
+	var wg sync.WaitGroup
+
+	concurrencyLimit := int(math.Min(float64(prwe.concurrency), float64(len(requests))))
+	wg.Add(concurrencyLimit) // used to wait for workers to be finished
+
+	var mu sync.Mutex
+	var errs error
+	// Run concurrencyLimit of workers until there
+	// is no more requests to execute in the input channel.
+	for i := 0; i < concurrencyLimit; i++ {
+		go func() {
+			defer wg.Done()
+			for {
+				select {
+				case <-ctx.Done(): // Check firstly to ensure that the context wasn't cancelled.
+					return
+
+				case request, ok := <-input:
+					if !ok {
+						return
+					}
+
+					buf := bufferPool.Get().(*buffer)
+					buf.protobuf.Reset()
+
+					errMarshal := buf.protobuf.Marshal(request)
+					if errMarshal != nil {
+						mu.Lock()
+						errs = multierr.Append(errs, consumererror.NewPermanent(errMarshal))
+						mu.Unlock()
+						bufferPool.Put(buf)
+						return
+					}
+
+					if errExecute := prwe.execute(ctx, buf); errExecute != nil {
+						mu.Lock()
+						errs = multierr.Append(errs, consumererror.NewPermanent(errExecute))
+						mu.Unlock()
+					}
+					bufferPool.Put(buf)
+				}
+			}
+		}()
+	}
+	wg.Wait()
+
+	return errs
+}
+
+func (prwe *prwExporter) handleExportV2(ctx context.Context, symbolsTable writev2.SymbolsTable, tsMap map[string]*writev2.TimeSeries) error {
+	// There are no metrics to export, so return.
+	if len(tsMap) == 0 {
+		return nil
+	}
+
+	// TODO implement batching
+	// TODO how do we handle symbolsTable with batching?
+	requests := make([]*writev2.Request, 0)
+	tsArray := make([]writev2.TimeSeries, 0, len(tsMap))
+	for _, v := range tsMap {
+		tsArray = append(tsArray, *v)
+	}
+
+	requests = append(requests, &writev2.Request{
+		// TODO sort
+		// Prometheus requires time series to be sorted by Timestamp to avoid out of order problems.
+		// See:
+		// * https://github.com/open-telemetry/wg-prometheus/issues/10
+		// * https://github.com/open-telemetry/opentelemetry-collector/issues/2315
+		// Timeseries: orderBySampleTimestamp(tsArray),
+		Timeseries: tsArray,
+		Symbols:    symbolsTable.Symbols(),
+	})
+
+	// TODO implement WAl support, can be done after #15277 is fixed
path: exporter/prometheusremotewriteexporter/exporter_v2.go
user: login: jmichalek132
body: Technically no, but I would leave that on a follow up PR I will add a task to the TODO list.
created_at: 2025-05-08T17:15:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -118,5 +122,14 @@ func (cfg *Config) Validate() error {
 		return errors.New("compression type must be snappy")
 	}
 
+	err := cfg.RemoteWriteProtoMsg.Validate()
+	if err != nil {
+		return err
+	}
+
+	if !enableSendingRW2FeatureGate.IsEnabled() && cfg.RemoteWriteProtoMsg == config.RemoteWriteProtoMsgV2 {
+		return errors.New("remote write v2 is not supported, please enable the feature gate")
path: exporter/prometheusremotewriteexporter/config.go
user: login: ArthurSens
body: ```suggestion
		return fmt.Errorf("remote write v2 is only supported with the feature gate %s", enableSendingRW2FeatureGate.ID())
```
created_at: 2025-05-08T18:03:15Z
author_association: MEMBER

PR comments:
user: login: bogdandrutu
created_at: 2024-11-13T23:39:23Z
body: Needs a rebase
author_association: MEMBER
user: login: jmichalek132
created_at: 2024-11-14T10:24:29Z
body: > Needs a rebase

Rebased and the pipeline is passing again too.
author_association: CONTRIBUTOR
user: login: TylerHelmuth
created_at: 2024-11-18T18:22:49Z
body: Needs another rebase
author_association: MEMBER
user: login: dashpole
created_at: 2024-11-18T18:33:05Z
body: attempted a manual rebase.
author_association: CONTRIBUTOR
user: login: dashpole
created_at: 2024-11-18T18:35:13Z
body: didn't work :(
author_association: CONTRIBUTOR
user: login: jmichalek132
created_at: 2024-11-19T13:37:13Z
body: > didn't work :(

I can fix it later today.
author_association: CONTRIBUTOR
user: login: jmichalek132
created_at: 2024-11-20T11:42:23Z
body: > didn't work :(

I don't think the pipeline failure now is due to my changes.
author_association: CONTRIBUTOR
user: login: dashpole
created_at: 2024-11-20T14:45:24Z
body: It looks like you need a `make gotidy`
author_association: CONTRIBUTOR
user: login: jmichalek132
created_at: 2024-11-20T16:20:49Z
body: > It looks like you need a `make gotidy`

ran it globally see if that helps.
author_association: CONTRIBUTOR
user: login: dashpole
created_at: 2024-12-02T16:52:53Z
body: needs rebase :/
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2024-12-31T05:21:11Z
body: This PR was marked stale due to lack of activity. It will be closed in 14 days.
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-01-21T05:21:13Z
body: This PR was marked stale due to lack of activity. It will be closed in 14 days.
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-02-05T05:21:04Z
body: Closed as inactive. Feel free to reopen if this PR is still being worked on.
author_association: CONTRIBUTOR
user: login: jmichalek132
created_at: 2025-05-06T19:01:02Z
body: @ywwg @aknuds1 @bwplotka @ArthurSens review please. All existing comments are resolved so would like to hopefully get this merged so I can continue with other PRs for PRW2 work.
author_association: CONTRIBUTOR
user: login: jmichalek132
created_at: 2025-05-09T07:42:12Z
body: > LGTM, thanks for continuing the work here even after all these months!

Thank you for the patience and all the help ;).
author_association: CONTRIBUTOR