kafka_client/lib.rs
1//! Kafka Rust Client
2//!
3//! A pure Rust Kafka client library based on Tokio async runtime.
4//! Supports SASL authentication (PLAIN, SCRAM-SHA-256, SCRAM-SHA-512, GSSAPI/Kerberos)
5//! and TLS encryption.
6//!
7//! # Quick Start
8//!
9//! ```ignore
10//! use kafka_client::Client;
11//!
12//! // Create client — connects to cluster, discovers all brokers
13//! let client = Client::builder(vec!["localhost:9092".into()])
14//! .with_plaintext()
15//! .build()
16//! .await?;
17//!
18//! // Producer — send messages
19//! let producer = client.producer_default().await;
20//! producer.send(ProducerRecord::new("my-topic", b"hello".into())).await?;
21//!
22//! // Consumer — read messages
23//! let mut consumer = client.consumer_default();
24//! consumer.subscribe(vec!["my-topic".into()]).await?;
25//! let records = consumer.poll().await?;
26//!
27//! client.close().await?;
28//! ```
29//!
30//! # Advanced Configuration
31//!
32//! ```ignore
33//! // Custom producer config
34//! let producer = client.producer(
35//! ProducerConfig::new().with_acks(-1).with_retries(3)
36//! ).await;
37//!
38//! // Consumer with group coordination
39//! let mut consumer = client.consumer(
40//! ConsumerConfig::new("my-group").with_earliest()
41//! );
42//! ```
43
44// Internal modules (layered architecture)
45pub mod admin;
46mod cluster;
47pub mod connection; // Public for advanced users who need low-level access
48mod consumer;
49mod error;
50mod producer;
51mod sasl;
52pub mod transport; // Public for advanced users who need low-level access
53mod wire;
54
55// Public re-exports
56pub use error::{KafkaError, KafkaErrorCode, Result};
57pub use kafka_client_protocol as protocol;
58pub use krb5_gss::gss::GssContext;
59pub use krb5_gss::{KerberosCredentials, KerberosError};
60pub use sasl::{SaslCredentials, SaslMechanismType};
61pub use transport::{SecurityProtocol, TlsConfig};
62
63// Producer types
64pub use producer::{
65 Header, PartitionRouter, PartitionRouting, Producer, ProducerConfig, ProducerRecord,
66 RecordMetadata,
67};
68
69// Consumer types
70pub use consumer::{
71 AutoOffsetReset, Consumer, ConsumerConfig, ConsumerRecord, ConsumerStream, GroupHandle,
72 OffsetHandle, PartitionAssignmentStrategy,
73};
74
75// Metadata types (read-only queries)
76pub use cluster::MetadataCache;
77
78/// Library name
79pub const NAME: &str = env!("CARGO_PKG_NAME");
80
81/// Library version
82pub const VERSION: &str = env!("CARGO_PKG_VERSION");
83
84use std::sync::Arc;
85use std::time::Duration;
86
87use crate::cluster::ClusterClient;
88
89// ===========================================================================
90// Client — unified entry point
91// ===========================================================================
92
93/// Unified Kafka client.
94///
95/// Manages the lifecycle of the connection to a Kafka cluster internally.
96/// Provides factory methods for creating [`Producer`] and [`Consumer`] instances.
97///
98/// # Examples
99///
100/// ```ignore
101/// use kafka_client::Client;
102///
103/// let client = Client::builder(vec!["localhost:9092".into()])
104/// .with_plaintext()
105/// .build()
106/// .await?;
107///
108/// let producer = client.producer_default().await;
109/// let consumer = client.consumer_default();
110/// ```
111pub struct Client {
112 cluster: Arc<ClusterClient>,
113}
114
115impl Client {
116 /// Create a builder for constructing the client.
117 ///
118 /// Accepts hostnames or IP addresses (e.g. `"localhost:9092"`).
119 /// Hostnames are resolved during `build()`.
120 pub fn builder(bootstrap_servers: Vec<String>) -> ClientBuilder {
121 ClientBuilder::new(bootstrap_servers)
122 }
123
124 // ------------------------------------------------------------------
125 // Producer factories
126 // ------------------------------------------------------------------
127
128 /// Create a [`Producer`] with default configuration.
129 ///
130 /// Equivalent to `client.producer(ProducerConfig::default()).await`.
131 pub async fn producer_default(&self) -> Producer {
132 Producer::new(self.cluster.clone(), ProducerConfig::default()).await
133 }
134
135 /// Create a [`Producer`] with custom configuration.
136 ///
137 /// # Example
138 ///
139 /// ```ignore
140 /// let producer = client.producer(
141 /// ProducerConfig::new().with_acks(-1).with_retries(5)
142 /// ).await?;
143 /// ```
144 pub async fn producer(&self, config: ProducerConfig) -> Producer {
145 Producer::new(self.cluster.clone(), config).await
146 }
147
148 // ------------------------------------------------------------------
149 // Consumer factories
150 // ------------------------------------------------------------------
151
152 /// Create a [`Consumer`] with default configuration.
153 ///
154 /// Creates a direct-mode consumer (no consumer group). All partitions
155 /// of the subscribed topics are fetched directly from the cluster.
156 /// Use [`Consumer`](Consumer) with `ConsumerConfig::new("my-group")`
157 /// for group-coordinated consumption.
158 pub fn consumer_default(&self) -> Consumer {
159 Consumer::new(self.cluster.clone(), ConsumerConfig::default())
160 }
161
162 /// Create a [`Consumer`] with custom configuration.
163 ///
164 /// # Example
165 ///
166 /// ```ignore
167 /// // Simple consumer (no consumer group)
168 /// let consumer = client.consumer(ConsumerConfig::default());
169 ///
170 /// // Group consumer
171 /// let consumer = client.consumer(
172 /// ConsumerConfig::new("my-group").with_earliest()
173 /// );
174 /// ```
175 pub fn consumer(&self, config: ConsumerConfig) -> Consumer {
176 Consumer::new(self.cluster.clone(), config)
177 }
178
179 // ------------------------------------------------------------------
180 // Admin client
181 // ------------------------------------------------------------------
182
183 /// Create an [`AdminClient`](admin::AdminClient) for cluster management.
184 ///
185 /// Used for creating/deleting topics, listing groups, describing
186 /// the cluster, and other administrative operations.
187 pub fn admin(&self) -> admin::AdminClient {
188 admin::AdminClient::new(self.cluster.clone())
189 }
190
191 // ------------------------------------------------------------------
192 // Metadata (read-only)
193 // ------------------------------------------------------------------
194
195 /// Get a reference to the metadata cache.
196 ///
197 /// Useful for discovering topics, partitions, and broker addresses
198 /// without sending RPC requests.
199 pub fn metadata(&self) -> &MetadataCache {
200 self.cluster.metadata()
201 }
202
203 // ------------------------------------------------------------------
204 // Lifecycle
205 // ------------------------------------------------------------------
206
207 /// Force a metadata refresh from the cluster.
208 ///
209 /// Useful when you need up-to-date partition leadership information
210 /// before admin operations.
211 pub async fn refresh_metadata(&self) -> Result<()> {
212 self.cluster.refresh_metadata().await
213 }
214
215 /// Send a request to any available broker (advanced usage).
216 ///
217 /// Useful for admin operations like creating/deleting topics,
218 /// or custom protocol requests.
219 ///
220 /// # Example
221 ///
222 /// ```ignore
223 /// use kafka_client::protocol::{CreateTopicsRequest, CreateTopicsResponse};
224 ///
225 /// let response: CreateTopicsResponse = client.send_to_any_broker(&request).await?;
226 /// ```
227 pub async fn send_to_any_broker<Req, Resp>(&self, request: &Req) -> Result<Resp>
228 where
229 Req: kafka_client_protocol::Request,
230 Resp: kafka_client_protocol::Response,
231 {
232 self.cluster.send_to_any_broker(request).await
233 }
234
235 /// Close the client, releasing all broker connections.
236 pub async fn close(&self) -> Result<()> {
237 self.cluster.close().await
238 }
239}
240
241// ===========================================================================
242// ClientConfig — declarative configuration
243// ===========================================================================
244
245/// Declarative configuration for creating a [`Client`].
246///
247/// Alternative to the [`ClientBuilder`] — useful when config comes from
248/// a file, environment, or serialized source.
249///
250/// # Example
251/// ```ignore
252/// use kafka_client::{Client, ClientConfig};
253/// let client = Client::connect(ClientConfig {
254/// bootstrap_servers: vec!["localhost:9092".into()],
255/// client_id: "my-app".into(),
256/// ..Default::default()
257/// }).await?;
258/// ```
259pub struct ClientConfig {
260 /// Bootstrap server addresses (host:port strings).
261 pub bootstrap_servers: Vec<String>,
262 /// Security protocol. Defaults to `Plaintext` via `ClientBuilder`.
263 pub security_protocol: crate::transport::SecurityProtocol,
264 /// Client ID sent to Kafka brokers.
265 pub client_id: String,
266 /// SASL credentials (PLAIN, SCRAM-SHA-256, SCRAM-SHA-512).
267 pub sasl: Option<crate::sasl::SaslCredentials>,
268 /// Kerberos credentials (principal + keytab).
269 pub kerberos: Option<krb5_gss::KerberosCredentials>,
270 /// KDC hostname (Kerberos only).
271 pub kdc_host: Option<String>,
272 /// KDC port (default 88).
273 pub kdc_port: u16,
274 /// Broker hostname for the Kerberos service principal.
275 pub broker_hostname: Option<String>,
276 /// Metadata cache TTL (default 5 minutes).
277 pub metadata_ttl: Duration,
278}
279
280impl Default for ClientConfig {
281 fn default() -> Self {
282 Self {
283 bootstrap_servers: Vec::new(),
284 security_protocol: crate::transport::SecurityProtocol::Plaintext,
285 client_id: NAME.to_string(),
286 sasl: None,
287 kerberos: None,
288 kdc_host: None,
289 kdc_port: 88,
290 broker_hostname: None,
291 metadata_ttl: Duration::from_secs(300),
292 }
293 }
294}
295
296impl ClientConfig {
297 /// Apply SASL credentials and set security protocol to `SaslPlaintext`.
298 pub fn with_sasl(
299 mut self,
300 mechanism: SaslMechanismType,
301 username: String,
302 password: String,
303 ) -> Self {
304 self.sasl = Some(SaslCredentials::new(mechanism, username, password));
305 self.security_protocol = crate::transport::SecurityProtocol::SaslPlaintext;
306 self
307 }
308}
309
310// ===========================================================================
311// ClientBuilder — chainable builder (wraps ClientConfig)
312// ===========================================================================
313
314/// Builder for constructing a [`Client`].
315///
316/// Supports plaintext, TLS, SASL (PLAIN, SCRAM-SHA-256, SCRAM-SHA-512),
317/// and Kerberos (SASL/GSSAPI) authentication.
318///
319/// # Example
320/// ```ignore
321/// let client = Client::builder(vec!["localhost:9092".into()])
322/// .with_kerberos(creds)
323/// .with_kdc("kdc.example.com", 88)
324/// .build()
325/// .await?;
326/// ```
327pub struct ClientBuilder {
328 config: ClientConfig,
329}
330
331impl ClientBuilder {
332 /// Create a new builder with the given bootstrap servers.
333 pub fn new(bootstrap_servers: Vec<String>) -> Self {
334 Self {
335 config: ClientConfig {
336 bootstrap_servers,
337 security_protocol: crate::transport::SecurityProtocol::Plaintext,
338 client_id: NAME.to_string(),
339 sasl: None,
340 kerberos: None,
341 metadata_ttl: Duration::from_secs(300),
342 kdc_host: None,
343 kdc_port: 88,
344 broker_hostname: None,
345 },
346 }
347 }
348
349 // --- Security protocol ---
350
351 /// Use plaintext (no encryption, no authentication).
352 pub fn with_plaintext(mut self) -> Self {
353 self.config.security_protocol = crate::transport::SecurityProtocol::Plaintext;
354 self
355 }
356
357 /// Use TLS encryption with the given domain.
358 pub fn with_tls(mut self, domain: impl Into<String>) -> Self {
359 self.config.security_protocol =
360 crate::transport::SecurityProtocol::Ssl(crate::transport::TlsConfig {
361 domain: domain.into(),
362 ..Default::default()
363 });
364 self
365 }
366
367 /// Use TLS with full custom configuration.
368 pub fn with_tls_config(mut self, tls_config: crate::transport::TlsConfig) -> Self {
369 self.config.security_protocol = crate::transport::SecurityProtocol::Ssl(tls_config);
370 self
371 }
372
373 // --- SASL ---
374
375 /// Configure SASL authentication with a custom mechanism.
376 ///
377 /// Shortcut for `.with_sasl_credentials(...)` that also sets `SaslPlaintext`.
378 pub fn with_sasl(
379 mut self,
380 mechanism: SaslMechanismType,
381 username: impl Into<String>,
382 password: impl Into<String>,
383 ) -> Self {
384 self.config.sasl = Some(SaslCredentials::new(mechanism, username, password));
385 self.config.security_protocol = crate::transport::SecurityProtocol::SaslPlaintext;
386 self
387 }
388
389 /// Configure SASL + TLS authentication.
390 pub fn with_sasl_tls(
391 mut self,
392 tls_config: crate::transport::TlsConfig,
393 mechanism: SaslMechanismType,
394 username: impl Into<String>,
395 password: impl Into<String>,
396 ) -> Self {
397 self.config.sasl = Some(SaslCredentials::new(mechanism, username, password));
398 self.config.security_protocol = crate::transport::SecurityProtocol::SaslSsl(tls_config);
399 self
400 }
401
402 // Convenience SASL shortcuts
403
404 /// SASL PLAIN without TLS.
405 pub fn with_sasl_plaintext(
406 self,
407 username: impl Into<String>,
408 password: impl Into<String>,
409 ) -> Self {
410 self.with_sasl(SaslMechanismType::Plain, username, password)
411 }
412
413 /// SASL PLAIN with TLS (domain-based config).
414 pub fn with_sasl_ssl(
415 self,
416 domain: impl Into<String>,
417 username: impl Into<String>,
418 password: impl Into<String>,
419 ) -> Self {
420 let tls_config = crate::transport::TlsConfig {
421 domain: domain.into(),
422 ..Default::default()
423 };
424 self.with_sasl_tls(tls_config, SaslMechanismType::Plain, username, password)
425 }
426
427 /// Set SASL credentials without modifying the security protocol.
428 ///
429 /// Use this when you need to set SASL + TLS separately:
430 /// ```ignore
431 /// Client::builder(servers)
432 /// .with_tls(domain)
433 /// .with_sasl_credentials(mech, user, pass)
434 /// .build()
435 /// ```
436 pub fn with_sasl_credentials(
437 mut self,
438 mechanism: SaslMechanismType,
439 username: impl Into<String>,
440 password: impl Into<String>,
441 ) -> Self {
442 self.config.sasl = Some(SaslCredentials::new(mechanism, username, password));
443 self
444 }
445
446 // --- Kerberos ---
447
448 /// Set Kerberos credentials without modifying the security protocol.
449 ///
450 /// Use together with [`with_tls`](Self::with_tls) for TLS-secured Kerberos:
451 /// ```ignore
452 /// Client::builder(servers)
453 /// .with_tls("broker.example.com")
454 /// .with_kerberos(creds)
455 /// .with_kdc("kdc.example.com", 88)
456 /// .build()
457 /// ```
458 /// Or use [`with_kerberos_tls`](Self::with_kerberos_tls) for a single call.
459 pub fn with_kerberos(mut self, credentials: krb5_gss::KerberosCredentials) -> Self {
460 self.config.kerberos = Some(credentials);
461 self
462 }
463
464 /// Configure Kerberos + TLS in one call.
465 pub fn with_kerberos_tls(
466 mut self,
467 tls_config: crate::transport::TlsConfig,
468 credentials: krb5_gss::KerberosCredentials,
469 ) -> Self {
470 self.config.kerberos = Some(credentials);
471 self.config.security_protocol = crate::transport::SecurityProtocol::SaslSsl(tls_config);
472 self
473 }
474
475 /// Set the KDC address (host:port). Only effective when Kerberos is enabled.
476 pub fn with_kdc(mut self, host: impl Into<String>, port: u16) -> Self {
477 self.config.kdc_host = Some(host.into());
478 self.config.kdc_port = port;
479 self
480 }
481
482 /// Set the broker hostname used in the Kerberos service principal.
483 pub fn with_broker_hostname(mut self, host: impl Into<String>) -> Self {
484 self.config.broker_hostname = Some(host.into());
485 self
486 }
487
488 // --- Other settings ---
489
490 /// Set a custom client ID (sent to Kafka brokers).
491 pub fn with_client_id(mut self, client_id: impl Into<String>) -> Self {
492 self.config.client_id = client_id.into();
493 self
494 }
495
496 /// Override the metadata cache TTL. Default is 5 minutes.
497 pub fn with_metadata_ttl(mut self, ttl: Duration) -> Self {
498 self.config.metadata_ttl = ttl;
499 self
500 }
501
502 // --- Build ---
503
504 /// Connect to the cluster and build the [`Client`].
505 pub async fn build(self) -> Result<Client> {
506 Client::connect(self.config).await
507 }
508}
509
510impl Client {
511 /// Connect to a Kafka cluster from a [`ClientConfig`].
512 pub async fn connect(config: ClientConfig) -> Result<Self> {
513 let mut resolved = Vec::with_capacity(config.bootstrap_servers.len());
514 for server in &config.bootstrap_servers {
515 match tokio::net::lookup_host(server).await {
516 Ok(mut addrs) => {
517 if let Some(addr) = addrs.next() {
518 resolved.push(addr);
519 } else {
520 return Err(KafkaError::Io(format!(
521 "Failed to resolve bootstrap server: {}",
522 server
523 )));
524 }
525 }
526 Err(e) => {
527 return Err(KafkaError::Io(format!(
528 "Failed to resolve bootstrap server '{}': {}",
529 server, e
530 )));
531 }
532 }
533 }
534
535 let cluster_config = crate::cluster::ClusterConfig {
536 bootstrap_servers: resolved,
537 security_protocol: config.security_protocol,
538 client_id: config.client_id,
539 metadata_ttl: config.metadata_ttl,
540 sasl: config.sasl,
541 kerberos: config.kerberos,
542 kdc_host: config.kdc_host,
543 kdc_port: config.kdc_port,
544 broker_hostname: config.broker_hostname,
545 };
546
547 let cluster = ClusterClient::connect(cluster_config).await?;
548 Ok(Client {
549 cluster: Arc::new(cluster),
550 })
551 }
552}
553
554/// Convenience builder function — equivalent to `Client::builder(...)`.
555pub fn builder(bootstrap_servers: Vec<String>) -> ClientBuilder {
556 ClientBuilder::new(bootstrap_servers)
557}