Skip to content

kafkaexporter: Opt-in to use franz-go client #40364

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions .chloggen/f_add-kafkaexporter-franz-go-client.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# 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: 'kafkaexporter'

# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`).
note: "Add an Alpha feature gate `exporter.kafkaexporter.UseFranzGoClient` to use franz-go in the Kafka exporter for better performance."

# Mandatory: One or more tracking issues related to the change. You can use the PR number here if no issue exists.
issues: [40364]

# (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: |
This change adds an experimental opt-in support to use the franz-go client in the Kafka exporter.
The franz-go client is a high-performance Kafka client that can improve the performance of the Kafka exporter.
The default client remains sarama, which is used by the Kafka receiver and other components.
The franz-go client can be enabled by setting `client_type=franz-go` in the configuration.

# 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]
6 changes: 6 additions & 0 deletions exporter/kafkaexporter/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,15 @@ processors for higher throughput and resiliency. Message payload encoding is con

## Configuration settings

> [!NOTE]
> You can opt-in to use [`franz-go`](https://github.com/twmb/franz-go) client by enabling the feature gate
> `exporter.kafkaexporter.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)

There are no required settings.

The following settings can be optionally configured:

- `brokers` (default = localhost:9092): The list of kafka brokers.
- `protocol_version` (default = 2.1.0): Kafka protocol version.
- `resolve_canonical_bootstrap_servers_only` (default = false): Whether to resolve then reverse-lookup broker IPs during startup.
Expand Down
2 changes: 1 addition & 1 deletion exporter/kafkaexporter/generated_package_test.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 8 additions & 3 deletions exporter/kafkaexporter/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ require (
github.com/open-telemetry/opentelemetry-collector-contrib/pkg/translator/jaeger v0.127.0
github.com/open-telemetry/opentelemetry-collector-contrib/pkg/translator/zipkin v0.127.0
github.com/stretchr/testify v1.10.0
github.com/twmb/franz-go v1.18.1
github.com/twmb/franz-go/pkg/kfake v0.0.0-20250320172111-35ab5e5f5327
go.opentelemetry.io/collector/client v1.33.0
go.opentelemetry.io/collector/component v1.33.0
go.opentelemetry.io/collector/component/componenttest v0.127.0
Expand All @@ -26,6 +28,7 @@ require (
go.opentelemetry.io/collector/consumer/consumererror v0.127.0
go.opentelemetry.io/collector/exporter v0.127.0
go.opentelemetry.io/collector/exporter/exportertest v0.127.0
go.opentelemetry.io/collector/featuregate v1.33.0
go.opentelemetry.io/collector/pdata v1.33.0
go.opentelemetry.io/collector/pdata/testdata v0.127.0
go.uber.org/goleak v1.3.0
Expand Down Expand Up @@ -74,7 +77,7 @@ require (
github.com/jcmturner/gokrb5/v8 v8.4.4 // indirect
github.com/jcmturner/rpc/v2 v2.0.3 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/compress v1.17.11 // indirect
github.com/klauspost/compress v1.18.0 // indirect
github.com/knadh/koanf/maps v0.1.2 // indirect
github.com/knadh/koanf/providers/confmap v1.0.0 // indirect
github.com/knadh/koanf/v2 v2.2.0 // indirect
Expand All @@ -87,6 +90,9 @@ require (
github.com/pierrec/lz4/v4 v4.1.22 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 // indirect
github.com/twmb/franz-go/pkg/kmsg v1.11.2 // indirect
github.com/twmb/franz-go/pkg/sasl/kerberos v1.1.0 // indirect
github.com/twmb/franz-go/plugin/kzap v1.1.2 // indirect
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
github.com/xdg-go/scram v1.1.2 // indirect
github.com/xdg-go/stringprep v1.0.4 // indirect
Expand All @@ -99,7 +105,6 @@ require (
go.opentelemetry.io/collector/exporter/xexporter v0.127.0 // indirect
go.opentelemetry.io/collector/extension v1.33.0 // indirect
go.opentelemetry.io/collector/extension/xextension v0.127.0 // indirect
go.opentelemetry.io/collector/featuregate v1.33.0 // indirect
go.opentelemetry.io/collector/internal/telemetry v0.127.0 // indirect
go.opentelemetry.io/collector/pdata/pprofile v0.127.0 // indirect
go.opentelemetry.io/collector/pipeline v0.127.0 // indirect
Expand All @@ -115,7 +120,7 @@ require (
go.opentelemetry.io/otel/trace v1.36.0 // indirect
golang.org/x/crypto v0.37.0 // indirect
golang.org/x/net v0.39.0 // indirect
golang.org/x/sys v0.32.0 // indirect
golang.org/x/sys v0.33.0 // indirect
golang.org/x/text v0.25.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a // indirect
google.golang.org/grpc v1.72.2 // indirect
Expand Down
29 changes: 23 additions & 6 deletions exporter/kafkaexporter/go.sum

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions exporter/kafkaexporter/internal/kafkaclient/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0

// Package kafkaclient provides implementations of Kafka producers using
// different client libraries (Sarama, Franz-go).
package kafkaclient // import "github.com/open-telemetry/opentelemetry-collector-contrib/exporter/kafkaexporter/internal/kafkaclient"
72 changes: 72 additions & 0 deletions exporter/kafkaexporter/internal/kafkaclient/franzgo.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0

package kafkaclient // import "github.com/open-telemetry/opentelemetry-collector-contrib/exporter/kafkaexporter/internal/kafkaclient"

import (
"context"
"errors"

"github.com/twmb/franz-go/pkg/kgo"
)

// FranzSyncProducer is a wrapper around the franz-go client that implements
// the Producer interface. Allowing us to use the franz-go client while
// maintaining compatibility with the existing Kafka exporter code.
type FranzSyncProducer struct {
client *kgo.Client
metadataKeys []string
}

// NewFranzSyncProducer Franz-go producer from a kgo.Client and a Messenger.
func NewFranzSyncProducer(client *kgo.Client,
metadataKeys []string,
) FranzSyncProducer {
return FranzSyncProducer{
client: client,
metadataKeys: metadataKeys,
}
}

// ExportData sends a batch of messages to Kafka
func (p FranzSyncProducer) ExportData(ctx context.Context, msgs Messages) error {
messages := makeFranzMessages(msgs)
setMessageHeaders(ctx, messages, p.metadataKeys,
func(key string, value []byte) kgo.RecordHeader {
return kgo.RecordHeader{Key: key, Value: value}
},
func(m *kgo.Record) []kgo.RecordHeader { return m.Headers },
func(m *kgo.Record, h []kgo.RecordHeader) { m.Headers = h },
)
result := p.client.ProduceSync(ctx, messages...)
var errs []error
for _, r := range result {
if r.Err != nil {
errs = append(errs, r.Err)
}
}
return errors.Join(errs...)
}

// Close shuts down the producer and flushes any remaining messages.
func (p FranzSyncProducer) Close() error {
p.client.Close()
return nil
}

func makeFranzMessages(messages Messages) []*kgo.Record {
msgs := make([]*kgo.Record, 0, messages.Count)
for _, msg := range messages.TopicMessages {
for _, message := range msg.Messages {
msg := &kgo.Record{Topic: msg.Topic}
if message.Key != nil {
msg.Key = message.Key
}
if message.Value != nil {
msg.Value = message.Value
}
msgs = append(msgs, msg)
}
}
return msgs
}
72 changes: 72 additions & 0 deletions exporter/kafkaexporter/internal/kafkaclient/headers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0

package kafkaclient // import "github.com/open-telemetry/opentelemetry-collector-contrib/exporter/kafkaexporter/internal/kafkaclient"

import (
"context"

"go.opentelemetry.io/collector/client"
)

// metadataToHeaders converts metadata from the context into a slice of headers using the provided header constructor.
// This function is generic and can be used for both Sarama and Franz-go header types.
func metadataToHeaders[H any](ctx context.Context, keys []string,
makeHeader func(key string, value []byte) H,
) []H {
if len(keys) == 0 {
return nil
}
info := client.FromContext(ctx)
headers := make([]H, 0, len(keys))
for _, key := range keys {
valueSlice := info.Metadata.Get(key)
for _, v := range valueSlice {
headers = append(headers, makeHeader(key, []byte(v)))
}
}
return headers
}

// setMessageHeaders is a generic helper for setting headers on a slice of messages.
// - messages: the messages to set headers on
// - ctx: context for extracting metadata
// - metadataKeys: which metadata keys to extract
// - makeHeader: constructs the header type for the target client (Sarama/Franz-go)
// - getHeaders: gets the headers from a message
// - setHeaders: sets the headers on a message
// Usage example (Sarama):
//
// setMessageHeaders(ctx, allMessages, keys, makeHeader, getHeaders, setHeaders)
func setMessageHeaders[M any, H any](ctx context.Context,
messages []M,
metadataKeys []string,
makeHeader func(key string, value []byte) H,
getHeaders func(M) []H,
setHeadersFunc func(M, []H),
Comment on lines +44 to +46
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there anyway around needing to pass callback functions here?

Copy link
Contributor Author

@marclop marclop Jun 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can refactor this once we permanently remove Sarama to not have this abstraction.

Currently, this ensures the same logic is applied for both adapters. I don't love it but it seems like the best approach to apply this generic flow consistently.

) {
setHeaders(
messages,
metadataToHeaders(ctx, metadataKeys, makeHeader),
getHeaders,
setHeadersFunc,
)
}

// setHeaders sets or appends headers on each message in messages using the provided get/set functions.
func setHeaders[M any, H any](messages []M, headers []H,
getHeaders func(M) []H,
setHeaders func(M, []H),
) {
if len(headers) == 0 || len(messages) == 0 {
return
}
for i := range messages {
h := getHeaders(messages[i])
if len(h) == 0 {
setHeaders(messages[i], headers)
} else {
setHeaders(messages[i], append(h, headers...))
}
}
}
21 changes: 21 additions & 0 deletions exporter/kafkaexporter/internal/kafkaclient/message.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0

package kafkaclient

import "github.com/open-telemetry/opentelemetry-collector-contrib/exporter/kafkaexporter/internal/marshaler"

// Messages is a collection of messages (with count) to be sent to Kafka.
type Messages struct {
// Count is the total number of messages across all topics.
// Populating this field allows the downstream method to preallocate
// the slice of messages, which can improve performance.
Count int
TopicMessages []TopicMessages
}

// TopicMessages represents a collection of messages for a specific topic.
type TopicMessages struct {
Topic string
Messages []marshaler.Message
}
Loading
Loading