github.com/rudderlabs/rudder-go-kit@v0.30.0/kafkaclient/client.go (about)

     1  package client
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"net"
     7  	"time"
     8  
     9  	"github.com/segmentio/kafka-go"
    10  	"golang.org/x/crypto/ssh"
    11  )
    12  
    13  // Logger specifies a logger used to report internal changes within the consumer
    14  type Logger interface {
    15  	Printf(format string, args ...interface{})
    16  }
    17  
    18  // MessageHeader is a key/value pair type representing headers set on records
    19  type MessageHeader struct {
    20  	Key   string
    21  	Value []byte
    22  }
    23  
    24  // Message is a data structure representing a Kafka message
    25  type Message struct {
    26  	Key, Value []byte
    27  	Topic      string
    28  	Partition  int32
    29  	Offset     int64
    30  	Headers    []MessageHeader
    31  	Timestamp  time.Time
    32  }
    33  
    34  type Client struct {
    35  	network   string
    36  	addresses []string
    37  	dialer    *kafka.Dialer
    38  	config    *Config
    39  }
    40  
    41  // New returns a new Kafka client
    42  func New(network string, addresses []string, conf Config) (*Client, error) {
    43  	conf.defaults()
    44  
    45  	dialer := kafka.Dialer{
    46  		DualStack: true,
    47  		Timeout:   conf.DialTimeout,
    48  	}
    49  
    50  	if conf.ClientID != "" {
    51  		dialer.ClientID = conf.ClientID
    52  	}
    53  	if conf.TLS != nil {
    54  		var err error
    55  		dialer.TLS, err = conf.TLS.build()
    56  		if err != nil {
    57  			return nil, err
    58  		}
    59  	}
    60  
    61  	if conf.SASL != nil {
    62  		var err error
    63  		dialer.SASLMechanism, err = conf.SASL.build()
    64  		if err != nil {
    65  			return nil, err
    66  		}
    67  	}
    68  
    69  	if conf.SSHConfig != nil {
    70  		signer, err := ssh.ParsePrivateKey([]byte(conf.SSHConfig.PrivateKey))
    71  		if err != nil {
    72  			return nil, fmt.Errorf("cannot parse SSH private key: %w", err)
    73  		}
    74  
    75  		sshConfig := &ssh.ClientConfig{
    76  			User:            conf.SSHConfig.User,
    77  			Auth:            []ssh.AuthMethod{ssh.PublicKeys(signer)},
    78  			Timeout:         conf.DialTimeout,
    79  			HostKeyCallback: ssh.InsecureIgnoreHostKey(), // skipcq: GSC-G106
    80  		}
    81  
    82  		dialer.DialFunc = func(ctx context.Context, network, address string) (net.Conn, error) {
    83  			sshClient, err := ssh.Dial("tcp", conf.SSHConfig.Host, sshConfig)
    84  			if err != nil {
    85  				return nil, fmt.Errorf("cannot dial SSH host %q: %w", conf.SSHConfig.Host, err)
    86  			}
    87  
    88  			conn, err := sshClient.Dial(network, address)
    89  			if err != nil {
    90  				return nil, fmt.Errorf(
    91  					"cannot dial address %q over SSH (host %q): %w", address, conf.SSHConfig.Host, err,
    92  				)
    93  			}
    94  			return conn, nil
    95  		}
    96  	}
    97  
    98  	return &Client{
    99  		network:   network,
   100  		addresses: addresses,
   101  		dialer:    &dialer,
   102  		config:    &conf,
   103  	}, nil
   104  }
   105  
   106  // NewConfluentCloud returns a Kafka client pre-configured to connect to Confluent Cloud
   107  func NewConfluentCloud(addresses []string, key, secret string, conf Config) (*Client, error) {
   108  	conf.SASL = &SASL{
   109  		ScramHashGen: ScramPlainText,
   110  		Username:     key,
   111  		Password:     secret,
   112  	}
   113  	conf.TLS = &TLS{
   114  		WithSystemCertPool: true,
   115  	}
   116  	return New("tcp", addresses, conf)
   117  }
   118  
   119  // NewAzureEventHubs returns a Kafka client pre-configured to connect to Azure Event Hubs
   120  // addresses should be in the form of "host:port" where the port is usually 9093 on Azure Event Hubs
   121  // Also make sure to select at least the Standard tier since the Basic tier does not support Kafka
   122  func NewAzureEventHubs(addresses []string, connectionString string, conf Config) (*Client, error) {
   123  	conf.SASL = &SASL{
   124  		ScramHashGen: ScramPlainText,
   125  		Username:     "$ConnectionString",
   126  		Password:     connectionString,
   127  	}
   128  	conf.TLS = &TLS{
   129  		WithSystemCertPool: true,
   130  	}
   131  	return New("tcp", addresses, conf)
   132  }
   133  
   134  // Ping is used to check the connectivity only, then it discards the connection
   135  // Ping ensures that at least one of the provided addresses is reachable.
   136  func (c *Client) Ping(ctx context.Context) error {
   137  	var lastErr error
   138  	for _, addr := range c.addresses {
   139  		conn, err := c.dialer.DialContext(ctx, c.network, kafka.TCP(addr).String())
   140  		if err == nil {
   141  			go func() { _ = conn.Close() }()
   142  			// we can connect to at least one address, no need to check all of them
   143  			return nil
   144  		}
   145  		lastErr = err
   146  	}
   147  
   148  	return fmt.Errorf(
   149  		"could not dial any of the addresses %s/%s: %w", c.network, kafka.TCP(c.addresses...).String(), lastErr,
   150  	)
   151  }