Repository: open-telemetry/opentelemetry-collector-contrib
Pull request #38812: [prometheuswritereceiver] target info
Author: login: perebaj
Labels: name: ready to merge
description: Code review completed; ready to merge by maintainers
name: receiver/prometheusremotewrite
Created: 2025-03-19T22:06:08Z | Merged: 2025-04-24T05:27:19Z

<!--Ex. Fixing a bug - Describe the bug and how this fixes the issue.
Ex. Adding a feature - Explain what this achieves.-->
#### Description

Cache `target_info` metrics so it can be used to populate metrics' Resource Attributes.
<!-- Issue number (e.g. #1234) or full URL to issue, if applicable. -->
#### Link to tracking issue
Partially fixes #37277



Linked issue #37277: Prometheus Remote-Write receiver Alpha
### Component(s)

receiver/prometheusremotewrite

### Describe the issue you're reporting

- [x] Handle empty otel_scope_name and otel_scope_version
- [x] Handle Timeseries samples
- [x] Handle Counters
- [x] Handle Histograms: https://github.com/open-telemetry/opentelemetry-collector-contrib/issues/41043
- [ ] ~Handle Summaries~ Dropping during alpha
- [x] Handle Created Timestamp
- [x] Return number of samples and histograms ingested via response headers
- [x] Handle multiple timeseries with same metric name+type+unit
- [x] Handle target_info
- [x] Implement cache expiry configuration
Linked issue comments: user: login: github-actions[bot]
created_at: 2025-01-16T18:52:58Z
body: Pinging code owners for receiver/prometheusremotewrite: @dashpole @ArthurSens. See [Adding Labels via Comments](https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/main/CONTRIBUTING.md#adding-labels-via-comments) if you do not have permissions to add labels yourself. For example, comment '/label priority:p2 -needs-triaged' to set the priority and remove the needs-triaged label.
author_association: CONTRIBUTOR
user: login: adwait-godbole
created_at: 2025-01-24T16:15:22Z
body: Hi @ArthurSens! Hi @dashpole! I am eager to be a part of this project and looking forward to work under your mentorship and guidance. Are there any pre-requisites / pre-tests for the selection process of mentees ?
author_association: NONE
user: login: ArthurSens
created_at: 2025-01-28T16:18:20Z
body: Hi Adwait, glad to see your interest. There are no pre-requisites or tests, just make sure to apply following the LFX process. Also be aware that our proposal is still just a PR, we still need both CNCF and LFX to approve the mentorship
author_association: MEMBER
user: login: PiyushRajDev
created_at: 2025-01-30T05:58:57Z
body: Hello @ArthurSens ! I am looking forward to contribute to this project. Can you please guide me on how to get started?
author_association: NONE
user: login: ArthurSens
created_at: 2025-01-30T23:02:44Z
body: > Hello [@ArthurSens](https://github.com/ArthurSens) ! I am looking forward to contribute to this project. Can you please guide me on how to get started?

A good starting point is understanding [Remote-Write](https://prometheus.io/docs/specs/remote_write_spec_2_0/) and the [OTLP->Prometheus compatibility spec](https://opentelemetry.io/docs/specs/otel/compatibility/prometheus_and_openmetrics/). If you few comfortable tackling some of the checkboxes above, that's also nice :)
author_association: MEMBER
user: login: hanshal101
created_at: 2025-02-02T15:28:47Z
body: Hey @ArthurSens, Hanshal here. I am an open-source developer contributed to various projects like Buildsafe, Glasskube, Cyclops and a couple of Apache's too. I am currently interning at CloudRaft as a DevOps Intern. I recently published a blog on OTel Auto Instrumentation, and my curiosity towards observability brought me here. I'm looking forward to working on this issue if it gets accepted.
I am pretty much aware of the `OTLP -> Prometheus` spec; what else should I focus on more to understand the issue in a better way?

Thank you.
author_association: NONE
user: login: ArthurSens
created_at: 2025-02-02T15:35:32Z
body: Hi Hanshal, this project is quite simple so we're not requiring that much experience, to be honest. There's nothing else to be familiar with besides the two protocols that were already mentioned: Prometheus Remote Write and OTLP.

We'd love to mentor someone who hasn't had a chance with OSS yet and is excited to participate in the community, that's all.

Anyone interested, just make sure you follow the LFX process correctly.
author_association: MEMBER
user: login: PiyushRajDev
created_at: 2025-02-05T06:58:51Z
body: Hello @ArthurSens ! I have read through the suggested documentation and familiarized myself with the protocols. As I dive deeper into the project, I may encounter some challenges along the way. Would it be possible to join a Slack channel or another communication platform where I can ask quick clarifying questions when needed? I understand you’re busy, so I really appreciate any help or pointers you can offer.
author_association: NONE
user: login: pratikgitss
created_at: 2025-02-05T07:10:46Z
body: I agree with @PiyushRajDev, it would be great to have a communication channel as I have also gone through the protocols and I am figuring out how to tackle the requirements mentioned above and will be really helpful if there is a channel to get the questions clarified.
author_association: NONE
user: login: ArthurSens
created_at: 2025-02-05T08:39:24Z
body: Hello hello 👋 -- This issue is a great communication channel! Feel free to ask questions here :)

But yes, there is also the CNCF slack. We have a channel called #otel-prometheus that can also be used to ask questions. Asking it here would be a lot better though, since the information would be accessible to all.
author_association: MEMBER
user: login: sonalgaud12
created_at: 2025-02-05T21:49:48Z
body: Hey everyone, I'm Sonal! Recently, I've been contributing to the OTEL Collector and OTEL Collector Contrib, specifically under the Receiver package. I'm really excited about this project and believe there's so much to learn from it. By the way, are there any upcoming meetings or briefings about the project? I'd love to contribute more!
author_association: CONTRIBUTOR
user: login: PiyushRajDev
created_at: 2025-02-06T03:53:30Z
body: Hello @sonalgaud, there are not any meeting or briefings as such but we need to just apply through the LFX process for this issue.
author_association: NONE
user: login: Brant-lzh
created_at: 2025-02-08T03:45:31Z
body: Hello, I just need this feature and I am very eager to participate in this project, so what do I need to do now?
author_association: NONE
user: login: PiyushRajDev
created_at: 2025-02-08T04:30:15Z
body: Hey @Brant-lzh, for getting started you need to refer to one of the messages sent by @ArthurSens up above on this thread where he sent  links of the two protcols for getting started. 



author_association: NONE
user: login: Brant-lzh
created_at: 2025-02-08T07:11:42Z
body: Hey @PiyushRajDev ，I understand these contents. I have a certain understanding of prometheus and opentelemetry. How should I start now? Is there a working group?
author_association: NONE
user: login: sonalgaud12
created_at: 2025-02-08T07:19:22Z
body: Hey @Brant-lzh , this project will be developed under the LFX mentorship program. You’ll need to apply for it, and the selected candidate will be working on it. For communication, you can join the CNCF Slack channel named **otel-prometheus**.
author_association: CONTRIBUTOR
user: login: Brant-lzh
created_at: 2025-02-08T07:28:56Z
body: Hey @sonalgaud12 , I joined the Slack Channel, and how to  apply for this LFX mentorship program? Is there any relevant documentation?
author_association: NONE
user: login: sonalgaud12
created_at: 2025-02-08T07:49:12Z
body: Yes @Brant-lzh , refer to this: https://github.com/cncf/mentoring/tree/main/programs/lfx-mentorship/2025/01-Mar-May 
author_association: CONTRIBUTOR
user: login: Brant-lzh
created_at: 2025-02-08T09:24:12Z
body: @sonalgaud12 thx!
author_association: NONE
user: login: PiyushRajDev
created_at: 2025-02-11T07:20:14Z
body: Hey @ArthurSens ,  Could you please point me to any existing implementations or helper functions in the code that follow a similar structure to addCounterDatapoints? It would be helpful to review how similar cases are handled elsewhere in the code for consistency.
author_association: NONE
user: login: ArthurSens
created_at: 2025-02-11T18:38:56Z
body: It should be very very similar to [`addGaugeDatapoints`](https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/e89eaf3ce773a013cf249dd3fa7f648f3f0180e0/receiver/prometheusremotewritereceiver/receiver.go#L233), but I wouldn't implement this first though, let's first handle the scope attributes or the samples.
author_association: MEMBER
user: login: sonalgaud12
created_at: 2025-02-11T19:20:45Z
body: Hey @ArthurSens , for the first to do I could think of two possible solution. 
1. Use Collector Build Tags
2. Use a Default Fallback Value (If Build Tags Are Not Available)
As the comment suggest the collector build approach do let me know which one will be better I'll raise an PR. 
author_association: CONTRIBUTOR
user: login: PiyushRajDev
created_at: 2025-02-11T19:46:02Z
body: Hi @sonalgaud12 ,  There's already a PR addressing this: [#37791](https://github.com/open-telemetry/opentelemetry-collector-contrib/pull/37791)

The PR is currently implementing the Collector Build Tags approach using settings.BuildInfo. 
Perhaps we could collaborate there and help review the existing implementation?
author_association: NONE
user: login: sonalgaud12
created_at: 2025-02-11T19:57:40Z
body: I see, actually I was thinking of adding an scopedetail func to fetch data from collector settings
author_association: CONTRIBUTOR
user: login: PiyushRajDev
created_at: 2025-02-11T20:02:26Z
body: That's an interesting approach, having a scopedetail function could make the code more maintainable and could be reused in both addGaugeDatapoints and addCounterDatapoints.
It could work well with the current PR's approach of using settings.BuildInfo.
Would you like to suggest this in the PR discussion?
author_association: NONE
user: login: ArthurSens
created_at: 2025-03-21T16:56:33Z
body: Whoops, this was closed by accident
author_association: MEMBER
user: login: elliesaber
created_at: 2025-06-04T18:24:58Z
body: Hi team 👋

Thanks for opening this. Is there any update or rough ETA on when an alpha version might be available in contrib?

We’d be happy to help test early builds if that would be useful. Thanks again for the great work!

author_association: NONE
user: login: ArthurSens
created_at: 2025-06-04T22:54:41Z
body: Opening since it was closed by accident 😅. @perebaj please be careful with future PR descriptions since the string `Fixes #...` will close the mentioned PR 
author_association: MEMBER
user: login: ArthurSens
created_at: 2025-06-04T22:58:01Z
body: > Hi team 👋
> 
> Thanks for opening this. Is there any update or rough ETA on when an alpha version might be available in contrib?
> 
> We’d be happy to help test early builds if that would be useful. Thanks again for the great work!

Hi, thanks for the interest in this component :) 

I believe we're super super close to done! We want to finish the implementation of histogram translations, which we already have a PR close to done (https://github.com/open-telemetry/opentelemetry-collector-contrib/pull/39864) and then that should be it :) 

We're having awkward timing because KubeCon Japan is happening next week, and some of us will be absent for a while, but I'm hoping we can get to the Alpha stage sometime in June!
author_association: MEMBER
user: login: perebaj
created_at: 2025-06-11T18:36:49Z
body: @elliesaber I receive an email with a doubt about proto. Are you already facing it? If it was the case, you can send a message on my slack and I can help you with debugging
author_association: CONTRIBUTOR
user: login: elliesaber
created_at: 2025-06-16T18:25:33Z
body: > [@elliesaber](https://github.com/elliesaber) I receive an email with a doubt about proto. Are you already facing it? If it was the case, you can send a message on my slack and I can help you with debugging

I'm currently testing the new prometheusremotewrite receiver and really excited about its potential, thanks for all the work getting this into the contrib repo!

During testing, I encountered a few things I'd love your input on:

**1. Protocol Support**
It looks like the receiver currently supports only protoType: 2, based on [this line in the code](https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/main/receiver/prometheusremotewritereceiver/receiver.go#L162).

Since Prometheus typically uses the legacy protoType: 0 by default, I had to enable Native Histograms to force protoType: 2.

Is support for protoType: 0 planned for future releases?

Or is this already being tracked somewhere?

**2. Memory Usage Observations**
After switching to proto v2, I noticed the Collector pod's memory usage steadily increased, eventually exceeding 1.6 GiB and getting evicted by Kubernetes. I'm using the memory_limiter processor with:

```
check_interval: 500ms
limit_percentage: 75
spike_limit_percentage: 10
```
Even with this configuration, the Collector was unable to stay within memory bounds.

Example log output:

```
2025-06-16T18:17:52.229Z  info  memorylimiter.go:205  Memory usage is above soft limit. Forcing a GC.  
cur_mem_mib: 1381, service.name: otelcontribcol, version: 0.128.0-dev
```
Is this expected behavior, or might it suggest a memory leak or backpressure scenario?

**3. Panic in translateV2 via Sum.DataPoints()**
Under memory pressure, the collector crashed with the following trace:

```
2025-06-16T18:17:54.931Z  error  http/server.go:3638  http: panic serving 10.72.32.243:58174: runtime error: invalid memory address or nil pointer dereference

go.opentelemetry.io/collector/pdata/pmetric.Sum.DataPoints
go.opentelemetry.io/collector-contrib/receiver/prometheusremotewritereceiver.(*prometheusRemoteWriteReceiver).translateV2
```
...
This seems to occur when translateV2() calls .Sum().DataPoints() on a possibly nil or malformed input. It may be worth adding a nil check here to guard against malformed remote write payloads or internal state issues.

Happy to help reproduce or provide additional debug context if helpful. let me know what would be most useful.
Thanks again for all your work 

author_association: NONE
user: login: perebaj
created_at: 2025-06-18T12:31:45Z
body: @elliesaber 

1) I'm not sure about it, I will try to find it out today on the SIG meeting that we have
2) I have a hunch what this might be. I'll open an investigation into this. Thanks for the information
3) Investigating as well
author_association: CONTRIBUTOR
user: login: dashpole
created_at: 2025-06-18T15:12:30Z
body: > It looks like the receiver currently supports only protoType: 2, based on [this line in the code](https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/main/receiver/prometheusremotewritereceiver/receiver.go#L162).

I think you mean it only supports PRW 2.0, and not PRW 1.0.  Both use the same protobuf versions.  We use PRW 2.0 because it adds transactional histograms and other complex types.  It is intentional that the receiver only supports PRW 2.0.

> After switching to proto v2, I noticed the Collector pod's memory usage steadily increased, eventually exceeding 1.6 GiB and getting evicted by Kubernetes. I'm using the memory_limiter processor with:

Can you share your PRW receiver configuration before and after the memory issue?  I'm confused how you were able to "switch to proto v2", given the receiver only supports PRW 2.0 and doesn't support PRW 1.0.
author_association: CONTRIBUTOR
user: login: elliesaber
created_at: 2025-06-18T16:30:27Z
body: > > It looks like the receiver currently supports only protoType: 2, based on [this line in the code](https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/main/receiver/prometheusremotewritereceiver/receiver.go#L162).
> 
> I think you mean it only supports PRW 2.0, and not PRW 1.0. Both use the same protobuf versions. We use PRW 2.0 because it adds transactional histograms and other complex types. It is intentional that the receiver only supports PRW 2.0.
> 
> > After switching to proto v2, I noticed the Collector pod's memory usage steadily increased, eventually exceeding 1.6 GiB and getting evicted by Kubernetes. I'm using the memory_limiter processor with:
> 
> Can you share your PRW receiver configuration before and after the memory issue? I'm confused how you were able to "switch to proto v2", given the receiver only supports PRW 2.0 and doesn't support PRW 1.0.

Thanks for the follow-up!

Just to clarify my earlier point, I was referring to Prometheus's default behavior, not the collector. As of now by default, Prometheus sends remote write data using protoType: 0 (the legacy protobuf format). To get it to emit protoType: 2, which the prometheusremotewritereceiver currently supports, I had to explicitly enable Native Histograms and configure remote write accordingly.

In my setup (using the Prometheus Operator), I enabled it like this:
```yaml
prometheus:
  prometheusSpec:
    enableFeatures:
      - native-histograms
    remoteWrite:
      - url: <otel-collector-endpoint>:8086/api/v1/write
        messageVersion: V2.0
        sendExemplars: true
        name: oxfarm-main-to-otel

```
After enabling this, I started seeing the collector accepting data but under load, it eventually ran into memory pressure and produced the panic I shared earlier in this issue. So while the switch to protoType: 2 enabled communication, it also surfaced some stability issues under higher memory use.

Just wanted to clarify the protoType angle and share context in case it's helpful to others. Maybe support for protoType: 0 could be considered in the future, or a quick note in the docs about the need for native histograms and V2.0 message format could help folks avoid confusion early on.

author_association: NONE
user: login: dashpole
created_at: 2025-06-18T17:14:14Z
body: > it also surfaced some stability issues under higher memory use.

I see, but we have no reason to think this is because of the v2 protocol, right?  It just means that the new receiver has high memory usage.

> a quick note in the docs about the need for native histograms and V2.0 message format could help folks avoid confusion early on.

That sounds like a good addition to the docs.
author_association: CONTRIBUTOR
user: login: elliesaber
created_at: 2025-06-18T17:45:17Z
body: > > it also surfaced some stability issues under higher memory use.
> 
> I see, but we have no reason to think this is because of the v2 protocol, right? It just means that the new receiver has high memory usage.
> 

Just to highlight one other piece I don’t want to get lost: beyond the high memory usage, I also hit a collector panic due to a nil pointer dereference in translateV2() when it tried to call .Sum().DataPoints().


The crash happens in translateV2() at this line:
```yaml
pmetric.Sum.DataPoints()

```
...which seems to be called without checking if .Sum() is actually present.

This is reproducible with Prometheus sending protoType: 2 and Native Histograms enabled. Here's the full trace again for reference:
```yaml
runtime panic: invalid memory address or nil pointer dereference
go.opentelemetry.io/collector/pdata/pmetric.Sum.DataPoints
...
receiver/prometheusremotewritereceiver/receiver.go:375

```
I believe the receiver should defensively check metric types before trying to access Sum.DataPoints(), to prevent crashes from malformed input.

Let me know if you'd like help isolating the payload or testing a patch!
author_association: NONE

Diff:
diff --git a/.chloggen/target-info.yaml b/.chloggen/target-info.yaml
new file mode 100644
index 0000000000000..3b7a67caf7a65
--- /dev/null
+++ b/.chloggen/target-info.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: prometheusremotewritereceiver
+
+# A brief description of the change.  Surround your text with quotes ("") if it needs to start with a backtick (`).
+note: Cache `target_info` metrics so it can be used to populate metrics' Resource Attributes.
+
+# Mandatory: One or more tracking issues related to the change. You can use the PR number here if no issue exists.
+issues: [37277]
+
+# (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: You can read more about resource attributes handling in https://opentelemetry.io/docs/specs/otel/compatibility/prometheus_and_openmetrics/#resource-attributes-1
+
+# 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]
diff --git a/receiver/prometheusremotewritereceiver/receiver.go b/receiver/prometheusremotewritereceiver/receiver.go
index 333caba33e7ed..6495e45d6340e 100644
--- a/receiver/prometheusremotewritereceiver/receiver.go
+++ b/receiver/prometheusremotewritereceiver/receiver.go
@@ -37,6 +37,7 @@ func newRemoteWriteReceiver(settings receiver.Settings, cfg *Config, nextConsume
 		server: &http.Server{
 			ReadTimeout: 60 * time.Second,
 		},
+		rmCache: make(map[uint64]pmetric.ResourceMetrics),
 	}, nil
 }
 
@@ -44,9 +45,10 @@ type prometheusRemoteWriteReceiver struct {
 	settings     receiver.Settings
 	nextConsumer consumer.Metrics
 
-	config *Config
-	server *http.Server
-	wg     sync.WaitGroup
+	config  *Config
+	server  *http.Server
+	wg      sync.WaitGroup
+	rmCache map[uint64]pmetric.ResourceMetrics
 }
 
 // MetricIdentity contains all the components that uniquely identify a metric
@@ -163,7 +165,7 @@ func (prw *prometheusRemoteWriteReceiver) handlePRW(w http.ResponseWriter, req *
 		return
 	}
 
-	_, stats, err := prw.translateV2(req.Context(), &prw2Req)
+	m, stats, err := prw.translateV2(req.Context(), &prw2Req)
 	stats.SetHeaders(w)
 	if err != nil {
 		http.Error(w, err.Error(), http.StatusBadRequest) // Following instructions at https://prometheus.io/docs/specs/remote_write_spec_2_0/#invalid-samples
@@ -171,6 +173,8 @@ func (prw *prometheusRemoteWriteReceiver) handlePRW(w http.ResponseWriter, req *
 	}
 
 	w.WriteHeader(http.StatusNoContent)
+	// TODO(@perebaj): Evaluate if we should use the obsreport here. Ref: https://github.com/open-telemetry/opentelemetry-collector-contrib/pull/38812#discussion_r2053094391
+	_ = prw.nextConsumer.ConsumeMetrics(req.Context(), m)
 }
 
 // parseProto parses the content-type header and returns the version of the remote-write protocol.
@@ -213,11 +217,6 @@ func (prw *prometheusRemoteWriteReceiver) translateV2(_ context.Context, req *wr
 		otelMetrics      = pmetric.NewMetrics()
 		labelsBuilder    = labels.NewScratchBuilder(0)
 		stats            = promremote.WriteResponseStats{}
-		// Prometheus Remote-Write can send multiple time series with the same labels in the same request.
-		// Instead of creating a whole new OTLP metric, we just append the new sample to the existing OTLP metric.
-		// This cache is called "intra" because in the future we'll have a "interRequestCache" to cache resourceAttributes
-		// between requests based on the metric "target_info".
-		intraRequestCache = make(map[uint64]pmetric.ResourceMetrics)
 		// The key is composed by: resource_hash:scope_name:scope_version:metric_name:unit:type
 		metricCache = make(map[uint64]pmetric.Metric)
 	)
@@ -232,16 +231,41 @@ func (prw *prometheusRemoteWriteReceiver) translateV2(_ context.Context, req *wr
 			continue
 		}
 
+		// If the metric name is equal to target_info, we use its labels as attributes of the resource
+		// Ref: https://opentelemetry.io/docs/specs/otel/compatibility/prometheus_and_openmetrics/#resource-attributes-1
+		if ls.Get(labels.MetricName) == "target_info" {
+			var rm pmetric.ResourceMetrics
+			hashedLabels := xxhash.Sum64String(ls.Get("job") + string([]byte{'\xff'}) + ls.Get("instance"))
+
+			if existingRM, ok := prw.rmCache[hashedLabels]; ok {
+				rm = existingRM
+			} else {
+				rm = otelMetrics.ResourceMetrics().AppendEmpty()
+			}
+
+			attrs := rm.Resource().Attributes()
+			parseJobAndInstance(attrs, ls.Get("job"), ls.Get("instance"))
+
+			// Add the remaining labels as resource attributes
+			for _, l := range ls {
+				if l.Name != "job" && l.Name != "instance" && l.Name != labels.MetricName {
+					attrs.PutStr(l.Name, l.Value)
+				}
+			}
+			prw.rmCache[hashedLabels] = rm
+			continue
+		}
+
+		// For metrics other than target_info, we need to follow the standard process of creating a metric.
 		var rm pmetric.ResourceMetrics
 		hashedLabels := xxhash.Sum64String(ls.Get("job") + string([]byte{'\xff'}) + ls.Get("instance"))
-		intraCacheEntry, ok := intraRequestCache[hashedLabels]
+		existingRM, ok := prw.rmCache[hashedLabels]
 		if ok {
-			// We found the same time series in the same request, so we should append to the same OTLP metric.
-			rm = intraCacheEntry
+			rm = existingRM
 		} else {
 			rm = otelMetrics.ResourceMetrics().AppendEmpty()
 			parseJobAndInstance(rm.Resource().Attributes(), ls.Get("job"), ls.Get("instance"))
-			intraRequestCache[hashedLabels] = rm
+			prw.rmCache[hashedLabels] = rm
 		}
 
 		scopeName, scopeVersion := prw.extractScopeInfo(ls)
diff --git a/receiver/prometheusremotewritereceiver/receiver_test.go b/receiver/prometheusremotewritereceiver/receiver_test.go
index 1712054f7b9ec..405913b9d18d9 100644
--- a/receiver/prometheusremotewritereceiver/receiver_test.go
+++ b/receiver/prometheusremotewritereceiver/receiver_test.go
@@ -7,8 +7,10 @@ import (
 	"bytes"
 	"context"
 	"fmt"
+	"io"
 	"net/http"
 	"net/http/httptest"
+	"sync"
 	"testing"
 	"time"
 
@@ -18,6 +20,7 @@ import (
 	writev2 "github.com/prometheus/prometheus/prompb/io/prometheus/write/v2"
 	"github.com/prometheus/prometheus/storage/remote"
 	"github.com/stretchr/testify/assert"
+	"go.opentelemetry.io/collector/consumer"
 	"go.opentelemetry.io/collector/consumer/consumertest"
 	"go.opentelemetry.io/collector/pdata/pcommon"
 	"go.opentelemetry.io/collector/pdata/pmetric"
@@ -406,8 +409,72 @@ func TestTranslateV2(t *testing.T) {
 				return expected
 			}(),
 		},
+		{
+			name: "service with target_info metric",
+			request: &writev2.Request{
+				Symbols: []string{
+					"",
+					"job", "production/service_a", // 1, 2
+					"instance", "host1", // 3, 4
+					"machine_type", "n1-standard-1", // 5, 6
+					"cloud_provider", "gcp", // 7, 8
+					"region", "us-central1", // 9, 10
+					"datacenter", "sdc", // 11, 12
+					"__name__", "normal_metric", // 13, 14
+					"d", "e", // 15, 16
+					"__name__", "target_info", // 17, 18
+				},
+				Timeseries: []writev2.TimeSeries{
+					// Generating 2 metrics, one have the target_info in the name and the other is a normal gauge.
+					// The target_info metric should be translated to use the resource attributes.
+					// The normal_metric should be translated as usual.
+					{
+						// target_info metric
+						Metadata:   writev2.Metadata{Type: writev2.Metadata_METRIC_TYPE_GAUGE},
+						LabelsRefs: []uint32{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 17, 18},
+					},
+					{
+						// normal metric
+						Metadata:   writev2.Metadata{Type: writev2.Metadata_METRIC_TYPE_GAUGE},
+						LabelsRefs: []uint32{13, 14, 1, 2, 3, 4, 15, 16},
+						Samples:    []writev2.Sample{{Value: 1, Timestamp: 1}},
+					},
+				},
+			},
+			expectedMetrics: func() pmetric.Metrics {
+				metrics := pmetric.NewMetrics()
+
+				rm := metrics.ResourceMetrics().AppendEmpty()
+				attrs := rm.Resource().Attributes()
+				attrs.PutStr("service.namespace", "production")
+				attrs.PutStr("service.name", "service_a")
+				attrs.PutStr("service.instance.id", "host1")
+				attrs.PutStr("machine_type", "n1-standard-1")
+				attrs.PutStr("cloud_provider", "gcp")
+				attrs.PutStr("region", "us-central1")
+				attrs.PutStr("datacenter", "sdc")
+
+				sm := rm.ScopeMetrics().AppendEmpty()
+				sm.Scope().SetName("OpenTelemetry Collector")
+				sm.Scope().SetVersion("latest")
+
+				m := sm.Metrics().AppendEmpty()
+				m.SetName("normal_metric")
+				m.SetUnit("")
+				m.SetDescription("")
+
+				dp := m.SetEmptyGauge().DataPoints().AppendEmpty()
+				dp.SetDoubleValue(1.0)
+				dp.SetTimestamp(pcommon.Timestamp(1 * int64(time.Millisecond)))
+				dp.Attributes().PutStr("d", "e")
+
+				return metrics
+			}(),
+		},
 	} {
 		t.Run(tc.name, func(t *testing.T) {
+			// since we are using the rmCache to store values across requests, we need to clear it after each test, otherwise it will affect the next test
+			prwReceiver.rmCache = make(map[uint64]pmetric.ResourceMetrics)
 			metrics, stats, err := prwReceiver.translateV2(ctx, tc.request)
 			if tc.expectError != "" {
 				assert.ErrorContains(t, err, tc.expectError)
@@ -420,3 +487,170 @@ func TestTranslateV2(t *testing.T) {
 		})
 	}
 }
+
+type nonMutatingConsumer struct{}
+
+// Capabilities returns the base consumer capabilities.
+func (bc nonMutatingConsumer) Capabilities() consumer.Capabilities {
+	return consumer.Capabilities{MutatesData: false}
+}
+
+type MockConsumer struct {
+	nonMutatingConsumer
+	mu         sync.Mutex
+	metrics    []pmetric.Metrics
+	dataPoints int
+}
+
+func (m *MockConsumer) ConsumeMetrics(_ context.Context, md pmetric.Metrics) error {
+	m.mu.Lock()
+	defer m.mu.Unlock()
+	m.metrics = append(m.metrics, md)
+	m.dataPoints += md.DataPointCount()
+	return nil
+}
+
+func TestTargetInfoWithMultipleRequests(t *testing.T) {
+	tests := []struct {
+		name     string
+		requests []*writev2.Request
+	}{
+		{
+			name: "target_info first, normal metric second",
+			requests: []*writev2.Request{
+				{
+					Symbols: []string{
+						"",
+						"job", "production/service_a", // 1, 2
+						"instance", "host1", // 3, 4
+						"machine_type", "n1-standard-1", // 5, 6
+						"cloud_provider", "gcp", // 7, 8
+						"region", "us-central1", // 9, 10
+						"__name__", "target_info", // 11, 12
+					},
+					Timeseries: []writev2.TimeSeries{
+						{
+							Metadata:   writev2.Metadata{Type: writev2.Metadata_METRIC_TYPE_GAUGE},
+							LabelsRefs: []uint32{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12},
+						},
+					},
+				},
+				{
+					Symbols: []string{
+						"",
+						"job", "production/service_a", // 1, 2
+						"instance", "host1", // 3, 4
+						"__name__", "normal_metric", // 5, 6
+						"foo", "bar", // 7, 8
+					},
+					Timeseries: []writev2.TimeSeries{
+						{
+							Metadata:   writev2.Metadata{Type: writev2.Metadata_METRIC_TYPE_GAUGE},
+							LabelsRefs: []uint32{5, 6, 1, 2, 3, 4, 7, 8},
+							Samples:    []writev2.Sample{{Value: 2, Timestamp: 2}},
+						},
+					},
+				},
+			},
+		},
+		{
+			name: "normal metric first, target_info second",
+			requests: []*writev2.Request{
+				{
+					Symbols: []string{
+						"",
+						"job", "production/service_a", // 1, 2
+						"instance", "host1", // 3, 4
+						"__name__", "normal_metric", // 5, 6
+						"foo", "bar", // 7, 8
+					},
+					Timeseries: []writev2.TimeSeries{
+						{
+							Metadata:   writev2.Metadata{Type: writev2.Metadata_METRIC_TYPE_GAUGE},
+							LabelsRefs: []uint32{5, 6, 1, 2, 3, 4, 7, 8},
+							Samples:    []writev2.Sample{{Value: 2, Timestamp: 2}},
+						},
+					},
+				},
+				{
+					Symbols: []string{
+						"",
+						"job", "production/service_a", // 1, 2
+						"instance", "host1", // 3, 4
+						"machine_type", "n1-standard-1", // 5, 6
+						"cloud_provider", "gcp", // 7, 8
+						"region", "us-central1", // 9, 10
+						"__name__", "target_info", // 11, 12
+					},
+					Timeseries: []writev2.TimeSeries{
+						{
+							Metadata:   writev2.Metadata{Type: writev2.Metadata_METRIC_TYPE_GAUGE},
+							LabelsRefs: []uint32{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12},
+						},
+					},
+				},
+			},
+		},
+	}
+
+	// Using the same expected metrics for both tests, because we are just checking if the order of the requests changes the result.
+	expectedMetrics := func() pmetric.Metrics {
+		metrics := pmetric.NewMetrics()
+		rm := metrics.ResourceMetrics().AppendEmpty()
+		attrs := rm.Resource().Attributes()
+		attrs.PutStr("service.namespace", "production")
+		attrs.PutStr("service.name", "service_a")
+		attrs.PutStr("service.instance.id", "host1")
+		attrs.PutStr("machine_type", "n1-standard-1")
+		attrs.PutStr("cloud_provider", "gcp")
+		attrs.PutStr("region", "us-central1")
+
+		sm := rm.ScopeMetrics().AppendEmpty()
+		sm.Scope().SetName("OpenTelemetry Collector")
+		sm.Scope().SetVersion("latest")
+		m1 := sm.Metrics().AppendEmpty()
+		m1.SetName("normal_metric")
+		m1.SetUnit("")
+		m1.SetDescription("")
+		dp1 := m1.SetEmptyGauge().DataPoints().AppendEmpty()
+		dp1.SetDoubleValue(2.0)
+		dp1.SetTimestamp(pcommon.Timestamp(2 * int64(time.Millisecond)))
+		dp1.Attributes().PutStr("foo", "bar")
+
+		return metrics
+	}()
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			mockConsumer := new(MockConsumer)
+			prwReceiver := setupMetricsReceiver(t)
+			prwReceiver.nextConsumer = mockConsumer
+
+			ts := httptest.NewServer(http.HandlerFunc(prwReceiver.handlePRW))
+			defer ts.Close()
+
+			for _, req := range tt.requests {
+				pBuf := proto.NewBuffer(nil)
+				// we don't need to compress the body to use the snappy compression in the unit test
+				// because the encoder is just initialized when we initialize the http server.
+				// so we can just use the uncompressed body.
+				err := pBuf.Marshal(req)
+				assert.NoError(t, err)
+
+				resp, err := http.Post(
+					ts.URL,
+					fmt.Sprintf("application/x-protobuf;proto=%s", promconfig.RemoteWriteProtoMsgV2),
+					bytes.NewBuffer(pBuf.Bytes()),
+				)
+				assert.NoError(t, err)
+				defer resp.Body.Close()
+
+				body, err := io.ReadAll(resp.Body)
+				assert.NoError(t, err)
+				assert.Equal(t, http.StatusNoContent, resp.StatusCode, string(body))
+			}
+
+			assert.NoError(t, pmetrictest.CompareMetrics(expectedMetrics, mockConsumer.metrics[0]))
+		})
+	}
+}


Reviews:
user: login: perebaj
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-21T21:44:47Z
user: login: perebaj
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-21T21:55:09Z
user: login: ArthurSens
body: Good start, but there are a few things not going in the right direction. Added a few comments below
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-22T20:49:07Z
user: login: perebaj
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-24T11:54:34Z
user: login: perebaj
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-21T18:06:07Z
user: login: atoulme
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-21T23:13:05Z
user: login: atoulme
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-21T23:14:31Z
user: login: perebaj
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-21T23:37:59Z
user: login: perebaj
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-21T23:55:24Z
user: login: ArthurSens
body: Awesome progress, I think we're almost done :) 

A few comments 
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-22T12:44:22Z
user: login: perebaj
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-23T00:11:14Z
user: login: perebaj
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-23T00:11:49Z
user: login: perebaj
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-23T00:12:10Z
user: login: perebaj
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-23T00:12:43Z
user: login: perebaj
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-23T00:13:10Z
user: login: perebaj
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-23T00:13:37Z
user: login: perebaj
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-23T00:14:39Z
user: login: perebaj
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-23T00:58:59Z
user: login: perebaj
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-23T01:04:26Z
user: login: ArthurSens
body: Just one very small comment, otherwise LGTM!
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-23T12:18:27Z
user: login: ArthurSens
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-23T12:22:28Z
user: login: ArthurSens
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-23T12:23:09Z
user: login: ArthurSens
body: Thank you! The hardest part of the mentorship is done 🎉 
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-23T12:29:16Z

Review comments:
diff_hunk: @@ -406,17 +408,194 @@ func TestTranslateV2(t *testing.T) {
 				return expected
 			}(),
 		},
+		{
+			name: "service with target_info metric",
+			request: &writev2.Request{
+				Symbols: []string{
+					"",
+					"job", "production/service_a", // 1, 2
+					"instance", "host1", // 3, 4
+					"machine_type", "n1-standard-1", // 5, 6
+					"cloud_provider", "gcp", // 7, 8
+					"region", "us-central1", // 9, 10
+					"datacenter", "sdc", // 11, 12
+					"__name__", "normal_metric", // 13, 14
+					"d", "e", // 15, 16
+				},
+				Timeseries: []writev2.TimeSeries{
+					// Generating 2 metrics, one is a type info and the other is a normal gauge.
+					// The type info metric should be translated to just contains the resource attributes.
+					// The normal_metric should be translated as usual.
+					{
+						Metadata:   writev2.Metadata{Type: writev2.Metadata_METRIC_TYPE_INFO},
+						LabelsRefs: []uint32{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12},
+					},
+					{
+						Metadata:   writev2.Metadata{Type: writev2.Metadata_METRIC_TYPE_GAUGE},
+						LabelsRefs: []uint32{13, 14, 1, 2, 3, 4, 15, 16},
+						Samples:    []writev2.Sample{{Value: 1, Timestamp: 1}},
+					},
+				},
+			},
+			expectedMetrics: func() pmetric.Metrics {
+				metrics := pmetric.NewMetrics()
+
+				rm := metrics.ResourceMetrics().AppendEmpty()
+				attrs := rm.Resource().Attributes()
+				attrs.PutStr("service.namespace", "production")
+				attrs.PutStr("service.name", "service_a")
+				attrs.PutStr("service.instance.id", "host1")
+				attrs.PutStr("machine.type", "n1-standard-1")
+				attrs.PutStr("cloud.provider", "gcp")
+				attrs.PutStr("region", "us-central1")
+				attrs.PutStr("datacenter", "sdc")
+
+				sm := rm.ScopeMetrics().AppendEmpty()
+				sm.Scope().SetName("OpenTelemetry Collector")
+				sm.Scope().SetVersion("latest")
+
+				m := sm.Metrics().AppendEmpty()
+				m.SetName("normal_metric")
+				m.SetUnit("")
+				m.SetDescription("")
+
+				dp := m.SetEmptyGauge().DataPoints().AppendEmpty()
+				dp.SetDoubleValue(1.0)
+				dp.SetTimestamp(pcommon.Timestamp(1 * int64(time.Millisecond)))
+				dp.Attributes().PutStr("d", "e")
+
+				return metrics
+			}(),
+		},
 	} {
 		t.Run(tc.name, func(t *testing.T) {
-			metrics, stats, err := prwReceiver.translateV2(ctx, tc.request)
+			// since we are using the interRequestCache to store values across requests, we need to clear it after each test, otherwise it will affect the next test
+			prwReceiver.interRequestCache = make(map[uint64]pmetric.ResourceMetrics)
+			metrics, _, err := prwReceiver.translateV2(ctx, tc.request)
path: receiver/prometheusremotewritereceiver/receiver_test.go
user: login: perebaj
body: @ArthurSens here we have 2 options to make it run smoothly, or we clean the cache for each request. Or we can move the `prwReceiver := setupMetricsReceiver(t)` to be initialized after t.Run... What do you think that is better?
created_at: 2025-03-21T21:39:14Z
author_association: CONTRIBUTOR
diff_hunk: @@ -406,17 +408,194 @@ func TestTranslateV2(t *testing.T) {
 				return expected
 			}(),
 		},
+		{
+			name: "service with target_info metric",
+			request: &writev2.Request{
+				Symbols: []string{
+					"",
+					"job", "production/service_a", // 1, 2
+					"instance", "host1", // 3, 4
+					"machine_type", "n1-standard-1", // 5, 6
+					"cloud_provider", "gcp", // 7, 8
+					"region", "us-central1", // 9, 10
+					"datacenter", "sdc", // 11, 12
+					"__name__", "normal_metric", // 13, 14
+					"d", "e", // 15, 16
+				},
+				Timeseries: []writev2.TimeSeries{
+					// Generating 2 metrics, one is a type info and the other is a normal gauge.
+					// The type info metric should be translated to just contains the resource attributes.
+					// The normal_metric should be translated as usual.
+					{
+						Metadata:   writev2.Metadata{Type: writev2.Metadata_METRIC_TYPE_INFO},
+						LabelsRefs: []uint32{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12},
+					},
+					{
+						Metadata:   writev2.Metadata{Type: writev2.Metadata_METRIC_TYPE_GAUGE},
+						LabelsRefs: []uint32{13, 14, 1, 2, 3, 4, 15, 16},
+						Samples:    []writev2.Sample{{Value: 1, Timestamp: 1}},
+					},
+				},
+			},
+			expectedMetrics: func() pmetric.Metrics {
+				metrics := pmetric.NewMetrics()
+
+				rm := metrics.ResourceMetrics().AppendEmpty()
+				attrs := rm.Resource().Attributes()
+				attrs.PutStr("service.namespace", "production")
+				attrs.PutStr("service.name", "service_a")
+				attrs.PutStr("service.instance.id", "host1")
+				attrs.PutStr("machine.type", "n1-standard-1")
+				attrs.PutStr("cloud.provider", "gcp")
+				attrs.PutStr("region", "us-central1")
+				attrs.PutStr("datacenter", "sdc")
+
+				sm := rm.ScopeMetrics().AppendEmpty()
+				sm.Scope().SetName("OpenTelemetry Collector")
+				sm.Scope().SetVersion("latest")
+
+				m := sm.Metrics().AppendEmpty()
+				m.SetName("normal_metric")
+				m.SetUnit("")
+				m.SetDescription("")
+
+				dp := m.SetEmptyGauge().DataPoints().AppendEmpty()
+				dp.SetDoubleValue(1.0)
+				dp.SetTimestamp(pcommon.Timestamp(1 * int64(time.Millisecond)))
+				dp.Attributes().PutStr("d", "e")
+
+				return metrics
+			}(),
+		},
 	} {
 		t.Run(tc.name, func(t *testing.T) {
-			metrics, stats, err := prwReceiver.translateV2(ctx, tc.request)
+			// since we are using the interRequestCache to store values across requests, we need to clear it after each test, otherwise it will affect the next test
+			prwReceiver.interRequestCache = make(map[uint64]pmetric.ResourceMetrics)
+			metrics, _, err := prwReceiver.translateV2(ctx, tc.request)
 			if tc.expectError != "" {
 				assert.ErrorContains(t, err, tc.expectError)
 				return
 			}
 
 			assert.NoError(t, err)
 			assert.NoError(t, pmetrictest.CompareMetrics(tc.expectedMetrics, metrics))
-			assert.Equal(t, tc.expectedStats, stats)
 		})
 	}
 }
+
+type nonMutatingConsumer struct{}
+
+// Capabilities returns the base consumer capabilities.
+func (bc nonMutatingConsumer) Capabilities() consumer.Capabilities {
+	return consumer.Capabilities{MutatesData: false}
+}
+
+type MockConsumer struct {
+	nonMutatingConsumer
+	mu         sync.Mutex
+	metrics    []pmetric.Metrics
+	dataPoints int
+}
+
+func (m *MockConsumer) ConsumeMetrics(_ context.Context, md pmetric.Metrics) error {
+	m.mu.Lock()
+	defer m.mu.Unlock()
+	m.metrics = append(m.metrics, md)
+	m.dataPoints += md.DataPointCount()
+	return nil
+}
path: receiver/prometheusremotewritereceiver/receiver_test.go
user: login: perebaj
body: Should those things be initialized here?
created_at: 2025-03-21T21:39:46Z
author_association: CONTRIBUTOR
diff_hunk: @@ -406,17 +408,194 @@ func TestTranslateV2(t *testing.T) {
 				return expected
 			}(),
 		},
+		{
+			name: "service with target_info metric",
+			request: &writev2.Request{
+				Symbols: []string{
+					"",
+					"job", "production/service_a", // 1, 2
+					"instance", "host1", // 3, 4
+					"machine_type", "n1-standard-1", // 5, 6
+					"cloud_provider", "gcp", // 7, 8
+					"region", "us-central1", // 9, 10
+					"datacenter", "sdc", // 11, 12
+					"__name__", "normal_metric", // 13, 14
+					"d", "e", // 15, 16
+				},
+				Timeseries: []writev2.TimeSeries{
+					// Generating 2 metrics, one is a type info and the other is a normal gauge.
+					// The type info metric should be translated to just contains the resource attributes.
+					// The normal_metric should be translated as usual.
+					{
+						Metadata:   writev2.Metadata{Type: writev2.Metadata_METRIC_TYPE_INFO},
+						LabelsRefs: []uint32{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12},
+					},
+					{
+						Metadata:   writev2.Metadata{Type: writev2.Metadata_METRIC_TYPE_GAUGE},
+						LabelsRefs: []uint32{13, 14, 1, 2, 3, 4, 15, 16},
+						Samples:    []writev2.Sample{{Value: 1, Timestamp: 1}},
+					},
+				},
+			},
+			expectedMetrics: func() pmetric.Metrics {
+				metrics := pmetric.NewMetrics()
+
+				rm := metrics.ResourceMetrics().AppendEmpty()
+				attrs := rm.Resource().Attributes()
+				attrs.PutStr("service.namespace", "production")
+				attrs.PutStr("service.name", "service_a")
+				attrs.PutStr("service.instance.id", "host1")
+				attrs.PutStr("machine.type", "n1-standard-1")
+				attrs.PutStr("cloud.provider", "gcp")
+				attrs.PutStr("region", "us-central1")
+				attrs.PutStr("datacenter", "sdc")
+
+				sm := rm.ScopeMetrics().AppendEmpty()
+				sm.Scope().SetName("OpenTelemetry Collector")
+				sm.Scope().SetVersion("latest")
+
+				m := sm.Metrics().AppendEmpty()
+				m.SetName("normal_metric")
+				m.SetUnit("")
+				m.SetDescription("")
+
+				dp := m.SetEmptyGauge().DataPoints().AppendEmpty()
+				dp.SetDoubleValue(1.0)
+				dp.SetTimestamp(pcommon.Timestamp(1 * int64(time.Millisecond)))
+				dp.Attributes().PutStr("d", "e")
+
+				return metrics
+			}(),
+		},
 	} {
 		t.Run(tc.name, func(t *testing.T) {
-			metrics, stats, err := prwReceiver.translateV2(ctx, tc.request)
+			// since we are using the interRequestCache to store values across requests, we need to clear it after each test, otherwise it will affect the next test
+			prwReceiver.interRequestCache = make(map[uint64]pmetric.ResourceMetrics)
+			metrics, _, err := prwReceiver.translateV2(ctx, tc.request)
 			if tc.expectError != "" {
 				assert.ErrorContains(t, err, tc.expectError)
 				return
 			}
 
 			assert.NoError(t, err)
 			assert.NoError(t, pmetrictest.CompareMetrics(tc.expectedMetrics, metrics))
-			assert.Equal(t, tc.expectedStats, stats)
 		})
 	}
 }
+
+type nonMutatingConsumer struct{}
+
+// Capabilities returns the base consumer capabilities.
+func (bc nonMutatingConsumer) Capabilities() consumer.Capabilities {
+	return consumer.Capabilities{MutatesData: false}
+}
+
+type MockConsumer struct {
+	nonMutatingConsumer
+	mu         sync.Mutex
+	metrics    []pmetric.Metrics
+	dataPoints int
+}
+
+func (m *MockConsumer) ConsumeMetrics(_ context.Context, md pmetric.Metrics) error {
+	m.mu.Lock()
+	defer m.mu.Unlock()
+	m.metrics = append(m.metrics, md)
+	m.dataPoints += md.DataPointCount()
+	return nil
+}
+
+func TestTargetInfoWithMultipleRequests(t *testing.T) {
+	prwReceiver := setupMetricsReceiver(t)
+	mockConsumer := new(MockConsumer)
+	prwReceiver.nextConsumer = mockConsumer
+	w := httptest.NewRecorder()
+
+	firstRequest := &writev2.Request{
+		Symbols: []string{
+			"",
+			"job", "production/service_a", // 1, 2
+			"instance", "host1", // 3, 4
+			"machine_type", "n1-standard-1", // 5, 6
+			"cloud_provider", "gcp", // 7, 8
+			"region", "us-central1", // 9, 10
+		},
+		Timeseries: []writev2.TimeSeries{
+			{
+				Metadata:   writev2.Metadata{Type: writev2.Metadata_METRIC_TYPE_INFO},
+				LabelsRefs: []uint32{1, 2, 3, 4, 5, 6, 7, 8, 9, 10},
+			},
+		},
+	}
+
+	buf := proto.NewBuffer(nil)
+	err := buf.Marshal(firstRequest)
+	assert.NoError(t, err)
+
+	req1 := httptest.NewRequest(http.MethodPost, "/api/v1/write", bytes.NewReader(buf.Bytes()))
+	req1.Header.Set("Content-Type", fmt.Sprintf("application/x-protobuf;proto=%s", promconfig.RemoteWriteProtoMsgV2))
+	req1.Header.Set("Content-Encoding", "snappy")
path: receiver/prometheusremotewritereceiver/receiver_test.go
user: login: perebaj
body: Here is the snappy bug that I mentioned on slack. In the tests that you created.

https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/288c6bdedf283230b23d266a88059695cbc89a73/receiver/prometheusremotewritereceiver/receiver_test.go#L107-L114

I notice that you are compressing and sending the data through the request. But this just works in your test cases, because the body is empty, in the cases where we are sending `real data`, the code crashes, and the only way that I found to solve it, was sending the request without the snappy compression. 

Should we take a look into the `handlePRW` to deal with data that comes with that type of compression to avoid this behavior?
created_at: 2025-03-21T21:44:36Z
author_association: CONTRIBUTOR
diff_hunk: @@ -177,32 +180,61 @@ func (prw *prometheusRemoteWriteReceiver) translateV2(_ context.Context, req *wr
 		// Instead of creating a whole new OTLP metric, we just append the new sample to the existing OTLP metric.
 		// This cache is called "intra" because in the future we'll have a "interRequestCache" to cache resourceAttributes
 		// between requests based on the metric "target_info".
-		intraRequestCache = make(map[uint64]pmetric.ResourceMetrics)
+		// intraRequestCache = make(map[uint64]pmetric.ResourceMetrics)
path: receiver/prometheusremotewritereceiver/receiver.go
user: login: perebaj
body: ```suggestion
```
created_at: 2025-03-21T21:53:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -123,14 +125,15 @@ func (prw *prometheusRemoteWriteReceiver) handlePRW(w http.ResponseWriter, req *
 		return
 	}
 
-	_, stats, err := prw.translateV2(req.Context(), &prw2Req)
+	m, stats, err := prw.translateV2(req.Context(), &prw2Req)
 	stats.SetHeaders(w)
 	if err != nil {
 		http.Error(w, err.Error(), http.StatusBadRequest) // Following instructions at https://prometheus.io/docs/specs/remote_write_spec_2_0/#invalid-samples
 		return
 	}
 
 	w.WriteHeader(http.StatusNoContent)
+	_ = prw.nextConsumer.ConsumeMetrics(req.Context(), m)
path: receiver/prometheusremotewritereceiver/receiver.go
user: login: perebaj
body: Should I deal with that error?
created_at: 2025-03-21T21:54:12Z
author_association: CONTRIBUTOR
diff_hunk: @@ -177,32 +180,61 @@ func (prw *prometheusRemoteWriteReceiver) translateV2(_ context.Context, req *wr
 		// Instead of creating a whole new OTLP metric, we just append the new sample to the existing OTLP metric.
 		// This cache is called "intra" because in the future we'll have a "interRequestCache" to cache resourceAttributes
 		// between requests based on the metric "target_info".
-		intraRequestCache = make(map[uint64]pmetric.ResourceMetrics)
+		// intraRequestCache = make(map[uint64]pmetric.ResourceMetrics)
 		// The key is composed by: resource_hash:scope_name:scope_version:metric_name:unit:type
 		// TODO: use the appropriate hash function.
 		metricCache = make(map[string]pmetric.Metric)
 	)
 
 	for _, ts := range req.Timeseries {
 		ls := ts.ToLabels(&labelsBuilder, req.Symbols)
-		if !ls.Has(labels.MetricName) {
+		if !ls.Has(labels.MetricName) && ts.Metadata.Type != writev2.Metadata_METRIC_TYPE_INFO {
 			badRequestErrors = errors.Join(badRequestErrors, fmt.Errorf("missing metric name in labels"))
 			continue
 		} else if duplicateLabel, hasDuplicate := ls.HasDuplicateLabelNames(); hasDuplicate {
 			badRequestErrors = errors.Join(badRequestErrors, fmt.Errorf("duplicate label %q in labels", duplicateLabel))
 			continue
 		}
 
+		// If it is a metric of type INFO, we use its labels as attributes of the resource
+		// Ref: https://opentelemetry.io/docs/specs/otel/compatibility/prometheus_and_openmetrics/#resource-attributes-1
+		if ts.Metadata.Type == writev2.Metadata_METRIC_TYPE_INFO {
+			var rm pmetric.ResourceMetrics
+			hashedLabels := xxhash.Sum64String(ls.Get("job") + string([]byte{'\xff'}) + ls.Get("instance"))
+
+			// search or create the ResourceMetrics
path: receiver/prometheusremotewritereceiver/receiver.go
user: login: perebaj
body: ```suggestion
```
created_at: 2025-03-21T21:54:43Z
author_association: CONTRIBUTOR
diff_hunk: @@ -177,32 +180,61 @@ func (prw *prometheusRemoteWriteReceiver) translateV2(_ context.Context, req *wr
 		// Instead of creating a whole new OTLP metric, we just append the new sample to the existing OTLP metric.
 		// This cache is called "intra" because in the future we'll have a "interRequestCache" to cache resourceAttributes
 		// between requests based on the metric "target_info".
-		intraRequestCache = make(map[uint64]pmetric.ResourceMetrics)
+		// intraRequestCache = make(map[uint64]pmetric.ResourceMetrics)
 		// The key is composed by: resource_hash:scope_name:scope_version:metric_name:unit:type
 		// TODO: use the appropriate hash function.
 		metricCache = make(map[string]pmetric.Metric)
 	)
 
 	for _, ts := range req.Timeseries {
 		ls := ts.ToLabels(&labelsBuilder, req.Symbols)
-		if !ls.Has(labels.MetricName) {
+		if !ls.Has(labels.MetricName) && ts.Metadata.Type != writev2.Metadata_METRIC_TYPE_INFO {
 			badRequestErrors = errors.Join(badRequestErrors, fmt.Errorf("missing metric name in labels"))
 			continue
 		} else if duplicateLabel, hasDuplicate := ls.HasDuplicateLabelNames(); hasDuplicate {
 			badRequestErrors = errors.Join(badRequestErrors, fmt.Errorf("duplicate label %q in labels", duplicateLabel))
 			continue
 		}
 
+		// If it is a metric of type INFO, we use its labels as attributes of the resource
+		// Ref: https://opentelemetry.io/docs/specs/otel/compatibility/prometheus_and_openmetrics/#resource-attributes-1
+		if ts.Metadata.Type == writev2.Metadata_METRIC_TYPE_INFO {
+			var rm pmetric.ResourceMetrics
+			hashedLabels := xxhash.Sum64String(ls.Get("job") + string([]byte{'\xff'}) + ls.Get("instance"))
+
+			// search or create the ResourceMetrics
+			if existingRM, ok := prw.interRequestCache[hashedLabels]; ok {
+				rm = existingRM
+			} else {
+				rm = otelMetrics.ResourceMetrics().AppendEmpty()
+			}
+
+			// Add all labels as resource attributes
path: receiver/prometheusremotewritereceiver/receiver.go
user: login: perebaj
body: ```suggestion
```
created_at: 2025-03-21T21:54:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -177,32 +180,61 @@ func (prw *prometheusRemoteWriteReceiver) translateV2(_ context.Context, req *wr
 		// Instead of creating a whole new OTLP metric, we just append the new sample to the existing OTLP metric.
 		// This cache is called "intra" because in the future we'll have a "interRequestCache" to cache resourceAttributes
 		// between requests based on the metric "target_info".
-		intraRequestCache = make(map[uint64]pmetric.ResourceMetrics)
+		// intraRequestCache = make(map[uint64]pmetric.ResourceMetrics)
 		// The key is composed by: resource_hash:scope_name:scope_version:metric_name:unit:type
 		// TODO: use the appropriate hash function.
 		metricCache = make(map[string]pmetric.Metric)
 	)
 
 	for _, ts := range req.Timeseries {
 		ls := ts.ToLabels(&labelsBuilder, req.Symbols)
-		if !ls.Has(labels.MetricName) {
+		if !ls.Has(labels.MetricName) && ts.Metadata.Type != writev2.Metadata_METRIC_TYPE_INFO {
 			badRequestErrors = errors.Join(badRequestErrors, fmt.Errorf("missing metric name in labels"))
 			continue
 		} else if duplicateLabel, hasDuplicate := ls.HasDuplicateLabelNames(); hasDuplicate {
 			badRequestErrors = errors.Join(badRequestErrors, fmt.Errorf("duplicate label %q in labels", duplicateLabel))
 			continue
 		}
 
+		// If it is a metric of type INFO, we use its labels as attributes of the resource
+		// Ref: https://opentelemetry.io/docs/specs/otel/compatibility/prometheus_and_openmetrics/#resource-attributes-1
+		if ts.Metadata.Type == writev2.Metadata_METRIC_TYPE_INFO {
+			var rm pmetric.ResourceMetrics
+			hashedLabels := xxhash.Sum64String(ls.Get("job") + string([]byte{'\xff'}) + ls.Get("instance"))
+
+			// search or create the ResourceMetrics
+			if existingRM, ok := prw.interRequestCache[hashedLabels]; ok {
+				rm = existingRM
+			} else {
+				rm = otelMetrics.ResourceMetrics().AppendEmpty()
+			}
+
+			// Add all labels as resource attributes
+			attrs := rm.Resource().Attributes()
+			parseJobAndInstance(attrs, ls.Get("job"), ls.Get("instance"))
+
+			// Add the remaining labels as resource attributes
+			for _, l := range ls {
+				if l.Name != "job" && l.Name != "instance" && l.Name != labels.MetricName {
+					// Convert the label name to the resource attribute format
path: receiver/prometheusremotewritereceiver/receiver.go
user: login: perebaj
body: ```suggestion
```
created_at: 2025-03-21T21:55:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -177,32 +180,57 @@ func (prw *prometheusRemoteWriteReceiver) translateV2(_ context.Context, req *wr
 		// Instead of creating a whole new OTLP metric, we just append the new sample to the existing OTLP metric.
 		// This cache is called "intra" because in the future we'll have a "interRequestCache" to cache resourceAttributes
 		// between requests based on the metric "target_info".
-		intraRequestCache = make(map[uint64]pmetric.ResourceMetrics)
 		// The key is composed by: resource_hash:scope_name:scope_version:metric_name:unit:type
 		// TODO: use the appropriate hash function.
 		metricCache = make(map[string]pmetric.Metric)
 	)
 
 	for _, ts := range req.Timeseries {
 		ls := ts.ToLabels(&labelsBuilder, req.Symbols)
-		if !ls.Has(labels.MetricName) {
+		if !ls.Has(labels.MetricName) && ts.Metadata.Type != writev2.Metadata_METRIC_TYPE_INFO {
 			badRequestErrors = errors.Join(badRequestErrors, fmt.Errorf("missing metric name in labels"))
 			continue
 		} else if duplicateLabel, hasDuplicate := ls.HasDuplicateLabelNames(); hasDuplicate {
 			badRequestErrors = errors.Join(badRequestErrors, fmt.Errorf("duplicate label %q in labels", duplicateLabel))
 			continue
 		}
 
+		// If it is a metric of type INFO, we use its labels as attributes of the resource
+		// Ref: https://opentelemetry.io/docs/specs/otel/compatibility/prometheus_and_openmetrics/#resource-attributes-1
+		if ts.Metadata.Type == writev2.Metadata_METRIC_TYPE_INFO {
path: receiver/prometheusremotewritereceiver/receiver.go
user: login: ArthurSens
body: It's not any metric of the kind `INFO`, just the metric with the name `target_info`. I completely understand the confusion here.

[The TYPE Info is mentioned by the exposition format OpenMetrics](https://prometheus.io/docs/specs/om/open_metrics_spec/#info), but in reality Prometheus has never adapted its storage to handle OM's Info metrics differently and therefore never pushes an Info metric via remote write.


We'll need to switch the logic here completely to only get resource attributes from the specific metric with name `target_info`
created_at: 2025-03-22T20:44:44Z
author_association: MEMBER
diff_hunk: @@ -406,17 +408,194 @@ func TestTranslateV2(t *testing.T) {
 				return expected
 			}(),
 		},
+		{
+			name: "service with target_info metric",
+			request: &writev2.Request{
+				Symbols: []string{
+					"",
+					"job", "production/service_a", // 1, 2
+					"instance", "host1", // 3, 4
+					"machine_type", "n1-standard-1", // 5, 6
+					"cloud_provider", "gcp", // 7, 8
+					"region", "us-central1", // 9, 10
+					"datacenter", "sdc", // 11, 12
+					"__name__", "normal_metric", // 13, 14
+					"d", "e", // 15, 16
+				},
+				Timeseries: []writev2.TimeSeries{
+					// Generating 2 metrics, one is a type info and the other is a normal gauge.
+					// The type info metric should be translated to just contains the resource attributes.
+					// The normal_metric should be translated as usual.
+					{
+						Metadata:   writev2.Metadata{Type: writev2.Metadata_METRIC_TYPE_INFO},
+						LabelsRefs: []uint32{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12},
+					},
+					{
+						Metadata:   writev2.Metadata{Type: writev2.Metadata_METRIC_TYPE_GAUGE},
+						LabelsRefs: []uint32{13, 14, 1, 2, 3, 4, 15, 16},
+						Samples:    []writev2.Sample{{Value: 1, Timestamp: 1}},
+					},
+				},
+			},
+			expectedMetrics: func() pmetric.Metrics {
+				metrics := pmetric.NewMetrics()
+
+				rm := metrics.ResourceMetrics().AppendEmpty()
+				attrs := rm.Resource().Attributes()
+				attrs.PutStr("service.namespace", "production")
+				attrs.PutStr("service.name", "service_a")
+				attrs.PutStr("service.instance.id", "host1")
+				attrs.PutStr("machine.type", "n1-standard-1")
+				attrs.PutStr("cloud.provider", "gcp")
+				attrs.PutStr("region", "us-central1")
+				attrs.PutStr("datacenter", "sdc")
+
+				sm := rm.ScopeMetrics().AppendEmpty()
+				sm.Scope().SetName("OpenTelemetry Collector")
+				sm.Scope().SetVersion("latest")
+
+				m := sm.Metrics().AppendEmpty()
+				m.SetName("normal_metric")
+				m.SetUnit("")
+				m.SetDescription("")
+
+				dp := m.SetEmptyGauge().DataPoints().AppendEmpty()
+				dp.SetDoubleValue(1.0)
+				dp.SetTimestamp(pcommon.Timestamp(1 * int64(time.Millisecond)))
+				dp.Attributes().PutStr("d", "e")
+
+				return metrics
+			}(),
+		},
 	} {
 		t.Run(tc.name, func(t *testing.T) {
-			metrics, stats, err := prwReceiver.translateV2(ctx, tc.request)
+			// since we are using the interRequestCache to store values across requests, we need to clear it after each test, otherwise it will affect the next test
+			prwReceiver.interRequestCache = make(map[uint64]pmetric.ResourceMetrics)
+			metrics, _, err := prwReceiver.translateV2(ctx, tc.request)
 			if tc.expectError != "" {
 				assert.ErrorContains(t, err, tc.expectError)
 				return
 			}
 
 			assert.NoError(t, err)
 			assert.NoError(t, pmetrictest.CompareMetrics(tc.expectedMetrics, metrics))
-			assert.Equal(t, tc.expectedStats, stats)
 		})
 	}
 }
+
+type nonMutatingConsumer struct{}
+
+// Capabilities returns the base consumer capabilities.
+func (bc nonMutatingConsumer) Capabilities() consumer.Capabilities {
+	return consumer.Capabilities{MutatesData: false}
+}
+
+type MockConsumer struct {
+	nonMutatingConsumer
+	mu         sync.Mutex
+	metrics    []pmetric.Metrics
+	dataPoints int
+}
+
+func (m *MockConsumer) ConsumeMetrics(_ context.Context, md pmetric.Metrics) error {
+	m.mu.Lock()
+	defer m.mu.Unlock()
+	m.metrics = append(m.metrics, md)
+	m.dataPoints += md.DataPointCount()
+	return nil
+}
+
+func TestTargetInfoWithMultipleRequests(t *testing.T) {
+	prwReceiver := setupMetricsReceiver(t)
+	mockConsumer := new(MockConsumer)
+	prwReceiver.nextConsumer = mockConsumer
+	w := httptest.NewRecorder()
+
+	firstRequest := &writev2.Request{
+		Symbols: []string{
+			"",
+			"job", "production/service_a", // 1, 2
+			"instance", "host1", // 3, 4
+			"machine_type", "n1-standard-1", // 5, 6
+			"cloud_provider", "gcp", // 7, 8
+			"region", "us-central1", // 9, 10
+		},
+		Timeseries: []writev2.TimeSeries{
+			{
+				Metadata:   writev2.Metadata{Type: writev2.Metadata_METRIC_TYPE_INFO},
+				LabelsRefs: []uint32{1, 2, 3, 4, 5, 6, 7, 8, 9, 10},
+			},
+		},
+	}
+
+	buf := proto.NewBuffer(nil)
+	err := buf.Marshal(firstRequest)
+	assert.NoError(t, err)
+
+	req1 := httptest.NewRequest(http.MethodPost, "/api/v1/write", bytes.NewReader(buf.Bytes()))
+	req1.Header.Set("Content-Type", fmt.Sprintf("application/x-protobuf;proto=%s", promconfig.RemoteWriteProtoMsgV2))
+	req1.Header.Set("Content-Encoding", "snappy")
path: receiver/prometheusremotewritereceiver/receiver_test.go
user: login: ArthurSens
body: If we have a bug unrelated to target_info, could we solve that first in a separate PR?
created_at: 2025-03-22T20:46:33Z
author_association: MEMBER
diff_hunk: @@ -406,17 +408,194 @@ func TestTranslateV2(t *testing.T) {
 				return expected
 			}(),
 		},
+		{
+			name: "service with target_info metric",
+			request: &writev2.Request{
+				Symbols: []string{
+					"",
+					"job", "production/service_a", // 1, 2
+					"instance", "host1", // 3, 4
+					"machine_type", "n1-standard-1", // 5, 6
+					"cloud_provider", "gcp", // 7, 8
+					"region", "us-central1", // 9, 10
+					"datacenter", "sdc", // 11, 12
+					"__name__", "normal_metric", // 13, 14
+					"d", "e", // 15, 16
+				},
+				Timeseries: []writev2.TimeSeries{
+					// Generating 2 metrics, one is a type info and the other is a normal gauge.
+					// The type info metric should be translated to just contains the resource attributes.
+					// The normal_metric should be translated as usual.
+					{
+						Metadata:   writev2.Metadata{Type: writev2.Metadata_METRIC_TYPE_INFO},
+						LabelsRefs: []uint32{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12},
+					},
+					{
+						Metadata:   writev2.Metadata{Type: writev2.Metadata_METRIC_TYPE_GAUGE},
+						LabelsRefs: []uint32{13, 14, 1, 2, 3, 4, 15, 16},
+						Samples:    []writev2.Sample{{Value: 1, Timestamp: 1}},
+					},
+				},
+			},
+			expectedMetrics: func() pmetric.Metrics {
+				metrics := pmetric.NewMetrics()
+
+				rm := metrics.ResourceMetrics().AppendEmpty()
+				attrs := rm.Resource().Attributes()
+				attrs.PutStr("service.namespace", "production")
+				attrs.PutStr("service.name", "service_a")
+				attrs.PutStr("service.instance.id", "host1")
+				attrs.PutStr("machine.type", "n1-standard-1")
+				attrs.PutStr("cloud.provider", "gcp")
+				attrs.PutStr("region", "us-central1")
+				attrs.PutStr("datacenter", "sdc")
+
+				sm := rm.ScopeMetrics().AppendEmpty()
+				sm.Scope().SetName("OpenTelemetry Collector")
+				sm.Scope().SetVersion("latest")
+
+				m := sm.Metrics().AppendEmpty()
+				m.SetName("normal_metric")
+				m.SetUnit("")
+				m.SetDescription("")
+
+				dp := m.SetEmptyGauge().DataPoints().AppendEmpty()
+				dp.SetDoubleValue(1.0)
+				dp.SetTimestamp(pcommon.Timestamp(1 * int64(time.Millisecond)))
+				dp.Attributes().PutStr("d", "e")
+
+				return metrics
+			}(),
+		},
 	} {
 		t.Run(tc.name, func(t *testing.T) {
-			metrics, stats, err := prwReceiver.translateV2(ctx, tc.request)
+			// since we are using the interRequestCache to store values across requests, we need to clear it after each test, otherwise it will affect the next test
+			prwReceiver.interRequestCache = make(map[uint64]pmetric.ResourceMetrics)
+			metrics, _, err := prwReceiver.translateV2(ctx, tc.request)
 			if tc.expectError != "" {
 				assert.ErrorContains(t, err, tc.expectError)
 				return
 			}
 
 			assert.NoError(t, err)
 			assert.NoError(t, pmetrictest.CompareMetrics(tc.expectedMetrics, metrics))
-			assert.Equal(t, tc.expectedStats, stats)
 		})
 	}
 }
+
+type nonMutatingConsumer struct{}
+
+// Capabilities returns the base consumer capabilities.
+func (bc nonMutatingConsumer) Capabilities() consumer.Capabilities {
+	return consumer.Capabilities{MutatesData: false}
+}
+
+type MockConsumer struct {
+	nonMutatingConsumer
+	mu         sync.Mutex
+	metrics    []pmetric.Metrics
+	dataPoints int
+}
+
+func (m *MockConsumer) ConsumeMetrics(_ context.Context, md pmetric.Metrics) error {
+	m.mu.Lock()
+	defer m.mu.Unlock()
+	m.metrics = append(m.metrics, md)
+	m.dataPoints += md.DataPointCount()
+	return nil
+}
+
+func TestTargetInfoWithMultipleRequests(t *testing.T) {
+	prwReceiver := setupMetricsReceiver(t)
+	mockConsumer := new(MockConsumer)
+	prwReceiver.nextConsumer = mockConsumer
+	w := httptest.NewRecorder()
+
+	firstRequest := &writev2.Request{
+		Symbols: []string{
+			"",
+			"job", "production/service_a", // 1, 2
+			"instance", "host1", // 3, 4
+			"machine_type", "n1-standard-1", // 5, 6
+			"cloud_provider", "gcp", // 7, 8
+			"region", "us-central1", // 9, 10
+		},
+		Timeseries: []writev2.TimeSeries{
+			{
+				Metadata:   writev2.Metadata{Type: writev2.Metadata_METRIC_TYPE_INFO},
+				LabelsRefs: []uint32{1, 2, 3, 4, 5, 6, 7, 8, 9, 10},
+			},
+		},
+	}
+
+	buf := proto.NewBuffer(nil)
+	err := buf.Marshal(firstRequest)
+	assert.NoError(t, err)
+
+	req1 := httptest.NewRequest(http.MethodPost, "/api/v1/write", bytes.NewReader(buf.Bytes()))
+	req1.Header.Set("Content-Type", fmt.Sprintf("application/x-protobuf;proto=%s", promconfig.RemoteWriteProtoMsgV2))
+	req1.Header.Set("Content-Encoding", "snappy")
path: receiver/prometheusremotewritereceiver/receiver_test.go
user: login: ArthurSens
body: > the only way that I found to solve it, was sending the request without the snappy compression.

This is not an option for us, [PRWv2 requires encoding messages with snappy](https://prometheus.io/docs/specs/prw/remote_write_spec_2_0/#:~:text=Protobuf%20Message%20MUST%20use%20binary%20Wire%20Format.%20Then%2C%20MUST%20be%20compressed%20with%20Google%E2%80%99s%20Snappy.%20Snappy%27s%20block%20format%20MUST%20be%20used%20%2D%2D%20the%20framed%20format%20MUST%20NOT%20be%20used.) :( 

created_at: 2025-03-22T20:48:23Z
author_association: MEMBER
diff_hunk: @@ -406,17 +408,194 @@ func TestTranslateV2(t *testing.T) {
 				return expected
 			}(),
 		},
+		{
+			name: "service with target_info metric",
+			request: &writev2.Request{
+				Symbols: []string{
+					"",
+					"job", "production/service_a", // 1, 2
+					"instance", "host1", // 3, 4
+					"machine_type", "n1-standard-1", // 5, 6
+					"cloud_provider", "gcp", // 7, 8
+					"region", "us-central1", // 9, 10
+					"datacenter", "sdc", // 11, 12
+					"__name__", "normal_metric", // 13, 14
+					"d", "e", // 15, 16
+				},
+				Timeseries: []writev2.TimeSeries{
+					// Generating 2 metrics, one is a type info and the other is a normal gauge.
+					// The type info metric should be translated to just contains the resource attributes.
+					// The normal_metric should be translated as usual.
+					{
+						Metadata:   writev2.Metadata{Type: writev2.Metadata_METRIC_TYPE_INFO},
+						LabelsRefs: []uint32{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12},
+					},
+					{
+						Metadata:   writev2.Metadata{Type: writev2.Metadata_METRIC_TYPE_GAUGE},
+						LabelsRefs: []uint32{13, 14, 1, 2, 3, 4, 15, 16},
+						Samples:    []writev2.Sample{{Value: 1, Timestamp: 1}},
+					},
+				},
+			},
+			expectedMetrics: func() pmetric.Metrics {
+				metrics := pmetric.NewMetrics()
+
+				rm := metrics.ResourceMetrics().AppendEmpty()
+				attrs := rm.Resource().Attributes()
+				attrs.PutStr("service.namespace", "production")
+				attrs.PutStr("service.name", "service_a")
+				attrs.PutStr("service.instance.id", "host1")
+				attrs.PutStr("machine.type", "n1-standard-1")
+				attrs.PutStr("cloud.provider", "gcp")
+				attrs.PutStr("region", "us-central1")
+				attrs.PutStr("datacenter", "sdc")
+
+				sm := rm.ScopeMetrics().AppendEmpty()
+				sm.Scope().SetName("OpenTelemetry Collector")
+				sm.Scope().SetVersion("latest")
+
+				m := sm.Metrics().AppendEmpty()
+				m.SetName("normal_metric")
+				m.SetUnit("")
+				m.SetDescription("")
+
+				dp := m.SetEmptyGauge().DataPoints().AppendEmpty()
+				dp.SetDoubleValue(1.0)
+				dp.SetTimestamp(pcommon.Timestamp(1 * int64(time.Millisecond)))
+				dp.Attributes().PutStr("d", "e")
+
+				return metrics
+			}(),
+		},
 	} {
 		t.Run(tc.name, func(t *testing.T) {
-			metrics, stats, err := prwReceiver.translateV2(ctx, tc.request)
+			// since we are using the interRequestCache to store values across requests, we need to clear it after each test, otherwise it will affect the next test
+			prwReceiver.interRequestCache = make(map[uint64]pmetric.ResourceMetrics)
+			metrics, _, err := prwReceiver.translateV2(ctx, tc.request)
 			if tc.expectError != "" {
 				assert.ErrorContains(t, err, tc.expectError)
 				return
 			}
 
 			assert.NoError(t, err)
 			assert.NoError(t, pmetrictest.CompareMetrics(tc.expectedMetrics, metrics))
-			assert.Equal(t, tc.expectedStats, stats)
 		})
 	}
 }
+
+type nonMutatingConsumer struct{}
+
+// Capabilities returns the base consumer capabilities.
+func (bc nonMutatingConsumer) Capabilities() consumer.Capabilities {
+	return consumer.Capabilities{MutatesData: false}
+}
+
+type MockConsumer struct {
+	nonMutatingConsumer
+	mu         sync.Mutex
+	metrics    []pmetric.Metrics
+	dataPoints int
+}
+
+func (m *MockConsumer) ConsumeMetrics(_ context.Context, md pmetric.Metrics) error {
+	m.mu.Lock()
+	defer m.mu.Unlock()
+	m.metrics = append(m.metrics, md)
+	m.dataPoints += md.DataPointCount()
+	return nil
+}
+
+func TestTargetInfoWithMultipleRequests(t *testing.T) {
+	prwReceiver := setupMetricsReceiver(t)
+	mockConsumer := new(MockConsumer)
+	prwReceiver.nextConsumer = mockConsumer
+	w := httptest.NewRecorder()
+
+	firstRequest := &writev2.Request{
+		Symbols: []string{
+			"",
+			"job", "production/service_a", // 1, 2
+			"instance", "host1", // 3, 4
+			"machine_type", "n1-standard-1", // 5, 6
+			"cloud_provider", "gcp", // 7, 8
+			"region", "us-central1", // 9, 10
+		},
+		Timeseries: []writev2.TimeSeries{
+			{
+				Metadata:   writev2.Metadata{Type: writev2.Metadata_METRIC_TYPE_INFO},
+				LabelsRefs: []uint32{1, 2, 3, 4, 5, 6, 7, 8, 9, 10},
+			},
+		},
+	}
+
+	buf := proto.NewBuffer(nil)
+	err := buf.Marshal(firstRequest)
+	assert.NoError(t, err)
+
+	req1 := httptest.NewRequest(http.MethodPost, "/api/v1/write", bytes.NewReader(buf.Bytes()))
+	req1.Header.Set("Content-Type", fmt.Sprintf("application/x-protobuf;proto=%s", promconfig.RemoteWriteProtoMsgV2))
+	req1.Header.Set("Content-Encoding", "snappy")
path: receiver/prometheusremotewritereceiver/receiver_test.go
user: login: perebaj
body: https://github.com/open-telemetry/opentelemetry-collector-contrib/pull/38899

Fixed
created_at: 2025-03-24T11:54:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -406,17 +410,217 @@ func TestTranslateV2(t *testing.T) {
 				return expected
 			}(),
 		},
+		{
+			name: "service with target_info metric",
+			request: &writev2.Request{
+				Symbols: []string{
+					"",
+					"job", "production/service_a", // 1, 2
+					"instance", "host1", // 3, 4
+					"machine_type", "n1-standard-1", // 5, 6
+					"cloud_provider", "gcp", // 7, 8
+					"region", "us-central1", // 9, 10
+					"datacenter", "sdc", // 11, 12
+					"__name__", "normal_metric", // 13, 14
+					"d", "e", // 15, 16
+					"__name__", "target_info", // 17, 18
+				},
+				Timeseries: []writev2.TimeSeries{
+					// Generating 2 metrics, one have the target_info in the name and the other is a normal gauge.
+					// The target_info metric should be translated to use the resource attributes.
+					// The normal_metric should be translated as usual.
+					{
+						// target_info metric
+						Metadata:   writev2.Metadata{Type: writev2.Metadata_METRIC_TYPE_INFO},
path: receiver/prometheusremotewritereceiver/receiver_test.go
user: login: perebaj
body: `writev2.Metadata{Type: writev2.Metadata_METRIC_TYPE_INFO}` I'm setting the type as METRIC_TYPE_INFO, but, when a metric comes as a `target_info` which type it carries? Is it matter ?
created_at: 2025-04-21T18:05:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -125,3 +125,9 @@ replace github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatatest
 replace github.com/open-telemetry/opentelemetry-collector-contrib/pkg/golden => ../../pkg/golden
 
 replace github.com/open-telemetry/opentelemetry-collector-contrib/internal/exp/metrics => ../../internal/exp/metrics
+
+// Temporary solution to unblock the development of target_info. Recently we found a bug in the decompression/compression of snappy requests.
+// The solution is being addressed in https://github.com/open-telemetry/opentelemetry-collector/pull/12825. For this reason we are using the forked version of the collector.
+replace go.opentelemetry.io/collector/config/confighttp => github.com/cardinalhq/opentelemetry-collector/config/confighttp v0.0.0-20250411101659-db48f79d3530
path: receiver/prometheusremotewritereceiver/go.mod
user: login: atoulme
body: this cannot be merged as is. The fix must land in core and we can then update to the latest snapshot to adopt it.
created_at: 2025-04-21T23:13:04Z
author_association: CONTRIBUTOR
diff_hunk: @@ -123,14 +125,15 @@ func (prw *prometheusRemoteWriteReceiver) handlePRW(w http.ResponseWriter, req *
 		return
 	}
 
-	_, stats, err := prw.translateV2(req.Context(), &prw2Req)
+	m, stats, err := prw.translateV2(req.Context(), &prw2Req)
 	stats.SetHeaders(w)
 	if err != nil {
 		http.Error(w, err.Error(), http.StatusBadRequest) // Following instructions at https://prometheus.io/docs/specs/remote_write_spec_2_0/#invalid-samples
 		return
 	}
 
 	w.WriteHeader(http.StatusNoContent)
+	_ = prw.nextConsumer.ConsumeMetrics(req.Context(), m)
path: receiver/prometheusremotewritereceiver/receiver.go
user: login: atoulme
body: typically these errors are returned to the obsreport object capturing the ingestion. The error is also logged.
created_at: 2025-04-21T23:14:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -125,3 +125,9 @@ replace github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatatest
 replace github.com/open-telemetry/opentelemetry-collector-contrib/pkg/golden => ../../pkg/golden
 
 replace github.com/open-telemetry/opentelemetry-collector-contrib/internal/exp/metrics => ../../internal/exp/metrics
+
+// Temporary solution to unblock the development of target_info. Recently we found a bug in the decompression/compression of snappy requests.
+// The solution is being addressed in https://github.com/open-telemetry/opentelemetry-collector/pull/12825. For this reason we are using the forked version of the collector.
+replace go.opentelemetry.io/collector/config/confighttp => github.com/cardinalhq/opentelemetry-collector/config/confighttp v0.0.0-20250411101659-db48f79d3530
path: receiver/prometheusremotewritereceiver/go.mod
user: login: perebaj
body: Make sense. I'm using this approach just to validate that the logic is ok. My idea is to keep it in draft until that PR merged. Then merge this one afterwards
created_at: 2025-04-21T23:37:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -123,14 +125,15 @@ func (prw *prometheusRemoteWriteReceiver) handlePRW(w http.ResponseWriter, req *
 		return
 	}
 
-	_, stats, err := prw.translateV2(req.Context(), &prw2Req)
+	m, stats, err := prw.translateV2(req.Context(), &prw2Req)
 	stats.SetHeaders(w)
 	if err != nil {
 		http.Error(w, err.Error(), http.StatusBadRequest) // Following instructions at https://prometheus.io/docs/specs/remote_write_spec_2_0/#invalid-samples
 		return
 	}
 
 	w.WriteHeader(http.StatusNoContent)
+	_ = prw.nextConsumer.ConsumeMetrics(req.Context(), m)
path: receiver/prometheusremotewritereceiver/receiver.go
user: login: perebaj
body: [2451c27](https://github.com/open-telemetry/opentelemetry-collector-contrib/pull/38812/commits/2451c2780651669016526d27a49337baf76da82d)
created_at: 2025-04-21T23:55:24Z
author_association: CONTRIBUTOR
diff_hunk: @@ -125,3 +125,9 @@ replace github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatatest
 replace github.com/open-telemetry/opentelemetry-collector-contrib/pkg/golden => ../../pkg/golden
 
 replace github.com/open-telemetry/opentelemetry-collector-contrib/internal/exp/metrics => ../../internal/exp/metrics
+
+// Temporary solution to unblock the development of target_info. Recently we found a bug in the decompression/compression of snappy requests.
+// The solution is being addressed in https://github.com/open-telemetry/opentelemetry-collector/pull/12825. For this reason we are using the forked version of the collector.
+replace go.opentelemetry.io/collector/config/confighttp => github.com/cardinalhq/opentelemetry-collector/config/confighttp v0.0.0-20250411101659-db48f79d3530
path: receiver/prometheusremotewritereceiver/go.mod
user: login: ArthurSens
body: Agreed, let's work on the issue upstream before merging this one!
created_at: 2025-04-22T12:15:17Z
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: prometheusremotewritereceiver
+
+# A brief description of the change.  Surround your text with quotes ("") if it needs to start with a backtick (`).
+note: Create a cache approach to deal with desyncronization between requests that will be processed.
path: .chloggen/target-info.yaml
user: login: ArthurSens
body: ```suggestion
note: Cache `target_info` metrics so it can be used to populate metrics' Resource Attributes.
```
created_at: 2025-04-22T12:27:14Z
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: prometheusremotewritereceiver
+
+# A brief description of the change.  Surround your text with quotes ("") if it needs to start with a backtick (`).
+note: Create a cache approach to deal with desyncronization between requests that will be processed.
+
+# Mandatory: One or more tracking issues related to the change. You can use the PR number here if no issue exists.
+issues: [37277]
+
+# (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: The target info can be send through this service in a desynchronized way, so we need to create a cache to deal with this edge case.
path: .chloggen/target-info.yaml
user: login: ArthurSens
body: ```suggestion
subtext: You can read more about resource attributes handling in https://opentelemetry.io/docs/specs/otel/compatibility/prometheus_and_openmetrics/#resource-attributes-1
```
created_at: 2025-04-22T12:28:07Z
author_association: MEMBER
diff_hunk: @@ -94,6 +98,15 @@ func (prw *prometheusRemoteWriteReceiver) Start(ctx context.Context, host compon
 	mux.HandleFunc("/api/v1/write", prw.handlePRW)
 	var err error
 
+	prw.obsrecv, err = receiverhelper.NewObsReport(receiverhelper.ObsReportSettings{
+		ReceiverID:             prw.settings.ID,
+		Transport:              "http",
+		ReceiverCreateSettings: prw.settings,
+	})
+	if err != nil {
+		return fmt.Errorf("failed to create obsreport: %w", err)
+	}
+
path: receiver/prometheusremotewritereceiver/receiver.go
user: login: ArthurSens
body: This change sounds unrelated to target_info handling, could we do it in a separate PR?
created_at: 2025-04-22T12:28:46Z
author_association: MEMBER
diff_hunk: @@ -232,16 +250,42 @@ func (prw *prometheusRemoteWriteReceiver) translateV2(_ context.Context, req *wr
 			continue
 		}
 
+		// If the metric name is equal to target_info, we use its labels as attributes of the resource
+		// Ref: https://opentelemetry.io/docs/specs/otel/compatibility/prometheus_and_openmetrics/#resource-attributes-1
+		if ls.Has(labels.MetricName) && ls.Get(labels.MetricName) == "target_info" {
path: receiver/prometheusremotewritereceiver/receiver.go
user: login: ArthurSens
body: ```suggestion
		if ls.Get(labels.MetricName) == "target_info" {
```

We're doing this validation a few lines above already
created_at: 2025-04-22T12:30:56Z
author_association: MEMBER
diff_hunk: @@ -232,16 +250,42 @@ func (prw *prometheusRemoteWriteReceiver) translateV2(_ context.Context, req *wr
 			continue
 		}
 
+		// If the metric name is equal to target_info, we use its labels as attributes of the resource
+		// Ref: https://opentelemetry.io/docs/specs/otel/compatibility/prometheus_and_openmetrics/#resource-attributes-1
+		if ls.Has(labels.MetricName) && ls.Get(labels.MetricName) == "target_info" {
+			var rm pmetric.ResourceMetrics
+			hashedLabels := xxhash.Sum64String(ls.Get("job") + string([]byte{'\xff'}) + ls.Get("instance"))
+
+			if existingRM, ok := prw.interRequestCache[hashedLabels]; ok {
+				rm = existingRM
+			} else {
+				rm = otelMetrics.ResourceMetrics().AppendEmpty()
+			}
+
+			attrs := rm.Resource().Attributes()
+			parseJobAndInstance(attrs, ls.Get("job"), ls.Get("instance"))
+
+			// Add the remaining labels as resource attributes
+			for _, l := range ls {
+				if l.Name != "job" && l.Name != "instance" && l.Name != labels.MetricName {
+					attrKey := strings.ReplaceAll(l.Name, "_", ".")
path: receiver/prometheusremotewritereceiver/receiver.go
user: login: ArthurSens
body: ```suggestion
```

We do this translation when sending OTLP -> Prometheus, but this requirement doesn't exist for the other way around
created_at: 2025-04-22T12:31:58Z
author_association: MEMBER
diff_hunk: @@ -217,7 +236,6 @@ func (prw *prometheusRemoteWriteReceiver) translateV2(_ context.Context, req *wr
 		// Instead of creating a whole new OTLP metric, we just append the new sample to the existing OTLP metric.
 		// This cache is called "intra" because in the future we'll have a "interRequestCache" to cache resourceAttributes
 		// between requests based on the metric "target_info".
path: receiver/prometheusremotewritereceiver/receiver.go
user: login: ArthurSens
body: Could we also delete those coments above the variable?
created_at: 2025-04-22T12:33:37Z
author_association: MEMBER
diff_hunk: @@ -406,17 +410,217 @@ func TestTranslateV2(t *testing.T) {
 				return expected
 			}(),
 		},
+		{
+			name: "service with target_info metric",
+			request: &writev2.Request{
+				Symbols: []string{
+					"",
+					"job", "production/service_a", // 1, 2
+					"instance", "host1", // 3, 4
+					"machine_type", "n1-standard-1", // 5, 6
+					"cloud_provider", "gcp", // 7, 8
+					"region", "us-central1", // 9, 10
+					"datacenter", "sdc", // 11, 12
+					"__name__", "normal_metric", // 13, 14
+					"d", "e", // 15, 16
+					"__name__", "target_info", // 17, 18
+				},
+				Timeseries: []writev2.TimeSeries{
+					// Generating 2 metrics, one have the target_info in the name and the other is a normal gauge.
+					// The target_info metric should be translated to use the resource attributes.
+					// The normal_metric should be translated as usual.
+					{
+						// target_info metric
+						Metadata:   writev2.Metadata{Type: writev2.Metadata_METRIC_TYPE_INFO},
path: receiver/prometheusremotewritereceiver/receiver_test.go
user: login: ArthurSens
body: It is a gauge, following the current spec: https://opentelemetry.io/docs/specs/otel/compatibility/prometheus_and_openmetrics/#info

Let's use Gauge while Prometheus hasn't implemented the `Info` type
created_at: 2025-04-22T12:35:35Z
author_association: MEMBER
diff_hunk: @@ -406,17 +410,217 @@ func TestTranslateV2(t *testing.T) {
 				return expected
 			}(),
 		},
+		{
+			name: "service with target_info metric",
+			request: &writev2.Request{
+				Symbols: []string{
+					"",
+					"job", "production/service_a", // 1, 2
+					"instance", "host1", // 3, 4
+					"machine_type", "n1-standard-1", // 5, 6
+					"cloud_provider", "gcp", // 7, 8
+					"region", "us-central1", // 9, 10
+					"datacenter", "sdc", // 11, 12
+					"__name__", "normal_metric", // 13, 14
+					"d", "e", // 15, 16
+					"__name__", "target_info", // 17, 18
+				},
+				Timeseries: []writev2.TimeSeries{
+					// Generating 2 metrics, one have the target_info in the name and the other is a normal gauge.
+					// The target_info metric should be translated to use the resource attributes.
+					// The normal_metric should be translated as usual.
+					{
+						// target_info metric
+						Metadata:   writev2.Metadata{Type: writev2.Metadata_METRIC_TYPE_INFO},
path: receiver/prometheusremotewritereceiver/receiver_test.go
user: login: ArthurSens
body: Actually the type doesn't feel important at the moment, we're only checking the metric name 🤔 
created_at: 2025-04-22T12:36:18Z
author_association: MEMBER
diff_hunk: @@ -406,17 +408,194 @@ func TestTranslateV2(t *testing.T) {
 				return expected
 			}(),
 		},
+		{
+			name: "service with target_info metric",
+			request: &writev2.Request{
+				Symbols: []string{
+					"",
+					"job", "production/service_a", // 1, 2
+					"instance", "host1", // 3, 4
+					"machine_type", "n1-standard-1", // 5, 6
+					"cloud_provider", "gcp", // 7, 8
+					"region", "us-central1", // 9, 10
+					"datacenter", "sdc", // 11, 12
+					"__name__", "normal_metric", // 13, 14
+					"d", "e", // 15, 16
+				},
+				Timeseries: []writev2.TimeSeries{
+					// Generating 2 metrics, one is a type info and the other is a normal gauge.
+					// The type info metric should be translated to just contains the resource attributes.
+					// The normal_metric should be translated as usual.
+					{
+						Metadata:   writev2.Metadata{Type: writev2.Metadata_METRIC_TYPE_INFO},
+						LabelsRefs: []uint32{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12},
+					},
+					{
+						Metadata:   writev2.Metadata{Type: writev2.Metadata_METRIC_TYPE_GAUGE},
+						LabelsRefs: []uint32{13, 14, 1, 2, 3, 4, 15, 16},
+						Samples:    []writev2.Sample{{Value: 1, Timestamp: 1}},
+					},
+				},
+			},
+			expectedMetrics: func() pmetric.Metrics {
+				metrics := pmetric.NewMetrics()
+
+				rm := metrics.ResourceMetrics().AppendEmpty()
+				attrs := rm.Resource().Attributes()
+				attrs.PutStr("service.namespace", "production")
+				attrs.PutStr("service.name", "service_a")
+				attrs.PutStr("service.instance.id", "host1")
+				attrs.PutStr("machine.type", "n1-standard-1")
+				attrs.PutStr("cloud.provider", "gcp")
+				attrs.PutStr("region", "us-central1")
+				attrs.PutStr("datacenter", "sdc")
+
+				sm := rm.ScopeMetrics().AppendEmpty()
+				sm.Scope().SetName("OpenTelemetry Collector")
+				sm.Scope().SetVersion("latest")
+
+				m := sm.Metrics().AppendEmpty()
+				m.SetName("normal_metric")
+				m.SetUnit("")
+				m.SetDescription("")
+
+				dp := m.SetEmptyGauge().DataPoints().AppendEmpty()
+				dp.SetDoubleValue(1.0)
+				dp.SetTimestamp(pcommon.Timestamp(1 * int64(time.Millisecond)))
+				dp.Attributes().PutStr("d", "e")
+
+				return metrics
+			}(),
+		},
 	} {
 		t.Run(tc.name, func(t *testing.T) {
-			metrics, stats, err := prwReceiver.translateV2(ctx, tc.request)
+			// since we are using the interRequestCache to store values across requests, we need to clear it after each test, otherwise it will affect the next test
+			prwReceiver.interRequestCache = make(map[uint64]pmetric.ResourceMetrics)
+			metrics, _, err := prwReceiver.translateV2(ctx, tc.request)
path: receiver/prometheusremotewritereceiver/receiver_test.go
user: login: ArthurSens
body: I'm fine with both, choose what you feel like is best :)
created_at: 2025-04-22T12:37:53Z
author_association: MEMBER
diff_hunk: @@ -406,17 +421,213 @@ func TestTranslateV2(t *testing.T) {
 				return expected
 			}(),
 		},
+		{
+			name: "service with target_info metric",
+			request: &writev2.Request{
+				Symbols: []string{
+					"",
+					"job", "production/service_a", // 1, 2
+					"instance", "host1", // 3, 4
+					"machine_type", "n1-standard-1", // 5, 6
+					"cloud_provider", "gcp", // 7, 8
+					"region", "us-central1", // 9, 10
+					"datacenter", "sdc", // 11, 12
+					"__name__", "normal_metric", // 13, 14
+					"d", "e", // 15, 16
+					"__name__", "target_info", // 17, 18
+				},
+				Timeseries: []writev2.TimeSeries{
+					// Generating 2 metrics, one have the target_info in the name and the other is a normal gauge.
+					// The target_info metric should be translated to use the resource attributes.
+					// The normal_metric should be translated as usual.
+					{
+						// target_info metric
+						Metadata:   writev2.Metadata{Type: writev2.Metadata_METRIC_TYPE_INFO},
+						LabelsRefs: []uint32{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 17, 18},
+					},
+					{
+						// normal metric
+						Metadata:   writev2.Metadata{Type: writev2.Metadata_METRIC_TYPE_GAUGE},
+						LabelsRefs: []uint32{13, 14, 1, 2, 3, 4, 15, 16},
+						Samples:    []writev2.Sample{{Value: 1, Timestamp: 1}},
+					},
+				},
+			},
+			expectedMetrics: func() pmetric.Metrics {
+				metrics := pmetric.NewMetrics()
+
+				rm := metrics.ResourceMetrics().AppendEmpty()
+				attrs := rm.Resource().Attributes()
+				attrs.PutStr("service.namespace", "production")
+				attrs.PutStr("service.name", "service_a")
+				attrs.PutStr("service.instance.id", "host1")
+				attrs.PutStr("machine.type", "n1-standard-1")
+				attrs.PutStr("cloud.provider", "gcp")
+				attrs.PutStr("region", "us-central1")
+				attrs.PutStr("datacenter", "sdc")
+
+				sm := rm.ScopeMetrics().AppendEmpty()
+				sm.Scope().SetName("OpenTelemetry Collector")
+				sm.Scope().SetVersion("latest")
+
+				m := sm.Metrics().AppendEmpty()
+				m.SetName("normal_metric")
+				m.SetUnit("")
+				m.SetDescription("")
+
+				dp := m.SetEmptyGauge().DataPoints().AppendEmpty()
+				dp.SetDoubleValue(1.0)
+				dp.SetTimestamp(pcommon.Timestamp(1 * int64(time.Millisecond)))
+				dp.Attributes().PutStr("d", "e")
+
+				return metrics
+			}(),
+		},
 	} {
 		t.Run(tc.name, func(t *testing.T) {
-			metrics, stats, err := prwReceiver.translateV2(ctx, tc.request)
+			// since we are using the interRequestCache to store values across requests, we need to clear it after each test, otherwise it will affect the next test
+			prwReceiver.interRequestCache = make(map[uint64]pmetric.ResourceMetrics)
+			metrics, _, err := prwReceiver.translateV2(ctx, tc.request)
 			if tc.expectError != "" {
 				assert.ErrorContains(t, err, tc.expectError)
 				return
 			}
 
 			assert.NoError(t, err)
 			assert.NoError(t, pmetrictest.CompareMetrics(tc.expectedMetrics, metrics))
-			assert.Equal(t, tc.expectedStats, stats)
path: receiver/prometheusremotewritereceiver/receiver_test.go
user: login: ArthurSens
body: Was this intentional? Not sure why we're deleting this line 🤔 
created_at: 2025-04-22T12:38:24Z
author_association: MEMBER
diff_hunk: @@ -406,17 +408,194 @@ func TestTranslateV2(t *testing.T) {
 				return expected
 			}(),
 		},
+		{
+			name: "service with target_info metric",
+			request: &writev2.Request{
+				Symbols: []string{
+					"",
+					"job", "production/service_a", // 1, 2
+					"instance", "host1", // 3, 4
+					"machine_type", "n1-standard-1", // 5, 6
+					"cloud_provider", "gcp", // 7, 8
+					"region", "us-central1", // 9, 10
+					"datacenter", "sdc", // 11, 12
+					"__name__", "normal_metric", // 13, 14
+					"d", "e", // 15, 16
+				},
+				Timeseries: []writev2.TimeSeries{
+					// Generating 2 metrics, one is a type info and the other is a normal gauge.
+					// The type info metric should be translated to just contains the resource attributes.
+					// The normal_metric should be translated as usual.
+					{
+						Metadata:   writev2.Metadata{Type: writev2.Metadata_METRIC_TYPE_INFO},
+						LabelsRefs: []uint32{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12},
+					},
+					{
+						Metadata:   writev2.Metadata{Type: writev2.Metadata_METRIC_TYPE_GAUGE},
+						LabelsRefs: []uint32{13, 14, 1, 2, 3, 4, 15, 16},
+						Samples:    []writev2.Sample{{Value: 1, Timestamp: 1}},
+					},
+				},
+			},
+			expectedMetrics: func() pmetric.Metrics {
+				metrics := pmetric.NewMetrics()
+
+				rm := metrics.ResourceMetrics().AppendEmpty()
+				attrs := rm.Resource().Attributes()
+				attrs.PutStr("service.namespace", "production")
+				attrs.PutStr("service.name", "service_a")
+				attrs.PutStr("service.instance.id", "host1")
+				attrs.PutStr("machine.type", "n1-standard-1")
+				attrs.PutStr("cloud.provider", "gcp")
+				attrs.PutStr("region", "us-central1")
+				attrs.PutStr("datacenter", "sdc")
+
+				sm := rm.ScopeMetrics().AppendEmpty()
+				sm.Scope().SetName("OpenTelemetry Collector")
+				sm.Scope().SetVersion("latest")
+
+				m := sm.Metrics().AppendEmpty()
+				m.SetName("normal_metric")
+				m.SetUnit("")
+				m.SetDescription("")
+
+				dp := m.SetEmptyGauge().DataPoints().AppendEmpty()
+				dp.SetDoubleValue(1.0)
+				dp.SetTimestamp(pcommon.Timestamp(1 * int64(time.Millisecond)))
+				dp.Attributes().PutStr("d", "e")
+
+				return metrics
+			}(),
+		},
 	} {
 		t.Run(tc.name, func(t *testing.T) {
-			metrics, stats, err := prwReceiver.translateV2(ctx, tc.request)
+			// since we are using the interRequestCache to store values across requests, we need to clear it after each test, otherwise it will affect the next test
+			prwReceiver.interRequestCache = make(map[uint64]pmetric.ResourceMetrics)
+			metrics, _, err := prwReceiver.translateV2(ctx, tc.request)
 			if tc.expectError != "" {
 				assert.ErrorContains(t, err, tc.expectError)
 				return
 			}
 
 			assert.NoError(t, err)
 			assert.NoError(t, pmetrictest.CompareMetrics(tc.expectedMetrics, metrics))
-			assert.Equal(t, tc.expectedStats, stats)
 		})
 	}
 }
+
+type nonMutatingConsumer struct{}
+
+// Capabilities returns the base consumer capabilities.
+func (bc nonMutatingConsumer) Capabilities() consumer.Capabilities {
+	return consumer.Capabilities{MutatesData: false}
+}
+
+type MockConsumer struct {
+	nonMutatingConsumer
+	mu         sync.Mutex
+	metrics    []pmetric.Metrics
+	dataPoints int
+}
+
+func (m *MockConsumer) ConsumeMetrics(_ context.Context, md pmetric.Metrics) error {
+	m.mu.Lock()
+	defer m.mu.Unlock()
+	m.metrics = append(m.metrics, md)
+	m.dataPoints += md.DataPointCount()
+	return nil
+}
path: receiver/prometheusremotewritereceiver/receiver_test.go
user: login: ArthurSens
body: What things?
created_at: 2025-04-22T12:38:40Z
author_association: MEMBER
diff_hunk: @@ -406,17 +408,194 @@ func TestTranslateV2(t *testing.T) {
 				return expected
 			}(),
 		},
+		{
+			name: "service with target_info metric",
+			request: &writev2.Request{
+				Symbols: []string{
+					"",
+					"job", "production/service_a", // 1, 2
+					"instance", "host1", // 3, 4
+					"machine_type", "n1-standard-1", // 5, 6
+					"cloud_provider", "gcp", // 7, 8
+					"region", "us-central1", // 9, 10
+					"datacenter", "sdc", // 11, 12
+					"__name__", "normal_metric", // 13, 14
+					"d", "e", // 15, 16
+				},
+				Timeseries: []writev2.TimeSeries{
+					// Generating 2 metrics, one is a type info and the other is a normal gauge.
+					// The type info metric should be translated to just contains the resource attributes.
+					// The normal_metric should be translated as usual.
+					{
+						Metadata:   writev2.Metadata{Type: writev2.Metadata_METRIC_TYPE_INFO},
+						LabelsRefs: []uint32{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12},
+					},
+					{
+						Metadata:   writev2.Metadata{Type: writev2.Metadata_METRIC_TYPE_GAUGE},
+						LabelsRefs: []uint32{13, 14, 1, 2, 3, 4, 15, 16},
+						Samples:    []writev2.Sample{{Value: 1, Timestamp: 1}},
+					},
+				},
+			},
+			expectedMetrics: func() pmetric.Metrics {
+				metrics := pmetric.NewMetrics()
+
+				rm := metrics.ResourceMetrics().AppendEmpty()
+				attrs := rm.Resource().Attributes()
+				attrs.PutStr("service.namespace", "production")
+				attrs.PutStr("service.name", "service_a")
+				attrs.PutStr("service.instance.id", "host1")
+				attrs.PutStr("machine.type", "n1-standard-1")
+				attrs.PutStr("cloud.provider", "gcp")
+				attrs.PutStr("region", "us-central1")
+				attrs.PutStr("datacenter", "sdc")
+
+				sm := rm.ScopeMetrics().AppendEmpty()
+				sm.Scope().SetName("OpenTelemetry Collector")
+				sm.Scope().SetVersion("latest")
+
+				m := sm.Metrics().AppendEmpty()
+				m.SetName("normal_metric")
+				m.SetUnit("")
+				m.SetDescription("")
+
+				dp := m.SetEmptyGauge().DataPoints().AppendEmpty()
+				dp.SetDoubleValue(1.0)
+				dp.SetTimestamp(pcommon.Timestamp(1 * int64(time.Millisecond)))
+				dp.Attributes().PutStr("d", "e")
+
+				return metrics
+			}(),
+		},
 	} {
 		t.Run(tc.name, func(t *testing.T) {
-			metrics, stats, err := prwReceiver.translateV2(ctx, tc.request)
+			// since we are using the interRequestCache to store values across requests, we need to clear it after each test, otherwise it will affect the next test
+			prwReceiver.interRequestCache = make(map[uint64]pmetric.ResourceMetrics)
+			metrics, _, err := prwReceiver.translateV2(ctx, tc.request)
 			if tc.expectError != "" {
 				assert.ErrorContains(t, err, tc.expectError)
 				return
 			}
 
 			assert.NoError(t, err)
 			assert.NoError(t, pmetrictest.CompareMetrics(tc.expectedMetrics, metrics))
-			assert.Equal(t, tc.expectedStats, stats)
 		})
 	}
 }
+
+type nonMutatingConsumer struct{}
+
+// Capabilities returns the base consumer capabilities.
+func (bc nonMutatingConsumer) Capabilities() consumer.Capabilities {
+	return consumer.Capabilities{MutatesData: false}
+}
+
+type MockConsumer struct {
+	nonMutatingConsumer
+	mu         sync.Mutex
+	metrics    []pmetric.Metrics
+	dataPoints int
+}
+
+func (m *MockConsumer) ConsumeMetrics(_ context.Context, md pmetric.Metrics) error {
+	m.mu.Lock()
+	defer m.mu.Unlock()
+	m.metrics = append(m.metrics, md)
+	m.dataPoints += md.DataPointCount()
+	return nil
+}
path: receiver/prometheusremotewritereceiver/receiver_test.go
user: login: ArthurSens
body: We jumped on a call and clarified. @perebaj meant declaring instead of initializing, and yes they can be declared here
created_at: 2025-04-22T12:39:44Z
author_association: MEMBER
diff_hunk: @@ -406,17 +421,213 @@ func TestTranslateV2(t *testing.T) {
 				return expected
 			}(),
 		},
+		{
+			name: "service with target_info metric",
+			request: &writev2.Request{
+				Symbols: []string{
+					"",
+					"job", "production/service_a", // 1, 2
+					"instance", "host1", // 3, 4
+					"machine_type", "n1-standard-1", // 5, 6
+					"cloud_provider", "gcp", // 7, 8
+					"region", "us-central1", // 9, 10
+					"datacenter", "sdc", // 11, 12
+					"__name__", "normal_metric", // 13, 14
+					"d", "e", // 15, 16
+					"__name__", "target_info", // 17, 18
+				},
+				Timeseries: []writev2.TimeSeries{
+					// Generating 2 metrics, one have the target_info in the name and the other is a normal gauge.
+					// The target_info metric should be translated to use the resource attributes.
+					// The normal_metric should be translated as usual.
+					{
+						// target_info metric
+						Metadata:   writev2.Metadata{Type: writev2.Metadata_METRIC_TYPE_INFO},
+						LabelsRefs: []uint32{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 17, 18},
+					},
+					{
+						// normal metric
+						Metadata:   writev2.Metadata{Type: writev2.Metadata_METRIC_TYPE_GAUGE},
+						LabelsRefs: []uint32{13, 14, 1, 2, 3, 4, 15, 16},
+						Samples:    []writev2.Sample{{Value: 1, Timestamp: 1}},
+					},
+				},
+			},
+			expectedMetrics: func() pmetric.Metrics {
+				metrics := pmetric.NewMetrics()
+
+				rm := metrics.ResourceMetrics().AppendEmpty()
+				attrs := rm.Resource().Attributes()
+				attrs.PutStr("service.namespace", "production")
+				attrs.PutStr("service.name", "service_a")
+				attrs.PutStr("service.instance.id", "host1")
+				attrs.PutStr("machine.type", "n1-standard-1")
+				attrs.PutStr("cloud.provider", "gcp")
+				attrs.PutStr("region", "us-central1")
+				attrs.PutStr("datacenter", "sdc")
+
+				sm := rm.ScopeMetrics().AppendEmpty()
+				sm.Scope().SetName("OpenTelemetry Collector")
+				sm.Scope().SetVersion("latest")
+
+				m := sm.Metrics().AppendEmpty()
+				m.SetName("normal_metric")
+				m.SetUnit("")
+				m.SetDescription("")
+
+				dp := m.SetEmptyGauge().DataPoints().AppendEmpty()
+				dp.SetDoubleValue(1.0)
+				dp.SetTimestamp(pcommon.Timestamp(1 * int64(time.Millisecond)))
+				dp.Attributes().PutStr("d", "e")
+
+				return metrics
+			}(),
+		},
 	} {
 		t.Run(tc.name, func(t *testing.T) {
-			metrics, stats, err := prwReceiver.translateV2(ctx, tc.request)
+			// since we are using the interRequestCache to store values across requests, we need to clear it after each test, otherwise it will affect the next test
+			prwReceiver.interRequestCache = make(map[uint64]pmetric.ResourceMetrics)
+			metrics, _, err := prwReceiver.translateV2(ctx, tc.request)
 			if tc.expectError != "" {
 				assert.ErrorContains(t, err, tc.expectError)
 				return
 			}
 
 			assert.NoError(t, err)
 			assert.NoError(t, pmetrictest.CompareMetrics(tc.expectedMetrics, metrics))
-			assert.Equal(t, tc.expectedStats, stats)
 		})
 	}
 }
+
+type nonMutatingConsumer struct{}
+
+// Capabilities returns the base consumer capabilities.
+func (bc nonMutatingConsumer) Capabilities() consumer.Capabilities {
+	return consumer.Capabilities{MutatesData: false}
+}
+
+type MockConsumer struct {
+	nonMutatingConsumer
+	mu         sync.Mutex
+	metrics    []pmetric.Metrics
+	dataPoints int
+}
+
+func (m *MockConsumer) ConsumeMetrics(_ context.Context, md pmetric.Metrics) error {
+	m.mu.Lock()
+	defer m.mu.Unlock()
+	m.metrics = append(m.metrics, md)
+	m.dataPoints += md.DataPointCount()
+	return nil
+}
+
+func TestTargetInfoWithMultipleRequests(t *testing.T) {
+	factory := NewFactory()
+	cfg := factory.CreateDefaultConfig()
+	cfg.(*Config).Endpoint = "localhost:9090"
path: receiver/prometheusremotewritereceiver/receiver_test.go
user: login: ArthurSens
body: If we hardcode the port number, we'll probably make this test flaky, as the port might have been assigned during another test already.

I feel like we're doing all this complicated stuff because we want to assure the snappy decoding is working, but, although this PR was hit by the snappy bug, asserting the bug is fixed is not in the scope of this PR.

Let's try re-writing this test ignoring the snappy encoding/decoding. (You can keep the go module changes until that bugfix PR isn't merged if it's required to make the test pass)
created_at: 2025-04-22T12:42:27Z
author_association: MEMBER
diff_hunk: @@ -406,17 +421,213 @@ func TestTranslateV2(t *testing.T) {
 				return expected
 			}(),
 		},
+		{
+			name: "service with target_info metric",
+			request: &writev2.Request{
+				Symbols: []string{
+					"",
+					"job", "production/service_a", // 1, 2
+					"instance", "host1", // 3, 4
+					"machine_type", "n1-standard-1", // 5, 6
+					"cloud_provider", "gcp", // 7, 8
+					"region", "us-central1", // 9, 10
+					"datacenter", "sdc", // 11, 12
+					"__name__", "normal_metric", // 13, 14
+					"d", "e", // 15, 16
+					"__name__", "target_info", // 17, 18
+				},
+				Timeseries: []writev2.TimeSeries{
+					// Generating 2 metrics, one have the target_info in the name and the other is a normal gauge.
+					// The target_info metric should be translated to use the resource attributes.
+					// The normal_metric should be translated as usual.
+					{
+						// target_info metric
+						Metadata:   writev2.Metadata{Type: writev2.Metadata_METRIC_TYPE_INFO},
+						LabelsRefs: []uint32{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 17, 18},
+					},
+					{
+						// normal metric
+						Metadata:   writev2.Metadata{Type: writev2.Metadata_METRIC_TYPE_GAUGE},
+						LabelsRefs: []uint32{13, 14, 1, 2, 3, 4, 15, 16},
+						Samples:    []writev2.Sample{{Value: 1, Timestamp: 1}},
+					},
+				},
+			},
+			expectedMetrics: func() pmetric.Metrics {
+				metrics := pmetric.NewMetrics()
+
+				rm := metrics.ResourceMetrics().AppendEmpty()
+				attrs := rm.Resource().Attributes()
+				attrs.PutStr("service.namespace", "production")
+				attrs.PutStr("service.name", "service_a")
+				attrs.PutStr("service.instance.id", "host1")
+				attrs.PutStr("machine.type", "n1-standard-1")
+				attrs.PutStr("cloud.provider", "gcp")
+				attrs.PutStr("region", "us-central1")
+				attrs.PutStr("datacenter", "sdc")
+
+				sm := rm.ScopeMetrics().AppendEmpty()
+				sm.Scope().SetName("OpenTelemetry Collector")
+				sm.Scope().SetVersion("latest")
+
+				m := sm.Metrics().AppendEmpty()
+				m.SetName("normal_metric")
+				m.SetUnit("")
+				m.SetDescription("")
+
+				dp := m.SetEmptyGauge().DataPoints().AppendEmpty()
+				dp.SetDoubleValue(1.0)
+				dp.SetTimestamp(pcommon.Timestamp(1 * int64(time.Millisecond)))
+				dp.Attributes().PutStr("d", "e")
+
+				return metrics
+			}(),
+		},
 	} {
 		t.Run(tc.name, func(t *testing.T) {
-			metrics, stats, err := prwReceiver.translateV2(ctx, tc.request)
+			// since we are using the interRequestCache to store values across requests, we need to clear it after each test, otherwise it will affect the next test
+			prwReceiver.interRequestCache = make(map[uint64]pmetric.ResourceMetrics)
+			metrics, _, err := prwReceiver.translateV2(ctx, tc.request)
 			if tc.expectError != "" {
 				assert.ErrorContains(t, err, tc.expectError)
 				return
 			}
 
 			assert.NoError(t, err)
 			assert.NoError(t, pmetrictest.CompareMetrics(tc.expectedMetrics, metrics))
-			assert.Equal(t, tc.expectedStats, stats)
 		})
 	}
 }
+
+type nonMutatingConsumer struct{}
+
+// Capabilities returns the base consumer capabilities.
+func (bc nonMutatingConsumer) Capabilities() consumer.Capabilities {
+	return consumer.Capabilities{MutatesData: false}
+}
+
+type MockConsumer struct {
+	nonMutatingConsumer
+	mu         sync.Mutex
+	metrics    []pmetric.Metrics
+	dataPoints int
+}
+
+func (m *MockConsumer) ConsumeMetrics(_ context.Context, md pmetric.Metrics) error {
+	m.mu.Lock()
+	defer m.mu.Unlock()
+	m.metrics = append(m.metrics, md)
+	m.dataPoints += md.DataPointCount()
+	return nil
+}
+
+func TestTargetInfoWithMultipleRequests(t *testing.T) {
+	factory := NewFactory()
+	cfg := factory.CreateDefaultConfig()
+	cfg.(*Config).Endpoint = "localhost:9090"
+
+	mockConsumer := new(MockConsumer)
+	prwReceiver, err := factory.CreateMetrics(context.Background(), receivertest.NewNopSettings(metadata.Type), cfg, mockConsumer)
+	assert.NoError(t, err)
+	assert.NotNil(t, prwReceiver)
+
+	host := componenttest.NewNopHost()
+	err = prwReceiver.Start(context.Background(), host)
+	assert.NoError(t, err)
+	defer func() {
+		assert.NoError(t, prwReceiver.Shutdown(context.Background()))
+	}()
+
+	// Define two different metrics, one is target_info and the other is a normal metric.
+	// In the end, we expect that we have 1 final metric. All labels from the target_info metric should be translated to resource attributes and
+	// the normal metric should be translated as usual.
+	requests := []*writev2.Request{
+		{
+			Symbols: []string{
+				"",
+				"job", "production/service_a", // 1, 2
+				"instance", "host1", // 3, 4
+				"machine_type", "n1-standard-1", // 5, 6
+				"cloud_provider", "gcp", // 7, 8
+				"region", "us-central1", // 9, 10
+				"__name__", "target_info", // 11, 12
path: receiver/prometheusremotewritereceiver/receiver_test.go
user: login: ArthurSens
body: I have a feeling that our current implementation only works if `target_info` is sent before the metric it was supposed to complement. If that's the behavior, that's totally ok, but let's also document this requirement in our README.

If I'm incorrect, let's also add a test case where target_info is sent after the original metric!
created_at: 2025-04-22T12:43:49Z
author_association: MEMBER
diff_hunk: @@ -94,6 +98,15 @@ func (prw *prometheusRemoteWriteReceiver) Start(ctx context.Context, host compon
 	mux.HandleFunc("/api/v1/write", prw.handlePRW)
 	var err error
 
+	prw.obsrecv, err = receiverhelper.NewObsReport(receiverhelper.ObsReportSettings{
+		ReceiverID:             prw.settings.ID,
+		Transport:              "http",
+		ReceiverCreateSettings: prw.settings,
+	})
+	if err != nil {
+		return fmt.Errorf("failed to create obsreport: %w", err)
+	}
+
path: receiver/prometheusremotewritereceiver/receiver.go
user: login: perebaj
body: [7739e72](https://github.com/open-telemetry/opentelemetry-collector-contrib/pull/38812/commits/7739e726e3c6869ac2eb6238edaf248429417f7b)

Done here. I know that you mentioned to not put a TODO task here, but I can't rely on my memory 🫠 to remeber it
created_at: 2025-04-23T00:11:14Z
author_association: CONTRIBUTOR
diff_hunk: @@ -232,16 +250,42 @@ func (prw *prometheusRemoteWriteReceiver) translateV2(_ context.Context, req *wr
 			continue
 		}
 
+		// If the metric name is equal to target_info, we use its labels as attributes of the resource
+		// Ref: https://opentelemetry.io/docs/specs/otel/compatibility/prometheus_and_openmetrics/#resource-attributes-1
+		if ls.Has(labels.MetricName) && ls.Get(labels.MetricName) == "target_info" {
path: receiver/prometheusremotewritereceiver/receiver.go
user: login: perebaj
body: https://github.com/open-telemetry/opentelemetry-collector-contrib/pull/38812/commits/05a8845ee34f26e73e18a8e7b40cc7012b798355
created_at: 2025-04-23T00:11:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -217,7 +236,6 @@ func (prw *prometheusRemoteWriteReceiver) translateV2(_ context.Context, req *wr
 		// Instead of creating a whole new OTLP metric, we just append the new sample to the existing OTLP metric.
 		// This cache is called "intra" because in the future we'll have a "interRequestCache" to cache resourceAttributes
 		// between requests based on the metric "target_info".
path: receiver/prometheusremotewritereceiver/receiver.go
user: login: perebaj
body: https://github.com/open-telemetry/opentelemetry-collector-contrib/pull/38812/commits/bc70d0c56021018f6ce87fe7ecc2beda13e886f3
created_at: 2025-04-23T00:12:10Z
author_association: CONTRIBUTOR
diff_hunk: @@ -232,16 +250,42 @@ func (prw *prometheusRemoteWriteReceiver) translateV2(_ context.Context, req *wr
 			continue
 		}
 
+		// If the metric name is equal to target_info, we use its labels as attributes of the resource
+		// Ref: https://opentelemetry.io/docs/specs/otel/compatibility/prometheus_and_openmetrics/#resource-attributes-1
+		if ls.Has(labels.MetricName) && ls.Get(labels.MetricName) == "target_info" {
+			var rm pmetric.ResourceMetrics
+			hashedLabels := xxhash.Sum64String(ls.Get("job") + string([]byte{'\xff'}) + ls.Get("instance"))
+
+			if existingRM, ok := prw.interRequestCache[hashedLabels]; ok {
+				rm = existingRM
+			} else {
+				rm = otelMetrics.ResourceMetrics().AppendEmpty()
+			}
+
+			attrs := rm.Resource().Attributes()
+			parseJobAndInstance(attrs, ls.Get("job"), ls.Get("instance"))
+
+			// Add the remaining labels as resource attributes
+			for _, l := range ls {
+				if l.Name != "job" && l.Name != "instance" && l.Name != labels.MetricName {
+					attrKey := strings.ReplaceAll(l.Name, "_", ".")
path: receiver/prometheusremotewritereceiver/receiver.go
user: login: perebaj
body: https://github.com/open-telemetry/opentelemetry-collector-contrib/pull/38812/commits/d2ae2f440cde588a3a01084b12a9e80ef2bb28d5
created_at: 2025-04-23T00:12:43Z
author_association: CONTRIBUTOR
diff_hunk: @@ -406,17 +421,213 @@ func TestTranslateV2(t *testing.T) {
 				return expected
 			}(),
 		},
+		{
+			name: "service with target_info metric",
+			request: &writev2.Request{
+				Symbols: []string{
+					"",
+					"job", "production/service_a", // 1, 2
+					"instance", "host1", // 3, 4
+					"machine_type", "n1-standard-1", // 5, 6
+					"cloud_provider", "gcp", // 7, 8
+					"region", "us-central1", // 9, 10
+					"datacenter", "sdc", // 11, 12
+					"__name__", "normal_metric", // 13, 14
+					"d", "e", // 15, 16
+					"__name__", "target_info", // 17, 18
+				},
+				Timeseries: []writev2.TimeSeries{
+					// Generating 2 metrics, one have the target_info in the name and the other is a normal gauge.
+					// The target_info metric should be translated to use the resource attributes.
+					// The normal_metric should be translated as usual.
+					{
+						// target_info metric
+						Metadata:   writev2.Metadata{Type: writev2.Metadata_METRIC_TYPE_INFO},
+						LabelsRefs: []uint32{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 17, 18},
+					},
+					{
+						// normal metric
+						Metadata:   writev2.Metadata{Type: writev2.Metadata_METRIC_TYPE_GAUGE},
+						LabelsRefs: []uint32{13, 14, 1, 2, 3, 4, 15, 16},
+						Samples:    []writev2.Sample{{Value: 1, Timestamp: 1}},
+					},
+				},
+			},
+			expectedMetrics: func() pmetric.Metrics {
+				metrics := pmetric.NewMetrics()
+
+				rm := metrics.ResourceMetrics().AppendEmpty()
+				attrs := rm.Resource().Attributes()
+				attrs.PutStr("service.namespace", "production")
+				attrs.PutStr("service.name", "service_a")
+				attrs.PutStr("service.instance.id", "host1")
+				attrs.PutStr("machine.type", "n1-standard-1")
+				attrs.PutStr("cloud.provider", "gcp")
+				attrs.PutStr("region", "us-central1")
+				attrs.PutStr("datacenter", "sdc")
+
+				sm := rm.ScopeMetrics().AppendEmpty()
+				sm.Scope().SetName("OpenTelemetry Collector")
+				sm.Scope().SetVersion("latest")
+
+				m := sm.Metrics().AppendEmpty()
+				m.SetName("normal_metric")
+				m.SetUnit("")
+				m.SetDescription("")
+
+				dp := m.SetEmptyGauge().DataPoints().AppendEmpty()
+				dp.SetDoubleValue(1.0)
+				dp.SetTimestamp(pcommon.Timestamp(1 * int64(time.Millisecond)))
+				dp.Attributes().PutStr("d", "e")
+
+				return metrics
+			}(),
+		},
 	} {
 		t.Run(tc.name, func(t *testing.T) {
-			metrics, stats, err := prwReceiver.translateV2(ctx, tc.request)
+			// since we are using the interRequestCache to store values across requests, we need to clear it after each test, otherwise it will affect the next test
+			prwReceiver.interRequestCache = make(map[uint64]pmetric.ResourceMetrics)
+			metrics, _, err := prwReceiver.translateV2(ctx, tc.request)
 			if tc.expectError != "" {
 				assert.ErrorContains(t, err, tc.expectError)
 				return
 			}
 
 			assert.NoError(t, err)
 			assert.NoError(t, pmetrictest.CompareMetrics(tc.expectedMetrics, metrics))
-			assert.Equal(t, tc.expectedStats, stats)
path: receiver/prometheusremotewritereceiver/receiver_test.go
user: login: perebaj
body: I don't know why I removed it. my bad. Fixed [14fbe4c](https://github.com/open-telemetry/opentelemetry-collector-contrib/pull/38812/commits/14fbe4ca0a428c54e004de6e748753a3f417056f)
created_at: 2025-04-23T00:13:10Z
author_association: CONTRIBUTOR
diff_hunk: @@ -406,17 +410,217 @@ func TestTranslateV2(t *testing.T) {
 				return expected
 			}(),
 		},
+		{
+			name: "service with target_info metric",
+			request: &writev2.Request{
+				Symbols: []string{
+					"",
+					"job", "production/service_a", // 1, 2
+					"instance", "host1", // 3, 4
+					"machine_type", "n1-standard-1", // 5, 6
+					"cloud_provider", "gcp", // 7, 8
+					"region", "us-central1", // 9, 10
+					"datacenter", "sdc", // 11, 12
+					"__name__", "normal_metric", // 13, 14
+					"d", "e", // 15, 16
+					"__name__", "target_info", // 17, 18
+				},
+				Timeseries: []writev2.TimeSeries{
+					// Generating 2 metrics, one have the target_info in the name and the other is a normal gauge.
+					// The target_info metric should be translated to use the resource attributes.
+					// The normal_metric should be translated as usual.
+					{
+						// target_info metric
+						Metadata:   writev2.Metadata{Type: writev2.Metadata_METRIC_TYPE_INFO},
path: receiver/prometheusremotewritereceiver/receiver_test.go
user: login: perebaj
body: [3ce627e](https://github.com/open-telemetry/opentelemetry-collector-contrib/pull/38812/commits/3ce627e2a4ec12d841341eda1dc07deb8cab4f7e)
created_at: 2025-04-23T00:13:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -406,17 +421,213 @@ func TestTranslateV2(t *testing.T) {
 				return expected
 			}(),
 		},
+		{
+			name: "service with target_info metric",
+			request: &writev2.Request{
+				Symbols: []string{
+					"",
+					"job", "production/service_a", // 1, 2
+					"instance", "host1", // 3, 4
+					"machine_type", "n1-standard-1", // 5, 6
+					"cloud_provider", "gcp", // 7, 8
+					"region", "us-central1", // 9, 10
+					"datacenter", "sdc", // 11, 12
+					"__name__", "normal_metric", // 13, 14
+					"d", "e", // 15, 16
+					"__name__", "target_info", // 17, 18
+				},
+				Timeseries: []writev2.TimeSeries{
+					// Generating 2 metrics, one have the target_info in the name and the other is a normal gauge.
+					// The target_info metric should be translated to use the resource attributes.
+					// The normal_metric should be translated as usual.
+					{
+						// target_info metric
+						Metadata:   writev2.Metadata{Type: writev2.Metadata_METRIC_TYPE_INFO},
+						LabelsRefs: []uint32{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 17, 18},
+					},
+					{
+						// normal metric
+						Metadata:   writev2.Metadata{Type: writev2.Metadata_METRIC_TYPE_GAUGE},
+						LabelsRefs: []uint32{13, 14, 1, 2, 3, 4, 15, 16},
+						Samples:    []writev2.Sample{{Value: 1, Timestamp: 1}},
+					},
+				},
+			},
+			expectedMetrics: func() pmetric.Metrics {
+				metrics := pmetric.NewMetrics()
+
+				rm := metrics.ResourceMetrics().AppendEmpty()
+				attrs := rm.Resource().Attributes()
+				attrs.PutStr("service.namespace", "production")
+				attrs.PutStr("service.name", "service_a")
+				attrs.PutStr("service.instance.id", "host1")
+				attrs.PutStr("machine.type", "n1-standard-1")
+				attrs.PutStr("cloud.provider", "gcp")
+				attrs.PutStr("region", "us-central1")
+				attrs.PutStr("datacenter", "sdc")
+
+				sm := rm.ScopeMetrics().AppendEmpty()
+				sm.Scope().SetName("OpenTelemetry Collector")
+				sm.Scope().SetVersion("latest")
+
+				m := sm.Metrics().AppendEmpty()
+				m.SetName("normal_metric")
+				m.SetUnit("")
+				m.SetDescription("")
+
+				dp := m.SetEmptyGauge().DataPoints().AppendEmpty()
+				dp.SetDoubleValue(1.0)
+				dp.SetTimestamp(pcommon.Timestamp(1 * int64(time.Millisecond)))
+				dp.Attributes().PutStr("d", "e")
+
+				return metrics
+			}(),
+		},
 	} {
 		t.Run(tc.name, func(t *testing.T) {
-			metrics, stats, err := prwReceiver.translateV2(ctx, tc.request)
+			// since we are using the interRequestCache to store values across requests, we need to clear it after each test, otherwise it will affect the next test
+			prwReceiver.interRequestCache = make(map[uint64]pmetric.ResourceMetrics)
+			metrics, _, err := prwReceiver.translateV2(ctx, tc.request)
 			if tc.expectError != "" {
 				assert.ErrorContains(t, err, tc.expectError)
 				return
 			}
 
 			assert.NoError(t, err)
 			assert.NoError(t, pmetrictest.CompareMetrics(tc.expectedMetrics, metrics))
-			assert.Equal(t, tc.expectedStats, stats)
 		})
 	}
 }
+
+type nonMutatingConsumer struct{}
+
+// Capabilities returns the base consumer capabilities.
+func (bc nonMutatingConsumer) Capabilities() consumer.Capabilities {
+	return consumer.Capabilities{MutatesData: false}
+}
+
+type MockConsumer struct {
+	nonMutatingConsumer
+	mu         sync.Mutex
+	metrics    []pmetric.Metrics
+	dataPoints int
+}
+
+func (m *MockConsumer) ConsumeMetrics(_ context.Context, md pmetric.Metrics) error {
+	m.mu.Lock()
+	defer m.mu.Unlock()
+	m.metrics = append(m.metrics, md)
+	m.dataPoints += md.DataPointCount()
+	return nil
+}
+
+func TestTargetInfoWithMultipleRequests(t *testing.T) {
+	factory := NewFactory()
+	cfg := factory.CreateDefaultConfig()
+	cfg.(*Config).Endpoint = "localhost:9090"
+
+	mockConsumer := new(MockConsumer)
+	prwReceiver, err := factory.CreateMetrics(context.Background(), receivertest.NewNopSettings(metadata.Type), cfg, mockConsumer)
+	assert.NoError(t, err)
+	assert.NotNil(t, prwReceiver)
+
+	host := componenttest.NewNopHost()
+	err = prwReceiver.Start(context.Background(), host)
+	assert.NoError(t, err)
+	defer func() {
+		assert.NoError(t, prwReceiver.Shutdown(context.Background()))
+	}()
+
+	// Define two different metrics, one is target_info and the other is a normal metric.
+	// In the end, we expect that we have 1 final metric. All labels from the target_info metric should be translated to resource attributes and
+	// the normal metric should be translated as usual.
+	requests := []*writev2.Request{
+		{
+			Symbols: []string{
+				"",
+				"job", "production/service_a", // 1, 2
+				"instance", "host1", // 3, 4
+				"machine_type", "n1-standard-1", // 5, 6
+				"cloud_provider", "gcp", // 7, 8
+				"region", "us-central1", // 9, 10
+				"__name__", "target_info", // 11, 12
path: receiver/prometheusremotewritereceiver/receiver_test.go
user: login: perebaj
body: [c4e8771](https://github.com/open-telemetry/opentelemetry-collector-contrib/pull/38812/commits/c4e8771c1c1693bfd7d4bd865bc17e19519f780f)

We have both approaches working properly. I added a new test case here to assure it
created_at: 2025-04-23T00:14:38Z
author_association: CONTRIBUTOR
diff_hunk: @@ -406,17 +421,213 @@ func TestTranslateV2(t *testing.T) {
 				return expected
 			}(),
 		},
+		{
+			name: "service with target_info metric",
+			request: &writev2.Request{
+				Symbols: []string{
+					"",
+					"job", "production/service_a", // 1, 2
+					"instance", "host1", // 3, 4
+					"machine_type", "n1-standard-1", // 5, 6
+					"cloud_provider", "gcp", // 7, 8
+					"region", "us-central1", // 9, 10
+					"datacenter", "sdc", // 11, 12
+					"__name__", "normal_metric", // 13, 14
+					"d", "e", // 15, 16
+					"__name__", "target_info", // 17, 18
+				},
+				Timeseries: []writev2.TimeSeries{
+					// Generating 2 metrics, one have the target_info in the name and the other is a normal gauge.
+					// The target_info metric should be translated to use the resource attributes.
+					// The normal_metric should be translated as usual.
+					{
+						// target_info metric
+						Metadata:   writev2.Metadata{Type: writev2.Metadata_METRIC_TYPE_INFO},
+						LabelsRefs: []uint32{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 17, 18},
+					},
+					{
+						// normal metric
+						Metadata:   writev2.Metadata{Type: writev2.Metadata_METRIC_TYPE_GAUGE},
+						LabelsRefs: []uint32{13, 14, 1, 2, 3, 4, 15, 16},
+						Samples:    []writev2.Sample{{Value: 1, Timestamp: 1}},
+					},
+				},
+			},
+			expectedMetrics: func() pmetric.Metrics {
+				metrics := pmetric.NewMetrics()
+
+				rm := metrics.ResourceMetrics().AppendEmpty()
+				attrs := rm.Resource().Attributes()
+				attrs.PutStr("service.namespace", "production")
+				attrs.PutStr("service.name", "service_a")
+				attrs.PutStr("service.instance.id", "host1")
+				attrs.PutStr("machine.type", "n1-standard-1")
+				attrs.PutStr("cloud.provider", "gcp")
+				attrs.PutStr("region", "us-central1")
+				attrs.PutStr("datacenter", "sdc")
+
+				sm := rm.ScopeMetrics().AppendEmpty()
+				sm.Scope().SetName("OpenTelemetry Collector")
+				sm.Scope().SetVersion("latest")
+
+				m := sm.Metrics().AppendEmpty()
+				m.SetName("normal_metric")
+				m.SetUnit("")
+				m.SetDescription("")
+
+				dp := m.SetEmptyGauge().DataPoints().AppendEmpty()
+				dp.SetDoubleValue(1.0)
+				dp.SetTimestamp(pcommon.Timestamp(1 * int64(time.Millisecond)))
+				dp.Attributes().PutStr("d", "e")
+
+				return metrics
+			}(),
+		},
 	} {
 		t.Run(tc.name, func(t *testing.T) {
-			metrics, stats, err := prwReceiver.translateV2(ctx, tc.request)
+			// since we are using the interRequestCache to store values across requests, we need to clear it after each test, otherwise it will affect the next test
+			prwReceiver.interRequestCache = make(map[uint64]pmetric.ResourceMetrics)
+			metrics, _, err := prwReceiver.translateV2(ctx, tc.request)
 			if tc.expectError != "" {
 				assert.ErrorContains(t, err, tc.expectError)
 				return
 			}
 
 			assert.NoError(t, err)
 			assert.NoError(t, pmetrictest.CompareMetrics(tc.expectedMetrics, metrics))
-			assert.Equal(t, tc.expectedStats, stats)
 		})
 	}
 }
+
+type nonMutatingConsumer struct{}
+
+// Capabilities returns the base consumer capabilities.
+func (bc nonMutatingConsumer) Capabilities() consumer.Capabilities {
+	return consumer.Capabilities{MutatesData: false}
+}
+
+type MockConsumer struct {
+	nonMutatingConsumer
+	mu         sync.Mutex
+	metrics    []pmetric.Metrics
+	dataPoints int
+}
+
+func (m *MockConsumer) ConsumeMetrics(_ context.Context, md pmetric.Metrics) error {
+	m.mu.Lock()
+	defer m.mu.Unlock()
+	m.metrics = append(m.metrics, md)
+	m.dataPoints += md.DataPointCount()
+	return nil
+}
+
+func TestTargetInfoWithMultipleRequests(t *testing.T) {
+	factory := NewFactory()
+	cfg := factory.CreateDefaultConfig()
+	cfg.(*Config).Endpoint = "localhost:9090"
path: receiver/prometheusremotewritereceiver/receiver_test.go
user: login: perebaj
body: The alternative that I found was this one

https://github.com/open-telemetry/opentelemetry-collector-contrib/pull/38812/commits/130c2b81c936443ad3d56b49cf867f439c81fee6

We can send the body just as a proto, without compression, and it will work! Using this approach, we don't even need to do the replacement thing.
created_at: 2025-04-23T00:58:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -123,14 +125,15 @@ func (prw *prometheusRemoteWriteReceiver) handlePRW(w http.ResponseWriter, req *
 		return
 	}
 
-	_, stats, err := prw.translateV2(req.Context(), &prw2Req)
+	m, stats, err := prw.translateV2(req.Context(), &prw2Req)
 	stats.SetHeaders(w)
 	if err != nil {
 		http.Error(w, err.Error(), http.StatusBadRequest) // Following instructions at https://prometheus.io/docs/specs/remote_write_spec_2_0/#invalid-samples
 		return
 	}
 
 	w.WriteHeader(http.StatusNoContent)
+	_ = prw.nextConsumer.ConsumeMetrics(req.Context(), m)
path: receiver/prometheusremotewritereceiver/receiver.go
user: login: perebaj
body: I added a TODO comment here to address it in the future, this because this change is unrelated to the target_info feature
created_at: 2025-04-23T01:04:22Z
author_association: CONTRIBUTOR
diff_hunk: @@ -37,16 +37,18 @@ func newRemoteWriteReceiver(settings receiver.Settings, cfg *Config, nextConsume
 		server: &http.Server{
 			ReadTimeout: 60 * time.Second,
 		},
+		interRequestCache: make(map[uint64]pmetric.ResourceMetrics),
 	}, nil
 }
 
 type prometheusRemoteWriteReceiver struct {
 	settings     receiver.Settings
 	nextConsumer consumer.Metrics
 
-	config *Config
-	server *http.Server
-	wg     sync.WaitGroup
+	config            *Config
+	server            *http.Server
+	wg                sync.WaitGroup
+	interRequestCache map[uint64]pmetric.ResourceMetrics
path: receiver/prometheusremotewritereceiver/receiver.go
user: login: ArthurSens
body: ```suggestion
	rmCache map[uint64]pmetric.ResourceMetrics
```

In the past we thought we would need two caches, and that's why we had intra and inter request cache. Now that we only have one, maybe naming them intra/inter doesn't make sense anymore. What do you think about renaming to something simpler? :)
created_at: 2025-04-23T12:12:31Z
author_association: MEMBER
diff_hunk: @@ -406,8 +409,72 @@ func TestTranslateV2(t *testing.T) {
 				return expected
 			}(),
 		},
+		{
+			name: "service with target_info metric",
+			request: &writev2.Request{
+				Symbols: []string{
+					"",
+					"job", "production/service_a", // 1, 2
+					"instance", "host1", // 3, 4
+					"machine_type", "n1-standard-1", // 5, 6
+					"cloud_provider", "gcp", // 7, 8
+					"region", "us-central1", // 9, 10
+					"datacenter", "sdc", // 11, 12
+					"__name__", "normal_metric", // 13, 14
+					"d", "e", // 15, 16
+					"__name__", "target_info", // 17, 18
+				},
+				Timeseries: []writev2.TimeSeries{
+					// Generating 2 metrics, one have the target_info in the name and the other is a normal gauge.
+					// The target_info metric should be translated to use the resource attributes.
+					// The normal_metric should be translated as usual.
+					{
+						// target_info metric
+						Metadata:   writev2.Metadata{Type: writev2.Metadata_METRIC_TYPE_GAUGE},
+						LabelsRefs: []uint32{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 17, 18},
+					},
+					{
+						// normal metric
+						Metadata:   writev2.Metadata{Type: writev2.Metadata_METRIC_TYPE_GAUGE},
+						LabelsRefs: []uint32{13, 14, 1, 2, 3, 4, 15, 16},
+						Samples:    []writev2.Sample{{Value: 1, Timestamp: 1}},
+					},
+				},
+			},
+			expectedMetrics: func() pmetric.Metrics {
+				metrics := pmetric.NewMetrics()
+
+				rm := metrics.ResourceMetrics().AppendEmpty()
+				attrs := rm.Resource().Attributes()
+				attrs.PutStr("service.namespace", "production")
+				attrs.PutStr("service.name", "service_a")
+				attrs.PutStr("service.instance.id", "host1")
+				attrs.PutStr("machine_type", "n1-standard-1")
+				attrs.PutStr("cloud_provider", "gcp")
+				attrs.PutStr("region", "us-central1")
+				attrs.PutStr("datacenter", "sdc")
+
+				sm := rm.ScopeMetrics().AppendEmpty()
+				sm.Scope().SetName("OpenTelemetry Collector")
+				sm.Scope().SetVersion("latest")
+
+				m := sm.Metrics().AppendEmpty()
+				m.SetName("normal_metric")
+				m.SetUnit("")
+				m.SetDescription("")
+
+				dp := m.SetEmptyGauge().DataPoints().AppendEmpty()
+				dp.SetDoubleValue(1.0)
+				dp.SetTimestamp(pcommon.Timestamp(1 * int64(time.Millisecond)))
+				dp.Attributes().PutStr("d", "e")
+
+				return metrics
+			}(),
+		},
 	} {
 		t.Run(tc.name, func(t *testing.T) {
+			// since we are using the interRequestCache to store values across requests, we need to clear it after each test, otherwise it will affect the next test
path: receiver/prometheusremotewritereceiver/receiver_test.go
user: login: ArthurSens
body: We might want to update this comment as well if we're renaming the variable
created_at: 2025-04-23T12:15:03Z
author_association: MEMBER
diff_hunk: @@ -94,6 +98,15 @@ func (prw *prometheusRemoteWriteReceiver) Start(ctx context.Context, host compon
 	mux.HandleFunc("/api/v1/write", prw.handlePRW)
 	var err error
 
+	prw.obsrecv, err = receiverhelper.NewObsReport(receiverhelper.ObsReportSettings{
+		ReceiverID:             prw.settings.ID,
+		Transport:              "http",
+		ReceiverCreateSettings: prw.settings,
+	})
+	if err != nil {
+		return fmt.Errorf("failed to create obsreport: %w", err)
+	}
+
path: receiver/prometheusremotewritereceiver/receiver.go
user: login: ArthurSens
body: fair enough :) 
created_at: 2025-04-23T12:22:28Z
author_association: MEMBER
diff_hunk: @@ -406,17 +421,213 @@ func TestTranslateV2(t *testing.T) {
 				return expected
 			}(),
 		},
+		{
+			name: "service with target_info metric",
+			request: &writev2.Request{
+				Symbols: []string{
+					"",
+					"job", "production/service_a", // 1, 2
+					"instance", "host1", // 3, 4
+					"machine_type", "n1-standard-1", // 5, 6
+					"cloud_provider", "gcp", // 7, 8
+					"region", "us-central1", // 9, 10
+					"datacenter", "sdc", // 11, 12
+					"__name__", "normal_metric", // 13, 14
+					"d", "e", // 15, 16
+					"__name__", "target_info", // 17, 18
+				},
+				Timeseries: []writev2.TimeSeries{
+					// Generating 2 metrics, one have the target_info in the name and the other is a normal gauge.
+					// The target_info metric should be translated to use the resource attributes.
+					// The normal_metric should be translated as usual.
+					{
+						// target_info metric
+						Metadata:   writev2.Metadata{Type: writev2.Metadata_METRIC_TYPE_INFO},
+						LabelsRefs: []uint32{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 17, 18},
+					},
+					{
+						// normal metric
+						Metadata:   writev2.Metadata{Type: writev2.Metadata_METRIC_TYPE_GAUGE},
+						LabelsRefs: []uint32{13, 14, 1, 2, 3, 4, 15, 16},
+						Samples:    []writev2.Sample{{Value: 1, Timestamp: 1}},
+					},
+				},
+			},
+			expectedMetrics: func() pmetric.Metrics {
+				metrics := pmetric.NewMetrics()
+
+				rm := metrics.ResourceMetrics().AppendEmpty()
+				attrs := rm.Resource().Attributes()
+				attrs.PutStr("service.namespace", "production")
+				attrs.PutStr("service.name", "service_a")
+				attrs.PutStr("service.instance.id", "host1")
+				attrs.PutStr("machine.type", "n1-standard-1")
+				attrs.PutStr("cloud.provider", "gcp")
+				attrs.PutStr("region", "us-central1")
+				attrs.PutStr("datacenter", "sdc")
+
+				sm := rm.ScopeMetrics().AppendEmpty()
+				sm.Scope().SetName("OpenTelemetry Collector")
+				sm.Scope().SetVersion("latest")
+
+				m := sm.Metrics().AppendEmpty()
+				m.SetName("normal_metric")
+				m.SetUnit("")
+				m.SetDescription("")
+
+				dp := m.SetEmptyGauge().DataPoints().AppendEmpty()
+				dp.SetDoubleValue(1.0)
+				dp.SetTimestamp(pcommon.Timestamp(1 * int64(time.Millisecond)))
+				dp.Attributes().PutStr("d", "e")
+
+				return metrics
+			}(),
+		},
 	} {
 		t.Run(tc.name, func(t *testing.T) {
-			metrics, stats, err := prwReceiver.translateV2(ctx, tc.request)
+			// since we are using the interRequestCache to store values across requests, we need to clear it after each test, otherwise it will affect the next test
+			prwReceiver.interRequestCache = make(map[uint64]pmetric.ResourceMetrics)
+			metrics, _, err := prwReceiver.translateV2(ctx, tc.request)
 			if tc.expectError != "" {
 				assert.ErrorContains(t, err, tc.expectError)
 				return
 			}
 
 			assert.NoError(t, err)
 			assert.NoError(t, pmetrictest.CompareMetrics(tc.expectedMetrics, metrics))
-			assert.Equal(t, tc.expectedStats, stats)
 		})
 	}
 }
+
+type nonMutatingConsumer struct{}
+
+// Capabilities returns the base consumer capabilities.
+func (bc nonMutatingConsumer) Capabilities() consumer.Capabilities {
+	return consumer.Capabilities{MutatesData: false}
+}
+
+type MockConsumer struct {
+	nonMutatingConsumer
+	mu         sync.Mutex
+	metrics    []pmetric.Metrics
+	dataPoints int
+}
+
+func (m *MockConsumer) ConsumeMetrics(_ context.Context, md pmetric.Metrics) error {
+	m.mu.Lock()
+	defer m.mu.Unlock()
+	m.metrics = append(m.metrics, md)
+	m.dataPoints += md.DataPointCount()
+	return nil
+}
+
+func TestTargetInfoWithMultipleRequests(t *testing.T) {
+	factory := NewFactory()
+	cfg := factory.CreateDefaultConfig()
+	cfg.(*Config).Endpoint = "localhost:9090"
path: receiver/prometheusremotewritereceiver/receiver_test.go
user: login: ArthurSens
body: brilliant!
created_at: 2025-04-23T12:23:09Z
author_association: MEMBER

PR comments:
user: login: github-actions[bot]
created_at: 2025-04-08T05:21:09Z
body: This PR was marked stale due to lack of activity. It will be closed in 14 days.
author_association: CONTRIBUTOR
user: login: perebaj
created_at: 2025-04-21T18:00:41Z
body: @ArthurSens I adapted the tests to use the forked version that [fixes that snappy compression](https://github.com/open-telemetry/opentelemetry-collector/pull/12825). But now I think that we have a different problem. 

How we will validate the multiple requests' behavior? This because, the way that you explain to me that this code should be done, is using the `MockConsumer`. The issue with this approach, is if we use a unit test, we won't be able to have the snappy working, because the middleware it's just initialized when we start the real Server, like you did [here](https://github.com/open-telemetry/opentelemetry-collector-contrib/pull/39446).

Besides that, I think that in the response of the `/api/v1/write` we don't have access to the translated metric. So, how can use validate that the target_info is working between different requests?
author_association: CONTRIBUTOR
user: login: perebaj
created_at: 2025-04-21T22:44:43Z
body: > @ArthurSens I adapted the tests to use the forked version that [fixes that snappy compression](https://github.com/open-telemetry/opentelemetry-collector/pull/12825). But now I think that we have a different problem.
> 
> How we will validate the multiple requests' behavior? This because, the way that you explain to me that this code should be done, is using the `MockConsumer`. The issue with this approach, is if we use a unit test, we won't be able to have the snappy working, because the middleware it's just initialized when we start the real Server, like you did [here](https://github.com/open-telemetry/opentelemetry-collector-contrib/pull/39446).
> 
> Besides that, I think that in the response of the `/api/v1/write` we don't have access to the translated metric. So, how can use validate that the target_info is working between different requests?

The answer was in my front of me the whole time

https://github.com/open-telemetry/opentelemetry-collector-contrib/pull/38812/commits/5a08906d0cf2fde3edf6333c3034c943491d012c#diff-946c2dc5f6b3b6d865be9b13c57625f05b095c373d8dcc960fa4357985903058R519

I can pass the `mockConsumer` for the `factory.CreateMetric` 😊. Test done and working!
author_association: CONTRIBUTOR