llingr-adapter-franz

module
v0.14.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 15, 2026 License: Apache-2.0

README

llingr-adapter-franz

CI Go Reference Lint Integration Tag License Go Version

Pure Go Kafka adapter for the llingr message processing ecosystem using franz-go.

Overview

This adapter wraps twmb/franz-go to implement nexus.BrokerPort[*kgo.Record] from llingr-nexus. This adapter is pure Go with no C dependencies.

Each adapter instance consumes from a single topic. For multiple topics, create multiple consumers.

Why franz-go?

Feature franz-go confluent-kafka-go
Implementation Pure Go CGO (librdkafka)
Producing speed ~4x faster Baseline
Consuming speed ~10-20x faster Baseline
Cross-compilation Easy Complex
Static binaries Supported Not supported

Broker Compatibility

franz-go works with any Kafka-compatible broker: Apache Kafka (0.8.0+), Redpanda, Amazon MSK, Confluent Platform, and Azure Event Hubs (Kafka protocol mode).

Installation

go get github.com/llingr/llingr-adapter-franz

Quick Start

See API Summary for more options.

var builder nexus.ConsumerBuilder[*kgo.Record] // init builder impl

adapter := franzadapter.New(ctx, "my-group",
    "broker-1:9092", "broker-2:9092", "broker-3:9092")

consumer, err := adapter.CreateConsumer(builder)
if err != nil {
    log.Fatal(err)
}

defer consumer.Shutdown()
consumer.Subscribe()

Authentication

Authentication method depends on your broker configuration - check with your platform team if unsure.

Auth Method Requires Section
Username/password username, password, CA cert SASL/SCRAM
Client certificates CA cert, client cert, client key mTLS
OAuth/OIDC token endpoint URL, client ID, client secret OAUTHBEARER
AWS MSK AWS credentials or IAM role AWS MSK IAM
Confluent Cloud API key, API secret SASL/SCRAM (PLAIN)

Use NewWithOptions() for SASL, TLS, or other custom kgo.Opt settings.

SASL/SCRAM

Username/password authentication with secure hashing. Use AsSha256Mechanism() or AsSha512Mechanism() depending on your broker configuration.

import (
    "crypto/tls"
    "crypto/x509"
    "os"

    "github.com/twmb/franz-go/pkg/sasl/scram"
)

// Load CA cert (get this from your platform team)
caCert, _ := os.ReadFile("/path/to/ca.crt")
caCertPool := x509.NewCertPool()
caCertPool.AppendCertsFromPEM(caCert)

adapter, err := franzadapter.NewWithOptions(ctx, "my-group",
    []string{"broker:9093"},
    kgo.SASL(scram.Auth{
        User: "user",
        Pass: "pass",
    }.AsSha256Mechanism()),
    kgo.DialTLSConfig(&tls.Config{
        RootCAs: caCertPool,
    }),
)
TLS

Encrypt traffic and verify the broker's certificate (no SASL).

import "crypto/tls"

caCert, _ := os.ReadFile("/path/to/ca.crt")
caCertPool := x509.NewCertPool()
caCertPool.AppendCertsFromPEM(caCert)

adapter, err := franzadapter.NewWithOptions(ctx, "my-group",
    []string{"broker:9093"},
    kgo.DialTLSConfig(&tls.Config{
        RootCAs: caCertPool,
    }),
)
mTLS (Client Certificates)

Mutual TLS where the broker also verifies the client's identity.

import "crypto/tls"

// Load CA cert
caCert, _ := os.ReadFile("/path/to/ca.crt")
caCertPool := x509.NewCertPool()
caCertPool.AppendCertsFromPEM(caCert)

// Load client cert and key
clientCert, _ := tls.LoadX509KeyPair("/path/to/client.crt", "/path/to/client.key")

adapter, err := franzadapter.NewWithOptions(ctx, "my-group",
    []string{"broker:9093"},
    kgo.DialTLSConfig(&tls.Config{
        RootCAs:      caCertPool,
        Certificates: []tls.Certificate{clientCert},
    }),
)
OAUTHBEARER

JWT-based authentication for identity providers like Keycloak, Okta, or Confluent Cloud. The provider function is called on each authentication attempt (initial connect and re-auth), so it should fetch a fresh token each time.

import (
    "encoding/json"
    "net/http"
    "net/url"
    "strings"

    "github.com/twmb/franz-go/pkg/sasl/oauth"
)

// Token provider - called on each authentication attempt
tokenProvider := oauth.Oauth(func(ctx context.Context) (oauth.Auth, error) {
    // Client credentials flow (common for service-to-service)
    data := url.Values{}
    data.Set("grant_type", "client_credentials")
    data.Set("client_id", "my-service")
    data.Set("client_secret", os.Getenv("OAUTH_CLIENT_SECRET"))

    // Keycloak example - Okta, Azure AD, Auth0 etc. have different endpoint URLs
    req, _ := http.NewRequestWithContext(ctx, "POST",
        "https://keycloak.example.com/realms/kafka/protocol/openid-connect/token",
        strings.NewReader(data.Encode()))
    req.Header.Set("Content-Type", "application/x-www-form-urlencoded")

    resp, err := http.DefaultClient.Do(req)
    if err != nil {
        return oauth.Auth{}, err
    }
    defer resp.Body.Close()

    var result struct {
        AccessToken string `json:"access_token"`
    }
    if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
        return oauth.Auth{}, err
    }
    return oauth.Auth{Token: result.AccessToken}, nil
})

adapter, err := franzadapter.NewWithOptions(ctx, "my-group",
    []string{"broker:9094"},
    kgo.SASL(tokenProvider),
)

Add kgo.DialTLSConfig(tlsConfig) if your broker requires TLS (SASL_SSL vs SASL_PLAINTEXT).

AWS MSK IAM

For Amazon MSK with IAM authentication. MSK requires TLS.

import (
    "crypto/tls"

    "github.com/twmb/franz-go/pkg/sasl/aws"
)

adapter, err := franzadapter.NewWithOptions(ctx, "my-group",
    []string{"broker:9093"},
    kgo.SASL(aws.Auth{
        AccessKey: os.Getenv("AWS_ACCESS_KEY_ID"),
        SecretKey: os.Getenv("AWS_SECRET_ACCESS_KEY"),
    }.AsManagedStreamingIAMMechanism()),
    kgo.DialTLSConfig(&tls.Config{}), // empty config uses system CA pool
)
Supported Mechanisms
Mechanism Package Use Case
SCRAM-SHA-256/512 sasl/scram Secure password authentication
PLAIN sasl/plain Simple username/password
OAUTHBEARER sasl/oauth Bearer tokens (Confluent, Keycloak)
GSSAPI sasl/kerberos Kerberos/Active Directory
AWS MSK IAM sasl/aws Amazon MSK native auth

API Summary

Function Use Case
New(ctx, group, brokers...) Simple setup, no auth
NewWithOptions(ctx, group, brokers, opts...) SASL, TLS, custom settings
NewCustom() + SetClient() Full kgo.Client control
Options Behaviour

When using New() or NewWithOptions():

  • CAN override: Balancers, fetch settings, timeouts, SASL, TLS
  • CANNOT override: DisableAutoCommit, BlockRebalanceOnPoll, rebalance callbacks

These restrictions ensure correct operation with nexus consumers.

Advanced Configuration

Full Client Control (NewCustom)

For complete control over kgo.Client configuration, use the two-phase setup:

// 1. Create adapter first (without client)
adapter := franzadapter.NewCustom()

// 2. Create kgo.Client with adapter's rebalance callbacks
client, err := kgo.NewClient(
    kgo.SeedBrokers("broker:9093"),
    kgo.ConsumerGroup("my-group"),
    kgo.ConsumeTopics("orders"),
    // Required for nexus adapters:
    kgo.DisableAutoCommit(),
    kgo.BlockRebalanceOnPoll(),
    kgo.OnPartitionsAssigned(adapter.OnAssigned),
    kgo.OnPartitionsRevoked(adapter.OnRevoked),
    kgo.OnPartitionsLost(adapter.OnLost),
    // Recommended:
    kgo.Balancers(kgo.CooperativeStickyBalancer()),
    // Your options:
    kgo.SASL(scram.Auth{User: "user", Pass: "pass"}.AsSha256Mechanism()),
    kgo.DialTLSConfig(tlsConfig),
)
if err != nil {
    log.Fatal(err)
}

// 3. Wire up
adapter.SetClient(client)
consumer := adapter.CreateConsumer(builder)
Rebalance Handling

The adapter uses cooperative sticky balancing with BlockRebalanceOnPoll to coordinate rebalances with the poll loop. This is handled transparently - you don't need to configure anything unless using NewCustom().

Consumer Group Compatibility

By default the adapter uses CooperativeStickyBalancer, which provides the most efficient rebalancing behaviour by only revoking partitions that need to move rather than stopping all processing during membership changes. This is the recommended configuration for new deployments where all consumers use this adapter.

When joining an existing consumer group that includes legacy consumers using different rebalance protocols (such as Java consumers configured with range or roundrobin assignors), the Kafka group coordinator requires all members to agree on a common protocol. If the existing members don't support cooperative rebalancing, the new consumer will fail to join the group.

For these mixed-deployment scenarios, the adapter provides CompatibilityBalancers() as an opt-in option that configures multiple fallback protocols in priority order: cooperative-sticky, sticky, roundrobin, and finally range. During group join, Kafka selects the highest-priority protocol that all members support. If the preferred cooperative protocol isn't available, the adapter logs a warning explaining which protocols were unavailable and which fallback was selected.

adapter := franzadapter.NewWithOptions(ctx, "my-group",
    []string{"broker:9092"},
    franzadapter.CompatibilityBalancers(),
)

This approach is particularly useful during rolling migrations where you're gradually replacing legacy consumers with ones using this adapter - the group continues operating with the legacy protocol until all members support cooperative rebalancing, at which point Kafka automatically upgrades to the more efficient protocol. Once migration is complete and all consumers support cooperative rebalancing, you can remove the CompatibilityBalancers() option to ensure the optimal protocol is always used and any misconfiguration fails explicitly rather than silently falling back.

Bandwidth Telemetry

The adapter implements nexus.BandwidthPort for wire-level bandwidth metering. Enable it by calling WithBandwidthInterval before CreateConsumer:

adapter := franzadapter.New(ctx, "my-group", "broker:9092")
adapter.WithBandwidthInterval(time.Minute) // collection cadence: 1s to 12h
consumer, _ := adapter.CreateConsumer(builder)

The adapter emits per-interval deltas by resetting internal accumulators after each collection. Byte counts reflect decompressed payload sizes as reported by franz-go's fetch batch hooks. Compression visibility is fully supported: CompressedBytes, UncompressedBytes, and the compression algorithm name are populated on every packet.

Byte counts measure at the application payload layer rather than the wire protocol layer. This typically results in values that are fractionally lower (under 1%) than wire-level counts from the same traffic, since Kafka protocol framing and record batch headers are not included. Both measurements are legitimate for bandwidth-based metering - they simply measure at different layers of the stack.

Repository Layout

This repo is split into two Go modules:

llingr-adapter-franz/
├── go.mod                     # franzadapter module - what consumers import
├── franzadapter/              # public API + unit tests
└── integration/
    ├── go.mod                 # separate module: testcontainers + Docker deps
    ├── simple_consumer_helper_test.go
    ├── simple_consumer_test.go
    └── franz_integration_test.go

The integration tests live in their own module so the large testcontainers/Docker dependency tree (around 40 transitive packages) stays out of go.mod. Anyone running go get github.com/llingr/llingr-adapter-franz pulls only the two direct deps the runtime actually needs: franz-go and llingr-nexus.

The integration module declares replace github.com/llingr/llingr-adapter-franz => ../, so its tests always exercise the local source tree rather than a published version. The integration module is never published.

Development

# Unit tests only (fast, no Docker, franzadapter module)
make test

# Integration tests (requires Docker, integration module)
make integration

# Build, unit tests, and integration tests (the default target)
make

The franzadapter module's minimum Go version is 1.25 (inherited from franz-go v1.21+). The integration module requires Go 1.26.3, so contributors running make integration need a 1.26.3 toolchain installed. Consumers of the published franzadapter package are unaffected.

CI runs each module as a separate job; the integration job has Docker available on the runner.

Licence

Apache-2.0 - see LICENSE and COPYRIGHT. Contributions are governed by CONTRIBUTING.md.

Directories

Path Synopsis
Package franzadapter implements nexus.BrokerPort for franz-go.
Package franzadapter implements nexus.BrokerPort for franz-go.
validate
Package validate provides configuration validation for franz-go clients.
Package validate provides configuration validation for franz-go clients.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL