1pub mod avro;
5pub mod config;
6mod metadata_error;
7pub mod metrics;
8mod offsets;
9pub mod rebalance;
10pub mod source;
11mod vnode_routing;
12
13pub mod avro_serializer;
15pub mod partitioner;
16pub mod sink;
17pub mod sink_config;
18pub mod sink_metrics;
19
20pub mod schema_registry;
22
23pub use avro::AvroDeserializer;
25pub use config::{
26 AssignmentStrategy, CompatibilityLevel, IsolationLevel, KafkaSourceConfig, OffsetReset,
27 SaslMechanism, SchemaEvolutionStrategy, SecurityProtocol, SrAuth, StartupMode,
28 TopicSubscription,
29};
30pub use metrics::KafkaSourceMetrics;
31pub use source::KafkaSource;
32
33pub use avro_serializer::AvroSerializer;
35pub use partitioner::{
36 KafkaPartitioner, KeyHashPartitioner, RoundRobinPartitioner, StickyPartitioner,
37};
38pub use sink::KafkaSink;
39pub use sink_config::{CompressionType, KafkaSinkConfig, PartitionStrategy};
40pub use sink_metrics::KafkaSinkMetrics;
41
42#[cfg(feature = "testing")]
44pub mod testing {
45 use crate::error::ConnectorError;
46
47 pub fn partition_vnodes(
53 source_identity: &str,
54 topic: &str,
55 total_partitions: i32,
56 vnode_count: u32,
57 ) -> Result<Vec<u32>, ConnectorError> {
58 super::vnode_routing::partition_vnodes(
59 source_identity,
60 topic,
61 total_partitions,
62 vnode_count,
63 )
64 }
65}
66
67pub use schema_registry::{CachedSchema, CompatibilityResult, SchemaRegistryClient, SchemaType};
69
70use std::sync::Arc;
71
72use crate::config::{ConfigKeySpec, ConnectorInfo};
73use crate::registry::ConnectorRegistry;
74
75pub fn register_kafka_source(
82 registry: &ConnectorRegistry,
83) -> Result<(), crate::error::ConnectorError> {
84 let info = ConnectorInfo {
85 name: "kafka".to_string(),
86 display_name: "Apache Kafka Source".to_string(),
87 version: env!("CARGO_PKG_VERSION").to_string(),
88 is_source: true,
89 is_sink: false,
90 config_keys: kafka_source_config_keys(),
91 };
92
93 registry.register_source(
94 "kafka",
95 info,
96 Arc::new(|registry: Option<&Arc<prometheus::Registry>>| {
97 let empty = Arc::new(arrow_schema::Schema::empty());
99 Ok(Box::new(KafkaSource::new(
100 empty,
101 KafkaSourceConfig::default(),
102 registry.map(Arc::as_ref),
103 )))
104 }),
105 )
106}
107
108pub fn register_kafka_sink(
115 registry: &ConnectorRegistry,
116) -> Result<(), crate::error::ConnectorError> {
117 let info = ConnectorInfo {
118 name: "kafka".to_string(),
119 display_name: "Apache Kafka Sink".to_string(),
120 version: env!("CARGO_PKG_VERSION").to_string(),
121 is_source: false,
122 is_sink: true,
123 config_keys: kafka_sink_config_keys(),
124 };
125
126 registry.register_sink(
127 "kafka",
128 info,
129 Arc::new(|_config, registry: Option<&Arc<prometheus::Registry>>| {
130 let empty = Arc::new(arrow_schema::Schema::empty());
132 Ok(Box::new(KafkaSink::new(
133 empty,
134 KafkaSinkConfig::default(),
135 registry.map(Arc::as_ref),
136 )))
137 }),
138 )
139}
140
141#[allow(clippy::too_many_lines)]
143fn kafka_source_config_keys() -> Vec<ConfigKeySpec> {
144 vec![
145 ConfigKeySpec::required("bootstrap.servers", "Kafka broker addresses"),
147 ConfigKeySpec::required("group.id", "Consumer group identifier"),
148 ConfigKeySpec::optional(
150 "topic",
151 "Comma-separated topics (required unless topic.pattern is set)",
152 "",
153 ),
154 ConfigKeySpec::optional(
155 "topic.pattern",
156 "Topic regex (required unless topic is set)",
157 "",
158 ),
159 ConfigKeySpec::optional("format", "Data format (json/csv/avro/raw/debezium)", "json"),
161 ConfigKeySpec::optional(
163 "security.protocol",
164 "Security protocol (plaintext/ssl/sasl_plaintext/sasl_ssl)",
165 "plaintext",
166 ),
167 ConfigKeySpec::optional(
168 "sasl.mechanism",
169 "SASL mechanism (PLAIN/SCRAM-SHA-256/SCRAM-SHA-512/GSSAPI/OAUTHBEARER)",
170 "",
171 ),
172 ConfigKeySpec::optional("sasl.username", "SASL username for PLAIN/SCRAM", ""),
173 ConfigKeySpec::optional("sasl.password", "SASL password for PLAIN/SCRAM", ""),
174 ConfigKeySpec::optional("ssl.ca.location", "SSL CA certificate file path", ""),
175 ConfigKeySpec::optional(
176 "ssl.certificate.location",
177 "Client SSL certificate file path",
178 "",
179 ),
180 ConfigKeySpec::optional("ssl.key.location", "Client SSL private key file path", ""),
181 ConfigKeySpec::optional("ssl.key.password", "Password for encrypted SSL key", ""),
182 ConfigKeySpec::optional(
184 "startup.mode",
185 "Startup mode (group-offsets/earliest/latest)",
186 "group-offsets",
187 ),
188 ConfigKeySpec::optional(
189 "startup.specific.offsets",
190 "Start from specific offsets (format: 'partition:offset,...')",
191 "",
192 ),
193 ConfigKeySpec::optional(
194 "startup.timestamp.ms",
195 "Start from timestamp (milliseconds since epoch)",
196 "",
197 ),
198 ConfigKeySpec::optional(
199 "auto.offset.reset",
200 "Fallback when no committed offset (earliest/latest/none)",
201 "earliest",
202 ),
203 ConfigKeySpec::optional(
204 "isolation.level",
205 "Transaction isolation (read_uncommitted/read_committed)",
206 "read_committed",
207 ),
208 ConfigKeySpec::optional("max.poll.records", "Max records per poll", "1000"),
209 ConfigKeySpec::optional(
210 "partition.assignment.strategy",
211 "Partition assignment (range/roundrobin/cooperative-sticky)",
212 "range",
213 ),
214 ConfigKeySpec::optional(
216 "session.timeout.ms",
217 "Consumer session timeout in milliseconds (production-safe default)",
218 "45000",
219 ),
220 ConfigKeySpec::optional(
221 "heartbeat.interval.ms",
222 "Consumer heartbeat interval in milliseconds",
223 "10000",
224 ),
225 ConfigKeySpec::optional(
226 "queued.max.messages.kbytes",
227 "Max per-partition pre-fetch queue size in kbytes",
228 "16384",
229 ),
230 ConfigKeySpec::optional("fetch.min.bytes", "Minimum bytes per fetch request", "1"),
232 ConfigKeySpec::optional(
233 "fetch.max.bytes",
234 "Maximum bytes per fetch request",
235 "52428800",
236 ),
237 ConfigKeySpec::optional(
238 "fetch.max.wait.ms",
239 "Max wait time for fetch.min.bytes",
240 "500",
241 ),
242 ConfigKeySpec::optional(
243 "max.partition.fetch.bytes",
244 "Max bytes per partition per fetch",
245 "1048576",
246 ),
247 ConfigKeySpec::optional(
249 "include.metadata",
250 "Include _partition/_offset/_timestamp columns",
251 "false",
252 ),
253 ConfigKeySpec::optional("include.headers", "Include _headers column", "false"),
254 ConfigKeySpec::optional(
256 "backpressure.high.watermark",
257 "Channel fill ratio to pause",
258 "0.8",
259 ),
260 ConfigKeySpec::optional(
261 "backpressure.low.watermark",
262 "Channel fill ratio to resume",
263 "0.25",
264 ),
265 ConfigKeySpec::optional(
267 "max.deser.error.rate",
268 "Max tolerated deserialization error rate per batch (0.0-1.0)",
269 "0.5",
270 ),
271 ConfigKeySpec::optional(
273 "schema.registry.url",
274 "Confluent Schema Registry URL (required for Avro)",
275 "",
276 ),
277 ConfigKeySpec::optional("schema.registry.username", "Schema Registry username", ""),
278 ConfigKeySpec::optional("schema.registry.password", "Schema Registry password", ""),
279 ConfigKeySpec::optional(
280 "schema.registry.ssl.ca.location",
281 "Schema Registry SSL CA cert path",
282 "",
283 ),
284 ConfigKeySpec::optional(
285 "schema.registry.ssl.certificate.location",
286 "Schema Registry SSL client cert path",
287 "",
288 ),
289 ConfigKeySpec::optional(
290 "schema.registry.ssl.key.location",
291 "Schema Registry SSL client key path",
292 "",
293 ),
294 ConfigKeySpec::optional(
295 "schema.compatibility",
296 "Schema compatibility level override",
297 "",
298 ),
299 ConfigKeySpec::optional(
300 "schema.evolution.strategy",
301 "Runtime schema evolution handling (log/reject/ignore)",
302 "log",
303 ),
304 ConfigKeySpec::optional(
305 "schema.registry.subject.name.strategy",
306 "Schema Registry subject naming (topic-name/record-name/topic-record-name)",
307 "topic-name",
308 ),
309 ConfigKeySpec::optional(
310 "schema.registry.record.name",
311 "Avro record name for record-based subject naming",
312 "",
313 ),
314 ConfigKeySpec::optional(
315 "schema.registry.discovery.timeout.ms",
316 "Schema discovery timeout in milliseconds",
317 "10000",
318 ),
319 ConfigKeySpec::optional(
320 "max.poll.interval.ms",
321 "Maximum interval between consumer polls in milliseconds",
322 "600000",
323 ),
324 ConfigKeySpec::optional(
325 "broker.commit.on.checkpoint",
326 "Commit broker offsets after checkpoint completion",
327 "true",
328 ),
329 ConfigKeySpec::optional(
330 "reader.channel.capacity",
331 "Bounded reader channel capacity in records",
332 "8192",
333 ),
334 ]
335}
336
337fn kafka_sink_config_keys() -> Vec<ConfigKeySpec> {
339 vec![
340 ConfigKeySpec::required("bootstrap.servers", "Kafka broker addresses"),
342 ConfigKeySpec::required("topic", "Target Kafka topic"),
343 ConfigKeySpec::optional("format", "Serialization format (json/csv/avro/raw)", "json"),
345 ConfigKeySpec::optional(
347 "security.protocol",
348 "Security protocol (plaintext/ssl/sasl_plaintext/sasl_ssl)",
349 "plaintext",
350 ),
351 ConfigKeySpec::optional(
352 "sasl.mechanism",
353 "SASL mechanism (PLAIN/SCRAM-SHA-256/SCRAM-SHA-512/GSSAPI/OAUTHBEARER)",
354 "",
355 ),
356 ConfigKeySpec::optional("sasl.username", "SASL username for PLAIN/SCRAM", ""),
357 ConfigKeySpec::optional("sasl.password", "SASL password for PLAIN/SCRAM", ""),
358 ConfigKeySpec::optional("ssl.ca.location", "SSL CA certificate file path", ""),
359 ConfigKeySpec::optional(
360 "ssl.certificate.location",
361 "Client SSL certificate file path",
362 "",
363 ),
364 ConfigKeySpec::optional("ssl.key.location", "Client SSL private key file path", ""),
365 ConfigKeySpec::optional("ssl.key.password", "Password for encrypted SSL key", ""),
366 ConfigKeySpec::optional(
367 "max.in.flight.requests",
368 "Maximum in-flight producer requests per connection (1-5)",
369 "5",
370 ),
371 ConfigKeySpec::optional(
372 "delivery.timeout.ms",
373 "Delivery timeout in milliseconds",
374 "120000",
375 ),
376 ConfigKeySpec::optional("key.column", "Column name to use as Kafka message key", ""),
378 ConfigKeySpec::optional("envelope", "Output envelope (append/upsert)", "append"),
379 ConfigKeySpec::optional(
380 "partitioner",
381 "Partitioning strategy (key-hash/round-robin/sticky)",
382 "key-hash",
383 ),
384 ConfigKeySpec::optional("linger.ms", "Producer linger time in milliseconds", "5"),
386 ConfigKeySpec::optional("batch.size", "Producer batch size in bytes", "16384"),
387 ConfigKeySpec::optional("batch.num.messages", "Max messages per batch", "10000"),
388 ConfigKeySpec::optional(
389 "compression.type",
390 "Compression (none/gzip/snappy/lz4/zstd)",
391 "none",
392 ),
393 ConfigKeySpec::optional(
395 "dlq.topic",
396 "Dead letter queue topic for failed records",
397 "",
398 ),
399 ConfigKeySpec::optional(
401 "schema.registry.url",
402 "Confluent Schema Registry URL (required for Avro)",
403 "",
404 ),
405 ConfigKeySpec::optional("schema.registry.username", "Schema Registry username", ""),
406 ConfigKeySpec::optional("schema.registry.password", "Schema Registry password", ""),
407 ConfigKeySpec::optional(
408 "schema.registry.ssl.ca.location",
409 "Schema Registry SSL CA cert path",
410 "",
411 ),
412 ConfigKeySpec::optional(
413 "schema.compatibility",
414 "Schema compatibility level override",
415 "",
416 ),
417 ]
418}
419
420#[cfg(test)]
421mod tests {
422 use super::*;
423
424 #[test]
425 fn test_register_kafka_source() {
426 let registry = ConnectorRegistry::new();
427 register_kafka_source(®istry).unwrap();
428
429 let sources = registry.list_sources();
430 assert!(sources.contains(&"kafka".to_string()));
431
432 let info = registry.source_info("kafka");
433 assert!(info.is_some());
434 let info = info.unwrap();
435 assert_eq!(info.name, "kafka");
436 assert!(info.is_source);
437 assert!(!info.is_sink);
438 let required: Vec<&str> = info
439 .config_keys
440 .iter()
441 .filter(|key| key.required)
442 .map(|key| key.key.as_str())
443 .collect();
444 assert!(required.contains(&"bootstrap.servers"));
445 assert!(required.contains(&"group.id"));
446 assert!(!required.contains(&"topic"));
447 assert!(!required.contains(&"topic.pattern"));
448 assert!(info.config_keys.iter().any(|key| key.key == "topic"));
449 assert!(info
450 .config_keys
451 .iter()
452 .any(|key| key.key == "topic.pattern"));
453 for supported in [
454 "schema.registry.subject.name.strategy",
455 "schema.registry.record.name",
456 "schema.registry.discovery.timeout.ms",
457 "max.poll.interval.ms",
458 "broker.commit.on.checkpoint",
459 "reader.channel.capacity",
460 ] {
461 assert!(
462 info.config_keys.iter().any(|key| key.key == supported),
463 "registered Kafka source descriptor omits {supported}"
464 );
465 }
466 }
467
468 #[test]
469 fn test_factory_creates_source() {
470 let registry = ConnectorRegistry::new();
471 register_kafka_source(®istry).unwrap();
472
473 let config = crate::config::ConnectorConfig::new("kafka");
474 let source = registry.create_source(&config, None);
475 assert!(source.is_ok());
476 }
477
478 #[test]
479 fn test_register_kafka_sink() {
480 let registry = ConnectorRegistry::new();
481 register_kafka_sink(®istry).unwrap();
482
483 let sinks = registry.list_sinks();
484 assert!(sinks.contains(&"kafka".to_string()));
485
486 let info = registry.sink_info("kafka");
487 assert!(info.is_some());
488 let info = info.unwrap();
489 assert_eq!(info.name, "kafka");
490 assert!(!info.is_source);
491 assert!(info.is_sink);
492 assert!(info.config_keys.iter().any(|key| key.key == "envelope"));
493 assert!(info.config_keys.iter().any(|key| key.key == "key.column"));
494 assert!(!info
495 .config_keys
496 .iter()
497 .any(|key| key.key == "delivery.guarantee"));
498 }
499
500 #[test]
501 fn test_factory_creates_sink() {
502 let registry = ConnectorRegistry::new();
503 register_kafka_sink(®istry).unwrap();
504
505 let config = crate::config::ConnectorConfig::new("kafka");
506 let sink = registry.create_sink(&config, None);
507 assert!(sink.is_ok());
508 }
509}