1use std::fmt;
8use thiserror::Error;
9
10pub type Result<T> = std::result::Result<T, KafkaError>;
12
13#[derive(Error, Debug)]
40pub enum KafkaError {
41 #[error("Failed to connect to Kafka broker {broker}: {message}")]
43 Connection {
44 message: String,
46 broker: String,
48 },
49
50 #[error("Failed to consume message from topic {topic}: {message}")]
52 Consumption {
53 message: String,
55 topic: String,
57 partition: Option<i32>,
59 offset: Option<i64>,
61 },
62
63 #[error("Failed to produce message to topic {topic}: {message}")]
65 Production {
66 message: String,
68 topic: String,
70 partition: Option<i32>,
72 },
73
74 #[error("Serialization error: {message}")]
76 Serialization {
77 message: String,
79 context: Option<String>,
81 },
82
83 #[error("Deserialization error: {message}")]
85 Deserialization {
86 message: String,
88 context: Option<String>,
90 payload_sample: Option<Vec<u8>>,
92 },
93
94 #[error("Invalid configuration: {message}")]
96 InvalidConfiguration {
97 message: String,
99 field: Option<String>,
101 },
102
103 #[error("Failed to commit offsets for topic {topic}: {message}")]
105 OffsetCommit {
106 message: String,
108 topic: String,
110 partition: i32,
112 offset: i64,
114 },
115
116 #[error("Consumer group coordination error for group {group_id}: {message}")]
118 GroupCoordination {
119 message: String,
121 group_id: String,
123 },
124
125 #[error("Partition assignment error: {message}")]
127 PartitionAssignment {
128 message: String,
130 topics: Vec<String>,
132 },
133
134 #[error("Authentication/Authorization error: {message}")]
136 AuthError {
137 message: String,
139 protocol: String,
141 },
142
143 #[error("Network I/O error: {message}")]
145 IoError {
146 message: String,
148 #[source]
150 source: Option<std::io::Error>,
151 },
152
153 #[error("Operation timed out after {timeout_ms}ms: {operation}")]
155 Timeout {
156 operation: String,
158 timeout_ms: u64,
160 },
161
162 #[error("Topic metadata error for topic {topic}: {message}")]
164 TopicMetadata {
165 message: String,
167 topic: String,
169 },
170
171 #[error("Topic partition {topic}:{partition} does not exist")]
173 PartitionNotFound {
174 topic: String,
176 partition: i32,
178 },
179
180 #[error("Resource limit exceeded: {message}")]
182 ResourceExhausted {
183 message: String,
185 resource: String,
187 },
188
189 #[error("Kafka client error: {message} (error code: {code:?})")]
191 KafkaClient {
192 message: String,
194 code: Option<i32>,
196 },
197
198 #[error("Kafka error: {0}")]
200 Other(String),
201}
202
203impl KafkaError {
204 pub fn connection(message: impl Into<String>, broker: impl Into<String>) -> Self {
206 Self::Connection {
207 message: message.into(),
208 broker: broker.into(),
209 }
210 }
211
212 pub fn consumption(
214 message: impl Into<String>,
215 topic: impl Into<String>,
216 partition: Option<i32>,
217 offset: Option<i64>,
218 ) -> Self {
219 Self::Consumption {
220 message: message.into(),
221 topic: topic.into(),
222 partition,
223 offset,
224 }
225 }
226
227 pub fn production(
229 message: impl Into<String>,
230 topic: impl Into<String>,
231 partition: Option<i32>,
232 ) -> Self {
233 Self::Production {
234 message: message.into(),
235 topic: topic.into(),
236 partition,
237 }
238 }
239
240 pub fn serialization(message: impl Into<String>, context: Option<String>) -> Self {
242 Self::Serialization {
243 message: message.into(),
244 context,
245 }
246 }
247
248 pub fn deserialization(
250 message: impl Into<String>,
251 context: Option<String>,
252 payload_sample: Option<Vec<u8>>,
253 ) -> Self {
254 Self::Deserialization {
255 message: message.into(),
256 context,
257 payload_sample,
258 }
259 }
260
261 pub fn invalid_config(message: impl Into<String>, field: Option<String>) -> Self {
263 Self::InvalidConfiguration {
264 message: message.into(),
265 field,
266 }
267 }
268
269 pub fn offset_commit(
271 message: impl Into<String>,
272 topic: impl Into<String>,
273 partition: i32,
274 offset: i64,
275 ) -> Self {
276 Self::OffsetCommit {
277 message: message.into(),
278 topic: topic.into(),
279 partition,
280 offset,
281 }
282 }
283
284 pub fn group_coordination(message: impl Into<String>, group_id: impl Into<String>) -> Self {
286 Self::GroupCoordination {
287 message: message.into(),
288 group_id: group_id.into(),
289 }
290 }
291
292 pub fn partition_assignment(message: impl Into<String>, topics: Vec<String>) -> Self {
294 Self::PartitionAssignment {
295 message: message.into(),
296 topics,
297 }
298 }
299
300 pub fn auth_error(message: impl Into<String>, protocol: impl Into<String>) -> Self {
302 Self::AuthError {
303 message: message.into(),
304 protocol: protocol.into(),
305 }
306 }
307
308 pub fn timeout(operation: impl Into<String>, timeout_ms: u64) -> Self {
310 Self::Timeout {
311 operation: operation.into(),
312 timeout_ms,
313 }
314 }
315
316 pub fn topic_metadata(message: impl Into<String>, topic: impl Into<String>) -> Self {
318 Self::TopicMetadata {
319 message: message.into(),
320 topic: topic.into(),
321 }
322 }
323
324 pub fn partition_not_found(topic: impl Into<String>, partition: i32) -> Self {
326 Self::PartitionNotFound {
327 topic: topic.into(),
328 partition,
329 }
330 }
331
332 pub fn resource_exhausted(message: impl Into<String>, resource: impl Into<String>) -> Self {
334 Self::ResourceExhausted {
335 message: message.into(),
336 resource: resource.into(),
337 }
338 }
339
340 pub fn kafka_client(message: impl Into<String>, code: Option<i32>) -> Self {
342 Self::KafkaClient {
343 message: message.into(),
344 code,
345 }
346 }
347
348 pub fn is_retryable(&self) -> bool {
352 matches!(
353 self,
354 KafkaError::Timeout { .. }
355 | KafkaError::IoError { .. }
356 | KafkaError::Connection { .. }
357 | KafkaError::GroupCoordination { .. }
358 | KafkaError::ResourceExhausted { .. }
359 )
360 }
361
362 pub fn is_fatal(&self) -> bool {
366 matches!(
367 self,
368 KafkaError::AuthError { .. } | KafkaError::InvalidConfiguration { .. }
369 )
370 }
371
372 pub fn severity(&self) -> ErrorSeverity {
376 match self {
377 KafkaError::InvalidConfiguration { .. } | KafkaError::AuthError { .. } => {
378 ErrorSeverity::Critical
379 }
380 KafkaError::Connection { .. }
381 | KafkaError::GroupCoordination { .. }
382 | KafkaError::Production { .. } => ErrorSeverity::Error,
383 KafkaError::Timeout { .. }
384 | KafkaError::ResourceExhausted { .. }
385 | KafkaError::Consumption { .. } => ErrorSeverity::Warning,
386 _ => ErrorSeverity::Info,
387 }
388 }
389}
390
391#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
393pub enum ErrorSeverity {
394 Info,
396 Warning,
398 Error,
400 Critical,
402}
403
404impl fmt::Display for ErrorSeverity {
405 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
406 match self {
407 ErrorSeverity::Info => write!(f, "INFO"),
408 ErrorSeverity::Warning => write!(f, "WARNING"),
409 ErrorSeverity::Error => write!(f, "ERROR"),
410 ErrorSeverity::Critical => write!(f, "CRITICAL"),
411 }
412 }
413}
414
415impl From<std::io::Error> for KafkaError {
418 fn from(err: std::io::Error) -> Self {
419 KafkaError::IoError {
420 message: err.to_string(),
421 source: Some(err),
422 }
423 }
424}
425
426impl From<serde_json::Error> for KafkaError {
427 fn from(err: serde_json::Error) -> Self {
428 KafkaError::Serialization {
429 message: err.to_string(),
430 context: Some("JSON serialization".to_string()),
431 }
432 }
433}
434
435impl From<bincode::Error> for KafkaError {
436 fn from(err: bincode::Error) -> Self {
437 KafkaError::Serialization {
438 message: err.to_string(),
439 context: Some("Bincode serialization".to_string()),
440 }
441 }
442}
443
444impl From<rdkafka::error::KafkaError> for KafkaError {
446 fn from(err: rdkafka::error::KafkaError) -> Self {
447 use rdkafka::error::KafkaError as RdKafkaError;
448
449 match err {
450 RdKafkaError::ClientCreation(msg) => KafkaError::connection(msg, "unknown"),
452
453 RdKafkaError::MetadataFetch(err) => {
455 KafkaError::topic_metadata(err.to_string(), "unknown")
456 }
457
458 RdKafkaError::Subscription(msg) => {
460 KafkaError::partition_assignment(msg, Vec::new())
461 }
462
463 RdKafkaError::GroupListFetch(err) => {
465 KafkaError::group_coordination(err.to_string(), "unknown")
466 }
467
468 RdKafkaError::MessageProduction(code) => KafkaError::production(
470 format!("Message production failed with code: {:?}", code),
471 "unknown",
472 None,
473 ),
474
475 RdKafkaError::MessageConsumption(code) => KafkaError::consumption(
477 format!("Message consumption failed with code: {:?}", code),
478 "unknown",
479 None,
480 None,
481 ),
482
483 RdKafkaError::StoreOffset(err) | RdKafkaError::OffsetFetch(err) => {
485 KafkaError::offset_commit(err.to_string(), "unknown", -1, -1)
486 }
487
488 RdKafkaError::PartitionEOF(partition) => KafkaError::consumption(
490 format!("Reached end of partition {}", partition),
491 "unknown",
492 Some(partition),
493 None,
494 ),
495
496 _ => KafkaError::kafka_client(err.to_string(), None),
498 }
499 }
500}
501
502#[cfg(test)]
503mod tests {
504 use super::*;
505
506 #[test]
507 fn test_error_creation() {
508 let err = KafkaError::connection("Connection refused", "localhost:9092");
509 assert!(matches!(err, KafkaError::Connection { .. }));
510 }
511
512 #[test]
513 fn test_error_is_retryable() {
514 let timeout_err = KafkaError::timeout("poll", 1000);
515 assert!(timeout_err.is_retryable());
516
517 let auth_err = KafkaError::auth_error("Invalid credentials", "SASL_SSL");
518 assert!(!auth_err.is_retryable());
519 }
520
521 #[test]
522 fn test_error_is_fatal() {
523 let auth_err = KafkaError::auth_error("Invalid credentials", "SASL_SSL");
524 assert!(auth_err.is_fatal());
525
526 let timeout_err = KafkaError::timeout("poll", 1000);
527 assert!(!timeout_err.is_fatal());
528 }
529
530 #[test]
531 fn test_error_severity() {
532 let critical = KafkaError::invalid_config("Missing bootstrap servers", None);
533 assert_eq!(critical.severity(), ErrorSeverity::Critical);
534
535 let error = KafkaError::connection("Connection refused", "localhost:9092");
536 assert_eq!(error.severity(), ErrorSeverity::Error);
537
538 let warning = KafkaError::timeout("poll", 1000);
539 assert_eq!(warning.severity(), ErrorSeverity::Warning);
540 }
541
542 #[test]
543 fn test_error_display() {
544 let err = KafkaError::connection("Connection refused", "localhost:9092");
545 let display = format!("{}", err);
546 assert!(display.contains("localhost:9092"));
547 assert!(display.contains("Connection refused"));
548 }
549
550 #[test]
551 fn test_io_error_conversion() {
552 let io_err = std::io::Error::new(std::io::ErrorKind::ConnectionRefused, "test error");
553 let kafka_err: KafkaError = io_err.into();
554 assert!(matches!(kafka_err, KafkaError::IoError { .. }));
555 }
556
557 #[test]
558 fn test_serde_error_conversion() {
559 let json_err = serde_json::from_str::<serde_json::Value>("invalid json").unwrap_err();
560 let kafka_err: KafkaError = json_err.into();
561 assert!(matches!(kafka_err, KafkaError::Serialization { .. }));
562 }
563}