Repository: open-telemetry/opentelemetry-collector-contrib (Go)

Issue:
[receiver/kafka] use franz-go client by default
### Component(s)

receiver/kafka

### Describe the issue you're reporting

In v0.129.0 @marclop [introduced](https://github.com/open-telemetry/opentelemetry-collector-contrib/pull/40716) a new feature gate to the Kafka receiver to use the https://github.com/twmb/franz-go Kafka client. As can be seen in the original PR, this has significantly better performance. Moreover, with this client it is possible to use regex topic consuming.

We're running with this in production with good results, and we have had reports from other users that it's working better for them than the default Sarama implementation too.

I suggest we bump the feature gate along to beta, enabling it by default.

### Tip

<sub>[React](https://github.blog/news-insights/product-news/add-reactions-to-pull-requests-issues-and-comments/) with 👍 to help prioritize this issue. Please use comments to provide useful context, avoiding `+1` or `me too`, to help us triage it. Learn more [here](https://opentelemetry.io/community/end-user/issue-participation/).</sub>


Discussion:
Pinging code owners:
- receiver/kafka: @pavolloffay @MovieStoreGuy @axw

 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.

As a user, we have noticed a significant performance improvement when using the collector with this feature enabled.

The image below shows the improvement achieved by enabling the feature gate only in a small deployment.

<img width="531" height="183" alt="Image" src="https://github.com/user-attachments/assets/db1d5c23-49f8-45c9-9b19-27f4d2c47426" />

I can pick this issue if the code owners are ok with it 🙏 
@paulojmdias that's awesome, thanks for sharing.

> I can pick this issue if the code owners are ok with it 🙏

Absolutely, assigning you - thanks for offering!
An [issue has recently been created against this repository](https://github.com/open-telemetry/opentelemetry-collector-contrib/issues/42226) regarding the updated library, claiming it's caused data loss. I don't have enough context to properly evaluate it, but I think we should validate that it's invalid or fixed before upgrading the feature gate stability.
@crobert-1 I've had a look and tried to reproduce without success, but I'll let the bug reporter come back to us and see if I've missed anything in the minimal reproduction I've created.



Patch:
diff --git a/.chloggen/receiver_kafka_franz_go_beta.yaml b/.chloggen/receiver_kafka_franz_go_beta.yaml
new file mode 100644
index 0000000000000..a1ecc859dada0
--- /dev/null
+++ b/.chloggen/receiver_kafka_franz_go_beta.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: receiver/kafkareceiver
+
+# A brief description of the change.  Surround your text with quotes ("") if it needs to start with a backtick (`).
+note: "Use franz-go client for Kafka receiver as default, promoting the receiver.kafkareceiver.UseFranzGo feature gate to Beta."
+
+# Mandatory: One or more tracking issues related to the change. You can use the PR number here if no issue exists.
+issues: [42155]
+
+# (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:
+
+# 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/kafkareceiver/README.md b/receiver/kafkareceiver/README.md
index b49634c021450..9d96c81f3fd00 100644
--- a/receiver/kafkareceiver/README.md
+++ b/receiver/kafkareceiver/README.md
@@ -23,7 +23,7 @@ If used in conjunction with the `kafkaexporter` configured with `include_metadat
 ## Getting Started
 
 > [!NOTE]
-> You can opt-in to use [`franz-go`](https://github.com/twmb/franz-go) client by enabling the feature gate
+> You can opt out of using the [`franz-go`](https://github.com/twmb/franz-go) client by disabling the feature gate
 > `receiver.kafkareceiver.UseFranzGo` when you run the OpenTelemetry Collector. See the following page
 > for more details: [Feature Gates](https://github.com/open-telemetry/opentelemetry-collector/tree/main/featuregate#controlling-gates)
 >
diff --git a/receiver/kafkareceiver/consumer_franz.go b/receiver/kafkareceiver/consumer_franz.go
index 4bbc070bdb951..49f60e4ef9781 100644
--- a/receiver/kafkareceiver/consumer_franz.go
+++ b/receiver/kafkareceiver/consumer_franz.go
@@ -14,6 +14,7 @@ import (
 	"github.com/cenkalti/backoff/v4"
 	"github.com/twmb/franz-go/pkg/kgo"
 	"go.opentelemetry.io/collector/component"
+	"go.opentelemetry.io/collector/component/componentstatus"
 	"go.opentelemetry.io/collector/consumer/consumererror"
 	"go.opentelemetry.io/collector/featuregate"
 	"go.opentelemetry.io/collector/receiver"
@@ -34,7 +35,7 @@ const franzGoConsumerFeatureGateName = "receiver.kafkareceiver.UseFranzGo"
 // the Kafka receiver will use the franz-go client, which is more performant and has
 // better support for modern Kafka features.
 var franzGoConsumerFeatureGate = featuregate.GlobalRegistry().MustRegister(
-	franzGoConsumerFeatureGateName, featuregate.StageAlpha,
+	franzGoConsumerFeatureGateName, featuregate.StageBeta,
 	featuregate.WithRegisterDescription("When enabled, the Kafka receiver will use the franz-go client to consume messages."),
 	featuregate.WithRegisterFromVersion("v0.129.0"),
 )
@@ -63,6 +64,11 @@ type franzConsumer struct {
 	client      *kgo.Client
 	obsrecv     *receiverhelper.ObsReport
 	assignments map[topicPartition]*pc
+
+	// ---- status reporting (parity with Sarama) ----
+	host         component.Host
+	stoppingOnce sync.Once
+	stoppedOnce  sync.Once
 }
 
 // pc represents the partition consumer shared information.
@@ -137,6 +143,22 @@ func newFranzKafkaConsumer(
 	}, nil
 }
 
+// reportStatus emits a component status event if we have a host.
+func (c *franzConsumer) reportStatus(s componentstatus.Status) {
+	if c.host == nil {
+		return
+	}
+	componentstatus.ReportStatus(c.host, componentstatus.NewEvent(s))
+}
+
+// reportRecoverable reports a recoverable error status event.
+func (c *franzConsumer) reportRecoverable(err error) {
+	if c.host == nil || err == nil {
+		return
+	}
+	componentstatus.ReportStatus(c.host, componentstatus.NewRecoverableErrorEvent(err))
+}
+
 func (c *franzConsumer) Start(ctx context.Context, host component.Host) error {
 	c.mu.Lock()
 	defer c.mu.Unlock()
@@ -149,6 +171,10 @@ func (c *franzConsumer) Start(ctx context.Context, host component.Host) error {
 		close(c.started)
 	}
 
+	// Parity with Sarama: report "Starting" as soon as Start() is called.
+	c.host = host
+	c.reportStatus(componentstatus.StatusStarting)
+
 	obsrecv, err := receiverhelper.NewObsReport(receiverhelper.ObsReportSettings{
 		ReceiverID:             c.settings.ID,
 		Transport:              transport,
@@ -205,7 +231,11 @@ func (c *franzConsumer) Start(ctx context.Context, host component.Host) error {
 }
 
 func (c *franzConsumer) consumeLoop(ctx context.Context) {
-	defer close(c.consumerClosed)
+	// Parity with Sarama: when the loop exits, report Stopped.
+	defer func() {
+		c.stoppedOnce.Do(func() { c.reportStatus(componentstatus.StatusStopped) })
+		close(c.consumerClosed)
+	}()
 
 	for {
 		// Consume messages until the ctx is cancelled (the client is closed).
@@ -237,6 +267,8 @@ func (c *franzConsumer) consume(ctx context.Context, size int) bool {
 			zap.String("topic", topic),
 			zap.Int64("partition", int64(partition)),
 		)
+		// Parity with Sarama: report recoverable error while consuming.
+		c.reportRecoverable(err)
 		if !hasError {
 			hasError = true
 		}
@@ -354,14 +386,23 @@ func (c *franzConsumer) consume(ctx context.Context, size int) bool {
 	if !c.config.AutoCommit.Enable {
 		if err := c.client.CommitMarkedOffsets(ctx); err != nil {
 			c.settings.Logger.Error("failed to commit offsets", zap.Error(err))
+			// Surface as recoverable error (parity with Sarama’s loop error reporting).
+			c.reportRecoverable(err)
 		}
 	}
 	return true
 }
 
 func (c *franzConsumer) Shutdown(ctx context.Context) error {
+	// Parity with Sarama: report Stopping at shutdown start.
+	c.stoppingOnce.Do(func() { c.reportStatus(componentstatus.StatusStopping) })
+
 	if !c.triggerShutdown() {
-		return errors.New("kafka consumer: consumer isn't running")
+		// Idempotent: never fail if not started.
+		// We still want to ensure Stopped is eventually emitted (consumeLoop defer handles it).
+		// However, if the loop was never started, emit Stopped here too.
+		c.stoppedOnce.Do(func() { c.reportStatus(componentstatus.StatusStopped) })
+		return nil
 	}
 
 	select {
@@ -369,6 +410,7 @@ func (c *franzConsumer) Shutdown(ctx context.Context) error {
 		return context.Cause(ctx)
 	case <-c.consumerClosed:
 	}
+
 	return nil
 }
 
@@ -387,11 +429,14 @@ func (c *franzConsumer) triggerShutdown() bool {
 		return true
 	default:
 		close(c.closing)
+		client := c.client
 		c.mu.Unlock()
 		// Close the client without holding the write mutex, otherwise, the
 		// Shutdown will deadlock when `franzConsumer` inevitably calls the
 		// lost/assigned callback.
-		c.client.Close()
+		if client != nil {
+			client.Close()
+		}
 	}
 	return true
 }
@@ -399,6 +444,9 @@ func (c *franzConsumer) triggerShutdown() bool {
 // assigned must be set as kgo.OnPartitionsAssigned callback. Ensuring all
 // assigned partitions to this consumer process received records.
 func (c *franzConsumer) assigned(ctx context.Context, _ *kgo.Client, assigned map[string][]int32) {
+	// Report OK on each successful assignment so we can recover status after transient errors.
+	c.reportStatus(componentstatus.StatusOK)
+
 	c.mu.Lock()
 	defer c.mu.Unlock()
 	for topic, partitions := range assigned {
@@ -468,6 +516,8 @@ func (c *franzConsumer) lost(ctx context.Context, _ *kgo.Client,
 	// away from this consumer.
 	if err := c.client.CommitMarkedOffsets(ctx); err != nil {
 		c.settings.Logger.Error("failed to commit marked offsets", zap.Error(err))
+		// Parity with Sarama: report recoverable error on commit errors.
+		c.reportRecoverable(err)
 	}
 }
 


Test patch:
diff --git a/receiver/kafkareceiver/consumer_franz_test.go b/receiver/kafkareceiver/consumer_franz_test.go
index bae1434a0ce4a..f35d6a5f14657 100644
--- a/receiver/kafkareceiver/consumer_franz_test.go
+++ b/receiver/kafkareceiver/consumer_franz_test.go
@@ -199,6 +199,13 @@ func TestConsumerShutdownConsuming(t *testing.T) {
 			consumer, e := newFranzKafkaConsumer(cfg, settings, []string{topic}, consumeFn)
 			require.NoError(tb, e)
 			require.NoError(tb, consumer.Start(ctx, componenttest.NewNopHost()))
+			// Wait until the group has assigned at least one partition.
+			require.Eventually(tb, func() bool {
+				consumer.mu.RLock()
+				n := len(consumer.assignments)
+				consumer.mu.RUnlock()
+				return n > 0
+			}, 10*time.Second, 10*time.Millisecond)
 			require.NoError(tb, kafkaClient.ProduceSync(ctx, rs...).FirstErr())
 
 			select {
@@ -239,9 +246,12 @@ func TestConsumerShutdownNotStarted(t *testing.T) {
 	require.NoError(t, err)
 
 	for range 2 {
-		require.EqualError(t, c.Shutdown(t.Context()),
-			"kafka consumer: consumer isn't running")
+		require.NoError(t, c.Shutdown(t.Context()))
 	}
+
+	// Verify internal signal that there's nothing to shut down.
+	// (Same package, so we can call the unexported helper.)
+	require.False(t, c.triggerShutdown(), "triggerShutdown should indicate no-op when never started")
 }
 
 // TestRaceLostVsConsume verifies no data race occurs between concurrent
diff --git a/receiver/kafkareceiver/factory_test.go b/receiver/kafkareceiver/factory_test.go
index ed080eeb39460..6395c62e59c3f 100644
--- a/receiver/kafkareceiver/factory_test.go
+++ b/receiver/kafkareceiver/factory_test.go
@@ -16,6 +16,38 @@ import (
 	"github.com/open-telemetry/opentelemetry-collector-contrib/receiver/kafkareceiver/internal/metadata"
 )
 
+func encodingFromReceiver(tb testing.TB, r any, section string) string {
+	tb.Helper()
+
+	switch rc := r.(type) {
+	case *saramaConsumer:
+		switch section {
+		case "Traces":
+			return rc.config.Traces.Encoding
+		case "Metrics":
+			return rc.config.Metrics.Encoding
+		case "Logs":
+			return rc.config.Logs.Encoding
+		case "Profiles":
+			return rc.config.Profiles.Encoding
+		}
+	case *franzConsumer:
+		switch section {
+		case "Traces":
+			return rc.config.Traces.Encoding
+		case "Metrics":
+			return rc.config.Metrics.Encoding
+		case "Logs":
+			return rc.config.Logs.Encoding
+		case "Profiles":
+			return rc.config.Profiles.Encoding
+		}
+	}
+
+	tb.Fatalf("unsupported receiver type %T or section %q", r, section)
+	return ""
+}
+
 func TestCreateDefaultConfig(t *testing.T) {
 	cfg := createDefaultConfig().(*Config)
 	assert.NotNil(t, cfg, "failed to create default config")
@@ -37,24 +69,42 @@ func TestCreateTraces(t *testing.T) {
 func TestWithTracesUnmarshalers(t *testing.T) {
 	f := NewFactory()
 
-	t.Run("custom_encoding", func(t *testing.T) {
+	t.Run("custom_encoding/sarama", func(t *testing.T) {
+		setFranzGo(t, false)
+		cfg := createDefaultConfig().(*Config)
+		cfg.Traces.Encoding = "custom"
+		receiver, err := f.CreateTraces(t.Context(), receivertest.NewNopSettings(metadata.Type), cfg, nil)
+		require.NoError(t, err)
+		require.NotNil(t, receiver)
+		assert.Equal(t, "custom", encodingFromReceiver(t, receiver, "Traces"))
+	})
+
+	t.Run("custom_encoding/franzgo", func(t *testing.T) {
+		setFranzGo(t, true)
 		cfg := createDefaultConfig().(*Config)
 		cfg.Traces.Encoding = "custom"
 		receiver, err := f.CreateTraces(t.Context(), receivertest.NewNopSettings(metadata.Type), cfg, nil)
-		tracesConsumer, ok := receiver.(*saramaConsumer)
-		require.True(t, ok)
-		require.Equal(t, "custom", tracesConsumer.config.Traces.Encoding)
 		require.NoError(t, err)
 		require.NotNil(t, receiver)
+		assert.Equal(t, "custom", encodingFromReceiver(t, receiver, "Traces"))
+	})
+
+	t.Run("default_encoding/sarama", func(t *testing.T) {
+		setFranzGo(t, false)
+		cfg := createDefaultConfig()
+		receiver, err := f.CreateTraces(t.Context(), receivertest.NewNopSettings(metadata.Type), cfg, nil)
+		require.NoError(t, err)
+		require.NotNil(t, receiver)
+		assert.Equal(t, defaultTracesEncoding, encodingFromReceiver(t, receiver, "Traces"))
 	})
-	t.Run("default_encoding", func(t *testing.T) {
+
+	t.Run("default_encoding/franzgo", func(t *testing.T) {
+		setFranzGo(t, true)
 		cfg := createDefaultConfig()
 		receiver, err := f.CreateTraces(t.Context(), receivertest.NewNopSettings(metadata.Type), cfg, nil)
-		tracesConsumer, ok := receiver.(*saramaConsumer)
-		require.True(t, ok)
-		require.Equal(t, defaultTracesEncoding, tracesConsumer.config.Traces.Encoding)
 		require.NoError(t, err)
-		assert.NotNil(t, receiver)
+		require.NotNil(t, receiver)
+		assert.Equal(t, defaultTracesEncoding, encodingFromReceiver(t, receiver, "Traces"))
 	})
 }
 
@@ -71,24 +121,42 @@ func TestCreateMetrics(t *testing.T) {
 func TestWithMetricsUnmarshalers(t *testing.T) {
 	f := NewFactory()
 
-	t.Run("custom_encoding", func(t *testing.T) {
+	t.Run("custom_encoding/sarama", func(t *testing.T) {
+		setFranzGo(t, false)
+		cfg := createDefaultConfig().(*Config)
+		cfg.Metrics.Encoding = "custom"
+		receiver, err := f.CreateMetrics(t.Context(), receivertest.NewNopSettings(metadata.Type), cfg, nil)
+		require.NoError(t, err)
+		require.NotNil(t, receiver)
+		assert.Equal(t, "custom", encodingFromReceiver(t, receiver, "Metrics"))
+	})
+
+	t.Run("custom_encoding/franzgo", func(t *testing.T) {
+		setFranzGo(t, true)
 		cfg := createDefaultConfig().(*Config)
 		cfg.Metrics.Encoding = "custom"
 		receiver, err := f.CreateMetrics(t.Context(), receivertest.NewNopSettings(metadata.Type), cfg, nil)
-		metricsConsumer, ok := receiver.(*saramaConsumer)
-		require.True(t, ok)
-		require.Equal(t, "custom", metricsConsumer.config.Metrics.Encoding)
 		require.NoError(t, err)
 		require.NotNil(t, receiver)
+		assert.Equal(t, "custom", encodingFromReceiver(t, receiver, "Metrics"))
+	})
+
+	t.Run("default_encoding/sarama", func(t *testing.T) {
+		setFranzGo(t, false)
+		cfg := createDefaultConfig()
+		receiver, err := f.CreateMetrics(t.Context(), receivertest.NewNopSettings(metadata.Type), cfg, nil)
+		require.NoError(t, err)
+		require.NotNil(t, receiver)
+		assert.Equal(t, defaultMetricsEncoding, encodingFromReceiver(t, receiver, "Metrics"))
 	})
-	t.Run("default_encoding", func(t *testing.T) {
+
+	t.Run("default_encoding/franzgo", func(t *testing.T) {
+		setFranzGo(t, true)
 		cfg := createDefaultConfig()
 		receiver, err := f.CreateMetrics(t.Context(), receivertest.NewNopSettings(metadata.Type), cfg, nil)
-		metricsConsumer, ok := receiver.(*saramaConsumer)
-		require.True(t, ok)
-		require.Equal(t, defaultMetricsEncoding, metricsConsumer.config.Metrics.Encoding)
 		require.NoError(t, err)
-		assert.NotNil(t, receiver)
+		require.NotNil(t, receiver)
+		assert.Equal(t, defaultMetricsEncoding, encodingFromReceiver(t, receiver, "Metrics"))
 	})
 }
 
@@ -105,24 +173,42 @@ func TestCreateLogs(t *testing.T) {
 func TestWithLogsUnmarshalers(t *testing.T) {
 	f := NewFactory()
 
-	t.Run("custom_encoding", func(t *testing.T) {
+	t.Run("custom_encoding/sarama", func(t *testing.T) {
+		setFranzGo(t, false)
+		cfg := createDefaultConfig().(*Config)
+		cfg.Logs.Encoding = "custom"
+		receiver, err := f.CreateLogs(t.Context(), receivertest.NewNopSettings(metadata.Type), cfg, nil)
+		require.NoError(t, err)
+		require.NotNil(t, receiver)
+		assert.Equal(t, "custom", encodingFromReceiver(t, receiver, "Logs"))
+	})
+
+	t.Run("custom_encoding/franzgo", func(t *testing.T) {
+		setFranzGo(t, true)
 		cfg := createDefaultConfig().(*Config)
 		cfg.Logs.Encoding = "custom"
 		receiver, err := f.CreateLogs(t.Context(), receivertest.NewNopSettings(metadata.Type), cfg, nil)
-		logsConsumer, ok := receiver.(*saramaConsumer)
-		require.True(t, ok)
-		require.Equal(t, "custom", logsConsumer.config.Logs.Encoding)
 		require.NoError(t, err)
 		require.NotNil(t, receiver)
+		assert.Equal(t, "custom", encodingFromReceiver(t, receiver, "Logs"))
+	})
+
+	t.Run("default_encoding/sarama", func(t *testing.T) {
+		setFranzGo(t, false)
+		cfg := createDefaultConfig()
+		receiver, err := f.CreateLogs(t.Context(), receivertest.NewNopSettings(metadata.Type), cfg, nil)
+		require.NoError(t, err)
+		require.NotNil(t, receiver)
+		assert.Equal(t, defaultLogsEncoding, encodingFromReceiver(t, receiver, "Logs"))
 	})
-	t.Run("default_encoding", func(t *testing.T) {
+
+	t.Run("default_encoding/franzgo", func(t *testing.T) {
+		setFranzGo(t, true)
 		cfg := createDefaultConfig()
 		receiver, err := f.CreateLogs(t.Context(), receivertest.NewNopSettings(metadata.Type), cfg, nil)
-		logsConsumer, ok := receiver.(*saramaConsumer)
-		require.True(t, ok)
-		require.Equal(t, defaultLogsEncoding, logsConsumer.config.Logs.Encoding)
 		require.NoError(t, err)
-		assert.NotNil(t, receiver)
+		require.NotNil(t, receiver)
+		assert.Equal(t, defaultLogsEncoding, encodingFromReceiver(t, receiver, "Logs"))
 	})
 }
 
@@ -139,23 +225,41 @@ func TestCreateProfiles(t *testing.T) {
 func TestWithProfilesUnmarshalers(t *testing.T) {
 	f := NewFactory()
 
-	t.Run("custom_encoding", func(t *testing.T) {
+	t.Run("custom_encoding/sarama", func(t *testing.T) {
+		setFranzGo(t, false)
+		cfg := createDefaultConfig().(*Config)
+		cfg.Profiles.Encoding = "custom"
+		receiver, err := f.(xreceiver.Factory).CreateProfiles(t.Context(), receivertest.NewNopSettings(metadata.Type), cfg, nil)
+		require.NoError(t, err)
+		require.NotNil(t, receiver)
+		assert.Equal(t, "custom", encodingFromReceiver(t, receiver, "Profiles"))
+	})
+
+	t.Run("custom_encoding/franzgo", func(t *testing.T) {
+		setFranzGo(t, true)
 		cfg := createDefaultConfig().(*Config)
 		cfg.Profiles.Encoding = "custom"
 		receiver, err := f.(xreceiver.Factory).CreateProfiles(t.Context(), receivertest.NewNopSettings(metadata.Type), cfg, nil)
-		profilesConsumer, ok := receiver.(*saramaConsumer)
-		require.True(t, ok)
-		require.Equal(t, "custom", profilesConsumer.config.Profiles.Encoding)
 		require.NoError(t, err)
 		require.NotNil(t, receiver)
+		assert.Equal(t, "custom", encodingFromReceiver(t, receiver, "Profiles"))
 	})
-	t.Run("default_encoding", func(t *testing.T) {
+
+	t.Run("default_encoding/sarama", func(t *testing.T) {
+		setFranzGo(t, false)
+		cfg := createDefaultConfig()
+		receiver, err := f.(xreceiver.Factory).CreateProfiles(t.Context(), receivertest.NewNopSettings(metadata.Type), cfg, nil)
+		require.NoError(t, err)
+		require.NotNil(t, receiver)
+		assert.Equal(t, defaultProfilesEncoding, encodingFromReceiver(t, receiver, "Profiles"))
+	})
+
+	t.Run("default_encoding/franzgo", func(t *testing.T) {
+		setFranzGo(t, true)
 		cfg := createDefaultConfig()
 		receiver, err := f.(xreceiver.Factory).CreateProfiles(t.Context(), receivertest.NewNopSettings(metadata.Type), cfg, nil)
-		profilesConsumer, ok := receiver.(*saramaConsumer)
-		require.True(t, ok)
-		require.Equal(t, defaultProfilesEncoding, profilesConsumer.config.Profiles.Encoding)
 		require.NoError(t, err)
-		assert.NotNil(t, receiver)
+		require.NotNil(t, receiver)
+		assert.Equal(t, defaultProfilesEncoding, encodingFromReceiver(t, receiver, "Profiles"))
 	})
 }
diff --git a/receiver/kafkareceiver/kafka_receiver_test.go b/receiver/kafkareceiver/kafka_receiver_test.go
index edb6c61ee239f..799e6992e47c4 100644
--- a/receiver/kafkareceiver/kafka_receiver_test.go
+++ b/receiver/kafkareceiver/kafka_receiver_test.go
@@ -61,10 +61,10 @@ func init() {
 func runTestForClients(t *testing.T, fn func(t *testing.T)) {
 	clients := []string{"Sarama", "Franz"}
 	for _, client := range clients {
-		if client == "Franz" {
-			setFranzGo(t, true)
-		}
-		t.Run(client, fn)
+		t.Run(client, func(t *testing.T) {
+			setFranzGo(t, client == "Franz")
+			fn(t)
+		})
 	}
 }
 
@@ -762,83 +762,97 @@ func TestNewProfilesReceiver(t *testing.T) {
 }
 
 func TestComponentStatus(t *testing.T) {
-	t.Parallel()
-	_, receiverConfig := mustNewFakeCluster(t, kfake.SeedTopics(1, "otlp_spans"))
-
-	statusEventCh := make(chan *componentstatus.Event, 10)
-	waitStatusEvent := func() *componentstatus.Event {
-		select {
-		case event := <-statusEventCh:
-			return event
-		case <-time.After(10 * time.Second):
-			t.Fatal("timed out waiting for status event")
+	runTestForClients(t, func(t *testing.T) {
+		_, receiverConfig := mustNewFakeCluster(t, kfake.SeedTopics(1, "otlp_spans"))
+
+		statusEventCh := make(chan *componentstatus.Event, 10)
+		waitStatusEvent := func(timeout time.Duration) *componentstatus.Event {
+			select {
+			case event := <-statusEventCh:
+				return event
+			case <-time.After(timeout):
+				return nil
+			}
 		}
-		panic("unreachable")
-	}
-	assertNoStatusEvent := func(t *testing.T) {
-		t.Helper()
-		select {
-		case event := <-statusEventCh:
-			t.Fatalf("unexpected status event received: %+v", event)
-		case <-time.After(100 * time.Millisecond):
+		assertNoStatusEvent := func(t *testing.T) {
+			t.Helper()
+			select {
+			case event := <-statusEventCh:
+				t.Fatalf("unexpected status event received: %+v", event)
+			case <-time.After(100 * time.Millisecond):
+			}
 		}
-	}
 
-	// Create an intermediate TCP listener which will proxy the connection to the
-	// fake Kafka cluster. This can be used to verify the initial "OK" status is
-	// reported only after the broker connection is established.
-	lis, err := net.Listen("tcp", "localhost:0")
-	require.NoError(t, err)
-	t.Cleanup(func() { assert.NoError(t, lis.Close()) })
-	brokers := receiverConfig.Brokers
-	receiverConfig.Brokers = []string{lis.Addr().String()}
+		// Create an intermediate TCP listener which will proxy the connection to the
+		// fake Kafka cluster. This can be used to verify the initial "OK" status is
+		// reported only after the broker connection is established.
+		lis, err := net.Listen("tcp", "localhost:0")
+		require.NoError(t, err)
+		t.Cleanup(func() { assert.NoError(t, lis.Close()) })
+		brokers := receiverConfig.Brokers
+		receiverConfig.Brokers = []string{lis.Addr().String()}
 
-	f := NewFactory()
-	r, err := f.CreateTraces(t.Context(), receivertest.NewNopSettings(metadata.Type), receiverConfig, &consumertest.TracesSink{})
-	require.NoError(t, err)
-	require.NoError(t, r.Start(t.Context(), &statusReporterHost{
-		report: func(event *componentstatus.Event) {
-			statusEventCh <- event
-		},
-	}))
-	t.Cleanup(func() {
-		assert.NoError(t, r.Shutdown(t.Context()))
-	})
+		f := NewFactory()
+		r, err := f.CreateTraces(t.Context(), receivertest.NewNopSettings(metadata.Type), receiverConfig, &consumertest.TracesSink{})
+		require.NoError(t, err)
+		require.NoError(t, r.Start(t.Context(), &statusReporterHost{
+			report: func(event *componentstatus.Event) {
+				statusEventCh <- event
+			},
+		}))
+		t.Cleanup(func() {
+			assert.NoError(t, r.Shutdown(context.Background())) //nolint:usetesting
+		})
+
+		// Connection to the Kafka cluster is asynchronous; the receiver
+		// will report that it is starting before the connection is established.
+		e := waitStatusEvent(10 * time.Second)
+		require.NotNil(t, e, "timed out waiting for StatusStarting")
+		assert.Equal(t, componentstatus.StatusStarting, e.Status())
+		// The StatusOK event should not be reported yet, as the connection to the
+		// fake Kafka cluster is not established yet.
+		assertNoStatusEvent(t)
+
+		// Accept the connection, proxy to the fake Kafka cluster.
+		var wg sync.WaitGroup
+		conn, err := lis.Accept()
+		require.NoError(t, err)
+		kfakeConn, err := net.Dial("tcp", brokers[0])
+		require.NoError(t, err)
+		wg.Add(2)
+		go func() {
+			defer wg.Done()
+			_, _ = io.Copy(conn, kfakeConn)
+		}()
+		go func() {
+			defer wg.Done()
+			_, _ = io.Copy(kfakeConn, conn)
+		}()
+		t.Cleanup(func() {
+			_ = conn.Close()
+			_ = kfakeConn.Close()
+			wg.Wait()
+		})
+
+		// Now we expect StatusOK after the proxy connects through to the fake cluster.
+		e = waitStatusEvent(10 * time.Second)
+		require.NotNil(t, e, "timed out waiting for StatusOK")
+		assert.Equal(t, componentstatus.StatusOK, e.Status())
+		assertNoStatusEvent(t)
+
+		// Shut down and check we see Stopping then Stopped.
+		require.NoError(t, r.Shutdown(t.Context()))
 
-	// Connection to the Kafka cluster is asynchronous; the receiver
-	// will report that it is starting before the connection is established.
-	assert.Equal(t, componentstatus.StatusStarting, waitStatusEvent().Status())
-	// The StatusOK event should not be reported yet, as the connection to the
-	// fake Kafka cluster is not established yet.
-	assertNoStatusEvent(t)
-
-	// Accept the connection, proxy to the fake Kafka cluster.
-	var wg sync.WaitGroup
-	conn, err := lis.Accept()
-	require.NoError(t, err)
-	kfakeConn, err := net.Dial("tcp", brokers[0])
-	require.NoError(t, err)
-	wg.Add(2)
-	go func() {
-		defer wg.Done()
-		_, _ = io.Copy(conn, kfakeConn)
-	}()
-	go func() {
-		defer wg.Done()
-		_, _ = io.Copy(kfakeConn, conn)
-	}()
-	defer wg.Wait()
-	defer conn.Close()
-	defer kfakeConn.Close()
-
-	assert.Equal(t, componentstatus.StatusOK, waitStatusEvent().Status())
-	assertNoStatusEvent(t)
-
-	assert.NoError(t, r.Shutdown(t.Context()))
-
-	assert.Equal(t, componentstatus.StatusStopping, waitStatusEvent().Status())
-	assert.Equal(t, componentstatus.StatusStopped, waitStatusEvent().Status())
-	assertNoStatusEvent(t)
+		e = waitStatusEvent(2 * time.Second)
+		require.NotNil(t, e, "expected StatusStopping")
+		assert.Equal(t, componentstatus.StatusStopping, e.Status())
+
+		e = waitStatusEvent(2 * time.Second)
+		require.NotNil(t, e, "expected StatusStopped")
+		assert.Equal(t, componentstatus.StatusStopped, e.Status())
+
+		assertNoStatusEvent(t)
+	})
 }
 
 func mustNewTracesReceiver(tb testing.TB, cfg *Config, nextConsumer consumer.Traces) {