Repository: open-telemetry/opentelemetry-operator

Issue:
Operator does not respect new metrics readers, favors deprecated address
### Component(s)

collector

### What happened?

## Description

Although `service::telemetry::metrics::address` field was deprecated in https://github.com/open-telemetry/opentelemetry-collector/pull/11205 starting from `v0.111.0`, an address defaulting was implemented in https://github.com/open-telemetry/opentelemetry-operator/pull/3361.

This can lead to issues:

* When using opentelemetry-operator, with a contrib distro collector, setting the telemetry like this:

```yaml
  telemetry:
    metrics:
      level: detailed
      readers:
        - pull:
            exporter:
              prometheus:
                host: ""
                port: 8888
```

gets omitted completly:

```yaml
      Telemetry:
        Metrics:
          Address:  0.0.0.0:8888
``` 
 
* On our custom otel-collector distribution, where we set readers through our operator, it also sets the deprecated address:

### Before deploying

```yaml
  telemetry:
    metrics:
      level: detailed
      readers:
        - pull:
            exporter:
              prometheus:
                host: ""
                port: 8888
```

### After deploying

```yaml
  telemetry:
    metrics:
      address: 0.0.0.0:8888
      level: detailed
      readers:
        - pull:
            exporter:
              prometheus:
                host: ""
                port: 8888
```

Which can cause the collector to crash:

```sh
Error: failed to create SDK: binding address 0.0.0.0:8888 for Prometheus exporter: listen tcp 0.0.0.0:8888: bind: address already in use
2025/02/20 10:38:05 collector server run finished with error: failed to create SDK: binding address 0.0.0.0:8888 for Prometheus exporter: listen tcp 0.0.0.0:8888: bind: address already in use
stream closed EOF for collector/otelcollector-example-collector-dh7qv (otc-container)
```

## Steps to Reproduce

1. Set a reader in a collector's config as suggested in the deprecation notice: https://github.com/open-telemetry/opentelemetry-collector/pull/11205
2. Deploy the collector.
3. Eventually it will omit the set telemetry configuration or the collector crashes.

## Expected Result

The operator should respect the deprecation and not set a default address, instead it should support and default to setting a reader.

## Actual Result

The operator omits the telemetry config and sets the deprecated default, or it crashes.


### Kubernetes Version

v1.31.2

### Operator version

v0.117.0

### Collector version

v0.120.0

### Environment information

## Environment
OS: Ubuntu 20.04



### Log output

```shell
Error: failed to create SDK: binding address 0.0.0.0:8888 for Prometheus exporter: listen tcp 0.0.0.0:8888: bind: address already in use
2025/02/20 10:38:05 collector server run finished with error: failed to create SDK: binding address 0.0.0.0:8888 for Prometheus exporter: listen tcp 0.0.0.0:8888: bind: address already in use
stream closed EOF for collector/otelcollector-example-collector-dh7qv (otc-container)
```

### Additional context

_No response_

Discussion:
@shuheiktgw: Let me fix this issue 🙏 @iblancasa: > Let me fix this issue 🙏

Go for it.@ChrisMcD1: This also impacts users of the new 

```
telemetry:
    metrics:
      readers:
        - pull:
            exporter:
              prometheus:
                without_type_suffix: false
```
and `without_units` and `without_scope_info`. As far as I can tell there is no way to change those settings without _also_ changing the port, since it will conflict with the default that has already bound to port 8888

Solution patch:
diff --git a/.chloggen/3730.yaml b/.chloggen/3730.yaml
new file mode 100755
index 00000000..43f13172
--- /dev/null
+++ b/.chloggen/3730.yaml
@@ -0,0 +1,16 @@
+# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix'
+change_type: bug_fix
+
+# The name of the component, or a single word describing the area of concern, (e.g. collector, target allocator, auto-instrumentation, opamp, github action)
+component: collector
+
+# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`).
+note: Fix the default configuration for the internal metrics endpoint
+
+# One or more tracking issues related to the change
+issues: [3730]
+
+# (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:
diff --git a/apis/v1beta1/config.go b/apis/v1beta1/config.go
index 2a6a580d..fd84156e 100644
--- a/apis/v1beta1/config.go
+++ b/apis/v1beta1/config.go
@@ -6,16 +6,11 @@ package v1beta1
 import (
 	"bytes"
 	"encoding/json"
-	"errors"
-	"fmt"
-	"reflect"
-	"regexp"
 	"sort"
-	"strconv"
-	"strings"
 
 	"dario.cat/mergo"
 	"github.com/go-logr/logr"
+	otelConfig "go.opentelemetry.io/contrib/otelconf/v0.3.0"
 	"gopkg.in/yaml.v3"
 	corev1 "k8s.io/api/core/v1"
 	rbacv1 "k8s.io/api/rbac/v1"
@@ -398,24 +393,24 @@ func (c *Config) Yaml() (string, error) {
 // Returns null objects in the config.
 func (c *Config) nullObjects() []string {
 	var nullKeys []string
-	if nulls := hasNullValue(c.Receivers.Object); len(nulls) > 0 {
+	if nulls := getNullValuedKeys(c.Receivers.Object); len(nulls) > 0 {
 		nullKeys = append(nullKeys, addPrefix("receivers.", nulls)...)
 	}
-	if nulls := hasNullValue(c.Exporters.Object); len(nulls) > 0 {
+	if nulls := getNullValuedKeys(c.Exporters.Object); len(nulls) > 0 {
 		nullKeys = append(nullKeys, addPrefix("exporters.", nulls)...)
 	}
 	if c.Processors != nil {
-		if nulls := hasNullValue(c.Processors.Object); len(nulls) > 0 {
+		if nulls := getNullValuedKeys(c.Processors.Object); len(nulls) > 0 {
 			nullKeys = append(nullKeys, addPrefix("processors.", nulls)...)
 		}
 	}
 	if c.Extensions != nil {
-		if nulls := hasNullValue(c.Extensions.Object); len(nulls) > 0 {
+		if nulls := getNullValuedKeys(c.Extensions.Object); len(nulls) > 0 {
 			nullKeys = append(nullKeys, addPrefix("extensions.", nulls)...)
 		}
 	}
 	if c.Connectors != nil {
-		if nulls := hasNullValue(c.Connectors.Object); len(nulls) > 0 {
+		if nulls := getNullValuedKeys(c.Connectors.Object); len(nulls) > 0 {
 			nullKeys = append(nullKeys, addPrefix("connectors.", nulls)...)
 		}
 	}
@@ -445,64 +440,48 @@ const (
 // because the port is used to generate Service objects and mappings.
 func (s *Service) MetricsEndpoint(logger logr.Logger) (string, int32, error) {
 	telemetry := s.GetTelemetry()
-	if telemetry == nil || telemetry.Metrics.Address == "" {
+	if telemetry == nil {
 		return defaultServiceHost, defaultServicePort, nil
 	}
 
-	// The regex below matches on strings that end with a colon followed by the environment variable expansion syntax.
-	// So it should match on strings ending with: ":${env:POD_IP}" or ":${POD_IP}".
-	const portEnvVarRegex = `:\${[env:]?.*}$`
-	isPortEnvVar := regexp.MustCompile(portEnvVarRegex).MatchString(telemetry.Metrics.Address)
-	if isPortEnvVar {
-		errMsg := fmt.Sprintf("couldn't determine metrics port from configuration: %s",
-			telemetry.Metrics.Address)
-		logger.Info(errMsg)
-		return "", 0, errors.New(errMsg)
-	}
+	if telemetry.Metrics.Address != "" && len(telemetry.Metrics.Readers) == 0 {
+		host, port, err := parseAddressEndpoint(telemetry.Metrics.Address)
+		if err != nil {
+			return "", 0, err
+		}
 
-	// The regex below matches on strings that end with a colon followed by 1 or more numbers (representing the port).
-	const explicitPortRegex = `:(\d+$)`
-	explicitPortMatches := regexp.MustCompile(explicitPortRegex).FindStringSubmatch(telemetry.Metrics.Address)
-	if len(explicitPortMatches) <= 1 {
-		return telemetry.Metrics.Address, defaultServicePort, nil
+		return host, port, nil
 	}
 
-	port, err := strconv.ParseInt(explicitPortMatches[1], 10, 32)
-	if err != nil {
-		errMsg := fmt.Sprintf("couldn't determine metrics port from configuration: %s",
-			telemetry.Metrics.Address)
-		logger.Info(errMsg, "error", err)
-		return "", 0, err
-	}
-
-	host, _, _ := strings.Cut(telemetry.Metrics.Address, explicitPortMatches[0])
-	return host, int32(port), nil
+	return defaultServiceHost, defaultServicePort, nil
 }
 
 // ApplyDefaults inserts configuration defaults if it has not been set.
 func (s *Service) ApplyDefaults(logger logr.Logger) error {
-	telemetryAddr, telemetryPort, err := s.MetricsEndpoint(logger)
-	if err != nil {
-		return err
-	}
+	tel := s.GetTelemetry()
 
-	tm := &AnyConfig{
-		Object: map[string]interface{}{
-			"metrics": map[string]interface{}{
-				"address": fmt.Sprintf("%s:%d", telemetryAddr, telemetryPort),
-			},
-		},
+	if tel == nil {
+		tel = &Telemetry{}
 	}
 
-	if s.Telemetry == nil {
-		s.Telemetry = tm
+	if tel.Metrics.Address != "" || len(tel.Metrics.Readers) != 0 {
+		// The user already set the address or the readers, so we don't need to do anything
 		return nil
 	}
-	// NOTE: Merge without overwrite. If a telemetry endpoint is specified, the defaulting
-	// respects the configuration and returns an equal value.
-	if err := mergo.Merge(s.Telemetry, tm); err != nil {
-		return fmt.Errorf("telemetry config merge failed: %w", err)
+
+	host, port, err := s.MetricsEndpoint(logger)
+	if err != nil {
+		return err
+	}
+
+	reader := AddPrometheusMetricsEndpoint(host, port)
+	tel.Metrics.Readers = append(tel.Metrics.Readers, reader)
+
+	s.Telemetry, err = tel.ToAnyConfig()
+	if err != nil {
+		return err
 	}
+
 	return nil
 }
 
@@ -517,6 +496,23 @@ type MetricsConfig struct {
 
 	// Address is the [address]:port that metrics exposition should be bound to.
 	Address string `json:"address,omitempty" yaml:"address,omitempty"`
+
+	otelConfig.MeterProvider `mapstructure:",squash"`
+}
+
+func (in *MetricsConfig) DeepCopyInto(out *MetricsConfig) {
+	*out = *in
+	out.MeterProvider = in.MeterProvider
+}
+
+// DeepCopy creates a new deepcopy of MetricsConfig.
+func (in *MetricsConfig) DeepCopy() *MetricsConfig {
+	if in == nil {
+		return nil
+	}
+	out := new(MetricsConfig)
+	in.DeepCopyInto(out)
+	return out
 }
 
 // Telemetry is an intermediary type that allows for easy access to the collector's telemetry settings.
@@ -530,6 +526,38 @@ type Telemetry struct {
 	Resource map[string]*string `json:"resource,omitempty" yaml:"resource,omitempty"`
 }
 
+// ToAnyConfig converts the Telemetry struct to an AnyConfig struct.
+func (t *Telemetry) ToAnyConfig() (*AnyConfig, error) {
+	data, err := json.Marshal(t)
+	if err != nil {
+		return nil, err
+	}
+	var result map[string]interface{}
+	if err := json.Unmarshal(data, &result); err != nil {
+		return nil, err
+	}
+
+	normalizeConfig(result)
+
+	return &AnyConfig{
+		Object: result,
+	}, nil
+}
+
+func AddPrometheusMetricsEndpoint(host string, port int32) otelConfig.MetricReader {
+	portInt := int(port)
+	return otelConfig.MetricReader{
+		Pull: &otelConfig.PullMetricReader{
+			Exporter: otelConfig.PullMetricExporter{
+				Prometheus: &otelConfig.Prometheus{
+					Host: &host,
+					Port: &portInt,
+				},
+			},
+		},
+	}
+}
+
 // GetTelemetry serves as a helper function to access the fields we care about in the underlying telemetry struct.
 // This exists to avoid needing to worry extra fields in the telemetry struct.
 func (s *Service) GetTelemetry() *Telemetry {
@@ -548,32 +576,3 @@ func (s *Service) GetTelemetry() *Telemetry {
 	}
 	return t
 }
-
-func hasNullValue(cfg map[string]interface{}) []string {
-	var nullKeys []string
-	for k, v := range cfg {
-		if v == nil {
-			nullKeys = append(nullKeys, fmt.Sprintf("%s:", k))
-		}
-		if reflect.ValueOf(v).Kind() == reflect.Map {
-			var nulls []string
-			val, ok := v.(map[string]interface{})
-			if ok {
-				nulls = hasNullValue(val)
-			}
-			if len(nulls) > 0 {
-				prefixed := addPrefix(k+".", nulls)
-				nullKeys = append(nullKeys, prefixed...)
-			}
-		}
-	}
-	return nullKeys
-}
-
-func addPrefix(prefix string, arr []string) []string {
-	var prefixed []string
-	for _, v := range arr {
-		prefixed = append(prefixed, fmt.Sprintf("%s%s", prefix, v))
-	}
-	return prefixed
-}
diff --git a/apis/v1beta1/helpers.go b/apis/v1beta1/helpers.go
new file mode 100644
index 00000000..8e3da930
--- /dev/null
+++ b/apis/v1beta1/helpers.go
@@ -0,0 +1,121 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+package v1beta1
+
+import (
+	"errors"
+	"fmt"
+	"math"
+	"reflect"
+	"regexp"
+	"strconv"
+	"strings"
+)
+
+// parseAddressEndpoint parses the address and returns the host and port.
+// If the address is an environment variable, it returns the default port.
+// If the address is an explicit port, it returns the port.
+// If the address is not valid, it returns an error.
+func parseAddressEndpoint(address string) (string, int32, error) {
+	// The regex below matches on strings that end with a colon followed by the environment variable expansion syntax.
+	// So it should match on strings ending with: ":${env:POD_IP}" or ":${POD_IP}".
+	const portEnvVarRegex = `:\${[env:]?.*}$`
+	isPortEnvVar := regexp.MustCompile(portEnvVarRegex).MatchString(address)
+	if isPortEnvVar {
+		errMsg := fmt.Sprintf("couldn't determine metrics port from configuration: %s", address)
+		return "", 0, errors.New(errMsg)
+	}
+
+	// The regex below matches on strings that end with a colon followed by 1 or more numbers (representing the port).
+	const explicitPortRegex = `:(\d+$)`
+	explicitPortMatches := regexp.MustCompile(explicitPortRegex).FindStringSubmatch(address)
+	if len(explicitPortMatches) <= 1 {
+		return address, defaultServicePort, nil
+	}
+
+	port, err := strconv.ParseInt(explicitPortMatches[1], 10, 32)
+	if err != nil {
+		errMsg := fmt.Sprintf("couldn't determine metrics port from configuration: %s", address)
+		return "", 0, errors.New(errMsg)
+	}
+
+	host, _, _ := strings.Cut(address, explicitPortMatches[0])
+	return host, intToInt32Safe(int(port)), nil
+}
+
+// addPrefix adds a prefix to each element of the array.
+func addPrefix(prefix string, arr []string) []string {
+	if len(arr) == 0 {
+		return []string{}
+	}
+	var prefixed []string
+	for _, v := range arr {
+		prefixed = append(prefixed, fmt.Sprintf("%s%s", prefix, v))
+	}
+	return prefixed
+}
+
+// intToInt32Safe converts an int to an int32.
+func intToInt32Safe(v int) int32 {
+	if v > math.MaxInt32 {
+		return math.MaxInt32
+	}
+	if v < math.MinInt32 {
+		return math.MinInt32
+	}
+	return int32(v)
+}
+
+// getNullValuedKeys returns keys from the input map whose values are nil. Keys from nested maps are prefixed
+// by the name of the parent key, using a dot notation.
+func getNullValuedKeys(cfg map[string]interface{}) []string {
+	var nullKeys []string
+	for k, v := range cfg {
+		if v == nil {
+			nullKeys = append(nullKeys, fmt.Sprintf("%s:", k))
+		}
+		if reflect.ValueOf(v).Kind() == reflect.Map {
+			var nulls []string
+			val, ok := v.(map[string]interface{})
+			if ok {
+				nulls = getNullValuedKeys(val)
+			}
+			if len(nulls) > 0 {
+				prefixed := addPrefix(k+".", nulls)
+				nullKeys = append(nullKeys, prefixed...)
+			}
+		}
+	}
+	return nullKeys
+}
+
+// normalizeConfig fixes the config to be valid for the collector.
+// It removes nil values, converts float64 to int32.
+func normalizeConfig(m map[string]interface{}) {
+	for k, v := range m {
+		switch val := v.(type) {
+		case nil:
+			// We remove those fields which value is nil. This prevents issues when
+			// unmarshalling the config in the collector
+			delete(m, k)
+		case map[string]interface{}:
+			normalizeConfig(val)
+		case []interface{}:
+			for i, item := range val {
+				if item == nil {
+					val[i] = map[string]interface{}{}
+				} else if sub, ok := item.(map[string]interface{}); ok {
+					normalizeConfig(sub)
+				}
+			}
+		case float64:
+			// All numbers (even int32, int64, etc.) are parsed as float64 by default
+			// We need to convert them to the correct type
+			if k == "port" {
+				m[k] = int32(val)
+			}
+		default:
+		}
+	}
+}
diff --git a/apis/v1beta1/zz_generated.deepcopy.go b/apis/v1beta1/zz_generated.deepcopy.go
index 8b01b5c8..649dda72 100644
--- a/apis/v1beta1/zz_generated.deepcopy.go
+++ b/apis/v1beta1/zz_generated.deepcopy.go
@@ -164,21 +164,6 @@ func (in *MetricSpec) DeepCopy() *MetricSpec {
 	return out
 }
 
-// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
-func (in *MetricsConfig) DeepCopyInto(out *MetricsConfig) {
-	*out = *in
-}
-
-// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MetricsConfig.
-func (in *MetricsConfig) DeepCopy() *MetricsConfig {
-	if in == nil {
-		return nil
-	}
-	out := new(MetricsConfig)
-	in.DeepCopyInto(out)
-	return out
-}
-
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *MetricsConfigSpec) DeepCopyInto(out *MetricsConfigSpec) {
 	*out = *in
@@ -816,7 +801,7 @@ func (in *TargetAllocatorPrometheusCR) DeepCopy() *TargetAllocatorPrometheusCR {
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *Telemetry) DeepCopyInto(out *Telemetry) {
 	*out = *in
-	out.Metrics = in.Metrics
+	in.Metrics.DeepCopyInto(&out.Metrics)
 	if in.Resource != nil {
 		in, out := &in.Resource, &out.Resource
 		*out = make(map[string]*string, len(*in))
diff --git a/go.mod b/go.mod
index 57bf1648..9169904d 100644
--- a/go.mod
+++ b/go.mod
@@ -153,7 +153,7 @@ require (
 	github.com/jmespath/go-jmespath v0.4.1-0.20220621161143-b0104c826a24 // indirect
 	github.com/josharian/intern v1.0.0 // indirect
 	github.com/jpillora/backoff v1.0.0 // indirect
-	github.com/klauspost/compress v1.17.11 // indirect
+	github.com/klauspost/compress v1.18.0 // indirect
 	github.com/klauspost/cpuid/v2 v2.2.8 // indirect
 	github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b // indirect
 	github.com/kylelemons/godebug v1.1.0 // indirect
@@ -194,10 +194,10 @@ require (
 	github.com/ugorji/go/codec v1.2.12 // indirect
 	github.com/vultr/govultr/v2 v2.17.2 // indirect
 	github.com/x448/float16 v0.8.4 // indirect
-	github.com/yusufpapurcu/wmi v1.2.3 // indirect
+	github.com/yusufpapurcu/wmi v1.2.4 // indirect
 	go.mongodb.org/mongo-driver v1.17.2 // indirect
 	go.opentelemetry.io/auto/sdk v1.1.0 // indirect
-	go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0 // indirect
+	go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect
 	go.opentelemetry.io/otel/trace v1.35.0 // indirect
 	go.opentelemetry.io/proto/otlp v1.5.0 // indirect
 	go.uber.org/atomic v1.11.0 // indirect
@@ -218,7 +218,7 @@ require (
 	google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb // indirect
 	google.golang.org/genproto/googleapis/rpc v0.0.0-20250227231956-55c901821b1e // indirect
 	google.golang.org/grpc v1.71.0 // indirect
-	google.golang.org/protobuf v1.36.5 // indirect
+	google.golang.org/protobuf v1.36.6 // indirect
 	gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
 	gopkg.in/inf.v0 v0.9.1 // indirect
 	gopkg.in/ini.v1 v1.67.0 // indirect
@@ -227,8 +227,22 @@ require (
 	sigs.k8s.io/structured-merge-diff/v4 v4.5.0 // indirect
 )
 
+require go.opentelemetry.io/contrib/otelconf v0.15.0
+
 require (
 	github.com/envoyproxy/go-control-plane/envoy v1.32.4 // indirect
+	github.com/golang/snappy v1.0.0 // indirect
 	github.com/google/btree v1.1.3 // indirect
 	github.com/prometheus/sigv4 v0.1.0 // indirect
+	go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.11.0 // indirect
+	go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.11.0 // indirect
+	go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.35.0 // indirect
+	go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 // indirect
+	go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0 // indirect
+	go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0 // indirect
+	go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.11.0 // indirect
+	go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.35.0 // indirect
+	go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.35.0 // indirect
+	go.opentelemetry.io/otel/log v0.11.0 // indirect
+	go.opentelemetry.io/otel/sdk/log v0.11.0 // indirect
 )
diff --git a/go.sum b/go.sum
index 39a7b88d..8625f9fd 100644
--- a/go.sum
+++ b/go.sum
@@ -206,8 +206,8 @@ github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y
 github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
 github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
 github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
-github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
-github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
+github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs=
+github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
 github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
 github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=
 github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
@@ -320,8 +320,8 @@ github.com/keybase/go-keychain v0.0.0-20231219164618-57a3676c3af6 h1:IsMZxCuZqKu
 github.com/keybase/go-keychain v0.0.0-20231219164618-57a3676c3af6/go.mod h1:3VeWNIJaW+O5xpRQbPp0Ybqu1vJd/pm7s2F473HRrkw=
 github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
 github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
-github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
-github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
+github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
+github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
 github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
 github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM=
 github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
@@ -527,30 +527,50 @@ github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
 github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
 github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
-github.com/yusufpapurcu/wmi v1.2.3 h1:E1ctvB7uKFMOJw3fdOW32DwGE9I7t++CRUEMKvFoFiw=
-github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
+github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
+github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
 go.mongodb.org/mongo-driver v1.17.2 h1:gvZyk8352qSfzyZ2UMWcpDpMSGEr1eqE4T793SqyhzM=
 go.mongodb.org/mongo-driver v1.17.2/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ=
 go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
 go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
 go.opentelemetry.io/collector/featuregate v1.29.0 h1:DTDeaokT53AA+SiYBH8GC893cmppThU65b3XyFfLPWw=
 go.opentelemetry.io/collector/featuregate v1.29.0/go.mod h1:Y/KsHbvREENKvvN9RlpiWk/IGBK+CATBYzIIpU7nccc=
-go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0 h1:CV7UdSGJt/Ao6Gp4CXckLxVRRsRgDHoI8XjbL3PDl8s=
-go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0/go.mod h1:FRmFuRJfag1IZ2dPkHnEoSFVgTVPUd2qf5Vi69hLb8I=
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU=
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ=
+go.opentelemetry.io/contrib/otelconf v0.15.0 h1:BLNiIUsrNcqhSKpsa6CnhE6LdrpY1A8X0szMVsu99eo=
+go.opentelemetry.io/contrib/otelconf v0.15.0/go.mod h1:OPH1seO5z9dp1P26gnLtoM9ht7JDvh3Ws6XRHuXqImY=
 go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ=
 go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y=
+go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.11.0 h1:HMUytBT3uGhPKYY/u/G5MR9itrlSO2SMOsSD3Tk3k7A=
+go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.11.0/go.mod h1:hdDXsiNLmdW/9BF2jQpnHHlhFajpWCEYfM6e5m2OAZg=
+go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.11.0 h1:C/Wi2F8wEmbxJ9Kuzw/nhP+Z9XaHYMkyDmXy6yR2cjw=
+go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.11.0/go.mod h1:0Lr9vmGKzadCTgsiBydxr6GEZ8SsZ7Ks53LzjWG5Ar4=
+go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.35.0 h1:QcFwRrZLc82r8wODjvyCbP7Ifp3UANaBSmhDSFjnqSc=
+go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.35.0/go.mod h1:CXIWhUomyWBG/oY2/r/kLp6K/cmx9e/7DLpBuuGdLCA=
 go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.35.0 h1:0NIXxOCFx+SKbhCVxwl3ETG8ClLPAa0KuKV6p3yhxP8=
 go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.35.0/go.mod h1:ChZSJbbfbl/DcRZNc9Gqh6DYGlfjw4PvO1pEOZH1ZsE=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0 h1:Vh5HayB/0HHfOQA7Ctx69E/Y/DcQSMPpKANYVMQ7fBA=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0/go.mod h1:cpgtDBaqD/6ok/UG0jT15/uKjAY8mRA53diogHBg3UI=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.33.0 h1:wpMfgF8E1rkrT1Z6meFh1NDtownE9Ii3n3X2GJYjsaU=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.33.0/go.mod h1:wAy0T/dUbs468uOlkT31xjvqQgEVXv58BRFWEgn5v/0=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 h1:1fTNlAIJZGWLP5FVu0fikVry1IsiUnXjf7QFvoNN3Xw=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0/go.mod h1:zjPK58DtkqQFn+YUMbx0M2XV3QgKU0gS9LeGohREyK4=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0 h1:m639+BofXTvcY1q8CGs4ItwQarYtJPOWmVobfM1HpVI=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0/go.mod h1:LjReUci/F4BUyv+y4dwnq3h/26iNOeC3wAIqgvTIZVo=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0 h1:xJ2qHD0C1BeYVTLLR9sX12+Qb95kfeD/byKj6Ky1pXg=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0/go.mod h1:u5BF1xyjstDowA1R5QAO9JHzqK+ublenEW/dyqTjBVk=
 go.opentelemetry.io/otel/exporters/prometheus v0.57.0 h1:AHh/lAP1BHrY5gBwk8ncc25FXWm/gmmY3BX258z5nuk=
 go.opentelemetry.io/otel/exporters/prometheus v0.57.0/go.mod h1:QpFWz1QxqevfjwzYdbMb4Y1NnlJvqSGwyuU0B4iuc9c=
+go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.11.0 h1:k6KdfZk72tVW/QVZf60xlDziDvYAePj5QHwoQvrB2m8=
+go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.11.0/go.mod h1:5Y3ZJLqzi/x/kYtrSrPSx7TFI/SGsL7q2kME027tH6I=
+go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.35.0 h1:PB3Zrjs1sG1GBX51SXyTSoOTqcDglmsk7nT6tkKPb/k=
+go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.35.0/go.mod h1:U2R3XyVPzn0WX7wOIypPuptulsMcPDPs/oiSVOMVnHY=
+go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.35.0 h1:T0Ec2E+3YZf5bgTNQVet8iTDW7oIk03tXHq+wkwIDnE=
+go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.35.0/go.mod h1:30v2gqH+vYGJsesLWFov8u47EpYTcIQcBjKpI6pJThg=
+go.opentelemetry.io/otel/log v0.11.0 h1:c24Hrlk5WJ8JWcwbQxdBqxZdOK7PcP/LFtOtwpDTe3Y=
+go.opentelemetry.io/otel/log v0.11.0/go.mod h1:U/sxQ83FPmT29trrifhQg+Zj2lo1/IPN1PF6RTFqdwc=
 go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M=
 go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE=
 go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY=
 go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg=
+go.opentelemetry.io/otel/sdk/log v0.11.0 h1:7bAOpjpGglWhdEzP8z0VXc4jObOiDEwr3IYbhBnjk2c=
+go.opentelemetry.io/otel/sdk/log v0.11.0/go.mod h1:dndLTxZbwBstZoqsJB3kGsRPkpAgaJrWfQg3lhlHFFY=
 go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o=
 go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w=
 go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs=
@@ -667,8 +687,8 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20250227231956-55c901821b1e h1:
 google.golang.org/genproto/googleapis/rpc v0.0.0-20250227231956-55c901821b1e/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I=
 google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg=
 google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec=
-google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
-google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
+google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
+google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
 gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
diff --git a/pkg/collector/upgrade/v0_111_0.go b/pkg/collector/upgrade/v0_111_0.go
index ce42ee55..0d96a4e6 100644
--- a/pkg/collector/upgrade/v0_111_0.go
+++ b/pkg/collector/upgrade/v0_111_0.go
@@ -4,9 +4,41 @@
 package upgrade
 
 import (
+	"fmt"
+
+	"dario.cat/mergo"
+	"github.com/go-logr/logr"
+
 	"github.com/open-telemetry/opentelemetry-operator/apis/v1beta1"
 )
 
 func upgrade0_111_0(u VersionUpgrade, otelcol *v1beta1.OpenTelemetryCollector) (*v1beta1.OpenTelemetryCollector, error) { //nolint:unparam
-	return otelcol, otelcol.Spec.Config.Service.ApplyDefaults(u.Log)
+
+	return otelcol, applyDefaults(otelcol, u.Log)
+}
+
+func applyDefaults(otelcol *v1beta1.OpenTelemetryCollector, logger logr.Logger) error {
+	telemetryAddr, telemetryPort, err := otelcol.Spec.Config.Service.MetricsEndpoint(logger)
+	if err != nil {
+		return err
+	}
+
+	tm := &v1beta1.AnyConfig{
+		Object: map[string]interface{}{
+			"metrics": map[string]interface{}{
+				"address": fmt.Sprintf("%s:%d", telemetryAddr, telemetryPort),
+			},
+		},
+	}
+
+	if otelcol.Spec.Config.Service.Telemetry == nil {
+		otelcol.Spec.Config.Service.Telemetry = tm
+		return nil
+	}
+	// NOTE: Merge without overwrite. If a telemetry endpoint is specified, the defaulting
+	// respects the configuration and returns an equal value.
+	if err := mergo.Merge(otelcol.Spec.Config.Service.Telemetry, tm); err != nil {
+		return fmt.Errorf("telemetry config merge failed: %w", err)
+	}
+	return nil
 }
diff --git a/pkg/collector/upgrade/v0_122_0.go b/pkg/collector/upgrade/v0_122_0.go
new file mode 100644
index 00000000..fd9f45d2
--- /dev/null
+++ b/pkg/collector/upgrade/v0_122_0.go
@@ -0,0 +1,38 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+package upgrade
+
+import (
+	"github.com/open-telemetry/opentelemetry-operator/apis/v1beta1"
+)
+
+func upgrade0_122_0(u VersionUpgrade, otelcol *v1beta1.OpenTelemetryCollector) (*v1beta1.OpenTelemetryCollector, error) { //nolint:unparam
+	tel := otelcol.Spec.Config.Service.GetTelemetry()
+
+	if tel == nil || tel.Metrics.Address == "" {
+		return otelcol, nil
+	}
+
+	host, port, err := otelcol.Spec.Config.Service.MetricsEndpoint(u.Log)
+	if err != nil {
+		return otelcol, err
+	}
+
+	// service.telemetry.metrics.address is deprecated and should not be used anymore.
+	// Setting the "address" field to an empty string explicitly removes the value
+	// during Kubernetes serialization. Directly deleting the field from the map using
+	// delete(metrics, "address") does not work because Kubernetes treats missing fields
+	// differently from explicitly empty ones. By assigning "", we ensure the configuration
+	// is updated correctly when the resource is persisted.
+	tel.Metrics.Address = ""
+	reader := v1beta1.AddPrometheusMetricsEndpoint(host, port)
+	tel.Metrics.Readers = append(tel.Metrics.Readers, reader)
+
+	otelcol.Spec.Config.Service.Telemetry, err = tel.ToAnyConfig()
+	if err != nil {
+		return otelcol, err
+	}
+
+	return otelcol, nil
+}
diff --git a/pkg/collector/upgrade/versions.go b/pkg/collector/upgrade/versions.go
index efc686c3..71069fa9 100644
--- a/pkg/collector/upgrade/versions.go
+++ b/pkg/collector/upgrade/versions.go
@@ -99,6 +99,10 @@ var (
 			Version:        *semver.MustParse("0.111.0"),
 			upgradeV1beta1: upgrade0_111_0,
 		},
+		{
+			Version:        *semver.MustParse("0.122.0"),
+			upgradeV1beta1: upgrade0_122_0,
+		},
 	}
 
 	// Latest represents the latest version that we need to upgrade. This is not necessarily the latest known version.



Test patch:
diff --git a/apis/v1beta1/collector_webhook_test.go b/apis/v1beta1/collector_webhook_test.go
index 8d482aa8..1b1d1d87 100644
--- a/apis/v1beta1/collector_webhook_test.go
+++ b/apis/v1beta1/collector_webhook_test.go
@@ -11,6 +11,7 @@ import (
 	"testing"
 
 	"github.com/go-logr/logr"
+	"github.com/google/go-cmp/cmp"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 	"gopkg.in/yaml.v3"
@@ -158,9 +159,11 @@ func TestCollectorDefaultingWebhook(t *testing.T) {
 					Mode:            v1beta1.ModeDeployment,
 					UpgradeStrategy: v1beta1.UpgradeStrategyAutomatic,
 					Config: func() v1beta1.Config {
-						const input = `{"receivers":{"otlp":{"protocols":{"grpc":{"endpoint":"0.0.0.0:4317"},"http":{"endpoint":"0.0.0.0:4318"}}}},"exporters":{"debug":null},"service":{"telemetry":{"metrics":{"address":"0.0.0.0:8888"}},"pipelines":{"traces":{"receivers":["otlp"],"exporters":["debug"]}}}}`
+						const input = `{"receivers":{"otlp":{"protocols":{"grpc":{"endpoint":"0.0.0.0:4317"},"http":{"endpoint":"0.0.0.0:4318"}}}},"exporters":{"debug":null},"service":{"telemetry":{"metrics":{"readers":[{"pull":{"exporter":{"prometheus":{"host":"0.0.0.0","port":8888}}}}]}},"pipelines":{"traces":{"receivers":["otlp"],"exporters":["debug"]}}}}`
 						var cfg v1beta1.Config
 						require.NoError(t, yaml.Unmarshal([]byte(input), &cfg))
+						// This is a workaround to avoid the type mismatch because how go-yaml unmarshals
+						cfg.Service.Telemetry.Object["metrics"].(map[string]interface{})["readers"].([]interface{})[0].(map[string]interface{})["pull"].(map[string]interface{})["exporter"].(map[string]interface{})["prometheus"].(map[string]interface{})["port"] = int32(8888)
 						return cfg
 					}(),
 				},
@@ -171,7 +174,7 @@ func TestCollectorDefaultingWebhook(t *testing.T) {
 			otelcol: v1beta1.OpenTelemetryCollector{
 				Spec: v1beta1.OpenTelemetryCollectorSpec{
 					Config: func() v1beta1.Config {
-						const input = `{"receivers":{"otlp":{"protocols":{"grpc":{"headers":{"example":"another"}},"http":{"endpoint":"0.0.0.0:4000"}}}},"exporters":{"debug":null},"service":{"telemetry":{"metrics":{"address":"1.2.3.4:7654"}},"pipelines":{"traces":{"receivers":["otlp"],"exporters":["debug"]}}}}`
+						const input = `{"receivers":{"otlp":{"protocols":{"grpc":{"headers":{"example":"another"}},"http":{"endpoint":"0.0.0.0:4000"}}}},"exporters":{"debug":null},"service":{"telemetry":{"metrics":{"readers":[{"pull":{"exporter":{"prometheus":{"host":"localhost","port":9999}}}}]}},"pipelines":{"traces":{"receivers":["otlp"],"exporters":["debug"]}}}}`
 						var cfg v1beta1.Config
 						require.NoError(t, yaml.Unmarshal([]byte(input), &cfg))
 						return cfg
@@ -190,7 +193,7 @@ func TestCollectorDefaultingWebhook(t *testing.T) {
 					Mode:            v1beta1.ModeDeployment,
 					UpgradeStrategy: v1beta1.UpgradeStrategyAutomatic,
 					Config: func() v1beta1.Config {
-						const input = `{"receivers":{"otlp":{"protocols":{"grpc":{"endpoint":"0.0.0.0:4317","headers":{"example":"another"}},"http":{"endpoint":"0.0.0.0:4000"}}}},"exporters":{"debug":null},"service":{"telemetry":{"metrics":{"address":"1.2.3.4:7654"}},"pipelines":{"traces":{"receivers":["otlp"],"exporters":["debug"]}}}}`
+						const input = `{"receivers":{"otlp":{"protocols":{"grpc":{"endpoint":"0.0.0.0:4317","headers":{"example":"another"}},"http":{"endpoint":"0.0.0.0:4000"}}}},"exporters":{"debug":null},"service":{"telemetry":{"metrics":{"readers":[{"pull":{"exporter":{"prometheus":{"host":"localhost","port":9999}}}}]}},"pipelines":{"traces":{"receivers":["otlp"],"exporters":["debug"]}}}}`
 						var cfg v1beta1.Config
 						require.NoError(t, yaml.Unmarshal([]byte(input), &cfg))
 						return cfg
@@ -547,7 +550,9 @@ func TestCollectorDefaultingWebhook(t *testing.T) {
 				assert.NoError(t, test.expected.Spec.Config.Service.ApplyDefaults(logr.Discard()), "could not apply defaults")
 			}
 			assert.NoError(t, err)
-			assert.Equal(t, test.expected, test.otelcol)
+			if diff := cmp.Diff(test.expected, test.otelcol); diff != "" {
+				t.Errorf("v1beta1.OpenTelemetryCollector mismatch (-want +got):\n%s", diff)
+			}
 		})
 	}
 }
@@ -582,7 +587,18 @@ func TestOTELColValidatingWebhook(t *testing.T) {
 			Telemetry: &v1beta1.AnyConfig{
 				Object: map[string]interface{}{
 					"metrics": map[string]interface{}{
-						"address": "${env:POD_ID}:8888",
+						"readers": []map[string]interface{}{
+							{
+								"pull": map[string]interface{}{
+									"exporter": map[string]interface{}{
+										"prometheus": map[string]interface{}{
+											"host": "${env:POD_ID}",
+											"port": int32(8888),
+										},
+									},
+								},
+							},
+						},
 					},
 				},
 			},
diff --git a/apis/v1beta1/helpers_test.go b/apis/v1beta1/helpers_test.go
new file mode 100644
index 00000000..d7e865bc
--- /dev/null
+++ b/apis/v1beta1/helpers_test.go
@@ -0,0 +1,272 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+package v1beta1
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/require"
+)
+
+func TestParseAddressEndpoint(t *testing.T) {
+	tests := []struct {
+		name          string
+		address       string
+		expectedHost  string
+		expectedPort  int32
+		expectedError bool
+	}{
+		{
+			name:          "valid address with port",
+			address:       "localhost:8080",
+			expectedHost:  "localhost",
+			expectedPort:  8080,
+			expectedError: false,
+		},
+		{
+			name:          "valid address with port and path",
+			address:       "localhost:8080/metrics",
+			expectedHost:  "localhost:8080/metrics",
+			expectedPort:  defaultServicePort,
+			expectedError: false,
+		},
+		{
+			name:          "address without port",
+			address:       "localhost",
+			expectedHost:  "localhost",
+			expectedPort:  defaultServicePort,
+			expectedError: false,
+		},
+		{
+			name:          "address with environment variable port",
+			address:       "localhost:${POD_IP}",
+			expectedHost:  "",
+			expectedPort:  0,
+			expectedError: true,
+		},
+		{
+			name:          "address with environment variable port with env prefix",
+			address:       "localhost:${env:POD_IP}",
+			expectedHost:  "",
+			expectedPort:  0,
+			expectedError: true,
+		},
+		{
+			name:          "empty address",
+			address:       "",
+			expectedHost:  "",
+			expectedPort:  defaultServicePort,
+			expectedError: false,
+		},
+		{
+			name:          "invalid port",
+			address:       "localhost:invalid",
+			expectedHost:  "localhost:invalid",
+			expectedPort:  defaultServicePort,
+			expectedError: false,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			host, port, err := parseAddressEndpoint(tt.address)
+
+			if tt.expectedError {
+				require.Error(t, err)
+			} else {
+				require.NoError(t, err)
+				require.Equal(t, host, tt.expectedHost)
+				require.Equal(t, port, tt.expectedPort)
+			}
+		})
+	}
+}
+
+func TestAddPrefix(t *testing.T) {
+	tests := []struct {
+		name     string
+		prefix   string
+		arr      []string
+		expected []string
+	}{
+		{
+			name:     "empty array",
+			prefix:   "prefix-",
+			arr:      []string{},
+			expected: []string{},
+		},
+		{
+			name:     "single element",
+			prefix:   "prefix-",
+			arr:      []string{"item"},
+			expected: []string{"prefix-item"},
+		},
+		{
+			name:     "multiple elements",
+			prefix:   "prefix-",
+			arr:      []string{"item1", "item2", "item3"},
+			expected: []string{"prefix-item1", "prefix-item2", "prefix-item3"},
+		},
+		{
+			name:     "empty prefix",
+			prefix:   "",
+			arr:      []string{"item1", "item2"},
+			expected: []string{"item1", "item2"},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			result := addPrefix(tt.prefix, tt.arr)
+			require.Equal(t, result, tt.expected)
+		})
+	}
+}
+
+func TestGetNullValue(t *testing.T) {
+	tests := []struct {
+		name     string
+		cfg      map[string]interface{}
+		expected []string
+	}{
+		{
+			name:     "empty map",
+			cfg:      map[string]interface{}{},
+			expected: []string{},
+		},
+		{
+			name: "single null value",
+			cfg: map[string]interface{}{
+				"key": nil,
+			},
+			expected: []string{"key:"},
+		},
+		{
+			name: "multiple null values",
+			cfg: map[string]interface{}{
+				"key1": nil,
+				"key2": nil,
+			},
+			expected: []string{"key1:", "key2:"},
+		},
+		{
+			name: "nested null values",
+			cfg: map[string]interface{}{
+				"parent": map[string]interface{}{
+					"child": nil,
+				},
+			},
+			expected: []string{"parent.child:"},
+		},
+		{
+			name: "deeply nested null values",
+			cfg: map[string]interface{}{
+				"parent": map[string]interface{}{
+					"child": map[string]interface{}{
+						"grandchild": nil,
+					},
+				},
+			},
+			expected: []string{"parent.child.grandchild:"},
+		},
+		{
+			name: "mixed null and non-null values",
+			cfg: map[string]interface{}{
+				"key1": nil,
+				"key2": "value",
+				"key3": map[string]interface{}{
+					"child1": nil,
+					"child2": "value",
+				},
+			},
+			expected: []string{"key1:", "key3.child1:"},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			result := getNullValuedKeys(tt.cfg)
+			require.Equal(t, len(result), len(tt.expected))
+
+			for _, expected := range tt.expected {
+				found := false
+				for _, actual := range result {
+					if actual == expected {
+						found = true
+						break
+					}
+				}
+				require.True(t, found, "getNullValuedKeys() missing expected value: %s", expected)
+			}
+		})
+	}
+}
+
+func TestNormalizeConfig(t *testing.T) {
+	tests := []struct {
+		name     string
+		input    map[string]interface{}
+		expected map[string]interface{}
+	}{
+		{
+			name: "remove nil values",
+			input: map[string]interface{}{
+				"key1": "value1",
+				"key2": nil,
+			},
+			expected: map[string]interface{}{
+				"key1": "value1",
+			},
+		},
+		{
+			name: "convert port float64 to int32",
+			input: map[string]interface{}{
+				"port": float64(8080),
+			},
+			expected: map[string]interface{}{
+				"port": int32(8080),
+			},
+		},
+		{
+			name: "normalize nested map",
+			input: map[string]interface{}{
+				"parent": map[string]interface{}{
+					"child": nil,
+					"port":  float64(8080),
+				},
+			},
+			expected: map[string]interface{}{
+				"parent": map[string]interface{}{
+					"port": int32(8080),
+				},
+			},
+		},
+		{
+			name: "normalize array with nil values",
+			input: map[string]interface{}{
+				"items": []interface{}{
+					nil,
+					map[string]interface{}{
+						"key": "value",
+					},
+				},
+			},
+			expected: map[string]interface{}{
+				"items": []interface{}{
+					map[string]interface{}{},
+					map[string]interface{}{
+						"key": "value",
+					},
+				},
+			},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			normalizeConfig(tt.input)
+			require.Equal(t, tt.input, tt.expected)
+		})
+	}
+}
diff --git a/pkg/collector/upgrade/upgrade_test.go b/pkg/collector/upgrade/upgrade_test.go
index 89504013..ef4e3e00 100644
--- a/pkg/collector/upgrade/upgrade_test.go
+++ b/pkg/collector/upgrade/upgrade_test.go
@@ -130,7 +130,7 @@ func TestEnvVarUpdates(t *testing.T) {
 	require.Equal(t, collectorInstance.Status.Version, persisted.Status.Version)
 
 	currentV := version.Get()
-	currentV.OpenTelemetryCollector = "0.111.0"
+	currentV.OpenTelemetryCollector = "0.122.0"
 	up := &upgrade.VersionUpgrade{
 		Log:      logger,
 		Version:  currentV,
diff --git a/pkg/collector/upgrade/v0_122_0_test.go b/pkg/collector/upgrade/v0_122_0_test.go
new file mode 100644
index 00000000..ecb7cc84
--- /dev/null
+++ b/pkg/collector/upgrade/v0_122_0_test.go
@@ -0,0 +1,343 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+package upgrade
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	logf "sigs.k8s.io/controller-runtime/pkg/log"
+
+	"github.com/open-telemetry/opentelemetry-operator/apis/v1beta1"
+)
+
+var logger = logf.Log.WithName("unit-tests")
+
+func TestUpgrade0_122_0(t *testing.T) {
+	tests := []struct {
+		name           string
+		initialConfig  *v1beta1.OpenTelemetryCollector
+		expectedConfig *v1beta1.OpenTelemetryCollector
+	}{
+		{
+			name: "should remove address field from metrics config",
+			initialConfig: &v1beta1.OpenTelemetryCollector{
+				TypeMeta: metav1.TypeMeta{
+					Kind:       "OpenTelemetryCollector",
+					APIVersion: "v1beta1",
+				},
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "test-collector",
+					Namespace: "default",
+				},
+				Spec: v1beta1.OpenTelemetryCollectorSpec{
+					Config: v1beta1.Config{
+						Service: v1beta1.Service{
+							Telemetry: &v1beta1.AnyConfig{
+								Object: map[string]interface{}{
+									"metrics": map[string]interface{}{
+										"address": "0.0.0.0:8888",
+									},
+								},
+							},
+						},
+					},
+				},
+			},
+			expectedConfig: &v1beta1.OpenTelemetryCollector{
+				TypeMeta: metav1.TypeMeta{
+					Kind:       "OpenTelemetryCollector",
+					APIVersion: "v1beta1",
+				},
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "test-collector",
+					Namespace: "default",
+				},
+				Spec: v1beta1.OpenTelemetryCollectorSpec{
+					Config: v1beta1.Config{
+						Service: v1beta1.Service{
+							Telemetry: &v1beta1.AnyConfig{
+								Object: map[string]interface{}{
+									"metrics": map[string]interface{}{
+										"readers": []interface{}{
+											map[string]interface{}{
+												"pull": map[string]interface{}{
+													"exporter": map[string]interface{}{
+														"prometheus": map[string]interface{}{
+															"host": "0.0.0.0",
+															"port": int32(8888),
+														},
+													},
+												},
+											},
+										},
+									},
+								},
+							},
+						},
+					},
+				},
+			},
+		},
+		{
+			name: "should not modify config when metrics address is empty",
+			initialConfig: &v1beta1.OpenTelemetryCollector{
+				TypeMeta: metav1.TypeMeta{
+					Kind:       "OpenTelemetryCollector",
+					APIVersion: "v1beta1",
+				},
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "test-collector",
+					Namespace: "default",
+				},
+				Spec: v1beta1.OpenTelemetryCollectorSpec{
+					Config: v1beta1.Config{
+						Service: v1beta1.Service{
+							Telemetry: &v1beta1.AnyConfig{
+								Object: map[string]interface{}{
+									"metrics": map[string]interface{}{
+										"address": "",
+									},
+								},
+							},
+						},
+					},
+				},
+			},
+			expectedConfig: &v1beta1.OpenTelemetryCollector{
+				TypeMeta: metav1.TypeMeta{
+					Kind:       "OpenTelemetryCollector",
+					APIVersion: "v1beta1",
+				},
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "test-collector",
+					Namespace: "default",
+				},
+				Spec: v1beta1.OpenTelemetryCollectorSpec{
+					Config: v1beta1.Config{
+						Service: v1beta1.Service{
+							Telemetry: &v1beta1.AnyConfig{
+								Object: map[string]interface{}{
+									"metrics": map[string]interface{}{
+										"address": "",
+									},
+								},
+							},
+						},
+					},
+				},
+			},
+		},
+		{
+			name: "should not modify config when prometheus reader is already configured",
+			initialConfig: &v1beta1.OpenTelemetryCollector{
+				TypeMeta: metav1.TypeMeta{
+					Kind:       "OpenTelemetryCollector",
+					APIVersion: "v1beta1",
+				},
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "test-collector",
+					Namespace: "default",
+				},
+				Spec: v1beta1.OpenTelemetryCollectorSpec{
+					Config: v1beta1.Config{
+						Service: v1beta1.Service{
+							Telemetry: &v1beta1.AnyConfig{
+								Object: map[string]interface{}{
+									"metrics": map[string]interface{}{
+										"readers": []interface{}{
+											map[string]interface{}{
+												"pull": map[string]interface{}{
+													"exporter": map[string]interface{}{
+														"prometheus": map[string]interface{}{
+															"host": "0.0.0.0",
+															"port": int32(8888),
+														},
+													},
+												},
+											},
+										},
+									},
+								},
+							},
+						},
+					},
+				},
+			},
+			expectedConfig: &v1beta1.OpenTelemetryCollector{
+				TypeMeta: metav1.TypeMeta{
+					Kind:       "OpenTelemetryCollector",
+					APIVersion: "v1beta1",
+				},
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "test-collector",
+					Namespace: "default",
+				},
+				Spec: v1beta1.OpenTelemetryCollectorSpec{
+					Config: v1beta1.Config{
+						Service: v1beta1.Service{
+							Telemetry: &v1beta1.AnyConfig{
+								Object: map[string]interface{}{
+									"metrics": map[string]interface{}{
+										"readers": []interface{}{
+											map[string]interface{}{
+												"pull": map[string]interface{}{
+													"exporter": map[string]interface{}{
+														"prometheus": map[string]interface{}{
+															"host": "0.0.0.0",
+															"port": int32(8888),
+														},
+													},
+												},
+											},
+										},
+									},
+								},
+							},
+						},
+					},
+				},
+			},
+		},
+		{
+			name: "should not modify config when OTLP is configured",
+			initialConfig: &v1beta1.OpenTelemetryCollector{
+				TypeMeta: metav1.TypeMeta{
+					Kind:       "OpenTelemetryCollector",
+					APIVersion: "v1beta1",
+				},
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "test-collector",
+					Namespace: "default",
+				},
+				Spec: v1beta1.OpenTelemetryCollectorSpec{
+					Config: v1beta1.Config{
+						Service: v1beta1.Service{
+							Telemetry: &v1beta1.AnyConfig{
+								Object: map[string]interface{}{
+									"metrics": map[string]interface{}{
+										"readers": []interface{}{
+											map[string]interface{}{
+												"periodic": map[string]interface{}{
+													"interval": "30000",
+													"exporter": map[string]interface{}{
+														"otlp": map[string]interface{}{
+															"endpoint": "localhost:4317",
+														},
+													},
+												},
+											},
+										},
+									},
+								},
+							},
+						},
+					},
+				},
+			},
+			expectedConfig: &v1beta1.OpenTelemetryCollector{
+				TypeMeta: metav1.TypeMeta{
+					Kind:       "OpenTelemetryCollector",
+					APIVersion: "v1beta1",
+				},
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "test-collector",
+					Namespace: "default",
+				},
+				Spec: v1beta1.OpenTelemetryCollectorSpec{
+					Config: v1beta1.Config{
+						Service: v1beta1.Service{
+							Telemetry: &v1beta1.AnyConfig{
+								Object: map[string]interface{}{
+									"metrics": map[string]interface{}{
+										"readers": []interface{}{
+											map[string]interface{}{
+												"periodic": map[string]interface{}{
+													"interval": "30000",
+													"exporter": map[string]interface{}{
+														"otlp": map[string]interface{}{
+															"endpoint": "localhost:4317",
+														},
+													},
+												},
+											},
+										},
+									},
+								},
+							},
+						},
+					},
+				},
+			},
+		},
+		{
+			name: "should not modify config when metrics config is missing",
+			initialConfig: &v1beta1.OpenTelemetryCollector{
+				TypeMeta: metav1.TypeMeta{
+					Kind:       "OpenTelemetryCollector",
+					APIVersion: "v1beta1",
+				},
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "test-collector",
+					Namespace: "default",
+				},
+				Spec: v1beta1.OpenTelemetryCollectorSpec{
+					Config: v1beta1.Config{
+						Service: v1beta1.Service{
+							Telemetry: &v1beta1.AnyConfig{
+								Object: map[string]interface{}{},
+							},
+						},
+					},
+				},
+			},
+			expectedConfig: &v1beta1.OpenTelemetryCollector{
+				TypeMeta: metav1.TypeMeta{
+					Kind:       "OpenTelemetryCollector",
+					APIVersion: "v1beta1",
+				},
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "test-collector",
+					Namespace: "default",
+				},
+				Spec: v1beta1.OpenTelemetryCollectorSpec{
+					Config: v1beta1.Config{
+						Service: v1beta1.Service{
+							Telemetry: &v1beta1.AnyConfig{
+								Object: map[string]interface{}{},
+							},
+						},
+					},
+				},
+			},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			up := VersionUpgrade{
+				Log: logger,
+			}
+
+			result, err := upgrade0_122_0(up, tt.initialConfig)
+			require.NoError(t, err)
+			require.NotNil(t, result)
+
+			if tt.initialConfig.Spec.Config.Service.Telemetry != nil &&
+				tt.initialConfig.Spec.Config.Service.Telemetry.Object != nil {
+				metrics, ok := tt.initialConfig.Spec.Config.Service.Telemetry.Object["metrics"].(map[string]interface{})
+				if ok {
+					address, ok := metrics["address"].(string)
+					if ok && address != "" {
+						assert.Equal(t, "", address, "address field should be set to empty string")
+					}
+				}
+			}
+
+			assert.Equal(t, tt.expectedConfig.Spec.Config.Service.Telemetry, result.Spec.Config.Service.Telemetry)
+		})
+	}
+}
diff --git a/tests/e2e-multi-instrumentation/instrumentation-multi-multicontainer-go/02-assert.yaml b/tests/e2e-multi-instrumentation/instrumentation-multi-multicontainer-go/02-assert.yaml
index 6fd04f5d..f9f07909 100644
--- a/tests/e2e-multi-instrumentation/instrumentation-multi-multicontainer-go/02-assert.yaml
+++ b/tests/e2e-multi-instrumentation/instrumentation-multi-multicontainer-go/02-assert.yaml
@@ -94,7 +94,12 @@ spec:
         service:
           telemetry:
             metrics:
-              address: 0.0.0.0:8888
+              readers:
+                - pull:
+                    exporter:
+                      prometheus:
+                        host: 0.0.0.0
+                        port: 8888
           pipelines:
             traces:
               exporters:
diff --git a/tests/e2e-multi-instrumentation/instrumentation-multi-multicontainer/01-assert.yaml b/tests/e2e-multi-instrumentation/instrumentation-multi-multicontainer/01-assert.yaml
index 6ea8128c..ba7d348a 100644
--- a/tests/e2e-multi-instrumentation/instrumentation-multi-multicontainer/01-assert.yaml
+++ b/tests/e2e-multi-instrumentation/instrumentation-multi-multicontainer/01-assert.yaml
@@ -333,7 +333,12 @@ spec:
         service:
           telemetry:
             metrics:
-              address: 0.0.0.0:8888
+              readers:
+                - pull:
+                    exporter:
+                      prometheus:
+                        host: 0.0.0.0
+                        port: 8888
           pipelines:
             traces:
               exporters:
diff --git a/tests/e2e-openshift/must-gather/assert-install-target-allocator.yaml b/tests/e2e-openshift/must-gather/assert-install-target-allocator.yaml
index b70d638b..d5843823 100644
--- a/tests/e2e-openshift/must-gather/assert-install-target-allocator.yaml
+++ b/tests/e2e-openshift/must-gather/assert-install-target-allocator.yaml
@@ -48,7 +48,12 @@ data:
                     - jaeger
         telemetry:
             metrics:
-                address: 0.0.0.0:8888
+              readers:
+                - pull:
+                    exporter:
+                      prometheus:
+                        host: 0.0.0.0
+                        port: 8888
 kind: ConfigMap
 metadata:
   labels:
diff --git a/tests/e2e-ta-collector-mtls/ta-collector-mtls/00-assert.yaml b/tests/e2e-ta-collector-mtls/ta-collector-mtls/00-assert.yaml
index db11c838..d9e83668 100644
--- a/tests/e2e-ta-collector-mtls/ta-collector-mtls/00-assert.yaml
+++ b/tests/e2e-ta-collector-mtls/ta-collector-mtls/00-assert.yaml
@@ -61,10 +61,15 @@ data:
                     - prometheus
         telemetry:
             metrics:
-                address: 0.0.0.0:8888
+                readers:
+                    - pull:
+                        exporter:
+                            prometheus:
+                                host: 0.0.0.0
+                                port: 8888
 kind: ConfigMap
 metadata:
-  name: prometheus-cr-collector-19c94a81
+  name: prometheus-cr-collector-84db057e
 ---
 apiVersion: v1
 kind: Pod
diff --git a/tests/e2e-ta-collector-mtls/ta-disabled/00-assert.yaml b/tests/e2e-ta-collector-mtls/ta-disabled/00-assert.yaml
index 548b61d9..5b4ae21e 100644
--- a/tests/e2e-ta-collector-mtls/ta-disabled/00-assert.yaml
+++ b/tests/e2e-ta-collector-mtls/ta-disabled/00-assert.yaml
@@ -34,7 +34,12 @@ data:
     service:
       telemetry:
         metrics:
-          address: 0.0.0.0:8888
+          readers:
+            - pull:
+                exporter:
+                  prometheus:
+                    host: 0.0.0.0
+                    port: 8888
       pipelines:
         traces:
           exporters:
@@ -43,4 +48,4 @@ data:
             - jaeger
 kind: ConfigMap
 metadata:
-  name: prometheus-cr-collector-7a42612e
+  name: prometheus-cr-collector-5a741df8
diff --git a/tests/e2e-targetallocator-cr/targetallocator-label/00-assert.yaml b/tests/e2e-targetallocator-cr/targetallocator-label/00-assert.yaml
index 7aa573ed..0aa109cd 100644
--- a/tests/e2e-targetallocator-cr/targetallocator-label/00-assert.yaml
+++ b/tests/e2e-targetallocator-cr/targetallocator-label/00-assert.yaml
@@ -20,7 +20,12 @@ data:
     service:
       telemetry:
         metrics:
-          address: 0.0.0.0:8888
+          readers:
+            - pull:
+                exporter:
+                  prometheus:
+                    host: 0.0.0.0
+                    port: 8888
       pipelines:
         metrics:
           exporters:
diff --git a/tests/e2e-targetallocator-cr/targetallocator-label/01-assert.yaml b/tests/e2e-targetallocator-cr/targetallocator-label/01-assert.yaml
index c492114c..14755f6c 100644
--- a/tests/e2e-targetallocator-cr/targetallocator-label/01-assert.yaml
+++ b/tests/e2e-targetallocator-cr/targetallocator-label/01-assert.yaml
@@ -24,7 +24,12 @@ data:
                     - prometheus
         telemetry:
             metrics:
-                address: 0.0.0.0:8888
+                readers:
+                    - pull:
+                        exporter:
+                            prometheus:
+                                host: 0.0.0.0
+                                port: 8888
 ---
 apiVersion: v1
 data:
diff --git a/tests/e2e-targetallocator-cr/targetallocator-label/02-assert.yaml b/tests/e2e-targetallocator-cr/targetallocator-label/02-assert.yaml
index 7e0caf5f..cf36df71 100644
--- a/tests/e2e-targetallocator-cr/targetallocator-label/02-assert.yaml
+++ b/tests/e2e-targetallocator-cr/targetallocator-label/02-assert.yaml
@@ -24,7 +24,12 @@ data:
                     - prometheus
         telemetry:
             metrics:
-                address: 0.0.0.0:8888
+                readers:
+                    - pull:
+                        exporter:
+                            prometheus:
+                                host: 0.0.0.0
+                                port: 8888
 ---
 apiVersion: v1
 data:
diff --git a/tests/e2e-targetallocator/targetallocator-features/00-assert.yaml b/tests/e2e-targetallocator/targetallocator-features/00-assert.yaml
index e89f3f31..4cd66124 100644
--- a/tests/e2e-targetallocator/targetallocator-features/00-assert.yaml
+++ b/tests/e2e-targetallocator/targetallocator-features/00-assert.yaml
@@ -20,7 +20,7 @@ spec:
           items:
           - key: collector.yaml
             path: collector.yaml
-          name: stateful-collector-c055e8e3
+          name: stateful-collector-76928b61
         name: otc-internal
       - emptyDir: {}
         name: testvolume
diff --git a/tests/e2e-targetallocator/targetallocator-kubernetessd/00-assert.yaml b/tests/e2e-targetallocator/targetallocator-kubernetessd/00-assert.yaml
index 1a5b0b9d..1b1b4760 100644
--- a/tests/e2e-targetallocator/targetallocator-kubernetessd/00-assert.yaml
+++ b/tests/e2e-targetallocator/targetallocator-kubernetessd/00-assert.yaml
@@ -15,7 +15,7 @@ metadata:
 apiVersion: v1
 kind: ConfigMap
 metadata:
-  name: prometheus-kubernetessd-collector-9c184e3a
+  name: prometheus-kubernetessd-collector-45af7c16
 data:
   collector.yaml: |
     exporters:
@@ -37,7 +37,12 @@ data:
                     - prometheus
         telemetry:
             metrics:
-                address: 0.0.0.0:8888
+                readers:
+                    - pull:
+                        exporter:
+                            prometheus:
+                                host: 0.0.0.0
+                                port: 8888
 ---
 apiVersion: apps/v1
 kind: DaemonSet
diff --git a/tests/e2e-targetallocator/targetallocator-prometheuscr/00-assert.yaml b/tests/e2e-targetallocator/targetallocator-prometheuscr/00-assert.yaml
index 5185c911..cd49ee78 100644
--- a/tests/e2e-targetallocator/targetallocator-prometheuscr/00-assert.yaml
+++ b/tests/e2e-targetallocator/targetallocator-prometheuscr/00-assert.yaml
@@ -42,7 +42,15 @@ data:
                     - prometheus
         telemetry:
             metrics:
-                address: 0.0.0.0:8888
+                readers:
+                    - pull:
+                        exporter:
+                            prometheus:
+                                host: 0.0.0.0
+                                port: 8888
 kind: ConfigMap
 metadata:
-  name: prometheus-cr-collector-19c94a81
+  labels:
+      app.kubernetes.io/component: opentelemetry-collector
+      app.kubernetes.io/managed-by: opentelemetry-operator
+      app.kubernetes.io/name: prometheus-cr-collector
diff --git a/tests/e2e/managed-reconcile/02-assert.yaml b/tests/e2e/managed-reconcile/02-assert.yaml
index e9bb69b6..ac5a5eda 100644
--- a/tests/e2e/managed-reconcile/02-assert.yaml
+++ b/tests/e2e/managed-reconcile/02-assert.yaml
@@ -52,7 +52,7 @@ spec:
 apiVersion: v1
 kind: ConfigMap
 metadata:
-  name: simplest-collector-aec5aa11
+  name: simplest-collector-d5e146a9
 data:
   collector.yaml: |
     receivers:
@@ -67,7 +67,12 @@ data:
     service:
       telemetry:
         metrics:
-          address: 0.0.0.0:8888
+          readers:
+            - pull:
+                exporter:
+                  prometheus:
+                    host: 0.0.0.0
+                    port: 8888
       pipelines:
         traces:
           exporters:
diff --git a/tests/e2e/multiple-configmaps/00-assert.yaml b/tests/e2e/multiple-configmaps/00-assert.yaml
index 8ff6b44a..a6303205 100644
--- a/tests/e2e/multiple-configmaps/00-assert.yaml
+++ b/tests/e2e/multiple-configmaps/00-assert.yaml
@@ -25,7 +25,7 @@ spec:
       volumes:
         - name: otc-internal
           configMap:
-            name: simplest-with-configmaps-collector-aec5aa11
+            name: simplest-with-configmaps-collector-d5e146a9
             items:
               - key: collector.yaml
                 path: collector.yaml
diff --git a/tests/e2e/smoke-pod-labels/00-assert.yaml b/tests/e2e/smoke-pod-labels/00-assert.yaml
index 552ec888..f9332676 100644
--- a/tests/e2e/smoke-pod-labels/00-assert.yaml
+++ b/tests/e2e/smoke-pod-labels/00-assert.yaml
@@ -6,7 +6,7 @@ metadata:
     app.kubernetes.io/managed-by: opentelemetry-operator
     app.kubernetes.io/name: testlabel-collector
     app.kubernetes.io/part-of: opentelemetry
-    app.kubernetes.io/version: 0.94.0
+    app.kubernetes.io/version: 0.121.0
   name: testlabel-collector
 spec:
   selector:
@@ -21,7 +21,7 @@ spec:
         app.kubernetes.io/managed-by: opentelemetry-operator
         app.kubernetes.io/name: testlabel-collector
         app.kubernetes.io/part-of: opentelemetry
-        app.kubernetes.io/version: 0.94.0
+        app.kubernetes.io/version: 0.121.0
 status:
   availableReplicas: 1
   readyReplicas: 1
diff --git a/tests/e2e/smoke-pod-labels/00-install.yaml b/tests/e2e/smoke-pod-labels/00-install.yaml
index 69e00f4e..d110019f 100644
--- a/tests/e2e/smoke-pod-labels/00-install.yaml
+++ b/tests/e2e/smoke-pod-labels/00-install.yaml
@@ -3,7 +3,7 @@ kind: OpenTelemetryCollector
 metadata:
   name: testlabel
 spec:
-  image: ghcr.io/open-telemetry/opentelemetry-collector-releases/opentelemetry-collector-contrib:0.94.0
+  image: ghcr.io/open-telemetry/opentelemetry-collector-releases/opentelemetry-collector-contrib:0.121.0
   config: |
     receivers:
       otlp:
diff --git a/tests/e2e/smoke-targetallocator/00-assert.yaml b/tests/e2e/smoke-targetallocator/00-assert.yaml
index 1ba3d195..1fca1fcb 100644
--- a/tests/e2e/smoke-targetallocator/00-assert.yaml
+++ b/tests/e2e/smoke-targetallocator/00-assert.yaml
@@ -52,7 +52,12 @@ data:
                     - jaeger
         telemetry:
             metrics:
-                address: 0.0.0.0:8888
+                readers:
+                    - pull:
+                        exporter:
+                            prometheus:
+                                host: 0.0.0.0
+                                port: 8888
 kind: ConfigMap
 metadata:
-  name: stateful-collector-7a42612e
+  name: stateful-collector-5a741df8
diff --git a/tests/e2e/smoke-targetallocator/01-assert.yaml b/tests/e2e/smoke-targetallocator/01-error.yaml
similarity index 83%
rename from tests/e2e/smoke-targetallocator/01-assert.yaml
rename to tests/e2e/smoke-targetallocator/01-error.yaml
index e420b034..fd2f7b49 100644
--- a/tests/e2e/smoke-targetallocator/01-assert.yaml
+++ b/tests/e2e/smoke-targetallocator/01-error.yaml
@@ -5,4 +5,4 @@ metadata:
 status:
   replicas: 1
   readyReplicas: 1
-  observedGeneration: 2
+  observedGeneration: 1
diff --git a/tests/e2e/smoke-targetallocator/chainsaw-test.yaml b/tests/e2e/smoke-targetallocator/chainsaw-test.yaml
index ea19c198..c23f6435 100755
--- a/tests/e2e/smoke-targetallocator/chainsaw-test.yaml
+++ b/tests/e2e/smoke-targetallocator/chainsaw-test.yaml
@@ -55,5 +55,5 @@ spec:
     try:
     - apply:
         file: 01-change-ta-config.yaml
-    - assert:
-        file: 01-assert.yaml
+    - error:
+        file: 01-error.yaml
diff --git a/tests/e2e/statefulset-features/00-assert.yaml b/tests/e2e/statefulset-features/00-assert.yaml
index b4e2d060..09465cb7 100644
--- a/tests/e2e/statefulset-features/00-assert.yaml
+++ b/tests/e2e/statefulset-features/00-assert.yaml
@@ -20,7 +20,7 @@ spec:
            items:
            - key: collector.yaml
              path: collector.yaml
-           name: stateful-collector-52b86f05
+           name: stateful-collector-45c637b7
          name: otc-internal
        - emptyDir: {}
          name: testvolume
diff --git a/tests/e2e/statefulset-features/01-assert.yaml b/tests/e2e/statefulset-features/01-assert.yaml
index 9630e500..e3919cda 100644
--- a/tests/e2e/statefulset-features/01-assert.yaml
+++ b/tests/e2e/statefulset-features/01-assert.yaml
@@ -20,7 +20,7 @@ spec:
            items:
            - key: collector.yaml
              path: collector.yaml
-           name: stateful-collector-52b86f05
+           name: stateful-collector-45c637b7
          name: otc-internal
        - emptyDir: {}
          name: testvolume
diff --git a/tests/e2e/versioned-configmaps/00-assert.yaml b/tests/e2e/versioned-configmaps/00-assert.yaml
index d0fcfd2a..fbff82be 100644
--- a/tests/e2e/versioned-configmaps/00-assert.yaml
+++ b/tests/e2e/versioned-configmaps/00-assert.yaml
@@ -9,11 +9,11 @@ spec:
       volumes:
         - name: otc-internal
           configMap:
-            name: simple-collector-de9b8847
+            name: simple-collector-5723ff27
 status:
   readyReplicas: 1
 ---
 apiVersion: v1
 kind: ConfigMap
 metadata:
-  name: simple-collector-de9b8847
+  name: simple-collector-5723ff27
diff --git a/tests/e2e/versioned-configmaps/01-assert.yaml b/tests/e2e/versioned-configmaps/01-assert.yaml
index 1e291a0c..2053e15f 100644
--- a/tests/e2e/versioned-configmaps/01-assert.yaml
+++ b/tests/e2e/versioned-configmaps/01-assert.yaml
@@ -9,16 +9,16 @@ spec:
       volumes:
         - name: otc-internal
           configMap:
-            name: simple-collector-3f453d89
+            name: simple-collector-dfcfaead
 status:
   readyReplicas: 1
 ---
 apiVersion: v1
 kind: ConfigMap
 metadata:
-  name: simple-collector-3f453d89
+  name: simple-collector-dfcfaead
 ---
 apiVersion: v1
 kind: ConfigMap
 metadata:
-  name: simple-collector-de9b8847
+  name: simple-collector-5723ff27