1pub mod avro;
5pub mod config;
6pub mod latency_profile;
7mod librdkafka;
8mod metadata_error;
9pub mod metrics;
10mod offsets;
11pub mod rebalance;
12pub mod source;
13mod topic_config;
14mod vnode_routing;
15
16pub mod avro_serializer;
18pub mod partitioner;
19pub mod sink;
20pub mod sink_config;
21pub mod sink_metrics;
22
23mod schema_configuration;
25#[cfg(test)]
26mod schema_contract_tests;
27pub mod schema_registry;
28mod schema_resolution;
29
30pub use avro::AvroDeserializer;
32pub use config::{
33 AssignmentStrategy, CompatibilityLevel, GroupProtocol, IsolationLevel, KafkaMetadataColumn,
34 KafkaMetadataColumns, KafkaSourceConfig, LatencyProfile, OffsetReset, SaslMechanism,
35 SchemaEvolutionStrategy, SecurityProtocol, SrAuth, StartupMode, TopicSubscription,
36};
37pub use metrics::KafkaSourceMetrics;
38pub use source::KafkaSource;
39
40pub use avro_serializer::AvroSerializer;
42pub use partitioner::{
43 KafkaPartitioner, KeyHashPartitioner, RoundRobinPartitioner, StickyPartitioner,
44};
45pub use sink::KafkaSink;
46pub use sink_config::{CompressionType, KafkaSinkConfig, KeyFormat, PartitionStrategy};
47pub use sink_metrics::KafkaSinkMetrics;
48
49#[cfg(feature = "testing")]
51pub mod testing {
52 use crate::error::ConnectorError;
53
54 pub fn partition_vnodes(
60 source_identity: &str,
61 topic: &str,
62 total_partitions: i32,
63 vnode_count: u32,
64 ) -> Result<Vec<u32>, ConnectorError> {
65 super::vnode_routing::partition_vnodes(
66 source_identity,
67 topic,
68 total_partitions,
69 vnode_count,
70 )
71 }
72}
73
74pub use schema_registry::{CachedSchema, CompatibilityResult, SchemaRegistryClient, SchemaType};
76
77use std::sync::Arc;
78
79use crate::config::{ConfigKeySpec, ConnectorInfo};
80use crate::registry::ConnectorRegistry;
81
82pub fn register_kafka_source(
89 registry: &ConnectorRegistry,
90) -> Result<(), crate::error::ConnectorError> {
91 let info = ConnectorInfo {
92 schema_capabilities: crate::schema::resolution::SchemaCapabilities {
93 built_in_formats: vec![
94 "raw".into(),
95 "bytes".into(),
96 #[cfg(feature = "fpml")]
97 "fpml".into(),
98 ],
99 ..crate::schema::resolution::SchemaCapabilities::metadata(
100 &["avro"],
101 false,
102 crate::schema::resolution::SchemaPreparation::None,
103 )
104 },
105 name: "kafka".to_string(),
106 display_name: "Apache Kafka Source".to_string(),
107 version: env!("CARGO_PKG_VERSION").to_string(),
108 is_source: true,
109 is_sink: false,
110 config_keys: kafka_source_config_keys(),
111 };
112
113 registry.register_source(
114 "kafka",
115 info,
116 Arc::new(|registry: Option<&Arc<prometheus::Registry>>| {
117 let empty = Arc::new(arrow_schema::Schema::empty());
119 Ok(Box::new(KafkaSource::new(
120 empty,
121 KafkaSourceConfig::default(),
122 registry.map(Arc::as_ref),
123 )))
124 }),
125 )
126}
127
128pub fn register_kafka_sink(
135 registry: &ConnectorRegistry,
136) -> Result<(), crate::error::ConnectorError> {
137 let info = ConnectorInfo {
138 schema_capabilities: crate::schema::resolution::SchemaCapabilities::metadata(
139 &["avro"],
140 true,
141 crate::schema::resolution::SchemaPreparation::ExplicitRegistration,
142 ),
143 name: "kafka".to_string(),
144 display_name: "Apache Kafka Sink".to_string(),
145 version: env!("CARGO_PKG_VERSION").to_string(),
146 is_source: false,
147 is_sink: true,
148 config_keys: kafka_sink_config_keys(),
149 };
150
151 registry.register_sink(
152 "kafka",
153 info,
154 Arc::new(|config, registry: Option<&Arc<prometheus::Registry>>| {
155 let empty = Arc::new(arrow_schema::Schema::empty());
157 let parsed = if config.properties().is_empty() {
159 KafkaSinkConfig::default()
160 } else {
161 KafkaSinkConfig::from_config(config)?
162 };
163 Ok(Box::new(KafkaSink::new(
164 empty,
165 parsed,
166 registry.map(Arc::as_ref),
167 )))
168 }),
169 )
170}
171
172fn kafka_source_config_keys() -> Vec<ConfigKeySpec> {
174 let mut keys = vec![
175 ConfigKeySpec::required("bootstrap.servers", "Kafka broker addresses"),
177 ConfigKeySpec::required("group.id", "Consumer group identifier"),
178 ConfigKeySpec::optional(
180 "topic",
181 "Comma-separated topics (required unless topic.pattern is set)",
182 "",
183 ),
184 ConfigKeySpec::optional(
185 "topic.pattern",
186 "Topic regex (required unless topic is set)",
187 "",
188 ),
189 ConfigKeySpec::optional(
191 "format",
192 "Data format (json/csv/avro/raw/debezium/fpml)",
193 "json",
194 ),
195 ConfigKeySpec::optional(
197 "security.protocol",
198 "Security protocol (plaintext/ssl/sasl_plaintext/sasl_ssl)",
199 "plaintext",
200 ),
201 ConfigKeySpec::optional(
202 "sasl.mechanism",
203 "SASL mechanism (PLAIN/SCRAM-SHA-256/SCRAM-SHA-512/GSSAPI/OAUTHBEARER)",
204 "",
205 ),
206 ConfigKeySpec::optional("sasl.username", "SASL username for PLAIN/SCRAM", ""),
207 ConfigKeySpec::optional("sasl.password", "SASL password for PLAIN/SCRAM", ""),
208 ConfigKeySpec::optional("ssl.ca.location", "SSL CA certificate file path", ""),
209 ConfigKeySpec::optional(
210 "ssl.certificate.location",
211 "Client SSL certificate file path",
212 "",
213 ),
214 ConfigKeySpec::optional("ssl.key.location", "Client SSL private key file path", ""),
215 ConfigKeySpec::optional("ssl.key.password", "Password for encrypted SSL key", ""),
216 ];
217 keys.extend(kafka_source_consumer_config_keys());
218 #[cfg(feature = "fpml")]
219 keys.extend(crate::fpml::message::config_keys());
220 keys.extend([
221 ConfigKeySpec::optional(
223 "schema.registry.url",
224 "Confluent Schema Registry URL (required for Avro)",
225 "",
226 ),
227 ConfigKeySpec::optional("schema.registry.username", "Schema Registry username", ""),
228 ConfigKeySpec::optional("schema.registry.password", "Schema Registry password", ""),
229 ConfigKeySpec::optional(
230 "schema.registry.ssl.ca.location",
231 "Schema Registry SSL CA cert path",
232 "",
233 ),
234 ConfigKeySpec::optional(
235 "schema.registry.ssl.certificate.location",
236 "Schema Registry SSL client cert path",
237 "",
238 ),
239 ConfigKeySpec::optional(
240 "schema.registry.ssl.key.location",
241 "Schema Registry SSL client key path",
242 "",
243 ),
244 ConfigKeySpec::optional(
245 "schema.compatibility",
246 "Schema compatibility level override",
247 "",
248 ),
249 ConfigKeySpec::optional(
250 "schema.evolution.strategy",
251 "Runtime schema evolution handling (log/reject/ignore)",
252 "log",
253 ),
254 ConfigKeySpec::optional(
255 "schema.registry.subject.name.strategy",
256 "Schema Registry subject naming (topic-name/record-name/topic-record-name)",
257 "topic-name",
258 ),
259 ConfigKeySpec::optional(
260 "schema.registry.record.name",
261 "Avro record name for record-based subject naming",
262 "",
263 ),
264 ConfigKeySpec::optional(
265 "schema.registry.discovery.timeout.ms",
266 "Schema discovery timeout in milliseconds",
267 "10000",
268 ),
269 ConfigKeySpec::optional(
270 "max.poll.interval.ms",
271 "Maximum interval between consumer polls in milliseconds",
272 "600000",
273 ),
274 ConfigKeySpec::optional(
275 "broker.commit.on.checkpoint",
276 "Commit broker offsets after checkpoint completion",
277 "true",
278 ),
279 ConfigKeySpec::optional(
280 "reader.channel.capacity",
281 "Bounded reader channel capacity in records",
282 "8192",
283 ),
284 ]);
285 keys
286}
287
288fn kafka_source_consumer_config_keys() -> Vec<ConfigKeySpec> {
289 vec![
290 ConfigKeySpec::optional(
292 "startup.mode",
293 "Startup mode (group-offsets/earliest/latest)",
294 "group-offsets",
295 ),
296 ConfigKeySpec::optional(
297 "startup.specific.offsets",
298 "Start from specific offsets (format: 'partition:offset,...')",
299 "",
300 ),
301 ConfigKeySpec::optional(
302 "startup.timestamp.ms",
303 "Start from timestamp (milliseconds since epoch)",
304 "",
305 ),
306 ConfigKeySpec::optional(
307 "auto.offset.reset",
308 "Fallback when no committed offset (earliest/latest/none)",
309 "earliest",
310 ),
311 ConfigKeySpec::optional(
312 "isolation.level",
313 "Transaction isolation (read_uncommitted/read_committed)",
314 "read_committed",
315 ),
316 ConfigKeySpec::optional(
317 "latency.profile",
318 "Fetch preset (low_latency/balanced/throughput); explicit options override it",
319 "balanced",
320 ),
321 ConfigKeySpec::optional(
322 "group.protocol",
323 "Consumer group protocol (classic/consumer); consumer is KIP-848",
324 "classic",
325 ),
326 ConfigKeySpec::optional("max.poll.records", "Max records per poll", "1000"),
327 ConfigKeySpec::optional(
328 "partition.assignment.strategy",
329 "Partition assignment (range/roundrobin/cooperative-sticky)",
330 "range",
331 ),
332 ConfigKeySpec::optional(
334 "session.timeout.ms",
335 "Consumer session timeout in milliseconds (production-safe default)",
336 "45000",
337 ),
338 ConfigKeySpec::optional(
339 "heartbeat.interval.ms",
340 "Consumer heartbeat interval in milliseconds",
341 "10000",
342 ),
343 ConfigKeySpec::optional(
344 "queued.max.messages.kbytes",
345 "Max per-partition pre-fetch queue size in kbytes",
346 "16384",
347 ),
348 ConfigKeySpec::optional("fetch.min.bytes", "Minimum bytes per fetch request", "1"),
350 ConfigKeySpec::optional(
351 "fetch.max.bytes",
352 "Maximum bytes per fetch request",
353 "52428800",
354 ),
355 ConfigKeySpec::optional(
356 "fetch.max.wait.ms",
357 "Max wait time for fetch.min.bytes",
358 "500",
359 ),
360 ConfigKeySpec::optional(
361 "max.partition.fetch.bytes",
362 "Max bytes per partition per fetch",
363 "1048576",
364 ),
365 ConfigKeySpec::optional(
367 "metadata.columns",
368 "Typed record metadata (key,topic,partition,offset,timestamp,timestamp_type,headers)",
369 "",
370 ),
371 ConfigKeySpec::optional(
372 "max.poll.bytes",
373 "Key, value and header bytes staged per poll (at least one record)",
374 "16777216",
375 ),
376 ConfigKeySpec::optional(
377 "reader.channel.max.bytes",
378 "Key, value and header bytes the reader queue may hold",
379 "67108864",
380 ),
381 ConfigKeySpec::optional(
382 "record.max.bytes",
383 "Largest accepted record key and value; larger records fail or are skipped",
384 "16777216",
385 ),
386 ConfigKeySpec::optional(
388 "backpressure.high.watermark",
389 "Channel fill ratio to pause",
390 "0.8",
391 ),
392 ConfigKeySpec::optional(
393 "backpressure.low.watermark",
394 "Channel fill ratio to resume",
395 "0.25",
396 ),
397 ConfigKeySpec::optional(
399 "max.deser.error.rate",
400 "Max tolerated deserialization error rate per batch (0.0-1.0)",
401 "0.5",
402 ),
403 ]
404}
405
406fn kafka_sink_config_keys() -> Vec<ConfigKeySpec> {
408 vec![
409 ConfigKeySpec::required("bootstrap.servers", "Kafka broker addresses"),
411 ConfigKeySpec::required("topic", "Target Kafka topic"),
412 ConfigKeySpec::optional("format", "Serialization format (json/csv/avro/raw)", "json"),
414 ConfigKeySpec::optional(
416 "security.protocol",
417 "Security protocol (plaintext/ssl/sasl_plaintext/sasl_ssl)",
418 "plaintext",
419 ),
420 ConfigKeySpec::optional(
421 "sasl.mechanism",
422 "SASL mechanism (PLAIN/SCRAM-SHA-256/SCRAM-SHA-512/GSSAPI/OAUTHBEARER)",
423 "",
424 ),
425 ConfigKeySpec::optional("sasl.username", "SASL username for PLAIN/SCRAM", ""),
426 ConfigKeySpec::optional("sasl.password", "SASL password for PLAIN/SCRAM", ""),
427 ConfigKeySpec::optional("ssl.ca.location", "SSL CA certificate file path", ""),
428 ConfigKeySpec::optional(
429 "ssl.certificate.location",
430 "Client SSL certificate file path",
431 "",
432 ),
433 ConfigKeySpec::optional("ssl.key.location", "Client SSL private key file path", ""),
434 ConfigKeySpec::optional("ssl.key.password", "Password for encrypted SSL key", ""),
435 ConfigKeySpec::optional(
436 "max.in.flight.requests",
437 "Maximum in-flight producer requests per connection (1-5)",
438 "5",
439 ),
440 ConfigKeySpec::optional(
441 "delivery.timeout.ms",
442 "Delivery timeout in milliseconds",
443 "120000",
444 ),
445 ConfigKeySpec::optional(
447 "key.column",
448 "Key column, or comma-separated columns with key.format = json",
449 "",
450 ),
451 ConfigKeySpec::optional(
452 "key.format",
453 "Key bytes (native/kafka/text/json)",
454 "native",
455 ),
456 ConfigKeySpec::optional(
457 "partition.count",
458 "Partition count keyed records hash over; required for envelope = upsert",
459 "",
460 ),
461 ConfigKeySpec::optional("envelope", "Output envelope (append/upsert)", "append"),
462 ConfigKeySpec::optional(
463 "commit.topic",
464 "Compacted topic holding exactly-once commit markers (exactly-once only)",
465 "",
466 ),
467 ConfigKeySpec::optional(
468 "transaction.timeout.ms",
469 "Exactly-once transaction timeout in milliseconds",
470 "60000",
471 ),
472 ConfigKeySpec::optional(
473 "epoch.max.bytes",
474 "Exactly-once bound on one epoch's staged output bytes",
475 "67108864",
476 ),
477 ConfigKeySpec::optional(
478 "partitioner",
479 "Partitioning strategy (key-hash/round-robin/sticky)",
480 "key-hash",
481 ),
482 ConfigKeySpec::optional(
484 "latency.profile",
485 "Producer batching preset (low_latency/balanced/throughput); explicit options override it",
486 "balanced",
487 ),
488 ConfigKeySpec::optional("linger.ms", "Producer linger time in milliseconds", "5"),
489 ConfigKeySpec::optional("batch.size", "Producer batch size in bytes", "16384"),
490 ConfigKeySpec::optional("batch.num.messages", "Max messages per batch", "10000"),
491 ConfigKeySpec::optional(
492 "compression.type",
493 "Compression (none/gzip/snappy/lz4/zstd)",
494 "none",
495 ),
496 ConfigKeySpec::optional(
498 "dlq.topic",
499 "Dead letter queue topic for failed records",
500 "",
501 ),
502 ConfigKeySpec::optional(
504 "schema.registry.url",
505 "Confluent Schema Registry URL (required for Avro)",
506 "",
507 ),
508 ConfigKeySpec::optional("schema.registry.username", "Schema Registry username", ""),
509 ConfigKeySpec::optional("schema.registry.password", "Schema Registry password", ""),
510 ConfigKeySpec::optional(
511 "schema.registry.ssl.ca.location",
512 "Schema Registry SSL CA cert path",
513 "",
514 ),
515 ConfigKeySpec::optional(
516 "schema.compatibility",
517 "Schema compatibility level override",
518 "",
519 ),
520 ]
521}
522
523#[cfg(test)]
524mod tests {
525 use super::*;
526
527 #[test]
528 fn test_register_kafka_source() {
529 let registry = ConnectorRegistry::new();
530 register_kafka_source(®istry).unwrap();
531
532 let sources = registry.list_sources();
533 assert!(sources.contains(&"kafka".to_string()));
534
535 let info = registry.source_info("kafka");
536 assert!(info.is_some());
537 let info = info.unwrap();
538 assert_eq!(info.name, "kafka");
539 assert!(info.is_source);
540 assert!(!info.is_sink);
541 let required: Vec<&str> = info
542 .config_keys
543 .iter()
544 .filter(|key| key.required)
545 .map(|key| key.key.as_str())
546 .collect();
547 assert!(required.contains(&"bootstrap.servers"));
548 assert!(required.contains(&"group.id"));
549 assert!(!required.contains(&"topic"));
550 assert!(!required.contains(&"topic.pattern"));
551 assert!(info.config_keys.iter().any(|key| key.key == "topic"));
552 assert!(info
553 .config_keys
554 .iter()
555 .any(|key| key.key == "topic.pattern"));
556 for supported in [
557 "schema.registry.subject.name.strategy",
558 "schema.registry.record.name",
559 "schema.registry.discovery.timeout.ms",
560 "max.poll.interval.ms",
561 "broker.commit.on.checkpoint",
562 "reader.channel.capacity",
563 ] {
564 assert!(
565 info.config_keys.iter().any(|key| key.key == supported),
566 "registered Kafka source descriptor omits {supported}"
567 );
568 }
569 }
570
571 #[test]
572 fn test_factory_creates_source() {
573 let registry = ConnectorRegistry::new();
574 register_kafka_source(®istry).unwrap();
575
576 let config = crate::config::ConnectorConfig::new("kafka");
577 let source = registry.create_source(&config, None);
578 assert!(source.is_ok());
579 }
580
581 #[test]
582 fn test_register_kafka_sink() {
583 let registry = ConnectorRegistry::new();
584 register_kafka_sink(®istry).unwrap();
585
586 let sinks = registry.list_sinks();
587 assert!(sinks.contains(&"kafka".to_string()));
588
589 let info = registry.sink_info("kafka");
590 assert!(info.is_some());
591 let info = info.unwrap();
592 assert_eq!(info.name, "kafka");
593 assert!(!info.is_source);
594 assert!(info.is_sink);
595 assert!(info.config_keys.iter().any(|key| key.key == "envelope"));
596 assert!(info.config_keys.iter().any(|key| key.key == "key.column"));
597 assert!(!info
598 .config_keys
599 .iter()
600 .any(|key| key.key == "delivery.guarantee"));
601 }
602
603 #[test]
604 fn test_factory_creates_sink() {
605 let registry = ConnectorRegistry::new();
606 register_kafka_sink(®istry).unwrap();
607
608 let config = crate::config::ConnectorConfig::new("kafka");
609 let sink = registry.create_sink(&config, None);
610 assert!(sink.is_ok());
611 }
612}